DEV Community

HookWatch
HookWatch

Posted on

Connecting an AI trading agent to TradingView alerts without losing signals

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}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Verify a secret carried in the JSON body (TradingView can't send auth headers).
  2. De-duplicate on an alert_id you build in the alert message, e.g. {{ticker}}-{{interval}}-{{timenow}}.
  3. Timestamp and enqueue the signal.
  4. 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)
Enter fullscreen mode Exit fullscreen mode

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 86400 or 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_id in 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)