violent-move watch: display-only warning line + owner heads-up (dark by default)

New violent_watch.py assesses each scan from the cached Forex Factory week events, the
UTC clock and the report's own indicator states, and stamps SignalReport.watch_line
post-hoc (telegram alerts, CLI, dashboard). A standalone heads-up goes to the owner on
the fastest scanned timeframe when the level is "hot", once per UTC hour per trigger
set, capped per day. Config block `violent_watch` (enabled False, alert_line, headsup,
max_headsup_per_day, headsup_min_level); env VIOLENT_WATCH_ENABLED.

Rule chosen from a 3-year bar-by-bar sweep (backtest/research/watch_rule_eval.py,
recorded in sizing_campaign_2026-09.md): hot = high-impact USD release within 30 min
(before or after) OR a hot clock slot (weekday 12-14 UTC, 01 UTC, Sunday 22-23 UTC)
with at least one price-extreme reading (outside Bollinger bands, RSI <30/>=70, outside
Keltner); ~8.6% of bars, next-bar odds 3-5x normal, ~4 hot hours/day. watch = clock
slot alone or two price-extreme readings (~2x). Context readings (ADX rising, extreme
volatility, transitioning, MACD acceleration) are phrases only - letting them raise the
level flagged 27% of bars at ~2x and would have sent ~13 heads-ups/day (rejected).
Direction sentence: DOWN when bearish readings dominate (70-78% on both TFs), UP on M5
only. Never a gate or exit: every open-trade protection tested lost money.

Test-locked out of analyze_from_dataframes / engine / backtester. Module 30 (18 tests).
Suite: 453 passed. Guide PDFs not rebuilt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWV2yLt3YXHHGVYHSQU2EE
This commit is contained in:
Harwinder Sekhon
2026-09-05 21:19:24 -04:00
parent cf2ad73343
commit 97f6a0e206
12 changed files with 986 additions and 5 deletions
+45 -1
View File
@@ -87,7 +87,7 @@ _SCANNABLE_TFS = ("M5", "M15", "H1", "H4")
_KNOWN_TOP = {"telegram", "data_providers", "scanning", "pairs", "log_level",
"oanda", "deepseek", "agent_panel", "market_outlook",
"signal_log"}
"violent_watch", "signal_log"}
_KNOWN_OANDA = {
"account_id", "api_token", "base_url",
}
@@ -101,6 +101,8 @@ _KNOWN_AGENT_PANEL = {
"verdict_source",
}
_KNOWN_MARKET_OUTLOOK = {"enabled", "telegram", "max_per_day", "candles_bars"}
_KNOWN_VIOLENT_WATCH = {"enabled", "alert_line", "headsup",
"max_headsup_per_day", "headsup_min_level"}
_KNOWN_SIGNAL_LOG = {"enabled", "path", "log_blocked_scans"}
_KNOWN_TELEGRAM = {"token", "chat_id", "channel_id", "alert_min_quality"}
_KNOWN_SCANNING = {"pairs", "timeframes"}
@@ -113,6 +115,16 @@ _MARKET_OUTLOOK_DEFAULTS = {
"candles_bars": 20,
}
# Violent-move watch (display-only heads-up) — dark by default.
_VIOLENT_WATCH_DEFAULTS = {
"enabled": False,
"alert_line": True,
"headsup": True,
"max_headsup_per_day": 12,
"headsup_min_level": "hot",
}
_VALID_WATCH_LEVELS = ("watch", "hot")
class AppConfigError(Exception):
"""config.json is present but invalid — fail fast at boot."""
@@ -136,6 +148,8 @@ class AppConfig:
agent_panel_settings: Dict[str, Any] = field(default_factory=dict)
market_outlook_settings: Dict[str, Any] = field(
default_factory=lambda: dict(_MARKET_OUTLOOK_DEFAULTS))
violent_watch_settings: Dict[str, Any] = field(
default_factory=lambda: dict(_VIOLENT_WATCH_DEFAULTS))
signal_log_settings: Dict[str, Any] = field(default_factory=dict)
log_level: str = "INFO"
@@ -337,6 +351,25 @@ def load_app_config(path: str = CONFIG_PATH) -> AppConfig:
f"config.json: market_outlook.{key} must be a positive integer")
app.market_outlook_settings.update(outlook)
watch = raw.get("violent_watch", {})
_check_keys(watch, _KNOWN_VIOLENT_WATCH, '"violent_watch"')
for flag in ("enabled", "alert_line", "headsup"):
if flag in watch and not isinstance(watch[flag], bool):
raise AppConfigError(
f"config.json: violent_watch.{flag} must be true or false")
if "max_headsup_per_day" in watch:
cap = watch["max_headsup_per_day"]
if isinstance(cap, bool) or not isinstance(cap, int) or cap < 0:
raise AppConfigError(
"config.json: violent_watch.max_headsup_per_day must be a "
"non-negative integer")
if ("headsup_min_level" in watch
and watch["headsup_min_level"] not in _VALID_WATCH_LEVELS):
raise AppConfigError(
f'config.json: violent_watch.headsup_min_level '
f'"{watch["headsup_min_level"]}" not in {_VALID_WATCH_LEVELS}')
app.violent_watch_settings.update(watch)
slog = raw.get("signal_log", {})
_check_keys(slog, _KNOWN_SIGNAL_LOG, '"signal_log"')
if "enabled" in slog and not isinstance(slog["enabled"], bool):
@@ -462,6 +495,17 @@ def market_outlook_enabled(app: Optional[AppConfig] = None) -> bool:
return bool(app.market_outlook_settings.get("enabled", False))
# ── Violent-move watch resolution helper ─────────────────────────────
def violent_watch_enabled(app: Optional[AppConfig] = None) -> bool:
"""Whether the display-only violent-move watch is on (env wins)."""
env = os.environ.get("VIOLENT_WATCH_ENABLED")
if env:
return env.strip().lower() in ("1", "true", "yes", "on")
app = app or get_app_config()
return bool(app.violent_watch_settings.get("enabled", False))
# ── Signal-log resolution helpers (env → config.json → default) ──────
def signal_log_enabled(app: Optional[AppConfig] = None) -> bool: