#!/usr/bin/env python3 """ Dolphin Live Node — DolphinActor inside NT TradingNode ======================================================= Phase 1: paper_trading=True (live Binance Futures data, paper fills). Validates signal parity with nautilus_event_trader.py before live exec. To go live (Phase 2): set paper_trading=False in build_node(). """ import os import sys import asyncio from copy import deepcopy from pathlib import Path PROJECT_ROOT = Path(__file__).parent.parent sys.path.insert(0, str(PROJECT_ROOT / 'nautilus_dolphin')) sys.path.insert(0, str(PROJECT_ROOT / 'prod')) sys.path.insert(0, str(PROJECT_ROOT)) from dotenv import load_dotenv load_dotenv(PROJECT_ROOT / '.env') from nautilus_trader.live.node import TradingNode from nautilus_trader.config import TradingNodeConfig, LiveDataEngineConfig, CacheConfig from nautilus_trader.adapters.binance.config import BinanceDataClientConfig from nautilus_trader.adapters.binance.common.enums import BinanceAccountType from nautilus_trader.adapters.binance.factories import BinanceLiveDataClientFactory from nautilus_trader.model.identifiers import TraderId from prod.bingx.config import BingxExecClientConfig from prod.bingx.data_config import BingxDataClientConfig from prod.bingx.enums import BingxEnvironment from prod.bingx.data_factories import BingxLiveDataClientFactory from prod.bingx.factories import BingxLiveExecClientFactory # Nautilus changed this enum name across versions. _BINANCE_USDT_FUTURES_ACCOUNT_TYPE = getattr( BinanceAccountType, "USDT_FUTURES", getattr(BinanceAccountType, "USDT_FUTURE", None), ) if _BINANCE_USDT_FUTURES_ACCOUNT_TYPE is None: raise AttributeError("BinanceAccountType is missing both USDT_FUTURES and USDT_FUTURE") # --------------------------------------------------------------------------- # Universe — 50 OBF assets. Subscribed for live quote cache (order sizing). # Must cover the full eigen universe so _exec_submit_entry finds live quotes. # --------------------------------------------------------------------------- LIVE_ASSETS = [ "BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT", "XRPUSDT", "ADAUSDT", "DOGEUSDT", "TRXUSDT", "DOTUSDT", "MATICUSDT", "LTCUSDT", "AVAXUSDT", "LINKUSDT", "UNIUSDT", "ATOMUSDT", "ETCUSDT", "XLMUSDT", "BCHUSDT", "NEARUSDT", "ALGOUSDT", "VETUSDT", "FILUSDT", "APTUSDT", "OPUSDT", "ARBUSDT", "INJUSDT", "SUIUSDT", "SEIUSDT", "TIAUSDT", "ORDIUSDT", "WLDUSDT", "FETUSDT", "AGIXUSDT", "RENDERUSDT", "IOTAUSDT", "AAVEUSDT", "SNXUSDT", "CRVUSDT", "COMPUSDT", "MKRUSDT", "ENJUSDT", "MANAUSDT", "SANDUSDT", "AXSUSDT", "GALAUSDT", "ZECUSDT", "DASHUSDT", "XMRUSDT", "NEOUSDT", "QTUMUSDT", ] # --------------------------------------------------------------------------- # DolphinActor config — gold-standard params, must match nautilus_event_trader # --------------------------------------------------------------------------- DOLPHIN_CONFIG = { 'live_mode': True, 'venue': 'BINANCE', 'data_venue': 'BINANCE', 'exec_venue': 'BINANCE', 'direction': 'short_only', 'hazelcast': { 'host': '127.0.0.1:5701', 'cluster': 'dolphin', 'state_map': 'DOLPHIN_STATE_PRODGREEN', 'imap_pnl': 'DOLPHIN_PNL_PRODGREEN', }, 'paper_trade': {'initial_capital': 25000.0}, 'assets': LIVE_ASSETS, 'engine': { 'boost_mode': 'd_liq', # Signal 'vel_div_threshold': -0.020, 'vel_div_extreme': -0.050, # Leverage — gold spec: 8x soft / 9x hard 'min_leverage': 0.5, 'max_leverage': 8.0, 'abs_max_leverage': 9.0, 'leverage_convexity': 3.0, 'fraction': 0.20, 'max_account_leverage': 3.0, # Exits — gold spec: 250 bars max hold 'fixed_tp_pct': 0.0095, 'stop_pct': 1.0, 'max_hold_bars': 250, # Direction confirm 'use_direction_confirm': True, 'dc_lookback_bars': 7, 'dc_min_magnitude_bps': 0.75, 'dc_skip_contradicts': True, 'dc_leverage_boost': 1.0, 'dc_leverage_reduce': 0.5, # Asset selection — gold spec: IRP filter disabled in live 'use_asset_selection': True, 'min_irp_alignment': 0.0, 'asset_selector_lookback': 10, # Fees / slippage 'use_sp_fees': True, 'use_sp_slippage': True, 'sp_maker_entry_rate': 0.62, 'sp_maker_exit_rate': 0.50, # OB edge 'use_ob_edge': True, 'ob_edge_bps': 5.0, 'ob_confirm_rate': 0.40, 'ob_imbalance_bias': -0.09, 'ob_depth_scale': 1.0, # Alpha layers 'lookback': 100, 'use_alpha_layers': True, 'use_dynamic_leverage': True, 'seed': 42, # V7 RT exit engine (GREEN only) 'use_exit_v7': True, 'use_exit_v6': False, 'v6_bar_duration_sec': 5.0, 'bounce_model_path': str(PROJECT_ROOT / 'prod' / 'models' / 'bounce_detector_v3.pkl'), }, 'strategy_name': 'prodgreen', 'vol_p60': 0.00009868, } def _env_upper(name: str, default: str = "") -> str: return str(os.environ.get(name, default)).strip().upper() def _env_bool(name: str, default: bool = False) -> bool: raw = str(os.environ.get(name, str(default))).strip().lower() return raw in ("1", "true", "yes", "on") def _resolve_bingx_environment(value: str | None = None) -> BingxEnvironment: name = str(value or os.environ.get("DOLPHIN_BINGX_ENV", "VST")).strip().upper() return BingxEnvironment.LIVE if name == "LIVE" else BingxEnvironment.VST def _resolve_bingx_allow_mainnet(value: str | None = None) -> bool: if isinstance(value, bool): return value raw = str(value or os.environ.get("DOLPHIN_BINGX_ALLOW_MAINNET", "0")).strip().lower() return raw in ("1", "true", "yes", "on") def _resolve_bingx_recv_window_ms(value: str | None = None) -> int: raw = str(value or os.environ.get("DOLPHIN_BINGX_RECV_WINDOW_MS", "")).strip() try: parsed = int(raw) return parsed if parsed > 0 else 5_000 except (TypeError, ValueError): return 5_000 def build_actor_config( *, data_venue: str | None = None, exec_venue: str | None = None, ) -> dict: actor_cfg = deepcopy(DOLPHIN_CONFIG) resolved_data_venue = (data_venue or _env_upper("DOLPHIN_DATA_VENUE", actor_cfg["data_venue"])).upper() resolved_exec_venue = (exec_venue or _env_upper("DOLPHIN_EXEC_VENUE", actor_cfg["exec_venue"])).upper() actor_cfg["data_venue"] = resolved_data_venue actor_cfg["exec_venue"] = resolved_exec_venue actor_cfg["venue"] = resolved_exec_venue actor_cfg["direction"] = os.environ.get("DOLPHIN_DIRECTION", actor_cfg.get("direction", "short_only")) return actor_cfg def build_bingx_exec_client_config( *, resolved_bingx_env: BingxEnvironment, resolved_bingx_allow_mainnet: bool, resolved_bingx_recv_window_ms: int | None, assets: list[str] | None = None, ) -> BingxExecClientConfig: from prod.bingx.config import BingxInstrumentProviderConfig default_leverage = int(os.environ.get("DOLPHIN_BINGX_DEFAULT_LEVERAGE", "1")) symbol_filters = tuple(assets) if assets else None return BingxExecClientConfig( api_key=os.environ.get("BINGX_API_KEY"), secret_key=os.environ.get("BINGX_SECRET_KEY"), environment=resolved_bingx_env, allow_mainnet=resolved_bingx_allow_mainnet, recv_window_ms=resolved_bingx_recv_window_ms if resolved_bingx_recv_window_ms is not None else 5_000, default_leverage=default_leverage, leverage_by_symbol={symbol: default_leverage for symbol in (assets or [])} if assets else None, prefer_websocket=_env_bool("DOLPHIN_BINGX_PREFER_WEBSOCKET", True), instrument_provider=BingxInstrumentProviderConfig( load_all=True, symbol_filters=symbol_filters, ), ) def build_node( *, data_venue: str | None = None, exec_venue: str | None = None, trader_id: str | None = None, bingx_environment: BingxEnvironment | None = None, bingx_allow_mainnet: bool | None = None, bingx_recv_window_ms: int | None = None, ) -> TradingNode: resolved_bingx_env = bingx_environment or _resolve_bingx_environment() resolved_bingx_allow_mainnet = ( bingx_allow_mainnet if bingx_allow_mainnet is not None else _resolve_bingx_allow_mainnet() ) resolved_bingx_recv_window_ms = ( bingx_recv_window_ms if bingx_recv_window_ms is not None else _resolve_bingx_recv_window_ms() ) if resolved_bingx_env is BingxEnvironment.LIVE and not resolved_bingx_allow_mainnet: raise RuntimeError( "BingX LIVE requested but DOLPHIN_BINGX_ALLOW_MAINNET is not enabled" ) actor_cfg = build_actor_config(data_venue=data_venue, exec_venue=exec_venue) actor_cfg["bingx_environment"] = str(resolved_bingx_env.value) resolved_data_venue = actor_cfg["data_venue"] resolved_exec_venue = actor_cfg["exec_venue"] data_clients = {} exec_clients = {} if resolved_data_venue == "BINANCE": api_key = os.environ["BINANCE_API_KEY"] api_secret = os.environ["BINANCE_API_SECRET"] data_clients["BINANCE"] = BinanceDataClientConfig( account_type=_BINANCE_USDT_FUTURES_ACCOUNT_TYPE, api_key=api_key, api_secret=api_secret, testnet=False, ) elif resolved_data_venue == "BINGX": from prod.bingx.config import BingxInstrumentProviderConfig data_clients["BINGX"] = BingxDataClientConfig( environment=resolved_bingx_env, allow_mainnet=resolved_bingx_allow_mainnet, instrument_provider=BingxInstrumentProviderConfig( load_all=True, symbol_filters=tuple(actor_cfg.get("assets", [])), ), ) else: raise ValueError(f"Unsupported data venue: {resolved_data_venue}") if resolved_exec_venue == "BINGX": exec_clients["BINGX"] = build_bingx_exec_client_config( resolved_bingx_env=resolved_bingx_env, resolved_bingx_allow_mainnet=resolved_bingx_allow_mainnet, resolved_bingx_recv_window_ms=resolved_bingx_recv_window_ms, assets=actor_cfg.get("assets"), ) trader_id_value = trader_id or os.environ.get("DOLPHIN_TRADER_ID", "DOLPHIN-LIVE-001") from nautilus_dolphin.nautilus.dolphin_actor import DolphinActor actor = DolphinActor(config=actor_cfg) node_config = TradingNodeConfig( trader_id=TraderId(trader_id_value), data_clients=data_clients, exec_clients=exec_clients if exec_clients else None, data_engine=LiveDataEngineConfig(time_bars_build_with_no_updates=False), cache=CacheConfig(database=None), ) node = TradingNode(config=node_config) if "BINANCE" in data_clients: node.add_data_client_factory("BINANCE", BinanceLiveDataClientFactory) if "BINGX" in data_clients: node.add_data_client_factory("BINGX", BingxLiveDataClientFactory) if "BINGX" in exec_clients: node.add_exec_client_factory("BINGX", BingxLiveExecClientFactory) node.trader.add_strategy(actor) node.build() return node async def run() -> None: node = build_node() await node.run_async() if __name__ == "__main__": asyncio.run(run())