mirror of
https://github.com/lucasmos/GoldRegime_X.git
synced 2026-11-02 06:26:15 +00:00
feat: broker-specific model files and study DB separation
Cent and standard account models now live in separate files so optimizing/training one account never overwrites the other: - engine_hmm.py: get_model_path(tf, broker) → hmm_model_H1_headway_cent.pkl - engine_xgb.py: get_ensemble_path(tf, broker) → xgb_ensemble_H1_standard.pkl - optimizer.py: STUDY_DB replaced by _study_db(broker) → models/study_headway_cent.db + models/study_standard.db (fully isolated) - main.py: _M5_META_PATH → _m5_meta_path(broker); thread broker through all model path calls, cmd_export, cmd_report, _check_m5_readiness - validator.py: pass broker to hmm_model_path + get_ensemble_path - mt5_trader.py: pass broker to hmm_model_path + get_ensemble_path Backward-compat fallback to generic MODEL_PATH / ENSEMBLE_PKL_PATH preserved if broker-specific file absent (e.g. models not yet retrained). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
cea8b3bab4
commit
59085649ee
@@ -36,11 +36,14 @@ def _resolve_balance(args) -> float:
|
||||
return args.balance if args.balance is not None else args.min_cap
|
||||
|
||||
|
||||
_M5_META_PATH = Path("models/m5_meta.json")
|
||||
_M5_EXPIRY_HOURS = 120 # 5 days
|
||||
|
||||
|
||||
def _check_m5_readiness(tf: str) -> bool:
|
||||
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:
|
||||
"""Return True if the M5 model is fresh enough for live trading.
|
||||
|
||||
The M5 timeframe is sensitive to microstructure changes, so the model
|
||||
@@ -49,13 +52,14 @@ def _check_m5_readiness(tf: str) -> bool:
|
||||
"""
|
||||
if tf.upper() != "M5":
|
||||
return True
|
||||
if not _M5_META_PATH.exists():
|
||||
meta_path = _m5_meta_path(broker)
|
||||
if not meta_path.exists():
|
||||
print(
|
||||
"\nERROR: M5 model meta-data not found.\n"
|
||||
"Run python main.py --mode optimize --tf M5 before live trading."
|
||||
)
|
||||
return False
|
||||
meta = json.loads(_M5_META_PATH.read_text())
|
||||
meta = json.loads(meta_path.read_text())
|
||||
age_h = (time.time() - meta.get("timestamp", 0)) / 3600
|
||||
if age_h > _M5_EXPIRY_HOURS:
|
||||
print(
|
||||
@@ -142,8 +146,9 @@ def cmd_optimize(args):
|
||||
for k, v in study.best_params.items():
|
||||
print(f" {k}: {v}")
|
||||
if tf == "M5":
|
||||
_M5_META_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
_M5_META_PATH.write_text(json.dumps({
|
||||
meta_path = _m5_meta_path(broker)
|
||||
meta_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
meta_path.write_text(json.dumps({
|
||||
"timestamp": time.time(),
|
||||
"tf": "M5",
|
||||
"best_score": study.best_value,
|
||||
@@ -167,8 +172,8 @@ def cmd_train(args):
|
||||
params = {}
|
||||
|
||||
result, model_hmm, state_map, models_ensemble, thresholds, metrics, *_ = _train_for_tf(tf, balance, broker, params)
|
||||
save_hmm(model_hmm, hmm_model_path(tf))
|
||||
save_xgb_ensemble(models_ensemble, thresholds, metrics, get_ensemble_path(tf))
|
||||
save_hmm(model_hmm, hmm_model_path(tf, broker))
|
||||
save_xgb_ensemble(models_ensemble, thresholds, metrics, get_ensemble_path(tf, broker))
|
||||
|
||||
arm = AdaptiveRiskManager(balance, broker=broker)
|
||||
limits = arm.get_trade_limits()
|
||||
@@ -258,8 +263,9 @@ def cmd_compare(args):
|
||||
|
||||
|
||||
def cmd_export(args):
|
||||
tf = args.tf.upper()
|
||||
xgb_path = get_ensemble_path(tf)
|
||||
tf = args.tf.upper()
|
||||
broker = args.broker
|
||||
xgb_path = get_ensemble_path(tf, broker)
|
||||
if not xgb_path.exists():
|
||||
xgb_path = ENSEMBLE_PKL_PATH
|
||||
try:
|
||||
@@ -327,11 +333,10 @@ def cmd_demo(args):
|
||||
balance = _resolve_balance(args)
|
||||
tf = args.tf.upper()
|
||||
|
||||
if not _check_m5_readiness(tf):
|
||||
if not _check_m5_readiness(tf, args.broker):
|
||||
sys.exit(1)
|
||||
|
||||
logger.info(
|
||||
"Starting demo loop — TF=%s broker=%s balance=$%.0f",
|
||||
logger.info("Starting demo loop — TF=%s broker=%s balance=$%.0f",
|
||||
tf, args.broker, balance,
|
||||
)
|
||||
run_live_loop(tf=tf, broker=args.broker, account_size=balance,
|
||||
@@ -346,7 +351,7 @@ def cmd_live(args):
|
||||
balance = _resolve_balance(args)
|
||||
tf = args.tf.upper()
|
||||
|
||||
if not _check_m5_readiness(tf):
|
||||
if not _check_m5_readiness(tf, args.broker):
|
||||
sys.exit(1)
|
||||
|
||||
if not args.yes:
|
||||
@@ -382,7 +387,7 @@ def cmd_report(args):
|
||||
df = process_pipeline(obs_cov=params.get("obs_cov"), trans_cov=params.get("trans_cov"),
|
||||
save=False, tf=tf)
|
||||
|
||||
_hmm_path = hmm_model_path(tf)
|
||||
_hmm_path = hmm_model_path(tf, broker)
|
||||
if not _hmm_path.exists():
|
||||
_hmm_path = None # let the except branch fit a fresh one
|
||||
try:
|
||||
@@ -398,7 +403,7 @@ def cmd_report(args):
|
||||
|
||||
X, y, df_aligned = prepare_features(df, states)
|
||||
|
||||
_xgb_path = get_ensemble_path(tf)
|
||||
_xgb_path = get_ensemble_path(tf, broker)
|
||||
if not _xgb_path.exists():
|
||||
_xgb_path = ENSEMBLE_PKL_PATH
|
||||
try:
|
||||
|
||||
+7
-3
@@ -10,9 +10,13 @@ logger = setup_logger(__name__)
|
||||
MODEL_PATH = Path("models/hmm_model.pkl")
|
||||
|
||||
|
||||
def get_model_path(tf: str) -> Path:
|
||||
"""Return the TF-specific HMM model path, e.g. models/hmm_model_H1.pkl."""
|
||||
return Path(f"models/hmm_model_{tf.upper()}.pkl")
|
||||
def get_model_path(tf: str, broker: str = "headway_cent") -> Path:
|
||||
"""Return the TF+broker-specific HMM model path.
|
||||
|
||||
Example: get_model_path("H1", "headway_cent") → models/hmm_model_H1_headway_cent.pkl
|
||||
Falls back to the generic models/hmm_model_H1.pkl (then MODEL_PATH) if absent.
|
||||
"""
|
||||
return Path(f"models/hmm_model_{tf.upper()}_{broker}.pkl")
|
||||
STATE_NAMES_3 = {0: "Bull", 1: "Bear", 2: "Chop"}
|
||||
STATE_NAMES_2 = {0: "Bull", 1: "Bear"}
|
||||
STATE_NAMES_4 = {0: "Bull", 1: "Bear", 2: "Chop_Low", 3: "Chop_High"}
|
||||
|
||||
+7
-3
@@ -14,9 +14,13 @@ XGB_PKL_PATH = Path("models/xgb_model.pkl")
|
||||
ENSEMBLE_PKL_PATH = Path("models/xgb_ensemble.pkl")
|
||||
|
||||
|
||||
def get_ensemble_path(tf: str) -> Path:
|
||||
"""Return the TF-specific XGB ensemble path, e.g. models/xgb_ensemble_H1.pkl."""
|
||||
return Path(f"models/xgb_ensemble_{tf.upper()}.pkl")
|
||||
def get_ensemble_path(tf: str, broker: str = "headway_cent") -> Path:
|
||||
"""Return the TF+broker-specific XGB ensemble path.
|
||||
|
||||
Example: get_ensemble_path("H1", "headway_cent") → models/xgb_ensemble_H1_headway_cent.pkl
|
||||
Falls back to the generic models/xgb_ensemble_H1.pkl (then ENSEMBLE_PKL_PATH) if absent.
|
||||
"""
|
||||
return Path(f"models/xgb_ensemble_{tf.upper()}_{broker}.pkl")
|
||||
|
||||
# Base features always present; usdchf_log_return is added when the USDCHF
|
||||
# master CSV is available (run python main.py --mode consolidate first).
|
||||
|
||||
+3
-3
@@ -645,8 +645,8 @@ def _run_loop_inner(tf: str, broker: str, account_size: float, mt5,
|
||||
|
||||
display_account_info(trading_balance=account_size)
|
||||
|
||||
# Load models — prefer TF-specific file; fall back to generic
|
||||
hmm_path = hmm_model_path(tf)
|
||||
# Load models — prefer broker+TF specific file; fall back to generic
|
||||
hmm_path = hmm_model_path(tf, broker)
|
||||
if not hmm_path.exists():
|
||||
hmm_path = HMM_GENERIC_PATH
|
||||
try:
|
||||
@@ -654,7 +654,7 @@ def _run_loop_inner(tf: str, broker: str, account_size: float, mt5,
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError("HMM model not found. Run --mode train first.")
|
||||
|
||||
xgb_path = get_ensemble_path(tf)
|
||||
xgb_path = get_ensemble_path(tf, broker)
|
||||
if not xgb_path.exists():
|
||||
xgb_path = XGB_GENERIC_PATH
|
||||
try:
|
||||
|
||||
+9
-5
@@ -36,7 +36,9 @@ except ImportError:
|
||||
_PSUTIL_OK = False
|
||||
logger.debug("psutil not installed — RAM guard disabled.")
|
||||
|
||||
STUDY_DB = "sqlite:///models/study.db"
|
||||
def _study_db(broker: str) -> str:
|
||||
"""Return the SQLite storage URL for a given broker, e.g. sqlite:///models/study_headway_cent.db."""
|
||||
return f"sqlite:///models/study_{broker}.db"
|
||||
DD_HARD_LIMIT = 0.15
|
||||
# Minimum OOS trades before a trial is considered scoreable.
|
||||
# M5 has 288 bars/day — the OOS window (~2yr) allows up to ~870 trades at
|
||||
@@ -350,12 +352,13 @@ def run_optimization(
|
||||
"""
|
||||
tier = _get_tier(balance)
|
||||
name = _study_name(broker=broker, tier=tier, tf=tf)
|
||||
storage = _study_db(broker)
|
||||
|
||||
os.makedirs("models", exist_ok=True)
|
||||
|
||||
study = optuna.create_study(
|
||||
study_name=name,
|
||||
storage=STUDY_DB,
|
||||
storage=storage,
|
||||
direction="maximize",
|
||||
load_if_exists=True, # crash-safe resume
|
||||
pruner=optuna.pruners.MedianPruner(),
|
||||
@@ -370,7 +373,7 @@ def run_optimization(
|
||||
if already_done > 0:
|
||||
pct = already_done / n_trials * 100
|
||||
print(
|
||||
f"\nFailsafe: {already_done}/{n_trials} trials already in study.db "
|
||||
f"\nFailsafe: {already_done}/{n_trials} trials already in study_{broker}.db "
|
||||
f"({pct:.0f}% complete). "
|
||||
f"Resuming from trial #{already_done + 1} — {remaining} remaining.\n"
|
||||
)
|
||||
@@ -450,8 +453,9 @@ def get_best_params(
|
||||
"""
|
||||
tier = _get_tier(balance)
|
||||
name = _study_name(broker=broker, tier=tier, tf=tf)
|
||||
storage = _study_db(broker)
|
||||
try:
|
||||
study = optuna.load_study(study_name=name, storage=STUDY_DB)
|
||||
study = optuna.load_study(study_name=name, storage=storage)
|
||||
return study.best_params
|
||||
except Exception:
|
||||
if tier == "small":
|
||||
@@ -463,5 +467,5 @@ def get_best_params(
|
||||
"Growth study '%s' not found — using small-tier params from '%s'.",
|
||||
name, fallback,
|
||||
)
|
||||
study = optuna.load_study(study_name=fallback, storage=STUDY_DB)
|
||||
study = optuna.load_study(study_name=fallback, storage=storage)
|
||||
return study.best_params
|
||||
|
||||
+5
-5
@@ -127,8 +127,8 @@ def run_validation(
|
||||
logger.info("Loaded %d bars from %s for validation", len(df), sync_data_path)
|
||||
df = _apply_features(df, tf, obs_cov, trans_cov)
|
||||
|
||||
# Load models — prefer TF-specific file; fall back to generic
|
||||
hmm_path = hmm_model_path(tf)
|
||||
# Load models — prefer broker+TF specific file; fall back to generic
|
||||
hmm_path = hmm_model_path(tf, broker)
|
||||
if not hmm_path.exists():
|
||||
hmm_path = HMM_GENERIC_PATH
|
||||
if hmm_path.exists():
|
||||
@@ -144,13 +144,13 @@ def run_validation(
|
||||
"Run python main.py --mode train first."
|
||||
)
|
||||
|
||||
xgb_path = get_ensemble_path(tf)
|
||||
xgb_path = get_ensemble_path(tf, broker)
|
||||
if not xgb_path.exists():
|
||||
xgb_path = XGB_GENERIC_PATH
|
||||
if xgb_path.exists():
|
||||
logger.warning(
|
||||
"No TF-specific XGB ensemble found for %s; falling back to %s. "
|
||||
"Run --mode train --tf %s to create a dedicated model.", tf, xgb_path, tf
|
||||
"No broker+TF XGB ensemble found for %s/%s; falling back to %s. "
|
||||
"Run --mode train --tf %s --broker %s to create a dedicated model.", tf, broker, xgb_path, tf, broker
|
||||
)
|
||||
try:
|
||||
models_xgb, thresholds_xgb, _ = load_xgb_ensemble(xgb_path)
|
||||
|
||||
Reference in New Issue
Block a user