DEV Community

Fxm Brand
Fxm Brand

Posted on

I Turned a TradingView Indicator Into a Fully Autonomous Trading Bot — Here's the Webhook Bridge That Makes It Work

A Pine Script indicator can plot a perfect signal on a chart and still be operationally useless, because "visible on a chart" and "actionable by a program" are two completely different problems. This post covers the actual bridge — TradingView alert payloads, a webhook receiver, payload validation, and a dispatch layer to the broker — that turns the Goldmine indicator from something you stare at into something that trades without you. Code included for every stage.


The gap nobody mentions between "the indicator works" and "the bot works"

Pine Script is genuinely good at one thing: visualizing structure on a chart. Order blocks, fair value gaps, CHoCH/BOS labels — all of it renders cleanly and updates in real time. What Pine Script is not is a place you can run arbitrary logic, hit a broker API, or maintain state across restarts. It runs inside TradingView's sandbox, full stop.

So the moment you want a visual indicator to become an acting system — placing real orders, not just drawing boxes — you hit a hard boundary: TradingView's only way out of that sandbox is alert() and a webhook payload. Everything past that point (receiving the alert, validating it, deciding whether to act on it, actually placing the order) is infrastructure you build yourself, outside Pine Script entirely.

This post is that infrastructure — the part that turns the Goldmine indicator's visual signal into the Goldmine Trading Bot's executed trade.


Stage 1: Designing the alert payload inside Pine Script

The temptation is to fire an alert with a vague message string. Don't — every field the downstream system needs to make a safe decision has to be in the payload itself, because there's no way to "go back and ask the chart for more context" once the webhook fires.

//@version=5
indicator("Goldmine Signal Engine", overlay=true)

// ... structure detection logic producing `signalDetected`, `direction`, `confidence`, `invalidationLevel` ...

if signalDetected
    alertPayload = str.format(
        '{{"symbol":"{0}","direction":"{1}","confidence":{2},"entry":{3},"invalidation":{4},"timeframe":"{5}","timestamp":"{6}"}}',
        syminfo.ticker, direction, confidence, close, invalidationLevel, timeframe.period, str.tostring(time)
    )
    alert(alertPayload, alert.freq_once_per_bar_close)
Enter fullscreen mode Exit fullscreen mode

Two details here matter more than they look like they should:

  • alert.freq_once_per_bar_close, not alert.freq_all — firing on every tick instead of on bar close produces duplicate/premature signals from repainting intermediate values. This single setting eliminated a whole category of phantom signals in early testing.
  • The timestamp is generated in Pine Script, not on receipt — if your webhook receiver timestamps on arrival instead of trusting the chart's bar-close time, network latency and retry delays silently corrupt your signal-to-execution latency measurements later.

Stage 2: The webhook receiver

TradingView POSTs the payload to a public HTTPS endpoint. This is the part with the most exposed attack surface in the whole pipeline, and it's the part homegrown bots most often build carelessly.

from flask import Flask, request, abort
import hmac, hashlib, json

app = Flask(__name__)
WEBHOOK_SECRET = load_secret()  # never hardcode this

@app.route('/webhook/goldmine', methods=['POST'])
def receive_signal():
    raw_body = request.get_data()

    # TradingView doesn't sign requests natively — so validate a
    # shared secret embedded in the payload itself, not just the URL
    payload = json.loads(raw_body)
    if not hmac.compare_digest(payload.get('secret', ''), WEBHOOK_SECRET):
        abort(403)

    if not _valid_schema(payload):
        abort(400)  # malformed payload — reject, don't guess

    signal_queue.put(payload)
    return '', 200
Enter fullscreen mode Exit fullscreen mode

Why the shared secret matters: TradingView webhook URLs are, by design, public HTTPS endpoints. If your URL leaks — a screenshot, a log file, a misconfigured proxy — anyone can POST a fake signal to it. A secret embedded in the payload (not just relying on URL obscurity) is the minimum bar for a webhook that can trigger real trades.

Why validation happens before the queue, not after: A malformed payload that makes it into the execution pipeline is worse than one that gets rejected at the door. Fail loud and early here.


Stage 3: The dispatch layer — deciding whether to actually act

Receiving a valid signal is not the same as trading it. This is where the confidence threshold and risk governor from the bot's execution pipeline plug in:

def process_signal(payload):
    if payload['confidence'] < MIN_EXECUTION_THRESHOLD:
        log_skipped(payload, reason='below_threshold')
        return

    if not risk_governor.can_open_new_position(payload['symbol']):
        log_skipped(payload, reason='risk_ceiling')
        return

    if is_duplicate(payload):  # same structural level, different webhook retry
        log_skipped(payload, reason='duplicate')
        return

    execution_engine.execute(build_order(payload))
Enter fullscreen mode Exit fullscreen mode

Deduplication is the non-obvious one here. TradingView will retry a webhook delivery if it doesn't get a fast 200 response — meaning your endpoint needs to return quickly (queue and return, don't process synchronously) and your dispatch layer needs to recognize "this is the same signal arriving twice" rather than opening two positions from one structural event.


Stage 4: Latency — the metric nobody backtests

A backtest doesn't know that your webhook receiver is on a shared host with cold-start delay, or that your broker's order API has a 400ms round trip during high-volatility windows. In production, signal-to-execution latency on a fast-moving instrument like gold is a real, measurable variable — and it's invisible until you instrument it:

def execute(self, signal):
    signal_time = parse_chart_time(signal['timestamp'])
    received_time = time.time()
    latency_to_receipt = received_time - signal_time

    order = self.broker.place_order(...)
    execution_time = time.time()
    latency_to_fill = execution_time - received_time

    metrics.record('signal_to_receipt_ms', latency_to_receipt * 1000)
    metrics.record('receipt_to_fill_ms', latency_to_fill * 1000)
Enter fullscreen mode Exit fullscreen mode

Once this was instrumented, the actual bottleneck wasn't the interesting part (Pine Script alert firing, or the webhook receiver) — it was broker order-confirmation round trips during news-driven volatility spikes, which is exactly when signal quality matters most and latency budget matters least.


What broke before this was hardened

Webhook retries created duplicate positions before deduplication was keyed to the structural level (the invalidation price) rather than a timestamp — two webhook deliveries a few hundred milliseconds apart looked like "different" signals under naive timestamp comparison.

A cold-start delay on the receiver caused TradingView to retry, and the retry arrived after the original had already been processed — meaning the dispatch layer had to handle out-of-order and duplicate delivery as a normal case, not an edge case.

Payload schema drift — a Pine Script update that changed a field name silently broke the receiver's validation, and because validation failed closed (rejecting the payload), signals just stopped executing with no obvious error until logs were checked. Schema versioning in the payload itself fixed this going forward.


Where the packaged version fits in

The signal detection and visualization layer described here is the Goldmine indicator; the receiver, dispatch, and execution layers are what make up the Goldmine Trading Bot. If you're building your own bridge, the priority order that mattered most in practice was: payload validation and deduplication before latency optimization — a fast pipeline that occasionally double-fires is more dangerous than a slightly slower one that doesn't.

Full disclosure: both are products I built and sell. I'm posting the actual bridge architecture because I think it's a useful pattern for anyone connecting a visual/chart-based signal system to a real execution layer, TradingView-based or not.

The Indicator in Action


FAQ

Why not just run everything inside Pine Script?
Pine Script has no persistent state across restarts, no outbound HTTP beyond alert webhooks, and no access to broker APIs directly. It's a charting/visualization sandbox by design — the alert-and-webhook bridge is the only sanctioned way out of it.

How do you handle TradingView webhook retries reliably?
Return a fast 200 immediately (queue the payload, process asynchronously) and deduplicate on the structural signal identity, not the delivery timestamp — retries are expected behavior, not a failure mode to prevent.

Is a shared secret in the payload actually secure, or should I use something stronger?
It's a practical minimum given TradingView's webhook model doesn't support native request signing. If your threat model warrants more, IP allowlisting TradingView's published webhook IP ranges adds a second layer, though it doesn't replace payload-level validation.

What's the typical signal-to-fill latency you're seeing?
It varies significantly by broker and session volatility — the point of instrumenting it (Stage 4) isn't a specific number to quote, it's making the bottleneck visible so you know where to actually spend optimization effort.

Can this pattern work with indicators other than Pine Script/TradingView?
Yes — the receiver/dispatch/execution layers are indicator-agnostic. Anything that can fire an HTTP webhook on a signal (custom Python indicator, MetaTrader alert, another charting platform) can plug into the same bridge.

Grab The Bot Here

Grab The Grid System with 90% Win Rate Here


Let Talk

If you've bridged a visualization tool (chart, dashboard, monitoring alert) into something that takes real action downstream, what was the failure mode you didn't see coming until production? Webhook retry handling in particular seems to be the thing everyone underestimates until it duplicates something expensive.

Top comments (0)