All posts
Automation·May 6, 2026·12 min

How I Built a TradingView-to-Binance Futures Bridge That Won't Blow Up Your Account

A build log for a TradingView-to-Binance Futures webhook bridge with real risk rails: dry-run mode, notional caps, kill switch, idempotency and reconciliation.

Infrastructure, not financial advice. This article is about the engineering of an execution bridge — order routing, safety limits, and failure handling. Nothing here is a strategy recommendation, a signal, or a suggestion to trade leveraged products. Crypto perpetual futures can liquidate your entire position in seconds. Build the rails first; decide whether to trade at all separately.

Most "TradingView to Binance" tutorials stop at the fun part: a webhook fires, you slap the payload into an API call, an order appears, you post the screenshot. Then a duplicate alert double-fires at 3 a.m., or a decimal is off by one, or the exchange rejects a reduce-only order and your bot happily opens a fresh position on top of the one it was supposed to close. On spot that's an annoyance. On futures with leverage, that's how accounts get liquidated.

I built a bridge that assumes all of those things will happen, because over enough alerts they do. This is the build log — the architecture, the risk rails, and the specific failure modes I designed against. The point of the piece is not "look, it places orders." Any script can place an order. The point is everything that sits between the alert and the exchange to make sure a bad signal, a network blip, or my own typo can't cascade into a wipeout.

The threat model: what actually blows up accounts

Before writing a line of execution code, I wrote down the ways an automated futures bot loses money that have nothing to do with the strategy being wrong:

  • Duplicate fills. TradingView can send the same alert more than once (retries, chart reloads, multiple alert copies). Without deduplication, one signal becomes two, three, five positions.
  • Runaway sizing. A bug that computes quantity from equity can, on a bad read, size an order at 10x the intended notional. Leverage multiplies the damage.
  • Stale/replayed signals. A webhook received 40 seconds late — because your VPS was swapping or the exchange was slow — executes a signal the market has already moved past.
  • Reduce-only that isn't. You mean "close the long." The API call, misconfigured, opens a short. Now you're inverted, not flat.
  • Silent divergence. The bot thinks it's flat; the exchange says you're holding a position from a partially-filled order last night. Every subsequent decision is made on a fantasy state.
  • No off switch. Something is clearly wrong and there's no single, fast, reliable way to make the machine stop trading right now.

Every rail below maps to one of those. If a safety feature didn't map to a concrete failure, I didn't build it.

Architecture: three stages, one direction

The bridge is deliberately boring. Three stages, data flows one way, each stage can refuse to pass the signal along.

TradingView alert
      │  (HTTPS POST, HMAC-signed body)
      ▼
[1] Receiver  ── validate signature, parse, dedupe, timestamp check
      │
      ▼
[2] Risk gate ── notional cap, position cap, kill switch, dry-run
      │
      ▼
[3] Executor  ── reduce-only aware, idempotent client order id
      │
      ▼
Binance Futures (USD-M)  → reconcile actual fills back into state

Stage 1 never trusts the payload. Stage 2 never trusts the strategy. Stage 3 never trusts that the previous order did what it claimed. That distrust is the whole design.

Stage 1: the receiver refuses to trust the wire

The receiver is a small Flask app. Its only job is to decide whether a request is a real, fresh, well-formed signal — and reject everything else before it reaches anything that can spend money.

Three checks, in order:

  1. HMAC signature. TradingView's webhook body is signed with a shared secret; I verify it with a constant-time compare. An unsigned or wrongly-signed request is dropped with a 401 and logged. This stops random internet scanners (and a leaked webhook URL) from moving my positions.
  2. Freshness. The payload carries a timestamp. If it's older than a few seconds, I reject it. A signal I can't act on immediately is a signal I don't act on at all.
  3. Idempotency key. Every alert carries a unique signalId. I keep a short-lived set of seen IDs; a repeat is acknowledged with 200 (so TradingView stops retrying) but does not execute.
import hmac, hashlib, time, json
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["WEBHOOK_SECRET"].encode()
SEEN = {}                      # signalId -> expiry ts (use Redis in prod)
MAX_AGE_SEC = 5

def verify(raw: bytes, sig: str) -> bool:
    expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig or "")

@app.post("/webhook")
def webhook():
    raw = request.get_data()
    if not verify(raw, request.headers.get("X-Signature")):
        abort(401)
    data = json.loads(raw)

    # freshness
    if time.time() - data["ts"] > MAX_AGE_SEC:
        return "stale", 200          # ack so TV stops retrying, but do nothing

    # idempotency
    sid = data["signalId"]
    now = time.time()
    if sid in SEEN and SEEN[sid] > now:
        return "duplicate", 200
    SEEN[sid] = now + 300            # remember for 5 minutes

    return risk_gate(data)

Note what the receiver deliberately does not do: it does not call the exchange. It cannot. It hands a validated intent to the risk gate and nothing else.

Stage 2: the risk gate is the whole point

This is the stage I spent the most time on and the one every tutorial skips. The risk gate takes a validated intent and decides whether it is allowed, independent of whether the strategy thinks it's a good idea. It has veto power and no opinion about markets.

Kill switch. A single flag — a file on disk, a Redis key, a config value — that, when set, rejects everything. No exceptions, no override. When something looks wrong at 2 a.m., I don't want to be reasoning about individual orders; I want one switch that means stop. It's checked first because a kill switch you evaluate after other logic is a kill switch that can be bypassed by a bug in that logic.

Notional cap. Every order's notional value (price × quantity × leverage exposure) is computed and hard-capped. If a sizing bug asks for 10x the intended size, the cap clips or rejects it. This is the single most important rail: it converts "catastrophic loss" into "capped loss."

Position cap. Total open exposure across the account has a ceiling. Even a sequence of individually-fine orders can't stack into a position larger than I've decided I can stomach.

Dry-run mode. A global switch that runs the entire pipeline — validation, risk checks, quantity math, the fully-formed order object — and then logs it instead of sending it. Every new strategy runs in dry-run against live signals for days before a single real order is placed. Dry-run is not a toy; it's the default. Live is the exception you opt into.

DRY_RUN     = os.environ.get("DRY_RUN", "true") == "true"
MAX_NOTIONAL_USDT = 500          # per-order hard ceiling
MAX_OPEN_NOTIONAL = 1500         # account-wide ceiling

def risk_gate(intent):
    if kill_switch_active():
        return log_and_reject(intent, "KILL_SWITCH")

    price = mark_price(intent["symbol"])
    notional = price * intent["qty"]

    if notional > MAX_NOTIONAL_USDT:
        return log_and_reject(intent, f"OVER_NOTIONAL {notional:.0f}")

    if current_open_notional() + notional > MAX_OPEN_NOTIONAL:
        return log_and_reject(intent, "OVER_POSITION_CAP")

    order = build_order(intent, price)     # fully formed, not yet sent

    if DRY_RUN:
        log.info("DRY_RUN order (not sent): %s", order)
        return "dry-run ok", 200

    return execute(order)

The caps are intentionally small relative to account equity. A bridge you're still testing should feel almost pointless in size. You scale the caps up slowly, by hand, after the boring parts have proven stable — never before.

Stage 3: the executor assumes the exchange will surprise it

The executor's guiding assumption is that any order can partially fill, get rejected, time out, or succeed while the acknowledgement is lost in transit. So it's built around two ideas.

Idempotent client order IDs. Every order carries a deterministic newClientOrderId derived from the signalId. If a network timeout makes me unsure whether an order landed, I can safely retry: the exchange rejects a duplicate client order ID rather than placing a second order. Retries become safe instead of dangerous.

Reduce-only awareness. Closing orders are always sent with reduceOnly=true. If, because of a state mismatch, there's nothing to reduce, the exchange rejects it — which is exactly what I want. A "close" instruction can never silently open a new position. That one flag eliminates the inverted-position failure mode entirely.

def build_order(intent, price):
    reduce_only = intent["action"] in ("close_long", "close_short")
    side = "SELL" if intent["action"] in ("open_short", "close_long") else "BUY"
    return {
        "symbol": intent["symbol"],
        "side": side,
        "type": "MARKET",
        "quantity": round_step(intent["qty"], intent["symbol"]),
        "reduceOnly": reduce_only,
        "newClientOrderId": f"tv-{intent['signalId']}",   # idempotent
    }

Reconciliation: the state you believe vs. the state that's real

The most dangerous bug in an execution bot is silent divergence: the bot's internal idea of your position drifts from the exchange's reality, and every later decision compounds the error. So after every order — and on a timer regardless — the bridge pulls actual positions and open orders from the exchange and reconciles them against its own state. If they disagree, it does not "fix" anything automatically; it trips the kill switch and alerts me. A bot that's confused about its own position should stop, not improvise.

What I deliberately left out

  • No martingale, no averaging down, no "recovery" logic. Any feature whose job is to increase size after a loss is a feature designed to blow up an account eventually. The bridge cannot do it because I never gave it the ability.
  • No hidden leverage changes. Leverage is set once, explicitly, out of band. The bot never touches it.
  • No auto-scaling of caps. The risk caps only change when I edit the config by hand. Nothing about performance, streaks, or balance moves them.

The honest takeaway

The bridge is maybe 400 lines. Fewer than 40 of them place orders. The other 90% is the receiver, the risk gate, idempotency, reconciliation, logging, and the kill switch — the parts that make the 40 lines safe to run unattended. That ratio is the whole lesson. If you're building execution automation for leveraged products, the interesting engineering is never the order call; it's the machinery that ensures a bad signal or a bad night can only cost you a capped, survivable amount.

And to restate the note at the top, because it's the most important line here: this is infrastructure, not financial advice. Good rails make a bot safe to run. They do not make a strategy worth running. Those are two separate decisions, and only one of them is an engineering problem.

Building something in this space? I ship trading automation, Pine Script, MQL5 EAs, and full-stack tools with source and docs.

Start a project →