tvsignals-to-tg/README.md
Artemii Peretiachenko c2571f4558 Accept TradingView webhooks immediately and deliver in background.
Chart and Telegram work no longer block the HTTP response, avoiding TV webhook timeouts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-31 10:42:05 +02:00

157 lines
6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/h/<WEBHOOK_SECRET>`
or, for a specific forum topic:
`https://your-domain/h/<WEBHOOK_SECRET>/<TELEGRAM_MESSAGE_THREAD_ID>`
Same alert message body; different URLs → different topics (e.g. one alert per timeframe). Without a thread segment, the env `TELEGRAM_MESSAGE_THREAD_ID` is used.
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 TradingViews 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/h/<WEBHOOK_SECRET>` or `https://your-domain/h/<WEBHOOK_SECRET>/<thread_id>` (per-topic; same message body)
- 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 %)` / `TP13`
**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 `|priceentry| / |entrySL|` with the same sign as profit.
## Behavior
1. Validate secret + payload, then immediately respond `200` (`{"ok": true, "accepted": true}`) so TradingView does not time out
2. In the background: fetch ~90 klines from Binance USDT-M Futures (public, no API key)
3. Render PNG: candles + Entry / SL / TP13 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 from URL path or env `TELEGRAM_MESSAGE_THREAD_ID`
5. If chart/klines fail → text-only `sendMessage` fallback
6. If Telegram fails → logged only (HTTP already returned `200`)
## Endpoints
- `GET /health``{"status":"ok"}`
- `POST /h/<WEBHOOK_SECRET>` → signal payload above; topic from env `TELEGRAM_MESSAGE_THREAD_ID` (wrong/missing secret → `404`)
- `POST /h/<WEBHOOK_SECRET>/<thread_id>` → same payload; topic from path (`thread_id` must be `>= 1`)