"""DOLPHIN Paper Trading — Prefect Flow. Runs daily at 00:05 UTC. Processes yesterday's live scan data through the NDAlphaEngine champion stack. Logs virtual P&L to disk + Hazelcast. Blue deployment: champion SHORT (configs/blue.yml) Green deployment: bidirectional SHORT+LONG (configs/green.yml) [pending LONG validation] Usage: # Register flows (run once, after Prefect server is up): PREFECT_API_URL=http://localhost:4200/api python paper_trade_flow.py --register # Run manually for a specific date: PREFECT_API_URL=http://localhost:4200/api python paper_trade_flow.py \\ --date 2026-02-25 --config configs/blue.yml """ import sys, json, yaml, logging, argparse, csv, urllib.request from pathlib import Path from datetime import datetime, timedelta, date, timezone import numpy as np import pandas as pd HCM_DIR = Path(__file__).parent.parent sys.path.insert(0, str(HCM_DIR / 'nautilus_dolphin')) from prefect import flow, task, get_run_logger from prefect.schedules import Cron import hazelcast logging.basicConfig(level=logging.WARNING) # suppress Prefect noise below WARNING # ── Paths ─────────────────────────────────────────────────────────────────────── from dolphin_paths import get_eigenvalues_path, get_klines_dir SCANS_DIR = get_eigenvalues_path() # platform-aware: Win → NG3 dir, Linux → /mnt/ng6_data/eigenvalues KLINES_DIR = get_klines_dir() # vbt_cache_klines/ — NG5 parquet source (preferred) MC_MODELS_DIR = str(HCM_DIR / 'nautilus_dolphin' / 'mc_results' / 'models') # Columns that are eigenvalue metadata, not asset prices META_COLS = { 'timestamp', 'scan_number', 'v50_lambda_max_velocity', 'v150_lambda_max_velocity', 'v300_lambda_max_velocity', 'v750_lambda_max_velocity', 'vel_div', 'instability_50', 'instability_150', } HZ_HOST = "localhost:5701" HZ_CLUSTER = "dolphin" # Number of historical eigenvalue dates to use for ACB w750 threshold calibration ACB_HISTORY_DAYS = 60 # ── Helpers ────────────────────────────────────────────────────────────────────── def _get_recent_scan_dates(n: int) -> list: """Return sorted list of up to n most-recent eigenvalue date dirs.""" try: dirs = sorted( d.name for d in SCANS_DIR.iterdir() if d.is_dir() and len(d.name) == 10 and d.name.startswith('20') ) return dirs[-n:] except Exception: return [] def _fetch_btcusdt_klines_fallback(date_str: str) -> "dict[str, float]": """Fetch BTCUSDT 1m klines from Binance futures for date_str. Returns a dict mapping ISO timestamp strings (minute precision) → close price. Falls back to empty dict on any error (caller handles missing prices gracefully). """ try: from datetime import timezone as tz day_start = datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=tz.utc) day_end = day_start + timedelta(days=1) start_ms = int(day_start.timestamp() * 1000) end_ms = int(day_end.timestamp() * 1000) prices: dict[str, float] = {} # Binance returns max 1500 bars per request; 1440 bars/day fits in one call url = ( f"https://fapi.binance.com/fapi/v1/klines" f"?symbol=BTCUSDT&interval=1m" f"&startTime={start_ms}&endTime={end_ms}&limit=1500" ) with urllib.request.urlopen(url, timeout=10) as resp: bars = json.loads(resp.read()) for bar in bars: ts_ms = int(bar[0]) close = float(bar[4]) ts_iso = datetime.fromtimestamp(ts_ms / 1000, tz=tz.utc).strftime("%Y-%m-%dT%H:%M") prices[ts_iso] = close return prices except Exception: return {} def _load_scan_df_from_json(date_str: str) -> pd.DataFrame: """Load daily scan JSON files → DataFrame with vel_div + asset prices. Each scan JSON has: - windows: {50, 150, 300, 750} → tracking_data.lambda_max_velocity - pricing_data.current_prices → per-asset USD prices Returns one row per scan, sorted by scan_number. When current_prices is empty (scanner bug), falls back to Binance 1m klines. """ scan_dir = SCANS_DIR / date_str if not scan_dir.exists(): return pd.DataFrame() json_files = sorted(scan_dir.glob("scan_*.json"), key=lambda f: f.name) # Exclude the _Indicators.json companion files if any json_files = [f for f in json_files if '__Indicators' not in f.name] if not json_files: return pd.DataFrame() total = len(json_files) rows = [] for i, jf in enumerate(json_files): if i % 500 == 0 and i > 0: print(f" [scan load] {i}/{total} files...", flush=True) try: with open(jf, 'r', encoding='utf-8') as fh: data = json.load(fh) row = { 'scan_number': data.get('scan_number', 0), 'timestamp': data.get('timestamp', ''), } # Eigenvalue velocity per window windows = data.get('windows', {}) for w_key, vel_col, inst_col in [ ('50', 'v50_lambda_max_velocity', 'instability_50'), ('150', 'v150_lambda_max_velocity', 'instability_150'), ('300', 'v300_lambda_max_velocity', None), ('750', 'v750_lambda_max_velocity', None), ]: w_data = windows.get(str(w_key), {}) td = w_data.get('tracking_data', {}) vel = td.get('lambda_max_velocity') row[vel_col] = float(vel) if vel is not None else np.nan if inst_col is not None: rs = w_data.get('regime_signals', {}) row[inst_col] = float(rs.get('instability_score', 0.0) or 0.0) # vel_div = w50_vel − w150_vel (the primary alpha signal) v50 = row.get('v50_lambda_max_velocity', np.nan) v150 = row.get('v150_lambda_max_velocity', np.nan) row['vel_div'] = (v50 - v150) if (np.isfinite(v50) and np.isfinite(v150)) else np.nan # Asset prices pricing = data.get('pricing_data', {}) prices = pricing.get('current_prices', {}) row.update({sym: float(px) for sym, px in prices.items() if px is not None}) rows.append(row) except Exception: continue if not rows: return pd.DataFrame() df = pd.DataFrame(rows).sort_values('scan_number').reset_index(drop=True) # Fallback: if BTCUSDT prices are missing (scanner current_prices bug), # fetch 1m klines from Binance and fill by timestamp. if 'BTCUSDT' not in df.columns or df['BTCUSDT'].isna().all(): btc_klines = _fetch_btcusdt_klines_fallback(date_str) if btc_klines: def _lookup_btc(ts_str: str) -> float: # ts_str is ISO format from scan JSON; match to minute precision ts_min = str(ts_str)[:16] # "YYYY-MM-DDTHH:MM" return btc_klines.get(ts_min, np.nan) df['BTCUSDT'] = df['timestamp'].apply(_lookup_btc) # Forward-fill any gaps (scan timestamps between 1m bars) df['BTCUSDT'] = df['BTCUSDT'].ffill().bfill() return df def _load_scan_df_from_parquet(date_str: str) -> pd.DataFrame: """Load daily scan data from vbt_cache_klines/ parquet (NG5+ preferred path). Returns DataFrame with vel_div + asset prices, or empty DataFrame if not available. """ parq_path = KLINES_DIR / f"{date_str}.parquet" if not parq_path.exists(): return pd.DataFrame() try: df = pd.read_parquet(parq_path) if df.empty or 'vel_div' not in df.columns: return pd.DataFrame() return df.reset_index(drop=True) except Exception: return pd.DataFrame() # ── Tasks ─────────────────────────────────────────────────────────────────────── @task(name="load_config", retries=0) def load_config(config_path: str) -> dict: with open(config_path) as f: return yaml.safe_load(f) @task(name="load_day_scans", retries=2, retry_delay_seconds=10) def load_day_scans(date_str: str) -> pd.DataFrame: """Load scan data for one date → VBT-compatible DataFrame. Prefers vbt_cache_klines/ parquet (NG5 native, single file, fast). Falls back to eigenvalues/ JSON (NG3 legacy) if parquet unavailable. Extracts vel_div, eigenvalue features, and asset prices. """ log = get_run_logger() # ── Parquet path (NG5 native — preferred) ────────────────────────────────── df = _load_scan_df_from_parquet(date_str) if not df.empty: log.info(f" [parquet] Loaded {len(df)} scans for {date_str} | cols={len(df.columns)}") else: # ── JSON fallback (NG3 legacy) ───────────────────────────────────────── df = _load_scan_df_from_json(date_str) if df.empty: log.warning(f"No usable scan data for {date_str} in {SCANS_DIR}") return pd.DataFrame() log.info(f" [json] Loaded {len(df)} scans for {date_str} | cols={len(df.columns)}") if df.empty: log.warning(f"No usable scan data for {date_str} in {SCANS_DIR}") return pd.DataFrame() # Drop rows with NaN vel_div (warmup period at start of day) valid = df['vel_div'].notna() n_dropped = (~valid).sum() df = df[valid].reset_index(drop=True) # Verify BTCUSDT prices present (required for vol gate and DC) if 'BTCUSDT' not in df.columns: log.error(f"BTCUSDT prices missing from scan data on {date_str} — cannot run engine") return pd.DataFrame() log.info(f" Loaded {len(df)} scans for {date_str} | cols={len(df.columns)} | " f"vel_div range=[{df['vel_div'].min():.4f}, {df['vel_div'].max():.4f}] " f"| {n_dropped} warmup rows dropped") return df @task(name="run_engine_day", retries=0, persist_result=False) def run_engine_day(date_str: str, df: pd.DataFrame, engine, vol_p60: float, posture: str = 'APEX', direction: int = -1) -> dict: """Run one day through NDAlphaEngine. Returns daily stats dict.""" log = get_run_logger() if df.empty or posture == 'HIBERNATE': log.warning(f"Empty DataFrame or HIBERNATE for {date_str} — skipping.") return {'date': date_str, 'pnl': 0.0, 'capital': engine.capital, 'trades': 0, 'boost': 1.0, 'beta': 0.0, 'mc_status': 'NO_DATA', 'posture': posture} asset_cols = [c for c in df.columns if c not in META_COLS] # Vol gate: rolling 50-bar std of BTC returns bp = df['BTCUSDT'].values dvol = np.full(len(df), np.nan) for i in range(50, len(bp)): seg = bp[max(0, i - 50):i] if len(seg) >= 10 and seg[0] > 0: dvol[i] = float(np.std(np.diff(seg) / seg[:-1])) vol_ok = np.where(np.isfinite(dvol), dvol > vol_p60, False) # 1. Setup day engine.begin_day(date_str, posture=posture, direction=direction) # 2. Bar stream (replaces batch process_day) for ri in range(len(df)): row = df.iloc[ri] vd = row.get('vel_div') if vd is None or not np.isfinite(float(vd)): engine._global_bar_idx += 1 continue v50_raw = row.get('v50_lambda_max_velocity') v750_raw = row.get('v750_lambda_max_velocity') v50_val = float(v50_raw) if (v50_raw is not None and np.isfinite(float(v50_raw))) else 0.0 v750_val = float(v750_raw) if (v750_raw is not None and np.isfinite(float(v750_raw))) else 0.0 prices = {} for ac in asset_cols: p = row.get(ac) if p is not None and p > 0 and np.isfinite(p): prices[ac] = float(p) if not prices: engine._global_bar_idx += 1 continue # OB live step: fetch HZ snapshots and compute features BEFORE step_bar(). # This populates the live caches so get_placement/get_signal/get_market() # return real OBF values instead of NEUTRAL defaults. if engine.ob_engine is not None: try: engine.ob_engine.step_live(list(prices.keys()), ri) except Exception: pass # OBF degraded → NEUTRAL values, trading continues engine.step_bar( bar_idx=ri, vel_div=float(vd), prices=prices, vol_regime_ok=bool(vol_ok[ri]), v50_vel=v50_val, v750_vel=v750_val, ) # 3. Finalize day result = engine.end_day() result['posture'] = posture log.info(f" {date_str}: PnL={result.get('pnl', 0):+.2f} " f"T={result.get('trades', 0)} boost={result.get('boost', 1.0):.2f}x " f"MC={result.get('mc_status', '?')} Posture={posture}") return result @task(name="write_hz_state", retries=3, retry_delay_seconds=5, persist_result=False) def write_hz_state(hz_host: str, hz_cluster: str, imap_name: str, key: str, value: dict): """Write state dict to Hazelcast IMap. Creates own client per call (serialization-safe).""" client = hazelcast.HazelcastClient(cluster_name=hz_cluster, cluster_members=[hz_host]) try: client.get_map(imap_name).blocking().put(key, json.dumps(value)) finally: client.shutdown() @task(name="log_pnl", retries=0, persist_result=False) def log_pnl(log_dir: Path, date_str: str, result: dict, capital: float): log_dir.mkdir(parents=True, exist_ok=True) row = {**result, 'date': date_str, 'capital': capital, 'logged_at': datetime.now(timezone.utc).isoformat()} log_file = log_dir / f"paper_pnl_{date_str[:7]}.jsonl" with open(log_file, 'a') as f: f.write(json.dumps(row) + '\n') # ── Flow ──────────────────────────────────────────────────────────────────────── @flow(name="dolphin-paper-trade", log_prints=True) def dolphin_paper_trade_flow(config_path: str = "configs/blue.yml", run_date: str = None, instrument: bool = False): """Daily paper trading flow. Processes one day of live eigenvalue scans. Scheduled at 00:05 UTC — processes yesterday's data. Run manually with run_date='YYYY-MM-DD' for backtesting or debugging. """ log = get_run_logger() cfg = load_config(config_path) strategy_name = cfg['strategy_name'] eng_cfg = cfg['engine'] pt_cfg = cfg['paper_trade'] hz_cfg = cfg['hazelcast'] target_date = run_date or (date.today() - timedelta(days=1)).isoformat() log.info(f"=== {strategy_name.upper()} paper trade: {target_date} ===") # ── Lazy imports (numba JIT happens here) ────────────────────────────────── from nautilus_dolphin.nautilus.proxy_boost_engine import create_d_liq_engine from nautilus_dolphin.nautilus.adaptive_circuit_breaker import AdaptiveCircuitBreaker from mc.mc_ml import DolphinForewarner from nautilus_dolphin.nautilus.ob_features import OBFeatureEngine from nautilus_dolphin.nautilus.hz_ob_provider import HZOBProvider client = hazelcast.HazelcastClient(cluster_name=HZ_CLUSTER, cluster_members=[HZ_HOST]) imap_state = client.get_map(hz_cfg['imap_state']).blocking() # ---- Restore capital ---- STATE_KEY = f"state_{strategy_name}_{target_date}" restored_capital = pt_cfg['initial_capital'] peak_capital = pt_cfg['initial_capital'] stored_state = {} engine_state = None try: raw = imap_state.get(STATE_KEY) or imap_state.get('latest') or '{}' stored_state = json.loads(raw) if stored_state.get('strategy') == strategy_name and stored_state.get('capital', 0) > 0: restored_capital = float(stored_state['capital']) peak_capital = float(stored_state.get('peak_capital', restored_capital)) engine_state = stored_state.get('engine_state') log.info(f"[STATE] Restored capital={restored_capital:.2f} from HZ") except Exception as e: log.warning(f"[STATE] HZ restore failed: {e} — using config capital") # ── Engine — D_LIQ_GOLD config (8x/9x LiquidationGuardEngine) ─────────── # create_d_liq_engine() overrides max_leverage→8.0 / abs_max_leverage→9.0 # internally (D_LIQ_SOFT_CAP / D_LIQ_ABS_CAP constants), regardless of what # eng_cfg says. This is the certified gold leverage stack. engine = create_d_liq_engine( initial_capital = restored_capital, vel_div_threshold = eng_cfg['vel_div_threshold'], vel_div_extreme = eng_cfg['vel_div_extreme'], min_leverage = eng_cfg['min_leverage'], max_leverage = eng_cfg.get('max_leverage', 8.0), abs_max_leverage = eng_cfg.get('abs_max_leverage', 9.0), leverage_convexity = eng_cfg['leverage_convexity'], fraction = eng_cfg['fraction'], fixed_tp_pct = eng_cfg['fixed_tp_pct'], stop_pct = eng_cfg['stop_pct'], max_hold_bars = eng_cfg['max_hold_bars'], use_direction_confirm= eng_cfg['use_direction_confirm'], dc_lookback_bars = eng_cfg['dc_lookback_bars'], dc_min_magnitude_bps = eng_cfg['dc_min_magnitude_bps'], dc_skip_contradicts = eng_cfg['dc_skip_contradicts'], dc_leverage_boost = eng_cfg['dc_leverage_boost'], dc_leverage_reduce = eng_cfg['dc_leverage_reduce'], use_asset_selection = eng_cfg['use_asset_selection'], min_irp_alignment = eng_cfg['min_irp_alignment'], use_sp_fees = eng_cfg['use_sp_fees'], use_sp_slippage = eng_cfg['use_sp_slippage'], sp_maker_entry_rate = eng_cfg['sp_maker_entry_rate'], sp_maker_exit_rate = eng_cfg['sp_maker_exit_rate'], use_ob_edge = eng_cfg['use_ob_edge'], ob_edge_bps = eng_cfg['ob_edge_bps'], ob_confirm_rate = eng_cfg['ob_confirm_rate'], lookback = eng_cfg['lookback'], use_alpha_layers = eng_cfg['use_alpha_layers'], use_dynamic_leverage = eng_cfg['use_dynamic_leverage'], seed = eng_cfg.get('seed', 42), ) engine.set_esoteric_hazard_multiplier(0.0) # gold spec: hazard=0 → base_max_leverage=8.0 if engine_state: try: engine.restore_state(engine_state) log.info("[STATE] Restored full engine state (including open positions)") except Exception as e: log.error(f"[STATE] Failed to restore engine state: {e}") # ── ACB — preload w750 from recent history for valid p60 threshold ───────── # w750 calibration always uses NPZ history (threshold is a population statistic). # Daily ExF factors are sourced from HZ exf_latest (pre-lagged) when available; # fall back to NPZ disk scan if HZ data is absent or stale (>12 h). acb = AdaptiveCircuitBreaker() acb.config.EIGENVALUES_PATH = SCANS_DIR # CRITICAL: override Windows default for Linux recent_dates = _get_recent_scan_dates(ACB_HISTORY_DAYS) if target_date not in recent_dates: recent_dates = (recent_dates + [target_date])[-ACB_HISTORY_DAYS:] acb.preload_w750(recent_dates) log.info(f" ACB preloaded {len(recent_dates)} dates | w750_threshold={acb._w750_threshold:.6f}") # ── ACB HZ warm-up: pre-load today's boost from live exf_latest ──────────── # The ExF service applies per-indicator lag BEFORE pushing to HZ, so the # values in exf_latest are already delay-adjusted. Do NOT re-lag them. _acb_hz_ok = False try: features_map = client.get_map('DOLPHIN_FEATURES').blocking() exf_raw = features_map.get('exf_latest') if exf_raw: exf_snapshot = json.loads(exf_raw) # Live w750 from latest scan (may be absent during warmup) scan_raw = features_map.get('latest_eigen_scan') w750_live: float | None = None if scan_raw: scan_data = json.loads(scan_raw) w750_live = scan_data.get('w750_velocity') boost_info = acb.get_dynamic_boost_from_hz( target_date, exf_snapshot, w750_velocity=w750_live ) stale_s = boost_info.get('max_staleness_s', 0) log.info( f" ACB HZ: boost={boost_info['boost']:.4f} beta={boost_info['beta']:.2f} " f"signals={boost_info['signals']:.1f} staleness={stale_s:.0f}s" ) _acb_hz_ok = True else: log.warning(" ACB HZ: exf_latest not found — falling back to NPZ disk scan") except ValueError as _ve: log.warning(f" ACB HZ: snapshot stale ({_ve}) — falling back to NPZ disk scan") except Exception as _e: log.warning(f" ACB HZ: read failed ({_e}) — falling back to NPZ disk scan") if not _acb_hz_ok: # NPZ fallback: get_dynamic_boost_for_date() will read eigenvalues/ on demand log.info(f" ACB: using NPZ disk path for {target_date}") # ── Data Loading & Live OB Integration ──────────────────────────────────── df = load_day_scans(target_date) OB_ASSETS = [c for c in df.columns if c not in META_COLS] if not df.empty else ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"] live_ob = HZOBProvider(hz_cluster=HZ_CLUSTER, hz_host=HZ_HOST) ob_eng = OBFeatureEngine(live_ob) # NOTE: HZOBProvider has no historical snapshots, so preload_date() is a no-op. # OB features are fetched live via ob_eng.step_live() before each step_bar() call. # ── MC-Forewarner ────────────────────────────────────────────────────────── forewarner = DolphinForewarner(models_dir=MC_MODELS_DIR) mc_base_cfg = { 'trial_id': 0, 'vel_div_threshold': -0.020, 'vel_div_extreme': -0.050, 'use_direction_confirm': True, 'dc_lookback_bars': 7, 'dc_min_magnitude_bps': 0.75, 'dc_skip_contradicts': True, 'dc_leverage_boost': 1.00, 'dc_leverage_reduce': 0.50, 'vd_trend_lookback': 10, 'min_leverage': 0.50, 'max_leverage': 5.00, 'leverage_convexity': 3.00, 'fraction': 0.20, 'use_alpha_layers': True, 'use_dynamic_leverage': True, 'fixed_tp_pct': 0.0095, 'stop_pct': 1.00, 'max_hold_bars': 120, 'use_sp_fees': True, 'use_sp_slippage': True, 'sp_maker_entry_rate': 0.62, 'sp_maker_exit_rate': 0.50, 'use_ob_edge': True, 'ob_edge_bps': 5.00, 'ob_confirm_rate': 0.40, 'ob_imbalance_bias': -0.09, 'ob_depth_scale': 1.00, 'use_asset_selection': True, 'min_irp_alignment': 0.45, 'lookback': 100, 'acb_beta_high': 0.80, 'acb_beta_low': 0.20, 'acb_w750_threshold_pct': 60, } engine.set_ob_engine(ob_eng) engine.set_acb(acb) engine.set_mc_forewarner(forewarner, mc_base_cfg) engine.set_esoteric_hazard_multiplier(0.0) if instrument: engine._bar_log_enabled = True # vol_p60: 60th percentile of rolling 50-bar BTC return std # Calibrated from 55-day NG3 champion window (Dec31–Feb25). # TODO: compute adaptively from rolling scan history (Phase MIG3) vol_p60 = pt_cfg.get('vol_p60', 0.000099) # ── Run ──────────────────────────────────────────────────────────────────── # df was already loaded above to define OB_ASSETS # ── DOLPHIN_SAFETY (MIG3) ────────────────────────────────────────────────── try: safety_ref = client.get_cp_subsystem().get_atomic_reference('DOLPHIN_SAFETY').blocking() safety_raw = safety_ref.get() except Exception: safety_ref = client.get_map('DOLPHIN_SAFETY').blocking() safety_raw = safety_ref.get('latest') safety_state = json.loads(safety_raw) if safety_raw else {} posture = safety_state.get('posture', 'APEX') Rm = safety_state.get('Rm', 1.0) log.info(f"[SURVIVAL STACK] Posture={posture} | Rm={Rm:.3f}") # Apply Rm to absolute max leverage effective_max_lev = engine.abs_max_leverage * Rm engine.abs_max_leverage = max(1.0, effective_max_lev) if posture == 'STALKER': engine.abs_max_leverage = min(engine.abs_max_leverage, 2.0) dir_str = cfg.get('direction', 'short_only') direction_val = 1 if dir_str in ['long', 'long_only'] else -1 result = run_engine_day(target_date, df, engine, vol_p60, posture=posture, direction=direction_val) result['strategy'] = strategy_name result['capital'] = engine.capital # ── Hazelcast & State Persistence ────────────────────────────────────────── try: # PnL write imap_pnl = client.get_map(hz_cfg['imap_pnl']).blocking() imap_pnl.put(target_date, json.dumps(result)) # State persist write new_peak = max(engine.capital, peak_capital) new_drawdown = 1.0 - (engine.capital / new_peak) if new_peak > 0 else 0.0 new_state = { 'strategy': strategy_name, 'capital': engine.capital, 'date': target_date, 'pnl': result.get('pnl', 0.0), 'trades': result.get('trades', 0), 'peak_capital': new_peak, 'drawdown': new_drawdown, 'last_date': target_date, 'updated_at': datetime.now(timezone.utc).isoformat(), 'engine_state': engine.get_state(), } imap_state.put('latest', json.dumps(new_state)) imap_state.put(STATE_KEY, json.dumps(new_state)) log.info(f" HZ write OK → state & {hz_cfg['imap_pnl']}[{target_date}]") except Exception as e: log.error(f"[STATE] HZ persist failed: {e}") # Fallback: write to local JSON ledger ledger_dir = Path(__file__).parent / pt_cfg.get('log_dir', 'paper_logs') ledger_dir.mkdir(parents=True, exist_ok=True) ledger_path = ledger_dir / f"state_ledger_{strategy_name}.jsonl" with open(ledger_path, 'a') as f: f.write(json.dumps({ 'strategy': strategy_name, 'capital': engine.capital, 'date': target_date, 'pnl': result.get('pnl', 0.0), 'trades': result.get('trades', 0), 'peak_capital': peak_capital, 'drawdown': 1.0 - engine.capital / max(engine.capital, peak_capital) if max(engine.capital, peak_capital) > 0 else 0.0 }) + '\n') finally: live_ob.close() client.shutdown() # ── Instrumentation (--instrument flag) ──────────────────────────────────── if instrument: instr_dir = Path(__file__).parent / pt_cfg['log_dir'] instr_dir.mkdir(parents=True, exist_ok=True) trades_instr_path = instr_dir / f"E2E_trades_{target_date}.csv" with open(trades_instr_path, 'w', newline='') as f: cw = csv.writer(f) cw.writerow(['trade_id', 'asset', 'direction', 'entry_price', 'exit_price', 'entry_bar', 'exit_bar', 'bars_held', 'leverage', 'notional', 'pnl_pct', 'pnl_absolute', 'exit_reason', 'bucket_idx']) for t in engine.trade_history: cw.writerow([t.trade_id, t.asset, t.direction, f"{t.entry_price:.6f}", f"{t.exit_price:.6f}", t.entry_bar, t.exit_bar, t.bars_held, f"{t.leverage:.4f}", f"{t.notional:.4f}", f"{t.pnl_pct:.8f}", f"{t.pnl_absolute:.4f}", t.exit_reason, t.bucket_idx]) bars_instr_path = instr_dir / f"E2E_bars_{target_date}.csv" with open(bars_instr_path, 'w', newline='') as f: cw = csv.writer(f) cw.writerow(['date', 'bar_idx', 'vel_div', 'vol_ok', 'posture', 'regime_size_mult', 'position_open', 'boost', 'beta']) for b in engine._bar_log: cw.writerow([target_date, b['bar_idx'], f"{b['vel_div']:.8f}", b['vol_ok'], b['posture'], f"{b['regime_size_mult']:.6f}", b['position_open'], f"{b['boost']:.4f}", f"{b['beta']:.2f}"]) log.info(f" Instrumentation → {trades_instr_path.name} ({len(engine.trade_history)} trades), " f"{bars_instr_path.name} ({len(engine._bar_log)} bars)") # ── Disk log ─────────────────────────────────────────────────────────────── log_pnl(Path(__file__).parent / pt_cfg['log_dir'], target_date, result, engine.capital) log.info(f"=== DONE: {strategy_name} {target_date} | " f"PnL={result.get('pnl', 0):+.2f} | Capital={engine.capital:,.2f} ===") return result # ── CLI entry point ────────────────────────────────────────────────────────────── if __name__ == '__main__': parser = argparse.ArgumentParser(description='DOLPHIN paper trading flow') parser.add_argument('--config', default='configs/blue.yml', help='Strategy config YAML') parser.add_argument('--date', default=None, help='YYYY-MM-DD (default: yesterday)') parser.add_argument('--register', action='store_true', help='Register Prefect deployments') parser.add_argument('--instrument', action='store_true', help='Write per-trade + bar CSVs to log_dir') args = parser.parse_args() if args.register: from prefect.client.schemas.schedules import CronSchedule as CS for color, cfg_path in [('blue', 'configs/blue.yml'), ('green', 'configs/green.yml')]: abs_cfg = str(Path(__file__).parent / cfg_path) deployment = dolphin_paper_trade_flow.to_deployment( name=f"dolphin-paper-{color}", parameters={"config_path": abs_cfg}, schedule=CS(cron="5 0 * * *", timezone="UTC"), work_pool_name="dolphin", tags=[color, "paper-trade", "dolphin"], ) deployment.apply() print(f"Registered: dolphin-paper-{color}") else: import os os.environ.setdefault('PREFECT_API_URL', 'http://localhost:4200/api') dolphin_paper_trade_flow(config_path=args.config, run_date=args.date, instrument=args.instrument)