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:
+53
-1
@@ -25,6 +25,7 @@ bot, CLI and backtester all see the same overlay (live == backtest).
|
||||
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
@@ -90,7 +91,8 @@ _KNOWN_TOP = {"telegram", "data_providers", "scanning", "pairs", "log_level",
|
||||
# config.json files still carry the block and must keep
|
||||
# loading; load_app_config logs one warning when it is there.
|
||||
"market_outlook",
|
||||
"violent_watch", "signal_log", "news_archive", "news_model"}
|
||||
"violent_watch", "signal_log", "news_archive", "news_model",
|
||||
"trade_size"}
|
||||
_KNOWN_OANDA = {
|
||||
# "account_id" is TOLERATED but ignored: it belonged to the auto-
|
||||
# execution daemon removed 2026-09-03 (last commit 31002cb). The candle
|
||||
@@ -113,6 +115,19 @@ _KNOWN_SIGNAL_LOG = {"enabled", "path", "log_blocked_scans"}
|
||||
_KNOWN_NEWS_ARCHIVE = {"enabled", "path"}
|
||||
_KNOWN_NEWS_MODEL = {"path", "threads", "mode", "deepseek_audit_per_day"}
|
||||
_NEWS_MODEL_MODES = ("off", "fallback", "primary")
|
||||
# Lot-size guidance on the alert (exposure.py, display only). Off until
|
||||
# the owner sets risk_per_trade. The size reveals the owner's risk per
|
||||
# trade, so it is printed in the owner's own chat (telegram.chat_id) and
|
||||
# the CLI only; telegram_channel switches it on for every other Telegram
|
||||
# chat (the subscriber channel, a group), dashboard for the public page.
|
||||
_KNOWN_TRADE_SIZE = {"risk_per_trade", "contract_size", "lot_step",
|
||||
"telegram_channel", "dashboard"}
|
||||
_TRADE_SIZE_DEFAULTS = {
|
||||
"contract_size": 100.0, # ounces per 1.00 lot of XAU/USD
|
||||
"lot_step": 0.01, # smallest size increment the broker accepts
|
||||
"telegram_channel": False,
|
||||
"dashboard": False,
|
||||
}
|
||||
_KNOWN_TELEGRAM = {"token", "chat_id", "channel_id", "alert_min_quality"}
|
||||
_KNOWN_SCANNING = {"pairs", "timeframes"}
|
||||
_KNOWN_PAIR_KEYS = {"blocked_hours_utc"}
|
||||
@@ -153,6 +168,7 @@ class AppConfig:
|
||||
signal_log_settings: Dict[str, Any] = field(default_factory=dict)
|
||||
news_archive_settings: Dict[str, Any] = field(default_factory=dict)
|
||||
news_model_settings: Dict[str, Any] = field(default_factory=dict)
|
||||
trade_size_settings: Dict[str, Any] = field(default_factory=dict)
|
||||
log_level: str = "INFO"
|
||||
|
||||
|
||||
@@ -432,6 +448,27 @@ def load_app_config(path: str = CONFIG_PATH) -> AppConfig:
|
||||
"is not off")
|
||||
app.news_model_settings = dict(nm)
|
||||
|
||||
size = raw.get("trade_size", {})
|
||||
_check_keys(size, _KNOWN_TRADE_SIZE, '"trade_size"')
|
||||
for key in ("risk_per_trade", "contract_size", "lot_step"):
|
||||
if key in size:
|
||||
v = size[key]
|
||||
if (isinstance(v, bool) or not isinstance(v, (int, float))
|
||||
or not math.isfinite(v)):
|
||||
raise AppConfigError(
|
||||
f"config.json: trade_size.{key} must be a number")
|
||||
# risk_per_trade 0 = the size line is off (the default)
|
||||
if v < 0 or (v == 0 and key != "risk_per_trade"):
|
||||
raise AppConfigError(
|
||||
f"config.json: trade_size.{key} must be "
|
||||
+ ("zero (off) or a positive number"
|
||||
if key == "risk_per_trade" else "a positive number"))
|
||||
for flag in ("telegram_channel", "dashboard"):
|
||||
if flag in size and not isinstance(size[flag], bool):
|
||||
raise AppConfigError(
|
||||
f"config.json: trade_size.{flag} must be true or false")
|
||||
app.trade_size_settings = dict(size)
|
||||
|
||||
level = raw.get("log_level", app.log_level)
|
||||
if level not in _VALID_LOG_LEVELS:
|
||||
raise AppConfigError(
|
||||
@@ -685,6 +722,21 @@ def news_archive_path(app: Optional[AppConfig] = None) -> str:
|
||||
return path
|
||||
|
||||
|
||||
# ── Lot-size guidance (config.json only; display) ────────────────────
|
||||
|
||||
def trade_size_settings(app: Optional[AppConfig] = None) -> Optional[Dict[str, Any]]:
|
||||
"""The lot-size guidance settings with their defaults filled in, or
|
||||
None when the owner has not set `trade_size.risk_per_trade` (the size
|
||||
line is then off on every surface)."""
|
||||
app = app or get_app_config()
|
||||
block = app.trade_size_settings or {}
|
||||
if not block.get("risk_per_trade"):
|
||||
return None
|
||||
out = dict(_TRADE_SIZE_DEFAULTS)
|
||||
out.update(block)
|
||||
return out
|
||||
|
||||
|
||||
# ── PlatformConfig overlay ────────────────────────────────────────────
|
||||
|
||||
def apply_pair_overrides(
|
||||
|
||||
Reference in New Issue
Block a user