297 lines
11 KiB
Python
297 lines
11 KiB
Python
#!/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())
|