Conviction campaign 2026-09-27 (FAILED, research knob kept) + plain-words alert wording
Owner question: can score and confidence be combined into one number that is easier to understand and for the system to decide on? Research (pre-registered, nothing ships to the signal path): - signals.grading_mode: "pair" (default, the shipped two-floor rule, byte- identical: all five reference MD5s reproduce on both scripts), "min" (min(|score|, 0.6*conf)), "product" (|score|*conf/100); thresholds fixed from the control's corner points, no tuning. Helpers _final_signal_and_grade / _transitioning_boost / conviction / CONVICTION_THRESHOLDS in signal_aggregator; mirrored in K2 (GRADING_MODE_CODES, PF_conv_* packed from the live module); twin refuses other values; M15 OOS MD5 equal on both scripts for both modes. - Verdict: FAILED on M5 and M15. K-MIN moves 43-54 trades for -1.2R / -2.9R (clauses a,b,e / a,b,d); K-PROD -29R on M15 (PF 1.29, W3 DD 28.9R), neutral on M5. H1/H4 product +11.9R / +17.0R on W3 from extra low-score trades (H1 PF 1.98 -> 1.75, DD doubles): hypothesis only. - Code review (high) before the runs: 6 findings fixed, 1 recorded (engine_config_fingerprint omits the switch under "pair", so live rows keep their fingerprint). Delivery (books untouched): alerts lead with the grade and print the two raw numbers in words (main.plain_words: "Direction: strong buy · Reliability: good · timeframes agree", cut-offs = the grade table's own) on Telegram, /status, the CLI and the dashboard card (plain_words in the snapshot); "MTF" label replaced by "Other timeframes"; guides updated. Suite 566 passed. Evidence: backtest/research/conviction_campaign_2026-09-27.md
This commit is contained in:
+141
-30
@@ -33,6 +33,128 @@ import signal_log
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ── Grading rule (score + confidence -> final signal and grade) ─────────
|
||||
# "pair" is the shipped rule; "min" / "product" are the 2026-09-27 research
|
||||
# candidates (backtest/research/conviction_campaign_2026-09-27.md). The CUDA
|
||||
# twin (backtest/cuda_signal_kernels.py, K2) mirrors these helpers and packs
|
||||
# CONVICTION_THRESHOLDS from here, so this module is the single source.
|
||||
GRADING_MODES = ("pair", "min", "product")
|
||||
|
||||
# (aplus, a, b, c, strong, buy, boost_a, boost_b) on the conviction scale.
|
||||
# "min": the control's corners on the |score| axis (0.6 = the control's
|
||||
# score / confidence ratio). "product": the control's corner products
|
||||
# 40x65, 33x55, 28x50, 25x45, 35x55, 25x50, 30x50, 25x45, each / 100.
|
||||
CONVICTION_THRESHOLDS = {
|
||||
"min": (40.0, 33.0, 28.0, 25.0, 35.0, 25.0, 30.0, 25.0),
|
||||
"product": (26.0, 18.15, 14.0, 11.25, 19.25, 12.5, 15.0, 11.25),
|
||||
}
|
||||
|
||||
|
||||
def grading_mode_of(signals_cfg) -> str:
|
||||
"""The validated `signals.grading_mode`; an unknown value is a hard error."""
|
||||
mode = signals_cfg.grading_mode
|
||||
if mode not in GRADING_MODES:
|
||||
raise ValueError(
|
||||
f"signals.grading_mode={mode!r}; expected one of {GRADING_MODES}"
|
||||
)
|
||||
return mode
|
||||
|
||||
|
||||
def conviction(abs_score: float, confidence: float, mode: str) -> float:
|
||||
"""The single decision number of the research modes. Evaluation order is
|
||||
load-bearing for the CUDA mirror: min(a, b) with b = 0.6 * confidence in
|
||||
that argument order (PYMIN), product as (abs_score * confidence) / 100."""
|
||||
if mode == "min":
|
||||
return min(abs_score, 0.6 * confidence)
|
||||
if mode == "product":
|
||||
return abs_score * confidence / 100.0
|
||||
raise ValueError(f"conviction() has no formula for grading_mode={mode!r}")
|
||||
|
||||
|
||||
def _signal_pair(score: float, confidence: float) -> SignalStrength:
|
||||
"""The shipped final-signal rule (validated literals; do not edit)."""
|
||||
if score >= 35 and confidence >= 55:
|
||||
return SignalStrength.STRONG_BUY
|
||||
elif score >= 25 and confidence >= 50:
|
||||
return SignalStrength.BUY
|
||||
elif score <= -35 and confidence >= 55:
|
||||
return SignalStrength.STRONG_SELL
|
||||
elif score <= -25 and confidence >= 50:
|
||||
return SignalStrength.SELL
|
||||
return SignalStrength.NEUTRAL
|
||||
|
||||
|
||||
def _quality_pair(abs_score: float, confidence: float, mtf: bool) -> str:
|
||||
"""The shipped grade table (validated literals; do not edit)."""
|
||||
if abs_score >= 40 and confidence >= 65 and mtf:
|
||||
return "A+"
|
||||
elif abs_score >= 33 and confidence >= 55 and mtf:
|
||||
return "A"
|
||||
elif abs_score >= 28 and confidence >= 50:
|
||||
return "B"
|
||||
elif abs_score >= 25 and confidence >= 45:
|
||||
return "C"
|
||||
elif abs_score >= 15:
|
||||
return "D"
|
||||
return "F"
|
||||
|
||||
|
||||
def _signal_conv(score: float, conv: float, mode: str) -> SignalStrength:
|
||||
t_strong, t_buy = CONVICTION_THRESHOLDS[mode][4:6]
|
||||
if score > 0 and conv >= t_strong:
|
||||
return SignalStrength.STRONG_BUY
|
||||
elif score > 0 and conv >= t_buy:
|
||||
return SignalStrength.BUY
|
||||
elif score < 0 and conv >= t_strong:
|
||||
return SignalStrength.STRONG_SELL
|
||||
elif score < 0 and conv >= t_buy:
|
||||
return SignalStrength.SELL
|
||||
return SignalStrength.NEUTRAL
|
||||
|
||||
|
||||
def _quality_conv(abs_score: float, conv: float, mtf: bool, mode: str) -> str:
|
||||
t_aplus, t_a, t_b, t_c = CONVICTION_THRESHOLDS[mode][0:4]
|
||||
if conv >= t_aplus and mtf:
|
||||
return "A+"
|
||||
elif conv >= t_a and mtf:
|
||||
return "A"
|
||||
elif conv >= t_b:
|
||||
return "B"
|
||||
elif conv >= t_c:
|
||||
return "C"
|
||||
elif abs_score >= 15:
|
||||
return "D"
|
||||
return "F"
|
||||
|
||||
|
||||
def _final_signal_and_grade(score: float, confidence: float, mtf: bool, mode: str):
|
||||
"""(final_signal, signal_quality) for the post-penalty score / confidence.
|
||||
"pair" is the shipped rule; the research modes grade on one conviction
|
||||
number. The single dispatch point the CUDA K2 twin mirrors."""
|
||||
if mode == "pair":
|
||||
return _signal_pair(score, confidence), _quality_pair(abs(score), confidence, mtf)
|
||||
conv = conviction(abs(score), confidence, mode)
|
||||
return _signal_conv(score, conv, mode), _quality_conv(abs(score), conv, mtf, mode)
|
||||
|
||||
|
||||
def _transitioning_boost(abs_score: float, confidence: float, mtf: bool, mode: str) -> Optional[str]:
|
||||
"""Gate 6c's grade lift in a transitioning regime: "A", "B" or None.
|
||||
The "pair" branch is the shipped rule (validated literals; do not edit)."""
|
||||
if mode == "pair":
|
||||
if abs_score >= 30 and confidence >= 50 and mtf:
|
||||
return "A"
|
||||
elif abs_score >= 25 and confidence >= 45:
|
||||
return "B"
|
||||
return None
|
||||
conv = conviction(abs_score, confidence, mode)
|
||||
t_boost_a, t_boost_b = CONVICTION_THRESHOLDS[mode][6:8]
|
||||
if conv >= t_boost_a and mtf:
|
||||
return "A"
|
||||
elif conv >= t_boost_b:
|
||||
return "B"
|
||||
return None
|
||||
|
||||
|
||||
def _is_forex_weekend(now_utc: datetime) -> bool:
|
||||
"""Gold/FX market closed for the weekend: Friday 17:00 → Sunday 18:00
|
||||
NEW YORK time (= Fri 21:00 UTC in US summer, 22:00 in winter —
|
||||
@@ -169,6 +291,7 @@ class SignalAggregator:
|
||||
|
||||
def __init__(self, config: PlatformConfig):
|
||||
self.config = config
|
||||
grading_mode_of(config.signals) # fail at start, not at the first bar
|
||||
self.data_provider = make_data_provider(config)
|
||||
self.tech_engine = TechnicalEngine(config.technical)
|
||||
self._news_engine: Optional[NewsEngine] = None # lazy
|
||||
@@ -431,9 +554,15 @@ class SignalAggregator:
|
||||
multipliers actually in force (override included) and the
|
||||
timeframe set. Logged on every row so indicator-weight research
|
||||
can tell which rows share a scoring configuration."""
|
||||
signals = asdict(self.config.signals)
|
||||
# The research grading switch enters the hash only when it is in
|
||||
# force: under the shipped "pair" rule the scoring is byte-identical
|
||||
# to the pre-2026-09-27 code, so live rows keep the same fingerprint.
|
||||
if signals.get("grading_mode") == "pair":
|
||||
signals.pop("grading_mode")
|
||||
payload = {
|
||||
"technical": asdict(self.config.technical),
|
||||
"signals": asdict(self.config.signals),
|
||||
"signals": signals,
|
||||
"regime_weights": regime_weight_table(self.config.technical),
|
||||
"primary": self.config.primary_timeframe.value,
|
||||
"confirm": [tf.value for tf in self.config.confirmation_timeframes],
|
||||
@@ -1147,10 +1276,14 @@ class SignalAggregator:
|
||||
# ── 6c. Transitioning regime quality boost ──────────────────
|
||||
if report.market_regime == "transitioning" and report.signal_quality in ("B", "C", "D"):
|
||||
abs_score = abs(report.final_score)
|
||||
if abs_score >= 30 and report.final_confidence >= 50 and report.mtf_agreement:
|
||||
boost = _transitioning_boost(
|
||||
abs_score, report.final_confidence, report.mtf_agreement,
|
||||
grading_mode_of(self.config.signals),
|
||||
)
|
||||
if boost == "A":
|
||||
report.signal_quality = "A"
|
||||
report.action_recommendation = f"{report.final_signal.name.replace('_', ' ')} — Trend emerging, early entry"
|
||||
elif abs_score >= 25 and report.final_confidence >= 45:
|
||||
elif boost == "B":
|
||||
report.signal_quality = "B"
|
||||
report.action_recommendation = f"{report.final_signal.name.replace('_', ' ')} — Trend forming, watch closely"
|
||||
|
||||
@@ -1743,36 +1876,14 @@ class SignalAggregator:
|
||||
)
|
||||
confidence = 0.0
|
||||
|
||||
# Final signal
|
||||
if score >= 35 and confidence >= 55:
|
||||
report.final_signal = SignalStrength.STRONG_BUY
|
||||
elif score >= 25 and confidence >= 50:
|
||||
report.final_signal = SignalStrength.BUY
|
||||
elif score <= -35 and confidence >= 55:
|
||||
report.final_signal = SignalStrength.STRONG_SELL
|
||||
elif score <= -25 and confidence >= 50:
|
||||
report.final_signal = SignalStrength.SELL
|
||||
else:
|
||||
report.final_signal = SignalStrength.NEUTRAL
|
||||
|
||||
# Final signal (grading_mode: "pair" = the shipped rule; the research
|
||||
# modes grade on one conviction number — helpers at module top)
|
||||
report.final_signal, report.signal_quality = _final_signal_and_grade(
|
||||
score, confidence, report.mtf_agreement, grading_mode_of(self.config.signals)
|
||||
)
|
||||
report.final_score = score
|
||||
report.final_confidence = confidence
|
||||
|
||||
# Quality grade
|
||||
abs_score = abs(score)
|
||||
if abs_score >= 40 and confidence >= 65 and report.mtf_agreement:
|
||||
report.signal_quality = "A+"
|
||||
elif abs_score >= 33 and confidence >= 55 and report.mtf_agreement:
|
||||
report.signal_quality = "A"
|
||||
elif abs_score >= 28 and confidence >= 50:
|
||||
report.signal_quality = "B"
|
||||
elif abs_score >= 25 and confidence >= 45:
|
||||
report.signal_quality = "C"
|
||||
elif abs_score >= 15:
|
||||
report.signal_quality = "D"
|
||||
else:
|
||||
report.signal_quality = "F"
|
||||
|
||||
if not report.action_recommendation:
|
||||
sig_name = report.final_signal.name.replace("_", " ")
|
||||
if report.signal_quality in ("A+", "A"):
|
||||
|
||||
Reference in New Issue
Block a user