""" Deploy orchestrator — unified CLI for backtest → paper → live pipeline. Commands: backtest --strategy [--fast|--full] [--interval 1h] paper --strategy [--duration 3600] live --strategy [--testnet|--mainnet] list List all registered strategies and backtest results. """ from __future__ import annotations import argparse import asyncio import json import logging import os import sys from datetime import datetime from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) logging.basicConfig(level=logging.INFO, format="%(asctime)s [deploy] %(message)s", datefmt="%H:%M:%S") logger = logging.getLogger("ftdt-deploy") RESULTS_DIR = Path(__file__).resolve().parent.parent / "backtests" / "results" RESULTS_DIR.mkdir(parents=True, exist_ok=True) STRATEGY_REGISTRY = { "pairs": { "name": "Pairs Trading", "description": "BTC/ETH ratio Z-score mean reversion", "class": "strategies.nt.pairs_trading_nt.PairsTradingNT", }, "hurst_vpin": { "name": "Hurst VPIN", "description": "Hurst exponent regime filter + VPIN flow imbalance", "class": "strategies.nt.hurst_vpin_nt.HurstVPINNT", }, "as_mm": { "name": "Avellaneda-Stoikov", "description": "Stochastic control market making with inventory risk", "class": "strategies.nt.as_mm_nt.ASMarketMakingNT", }, "obi": { "name": "Order Book Imbalance", "description": "L2 bid/ask volume skew reversal", "class": None, # Not yet ported }, "funding_arb": { "name": "Funding Rate Arb", "description": "Delta-neutral carry — collect funding payments", "class": None, }, "momentum": { "name": "Momentum Breakout", "description": "Bollinger band breakout on trending instruments", "class": None, }, "mean_rev": { "name": "Mean Reversion", "description": "VWAP deviation oscillator", "class": None, }, } class DeployOrchestrator: """Unified deployment pipeline.""" @staticmethod def cmd_backtest(args): from backtests.vbt_runner import VBTBacktestRunner from backtests.nt_runner import NTBacktestRunner from framework.instruments import HyperliquidInstrumentCatalog strategy_key = args.strategy strategy_info = STRATEGY_REGISTRY.get(strategy_key) if not strategy_info: print(f"Unknown strategy: {strategy_key}") print(f"Available: {list(STRATEGY_REGISTRY.keys())}") return # Quick VectorBT backtest if not args.nt_only: print(f"\n{'='*60}") print(f" VectorBT Backtest: {strategy_info['name']}") print(f"{'='*60}") runner = VBTBacktestRunner() result = runner.run_strategy( strategy=strategy_key, interval=args.interval, testnet=args.testnet, ) if result: _save_result(strategy_key, "vbt", result) # Full NautilusTrader backtest if not args.vbt_only: print(f"\n{'='*60}") print(f" NautilusTrader Backtest: {strategy_info['name']}") print(f"{'='*60}") catalog = HyperliquidInstrumentCatalog(testnet=args.testnet) runner = NTBacktestRunner() result = runner.run_backtest( strategy=strategy_key, interval=args.interval, instruments=catalog.load(), ) if result: _save_result(strategy_key, "nt", result) @staticmethod def cmd_paper(args): from framework.data import HyperliquidDataProvider from framework.execution import PaperExecutionProvider from framework.config import StrategyConfig strategy_key = args.strategy strategy_info = STRATEGY_REGISTRY.get(strategy_key) if not strategy_info: print(f"Unknown strategy: {strategy_key}") return print(f"\n{'='*60}") print(f" Paper Trading: {strategy_info['name']}") print(f" Duration: {args.duration}s | Mainnet data") print(f"{'='*60}") provider = HyperliquidDataProvider(testnet=False) execution = PaperExecutionProvider() # Determine coin from strategy coin_map = {"pairs": "ETH", "hurst_vpin": "BTC", "as_mm": "BTC", "obi": "BTC", "funding_arb": "BTC", "momentum": "ETH"} coin = args.coin or coin_map.get(strategy_key, "BTC") async def _run(): start = asyncio.get_event_loop().time() while asyncio.get_event_loop().time() - start < args.duration: try: prices = provider.fetch_mark_prices() mark = prices.get(coin, 0) if mark > 0: # Simulate a signal check each tick _tick(strategy_key, coin, mark, provider, execution) await asyncio.sleep(1) except Exception as e: logger.warning("Paper loop error: %s", e) await asyncio.sleep(5) asyncio.run(_run()) @staticmethod def cmd_live(args): from framework.execution import HyperliquidExecutionProvider strategy_key = args.strategy strategy_info = STRATEGY_REGISTRY.get(strategy_key) if not strategy_info: print(f"Unknown strategy: {strategy_key}") return use_testnet = not args.mainnet env = "testnet" if use_testnet else "mainnet" private_key = os.environ.get(f"HYPERLIQUID_{env.upper()}_PK") if not private_key: env_file = Path(__file__).resolve().parent.parent / ".env" if env_file.exists(): for line in env_file.read_text().splitlines(): key = f"HYPERLIQUID_{env.upper()}_PK" if line.startswith(f"{key}="): private_key = line.split("=", 1)[1].strip() break if not private_key: print(f"ERROR: HYPERLIQUID_{env.upper()}_PK not set in .env or environment") return if not use_testnet: resp = input(f"\n⚠️ LIVE MAINNET for {strategy_key}. Confirm? (yes/no): ") if resp.lower() != "yes": print("Aborted.") return provider = HyperliquidExecutionProvider(private_key=private_key, testnet=use_testnet) print(f"\n{'='*60}") print(f" LIVE {env.upper()}: {strategy_info['name']}") print(f" Wallet: {provider.address}") print(f"{'='*60}") # Cancel existing orders provider.cancel_all() print("Run with Ctrl+C to stop. Existing node.py/paper_trader.py unaffected.") print("This is a standalone execution — for prod monitoring use the existing live node.") @staticmethod def cmd_list(args): print(f"\n{'='*60}") print(" Registered Strategies") print(f"{'='*60}") for key, info in STRATEGY_REGISTRY.items(): ported = "✅" if info["class"] else "⏳" print(f" {ported} {key:15s} {info['name']:30s} {info['description']}") print() # List backtest results results = sorted(RESULTS_DIR.glob("*.json"), key=os.path.getmtime, reverse=True) if results: print(f"{'='*60}") print(" Backtest Results") print(f"{'='*60}") for r in results[:10]: mtime = datetime.fromtimestamp(os.path.getmtime(r)).strftime("%Y-%m-%d %H:%M") size_kb = os.path.getsize(r) / 1024 print(f" {r.name:50s} {size_kb:6.1f}KB {mtime}") if len(results) > 10: print(f" ... and {len(results) - 10} more") def _save_result(strategy_key: str, engine: str, result: dict): ts = datetime.now().strftime("%Y%m%d-%H%M%S") path = RESULTS_DIR / f"{strategy_key}_{engine}_{ts}.json" with open(path, "w") as f: json.dump(result, f, indent=2, default=str) print(f" Saved: {path.name}") if "sharpe" in result: print(f" Sharpe: {result['sharpe']:.2f} | DD: {result.get('max_drawdown_pct', 0):.1f}% | Win: {result.get('win_rate', 0):.0%}") def _tick(strategy_key: str, coin: str, mark: float, provider, execution): """Single tick of paper trading logic — placeholder for full strategy logic.""" # Load strategy module dynamically strategy_class_path = STRATEGY_REGISTRY.get(strategy_key, {}).get("class") if not strategy_class_path: return module_path, class_name = strategy_class_path.rsplit(".", 1) import importlib try: mod = importlib.import_module(module_path) strategy_cls = getattr(mod, class_name) # Instantiate if not already cached if not hasattr(_tick, "_instances"): _tick._instances = {} if strategy_key not in _tick._instances: from framework.config import StrategyConfig cfg = StrategyConfig( name=STRATEGY_REGISTRY[strategy_key]["name"], instrument=f"{coin}-USD-PERP", asset=coin, allocation=10000.0, order_size=0.001, testnet=False, # paper uses mainnet data ) _tick._instances[strategy_key] = strategy_cls(cfg) strat = _tick._instances[strategy_key] sig = strat.compute_signal(price=mark) if sig: # Paper execution from framework.execution import PaperExecutionProvider as Pep pep = Pep() cloid = pep.submit( coin=coin, side="BUY" if "BUY" in sig.get("signal", "").upper() else "SELL", size=cfg.order_size, price=mark, fee_model=cfg.fee_model, mark_price=mark, ) logger.info("Paper signal: %s → %s | fill=%s", sig["signal"], cloid, mark) except Exception as e: logger.warning("Tick error for %s: %s", strategy_key, e) def main(): parser = argparse.ArgumentParser(description="FTDT Quant Lab — Deploy Orchestrator") sub = parser.add_subparsers(dest="command", help="Command") # backtest bt = sub.add_parser("backtest", help="Run backtest (VectorBT + NautilusTrader)") bt.add_argument("--strategy", "-s", required=True, help="Strategy key (pairs, hurst_vpin, as_mm, etc.)") bt.add_argument("--fast", dest="vbt_only", action="store_true", help="VectorBT quick backtest only") bt.add_argument("--full", dest="nt_only", action="store_true", help="NautilusTrader full backtest only") bt.add_argument("--interval", default="1h", help="Candle interval (1m, 5m, 15m, 1h, 4h, 1d)") bt.add_argument("--testnet", action="store_true", default=False, help="Use testnet data") # paper pp = sub.add_parser("paper", help="Run paper trading simulation") pp.add_argument("--strategy", "-s", required=True, help="Strategy key") pp.add_argument("--duration", type=int, default=3600, help="Duration in seconds (default: 3600)") pp.add_argument("--coin", help="Override trading coin (default: strategy default)") # live ll = sub.add_parser("live", help="Run live trading") ll.add_argument("--strategy", "-s", required=True, help="Strategy key") ll.add_argument("--testnet", action="store_true", default=True, help="Use testnet (default)") ll.add_argument("--mainnet", action="store_true", help="Use mainnet") # list sub.add_parser("list", help="List registered strategies and results") args = parser.parse_args() if not args.command: parser.print_help() return orch = DeployOrchestrator() getattr(orch, f"cmd_{args.command}")(args) if __name__ == "__main__": main()