Fix on Sell order lossing by blocking time
This commit is contained in:
@@ -97,6 +97,7 @@ Tracked currencies `["USD","XAU","EUR"]`; pre-event buffer 30 min, post-event 15
|
||||
| `sell_min_quality` | ["A+","A","B"] | SELLs below B are blocked |
|
||||
| `sell_vol_regimes` | ["high","extreme"] | SELLs only in these volatility regimes (quiet shorts fade vs gold's macro uptrend) |
|
||||
| `blocked_regimes` / `sell_blocked_regimes` | [] / [] | per-TF factory opt-ins. M15 factory sets `blocked_regimes=["trending"]` (mixed regime: strong ADX, EMAs unaligned — lost −6.8R/24) |
|
||||
| `sell_blocked_hours_utc` | [] | bar-CLOSE UTC hours where SELLs are blocked (BUYs unaffected). Per-TF factory opt-in: M15 sets [15, 17] (NY-session shorts, −33.5R/165 over 5 yr, negative 6/6 years both halves; BUYs there +34.3R) |
|
||||
| M5 gates | | `m5_require_mtf=True`; `m5_min_abs_score=30`; `m5_min_confirmation_score=10`; `m5_sell_max_quality=["B","C"]` (A-grade M5 SELLs enter at dip extremes and lose); `m5_blocked_vol_regimes=["high"]` (chop transition — 61% WR, negative total); `m5_sell_min_rsi=25` |
|
||||
| M5 pullback (XAU_PB) | | `m5_pullback_enabled=True`; min score 15 (lower than gate 1 BY DESIGN — mid-pullback scores weaker); ADX ≥ 20; RSI zones BUY (35,62) / SELL (38,65); retrace depth 0.8–6.0 ATR from the 40-bar swing extreme; band touch within 3 bars. Validated +38.3R/254 trades on 12mo |
|
||||
|
||||
@@ -197,6 +198,7 @@ Two entry points:
|
||||
- **6f MTF opposition**: ≥2 TFs pointing against the trade direction → D (never trade against consensus).
|
||||
- **6g XAU SELL volatility gate**: sell-direction & vol regime not in {high, extreme} → D.
|
||||
- **6g2 XAU regime gates**: regime in `blocked_regimes` (M15: mixed `trending`) → D; sell-direction & regime in `sell_blocked_regimes` → D.
|
||||
- **6g3 XAU SELL hour gate**: sell-direction & bar-CLOSE hour (label + candle duration — the entry hour under next-open fills) in `sell_blocked_hours_utc` → D. Per-TF opt-in; M15 factory blocks 15/17 UTC.
|
||||
- **6h EUR gates** (EUR only): blocked regimes (ranging/transitioning), quality < B, exhaustion count, session hours, |score| < min_entry_score, conf < min_entry_confidence, blocked vol regimes, indicator-agreement floor, RSI-extended block → each sets grade F with a warning.
|
||||
- **6i XAU M5 noise gates** (`_apply_m5_filters`, primary TF = M5 only): (1) |score| ≥ 30; (2) MTF agreement required; (3) ≥1 confirmation TF with score ≥ ±10 in trade direction (NEUTRAL confirms don't count); (4) SELL grade must be B/C (A-grade M5 shorts lose −0.017R/trade); (5) vol regime `high` blocked; (6) SELL blocked when RSI < 25. Each failure → D, setup killed.
|
||||
8. **Trade setup**: XAU + grade A+/A → `_generate_xau_atr_trade_setup` first; any pair + grade A+/A/B/C and no setup yet → `_generate_trade_setup` (swing-based).
|
||||
@@ -252,6 +254,7 @@ Default confirmation map (same in both factories, the bot, and backtest usage):
|
||||
**xauusd.py**: `PlatformConfig()` defaults + per-TF overrides (each validated; full evidence in the module docstring):
|
||||
- M5: append hour 9 UTC to blocked hours (6-yr replay: +207.1R vs +178.3R; M15-and-up keep hour 9 — it's positive there).
|
||||
- M15: `signals.tp_floor_rr = 1.0` (1R minimum targets: +116.6R vs +105.4R, better in train/val and 2024 OOS; **hurts M5** — per-TF only) and `xau_strategy.blocked_regimes = ["trending"]` (mixed regime block: +65.7R vs +59.0R). A transitioning-SELL block was tested and REJECTED (bull-market artifact — failed 2024 OOS).
|
||||
- M15: `xau_strategy.sell_blocked_hours_utc = [15, 17]` (SELL-only hour block, validated 2026-07-16 on the 5-yr replay: +176.7R PF 1.31 vs +144.9R PF 1.22, better in all 6 years, both 12-mo halves, and 2024-06→2025-03 OOS; M5 cross-TF pocket confirms; BUYs at those hours are +34.3R so `blocked_hours_utc` would be wrong).
|
||||
- The factory must stay equivalent to inline `PlatformConfig(symbol=XAUUSD, …)` + overrides — **enforced by a test in test_all.py**.
|
||||
|
||||
**eurusd.py**: sets symbol/display/pip, `xau_strategy.enabled = False` (belt-and-braces; XAU gates are also display-gated), `news.event_risk_caps_signals = True`, EUR session (7–20 h window, blocked [22,23,0-3], Mon–Fri), EUR regime-weight overrides (`EUR_REGIME_WEIGHTS`, and `M5_REGIME_WEIGHTS` + tighter trailing 0.45/0.5 + Mon–Thu + extra blocked hours [8,12,13,18,19] + swing lookback 20 on M5). Docstring = the complete no-edge verdict and closed research avenues.
|
||||
|
||||
@@ -228,6 +228,10 @@ class XauStrategyConfig:
|
||||
# Market regimes in which XAU SELL setups may not fire. Empty by
|
||||
# default; per-timeframe factories opt in.
|
||||
sell_blocked_regimes: List[str] = field(default_factory=list)
|
||||
# Bar-CLOSE (decision-time) UTC hours in which XAU SELL setups may
|
||||
# not fire. Empty by default; per-timeframe factories opt in.
|
||||
# BUYs are unaffected (unlike blocked_hours_utc, which is both-way).
|
||||
sell_blocked_hours_utc: List[int] = field(default_factory=list)
|
||||
|
||||
# ── M5-specific filters ──────────────────────────────────────
|
||||
# M5 signals are noisy; require stronger confirmation before
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
01:51:29 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 05:51 is 4320m old (limit: 180m)
|
||||
01:52:02 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 05:52 is 4320m old (limit: 180m)
|
||||
02:42:59 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 06:42 is 4320m old (limit: 180m)
|
||||
03:30:24 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 07:30 is 4320m old (limit: 180m)
|
||||
03:31:32 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 07:31 is 4320m old (limit: 180m)
|
||||
11:30:20 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 15:30 is 4320m old (limit: 180m)
|
||||
11:41:34 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 15:41 is 4320m old (limit: 180m)
|
||||
11:45:46 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 15:45 is 4320m old (limit: 180m)
|
||||
11:56:38 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 15:56 is 4320m old (limit: 180m)
|
||||
11:57:11 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 15:57 is 4320m old (limit: 180m)
|
||||
11:59:52 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 15:59 is 4320m old (limit: 180m)
|
||||
12:10:05 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 16:10 is 4320m old (limit: 180m)
|
||||
13:05:37 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 17:05 is 4320m old (limit: 180m)
|
||||
13:14:25 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 17:14 is 4320m old (limit: 180m)
|
||||
13:38:50 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 17:38 is 4320m old (limit: 180m)
|
||||
13:45:14 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 17:45 is 4320m old (limit: 180m)
|
||||
13:51:33 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 17:51 is 4320m old (limit: 180m)
|
||||
15:04:41 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 19:04 is 4320m old (limit: 180m)
|
||||
15:04:50 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 19:04 is 4320m old (limit: 180m)
|
||||
15:05:17 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 19:05 is 4320m old (limit: 180m)
|
||||
15:06:10 [WARNING] data_provider: BTC/USD 1h: last bar 2026-07-11 19:06 is 4320m old (limit: 180m)
|
||||
@@ -27,6 +27,26 @@ exclude it), so the block is M15-only. A second candidate — blocking
|
||||
transitioning-regime SELLs (-4.0R/47 on 12mo) — was REJECTED: those
|
||||
sells made +5.7R/32 in the 2024 OOS window (bull-market artifact).
|
||||
|
||||
M15 override 3 (validated 2026-07-16, 5-year replay 2021-07-17 ->
|
||||
2026-07-16 under the audit-fixed backtester, $0.30 spread, trailing
|
||||
exits): block SELL entries whose bar CLOSES at 15:00 or 17:00 UTC
|
||||
(sell_blocked_hours_utc=[15, 17], gate 6g3 — BUYs unaffected; those
|
||||
hours flank the already-blocked hour 16). The pocket lost -33.5R/165
|
||||
trades, negative in 6/6 calendar years and in BOTH halves of the last
|
||||
12 months (H1 -6.8R, H2 -4.2R); each hour independently is 5/6 years
|
||||
negative with both halves negative (hours 14/18 fail that screen and
|
||||
stay open). Cross-TF the M5 book CONFIRMS (same pocket -14.1R/59,
|
||||
5/6 years negative) rather than contradicts — unlike the rejected
|
||||
trending_down block. Full replay with the gate: +176.7R/1627 PF 1.31
|
||||
DD 29.1R vs +144.9R/1781 PF 1.22 DD 34.4R control; better in ALL SIX
|
||||
years; last-12mo halves both improve (H1 +36.3 vs +29.5, H2 +46.9 vs
|
||||
+43.2; 12mo PF 1.55 vs 1.43); OOS 2024-06-01 -> 2025-03-30 improves
|
||||
(+45.4R PF 1.47 DD 17.8 vs +41.9R PF 1.39 DD 21.7). SELL side goes
|
||||
-1.2R -> +30.6R over 5 years. BUYs at those hours made +34.3R, hence
|
||||
SELL-only rather than a blocked_hours_utc entry. Mechanism: NY-session
|
||||
shorts against gold's macro uptrend (15:00 = post-US-data reversal
|
||||
window, 17:00 = London close unwind).
|
||||
|
||||
M5 override (validated 2026-07-11, full-history replay 2020-03-17 ->
|
||||
2026-07-11 + 12mo book, $0.30 spread): block hour 9 UTC (post-London-
|
||||
open whipsaw). Hour-9 entries lost -28.9R/143 over 6 years, negative
|
||||
@@ -68,4 +88,6 @@ def make_config(
|
||||
config.signals.tp_floor_rr = 1.0
|
||||
# Block the mixed "trending" regime on M15 only — see docstring.
|
||||
config.xau_strategy.blocked_regimes = ["trending"]
|
||||
# Block SELL entries at 15:00/17:00 UTC on M15 only — see docstring.
|
||||
config.xau_strategy.sell_blocked_hours_utc = [15, 17]
|
||||
return config
|
||||
|
||||
@@ -804,6 +804,28 @@ class SignalAggregator:
|
||||
)
|
||||
report.signal_quality = "D"
|
||||
report.trade_setup = None
|
||||
elif xau_is_sell_dir and self.config.xau_strategy.sell_blocked_hours_utc:
|
||||
# ── 6g3. XAU SELL hour gate (opt-in via per-TF factories) ──
|
||||
# Evaluated on the bar's CLOSE time (= entry time under
|
||||
# next-open fills), the same clock the session filter and
|
||||
# backtest replay use — NOT the bar label hour.
|
||||
try:
|
||||
close_h = (
|
||||
pd.Timestamp(report.timestamp)
|
||||
+ pd.Timedelta(
|
||||
seconds=ForexDataProvider.CANDLE_SECONDS[
|
||||
self.config.primary_timeframe
|
||||
]
|
||||
)
|
||||
).hour
|
||||
except Exception:
|
||||
close_h = None
|
||||
if close_h in self.config.xau_strategy.sell_blocked_hours_utc:
|
||||
report.warnings.append(
|
||||
f"SELL FILTER: {close_h:02}:00 UTC entry blocked for shorts"
|
||||
)
|
||||
report.signal_quality = "D"
|
||||
report.trade_setup = None
|
||||
|
||||
# ── 6h. EUR/USD regime + quality + risk gate ──────────────
|
||||
if report.display_name == "EUR/USD" and self.config.eur_strategy.enabled:
|
||||
|
||||
+54
-2
@@ -198,6 +198,8 @@ def _():
|
||||
assert xc.tp_atr_multiplier == 1.0
|
||||
assert xc.sl_atr_multiplier == 2.0
|
||||
assert isinstance(xc.blocked_hours_utc, list)
|
||||
assert xc.sell_blocked_hours_utc == [], \
|
||||
"sell hour gate must be off by default (per-TF factories opt in)"
|
||||
|
||||
|
||||
@test("FilterConfig defaults")
|
||||
@@ -1846,10 +1848,11 @@ def _():
|
||||
)
|
||||
if tf == Timeframe.M15:
|
||||
# Documented per-TF overrides (see xauusd.py docstring):
|
||||
# 1R minimum targets + mixed-"trending"-regime block,
|
||||
# both validated for M15 only.
|
||||
# 1R minimum targets + mixed-"trending"-regime block +
|
||||
# SELL hour block (15/17 UTC), all validated for M15 only.
|
||||
inline_cfg.signals.tp_floor_rr = 1.0
|
||||
inline_cfg.xau_strategy.blocked_regimes = ["trending"]
|
||||
inline_cfg.xau_strategy.sell_blocked_hours_utc = [15, 17]
|
||||
if tf == Timeframe.M5:
|
||||
# Documented per-TF override (see xauusd.py docstring):
|
||||
# hour 9 UTC blocked on M5 only.
|
||||
@@ -2063,6 +2066,55 @@ def _():
|
||||
assert make_config(Timeframe.M5).xau_strategy.blocked_regimes == []
|
||||
|
||||
|
||||
@test("XAU SELL hour gate: blocks 15/17 UTC bar-close entries, spares others")
|
||||
def _():
|
||||
from pair_signals.xauusd import make_config
|
||||
|
||||
def xau_15min_df(end):
|
||||
np.random.seed(9)
|
||||
dates = pd.date_range(end=pd.Timestamp(end, tz="UTC"),
|
||||
periods=300, freq="15min")
|
||||
close = 3300.0 + np.cumsum(np.random.randn(300) * 2.0 - 1.0)
|
||||
return pd.DataFrame({
|
||||
"Open": close + np.random.randn(300) * 0.5,
|
||||
"High": close + np.abs(np.random.randn(300)),
|
||||
"Low": close - np.abs(np.random.randn(300)),
|
||||
"Close": close,
|
||||
"Volume": np.random.randint(100, 10000, 300).astype(float),
|
||||
}, index=pd.DatetimeIndex(dates, name="Datetime"))
|
||||
|
||||
def run(end, hours):
|
||||
cfg = make_config(Timeframe.M15)
|
||||
cfg.backtest.session_filter = False
|
||||
# Neutralize the other SELL gates so 6g3 is the one under test.
|
||||
cfg.xau_strategy.sell_min_quality = ["A+", "A", "B", "C"]
|
||||
cfg.xau_strategy.sell_vol_regimes = ["low", "normal", "high", "extreme"]
|
||||
cfg.xau_strategy.blocked_regimes = []
|
||||
cfg.xau_strategy.sell_blocked_hours_utc = hours
|
||||
df = xau_15min_df(end)
|
||||
dfs = {Timeframe.M15: df, Timeframe.M5: df, Timeframe.H1: df}
|
||||
return SignalAggregator(cfg).analyze_from_dataframes(
|
||||
symbol=XAUUSD, dfs=dfs, use_news=False)
|
||||
|
||||
# Same synthetic downtrend; only the index end (and thus the bar-close
|
||||
# hour) differs. Label 14:45 closes 15:00 (blocked); 19:45 closes
|
||||
# 20:00 (allowed). The gate reads bar-CLOSE time, not the label hour.
|
||||
control = run("2026-07-13 14:45", [])
|
||||
assert control.signal_quality in ("A+", "A", "B", "C") and control.final_score < 0, \
|
||||
f"probe data must yield an actionable sell, got {control.signal_quality}"
|
||||
blocked = run("2026-07-13 14:45", [15, 17])
|
||||
assert blocked.signal_quality == "D" and blocked.trade_setup is None, \
|
||||
f"15:00 UTC close must be gated to D, got {blocked.signal_quality}"
|
||||
assert any("blocked for shorts" in w for w in blocked.warnings), \
|
||||
f"missing SELL hour warning: {blocked.warnings}"
|
||||
open_hour = run("2026-07-13 19:45", [15, 17])
|
||||
assert open_hour.signal_quality == control.signal_quality, \
|
||||
f"20:00 UTC close must pass the gate, got {open_hour.signal_quality}"
|
||||
# Factory scoping: M15 opts in, M5 does not (its book must not move).
|
||||
assert make_config(Timeframe.M15).xau_strategy.sell_blocked_hours_utc == [15, 17]
|
||||
assert make_config(Timeframe.M5).xau_strategy.sell_blocked_hours_utc == []
|
||||
|
||||
|
||||
@test("NewsEngine._get_affected_pairs includes EUR/USD for EUR and USD events")
|
||||
def _():
|
||||
from news_engine import NewsEngine
|
||||
|
||||
Reference in New Issue
Block a user