mirror of
https://github.com/artemium428/tvsignals-to-tg.git
synced 2026-09-15 17:16:21 +00:00
Chart and Telegram work no longer block the HTTP response, avoiding TV webhook timeouts. Co-authored-by: Cursor <cursoragent@cursor.com>
157 lines
6 KiB
Markdown
157 lines
6 KiB
Markdown
# 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 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/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 %)` / `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 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 / 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 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`)
|