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:
@@ -202,6 +202,10 @@ K2_FLOAT_FIELDS = (
|
||||
"cp_trigger", "cp_height", "pb_retrace_atr", "bc_ratio", "rf_adx",
|
||||
)
|
||||
SKIP_NAMES = {1: "WEEKEND_FILTER", 2: "DAY_FILTER", 3: "SESSION_WINDOW"}
|
||||
# The grading rules K2 implements (fb2_aggregate: 0 = the shipped pair rule,
|
||||
# 1 = min conviction, 2 = product conviction). Pinned by name, never by the
|
||||
# position in signal_aggregator.GRADING_MODES; pack_params checks the two agree.
|
||||
GRADING_MODE_CODES = {"pair": 0, "min": 1, "product": 2}
|
||||
|
||||
# ── Config packing ────────────────────────────────────────────────────
|
||||
PARAMS_F = (
|
||||
@@ -220,6 +224,10 @@ PARAMS_F = (
|
||||
"big_candle_atr_multiple", "big_candle_lookback_hours",
|
||||
"regime_filter_min_adx", "pip_value", "fb2_ema_lo",
|
||||
"tp_scale_aplus", "tp_scale_a", "max_penalty",
|
||||
# grading_mode research thresholds (signal_aggregator.CONVICTION_THRESHOLDS;
|
||||
# all 0.0 under "pair"): aplus, a, b, c, strong, buy, boost_a, boost_b
|
||||
"conv_aplus", "conv_a", "conv_b", "conv_c",
|
||||
"conv_strong", "conv_buy", "conv_boost_a", "conv_boost_b",
|
||||
)
|
||||
PARAMS_I = (
|
||||
"ema_trend_period", "ema_mode_2ema", "ema_partial_neutral", "rsi_regime_aware",
|
||||
@@ -236,6 +244,7 @@ PARAMS_I = (
|
||||
"cp_min_sep_bars", "cp_flag_pole_bars", "cp_flag_min_bars", "cp_flag_max_bars",
|
||||
"quality_filter_mask", "strategies_mask", "primary_is_m5", "primary_bar_secs",
|
||||
"regime_filter", "n_confirm",
|
||||
"grading_mode", # 0 pair (shipped) / 1 min / 2 product
|
||||
)
|
||||
|
||||
# Fields of the four config dataclasses the kernels do NOT read, with the
|
||||
@@ -357,6 +366,17 @@ def pack_params(platform_cfg, bt_cfg) -> Tuple[np.ndarray, np.ndarray, np.ndarra
|
||||
"fb2_ema_lo": fb2_ema_lo_from_env(),
|
||||
"tp_scale_aplus": 2.0, "tp_scale_a": 1.5, "max_penalty": 30.0,
|
||||
}
|
||||
# grading rule: the live module is the single source of the mode list and
|
||||
# the thresholds (local import: this module must stay a leaf for the live
|
||||
# code, the reverse direction is the banned one)
|
||||
from signal_aggregator import CONVICTION_THRESHOLDS, GRADING_MODES, grading_mode_of
|
||||
assert set(GRADING_MODES) == set(GRADING_MODE_CODES), (
|
||||
f"signal_aggregator.GRADING_MODES {GRADING_MODES} != the kernel's {sorted(GRADING_MODE_CODES)}")
|
||||
grading_mode = grading_mode_of(s)
|
||||
conv_t = (0.0,) * 8 if grading_mode == "pair" else CONVICTION_THRESHOLDS[grading_mode]
|
||||
for name, val in zip(("conv_aplus", "conv_a", "conv_b", "conv_c",
|
||||
"conv_strong", "conv_buy", "conv_boost_a", "conv_boost_b"), conv_t):
|
||||
f[name] = val
|
||||
from providers.base import BaseDataProvider # CANDLE_SECONDS (the aggregator's clock)
|
||||
bar_secs = BaseDataProvider.CANDLE_SECONDS[platform_cfg.primary_timeframe]
|
||||
strategies = list(bt_cfg.strategies_only or [])
|
||||
@@ -410,6 +430,7 @@ def pack_params(platform_cfg, bt_cfg) -> Tuple[np.ndarray, np.ndarray, np.ndarra
|
||||
"primary_bar_secs": bar_secs,
|
||||
"regime_filter": 1 if b.regime_filter else 0,
|
||||
"n_confirm": len(platform_cfg.confirmation_timeframes),
|
||||
"grading_mode": GRADING_MODE_CODES[grading_mode],
|
||||
}
|
||||
pf = np.array([float(f[name]) for name in PARAMS_F], dtype=np.float64)
|
||||
pi = np.array([int(i[name]) for name in PARAMS_I], dtype=np.int32)
|
||||
@@ -1686,20 +1707,42 @@ void fb2_aggregate(const double* __restrict__ open_p, const double* __restrict__
|
||||
confidence -= total_penalty;
|
||||
confidence = PYMAX(0.0, PYMIN(95.0, confidence));
|
||||
if (exhaustion_blocked) { WARN(W_exhaustion_block, (double)exhaustion_count); confidence = 0.0; }
|
||||
int final_signal;
|
||||
if (score >= 35.0 && confidence >= 55.0) final_signal = 2;
|
||||
else if (score >= 25.0 && confidence >= 50.0) final_signal = 1;
|
||||
else if (score <= -35.0 && confidence >= 55.0) final_signal = -2;
|
||||
else if (score <= -25.0 && confidence >= 50.0) final_signal = -1;
|
||||
else final_signal = 0;
|
||||
// grading_mode 0 = the shipped pair rule (signal_aggregator._signal_pair /
|
||||
// _quality_pair, literals verbatim); 1 / 2 = the research conviction number
|
||||
// (signal_aggregator.conviction: PYMIN in the same argument order as
|
||||
// Python's min(abs, 0.6 * conf); product as (abs * conf) / 100, never
|
||||
// re-associated; thresholds ride PF from CONVICTION_THRESHOLDS).
|
||||
const int gmode = PI[PI_grading_mode];
|
||||
const double abs_score0 = fabs(score);
|
||||
int final_signal;
|
||||
int quality;
|
||||
if (abs_score0 >= 40.0 && confidence >= 65.0 && mtf) quality = Q_APLUS;
|
||||
else if (abs_score0 >= 33.0 && confidence >= 55.0 && mtf) quality = Q_A;
|
||||
else if (abs_score0 >= 28.0 && confidence >= 50.0) quality = Q_B;
|
||||
else if (abs_score0 >= 25.0 && confidence >= 45.0) quality = Q_C;
|
||||
else if (abs_score0 >= 15.0) quality = Q_D;
|
||||
else quality = Q_F;
|
||||
if (gmode == 0) {
|
||||
if (score >= 35.0 && confidence >= 55.0) final_signal = 2;
|
||||
else if (score >= 25.0 && confidence >= 50.0) final_signal = 1;
|
||||
else if (score <= -35.0 && confidence >= 55.0) final_signal = -2;
|
||||
else if (score <= -25.0 && confidence >= 50.0) final_signal = -1;
|
||||
else final_signal = 0;
|
||||
if (abs_score0 >= 40.0 && confidence >= 65.0 && mtf) quality = Q_APLUS;
|
||||
else if (abs_score0 >= 33.0 && confidence >= 55.0 && mtf) quality = Q_A;
|
||||
else if (abs_score0 >= 28.0 && confidence >= 50.0) quality = Q_B;
|
||||
else if (abs_score0 >= 25.0 && confidence >= 45.0) quality = Q_C;
|
||||
else if (abs_score0 >= 15.0) quality = Q_D;
|
||||
else quality = Q_F;
|
||||
} else {
|
||||
const double conv = (gmode == 1) ? PYMIN(abs_score0, 0.6 * confidence)
|
||||
: abs_score0 * confidence / 100.0;
|
||||
if (score > 0.0 && conv >= PF[PF_conv_strong]) final_signal = 2;
|
||||
else if (score > 0.0 && conv >= PF[PF_conv_buy]) final_signal = 1;
|
||||
else if (score < 0.0 && conv >= PF[PF_conv_strong]) final_signal = -2;
|
||||
else if (score < 0.0 && conv >= PF[PF_conv_buy]) final_signal = -1;
|
||||
else final_signal = 0;
|
||||
if (conv >= PF[PF_conv_aplus] && mtf) quality = Q_APLUS;
|
||||
else if (conv >= PF[PF_conv_a] && mtf) quality = Q_A;
|
||||
else if (conv >= PF[PF_conv_b]) quality = Q_B;
|
||||
else if (conv >= PF[PF_conv_c]) quality = Q_C;
|
||||
else if (abs_score0 >= 15.0) quality = Q_D;
|
||||
else quality = Q_F;
|
||||
}
|
||||
K2I(penalty_mask) = pmask; K2F(penalty_total) = total_penalty;
|
||||
K2I(exhaustion_count) = exhaustion_count; K2I(momentum_fading) = momentum_fading ? 1 : 0;
|
||||
|
||||
@@ -1715,8 +1758,18 @@ void fb2_aggregate(const double* __restrict__ open_p, const double* __restrict__
|
||||
// ── 6c. transitioning boost ──
|
||||
if (regime == REG_transitioning && (quality == Q_B || quality == Q_C || quality == Q_D)) {
|
||||
const double abs_score = fabs(score);
|
||||
if (abs_score >= 30.0 && confidence >= 50.0 && mtf) quality = Q_A;
|
||||
else if (abs_score >= 25.0 && confidence >= 45.0) quality = Q_B;
|
||||
bool boost_a, boost_b;
|
||||
if (gmode == 0) {
|
||||
boost_a = abs_score >= 30.0 && confidence >= 50.0 && mtf;
|
||||
boost_b = abs_score >= 25.0 && confidence >= 45.0;
|
||||
} else {
|
||||
const double conv6c = (gmode == 1) ? PYMIN(abs_score, 0.6 * confidence)
|
||||
: abs_score * confidence / 100.0;
|
||||
boost_a = conv6c >= PF[PF_conv_boost_a] && mtf;
|
||||
boost_b = conv6c >= PF[PF_conv_boost_b];
|
||||
}
|
||||
if (boost_a) quality = Q_A;
|
||||
else if (boost_b) quality = Q_B;
|
||||
}
|
||||
// ── 6d. exhausted trend cap ──
|
||||
if (trending_any && exhaustion_count >= 2 && (quality == Q_C || quality == Q_D)) {
|
||||
|
||||
Reference in New Issue
Block a user