Disclosure: I work on HookWatch, a hosted TradingView webhook reliability layer. Everything below works without it, and the code is MIT on GitHub.
A lot of people are wiring LLM agents to TradingView right now: an alert fires, the agent looks at the signal plus context (position, news, PnL), decides, and places an order. The first version usually looks like this:
@app.post("/webhook")
async def webhook(req: Request):
signal = await req.json()
decision = await agent.decide(signal) # 3-15 seconds
if decision.execute:
broker.place_order(...)
return {"ok": True}
It works in testing, then you start losing signals in production. Here's why, and the boring pattern that fixes it.
TradingView gives you about 3 seconds
From TradingView's own docs: if the receiving server takes longer than about 3 seconds, the request is cancelled. Also: ports 80/443 only, no IPv6, no custom headers, and the body is text/plain unless your alert message is valid JSON.
An LLM call alone usually takes longer than 3 seconds. So the handler above times out, TradingView treats the delivery as failed, and depending on where the code was when the connection dropped, your agent may or may not have acted. You get no retry you can rely on and no log on your side.
Split the handler from the agent
The webhook handler should do four cheap things and return:
- Verify a secret carried in the JSON body (TradingView can't send auth headers).
-
De-duplicate on an
alert_idyou build in the alert message, e.g.{{ticker}}-{{interval}}-{{timenow}}. - Timestamp and enqueue the signal.
- Return 200 within milliseconds.
The agent runs in a separate consumer:
@app.post("/webhook")
async def webhook(request: Request):
signal = json.loads(await request.body())
if not hmac.compare_digest(str(signal.pop("secret", "")), SECRET):
return Response("forbidden", status_code=403)
alert_id = signal.get("alert_id")
if not alert_id or alert_id in seen:
return Response("ok", status_code=200) # duplicates get 200 too
seen[alert_id] = time.time()
signal["_received_at"] = time.time()
try:
signals.put_nowait(signal)
except asyncio.QueueFull:
return Response("busy", status_code=503)
return Response("queued", status_code=200)
async def consumer():
while True:
signal = await signals.get()
decision = await agent_decide(signal) # slow is fine here
if risk_gate(signal, decision):
place_order(signal, decision)
Full runnable versions (FastAPI, Express, and the agent one): github.com/hookwatch-ops/tradingview-webhook-examples
Put a deterministic risk gate after the agent
The agent proposes; plain code decides whether anything reaches the broker. At minimum:
- Max signal age. If the queue backed up and a signal is 2 minutes old, a market order on it is probably wrong. Drop it.
- Position and size limits the model can't talk its way around.
- Structured output only. Parse the agent's answer into a fixed schema; anything else is a skip.
The part a fast handler doesn't fix: downtime
Fast ACK solves the 3-second problem. It doesn't help if your process is restarting, your VPS reboots, or you're deploying a new agent prompt at the moment the alert fires. TradingView doesn't queue alerts for you.
The fix is to make ingress separate from the bot:
- a tiny always-on receiver (or serverless function) that only writes to a durable queue: Redis Streams, SQS, Postgres
- durable dedupe: Redis
SET alert_id 1 NX EX 86400or a unique index - retries with backoff from the queue to the agent, and a dead-letter queue for signals that keep failing, so you can see what was missed
- an alert to yourself when no signals arrive during hours you expect them
Then your agent can crash, restart and redeploy without signals vanishing.
If you don't want to run that part
That ingress layer is what HookWatch does as a service: you point the TradingView alert at your HookWatch URL, it answers TradingView right away, queues the alert, delivers it to your receiver, retries failed deliveries (5 automatic retries over about 2 minutes) and keeps anything that still fails in a dead-letter queue. It's not signals and not a broker, and it doesn't make alerts faster; it just stops them from disappearing silently. There's a free trial (300 alerts or 7 days, no card) if you want to try it in front of your agent.
Either way, keep the secret check and the idempotency in your own receiver.
Checklist
- [ ] Handler returns 200 in well under 3 s, never calls the LLM
- [ ] Secret in the JSON body, constant-time compare
- [ ]
alert_idin every alert message, de-duplicated durably - [ ] Agent runs from a queue, with a max-signal-age check
- [ ] Deterministic risk gate between agent and broker
- [ ] Ingress survives bot restarts (your own queue or a hosted layer)
- [ ] You look at failures (DLQ / logs), not just successes
Not financial advice. Paper-trade any agent before giving it real money.
Top comments (0)