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:
@@ -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 ──────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user