Files
fx-quant/src/live/run.py
T
Brent Neale 072ac0f245 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>
2026-02-19 14:46:51 +10:00

151 lines
4.5 KiB
Python

"""
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()