run_live.py
Anonymous
plain_text
02/13/2026 6:44 AM
19.3 KB
8
Indexable
import json
import sys
import time
import threading
from pathlib import Path
import pandas as pd
sys.path.append(str(Path(__file__).resolve().parents[1]))
from src.config.loader import load_config
from src.core.logger import setup_logging
from src.core.timeutils import now_utc
from src.infra.exchange_binance import BinanceClient
from src.infra.ws_binance import BinanceWsClient
from src.infra.telegram_client import TelegramClient
from src.infra.storage import write_healthcheck_db, init_live_db
from src.ml.model_xgb import load_model
from src.ml.features import load_feature_list
from src.live.scanner import scan_once, scan_once_cached, _load_symbols, _prepare_1h
from src.live.state_machine import TradeStateMachine
from src.live.monitor import monitor_trades, monitor_trades_ws
from src.core.timeutils import sleep_until_next_close, timeframe_to_seconds
from src.structure.pivots import find_swings, detect_structure
def main():
logger = setup_logging("live")
cfg = load_config()
exchange = BinanceClient(
cfg.exchange.base_url,
cfg.exchange.timeout_sec,
cfg.exchange.max_retries,
cfg.exchange.backoff_sec,
cfg.exchange.min_interval_ms,
)
ws_kline_client = BinanceWsClient(cfg.exchange.ws_url, on_log=logger.info)
ws_btc_client = BinanceWsClient(cfg.exchange.ws_url, on_log=logger.info)
ws_book_client = BinanceWsClient(cfg.exchange.ws_url, on_log=logger.info)
telegram = TelegramClient(
cfg.telegram.enabled,
cfg.telegram.bot_token,
cfg.telegram.chat_id,
cfg.telegram.timeout_sec,
cfg.telegram.max_retries,
cfg.telegram.backoff_sec,
)
events_db = cfg.database.name
init_live_db(events_db, prefix="live")
state_machine = TradeStateMachine(
telegram=telegram,
events_db=events_db,
entry_expiry_hours=cfg.filters.entry_expiry_hours,
db_prefix="live",
)
model_entry_path = Path("data/models/model_entry.joblib")
entry_model_path = model_entry_path if model_entry_path.exists() else Path("data/models/model.joblib")
entry_model = load_model(str(entry_model_path))
logger.info("entry model: %s", entry_model_path)
strategy_models = {}
strategy_thresholds = {}
strategy_cfg_map = {
"reclaim": cfg.strategies.reclaim,
"breakout": cfg.strategies.breakout,
"trend_pullback": cfg.strategies.trend_pullback,
"mean_reversion": cfg.strategies.mean_reversion,
"liquidity_sweep": cfg.strategies.liquidity_sweep,
"volatility_expansion": cfg.strategies.volatility_expansion,
}
for strategy_name, strategy_cfg in strategy_cfg_map.items():
threshold_cfg = float(getattr(strategy_cfg, "threshold", cfg.ml.threshold))
model_path = Path(getattr(strategy_cfg, "model_path", "") or "")
meta_path = model_path.parent / "meta_entry.json"
if meta_path.exists():
try:
meta_data = json.loads(meta_path.read_text(encoding="utf-8"))
threshold_cfg = float(meta_data.get("threshold", threshold_cfg))
except Exception as exc:
logger.warning("failed read strategy meta (%s): %s", strategy_name, exc)
strategy_thresholds[strategy_name] = threshold_cfg
if model_path.exists():
try:
strategy_models[strategy_name] = load_model(str(model_path))
logger.info("strategy model (%s): %s", strategy_name, model_path)
except Exception as exc:
logger.warning("failed load strategy model (%s): %s", strategy_name, exc)
strategy_models[strategy_name] = entry_model
else:
logger.warning("strategy model missing (%s), fallback to default model: %s", strategy_name, model_path)
strategy_models[strategy_name] = entry_model
tp_model = None
tp_model_path = Path("data/models/model_tp.joblib")
if tp_model_path.exists():
tp_model = load_model(str(tp_model_path))
logger.info("tp model: %s", tp_model_path)
meta_path = Path("data/models/meta_entry.json")
legacy_meta_path = Path("data/models/meta.json")
if meta_path.exists():
meta = json.loads(meta_path.read_text(encoding="utf-8"))
elif legacy_meta_path.exists():
meta = json.loads(legacy_meta_path.read_text(encoding="utf-8"))
else:
meta = {}
entry_threshold = cfg.ml.threshold
entry_feature_order = meta.get("feature_order") or load_feature_list()
logger.info(
"live config: entry_tf=%s trend_tf=%s symbols=%d threshold=%.4f features=%d",
cfg.timeframes.entry_tf,
cfg.timeframes.trend_tf,
len(_load_symbols(cfg.universe.symbols_file)),
float(entry_threshold),
len(entry_feature_order),
)
tp_meta_path = Path("data/models/meta_tp.json")
if tp_meta_path.exists():
tp_meta = json.loads(tp_meta_path.read_text(encoding="utf-8"))
tp_feature_order = tp_meta.get("feature_order") or entry_feature_order
else:
tp_feature_order = entry_feature_order
symbols = _load_symbols(cfg.universe.symbols_file)
kline_symbols = [s for s in symbols if s != "BTCUSDT"]
cache_15m = {}
cache_1h = {}
btc_15m_cache = []
btc_1h_cache = []
live_cache_path = Path("data/processed/live_cache.json")
seed_entry_limit = int(cfg.live.entry_limit)
seed_trend_limit = int(cfg.live.trend_limit)
cache_entry_limit = int(cfg.live.entry_limit)
cache_trend_limit = int(cfg.live.trend_limit)
def _seed_cache():
for symbol in symbols:
df_15m = exchange.fetch_ohlcv(symbol, cfg.timeframes.entry_tf, limit=seed_entry_limit)
df_1h = exchange.fetch_ohlcv(symbol, cfg.timeframes.trend_tf, limit=seed_trend_limit)
cache_15m[symbol] = df_15m.to_dict("records")
cache_1h[symbol] = df_1h.to_dict("records")
btc = exchange.fetch_btc_ohlcv(cfg.timeframes.entry_tf, limit=seed_entry_limit)
btc_15m_cache.extend(btc.to_dict("records"))
btc_trend = exchange.fetch_btc_ohlcv(cfg.timeframes.trend_tf, limit=seed_trend_limit)
btc_1h_cache.extend(btc_trend.to_dict("records"))
logger.info(
"seed cache: symbols=%d entry_limit=%d trend_limit=%d btc_entry=%d btc_trend=%d",
len(symbols),
seed_entry_limit,
seed_trend_limit,
len(btc_15m_cache),
len(btc_1h_cache),
)
def _write_realtime_cache():
payload = {"updated_ts": int(now_utc().timestamp() * 1000), "symbols": {}}
entry_tf = cfg.timeframes.entry_tf
trend_tf = cfg.timeframes.trend_tf
for symbol in symbols:
if symbol == "BTCUSDT":
payload["symbols"][symbol] = {
entry_tf: list(btc_15m_cache),
trend_tf: list(btc_1h_cache),
}
else:
payload["symbols"][symbol] = {
entry_tf: list(cache_15m.get(symbol, [])),
trend_tf: list(cache_1h.get(symbol, [])),
}
live_cache_path.parent.mkdir(parents=True, exist_ok=True)
live_cache_path.write_text(json.dumps(payload, ensure_ascii=True), encoding="utf-8")
def _log_gap(symbol: str, interval: str, last_ts: int | None, new_ts: int, expected_ms: int):
if last_ts is None:
return
gap_ms = new_ts - last_ts
if gap_ms > int(expected_ms * 1.5):
logger.warning(
"gap detected: symbol=%s interval=%s last_ts=%d new_ts=%d gap_ms=%d expected_ms=%d",
symbol,
interval,
last_ts,
new_ts,
gap_ms,
expected_ms,
)
entry_ms = timeframe_to_seconds(cfg.timeframes.entry_tf) * 1000
trend_ms = timeframe_to_seconds(cfg.timeframes.trend_tf) * 1000
def _update_cache(cache: dict, symbol: str, kline: dict, interval: str, expected_ms: int):
entry = {
"ts": int(kline.get("t")),
"open": float(kline.get("o")),
"high": float(kline.get("h")),
"low": float(kline.get("l")),
"close": float(kline.get("c")),
"volume": float(kline.get("v")),
}
series = cache.setdefault(symbol, [])
last_ts = series[-1]["ts"] if series else None
_log_gap(symbol, interval, last_ts, entry["ts"], expected_ms)
if not series or series[-1]["ts"] != entry["ts"]:
series.append(entry)
else:
series[-1] = entry
if len(series) > cache_entry_limit:
del series[0]
def _update_cache_list(series: list, symbol: str, kline: dict, interval: str, expected_ms: int):
entry = {
"ts": int(kline.get("t")),
"open": float(kline.get("o")),
"high": float(kline.get("h")),
"low": float(kline.get("l")),
"close": float(kline.get("c")),
"volume": float(kline.get("v")),
}
last_ts = series[-1]["ts"] if series else None
_log_gap(symbol, interval, last_ts, entry["ts"], expected_ms)
if not series or series[-1]["ts"] != entry["ts"]:
series.append(entry)
else:
series[-1] = entry
if len(series) > cache_entry_limit:
del series[0]
_seed_cache()
_write_realtime_cache()
cooldowns = {}
last_scan_ts = 0
monitor_thread = None
def _start_monitor():
nonlocal monitor_thread
if monitor_thread and monitor_thread.is_alive():
return
monitor_thread = threading.Thread(
target=monitor_trades_ws,
args=(state_machine, ws_book_client),
kwargs={"max_minutes": None},
daemon=True,
)
monitor_thread.start()
def _on_kline(symbol: str, interval: str, k: dict):
nonlocal last_scan_ts
is_closed = bool(k.get("x"))
if interval == cfg.timeframes.entry_tf:
if symbol == "BTCUSDT":
_update_cache_list(btc_15m_cache, symbol, k, interval, entry_ms)
else:
_update_cache(cache_15m, symbol, k, interval, entry_ms)
if is_closed:
close_ts = int(k.get("T", 0))
if close_ts and close_ts != last_scan_ts:
last_scan_ts = close_ts
open_symbols = {
info["signal"]["candidate"].symbol
for info in state_machine.trades.values()
if info["state"].value not in ["closed", "expired"]
}
signals = scan_once_cached(
cfg,
symbols,
cache_15m,
cache_1h,
btc_15m_cache,
btc_1h_cache,
entry_model,
tp_model,
entry_feature_order,
tp_feature_order,
entry_threshold,
telegram,
cooldowns,
open_symbols,
strategy_models,
strategy_thresholds,
)
for signal in signals:
symbol_sig = signal["candidate"].symbol
direction_sig = signal["candidate"].direction.value
if state_machine.has_open_trade(symbol_sig):
if state_machine.cancel_new_if_opposite(symbol_sig, direction_sig):
state_machine.on_new_signal(signal["trade_id"], signal)
continue
state_machine.on_new_signal(signal["trade_id"], signal)
logger.info("scan complete: close_ts=%d signals=%d", close_ts, len(signals))
if signals:
_start_monitor()
_write_realtime_cache()
if interval == cfg.timeframes.trend_tf:
if symbol == "BTCUSDT":
_update_cache_list(btc_1h_cache, symbol, k, interval, trend_ms)
if len(btc_1h_cache) > cache_trend_limit:
del btc_1h_cache[0]
return
_update_cache(cache_1h, symbol, k, interval, trend_ms)
if len(cache_1h.get(symbol, [])) > cache_trend_limit:
del cache_1h[symbol][0]
if is_closed:
df_1h = pd.DataFrame(cache_1h.get(symbol, []))
if not df_1h.empty:
df_1h = _prepare_1h(df_1h, cfg)
latest = df_1h.iloc[-1]
trend_row = {
"ema50_1h": float(latest["ema50_1h"]),
"ema200_1h": float(latest["ema200_1h"]),
"adx_1h": float(latest["adx_1h"]),
}
swings = find_swings(df_1h)
struct = detect_structure(swings) if swings else None
state_machine.on_trend_update(symbol, trend_row, struct)
while True:
try:
ws_kline_client.start_klines(kline_symbols, [cfg.timeframes.entry_tf, cfg.timeframes.trend_tf], _on_kline)
ws_btc_client.start_klines(["BTCUSDT"], [cfg.timeframes.entry_tf, cfg.timeframes.trend_tf], _on_kline)
while True:
open_trades = sum(
1
for info in state_machine.trades.values()
if getattr(info.get("state"), "value", info.get("state")) not in ["closed", "expired"]
)
write_healthcheck_db(
events_db,
{"ts": int(now_utc().timestamp() * 1000), "open_trades": open_trades},
prefix="live",
)
time.sleep(5)
except Exception as exc:
logger.exception("live loop error: %s", exc)
ws_kline_client.stop()
ws_btc_client.stop()
ws_book_client.stop()
monitor_trades(state_machine, exchange, poll_seconds=30, max_minutes=None)
sleep_until_next_close(cfg.timeframes.entry_tf)
if __name__ == "__main__":
main()
Editor is loading...
Leave a Comment