mirror of
https://github.com/BrentNeale1/fx-quant.git
synced 2026-08-13 18:28:04 +00:00
Phase 2: Live paper trading engine + extended backtesting analytics
Track A — Live paper trading system: - Extract PositionManager from backtester into shared src/position_manager.py - Refactor backtester/engine.py to delegate to PositionManager - New src/live/ package: data_feed (OANDA polling), executor (paper/live orders), engine (LiveEngine orchestrator with 5 strategy slots), run.py entry point - Add phase2 config to system.yaml (S7_Tight, S9, S9_Filtered, S4F, S3) Track B — Extended backtesting analytics: - Regime analysis: per-year (2021-2023) breakdown shows 4/5 strategies trending UP - Correlation analysis: S7+S3 GBP_JPY overlap=16.9% (moderate), S9 pairs=12% (low) - Kelly sizing: S9_Filtered half-Kelly=7.3%, S4F=2.4%, S3=1.6% with Monte Carlo DD Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
39a6536284
commit
072ac0f245
+150
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
Entry point for the Phase 2 live paper trading engine.
|
||||
|
||||
Usage:
|
||||
python src/live/run.py # continuous loop
|
||||
python src/live/run.py --once # single iteration (for testing)
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
# Ensure project root on path
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
from src.position_manager import PositionManager
|
||||
from src.live.data_feed import OandaDataFeed
|
||||
from src.live.executor import OandaExecutor
|
||||
from src.live.engine import LiveEngine, StrategySlot
|
||||
|
||||
# Strategy imports
|
||||
from src.strategies_pkg.s7_liquidity_sweep import S7_Liquidity_Sweep
|
||||
from src.strategies_pkg.s9_london_session import S9_London_Session
|
||||
from src.strategies_pkg.s4f_ema_ribbon import S4F_EMA_Ribbon
|
||||
from src.strategies_pkg.s3_key_level_breakout import S3_KeyLevel_Breakout
|
||||
|
||||
|
||||
def load_config() -> dict:
|
||||
"""Load Phase 2 config from system.yaml."""
|
||||
cfg_path = os.path.join(ROOT, "config", "system.yaml")
|
||||
with open(cfg_path) as f:
|
||||
cfg = yaml.safe_load(f)
|
||||
return cfg.get("phase2", {})
|
||||
|
||||
|
||||
def build_slots(config: dict) -> list[StrategySlot]:
|
||||
"""Build StrategySlot instances from config."""
|
||||
starting_equity = config.get("starting_equity", 100_000)
|
||||
slots = []
|
||||
|
||||
# Strategy factory
|
||||
strategy_map = config.get("strategies", [])
|
||||
for entry in strategy_map:
|
||||
if not entry.get("enabled", True):
|
||||
continue
|
||||
|
||||
name = entry["name"]
|
||||
pair = entry["pair"]
|
||||
tf = entry.get("timeframe", "H1")
|
||||
htf_tf = entry.get("htf_timeframe", None)
|
||||
|
||||
# Create strategy instance
|
||||
if name == "S7_Tight":
|
||||
strategy = S7_Liquidity_Sweep()
|
||||
elif name == "S9":
|
||||
strategy = S9_London_Session()
|
||||
elif name == "S9_Filtered":
|
||||
strategy = S9_London_Session(pair=pair, filtered=True)
|
||||
elif name == "S4F":
|
||||
strategy = S4F_EMA_Ribbon()
|
||||
elif name == "S3":
|
||||
strategy = S3_KeyLevel_Breakout()
|
||||
else:
|
||||
print(f"Unknown strategy: {name}, skipping.")
|
||||
continue
|
||||
|
||||
# Data feeds
|
||||
data_feed = OandaDataFeed(pair=pair, granularity=tf, candle_count=250)
|
||||
htf_feed = None
|
||||
if htf_tf and htf_tf != tf:
|
||||
htf_feed = OandaDataFeed(pair=pair, granularity=htf_tf, candle_count=250)
|
||||
|
||||
# Position manager (each slot gets its own)
|
||||
pm = PositionManager(pair=pair, starting_equity=starting_equity)
|
||||
|
||||
slot = StrategySlot(
|
||||
name=name,
|
||||
strategy=strategy,
|
||||
pair=pair,
|
||||
timeframe=tf,
|
||||
data_feed=data_feed,
|
||||
position_manager=pm,
|
||||
htf_feed=htf_feed,
|
||||
enabled=True,
|
||||
)
|
||||
slots.append(slot)
|
||||
|
||||
return slots
|
||||
|
||||
|
||||
def main():
|
||||
config = load_config()
|
||||
if not config:
|
||||
print("ERROR: No phase2 config found in config/system.yaml")
|
||||
sys.exit(1)
|
||||
|
||||
paper_mode = config.get("paper_mode", True)
|
||||
interval = config.get("poll_interval_seconds", 60)
|
||||
max_dd = config.get("max_daily_drawdown_pct", 5.0)
|
||||
|
||||
# Check OANDA credentials
|
||||
env_path = os.path.join(ROOT, "config", ".env")
|
||||
if os.path.exists(env_path):
|
||||
for line in open(env_path).read().splitlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
k, v = line.split("=", 1)
|
||||
os.environ.setdefault(k.strip(), v.strip())
|
||||
|
||||
account_id = os.getenv("OANDA_ACCOUNT_ID", "")
|
||||
if not account_id and not paper_mode:
|
||||
print("ERROR: OANDA_ACCOUNT_ID not set in config/.env")
|
||||
print("Paper trading can run without it, but live mode requires it.")
|
||||
sys.exit(1)
|
||||
|
||||
if not account_id:
|
||||
print("WARNING: OANDA_ACCOUNT_ID empty — paper mode only, "
|
||||
"data feeds will still work for signal testing.")
|
||||
|
||||
# Build components
|
||||
executor = OandaExecutor(paper_mode=paper_mode)
|
||||
slots = build_slots(config)
|
||||
|
||||
if not slots:
|
||||
print("ERROR: No strategy slots configured.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Phase 2 Live Engine")
|
||||
print(f" Mode: {'PAPER' if paper_mode else 'LIVE'}")
|
||||
print(f" Slots: {len(slots)}")
|
||||
print(f" Poll interval: {interval}s")
|
||||
print(f" Max daily DD: {max_dd}%")
|
||||
print()
|
||||
|
||||
engine = LiveEngine(
|
||||
slots=slots,
|
||||
executor=executor,
|
||||
interval_seconds=interval,
|
||||
max_daily_drawdown_pct=max_dd,
|
||||
)
|
||||
engine.load_state()
|
||||
|
||||
if "--once" in sys.argv:
|
||||
engine.run_once()
|
||||
else:
|
||||
engine.run_loop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user