2026-03-30 23:42:32 +03:00
import argparse
import json
import sys
import time
from pathlib import Path
sys . path . insert ( 0 , str ( Path ( __file__ ) . parent ))
2026-05-10 15:53:16 +03:00
# Force UTF-8 output on Windows so emoji/em-dash in log lines don't crash the terminal
if hasattr ( sys . stdout , "reconfigure" ):
sys . stdout . reconfigure ( encoding = "utf-8" , errors = "replace" )
if hasattr ( sys . stderr , "reconfigure" ):
sys . stderr . reconfigure ( encoding = "utf-8" , errors = "replace" )
2026-03-30 23:42:32 +03:00
# Load .env for Telegram credentials (silently ignored if python-dotenv not installed)
try :
from dotenv import load_dotenv
load_dotenv ()
except ImportError :
pass
2026-04-23 10:11:41 +03:00
from src.logger import setup_logger , reconfigure_for_tf
2026-05-14 13:15:41 +03:00
from src.optimizer import (
run_optimization , get_best_params , _score_result as _calc_score ,
extract_consensus_params , run_wfa as optimizer_run_wfa ,
WFO_PARAMS , WFO_PARAMS_FAST ,
2026-05-16 20:25:18 +03:00
run_optimization_stage1 ,
2026-05-16 22:07:50 +03:00
CPCV_N_BLOCKS , CPCV_K_TEST , _N_PATHS ,
2026-05-30 13:01:16 +03:00
resolve_n_states as _optimizer_resolve_n_states ,
2026-05-14 13:15:41 +03:00
)
2026-03-30 23:42:32 +03:00
from src.risk_manager import AdaptiveRiskManager
2026-07-31 21:52:30 +03:00
from src.strategy_backtest import backtest_tf , load_live_bundle
2026-03-30 23:42:32 +03:00
logger = setup_logger ( "main" )
def _resolve_balance ( args ) -> float :
"""--balance takes precedence over --min_cap."""
return args . balance if args . balance is not None else args . min_cap
2026-05-30 13:01:16 +03:00
def resolve_n_states ( tf : str , params : dict ) -> int :
"""Return the canonical n_states for *tf* in both optimize and train paths.
2026-05-31 00:07:44 +03:00
The canonical regime contract mandates exactly 3 states for ALL timeframes:
TREND (0), MEAN_REVERSION (1), VOLATILITY_SHOCK (2).
Passing any other value raises immediately so mis-configured runs fail fast.
2026-05-30 13:01:16 +03:00
"""
2026-05-31 00:07:44 +03:00
requested = int ( params . get ( "n_states" , 3 ))
if requested != 3 :
raise ValueError (
f "n_states must be 3 for all TFs (canonical regime contract). "
f "Received { requested } for { tf } . "
f "Supported states: TREND(0), MEAN_REVERSION(1), VOLATILITY_SHOCK(2)."
)
return 3
2026-05-30 13:01:16 +03:00
2026-05-31 13:51:13 +03:00
def _extract_cpcv_metrics ( cpcv_json : dict ) -> dict :
"""Extract key CPCV metrics from unified schema or legacy flat payload."""
agg = cpcv_json . get ( "cpcv_aggregate_stats" ) if isinstance ( cpcv_json , dict ) else None
if isinstance ( agg , dict ):
return {
"score" : float ( agg . get ( "cpcv_score" , cpcv_json . get ( "final_score_and_penalties" , {}) . get ( "final_score" , 0.0 ))),
"n_valid_paths" : int ( agg . get ( "n_valid_paths" , 0 )),
"median_sharpe" : float ( agg . get ( "median_sharpe" , 0.0 )),
"median_trades" : int ( agg . get ( "median_trades" , 0 )),
"median_win_rate" : float ( agg . get ( "median_win_rate" , 0.0 )),
"median_drawdown" : float ( agg . get ( "median_drawdown" , 0.0 )),
"median_return" : float ( agg . get ( "median_return" , 0.0 )),
"std_sharpe" : float ( agg . get ( "std_sharpe" , 0.0 )),
}
return {
"score" : float ( cpcv_json . get ( "cpcv_score" , 0.0 )),
"n_valid_paths" : int ( cpcv_json . get ( "n_valid_paths" , 0 )),
"median_sharpe" : float ( cpcv_json . get ( "median_sharpe" , 0.0 )),
"median_trades" : int ( cpcv_json . get ( "median_trades" , 0 )),
"median_win_rate" : float ( cpcv_json . get ( "median_win_rate" , 0.0 )),
"median_drawdown" : float ( cpcv_json . get ( "median_drawdown" , 0.0 )),
"median_return" : float ( cpcv_json . get ( "median_return" , 0.0 )),
"std_sharpe" : float ( cpcv_json . get ( "std_sharpe" , 0.0 )),
}
2026-03-30 23:42:32 +03:00
_M5_EXPIRY_HOURS = 120 # 5 days
2026-04-12 17:57:50 +03:00
# Staleness thresholds per TF (days). If the saved model is older than this
# the live gate aborts with a warning and Telegram alert.
# M5 is tightest (14d) — microstructure regimes shift weekly.
# H1/M15 are more stable but should still re-optimise monthly.
_MODEL_STALE_DAYS = { "M5" : 14 , "M15" : 30 , "H1" : 30 }
2026-03-30 23:42:32 +03:00
2026-04-09 00:16:46 +03:00
def _m5_meta_path ( broker : str ) -> Path :
return Path ( f "models/m5_meta_ { broker } .json" )
def _check_m5_readiness ( tf : str , broker : str = "headway_cent" ) -> bool :
2026-03-30 23:42:32 +03:00
"""Return True if the M5 model is fresh enough for live trading.
The M5 timeframe is sensitive to microstructure changes, so the model
must have been optimised within the last 5 days. A meta.json timestamp
2026-07-31 21:52:30 +03:00
is written by cmd_optimize after each successful M5 optimization.
2026-03-30 23:42:32 +03:00
"""
if tf . upper () != "M5" :
return True
2026-04-09 00:16:46 +03:00
meta_path = _m5_meta_path ( broker )
if not meta_path . exists ():
2026-04-09 21:35:07 +03:00
# Legacy fallback: files created before per-broker naming used models/m5_meta.json
_legacy = Path ( "models/m5_meta.json" )
if _legacy . exists ():
logger . info (
2026-04-09 21:51:34 +03:00
"Migrating legacy m5_meta.json -> %s for broker= %s ." , meta_path , broker
2026-04-09 21:35:07 +03:00
)
meta_path . write_text ( _legacy . read_text ())
else :
print (
" \n ERROR: M5 model meta-data not found. \n "
"Run python main.py --mode optimize --tf M5 before live trading."
)
return False
2026-04-09 00:16:46 +03:00
meta = json . loads ( meta_path . read_text ())
2026-03-30 23:42:32 +03:00
age_h = ( time . time () - meta . get ( "timestamp" , 0 )) / 3600
if age_h > _M5_EXPIRY_HOURS :
print (
f " \n WARNING: M5 model is { age_h : .0f } hours old (limit: { _M5_EXPIRY_HOURS } h / 5 days). \n "
"REQUIRED ACTION: Run python main.py --mode optimize --tf M5 to refresh before live. \n "
" \n Pre-flight checklist: \n "
" 1. python main.py --mode sync_validate --period 3m --tf M5 \n "
" 2. python main.py --mode optimize --tf M5 --trials 500 --balance 15 \n "
" 3. python main.py --mode train --tf M5 \n "
)
return False
return True
2026-04-12 17:57:50 +03:00
def _check_model_staleness ( tf : str , broker : str , args ) -> None :
"""Abort live/demo start if the saved model exceeds the staleness threshold.
Sends a Telegram alert and calls ``sys.exit(1)`` when the model is too old.
Pass ``--skip_stale_check`` on the CLI to bypass this gate (e.g. for demo
testing when you intentionally don't want to re-optimise).
Does nothing for unknown TFs or when ``--skip_stale_check`` is set.
"""
if getattr ( args , "skip_stale_check" , False ):
logger . info ( "Staleness gate bypassed (--skip_stale_check)." )
return
from src.validator import check_model_age
from src.notifier import send_telegram_msg
max_age = _MODEL_STALE_DAYS . get ( tf . upper (), 30 )
age_days = check_model_age ( tf = tf , broker = broker )
if age_days <= max_age :
logger . info (
"Model freshness OK: %s / %s is %.1f days old (limit %d )." ,
tf , broker , age_days , max_age ,
)
return
age_str = f " { age_days : .0f } " if age_days != float ( "inf" ) else "unknown (file missing)"
msg = (
f "⚠️ Market Drift/Staleness detected. Pausing trade loop — "
f "[ { tf } ] model is { age_str } days old (limit: { max_age } days). \n "
f "Re-optimise before going live: \n "
f " python main.py --mode optimize --tf { tf } --broker { broker } --trials 500 \n "
f " python main.py --mode train --tf { tf } --broker { broker } \n "
f "Add --skip_stale_check to the live command to bypass this gate."
)
logger . warning ( "STALE MODEL [ %s / %s ]: %.0f days old — aborting." , tf , broker , age_days )
send_telegram_msg ( f "<b> { msg } </b>" )
print ( f " \n { msg } " )
sys . exit ( 1 )
def cmd_wfa ( args ):
2026-07-31 21:52:30 +03:00
print ( '[RETIRED] --mode wfa relied on the legacy ML stack (processor/HMM/XGB/backtester), removed in the notebook-engine consolidation. Use --mode optimize (grid-search-plateau); see CONSOLIDATION.md.' )
raise SystemExit ( 2 )
2026-04-12 17:57:50 +03:00
2026-03-30 23:42:32 +03:00
def cmd_process ( args ):
2026-07-31 21:52:30 +03:00
print ( '[RETIRED] --mode process relied on the legacy ML stack (processor/HMM/XGB/backtester), removed in the notebook-engine consolidation. Use --mode optimize (grid-search-plateau); see CONSOLIDATION.md.' )
raise SystemExit ( 2 )
2026-03-30 23:42:32 +03:00
def cmd_optimize ( args ):
2026-05-14 13:15:41 +03:00
balance = _resolve_balance ( args )
broker = args . broker
tfs = [ t . strip () . upper () for t in args . tf . split ( "," )]
wfo_mode = "fast" if getattr ( args , "fast_wfo" , False ) else "standard"
2026-07-31 21:52:30 +03:00
stage = getattr ( args , "stage" , None )
2026-05-14 13:15:41 +03:00
2026-03-30 23:42:32 +03:00
for tf in tfs :
2026-04-23 10:11:41 +03:00
reconfigure_for_tf ( tf )
2026-05-19 15:27:19 +03:00
2026-07-31 21:52:30 +03:00
# Auto-sync raw CSV from MT5 first; silently skipped when MT5 is unavailable.
2026-05-19 15:27:19 +03:00
try :
from src.mt5_sync import ensure_data_updated
ensure_data_updated ( tf = tf , symbol = "XAUUSD" )
except Exception as _sync_exc :
logger . warning ( "Auto-sync skipped ( %s ). Continuing with existing data." , _sync_exc )
2026-05-14 13:15:41 +03:00
logger . info (
2026-05-16 20:25:18 +03:00
"Optimizing [ %s ] broker= %s balance=$ %.0f trials= %d wfo_mode= %s stage= %s " ,
tf , broker , balance , args . trials , wfo_mode , stage or "joint" ,
2026-05-14 13:15:41 +03:00
)
2026-05-31 00:07:44 +03:00
if stage is not None :
print (
2026-07-31 21:52:30 +03:00
f "[DEPRECATED] --stage { stage !r} is ignored. "
"Running the unified grid-search-plateau pipeline."
2026-05-16 20:25:18 +03:00
)
2026-07-31 21:52:30 +03:00
# The notebook engine loads its own multi-asset panels and trains
# M15 + M5 in a single pass, so no processed parquet is required here.
2026-05-31 00:07:44 +03:00
study = run_optimization (
2026-07-31 21:52:30 +03:00
df = None ,
2026-05-31 00:07:44 +03:00
tf = tf ,
broker = broker ,
account_size = balance ,
n_trials = args . trials ,
wfo_mode = wfo_mode ,
n_jobs = args . n_jobs ,
)
2026-05-14 13:15:41 +03:00
2026-07-31 21:52:30 +03:00
print ( "" )
print ( f "=== Best Result [ { tf } ] ===" )
2026-03-30 23:42:32 +03:00
print ( f "Score: { study . best_value : .3f } " )
2026-05-14 13:15:41 +03:00
print ( f "Broker: { broker } | Balance: $ { balance : .0f } | WFO mode: { wfo_mode } " )
2026-07-31 21:52:30 +03:00
print ( "Best Params (grid-search-plateau center):" )
2026-03-30 23:42:32 +03:00
for k , v in study . best_params . items ():
print ( f " { k } : { v } " )
2026-05-14 13:15:41 +03:00
2026-03-30 23:42:32 +03:00
if tf == "M5" :
2026-04-09 00:16:46 +03:00
meta_path = _m5_meta_path ( broker )
meta_path . parent . mkdir ( parents = True , exist_ok = True )
meta_path . write_text ( json . dumps ({
2026-03-30 23:42:32 +03:00
"timestamp" : time . time (),
"tf" : "M5" ,
"best_score" : study . best_value ,
}))
print (
2026-07-31 21:52:30 +03:00
"Optimization Complete. The live model bundle has been exported. "
"You are cleared for M5 Live Trading for the next 5 days."
2026-03-30 23:42:32 +03:00
)
def cmd_train ( args ):
balance = _resolve_balance ( args )
2026-07-31 21:52:30 +03:00
broker = args . broker
tf = args . tf . upper ()
2026-04-23 10:11:41 +03:00
reconfigure_for_tf ( tf )
2026-03-30 23:42:32 +03:00
2026-07-31 21:52:30 +03:00
# With the notebook-engine consolidation, --mode optimize already trains AND
# exports the single live model bundle (models + thresholds + base_params).
# "train" now loads that exact bundle and reports a full-period backtest, so
# the reported edge matches what will actually trade.
try :
bundle = load_live_bundle ()
except FileNotFoundError as exc :
logger . error ( str ( exc ))
print (
f "ERROR: No live model bundle found for [ { tf } ]. "
f "Run python main.py --mode optimize --tf { tf } --broker { broker } first."
)
sys . exit ( 1 )
2026-03-30 23:42:32 +03:00
try :
params = get_best_params ( balance = balance , broker = broker , tf = tf )
2026-07-31 21:52:30 +03:00
logger . info ( "Grid-search-plateau params [ %s / %s ]: %s " , tf , broker , params )
2026-03-30 23:42:32 +03:00
except Exception :
2026-07-31 21:52:30 +03:00
logger . warning ( "Could not read exported params for tf= %s broker= %s ." , tf , broker )
2026-03-30 23:42:32 +03:00
2026-07-31 21:52:30 +03:00
try :
metrics = backtest_tf ( tf , bundle = bundle )
except Exception as exc :
logger . error ( "Backtest via the notebook engine failed: %s " , exc )
2026-04-09 00:27:20 +03:00
sys . exit ( 1 )
2026-04-08 23:44:05 +03:00
arm = AdaptiveRiskManager ( balance , broker = broker )
2026-05-09 20:55:10 +03:00
2026-07-31 21:52:30 +03:00
cpcv_score = None
_cpcv_json_path = Path ( f "reports/cpcv_ { tf . lower () } _ { broker } .json" )
if _cpcv_json_path . exists ():
try :
cpcv_score = _extract_cpcv_metrics ( json . loads ( _cpcv_json_path . read_text ()))[ "score" ]
except Exception :
cpcv_score = None
print ( "" )
print ( f "=== Training Results [ { tf } ] ===" )
2026-03-30 23:42:32 +03:00
print ( f "Broker: { broker } | Balance: $ { balance : .0f } | Tier: { 'small' if arm . is_small_account else 'growth' } " )
2026-07-31 21:52:30 +03:00
print ( "Model bundle exported by --mode optimize (single-source live model)." )
print (
f "Full-period Sharpe: { metrics . get ( 'sharpe' , 0.0 ) : .3f } "
f " | MaxDD: { metrics . get ( 'max_drawdown' , 0.0 ) : .1f } %"
f " | WR: { metrics . get ( 'win_rate' , 0.0 ) * 100 : .1f } %"
f " | Trades: { metrics . get ( 'trade_count' , 0 ) } "
)
2026-05-09 20:55:10 +03:00
if cpcv_score is not None :
print ( f "CPCV Validation Score (best trial): { cpcv_score : .3f } " )
2026-07-31 21:52:30 +03:00
print ( "" )
print ( "Live model bundle is ready. Run --mode sync_validate before going live." )
2026-03-30 23:42:32 +03:00
2026-05-14 13:15:41 +03:00
def cmd_extract_consensus ( args ):
2026-07-31 21:52:30 +03:00
print ( '[RETIRED] --mode extract_consensus relied on the legacy ML stack (processor/HMM/XGB/backtester), removed in the notebook-engine consolidation. Use --mode optimize (grid-search-plateau); see CONSOLIDATION.md.' )
raise SystemExit ( 2 )
2026-05-14 13:15:41 +03:00
2026-03-30 23:42:32 +03:00
def cmd_compare ( args ):
2026-07-31 21:52:30 +03:00
print ( '[RETIRED] --mode compare relied on the legacy ML stack (processor/HMM/XGB/backtester), removed in the notebook-engine consolidation. Use --mode optimize (grid-search-plateau); see CONSOLIDATION.md.' )
raise SystemExit ( 2 )
2026-03-30 23:42:32 +03:00
def cmd_export ( args ):
2026-07-31 21:52:30 +03:00
print ( '[RETIRED] --mode export relied on the legacy ML stack (processor/HMM/XGB/backtester), removed in the notebook-engine consolidation. Use --mode optimize (grid-search-plateau); see CONSOLIDATION.md.' )
raise SystemExit ( 2 )
2026-03-30 23:42:32 +03:00
def cmd_sync_validate ( args ):
"""Download recent MT5 bars then run the model validation gatekeeper."""
from src.mt5_sync import sync_mt5_data
2026-05-30 18:18:17 +03:00
from src.validator import run_validation , validate_strategy
2026-03-30 23:42:32 +03:00
balance = _resolve_balance ( args )
tf = args . tf . upper ()
2026-04-23 10:11:41 +03:00
reconfigure_for_tf ( tf )
2026-03-30 23:42:32 +03:00
logger . info ( "Syncing MT5 data [ %s ] period= %s ..." , tf , args . period )
try :
df = sync_mt5_data ( tf = tf , period = args . period )
logger . info (
"Sync complete: %d bars %s -> %s " ,
len ( df ), df . index . min (), df . index . max (),
)
except Exception as exc :
logger . error ( "MT5 sync failed: %s " , exc )
sys . exit ( 1 )
try :
result = run_validation ( tf = tf , broker = args . broker , account_size = balance )
except Exception as exc :
logger . error ( "Validation error: %s " , exc )
sys . exit ( 1 )
2026-05-30 18:18:17 +03:00
# ── Deployment gate (Phase F) ─────────────────────────────────────────
gate = validate_strategy ( result , tf = tf )
gate_status = gate [ "status" ]
gate_reason = gate [ "reason" ]
gate_details = gate [ "details" ]
2026-03-30 23:42:32 +03:00
print ( f " \n === Validation Result [ { tf } ] ===" )
2026-04-16 13:26:56 +03:00
_fdd = result . get ( "max_dd" , 0.0 )
_eff = result . get ( "avg_efficiency" , 0.0 )
_cost_e = result . get ( "cost_efficiency" , 0.0 )
2026-07-31 21:52:30 +03:00
_tr_pct = result . get ( "total_return" , 0.0 )
_payout = f " { _tr_pct : +.1f } % ($ { balance * _tr_pct / 100.0 : +,.2f } )"
2026-04-15 22:41:32 +03:00
print (
f " [ { tf } LIVE] Score: { result . get ( 'score' , 0.0 ) : .2f } "
f " | RF: { result . get ( 'recovery_factor' , 0.0 ) : .2f } "
f " | PF: { result . get ( 'profit_factor' , 1.0 ) : .2f } "
f " | Payoff: $ { result . get ( 'expected_payoff' , 0.0 ) * balance : .4f } "
f " | MaxDD: { _fdd * 100 : .1f } % (Floating)"
)
2026-04-16 13:26:56 +03:00
print (
f " Efficiency: { _eff : .2f } x ATR/Spread"
f " | CostEff: { _cost_e * 100 : .1f } %"
f " | Total Payout: { _payout } "
)
if _eff < 1.2 :
2026-05-10 15:53:16 +03:00
print ( " [!] WARNING: Low Market Efficiency — Spread is eating your edge." )
2026-04-16 13:26:56 +03:00
if _cost_e < 0.50 :
2026-05-10 15:53:16 +03:00
print ( f " [!] WARNING: Broker is consuming > { ( 1 - _cost_e ) * 100 : .0f } % of gross profit." )
2026-04-15 22:41:32 +03:00
print ( f " Sharpe: { result [ 'sharpe' ] : .3f } | Trades: { result [ 'n_trades' ] } | WR: { result [ 'win_rate' ] * 100 : .1f } %" )
print ( f " Status: { result [ 'status' ] . upper () } " )
2026-03-30 23:42:32 +03:00
print ( f " { result [ 'message' ] } " )
2026-05-30 18:18:17 +03:00
# Gate result
if gate_status == "fail" :
print ( f " \n [DEPLOYMENT GATE] FAIL — reason: { gate_reason } " )
print ( f " Details: { gate_details } " )
print (
" \n ABORTING: Deployment gate failed. "
"Retune with --mode optimize then --mode train before going live."
)
sys . exit ( 1 )
if gate_status == "warn" :
print ( f " \n [DEPLOYMENT GATE] WARN — { gate_reason } | details: { gate_details } " )
print ( " WARNING: Borderline performance — consider re-optimising before live trading." )
else :
print ( f " \n [DEPLOYMENT GATE] PASS — all checks OK | details: { gate_details } " )
2026-03-30 23:42:32 +03:00
if result [ "status" ] == "fail" :
print (
" \n ABORTING: Validation failed. "
"Retune with --mode optimize then --mode train before going live."
)
sys . exit ( 1 )
if result [ "status" ] == "warn" :
print ( " \n WARNING: Borderline performance — consider re-optimising before live trading." )
2026-04-08 22:52:56 +03:00
def cmd_demo ( args ):
"""Connect to MT5 demo account and start the live signal execution loop."""
2026-03-30 23:42:32 +03:00
from src.mt5_trader import run_live_loop
balance = _resolve_balance ( args )
tf = args . tf . upper ()
2026-04-09 00:16:46 +03:00
if not _check_m5_readiness ( tf , args . broker ):
2026-03-30 23:42:32 +03:00
sys . exit ( 1 )
2026-04-12 17:57:50 +03:00
_check_model_staleness ( tf , args . broker , args )
2026-04-09 00:16:46 +03:00
logger . info ( "Starting demo loop — TF= %s broker= %s balance=$ %.0f " ,
2026-04-08 22:52:56 +03:00
tf , args . broker , balance ,
)
run_live_loop ( tf = tf , broker = args . broker , account_size = balance ,
2026-05-09 08:52:33 +03:00
profit_target = getattr ( args , "profit_target" , None ))
2026-05-09 09:06:27 +03:00
def cmd_live ( args ):
2026-04-08 22:52:56 +03:00
"""Connect to MT5 live account and start the live signal execution loop."""
from src.mt5_trader import run_live_loop
balance = _resolve_balance ( args )
tf = args . tf . upper ()
2026-04-23 10:11:41 +03:00
reconfigure_for_tf ( tf )
2026-04-08 22:52:56 +03:00
2026-04-09 00:16:46 +03:00
if not _check_m5_readiness ( tf , args . broker ):
2026-04-08 22:52:56 +03:00
sys . exit ( 1 )
2026-04-12 17:57:50 +03:00
_check_model_staleness ( tf , args . broker , args )
2026-04-08 22:52:56 +03:00
if not args . yes :
2026-03-30 23:42:32 +03:00
print ( " \n " + "=" * 60 )
print ( " WARNING: LIVE ACCOUNT — real money is at risk." )
print ( " Ensure --mode sync_validate passed before continuing." )
print ( " Ensure the GoldRegimeX EA is removed from the XAUUSD chart." )
confirm = input ( " Type YES to confirm live trading: " ) . strip ()
if confirm != "YES" :
print ( "Aborted." )
sys . exit ( 0 )
print ( "=" * 60 + " \n " )
logger . info (
2026-04-08 22:52:56 +03:00
"Starting live loop — TF= %s broker= %s balance=$ %.0f " ,
tf , args . broker , balance ,
2026-03-30 23:42:32 +03:00
)
2026-04-08 22:52:56 +03:00
run_live_loop ( tf = tf , broker = args . broker , account_size = balance ,
2026-05-09 08:52:33 +03:00
profit_target = getattr ( args , "profit_target" , None ))
2026-03-30 23:42:32 +03:00
def cmd_report ( args ):
2026-07-31 21:52:30 +03:00
print ( '[RETIRED] --mode report relied on the legacy ML stack (processor/HMM/XGB/backtester), removed in the notebook-engine consolidation. Use --mode optimize (grid-search-plateau); see CONSOLIDATION.md.' )
raise SystemExit ( 2 )
2026-03-30 23:42:32 +03:00
def cmd_audit ( args ):
"""Print (and optionally send) the daily MT5 performance report."""
from src.auditor import get_daily_report
from src.notifier import send_telegram_msg
balance = _resolve_balance ( args )
report = get_daily_report ( broker = args . broker )
print ( report )
sent = send_telegram_msg ( report )
if sent :
print ( " \n Report also sent to Telegram." )
else :
print ( " \n (Telegram not configured — see .env.example)" )
def cmd_guardian ( args ):
"""Start the multi-TF health monitor loop."""
from src.guardian import run_guardian
balance = _resolve_balance ( args )
tfs = [ t . strip () . upper () for t in args . tf . split ( "," )]
interval = getattr ( args , "interval" , 3600 )
run_guardian (
tfs = tfs ,
broker = args . broker ,
account_size = balance ,
period = args . period ,
interval_sec = interval ,
)
2026-04-06 21:33:14 +03:00
def cmd_consolidate ( args ):
2026-05-10 22:40:40 +03:00
"""Consolidate multi-asset CSV exports in data/raw/ into per-TF master files.
2026-04-12 21:40:54 +03:00
2026-05-10 22:40:40 +03:00
Processes 5 assets × 3 timeframes = 15 master files in data/processed/.
Assets: USDCHF, XAGUSD, XTIUSD, US500, USDJPY.
Timeframes: H1, M15, M5.
2026-04-12 21:40:54 +03:00
"""
2026-05-10 22:40:40 +03:00
from src.data_consolidator import consolidate_asset , ASSET_CONFIGS
assets = list ( ASSET_CONFIGS . keys ())
tfs = [ "H1" , "M15" , "M5" ]
n_ok = 0
for asset in assets :
for tf in tfs :
result = consolidate_asset ( asset , tf )
if not result . empty :
out = ASSET_CONFIGS [ asset ][ tf ][ "output" ]
print ( f " { asset } { tf } : { len ( result ) } rows → data/processed/ { out } " )
n_ok += 1
else :
logger . warning ( " %s %s : no source files found — skipping." , asset , tf )
print ( f " \n Consolidation complete: { n_ok } / { len ( assets ) * len ( tfs ) } master files produced." )
2026-04-06 21:33:14 +03:00
2026-03-30 23:42:32 +03:00
def cmd_listen ( args ):
"""Start the Telegram remote control listener + nightly report scheduler."""
import threading
from src.remote_control import run_listener
from src.notifier import send_telegram_msg
from src.auditor import get_daily_report
balance = _resolve_balance ( args )
# ── Nightly audit scheduler (runs in background thread) ──────────────────
def _run_scheduler ():
try :
import schedule
except ImportError :
logger . warning (
"schedule package not installed — nightly report disabled. "
"Install with: pip install schedule"
)
return
def _send_nightly ():
report = get_daily_report ( broker = args . broker )
send_telegram_msg ( f "<b>Nightly Report</b> \n { report } " )
logger . info ( "Nightly report sent via Telegram." )
schedule . every () . day . at ( "23:55" ) . do ( _send_nightly )
logger . info ( "Nightly report scheduled at 23:55 UTC." )
while True :
schedule . run_pending ()
time . sleep ( 60 )
sched_thread = threading . Thread ( target = _run_scheduler , daemon = True )
sched_thread . start ()
# ── Blocking Telegram listener ────────────────────────────────────────────
run_listener ()
2026-04-23 22:20:04 +03:00
2026-04-23 15:18:44 +03:00
def cmd_sensitivity ( args ):
2026-07-31 21:52:30 +03:00
print ( '[RETIRED] --mode sensitivity relied on the legacy ML stack (processor/HMM/XGB/backtester), removed in the notebook-engine consolidation. Use --mode optimize (grid-search-plateau); see CONSOLIDATION.md.' )
raise SystemExit ( 2 )
2026-04-23 15:18:44 +03:00
2026-05-18 12:43:11 +03:00
def cmd_montecarlo ( args ):
2026-07-31 21:52:30 +03:00
print ( '[RETIRED] --mode montecarlo relied on the legacy ML stack (processor/HMM/XGB/backtester), removed in the notebook-engine consolidation. Use --mode optimize (grid-search-plateau); see CONSOLIDATION.md.' )
raise SystemExit ( 2 )
2026-05-18 12:43:11 +03:00
2026-05-31 00:07:44 +03:00
def _validate_h1_args ( args ) -> None :
"""Fail-fast validation for H1 gate/floor CLI overrides.
Called before mode dispatch so invalid ranges produce a clear error rather
than a cryptic crash inside the optimizer or signal engine.
"""
checks = [
( "h1_entry_prob" , 0.50 , 0.90 , "[0.50, 0.90]" ),
( "h1_min_median_sharpe" , - 1.0 , 2.0 , "[-1.0, 2.0]" ),
( "h1_min_median_pf" , 0.5 , 3.0 , "[0.5, 3.0]" ),
( "h1_max_trades_per_100" , 0.5 , 20.0 , "[0.5, 20.0]" ),
]
for attr , lo , hi , rng in checks :
val = getattr ( args , attr , None )
if val is not None and not ( lo <= val <= hi ):
print (
f " \n ERROR: -- { attr } { val } is out of valid range { rng } . \n "
f "Fix: pass a value within { rng } or omit the flag to use the default. \n "
)
sys . exit ( 1 )
2026-03-30 23:42:32 +03:00
def main ():
parser = argparse . ArgumentParser ( description = "Gold Regime X — Hybrid ML Trading System" )
parser . add_argument (
"--mode" ,
choices = [ "process" , "optimize" , "train" , "compare" , "export" , "report" ,
2026-05-30 18:18:17 +03:00
"sync_validate" , "sync_validation" , "demo" , "live" , "audit" , "guardian" , "listen" ,
2026-05-18 12:43:11 +03:00
"consolidate" , "wfa" , "sensitivity" , "extract_consensus" ,
"montecarlo" ],
2026-03-30 23:42:32 +03:00
required = True ,
)
parser . add_argument ( "--trials" , type = int , default = 250 )
parser . add_argument ( "--n_jobs" , type = int , default = 1 ,
2026-07-31 21:52:30 +03:00
help = "Parallel grid-search-plateau workers (default 1). "
"Passed through to the grid-search engine." )
2026-03-30 23:42:32 +03:00
parser . add_argument ( "--interval" , type = int , default = 3600 ,
help = "Guardian check interval in seconds (default 3600 = 1h)." )
parser . add_argument ( "--min_cap" , type = float , default = 15.0 ,
help = "Account capital in USD (legacy, use --balance)" )
parser . add_argument ( "--balance" , type = float , default = None ,
help = "Account balance in USD — overrides --min_cap" )
parser . add_argument ( "--broker" , type = str , default = "standard" ,
choices = [ "standard" , "headway_cent" ])
parser . add_argument ( "--tf" , type = str , default = "H1" ,
help = "Timeframe: H1 | M15 | M15,H1 (process/compare accept comma list)" )
parser . add_argument ( "--period" , type = str , default = "3m" ,
help = "Lookback window for MT5 sync, e.g. '3m' '6m' '12m'." )
2026-04-06 16:13:56 +03:00
parser . add_argument ( "--yes" , action = "store_true" ,
help = "Skip the interactive live-account confirmation (used when launched as a subprocess)." )
2026-04-10 21:35:53 +03:00
parser . add_argument ( "--profit_target" , type = float , default = None ,
help = "Quick-profit close threshold in USD. M5 defaults to 4.0; "
"other TFs disabled unless set. Pass 0 to disable on M5." )
2026-04-12 17:57:50 +03:00
parser . add_argument ( "--skip_stale_check" , action = "store_true" ,
help = "Bypass the model-staleness gate on --mode live/demo. "
"Use when intentionally running an older model (e.g. demo testing)." )
parser . add_argument ( "--train_days" , type = int , default = None ,
help = "WFA IS window in calendar days (default: H1=365, M15=180, M5=90)." )
parser . add_argument ( "--test_days" , type = int , default = None ,
help = "WFA OOS step size in calendar days (default: H1=90, M15=60, M5=30)." )
2026-05-14 13:15:41 +03:00
parser . add_argument ( "--fast_wfo" , action = "store_true" ,
help = "Use faster WFO window sizes for --mode optimize and --mode wfa." )
parser . add_argument ( "--top_n" , type = int , default = 10 ,
help = "Top-N trials to aggregate for --mode extract_consensus (default 10)." )
parser . add_argument ( "--min_wfe" , type = float , default = 0.0 ,
help = "Minimum WFE ratio filter for --mode extract_consensus (default 0)." )
2026-05-16 20:25:18 +03:00
parser . add_argument (
"--stage" , type = str , default = None , choices = [ "xgb" , "trading" ],
help = (
2026-05-31 00:07:44 +03:00
"[DEPRECATED] --stage is deprecated and ignored. "
"Unified single-stage CPCV optimization is always used. "
"Accepted for backward compatibility only."
2026-05-16 20:25:18 +03:00
),
)
2026-05-31 00:07:44 +03:00
# ── H1 profitability safeguards (override optimizer defaults) ─────────────
parser . add_argument (
"--h1_entry_prob" , type = float , default = None ,
help = "H1 XGBoost entry probability gate override [0.50, 0.90] (default 0.575). "
"Passed as fixed override to live/demo; also primes the search range start for optimize." ,
)
parser . add_argument (
"--h1_min_median_sharpe" , type = float , default = None ,
help = "H1 minimum acceptable median CPCV Sharpe floor. Trials below are penalized. "
"Default 0.10. Valid range [-1.0, 2.0]." ,
)
parser . add_argument (
"--h1_min_median_pf" , type = float , default = None ,
help = "H1 minimum acceptable median profit factor. Default 1.02. Valid range [0.5, 3.0]." ,
)
parser . add_argument (
"--h1_max_trades_per_100" , type = float , default = None ,
help = "H1 maximum trades per 100 bars before turnover penalty fires. "
"Default 4.0. Valid range [0.5, 20.0]." ,
)
2026-03-30 23:42:32 +03:00
args = parser . parse_args ()
2026-05-31 00:07:44 +03:00
_validate_h1_args ( args )
2026-03-30 23:42:32 +03:00
{
"process" : cmd_process ,
"optimize" : cmd_optimize ,
"train" : cmd_train ,
"compare" : cmd_compare ,
"export" : cmd_export ,
"report" : cmd_report ,
2026-05-30 18:18:17 +03:00
"sync_validate" : cmd_sync_validate ,
"sync_validation" : cmd_sync_validate ,
2026-04-08 22:52:56 +03:00
"demo" : cmd_demo ,
2026-03-30 23:42:32 +03:00
"live" : cmd_live ,
"audit" : cmd_audit ,
"guardian" : cmd_guardian ,
"listen" : cmd_listen ,
2026-04-06 21:33:14 +03:00
"consolidate" : cmd_consolidate ,
2026-04-12 17:57:50 +03:00
"wfa" : cmd_wfa ,
2026-04-23 15:18:44 +03:00
"sensitivity" : cmd_sensitivity ,
2026-05-14 13:15:41 +03:00
"extract_consensus" : cmd_extract_consensus ,
2026-05-18 12:43:11 +03:00
"montecarlo" : cmd_montecarlo ,
2026-03-30 23:42:32 +03:00
}[ args . mode ]( args )
if __name__ == "__main__" :
main ()