Skip to main content

Building a TradingView TA Adapter

The two earlier tutorials wired numeric data from a JSON API into BrighterTrading as an External Indicator. This one shows the other side of the platform: External Signals — boolean / directional events posted from outside, used as triggers in your strategies.

We're going to build a 30-line Python adapter that polls TradingView's technical-analysis recommendation for BTCUSDT and POSTs the result to BrighterTrading whenever it changes. When TradingView says STRONG_BUY, your strategy block evaluates triggered=true, direction=buy. When it flips to SELL, the signal goes the other way.

What you'll learn
  • The difference between External Indicators (numeric values) and External Signals (boolean / directional events)
  • Pull mode vs push mode — and why push mode usually wins for laptop-hosted adapters
  • How to configure a push-mode signal with typed parsing and a direction map
  • How to write an adapter using the tradingview-ta Python package
  • How to test the wiring with curl before writing any adapter code
  • Practical concerns: shared secrets, staleness TTL, where to actually run the adapter

Indicators vs Signals — pick the right shape first

Before writing any code, pick the right concept:

External IndicatorExternal Signal
OutputsNumeric values (one or more named)Boolean (triggered) + optional direction / confidence / raw value
Used inIndicator block — comparisons, math, thresholdsSignal block — is triggered / direction outputs in conditions
BacktestableYesNo (live/paper only — backtests of strategies referencing one will refuse to start)
Typical providersFunding rates, OI, Fear & Greed, on-chain statsTradingView alerts, 3commas signals, sentiment classifiers

TradingView's recommendation comes back as "STRONG_BUY", "BUY", "NEUTRAL", "SELL", "STRONG_SELL". That's a categorical / directional value — not a number. External Signal is the right fit.

Pull mode vs push mode

External Signals support two modes, sharing the same config row:

  • Pull mode — BrighterTrading polls a URL every N seconds. Your adapter has to expose a JSON endpoint reachable from the public internet (cloud hosting, ngrok, etc.).
  • Push mode — BrighterTrading exposes one endpoint (POST /webhook/signal/<token>); your adapter (or any provider — TradingView's own alerts, 3commas, custom code) just POSTs to it. No inbound URL on your end is needed.

Push mode is the right pick for an adapter running on your laptop. Outbound HTTPS works behind any home NAT or corporate firewall, and you don't have to keep a public endpoint alive.

Step 1: create the External Signal in push mode

In the right-side panel under Signals, click + External Signal:

External Signal dialog blank

Fill it in:

FieldValue
Signal NameTradingView BTC TA
ModePush (receive webhook)
Shared SecretA long random string of your choosing. Anyone who has the webhook URL and this secret can update your signal — pick a real password.
Direction JSONPath$.summary
Confidence JSONPath$.confidence (optional)
Raw Value JSONPath$.symbol (optional)
Triggered → TypeMapped (triggered iff direction is non-neutral)
Direction Map{"STRONG_BUY":"buy","BUY":"buy","NEUTRAL":"neutral","SELL":"sell","STRONG_SELL":"sell"}
Staleness TTL3600

External Signal dialog filled

Click Save Signal. An alert appears with your unique webhook URL, looking something like:

https://bt.brrd.tech/webhook/signal/VQl6rmR9uEGj1tifjDKDRiio5Cf5HmP5WPEINCPlQb0

Copy that URL into a note somewhere safe. You won't see it again unless you click the 📋 icon next to the signal in the panel afterwards.

The signal now appears in the right panel under External Signals:

Signal in panel

Notice the badges: push (the mode) and, once a webhook fires, triggered (the live state). You'll see those flip as the adapter runs.

Step 2: test the webhook with curl (before writing any adapter code)

Before you write a single line of Python, prove the wiring works. Replace the URL and secret with yours:

WEBHOOK="https://bt.brrd.tech/webhook/signal/YOUR_TOKEN"
SECRET="your-shared-secret"

curl -X POST "$WEBHOOK" \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: $SECRET" \
-d '{"symbol":"BTCUSDT","summary":"STRONG_BUY","confidence":0.92}'

Expected response:

{"ok":true,"parsed":{"confidence":0.92,"direction":"buy","raw_value":"BTCUSDT","triggered":true}}

The webhook handler validated the secret, parsed the JSON, applied the direction map (STRONG_BUYbuy), and used your triggered_type='mapped' rule to set triggered=true because buy is non-neutral.

Try the failure cases too — they're your debugging guide:

RequestExpected responseStatus
Wrong secret in X-Webhook-Secret header{"error":"auth failed"}403
Body that isn't valid JSON{"error":"invalid JSON"}400
Valid JSON but none of your JSONPaths match{"error":"configured JSONPaths yielded no matches", ...}422
Unknown token in URL{"error":"not found"}404

If you can hit 200 with STRONG_BUY and 403 with a wrong secret, the BrighterTrading side is correct. Now write the adapter.

Step 3: the adapter

Install the dependencies:

pip install tradingview-ta requests

Save this as tv_adapter.py:

"""
TradingView TA → BrighterTrading External Signal adapter.

Polls TradingView's BTCUSDT 1h analysis. When the recommendation
changes (STRONG_BUY / BUY / NEUTRAL / SELL / STRONG_SELL), POSTs the
new state to BrighterTrading. Signal updates only on change to avoid
spamming the webhook.

Run on any machine with outbound HTTPS — laptop, raspberry pi, VPS.
No inbound port required.
"""
import os
import time
import requests
from tradingview_ta import TA_Handler, Interval

WEBHOOK_URL = os.environ['BT_WEBHOOK_URL'] # https://bt.brrd.tech/webhook/signal/<token>
SHARED_SECRET = os.environ['BT_WEBHOOK_SECRET']

POLL_SECONDS = 60
SYMBOL = 'BTCUSDT'
EXCHANGE = 'BINANCE'
INTERVAL = Interval.INTERVAL_1_HOUR

handler = TA_Handler(symbol=SYMBOL, screener='crypto',
exchange=EXCHANGE, interval=INTERVAL)

last_recommendation = None
while True:
try:
analysis = handler.get_analysis()
s = analysis.summary # {'RECOMMENDATION': 'BUY', 'BUY': 14, 'SELL': 5, 'NEUTRAL': 7}
recommendation = s['RECOMMENDATION']

if recommendation != last_recommendation:
total = s['BUY'] + s['SELL'] + s['NEUTRAL']
non_neutral = s['BUY'] + s['SELL']
confidence = round(non_neutral / total, 2) if total else 0.0

payload = {
'symbol': SYMBOL,
'summary': recommendation,
'confidence': confidence,
'buy_count': s['BUY'],
'sell_count': s['SELL'],
'neutral_count': s['NEUTRAL'],
}

response = requests.post(
WEBHOOK_URL,
json=payload,
headers={'X-Webhook-Secret': SHARED_SECRET},
timeout=3,
)
print(f'[{time.strftime("%H:%M:%S")}] {recommendation} → '
f'{response.status_code} {response.text[:200]}')
last_recommendation = recommendation
else:
print(f'[{time.strftime("%H:%M:%S")}] still {recommendation}, no POST')

except Exception as e:
print(f'[{time.strftime("%H:%M:%S")}] error: {e}')

time.sleep(POLL_SECONDS)

Run it:

export BT_WEBHOOK_URL='https://bt.brrd.tech/webhook/signal/YOUR_TOKEN'
export BT_WEBHOOK_SECRET='your-shared-secret'
python tv_adapter.py

Output looks like:

[14:22:01] BUY → 200 {"ok":true,"parsed":{"confidence":0.65,"direction":"buy","raw_value":"BTCUSDT","triggered":true}}
[14:23:01] still BUY, no POST
[14:24:01] still BUY, no POST
[14:38:01] STRONG_SELL → 200 {"ok":true,"parsed":{"confidence":0.81,"direction":"sell","raw_value":"BTCUSDT","triggered":true}}

Anatomy of the script:

  • Lines 14–17 — env vars hold the webhook URL and shared secret. Don't hardcode them. Anyone who reads your source code can hijack the signal.
  • Line 26 — TradingView's get_analysis() does an internal websocket request to their scanner; no web scraping, no headless browser. Returns instantly.
  • Lines 30–31 — only POST when the recommendation changes. Your strategy doesn't care about repeats; this also reduces webhook traffic massively.
  • Lines 33–35 — confidence is computed locally as the fraction of indicators voting non-neutral. The signal config's Confidence JSONPath ($.confidence) extracts it.
  • Line 49 — 3-second timeout matches the platform's webhook handler SLA. If your network is slow you'll just retry next cycle.
  • No staleness handling needed in code — BrighterTrading's Staleness TTL (3600 in the dialog) auto-flips triggered to false if no fresh webhook arrives within the TTL. If your laptop sleeps for 2 hours, the signal correctly reports "not triggered" instead of getting stuck on stale state.

Step 4: use it in a strategy

In the Blockly toolbox under Signals, your saved External Signal appears as a draggable block alongside your regular signals. The dropdown exposes is triggered, direction, confidence, raw value. Drop it into any condition — for example:

If signal TradingView BTC TA direction is buy AND not has open position, open long with 2% SL / 4% TP.

The block resolves through the same code path that internal signals use. Strategies don't see anything special about it being external.

Step 5: viewing the webhook log

Each push signal keeps the most recent 100 webhook attempts in a log so you can debug payload mismatches. Click the log button next to the signal in the right panel to see entries with timestamps, parsed values, and parse_status (ok / auth_fail / invalid_json / jsonpath_miss). Useful when TradingView sends a payload shape you didn't anticipate.

Production concerns

Once the script works on your laptop or desktop, decide where it should actually run. Options in order of effort:

OptionProsCons
Run it locally with pm2 / systemd --user / nohupFree, full controlDies when the machine sleeps or restarts
A $5/mo VPS (DigitalOcean, Hetzner, OVH)Always on, full controlYou're now a sysadmin
Free-tier cloud hosting (Render, Railway, Fly.io)No server to babysitFree tiers come and go; the host may suspend or rate-limit your script silently

Honest take: the adapter is small enough that a $5/mo VPS dedicated to "trading helpers" is the right answer once you have more than one. Until then, run it on whatever computer you have running anyway and rely on the staleness TTL to keep your strategies from acting on stale state when the script isn't running.

Adapting this pattern

The shape of this tutorial — small Python adapter polls a third-party data source, pushes typed signals to BrighterTrading — generalizes broadly. The same scaffolding works for:

  • Reddit / X sentiment classifiers (count keyword frequency in recent posts)
  • News headline sentiment via an LLM API
  • A Telegram bot watching for messages from a specific signal channel
  • On-chain data via Etherscan / Alchemy
  • Any "I want my strategy to react to some external event" use case

The 30 lines stay 30 lines; only the data source and the payload dict change.