mirror of
https://github.com/artemium428/tvsignals-to-tg.git
synced 2026-09-15 17:16:21 +00:00
151 lines
3.9 KiB
Python
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
|