mirror of
https://github.com/lucasmos/GoldRegime_X.git
synced 2026-11-02 06:26:15 +00:00
feat: M5 scalping refinements — profit guard, tighter TPs, threshold tuning
optimizer.py: - Narrow M5 prob_threshold search to 0.50-0.53 (was 0.50-0.55) for high-frequency sensitivity; H1/M15 unchanged at 0.50-0.58 mt5_trader.py: - TF_PROB_THRESHOLD fallbacks: M5 0.70->0.52, M15/H1 0.65->0.54 - TF_SHORT_THRESHOLD fallbacks: M5 0.30->0.48, M15/H1 0.35->0.46 (now symmetric with buy thresholds; Optuna / CLI overrides take priority) - TF_TP_CONFIG: M5 runner 2.0->1.5 ATR (realistic scalp target); M5 chop 0.6->0.5; M15 TP1 1.5->1.0, runner 3.0->2.0, chop 1.5->0.8; H1 chop 1.5->1.0 - Add _apply_profit_guard(): moves SL to entry+2*spread when price reaches 70% of TP1 distance — fires before TP1 fills, applies to all TFs - signal_tracker gains tp1_level and guard_hit fields - run_live_loop() accepts prob_threshold_override / short_threshold_override main.py: - Add --prob_threshold and --short_threshold CLI args for live mode override e.g. --mode live --tf M5 --prob_threshold 0.52 --short_threshold 0.48 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
d1bf7af883
commit
bb04b54760
@@ -346,7 +346,9 @@ def cmd_live(args):
|
||||
"Starting live loop — TF=%s broker=%s balance=$%.0f dry_run=%s",
|
||||
tf, args.broker, balance, dry_run,
|
||||
)
|
||||
run_live_loop(tf=tf, broker=args.broker, account_size=balance, dry_run=dry_run)
|
||||
run_live_loop(tf=tf, broker=args.broker, account_size=balance, dry_run=dry_run,
|
||||
prob_threshold_override=getattr(args, "prob_threshold", None),
|
||||
short_threshold_override=getattr(args, "short_threshold", None))
|
||||
|
||||
|
||||
def cmd_report(args):
|
||||
@@ -553,6 +555,10 @@ def main():
|
||||
help="Account type for --mode live. 'live' requires confirmation.")
|
||||
parser.add_argument("--yes", action="store_true",
|
||||
help="Skip the interactive live-account confirmation (used when launched as a subprocess).")
|
||||
parser.add_argument("--prob_threshold", type=float, default=None,
|
||||
help="Override BUY probability threshold for --mode live (skips Optuna lookup).")
|
||||
parser.add_argument("--short_threshold", type=float, default=None,
|
||||
help="Override SELL probability threshold for --mode live (skips Optuna lookup).")
|
||||
|
||||
args = parser.parse_args()
|
||||
{
|
||||
|
||||
+62
-8
@@ -56,8 +56,8 @@ MIN_SPREAD_RATIO = 1.5 # TP1 must be at least 1.5× spread to b
|
||||
TF_ATR_MULTIPLIER = {"M5": 1.5, "M15": 2.0, "H1": 2.0}
|
||||
|
||||
# ── Per-timeframe signal thresholds and order parameters ──────────────────────
|
||||
TF_PROB_THRESHOLD = {"M5": 0.70, "M15": 0.65, "H1": 0.65}
|
||||
TF_SHORT_THRESHOLD = {"M5": 0.30, "M15": 0.35, "H1": 0.35} # fallback only
|
||||
TF_PROB_THRESHOLD = {"M5": 0.52, "M15": 0.54, "H1": 0.54} # fallback — Optuna value used when available
|
||||
TF_SHORT_THRESHOLD = {"M5": 0.48, "M15": 0.46, "H1": 0.46} # fallback only
|
||||
TF_DEFAULT_DEV = {"M5": 30, "M15": 20, "H1": 20}
|
||||
TF_HIGH_VOL_DEV = {"M5": 50, "M15": 50, "H1": 50}
|
||||
|
||||
@@ -68,9 +68,9 @@ TF_HIGH_VOL_DEV = {"M5": 50, "M15": 50, "H1": 50}
|
||||
# M5 uses tighter mults (0.8 / 2.0) — TP1 locks in quick profit, TP2 is realistic
|
||||
# for a scalp runner vs the original [1.0, 3.0] which rarely filled on M5.
|
||||
TF_TP_CONFIG = {
|
||||
"M5": {"trending": [0.8, 2.0], "chop": [0.6]},
|
||||
"M15": {"trending": [1.5, 3.0], "chop": [1.5]},
|
||||
"H1": {"trending": [1.5, 3.0], "chop": [1.5]},
|
||||
"M5": {"trending": [0.8, 1.5], "chop": [0.5]}, # fast partial 0.8x, runner 1.5x
|
||||
"M15": {"trending": [1.0, 2.0], "chop": [0.8]},
|
||||
"H1": {"trending": [1.5, 3.0], "chop": [1.0]},
|
||||
}
|
||||
|
||||
# Lazy MT5 timeframe map
|
||||
@@ -277,6 +277,47 @@ def _move_sl_to_breakeven(ticket: int, entry_price: float, mt5) -> None:
|
||||
ticket, res.retcode if res else "None")
|
||||
|
||||
|
||||
def _apply_profit_guard(signal_tracker: dict, mt5) -> None:
|
||||
"""Move SL to entry + 2×spread once price reaches 70% of TP1 distance.
|
||||
|
||||
Fires once per signal (guarded by signal_tracker["guard_hit"]). Applies
|
||||
to all open tickets in the tracker regardless of timeframe. This protects
|
||||
profit before TP1 fills by effectively making the position risk-free.
|
||||
"""
|
||||
if signal_tracker.get("guard_hit") or signal_tracker.get("tp1_hit"):
|
||||
return # already protected or TP1 already hit
|
||||
tp1_level = signal_tracker.get("tp1_level")
|
||||
entry = signal_tracker.get("entry_price", 0.0)
|
||||
direction = signal_tracker.get("direction")
|
||||
tickets = signal_tracker.get("tickets", [])
|
||||
if not tp1_level or not tickets or not direction:
|
||||
return
|
||||
|
||||
tick = mt5.symbol_info_tick(DEFAULT_SYMBOL)
|
||||
if not tick:
|
||||
return
|
||||
|
||||
spread = tick.ask - tick.bid
|
||||
tp1_dist = abs(tp1_level - entry)
|
||||
guard_buffer = tp1_dist * 0.70 # trigger at 70% of the way to TP1
|
||||
|
||||
if direction == "BUY":
|
||||
triggered = tick.bid >= entry + guard_buffer
|
||||
new_sl = round(entry + spread * 2, 2)
|
||||
else:
|
||||
triggered = tick.ask <= entry - guard_buffer
|
||||
new_sl = round(entry - spread * 2, 2)
|
||||
|
||||
if triggered:
|
||||
for ticket in tickets:
|
||||
_move_sl_to_breakeven(ticket, new_sl, mt5)
|
||||
signal_tracker["guard_hit"] = True
|
||||
logger.info(
|
||||
"Profit guard triggered: entry=%.2f new_sl=%.2f (70%% of TP1 at %.2f)",
|
||||
entry, new_sl, tp1_level,
|
||||
)
|
||||
|
||||
|
||||
def _close_position(ticket: int, mt5) -> None:
|
||||
"""Close a specific open position at market price."""
|
||||
positions = mt5.positions_get(ticket=ticket)
|
||||
@@ -512,6 +553,8 @@ def run_live_loop(
|
||||
broker: str = "headway_cent",
|
||||
account_size: float = None,
|
||||
dry_run: bool = False,
|
||||
prob_threshold_override: float = None,
|
||||
short_threshold_override: float = None,
|
||||
) -> None:
|
||||
"""Connect to MT5 and run the signal → order loop until interrupted.
|
||||
|
||||
@@ -598,6 +641,11 @@ def _run_loop_inner(tf: str, broker: str, account_size: float, dry_run: bool, mt
|
||||
else:
|
||||
prob_threshold = TF_PROB_THRESHOLD.get(tf.upper(), 0.65)
|
||||
short_threshold = TF_SHORT_THRESHOLD.get(tf.upper(), 0.35)
|
||||
# CLI override has highest priority — applied after Optuna resolution
|
||||
if prob_threshold_override is not None:
|
||||
prob_threshold = prob_threshold_override
|
||||
if short_threshold_override is not None:
|
||||
short_threshold = short_threshold_override
|
||||
|
||||
# Session state (persists across bars within a day)
|
||||
daily_trades = 0
|
||||
@@ -606,7 +654,8 @@ def _run_loop_inner(tf: str, broker: str, account_size: float, dry_run: bool, mt
|
||||
# Tracks the tickets and entry context of the most recently placed signal
|
||||
# so the break-even SL logic can fire when TP1 closes position 1.
|
||||
signal_tracker = {"tickets": [], "entry_price": 0.0,
|
||||
"direction": None, "tp1_hit": False}
|
||||
"direction": None, "tp1_hit": False,
|
||||
"tp1_level": None, "guard_hit": False}
|
||||
|
||||
arm = AdaptiveRiskManager(account_size, tf=tf)
|
||||
logger.info(
|
||||
@@ -616,7 +665,8 @@ def _run_loop_inner(tf: str, broker: str, account_size: float, dry_run: bool, mt
|
||||
logger.info(
|
||||
"Signal thresholds — BUY>%.3f SELL<%.3f (source: %s)",
|
||||
prob_threshold, short_threshold,
|
||||
"optuna" if prob_threshold_opt is not None else "hardcoded",
|
||||
"override" if (prob_threshold_override or short_threshold_override)
|
||||
else ("optuna" if prob_threshold_opt is not None else "hardcoded"),
|
||||
)
|
||||
|
||||
while True:
|
||||
@@ -702,6 +752,9 @@ def _run_loop_inner(tf: str, broker: str, account_size: float, dry_run: bool, mt
|
||||
# Log P&L for any positions that closed since last bar
|
||||
if closed:
|
||||
_log_closed_pnl(closed, mt5)
|
||||
# Profit guard: move SL to entry+spread when 70% to TP1 (all TFs)
|
||||
if active:
|
||||
_apply_profit_guard(signal_tracker, mt5)
|
||||
# TP1 hit: first position gone → move runner's SL to break-even
|
||||
if len(active) < len(signal_tracker["tickets"]) and not signal_tracker["tp1_hit"]:
|
||||
signal_tracker["tp1_hit"] = True
|
||||
@@ -797,7 +850,8 @@ def _run_loop_inner(tf: str, broker: str, account_size: float, dry_run: bool, mt
|
||||
|
||||
# ── 13. Send order(s) with state-aware staged TPs ─────────────
|
||||
signal_tracker = {"tickets": [], "entry_price": entry_price,
|
||||
"direction": direction, "tp1_hit": False}
|
||||
"direction": direction, "tp1_hit": False,
|
||||
"tp1_level": tp_levels[0], "guard_hit": False}
|
||||
logger.info(
|
||||
"SIGNAL %s | state=%d | prob=%.3f | lot×%d=%.2f | sl=%.2f | tp=%s | dev=%d",
|
||||
direction, hmm_state, prob, pos_per_trade, lot_per_pos,
|
||||
|
||||
+1
-1
@@ -109,7 +109,7 @@ def make_objective(balance: float = 15.0, broker: str = "standard", tf: str = "H
|
||||
# 0.42 (was 0.35) for symmetric buy/sell sensitivity.
|
||||
# short_threshold must stay below prob_threshold (no-trade zone must exist).
|
||||
if tf.upper() == "M5":
|
||||
prob_threshold = trial.suggest_float("prob_threshold", 0.50, 0.55)
|
||||
prob_threshold = trial.suggest_float("prob_threshold", 0.50, 0.53)
|
||||
short_threshold = trial.suggest_float("short_threshold", 0.44, 0.50)
|
||||
else:
|
||||
prob_threshold = trial.suggest_float("prob_threshold", 0.50, 0.58)
|
||||
|
||||
Reference in New Issue
Block a user