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:
Harwinder Sekhon
2026-09-27 14:54:36 -04:00
parent 4e0a083955
commit d81cdf845b
13 changed files with 808 additions and 59 deletions
+2
View File
@@ -419,6 +419,8 @@ def refuse_unsupported(platform_cfg: PlatformConfig, bt_cfg: BacktestConfig) ->
problems.append(f"big-candle window {n_bc + 3 * t.atr_period} bars (CUDA limit {ck.FEAT_MAX_P})")
if t.ema_mode not in ("4ema", "2ema"):
problems.append(f"technical.ema_mode={t.ema_mode!r}")
if platform_cfg.signals.grading_mode not in ck.GRADING_MODE_CODES:
problems.append(f"signals.grading_mode={platform_cfg.signals.grading_mode!r}")
if problems:
raise SystemExit("cudaBasedBacktesting.py cannot run this configuration: "
+ "; ".join(problems) + ". Use backtest/gpu_backtest.py.")
+67 -14
View File
@@ -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)) {
@@ -0,0 +1,250 @@
# Single "conviction" number vs the two-number grade — pre-registered campaign, 2026-09-27
Owner question (2026-09-27): "Can there be a combination of both [score and confidence] that is
easier to understand and also for the system to make decisions?" Chosen route (owner): research a
single decision number. This file was written BEFORE any candidate run. Rules first, results
appended below them. Nothing ships from this campaign; a pass is a hypothesis for a per-timeframe
override in a separate commit (CLAUDE.md rule 2).
## What is tested
Today (`signal_aggregator._compute_final_signal`, gate 6c) a grade needs BOTH a score floor and a
confidence floor: A+ |score| ≥ 40 & conf ≥ 65 & mtf; A 33 & 55 & mtf; B 28 & 50; C 25 & 45;
D |score| ≥ 15; STRONG 35 & 55; BUY/SELL 25 & 50; 6c boost A 30 & 50 & mtf, B 25 & 45.
Candidates replace the pair with one number. Thresholds are fixed analytically from the control's
corner points — no grid, no tuning (a data-mined threshold would need its own holdout):
- **K-MIN**: `conviction = min(|score|, 0.6 × confidence)`. Grades A+ ≥ 40 (mtf), A ≥ 33 (mtf),
B ≥ 28, C ≥ 25, D |score| ≥ 15 unchanged, else F; STRONG ≥ 35, BUY/SELL ≥ 25; 6c boost A ≥ 30
(mtf), B ≥ 25. (The control's corner ratios score/conf are 0.615, 0.60, 0.56, 0.556, so 0.6 is the
natural single scale.) Against the control: A+ needs conf ≥ 66.7 (was 65), B ≥ 46.7 (50),
C ≥ 41.7 (45), STRONG ≥ 58.3 (55), BUY/SELL ≥ 41.7 (50); score floors unchanged.
- **K-PROD**: `conviction = |score| × confidence / 100`. A+ ≥ 26 (40×65), A ≥ 18.15 (33×55),
B ≥ 14 (28×50), C ≥ 11.25 (25×45), D |score| ≥ 15, else F; STRONG ≥ 19.25 (35×55),
BUY/SELL ≥ 12.5 (25×50); 6c A ≥ 15 (30×50), B ≥ 11.25 (25×45). A genuine trade-off: high
confidence can lift a mid score and vice versa. Interaction on record: a product B/C with
|score| < 25 is demoted to D on M5 by the live gate 1 (`m5_min_abs_score` 25); the gate stays,
the count of demotions is reported (Step E). Property on record (code review, before the runs):
the D rung stays `|score| ≥ 15` in both candidates, and under the product form B/C can sit below
it (B needs |score| ≥ 14.74 at the 95 confidence cap, C ≥ 11.85), so an M15/H1/H4 product book can
hold a trade with |score| < 15. Kept as pre-registered (a score floor would make it a pair rule
again); the count of |score| < 15 and < 25 trades per candidate book is reported.
- Geometric mean √(|score|·conf) is a monotone transform of the product (same partition at
thresholds √T) — not a separate candidate.
- Direction stays sign(score); mtf agreement stays required for A+/A; the exhaustion hard block
(conf = 0) gives conviction 0 in both forms, so NEUTRAL and at most D, as today.
Switch: `signals.grading_mode` (`"pair"` default = today's literal rule, `"min"`, `"product"`),
read in `_compute_final_signal` and gate 6c (`signal_aggregator.py` helpers `_signal_pair`,
`_quality_pair`, `_transitioning_boost`, `conviction`, `CONVICTION_THRESHOLDS`), mirrored in K2
(`backtest/cuda_signal_kernels.py`, `PI_grading_mode`, `PF_conv_*` packed from the live module),
refused by the twin for other values. `pair` is byte-identical to HEAD `1454ea1` (Step A proves it).
Note: `engine_config_fingerprint` hashes `SignalConfig`, so the live fingerprint changes at the
deploy of this field even in `pair` mode (label-only; split signal-log comparisons there).
## Harness
Twin command template (research twin, CLAUDE.md §3/§9; seconds per book):
`venv/bin/python backtest/cudaBasedBacktesting.py --pair XAUUSD --spread <0.65|0.30>
--primary-tf <5min|15min|1h|4h> --start <s> --end <e> [--set signals.grading_mode=<min|product>]
--export-csv <out>`. Reference script (`backtest/gpu_backtest.py`, same flags, `--workers 12`)
for the neutrality proof and every paired MD5. Factory configuration; `--set` is the only override.
Evidence order (all must hold before any candidate number is read):
1. **Neutrality**: with the new code and no `--set`, the five pins reproduce on BOTH scripts
(M5 W2 `e599c8a8652f95b7fcdf5a7ef2b564f5`, M15 W2 `5b65ecf6fad9a8da412749066fd50d36`,
M15 OOS `09b8a8e7f5833436c20aef60ca0a7ff7`, H1 W3 `a44fd19b4067f46e5bd98d50cc3eff92`,
H4 W3 `f3587bae3c5dc3eda468ea3588b17dcb`), plus the twin with an explicit
`--set signals.grading_mode=pair` on M15 OOS.
2. **Twin parity on the new paths**: each candidate's M15 OOS $0.65 book is MD5-equal on both scripts.
3. A passing candidate is re-run once on `gpu_backtest.py` (W2, $0.65) and the two MD5s must be equal.
Windows: W2 2024-09-05 → 2026-09-05 (halves split at 2025-09-05); OOS 2024-06-01 → 2025-03-30
(halves at 2024-10-30); W3 2023-09-05 → 2026-09-05 (halves at 2025-03-05; max-DD clause); 5-year
2021-08-01 → 2026-08-04 (per-year clause; cache `20210531_20260804`). Costs: $0.65 headline, $0.30
comparability. Books are written outside the tree (§9 keeps no CSVs in-tree).
Code review gate (owner instruction): the diff is reviewed with the `code-review` skill before Step A;
outcome recorded here. Outcome (2026-09-27, `code-review` high, before Step A): 7 findings, 6 fixed in the same working
tree — kernel mode codes pinned by name (`GRADING_MODE_CODES`) and checked against the live list;
no zero-threshold fallback for a non-pair mode; the twin's refusal reads the kernel's map; `conviction()`
refuses modes it has no formula for; one `_final_signal_and_grade` dispatch point; `engine_config_fingerprint`
omits the switch under `pair` so live rows keep their fingerprint; CLAUDE.md test counts updated. One
recorded, not changed: the D rung under the product form (above). Suite green after the fixes.
## Candidates
| # | TF | `--set` | Hypothesis |
|---|---|---|---|
| K-MIN | M5, M15 (H1/H4 for the record) | `signals.grading_mode=min` | one scale on the score axis; nearly the control, loosens B/C/BUY confidence, tightens A+ |
| K-PROD | M5, M15 (H1/H4 for the record) | `signals.grading_mode=product` | genuine trade-off between the two axes; confidence can lift a mid score |
## Acceptance rule (CLAUDE.md rule 1, written before the runs)
A candidate PASSES only if, at $0.65, on W2: (a) total R ≥ control + 5R; (b) both halves ≥ the
control's halves − 1R (no half may fund the other); (c) PF ≥ 1.3 and ≥ control − 0.02; (d) OOS
total R ≥ control − 1R; (e) W3 max drawdown ≤ control + 10%; (f) the same sign of ΔR at $0.30.
A candidate that changes fewer than 20 trades on W2 is "no evidence", not a pass.
Judge run-level numbers, never the added/removed bucket alone (the 5-bar cooldown displaces
entries). Trade diff keyed on (entry time, direction).
Campaign clause (g): on the 5-year window at $0.65, no calendar year in which the control is
positive goes below −5R under the candidate (trend-artefact guard, `fixedtp_2026-08-05.md`
precedent); a W2 gain that lives in a single year is read as an artefact and noted.
The rule is judged independently on M5 W2 and M15 W2: the mode is read per config, so a pass ships
as a per-timeframe factory override (rule 2) and a timeframe where it fails keeps `pair`. H1 and H4
are run on W3 for the record (ΔR at $0.65, trades changed); they are not ship candidates here — a
positive H1/H4 result is a hypothesis for its own pre-registration. Nothing ships if no clause set
passes; the field stays a research-only knob (precedent `--fixed-rr-tp`).
## Run list
Step 0 suite green (Module 37 on the GPU) and the review gate. Step A neutrality (11 runs).
Step B twin parity on the new paths (4 runs). Step C books on the twin: {M5 W2, M5 OOS, M5 W3,
M15 W2, M15 OOS, M15 W3, H1 W3, H4 W3} × {$0.65, $0.30} × {control, min, product}. Step D per-year
(5-year, M5 and M15, $0.65, control/min/product). Step E K-PROD M5 W2 $0.65 with
`--record DIR --record-bars` (MD5 equal to the Step C book) to count `m5_score` demotions.
Step F reference-script rerun for any passer.
## Results
<!-- appended after the runs; nothing above this line changes -->
Reference script `Backend: CuPy GPU (NVIDIA GeForce RTX 4070 SUPER, cupy 14.0.1)`, `GPU cache fallbacks: 0`;
twin `Backend: CuPy GPU (NVIDIA GeForce RTX 4070 SUPER, cupy 14.0.1)`, `GPU cache fallbacks: 0 (no per-bar
recompute path in cudaBasedBacktesting)`. Every run from the kept cache windows.
### Step A — neutrality (new code, `grading_mode` at its default, no `--set`)
| Book | Pin | `gpu_backtest.py` | `cudaBasedBacktesting.py` |
|---|---|---|---|
| M5 W2 $0.65 | `e599c8a8652f95b7fcdf5a7ef2b564f5` | equal | equal |
| M15 W2 $0.65 | `5b65ecf6fad9a8da412749066fd50d36` | equal | equal |
| M15 OOS $0.65 | `09b8a8e7f5833436c20aef60ca0a7ff7` | equal | equal; also equal with an explicit `--set signals.grading_mode=pair` |
| H1 W3 $0.65 | `a44fd19b4067f46e5bd98d50cc3eff92` | equal | equal |
| H4 W3 $0.65 | `f3587bae3c5dc3eda468ea3588b17dcb` | equal | equal |
The refactor (helpers + switch) is byte-neutral on both scripts: the five pins stand.
### Step B — twin parity on the new code paths (M15 OOS $0.65)
| Candidate | `gpu_backtest.py` | `cudaBasedBacktesting.py` |
|---|---|---|
| K-MIN | `22726d8500383ce02e0a3cbf0412f6d8` | `22726d8500383ce02e0a3cbf0412f6d8` (equal) |
| K-PROD | `d1ae11651d5d7c8066b7e8addc0c7f36` | `d1ae11651d5d7c8066b7e8addc0c7f36` (equal) |
The twin's books below therefore count as evidence (instructions item 6, CUDA-twin addendum).
### Books (all on the twin; the control rows reproduce the reference script's MD5s)
Halves: W2 at 2025-09-05, OOS at 2024-10-30, W3 at 2025-03-05. Trades / R from the 63-column book.
| TF | Window | Cost | Candidate | Trades | Total R | H1 | H2 | WR | PF | Max DD | MD5 (8) |
|---|---|---|---|---|---|---|---|---|---|---|---|
| M5 | W2 | $0.65 | control | 781 | +146.07 | +26.42 | +119.65 | 52.9% | 1.52 | 10.1 | e599c8a8 |
| M5 | W2 | $0.65 | K-MIN | 812 | +144.86 | +24.73 | +120.13 | 52.8% | 1.50 | 11.5 | 2ab2b573 |
| M5 | W2 | $0.65 | K-PROD | 805 | +147.33 | +29.27 | +118.06 | 52.5% | 1.52 | 11.6 | a37dbb71 |
| M5 | W2 | $0.30 | control / K-MIN / K-PROD | 781 / 812 / 805 | +162.09 / +161.17 / +163.96 | +31.71 / +30.15 / +34.70 | +130.37 / +131.02 / +129.26 | | 1.60 / 1.57 / 1.59 | 8.9 / 10.6 / 10.9 | 7ab48db9 / 64f26c30 / ed14d7ec |
| M5 | OOS | $0.65 | control | 86 | +9.47 | +0.01 | +9.46 | 53.5% | 1.33 | 8.2 | 65c46e62 |
| M5 | OOS | $0.65 | K-MIN | 94 | +14.32 | +2.37 | +11.94 | 56.4% | 1.51 | 6.4 | bec7c87f |
| M5 | OOS | $0.65 | K-PROD | 91 | +13.03 | +0.10 | +12.93 | 54.9% | 1.45 | 4.5 | 217d0134 |
| M5 | OOS | $0.30 | control / K-MIN / K-PROD | 86 / 94 / 91 | +12.42 / +17.34 / +16.05 | | | | 1.46 / 1.66 / 1.58 | 6.7 / 4.8 / 4.1 | |
| M5 | W3 | $0.65 | control | 870 | +153.64 | +16.73 | +136.91 | 53.3% | 1.50 | 10.1 | d20ec8a3 |
| M5 | W3 | $0.65 | K-MIN | 905 | +153.84 | +20.64 | +133.20 | 53.3% | 1.48 | 11.5 | 5ad0c381 |
| M5 | W3 | $0.65 | K-PROD | 896 | +154.60 | +20.12 | +134.48 | 52.9% | 1.49 | 11.6 | 466bda90 |
| M5 | W3 | $0.30 | control / K-MIN / K-PROD | 870 / 905 / 896 | +172.40 / +172.92 / +173.99 | | | | 1.57 / 1.55 / 1.56 | 8.9 / 10.6 / 10.9 | |
| M15 | W2 | $0.65 | control | 813 | +154.15 | +70.10 | +84.04 | 62.2% | 1.53 | 15.6 | 5b65ecf6 |
| M15 | W2 | $0.65 | K-MIN | 827 | +151.25 | +69.05 | +82.21 | 61.9% | 1.51 | 16.0 | 5c7918dc |
| M15 | W2 | $0.65 | K-PROD | 1106 | +125.17 | +62.26 | +62.91 | 58.2% | 1.29 | 28.9 | ecfb62e5 |
| M15 | W2 | $0.30 | control / K-MIN / K-PROD | 813 / 827 / 1106 | +167.53 / +164.82 / +144.91 | +78.61 / +77.62 / +74.30 | +88.91 / +87.20 / +70.61 | | 1.59 / 1.57 / 1.34 | 14.5 / 14.9 / 26.8 | 15486607 / 5ab311cf / 582019a7 |
| M15 | OOS | $0.65 | control | 313 | +68.50 | +18.87 | +49.63 | 63.6% | 1.67 | 12.4 | 09b8a8e7 |
| M15 | OOS | $0.65 | K-MIN | 314 | +65.64 | +19.20 | +46.44 | 63.4% | 1.64 | 12.4 | 22726d85 |
| M15 | OOS | $0.65 | K-PROD | 408 | +53.98 | +26.67 | +27.32 | 59.1% | 1.36 | 17.3 | d1ae1165 |
| M15 | OOS | $0.30 | control / K-MIN / K-PROD | 313 / 314 / 408 | +76.21 / +73.36 / +64.35 | | | | 1.77 / 1.74 / 1.44 | 11.1 / 11.1 / 16.4 | |
| M15 | W3 | $0.65 | control | 1105 | +195.79 | +76.87 | +118.92 | 61.1% | 1.49 | 20.9 | 70195fb0 |
| M15 | W3 | $0.65 | K-MIN | 1126 | +191.25 | +71.90 | +119.35 | 60.7% | 1.47 | 21.3 | 23edd8bc |
| M15 | W3 | $0.65 | K-PROD | 1478 | +186.24 | +84.70 | +101.54 | 58.3% | 1.33 | 28.9 | 0246cbb4 |
| M15 | W3 | $0.30 | control / K-MIN / K-PROD | 1105 / 1126 / 1478 | +216.53 / +212.32 / +215.54 | | | | 1.55 / 1.53 / 1.39 | 19.6 / 20.0 / 26.8 | |
| H1 | W3 | $0.65 | control | 253 | +70.02 | +26.26 | +43.76 | 64.4% | 1.98 | 4.4 | a44fd19b |
| H1 | W3 | $0.65 | K-MIN | 268 | +74.81 | +28.75 | +46.06 | 64.9% | 1.99 | 5.7 | 24674555 |
| H1 | W3 | $0.65 | K-PROD | 351 | +81.88 | +30.68 | +51.19 | 60.1% | 1.75 | 8.5 | 2e8df9cc |
| H1 | W3 | $0.30 | control / K-MIN / K-PROD | 253 / 268 / 351 | +72.74 / +77.64 / +85.91 | | | | 2.03 / 2.04 / 1.80 | 4.3 / 5.6 / 8.3 | |
| H4 | W3 | $0.65 | control | 114 | +21.68 | +4.91 | +16.77 | 57.9% | 1.59 | 5.4 | f3587bae |
| H4 | W3 | $0.65 | K-MIN | 117 | +22.57 | +4.91 | +17.66 | 58.1% | 1.59 | 5.4 | 3ed9a44e |
| H4 | W3 | $0.65 | K-PROD | 151 | +38.69 | +13.97 | +24.72 | 58.9% | 1.78 | 6.3 | b696b617 |
| H4 | W3 | $0.30 | control / K-MIN / K-PROD | 114 / 117 / 151 | +22.52 / +23.43 / +39.88 | | | | 1.61 / 1.62 / 1.81 | 5.0 / 5.0 / 6.0 | |
### Trade diffs at $0.65 (key = entry time + direction; W2 for M5/M15, W3 for H1/H4)
- M5 K-MIN: 37 added (+1.74R), 6 removed (+4.53R), 775 common (+143.12R vs +141.54R in the control:
the cooldown shifts a few entries). Grade mix B 572 → 606, A 187 → 186, C 21 → 19. Changed 43.
- M5 K-PROD: 74 added (+16.51R), 50 removed (+10.12R), 731 common (+130.83R vs +135.95R). Grade
mix A 187 → 263, A+ 1 → 3, C 21 → 8; XAU_ATR setups 32 → 57 (more A grades reach the ATR
generator). Changed 124.
- M15 K-MIN: 34 added (−0.34R), 20 removed (+3.42R), 793 common (+151.59R vs +150.73R). A+ 22 → 10
(the A+ confidence floor rises to 66.7), B 443 → 465. Changed 54.
- M15 K-PROD: 544 added (+2.66R), 251 removed (+16.64R), 562 common (+122.51R vs +137.50R). The
book grows 813 → 1106 (C 60 → 196, A+ 22 → 79); 295 candidate trades carry |score| < 25 and earn
+5.11R between them. Changed 795.
- H1 (for the record): K-MIN +20 / −5, 248 common, ΔR +4.79; K-PROD +126 / −28, 225 common,
ΔR +11.86 with PF 1.98 → 1.75 and max DD 4.4 → 8.5R; 63 trades with |score| < 25 (+7.38R).
- H4 (for the record): K-MIN +3 / −0, ΔR +0.89 (3 trades changed: no evidence); K-PROD +40 / −3,
ΔR +17.01 with PF 1.59 → 1.78 and DD 5.4 → 6.3R, 28 trades with |score| < 25 (+15.41R).
- No candidate book on any timeframe holds a trade with |score| < 15 except the M5 chart-pattern
entries the control also has (127 XAU_CP trades, forced grade B by design, identical in all three
M5 books). The D-rung property recorded above did not materialise.
- Step E (M5 W2 $0.65 K-PROD with `--record --record-bars`, book MD5 `a37dbb71…` = the Step C
book): 82 analysed bars carry the gate-1 warning `M5 FILTER: |score| … < 25 minimum` as their
first warning, every one of them ending at grade D. That is the whole M5 gate-1 interaction.
### Per year, 5-year window 2021-08-01 → 2026-08-04 at $0.65 (twin)
| TF | Year | control n / R | K-MIN n / R | K-PROD n / R |
|---|---|---|---|---|
| M5 | 2021 | 12 / −1.99 | 14 / −0.28 | 13 / −0.55 |
| M5 | 2022 | 50 / −2.18 | 54 / −0.43 | 52 / +1.29 |
| M5 | 2023 | 61 / −2.75 | 62 / −2.10 | 61 / −3.57 |
| M5 | 2024 | 102 / +0.97 | 108 / +4.05 | 106 / +4.19 |
| M5 | 2025 | 332 / +40.54 | 346 / +38.87 | 342 / +41.63 |
| M5 | 2026 | 348 / +92.49 | 361 / +87.15 | 358 / +85.73 |
| M5 | total | 905 / +127.08 (PF 1.39, DD 13.5) | 945 / +127.25 (1.37, 12.4) | 932 / +128.72 (1.38, 12.2) |
| M15 | 2021 | 78 / −10.88 | 84 / −11.20 | 90 / −6.29 |
| M15 | 2022 | 267 / −24.63 | 273 / −26.34 | 340 / −47.01 |
| M15 | 2023 | 211 / +11.74 | 216 / +11.10 | 261 / +8.63 |
| M15 | 2024 | 343 / +53.35 | 345 / +49.12 | 448 / +62.45 |
| M15 | 2025 | 425 / +81.16 | 438 / +85.91 | 576 / +83.40 |
| M15 | 2026 | 224 / +38.10 | 227 / +32.98 | 317 / +4.32 |
| M15 | total | 1548 / +148.84 (PF 1.25, DD 48.3) | 1583 / +141.58 (1.23, 48.7) | 2032 / +105.50 (1.13, 77.4) |
### Verdicts against the pre-registered rule (W2 at $0.65 unless stated)
| Candidate | TF | (a) ΔR ≥ +5 | (b) halves ≥ ctrl − 1 | (c) PF ≥ 1.3 & ≥ ctrl − 0.02 | (d) OOS ≥ ctrl − 1 | (e) W3 DD ≤ ctrl + 10% | (f) same sign at $0.30 | (g) 5-yr years | changed | Verdict |
|---|---|---|---|---|---|---|---|---|---|---|
| K-MIN | M5 | ✗ −1.21 | ✗ H1 24.73 < 25.42 | ✓ 1.50 | ✓ +14.32 vs 9.47 | ✗ 11.5 > 11.1 | ✓ −0.92 | ✓ | 43 | **FAIL (a, b, e)** |
| K-PROD | M5 | ✗ +1.26 | ✗ H2 118.06 < 118.65 | ✓ 1.52 | ✓ +13.03 | ✗ 11.6 > 11.1 | ✓ +1.87 | ✓ | 124 | **FAIL (a, b, e)** |
| K-MIN | M15 | ✗ −2.90 | ✗ 69.05 < 69.10, 82.21 < 83.04 | ✓ 1.51 | ✗ 65.64 < 67.50 | ✓ 21.3 ≤ 23.0 | ✓ −2.71 | ✓ | 54 | **FAIL (a, b, d)** |
| K-PROD | M15 | ✗ −28.98 | ✗ | ✗ 1.29 | ✗ 53.98 | ✗ 28.9 > 23.0 | ✓ −22.62 | ✓ (2026 +4.32, still positive) | 795 | **FAIL (a–e)** |
Reading. K-MIN is the control with a slightly looser confidence floor on B/C/BUY and a tighter one
on A+: it moves 43–54 trades on W2 and changes nothing of substance (−1.2R on M5, −2.9R on M15, PF
within 0.02, OOS mixed). It is a re-labelling of the same rule with a small loss, not a better
decision number. K-PROD is a different rule: on M15 it lets confidence substitute for score, grows
the book by a third and gives back −29R with PF 1.29 and a W3 drawdown of 28.9R (5-year 77.4R, PF
1.13); on M5 it is neutral. On H1 and H4 the product rule adds R (+11.9R and +17.0R on W3) by
taking 40–100 extra low-score trades; H1's PF drops 1.98 → 1.75 and its drawdown doubles, H4 stays
"unproven" (114 → 151 trades). Both are hypotheses for their own pre-registration, not evidence.
## Not shipped (2026-09-27)
No candidate passes on M5 or M15. The two-number rule stays as validated; `signals.grading_mode`
remains a research-only knob (default `pair`, byte-identical books). Follow-ups:
- The "easier to understand" half of the owner's question is a display task: the grade already
combines score, confidence and timeframe agreement, and the surfaces can lead with it and
word the two raw numbers in plain language (no signal change, books untouched).
- H1 / H4 K-PROD (+11.9R / +17.0R on W3, more trades, lower H1 PF, higher DD) may be pre-registered
on its own if the owner wants it; H4 has too few trades for rule 1 and H1's edge already lives
in a small book (253 trades in 3 years).
- Do not re-run these two formulas on M5 or M15 with other thresholds: a tuned threshold would
need its own holdout and the corner-point versions here bracket the plausible range.