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). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@ if hasattr(sys.stderr, "reconfigure"):
|
||||
from agent_panel import attach_panel
|
||||
from app_config import get_app_config, violent_watch_enabled
|
||||
from config import PlatformConfig, Timeframe, SignalStrength, XAUUSD
|
||||
from exposure import attach_exposure
|
||||
from pair_signals import PAIR_PRODUCERS
|
||||
from signal_aggregator import SignalAggregator, SignalReport
|
||||
from violent_watch import attach_watch
|
||||
@@ -143,6 +144,25 @@ def plain_words(report: SignalReport) -> str:
|
||||
f"Reliability: {reliability_words(report.final_confidence)} · {agree}")
|
||||
|
||||
|
||||
def daily_trend_words(report: SignalReport) -> str:
|
||||
"""The daily trend in words, and whether the trade plan goes with it
|
||||
("Daily trend: rising — this buy goes with it"). "" when the scan
|
||||
carries no daily trend. Display only (2026-09-30): on M15 the signal
|
||||
core already drops a plan against a clear daily trend; on the other
|
||||
timeframes the line is information for the reader."""
|
||||
trend = getattr(report, "daily_trend", None)
|
||||
if trend is None:
|
||||
return ""
|
||||
if trend == 0:
|
||||
return "Daily trend: no clear direction"
|
||||
word = "rising" if trend > 0 else "falling"
|
||||
setup = report.trade_setup
|
||||
if setup is None:
|
||||
return f"Daily trend: {word}"
|
||||
side = "with" if (setup.direction == "BUY") == (trend > 0) else "against"
|
||||
return f"Daily trend: {word} — this {setup.direction.lower()} goes {side} it"
|
||||
|
||||
|
||||
def print_signal_report(report: SignalReport, detail: bool = False):
|
||||
sc = signal_color(report.final_signal)
|
||||
qc = quality_color(report.signal_quality)
|
||||
@@ -156,6 +176,9 @@ def print_signal_report(report: SignalReport, detail: bool = False):
|
||||
f"{sc}{report.final_score:+.1f}{Colors.RESET} · "
|
||||
f"{report.final_confidence:.0f}% · {mtf_tag}")
|
||||
print(f" {plain_words(report)}")
|
||||
trend_words = daily_trend_words(report)
|
||||
if trend_words:
|
||||
print(f" {trend_words}")
|
||||
print(f" {Colors.GRAY}{report.action_recommendation}{Colors.RESET}")
|
||||
|
||||
# ── 2. TRADE SETUP (if signal is actionable) ───────────────────
|
||||
@@ -182,6 +205,11 @@ def print_signal_report(report: SignalReport, detail: bool = False):
|
||||
f"{_fmt_price(arm_price)} (+{arm_r:.1f}R), trail the SL "
|
||||
f"${dist_r * risk_dist:.2f} behind the best price at candle close; "
|
||||
f"TP1 is a guide only.{Colors.RESET}")
|
||||
# Lot size for the owner's risk limit and the alerted plans still
|
||||
# open in this direction (exposure.attach_exposure; display only).
|
||||
for line in (report.size_line, report.open_line):
|
||||
if line:
|
||||
print(f" {Colors.BOLD}{line}{Colors.RESET}")
|
||||
|
||||
# ── 3. CONTEXT (price · regime · vol · MTF breakdown) ──────────
|
||||
print(f"\n {Colors.WHITE}{report.display_name}{Colors.RESET} "
|
||||
@@ -304,6 +332,10 @@ def report_to_dict(report: SignalReport) -> dict:
|
||||
"panel_probability": round(report.panel_probability, 1),
|
||||
"panel_context": report.panel_context,
|
||||
"watch_line": report.watch_line,
|
||||
"daily_trend": report.daily_trend,
|
||||
"daily_trend_words": daily_trend_words(report),
|
||||
"size_line": report.size_line,
|
||||
"open_line": report.open_line,
|
||||
"bullish_reasons": report.bullish_reasons,
|
||||
"bearish_reasons": report.bearish_reasons,
|
||||
"warnings": report.warnings,
|
||||
@@ -422,6 +454,10 @@ def run_live(args, config: PlatformConfig):
|
||||
attach_watch(report, config.primary_timeframe,
|
||||
get_app_config().violent_watch_settings)
|
||||
|
||||
# Display-only lot size (off until config.json sets trade_size) and
|
||||
# the alerted plans still open in this direction.
|
||||
attach_exposure(report, surface="cli")
|
||||
|
||||
if args.json:
|
||||
payload = report_to_dict(report)
|
||||
print(json.dumps(payload, indent=2))
|
||||
|
||||
Reference in New Issue
Block a user