Lesson 5 — Webhooks & Pine alerts
Build a secure webhook receiver for TradingView Pine alerts, validate HMAC signatures, map alert symbols to exchange testnet markets, and forward idempotent order instructions to the trading layer.
Prerequisites
- Lessons 1–4 completed (env, HMAC auth, market data, order placement)
- A public HTTPS webhook endpoint (ngrok for local testing)
- TradingView plan that supports webhooks (alerts with webhook URL)
1) TradingView Pine alert JSON template
Use a structured JSON body from your Pine script so the webhook can parse it reliably. Include a client idempotency token (client_ref) to avoid duplicate trades.
{
"symbol": "BTCUSDT", // use mapping compatible with your exchange lookup layer
"exchange": "binance",
"strategy": "rsi-break",
"signal": "BUY",
"side": "BUY",
"price": 42000.0,
"size": 0.001,
"client_ref": "tv_{{timenow}}_{{strategy.order_id}}"
}
Tip: In Pine use alert() with a JSON string then set TradingView webhook URL. Keep payload small and predictable. Use a template for client_ref that is unique per alert.
2) Flask webhook receiver (webhook.py)
This Flask example verifies an HMAC-SHA256 signature in the X-Signature header, validates payload fields, enqueues the order request, and returns a 200 → enqueue, don’t block. Replace mapping/queue with your infra (Redis, RabbitMQ, SQS).
from flask import Flask, request, jsonify, abort
import hmac, hashlib, os, json
from dotenv import load_dotenv
from queue import SimpleQueue
from mapping import map_symbol_to_market # implement in your codebase
from orders import enqueue_order # small wrapper to push to worker queue
load_dotenv()
WEBHOOK_SECRET = os.getenv('WEBHOOK_SECRET', 'replace-me')
app = Flask(__name__)
work_queue = SimpleQueue() # replace with Redis/Celery in production
def verify_hmac(raw_body: bytes, signature: str) -> bool:
if not signature:
return False
mac = hmac.new(WEBHOOK_SECRET.encode('utf-8'), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, signature)
@app.route('/webhook', methods=['POST'])
def webhook():
raw = request.get_data()
sig = request.headers.get('X-Signature') or request.headers.get('X-Signature-256')
if not verify_hmac(raw, sig):
abort(400, 'Invalid signature')
try:
payload = request.get_json(force=True)
except Exception:
abort(400, 'Invalid JSON')
required = ['symbol','side','client_ref','size']
if not all(k in payload for k in required):
return jsonify({'success': False, 'error': 'missing fields'}), 400
# Map TradingView symbol to exchange market
market_info = map_symbol_to_market(payload.get('symbol'), payload.get('exchange'))
if not market_info:
return jsonify({'success': False, 'error': 'market not found'}), 404
# Build idempotent order instruction
order_instruction = {
'client_ref': payload['client_ref'],
'exchange': payload.get('exchange'),
'symbol': market_info['symbol'],
'side': payload['side'],
'price': payload.get('price'),
'size': payload['size'],
'meta': {'raw_payload': payload}
}
# Enqueue for worker to place order (fast response to TradingView)
try:
enqueue_order(order_instruction) # push to queue (Redis/Celery/RQ) - implement this
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
return jsonify({'success': True}), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Important: Do not place orders synchronously in the HTTP request handler. Enqueue and let a worker process orders to avoid timeouts and to retry safely.
3) Mapping TradingView symbol → exchange market
Implement a conservative mapping layer (mapping.py) that resolves TradingView symbols to exchange-specific symbols or market IDs. Prefer a manual mapping table for high-risk markets.
# mapping.py (simple example)
# maintain a small dictionary for critical symbols
SYMBOL_MAP = {
('BTCUSDT','binance'): {'symbol': 'BTC/USDT'},
('ETHUSDT','binance'): {'symbol': 'ETH/USDT'}
}
def map_symbol_to_market(tv_symbol, exchange='binance'):
# try exact lookup then fallback to a lightweight search via exchange API
key = (tv_symbol.upper(), exchange.lower())
if key in SYMBOL_MAP:
return SYMBOL_MAP[key]
# fallback: call exchange REST search (carefully)
# from auth_hmac import public_get
# results = public_get('/markets', params={'q': tv_symbol})
# pick nearest result after heuristics
return None
Safety: prefer human-reviewed mapping for real-money trading. If automapping, add a confirmation step in staging and extensive logging.
4) Worker: validate idempotency & place order (worker.py)
Worker consumes queue messages and calls the order placement function. It must check idempotency (client_ref) and persist outcomes.
from orders import place_order_idempotent, log_order, find_order_by_client_ref
def worker_process(instruction):
# instruction: dict built in webhook
# 1) check idempotency
existing = find_order_by_client_ref(instruction['client_ref'])
if existing:
# already processed, skip or update status
return existing
# 2) pre-trade checks (size limits, daily PnL, hours)
# run pre_trade_checks(...) — implement as needed
# 3) place order via orders layer (handles retries)
resp = place_order_idempotent(
exchange=instruction['exchange'],
symbol=instruction['symbol'],
side=instruction['side'],
size=instruction['size'],
price=instruction.get('price'),
client_ref=instruction['client_ref']
)
# 4) persist result
log_order({'client_ref': instruction['client_ref'], 'response': resp})
return resp
Idempotency pattern: store client_ref before performing the external call or use transactional DB operations so duplicates are prevented under retries.
5) Pine → Webhook security & best practices
- TradingView cannot sign requests with HMAC. Use a shared secret: compute HMAC on the server side only if you control the alert sender. Alternatively, place the secret inside a lightweight relay you control that signs requests onward.
- Use a relay service or small script that receives TradingView alerts and attaches an HMAC using your secret before forwarding to your production webhook. This prevents exposing the secret in TradingView.
- Require client_ref and reject duplicate processing if client_ref already used.
- Rate-limit inbound alerts and queue them; reject or throttle bursts.
# relay example (pseudo)
# TradingView POST -> Relay (no secret) -> Relay computes HMAC using WEBHOOK_SECRET and forwards to your /webhook
# Relay should run where you control the environment and secret. Do NOT store your production signing secret in TradingView.
6) Testing and local debugging
- Use ngrok to provide a public HTTPS URL for local webhook testing: ngrok http 5000
- Manually compute HMAC for test requests and send with curl or Postman to simulate TradingView
- Start with sandbox testnets and tiny sizes for end-to-end validation
# example curl test (compute sig in Python, then curl)
curl -X POST https://your-ngrok-url/webhook -H "Content-Type: application/json" -H "X-Signature: {sig}" --data '{"symbol":"BTCUSDT","side":"BUY","size":0.001,"client_ref":"test-1"}'
7) Reliability & operational patterns
- Decouple webhook ingestion from execution via durable queue to allow retries and visibility.
- Implement worker retries with exponential backoff and idempotency tokens to avoid duplicate orders.
- Log raw alerts, mapping decisions, order requests and order responses for auditing and dispute resolution.
- Monitor webhook error rates and signature failures to detect suspicious traffic or misconfiguration.
What you’ll build next
Lesson 6 will cover backtesting & simulation so you can replay TradingView signals against historical market data to measure strategy performance before using real money.
