tvsignals-to-tg/app/binance.py
Artemii Peretiachenko d6c539dca9 Initial TradingView→Telegram webhook service with Render Blueprint.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 16:35:47 +02:00

151 lines
3.9 KiB
Python

from __future__ import annotations
import logging
import httpx
import pandas as pd
logger = logging.getLogger(__name__)
BINANCE_FUTURES_KLINES_URL = "https://fapi.binance.com/fapi/v1/klines"
DEFAULT_LIMIT = 90
# TradingView-style timeframe → Binance Futures interval
TIMEFRAME_MAP: dict[str, str] = {
"1": "1m",
"1m": "1m",
"3": "3m",
"3m": "3m",
"5": "5m",
"5m": "5m",
"15": "15m",
"15m": "15m",
"30": "30m",
"30m": "30m",
"60": "1h",
"1h": "1h",
"120": "2h",
"2h": "2h",
"240": "4h",
"4h": "4h",
"360": "6h",
"6h": "6h",
"480": "8h",
"8h": "8h",
"720": "12h",
"12h": "12h",
"d": "1d",
"1d": "1d",
"1D": "1d",
"D": "1d",
"w": "1w",
"1w": "1w",
"1W": "1w",
"W": "1w",
}
# TradingView / broker suffixes stripped before Binance Futures lookup
_PERP_SUFFIXES = (".P", ".PERP", "_PERP", "-PERP")
def to_binance_symbol(ticker: str) -> str:
"""Map TradingView ticker to Binance Futures symbol (e.g. BTCUSDT).
Accepts common TV forms:
- BTCUSDT.P / BTCUSDT
- BINANCE:BTCUSDT.P / BYBIT:ETHUSDT
- BTC/USDT, BTC-USDT, BTCUSDTPERP
"""
symbol = ticker.strip().upper()
if not symbol:
raise ValueError("Empty ticker")
# Exchange / broker prefix: BINANCE:BTCUSDT.P → BTCUSDT.P
if ":" in symbol:
symbol = symbol.rsplit(":", 1)[-1].strip()
symbol = symbol.replace(" ", "").replace("/", "").replace("-", "")
for suffix in _PERP_SUFFIXES:
if symbol.endswith(suffix):
symbol = symbol[: -len(suffix)]
break
else:
# BTCUSDTPERP (no separator)
if symbol.endswith("PERP") and len(symbol) > 4:
symbol = symbol[:-4]
# Continuous-contract markers (CME-style), ignore for Binance
if symbol.endswith("1!"):
symbol = symbol[:-2]
elif symbol.endswith("!"):
symbol = symbol[:-1]
if not symbol:
raise ValueError(f"Empty symbol after normalizing ticker: {ticker!r}")
return symbol
def to_binance_interval(visual_timeframe: str) -> str:
key = visual_timeframe.strip()
interval = TIMEFRAME_MAP.get(key) or TIMEFRAME_MAP.get(key.lower())
if interval is None:
raise ValueError(f"Unsupported visual_timeframe: {visual_timeframe!r}")
return interval
async def fetch_klines(
symbol: str,
interval: str,
*,
limit: int = DEFAULT_LIMIT,
end_ms: int | None = None,
timeout: float = 15.0,
) -> pd.DataFrame:
"""Fetch OHLCV klines from Binance USDT-M Futures.
If ``end_ms`` is set, returns candles ending at/before that UTC epoch millis
(useful for historical / as-of charts).
"""
params: dict[str, str | int] = {
"symbol": symbol,
"interval": interval,
"limit": limit,
}
if end_ms is not None:
params["endTime"] = end_ms
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.get(BINANCE_FUTURES_KLINES_URL, params=params)
response.raise_for_status()
raw = response.json()
if not raw:
raise ValueError(f"Empty klines for {symbol} {interval}")
df = pd.DataFrame(
raw,
columns=[
"open_time",
"open",
"high",
"low",
"close",
"volume",
"close_time",
"quote_volume",
"trades",
"taker_buy_base",
"taker_buy_quote",
"ignore",
],
)
df["Date"] = pd.to_datetime(df["open_time"], unit="ms", utc=True)
for col in ("open", "high", "low", "close", "volume"):
df[col] = pd.to_numeric(df[col], errors="coerce")
df = df.set_index("Date")[["open", "high", "low", "close", "volume"]]
df.columns = ["Open", "High", "Low", "Close", "Volume"]
df = df.dropna()
if df.empty:
raise ValueError(f"No valid OHLCV rows for {symbol} {interval}")
return df