Daily-trend veto on M15 + lot-size / already-open lines (campaign 2026-09-30), and the 2026-10-01 whole-codebase bug review

Two bodies of work that touch the same files, so one commit.

Direction and setup-size campaign (2026-09-30 → 10-01, owner's RISK-FIRST rule,
written before the runs):
- SIGNAL: M15 daily-trend veto (factory override 7, xau_strategy.daily_trend_veto
  "both"). A finished setup against a clear daily trend (closed D1 bars, Sunday
  stubs dropped; close vs its 50-bar EMA AND vs the close 60 kept days earlier)
  is removed as the last step of the core. One definition
  (signal_aggregator.daily_trend_series) for live, the bar replay and the twin's
  host side; D1 is a required frame of the M15 scan; mirrored in
  cuda_signal_kernels.py K2. W3 824 / +205.12R / PF 1.73 / max DD 12.4R vs
  1105 / +195.79R / 1.49 / 20.9R; 2019 → 2026 1473 / +223.33R / 1.42 / 41.5R vs
  2091 / +192.53R / 1.24 / 64.8R; unseen holdout −4.62R vs −6.63R.
- FAILED, not shipped: the veto on M5 (W3 drawdown 10.1R → 15.2R) and on H1
  (−9.7R), buy-only and sell-only on M15, the wide-stop cap
  (xau_strategy.wide_stop_cap_atr stays 0.0, a research knob).
- New M15 pins: W2 dfc647ee…, OOS e87b6de1…, W3 ad3b0dbd…. M5 / H1 / H4 pins
  unchanged. --set xau_strategy.daily_trend_veto=off reproduces the previous M15
  pins byte for byte. Twin and reference script MD5-equal on every window;
  recorder and feature-export parity re-proved; base-rate table rebuilt.
- DISPLAY (books byte-identical): exposure.py — the lot size for
  trade_size.risk_per_trade (new config.json block, off by default, private to
  the owner) and the count of alerted same-direction plans still open
  (signals.alerted_at, read-only open_alerted_setups); the daily trend in words
  on Telegram, the CLI and the dashboard; signals.daily_trend logged.
- providers/oanda: a date window with no bars is an empty page, not an error
  (a D1 pad starting on a Saturday could not be fetched).
- Reports only, nothing shipped: seven context screens (no state nominated) and
  eight outside data sources (none adds a forecast to gold's own daily trend).
- Independent verification (report section 5.13): every figure reproduced on
  both scripts, scoring identical to the previous commit on all four
  timeframes, no look-ahead. Caveats recorded there: this is a risk filter, not
  a more accurate direction call; nine tenths of the 2019 → 2026 gain is 2022;
  the neighbour clause is thin (a simple 100-day average misses the R line by
  1.2R); "never adds a trade" is an observation, not a guarantee.

Bug review of the whole codebase (2026-10-01). Signal path clean. 17 defects
elsewhere, plus 4 found by an independent review of the fixes; all fixed, each
with a test; no trade book changed:
- telegram_bot: one _deliver_alert for the candle scan and the level break. The
  row is stamped alerted_at BEFORE the send, on a one-worker alert-db pool, and
  un-stamped when no chat received it or the task is cancelled mid-stamp, so
  two alerts at one candle close count each other and no SQLite write runs on
  the event loop. The 5-bar cooldown counts bars of the scan's own frame
  (== gpu_backtest._apply_cooldown; clock time released the 22:00 UTC bar after
  the settlement pause) and is restored from signals.db at start-up. The
  lot-size line goes to the owner's own chat only (a group the owner ran
  /autoscan in used to receive it). /status gains a news-model line and escapes
  failure details.
- signal_log: mark_alerted returns the stamp it wrote; unmark_alerted;
  last_alerted_bars (read-only).
- advisory_outcome: candle continuity is judged bar by bar inside the walk; a
  hole after a trade's exit no longer voids it. The stored-candle replay
  resolves 108 of 111 logged setups instead of 99, all equal to the live
  resolver's R.
- providers/oanda: an empty date window is accepted only when it is 5 days or
  shorter and no malformed candle was skipped; a longer one is retried, then
  refused, instead of being cached as history with a hole.
- news_engine: blank RSS titles skipped; one unreadable headline no longer
  costs the local model its whole batch (checked on the real bundle); model
  health recorded; no DeepSeek audit call in a process that does not archive;
  event-direction cache keyed per release; peek_week_events never fetches.
- llm_client: chat_json never raises and never logs the API key (requests
  quotes the Authorization value in InvalidHeader; error bodies are redacted).
- signal_aggregator: the scan's news block is guarded; the replay path
  (use_news=False) assigns exactly the values it did before.
- agent_panel: PANEL_PROMPT_VERSION v2.2 — same prompt text, corrected input
  (event timing over the cached calendar week, headlines newest first).
  scripts/panel_replay.py replays the logged payload per persona;
  scripts/label_scan_outcomes.py counts with an ISO cutoff.
- test_all.py: no ALTER TABLE … DROP COLUMN (the live box has SQLite 3.34);
  tests that write Parquet print a notice and return without pyarrow.

Verification after the last code change: suite 632 passed, 0 failed (also with
both Parquet engines blocked and FB2_BACKEND=cpu, the live box's view); the
five reference MD5s reproduce on both backtesters, 0 cache fallbacks; Module 37
green; static Python 3.9 / SQLite 3.34 check clean on every touched file.

Evidence: backtest/research/daily_trend_campaign_2026-09-30.md,
context_screens_2026-10.md, new_inputs_2026-10.md; CLAUDE.md sections 2, 4, 6,
8, 9, 10 and 11; pair_signals/xauusd.py docstring (M15 override 7).
This commit is contained in:
Harwinder Sekhon
2026-10-01 10:28:28 -04:00
parent 42025da0b0
commit 2cc5915b00
31 changed files with 6021 additions and 286 deletions
+15 -4
View File
@@ -549,6 +549,7 @@ function cardShell(tf) {
<span class="grade d-none" data-f="grade"></span>
</div>
<p class="tf-plain d-none" data-f="plain"></p>
<p class="tf-plain d-none" data-f="trend"></p>
<p class="tf-action" data-f="action"></p>
<div class="tf-readouts">
<div>
@@ -589,7 +590,7 @@ function buildCards() {
TF_ORDER.forEach(tf => indChart(tf));
}
function planHTML(ts) {
function planHTML(ts, r) {
if (!ts) {
return `<div class="plan plan-empty">
<i class="fa-regular fa-circle-xmark me-1" aria-hidden="true"></i>
@@ -601,6 +602,11 @@ function planHTML(ts) {
const trail = (ts.trail_arm_r != null && ts.trail_dist_r != null)
? `Exit rule (tested): once the trade is ${ts.trail_arm_r}R in profit, the stop trails ${ts.trail_dist_r}R behind the best price, moved at candle close. Until then the stop is the exit; targets are guides.`
: "Exit rule: the trailing stop uses the platform default; targets are guides.";
// Lot size (present only when the owner shares it on this page) and the
// alerted plans still open in this direction. Both lines are written by
// exposure.py; the page prints them as they come, no arithmetic here.
const notes = [r && r.size_line, r && r.open_line].filter(Boolean)
.map(t => `<div class="plan-trail">${esc(t)}</div>`).join("");
return `<div class="plan ${buy ? "sig-buy" : "sig-sell"}">
<div class="plan-head">
<i class="fa-solid ${buy ? "fa-arrow-trend-up" : "fa-arrow-trend-down"}" aria-hidden="true"></i>
@@ -618,7 +624,7 @@ function planHTML(ts) {
<div><dt><i class="fa-solid fa-flag-checkered" aria-hidden="true"></i>Target 2</dt>
<dd class="up">${fmt$(ts.take_profit_2)}</dd></div>
</dl>
<div class="plan-trail">${esc(trail)}</div>
<div class="plan-trail">${esc(trail)}</div>${notes}
</div>`;
}
@@ -673,6 +679,9 @@ function renderCard(tf) {
: actionTail(r.action));
// Plain words for the two numbers below (from main.plain_words, same on every surface).
f("plain").toggleClass("d-none", !r.plain_words).text(r.plain_words || "");
// The daily trend in words (main.daily_trend_words). On M15 a plan against
// a clear daily trend is removed by the signal core; elsewhere this is context.
f("trend").toggleClass("d-none", !r.daily_trend_words).text(r.daily_trend_words || "");
const score = r.score == null ? 0 : r.score;
f("score").text(signed(r.score, 1))
@@ -692,7 +701,7 @@ function renderCard(tf) {
r.mtf_agreement ? "chip-up" : "") +
(r.exhaustion_count ? chip("fa-battery-quarter", `${r.exhaustion_count} exhaustion signs`, "chip-warn") : ""));
f("plan").html(planHTML(r.trade_setup));
f("plan").html(planHTML(r.trade_setup, r));
f("flags").html(flagsHTML(r, 2));
updateIndChart(tf);
}
@@ -838,11 +847,13 @@ function detailHTML(tf) {
${tile("Event risk", esc(r.event_risk || "—"), r.event_risk === "clear" ? "up" : r.event_risk ? "text-warning" : "")}
${tile("News tone", ns != null ? signed(ns, 2) : "—", ns > 0.05 ? "up" : ns < -0.05 ? "down" : "")}
${tile("Timeframes", r.mtf_agreement ? "agree" : "mixed", r.mtf_agreement ? "up" : "")}
${tile("Daily trend", r.daily_trend == null ? "—" : r.daily_trend > 0 ? "rising" : r.daily_trend < 0 ? "falling" : "no clear direction",
r.daily_trend > 0 ? "up" : r.daily_trend < 0 ? "down" : "")}
${tile("This timeframe alone", signed(r.primary_score, 1), toneClass(r.primary_signal))}
${tile("Exhaustion signs", esc(r.exhaustion_count == null ? "—" : r.exhaustion_count), r.exhaustion_count ? "text-warning" : "")}
</div>`);
out += sec("Trade plan", planHTML(r.trade_setup));
out += sec("Trade plan", planHTML(r.trade_setup, r));
out += sec("Why this signal", reasonsBlock(r) || `<div class="flat">Nothing was recorded on this scan.</div>`);
out += sec(`${TF_LABEL[tf]} indicators`,
analysisBlock(pa, price) || indTable(r.indicators) +