mirror of
https://github.com/BrentNeale1/fx-quant.git
synced 2026-08-01 21:07:45 +00:00
072ac0f245
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>
151 lines
4.5 KiB
Python
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()
|