Add chart-pattern entry archetype (XAU_CP) — disabled by default

Third entry archetype: completion of classical chart patterns (double
bottom/top, H&S top / inverse H&S, bull/bear flag) — the tight canon
with the strongest documented stats. New chart_patterns.py detector
module (pure functions, ATR-unit geometry, closed-bar slices only,
first-close-beyond trigger discipline so a breakout fires exactly
once and never repaints).

Pipeline step 10 in analyze_from_dataframes: fills only empty setup
slots, allows weak-composite F grades (normal at reversal completion)
but inherits session/day blocks, mirrors MTF-opposition + SELL
vol/hour + M5 protections. Geometry: SL = pattern anchor -/+ 0.5 ATR
(min $8), TP1 = measured move, $9 spread gate, trailing exit does the
real work. Strategy tag XAU_CP.

cp_enabled defaults to False everywhere — per-TF adoption happens in
pair_signals/xauusd.py only after the standard validation rule passes.
Backtester: --cp / --cp-patterns flags to test the candidate, XAU_CP
tag recognition.

Suite: 183/183 (21 new tests: detectors, trigger discipline,
invalidation, gates, pipeline, default-off safety).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kby7M9JwpYi8NQT2yoxAi
This commit is contained in:
Harwinder Sekhon
2026-07-16 14:23:17 -04:00
parent 68a62b0a53
commit a70fc91f96
5 changed files with 801 additions and 1 deletions
+153
View File
@@ -13,6 +13,7 @@ from typing import Dict, List, Optional
import pandas as pd
from chart_patterns import detect_chart_pattern
from config import PlatformConfig, SignalStrength, Timeframe
from data_provider import ForexDataProvider
from engine import TechnicalEngine, TechnicalAnalysis
@@ -98,6 +99,8 @@ class SignalAggregator:
# Diagnostic funnel for the M5 pullback route (shared across
# instances; read by backtest tooling, harmless in live use).
pb_funnel: Dict[str, int] = {}
# Same, for the chart-pattern route (XAU_CP).
cp_funnel: Dict[str, int] = {}
def __init__(self, config: PlatformConfig):
self.config = config
@@ -563,6 +566,138 @@ class SignalAggregator:
target = report.bullish_reasons if is_buy else report.bearish_reasons
target.insert(0, marker)
# ── Chart-pattern entry (XAU_CP) ────────────────────────────────
def _try_chart_pattern(
self, report: SignalReport, primary_df: pd.DataFrame, pip_value: float
) -> None:
"""Build an XAU_CP trade setup if a completed chart pattern
passes the pair-level protections.
Unlike the composite paths there is no minimum-score gate: a
reversal pattern completes exactly where the composite is still
pointing the old way. Protection comes from the pattern's own
structural gates plus the validated directional filters below.
"""
cfg = self.config.xau_strategy
funnel = SignalAggregator.cp_funnel
def _reject(reason: str) -> None:
funnel[reason] = funnel.get(reason, 0) + 1
pa = report.primary_analysis
if pa is None:
return
atr = pa.key_levels.get("atr", 0.0)
# Same $-ATR floor the swing generator applies (dead tape).
if atr < cfg.min_atr_threshold:
return
pat = detect_chart_pattern(primary_df, atr, cfg)
if pat is None:
return
_reject("detected")
direction = pat["direction"]
is_buy = direction == "BUY"
# Never against multi-TF consensus (mirror of gate 6f).
sigs = list(report.mtf_signals.values())
bull_n = sum(1 for s in sigs if s in (SignalStrength.BUY, SignalStrength.STRONG_BUY))
bear_n = sum(1 for s in sigs if s in (SignalStrength.SELL, SignalStrength.STRONG_SELL))
thr = self.config.signals.mtf_agreement_threshold
if (is_buy and bear_n >= thr) or (not is_buy and bull_n >= thr):
_reject("mtf_opposition")
return
# Directional protections — gold's macro-uptrend asymmetry is
# structural, so the validated SELL gates apply to pattern
# shorts too (quiet-vol shorts fade, blocked SELL hours lose).
if not is_buy:
if (report.volatility_regime
and report.volatility_regime not in cfg.sell_vol_regimes):
_reject("sell_vol")
return
if cfg.sell_blocked_hours_utc:
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 cfg.sell_blocked_hours_utc:
_reject("sell_hour")
return
if self.config.primary_timeframe == Timeframe.M5:
rsi_ind = pa.indicators.get("rsi")
if rsi_ind is not None and rsi_ind.value < cfg.m5_sell_min_rsi:
_reject("m5_sell_rsi")
return
# M5 chop-transition volatility block (validated, both ways).
if (self.config.primary_timeframe == Timeframe.M5
and report.volatility_regime in cfg.m5_blocked_vol_regimes):
_reject("vol_regime")
return
entry = report.current_price
min_sl_price = self.config.signals.min_sl_pips * pip_value
height = pat["height"]
if is_buy:
sl_dist = max(entry - (pat["anchor"] - 0.5 * atr), min_sl_price)
sl = entry - sl_dist
tp1 = entry + height
tp2 = entry + 1.5 * height
else:
sl_dist = max((pat["anchor"] + 0.5 * atr) - entry, min_sl_price)
sl = entry + sl_dist
tp1 = entry - height
tp2 = entry - 1.5 * height
# The measured move IS the pattern target — if it can't clear
# the RR floor or the spread gate, the pattern is untradeable.
rr = height / sl_dist if sl_dist > 0 else 0.0
if rr < self.config.signals.min_rr_ratio - 1e-9:
_reject("rr")
return
min_tp = (self.config.signals.round_trip_cost
* self.config.signals.min_tp_cost_multiple)
if height < min_tp:
_reject("spread_gate")
return
report.trade_setup = TradeSetup(
pair=report.symbol,
display_name=report.display_name,
direction=direction,
entry_price=entry,
stop_loss=sl,
take_profit_1=tp1,
take_profit_2=tp2,
risk_reward=round(rr, 2),
risk_pips=sl_dist / pip_value,
reward_pips=height / pip_value,
pip_value=pip_value,
strategy="XAU_CP",
)
if report.signal_quality not in ("A+", "A", "B"):
report.signal_quality = "B"
report.final_signal = (SignalStrength.BUY if is_buy
else SignalStrength.SELL)
report.action_recommendation = (
f"{direction} — {pat['pattern']} pattern completion"
)
marker = (f"XAU CHART PATTERN: {pat['pattern']} "
f"({direction}) break of {pat['trigger']:.2f}, "
f"height {height / atr:.1f} ATR")
target = report.bullish_reasons if is_buy else report.bearish_reasons
target.insert(0, marker)
_reject("accepted")
# ── Public: bar-replayable entry point ─────────────────────────
def analyze_from_dataframes(
@@ -825,6 +960,24 @@ class SignalAggregator:
and report.signal_quality != "F"):
self._try_m5_pullback(report, primary_df, pip_value)
# ── 10. Chart-pattern entry (XAU_CP) ────────────────────────
# Third archetype: completion of a classical chart pattern
# (double top/bottom, H&S, flag — see chart_patterns.py). Only
# fills empty slots (core + pullback setups take precedence).
# Unlike XAU_PB this must NOT require quality != "F": a weak
# composite (grade F on |score| < 15) is the normal state at a
# reversal-pattern completion. Session/day blocks still bind —
# they are detected by their warnings (and the live wrapper +
# backtest replay apply the bar-close session predicate again
# on top, so a pattern setup can never trade a blocked hour).
if (report.display_name == "XAU/USD"
and self.config.xau_strategy.cp_enabled
and report.trade_setup is None
and not any(w.startswith(("SESSION FILTER", "DAY FILTER",
"WEEKEND FILTER"))
for w in report.warnings)):
self._try_chart_pattern(report, primary_df, pip_value)
return report
# ── Internals ──────────────────────────────────────────────────