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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Harwinder Sekhon
2026-09-27 14:54:36 -04:00
parent 1454ea1838
commit 6af1237c2d
13 changed files with 808 additions and 59 deletions
+252 -1
View File
@@ -1310,6 +1310,158 @@ def _():
assert report.action_recommendation != ""
# ── grading_mode (research switch, campaign 2026-09-27) ──────────────
def _grading_report():
config = PlatformConfig()
agg = SignalAggregator(config)
dfs = {
Timeframe.H1: make_ohlcv(300, seed=42),
Timeframe.M15: make_ohlcv(300, seed=43),
Timeframe.H4: make_ohlcv(300, seed=44),
}
return config, agg, agg.analyze_from_dataframes("XAU/USD", dfs, use_news=False)
@test("SignalConfig.grading_mode defaults to 'pair'; _signal_pair/_quality_pair reproduce the CLAUDE.md §2 table")
def _():
from config import SignalConfig as _SC
from signal_aggregator import _signal_pair, _quality_pair, _transitioning_boost
assert _SC().grading_mode == "pair"
def table_signal(s, c):
if s >= 35 and c >= 55: return SignalStrength.STRONG_BUY
if s >= 25 and c >= 50: return SignalStrength.BUY
if s <= -35 and c >= 55: return SignalStrength.STRONG_SELL
if s <= -25 and c >= 50: return SignalStrength.SELL
return SignalStrength.NEUTRAL
def table_grade(a, c, m):
if a >= 40 and c >= 65 and m: return "A+"
if a >= 33 and c >= 55 and m: return "A"
if a >= 28 and c >= 50: return "B"
if a >= 25 and c >= 45: return "C"
if a >= 15: return "D"
return "F"
edges = []
for b in (15, 25, 28, 30, 33, 35, 40):
edges += [b - 0.01, b, b + 0.01]
scores = sorted({0.0} | {e for e in edges} | {-e for e in edges} | {-100.0, 100.0, 60.0, -60.0})
confs = [0.0, 44.99, 45.0, 49.99, 50.0, 54.99, 55.0, 64.99, 65.0, 95.0]
n = 0
for s in scores:
for c in confs:
assert _signal_pair(s, c) == table_signal(s, c), (s, c)
for m in (True, False):
assert _quality_pair(abs(s), c, m) == table_grade(abs(s), c, m), (s, c, m)
want = "A" if (abs(s) >= 30 and c >= 50 and m) else "B" if (abs(s) >= 25 and c >= 45) else None
assert _transitioning_boost(abs(s), c, m, "pair") == want, (s, c, m)
n += 1
assert n > 800
# the real report's grade is the helper of its own final values
config, agg, report = _grading_report()
assert report.signal_quality == _quality_pair(abs(report.final_score), report.final_confidence, report.mtf_agreement)
assert report.final_signal == _signal_pair(report.final_score, report.final_confidence)
@test("grading_mode='min': conviction = min(|score|, 0.6*conf); grades and signal at the boundaries")
def _():
from signal_aggregator import conviction, _signal_conv, _quality_conv, _transitioning_boost, CONVICTION_THRESHOLDS
conv = lambda s, c: conviction(abs(s), c, "min")
grade = lambda s, c, m=True: _quality_conv(abs(s), conv(s, c), m, "min")
sig = lambda s, c: _signal_conv(s, conv(s, c), "min")
assert conv(40, 66.7) >= 40 and conv(40, 66.6) < 40
assert conv(95, 95) == 0.6 * 95 and conv(10, 95) == 10 and conv(50, 95) == 50 # min picks the smaller axis
assert grade(40, 66.7) == "A+" and grade(40, 66.6) == "A" and grade(40, 66.7, False) == "B"
assert grade(28, 46.7) == "B" and grade(28, 46.6) == "C"
assert grade(25, 41.7) == "C" and grade(25, 41.6) == "D"
assert grade(14.9, 95) == "F" and grade(15, 0) == "D"
assert sig(35, 58.4) == SignalStrength.STRONG_BUY and sig(35, 58.3) == SignalStrength.BUY
assert sig(-35, 58.4) == SignalStrength.STRONG_SELL and sig(-25, 41.7) == SignalStrength.SELL
assert sig(0, 95) == SignalStrength.NEUTRAL and sig(60, 0) == SignalStrength.NEUTRAL
assert _transitioning_boost(26, 60, False, "min") == "B"
assert _transitioning_boost(30, 50, True, "min") == "A" and _transitioning_boost(30, 49.9, True, "min") == "B"
assert _transitioning_boost(24, 95, True, "min") is None
assert CONVICTION_THRESHOLDS["min"] == (40.0, 33.0, 28.0, 25.0, 35.0, 25.0, 30.0, 25.0)
# the real report under the mode: grade == helper of the recomputed final values
config, agg, report = _grading_report()
config.signals.grading_mode = "min"
agg._compute_final_signal(report)
assert report.signal_quality == _quality_conv(abs(report.final_score), conv(report.final_score, report.final_confidence),
report.mtf_agreement, "min")
@test("grading_mode='product': conviction = |score|*conf/100; the control corners reproduce; high confidence can lift a mid score")
def _():
from signal_aggregator import conviction, _signal_conv, _quality_conv, _transitioning_boost, CONVICTION_THRESHOLDS
conv = lambda s, c: conviction(abs(s), c, "product")
grade = lambda s, c, m=True: _quality_conv(abs(s), conv(s, c), m, "product")
sig = lambda s, c: _signal_conv(s, conv(s, c), "product")
assert conv(40, 65) == 26.0 and conv(33, 55) == 18.15 and conv(28, 50) == 14.0 and conv(25, 45) == 11.25
assert grade(40, 65) == "A+" and grade(33, 55) == "A" and grade(28, 50) == "B" and grade(25, 45) == "C"
assert grade(40, 64.9) == "A" and grade(33, 54.9) == "B"
assert sig(35, 55) == SignalStrength.STRONG_BUY and sig(35, 54.9) == SignalStrength.BUY
assert sig(25, 50) == SignalStrength.BUY and sig(-25, 50) == SignalStrength.SELL and sig(25, 49.9) == SignalStrength.NEUTRAL
assert grade(30, 87) == "A+" # |score| < 40 lifted by confidence
assert grade(24, 95) == "A" and grade(24, 95, False) == "B"
assert grade(20, 60) == "C" # |score| < 25: the M5 gate-1 interaction case
assert grade(20, 56) == "D" and grade(60, 0) == "D" # 11.2 < 11.25; conf 0 -> conviction 0
assert grade(14.9, 95) == "B" and grade(10, 95) == "F" # 14.155 >= 14 (the trade-off); 9.5 < 11.25 and |score| < 15
assert _transitioning_boost(30, 50, True, "product") == "A" and _transitioning_boost(25, 45, False, "product") == "B"
assert _transitioning_boost(25, 44.9, False, "product") is None
assert CONVICTION_THRESHOLDS["product"] == (26.0, 18.15, 14.0, 11.25, 19.25, 12.5, 15.0, 11.25)
config, agg, report = _grading_report()
config.signals.grading_mode = "product"
agg._compute_final_signal(report)
assert report.signal_quality == _quality_conv(abs(report.final_score), conv(report.final_score, report.final_confidence),
report.mtf_agreement, "product")
@test("grading_mode: gate 6c boost reads the same conviction in every mode")
def _():
from signal_aggregator import _transitioning_boost
for mode in ("pair", "min", "product"):
assert _transitioning_boost(30, 50, True, mode) == "A", mode
assert _transitioning_boost(30, 50, False, mode) == "B", mode
assert _transitioning_boost(26, 60, False, mode) == "B", mode
assert _transitioning_boost(10, 95, True, mode) is None, mode
assert _transitioning_boost(26, 44, False, "pair") is None
assert _transitioning_boost(26, 44, False, "min") == "B" # 0.6*44 = 26.4 >= 25
assert _transitioning_boost(26, 44, False, "product") == "B" # 11.44 >= 11.25
@test("grading_mode unknown raises at construction and at recompute; the fingerprint carries the field")
def _():
from signal_aggregator import grading_mode_of
config = PlatformConfig()
config.signals.grading_mode = "geomean"
try:
SignalAggregator(config)
raise AssertionError("an unknown grading_mode must fail at construction")
except ValueError as e:
assert "grading_mode" in str(e) and "geomean" in str(e), e
config, agg, report = _grading_report()
fp_pair = agg.engine_config_fingerprint()
config.signals.grading_mode = "geomean"
try:
agg._compute_final_signal(report)
raise AssertionError("an unknown grading_mode must fail at recompute")
except ValueError:
pass
config.signals.grading_mode = "product"
assert agg.engine_config_fingerprint() != fp_pair, "the mode is part of the scoring config"
config.signals.grading_mode = "pair"
assert agg.engine_config_fingerprint() == fp_pair
assert grading_mode_of(config.signals) == "pair"
from signal_aggregator import conviction as _conv
for bad in ("pair", "geomean", ""):
try:
_conv(30.0, 60.0, bad)
raise AssertionError(f"conviction() must refuse {bad!r}")
except ValueError:
pass
@test("Event-risk signal gating obeys news.event_risk_caps_signals")
def _():
config = PlatformConfig()
@@ -1728,11 +1880,50 @@ def _():
assert isinstance(msg, str)
assert len(msg) > 0
assert "XAU/USD" in msg
assert "Score" in msg
assert "Score *" not in msg and "Conf *" not in msg and "MTF ✅" not in msg and "MTF ❌" not in msg, \
"raw numbers replaced by words (2026-09-27)"
assert "📈 MTF:" not in msg, "the per-timeframe detail line is labelled in words"
assert "📐 Direction: *" in msg and "Reliability: *" in msg and "timeframes " in msg
import telegram_bot as _tb
assert msg.splitlines()[0].startswith(f"{_tb._SIGNAL_EMOJI[report.final_signal]} *Grade {report.signal_quality} ·"), "grade first"
assert "Regime" in msg
assert "Not financial advice" in msg
@test("plain words for score / confidence sit on the grade table's floors and reach every surface")
def _():
from main import direction_words, reliability_words, plain_words, report_to_dict
assert direction_words(40) == "very strong buy" and direction_words(39.99) == "strong buy"
assert direction_words(-33) == "strong sell" and direction_words(-32.99) == "clear sell"
assert direction_words(25) == "clear buy" and direction_words(24.99) == "weak buy"
assert direction_words(15) == "weak buy" and direction_words(14.99) == "no clear direction"
assert direction_words(0) == "no clear direction"
assert reliability_words(65) == "high" and reliability_words(64.99) == "good"
assert reliability_words(55) == "good" and reliability_words(54.99) == "fair"
assert reliability_words(45) == "fair" and reliability_words(44.99) == "low" and reliability_words(0) == "low"
report = SignalReport(symbol="XAU/USD", display_name="XAU/USD", timestamp="now")
report.final_score, report.final_confidence, report.mtf_agreement = 38.2, 62.0, True
assert plain_words(report) == "Direction: strong buy · Reliability: good · timeframes agree"
report.mtf_agreement = False
assert plain_words(report).endswith("timeframes disagree")
assert report_to_dict(report)["plain_words"] == plain_words(report)
# the words are consistent with the grade the same numbers would earn
from signal_aggregator import _quality_pair
for score, conf in ((40, 65), (33, 55), (28, 50), (25, 45), (15, 10), (5, 90)):
grade = _quality_pair(score, conf, True)
words = direction_words(score)
if grade == "A+":
assert words.startswith("very strong")
elif grade == "A":
assert words.startswith("strong")
elif grade in ("B", "C"):
assert words.startswith("clear"), (score, conf, words)
elif grade == "D":
assert words.startswith("weak")
else:
assert words == "no clear direction"
@test("format_signal_message makes chart pattern actions Markdown-safe")
def _():
report = SignalReport(symbol="XAU/USD", display_name="XAU/USD", timestamp="now")
@@ -14446,6 +14637,30 @@ def _():
assert dict(zip(_ck37.PARAMS_F, _ck37.pack_params(_mk37(Timeframe.M5), _m37_bt(_mk37(Timeframe.M5), dfs))[0].tolist()))["fb2_ema_lo"] == 0.9
with patch.dict(os.environ, {"FB2_EMA_LO": "wide"}):
assert _ck37.fb2_ema_lo_from_env() == 0.5
# grading_mode: the factories pack the shipped pair rule (code 0, no thresholds);
# the research modes pack 1 / 2 and the live module's threshold tuple verbatim
from signal_aggregator import CONVICTION_THRESHOLDS as _CT
conv_names = ("conv_aplus", "conv_a", "conv_b", "conv_c", "conv_strong", "conv_buy", "conv_boost_a", "conv_boost_b")
from signal_aggregator import GRADING_MODES as _GM
assert _ck37.GRADING_MODE_CODES == {"pair": 0, "min": 1, "product": 2} and set(_GM) == set(_ck37.GRADING_MODE_CODES)
for tf in (Timeframe.M5, Timeframe.M15, Timeframe.H1, Timeframe.H4):
pf0, pi0, _ = _ck37.pack_params(_mk37(tf), _m37_bt(_mk37(tf), dfs))
f0, i0 = dict(zip(_ck37.PARAMS_F, pf0.tolist())), dict(zip(_ck37.PARAMS_I, pi0.tolist()))
assert i0["grading_mode"] == 0 and all(f0[n] == 0.0 for n in conv_names), tf
for code, mode in ((1, "min"), (2, "product")):
cfg = _mk37(Timeframe.M15)
cfg.signals.grading_mode = mode
pfm, pim, _ = _ck37.pack_params(cfg, _m37_bt(cfg, dfs))
fm, im = dict(zip(_ck37.PARAMS_F, pfm.tolist())), dict(zip(_ck37.PARAMS_I, pim.tolist()))
assert im["grading_mode"] == code, mode
assert tuple(fm[n] for n in conv_names) == _CT[mode], mode
bad = _mk37(Timeframe.M15)
bad.signals.grading_mode = "geomean"
try:
_ck37.pack_params(bad, _m37_bt(bad, dfs))
raise AssertionError("the packer must refuse an unknown grading_mode")
except ValueError as e:
assert "grading_mode" in str(e), e
# the kernel #defines are generated from the same name lists
src = _ck37._defines()
for k, name in enumerate(_ck37.PARAMS_F):
@@ -14784,6 +14999,41 @@ def _():
print(f" ({t['n']} bars, {t['cands']} candidates, directions {sorted(t['direction'])})")
def _tweak_min37(cfg):
cfg.signals.grading_mode = "min"
def _tweak_product37(cfg):
cfg.signals.grading_mode = "product"
@test("CUDA stage: K2 == analyze_from_dataframes — M15 factory, grading_mode='min' (research conviction rule, all grades)")
def _():
if not _GPU37:
print(" (no CUDA: kernel not exercised)")
return
t = _k2_case37("M15 grading_mode=min", Timeframe.M15, _tweak_min37,
{"quality_filter": ["A+", "A", "B", "C", "D"]}, "main", 1, None)
assert {"A+", "A", "B", "C", "D", "F"} <= t["quality"], t["quality"]
assert t["cands"] >= 20, t["cands"]
print(f" ({t['n']} bars, {t['cands']} candidates, grades {sorted(t['quality'])})")
@test("CUDA stage: K2 == analyze_from_dataframes — M5 factory, grading_mode='product' (a low-score C reaches the M5 minimum-score gate)")
def _():
if not _GPU37:
print(" (no CUDA: kernel not exercised)")
return
t = _k2_case37("M5 grading_mode=product", Timeframe.M5, _tweak_product37,
{"quality_filter": ["A+", "A", "B", "C", "D"]}, "main", 1, None)
# (no product-C survives on this fixture: every low-score C is demoted by gate 1)
assert {"A+", "A", "B", "D", "F"} <= t["quality"], t["quality"]
# under the product rule a C/B can carry |score| < 25, so gate 1 fires
# (unreachable under the shipped pair rule — see the M5 factory case)
assert "m5_score" in t["warn"], sorted(t["warn"])
print(f" ({t['n']} bars, {t['cands']} candidates, m5_score demotions reachable)")
@test("CUDA stage: K2 == analyze_from_dataframes — H1 and H4 factories (H1 shorts blocked; D1 confirmation shorter than 30 bars)")
def _():
if not _GPU37:
@@ -15031,6 +15281,7 @@ def _():
(lambda c: setattr(c.technical, "swing_sr_lookback", _ck37.MAX_SR_LOOKBACK), "swing_sr_lookback"),
(lambda c: setattr(c.xau_strategy, "cp_lookback", _ck37.MAX_CP_LOOKBACK), "cp_lookback"),
(lambda c: setattr(c.technical, "ema_mode", "3ema"), "ema_mode"),
(lambda c: setattr(c.signals, "grading_mode", "geomean"), "grading_mode"),
(lambda c: setattr(c, "confirmation_timeframes",
[Timeframe.M5, Timeframe.H1, Timeframe.H4, Timeframe.D1, Timeframe.M30]), "confirmation timeframes"),
(lambda c: (setattr(c.xau_strategy, "big_candle_atr_multiple", 3.0),