# TradingView → Telegram setup service Accepts TradingView webhook alerts, renders a Binance Futures candlestick setup chart, and posts photo + caption into a Telegram forum topic. ## Quick start (Docker / VPS) 1. Copy env and fill Telegram values: ```bash cp .env.example .env ``` ```env TELEGRAM_BOT_TOKEN=... TELEGRAM_CHAT_ID=-100... TELEGRAM_MESSAGE_THREAD_ID=... HOST=0.0.0.0 PORT=8000 ``` 2. Build and run: ```bash docker compose up -d --build ``` 3. Health check: ```bash curl http://127.0.0.1:8000/health ``` 4. Put HTTPS in front (nginx/Caddy) and point TradingView webhook to: `https://your-domain/webhook` Bot must be added to the group/forum and allowed to post in the target topic. ## Local run (without Docker) ```bash python -m venv .venv source .venv/bin/activate pip install -r requirements.txt cp .env.example .env # fill values uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload ``` ## TradingView alert JSON The webhook accepts a **single-line or pretty-printed JSON** body, including TradingView’s common `Content-Type: text/plain`. Static example (alert **Webhook message** body) — primary setup (`signal_sequence: 1`): ```json { "ticker": "{{ticker}}", "action": "long", "entry_price": "65034.7", "current_price": "65034.7", "stop_loss_price": "63904.9", "take_profit_1_price": "66085.9", "take_profit_2_price": "67209.2", "take_profit_3_price": "68355.5", "visual_timeframe": "15", "signal_sequence": 1, "signal_time": 1721736000 } ``` Control update (`signal_sequence` > 1) — Pine freezes `entry_price` / SL / TPs / `signal_time` from seq 1 and sends live `current_price`: ```json { "ticker": "{{ticker}}", "action": "short", "entry_price": "65034.7", "current_price": "64000.1", "stop_loss_price": "65896.8", "take_profit_1_price": "63904.9", "take_profit_2_price": "62781.6", "take_profit_3_price": "61635.3", "visual_timeframe": "15", "signal_sequence": 2, "signal_time": 1721736000 } ``` ### PineScript `alert()` (recommended) Build the JSON inside `alert()`. A continuous one-line string is fine. In the TradingView alert dialog: - Webhook URL: `https://your-domain/webhook` - Message: only `{{alert_message}}` (do not paste a second JSON next to it) On **seq == 1**: store `entry_price = close`, freeze SL/TPs, and `signal_time = time / 1000` (bar open, unix seconds). On **seq > 1**: keep those frozen fields; only refresh `current_price` (= live `close`). Example shape: ```pinescript alert('{"ticker":"' + syminfo.ticker + '","action":"long","entry_price":"' + str.tostring(long_entry_price, format.mintick) + '","current_price":"' + str.tostring(close, format.mintick) + '","stop_loss_price":"' + str.tostring(long_sl_price, format.mintick) + '","take_profit_1_price":"' + str.tostring(long_tp1_price, format.mintick) + '","take_profit_2_price":"' + str.tostring(long_tp2_price, format.mintick) + '","take_profit_3_price":"' + str.tostring(long_tp3_price, format.mintick) + '","visual_timeframe":"' + timeframe.period + '","signal_sequence":' + str.tostring(buyCount) + ',"signal_time":' + str.tostring(signal_buy_time) + '}', alert.freq_once_per_bar_close) ``` (Same idea for shorts with `short_entry_price` / `sellCount` / `signal_sell_time`.) Caption emoji/labels are built by the service from `action` + `signal_sequence` — do not put them in the webhook JSON. Field notes: | Field | Description | |---|---| | `ticker` | any common TV form (`BTCUSDT.P`, `BTCUSDT`, `BINANCE:ETHUSDT`, `BTC/USDT`, …) → normalized to Binance Futures symbol for the chart; caption keeps the original | | `action` | `long` or `short` | | `entry_price` | trade entry from seq 1 (equals `current_price` on primary signal) | | `current_price` | live price (`close` at alert time) | | `*_price` | strings with your display precision | | `visual_timeframe` | `1`, `3`, `5`, `15`, `30`, `60`, `120`, `240`, `D`, `W` (also `15m`, `1h`, …) | | `signal_sequence` | `1` = primary setup; `>1` = control update of that trade | | `signal_time` | unix seconds of the seq-1 bar open (UTC); chart draws Entry/SL/TP zones from that candle | ## Caption format **seq `1` (setup):** - long: `BTCUSDT.P 💚 Buy` - short: `BTCUSDT.P 💔 Sell` - body: `Price` / `SL (risk %)` / `TP1–3` **seq `>1` (control):** - long: `BTCUSDT.P 🌱 Buy Seq: N` - short: `BTCUSDT.P 🥀 Sell Seq: N` - body: `Entry price` (from seq 1) / live `Price` / `Current profit: +1.6% (RR 1:1.2)` - no new SL/TP lines in the caption Prices are shown with `$` and thousand spaces (`65034.7` → `$65 034.7`). For seq 1, SL includes distance from entry: `SL: $63 904.9 (-2.37%)` (risk %, negative for both long and short). For seq >1, profit % is signed vs entry; RR is `|price−entry| / |entry−SL|` with the same sign as profit. ## Behavior 1. Validate payload 2. Fetch ~90 klines from Binance USDT-M Futures (public, no API key) 3. Render PNG: candles + Entry / SL / TP1–3 from the payload, starting at the `signal_time` candle (seq `>1` reuses frozen seq-1 levels/time and also marks live `Price`) 4. `sendPhoto` to `TELEGRAM_CHAT_ID` topic `TELEGRAM_MESSAGE_THREAD_ID` 5. If chart/klines fail → text-only `sendMessage` fallback (still `200`) 6. If Telegram fails → `502` ## Endpoints - `GET /health` → `{"status":"ok"}` - `POST /webhook` → signal payload above