market_outlook: add hourly advisory brief

This commit is contained in:
Harwinder Sekhon
2026-08-18 13:52:18 -04:00
parent bd62028b2f
commit 1f632eaa6a
8 changed files with 848 additions and 3 deletions
+53
View File
@@ -75,6 +75,16 @@ CREATE TABLE IF NOT EXISTS signals (
);
CREATE INDEX IF NOT EXISTS idx_signals_sym_tf_ts
ON signals (symbol, timeframe, logged_at);
CREATE TABLE IF NOT EXISTS outlooks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
logged_at TEXT NOT NULL,
surface TEXT NOT NULL,
bias TEXT,
setup_forming TEXT,
confidence REAL,
watch TEXT,
note TEXT
);
"""
# Additive migration: older DBs created before the panel columns existed
@@ -97,6 +107,12 @@ INSERT INTO signals (
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
_OUTLOOK_INSERT = """
INSERT INTO outlooks (
logged_at, surface, bias, setup_forming, confidence, watch, note
) VALUES (?, ?, ?, ?, ?, ?, ?)
"""
def _finite(x: Any) -> Optional[float]:
"""float(x) or None for non-numeric / non-finite values (NaN/inf —
@@ -323,6 +339,22 @@ class SignalLogger:
(verdict, confidence, rationale, takes_json, rowid))
self._conn.commit()
def log_outlook(self, outlook: Any, surface: str) -> int:
"""Insert one advisory market outlook; returns its rowid."""
row = (
datetime.now(timezone.utc).isoformat(),
surface,
getattr(outlook, "bias", None),
getattr(outlook, "setup_forming", None),
_finite(getattr(outlook, "confidence", None)),
getattr(outlook, "watch", None),
getattr(outlook, "note", None),
)
with self._lock:
cur = self._conn.execute(_OUTLOOK_INSERT, row)
self._conn.commit()
return int(cur.lastrowid)
def close(self) -> None:
with self._lock:
self._conn.close()
@@ -392,6 +424,27 @@ def log_panel_update(report: Any) -> None:
exc_info=True)
def log_outlook(outlook: Any, surface: str = "live") -> None:
"""Persist one advisory outlook when signal logging is enabled.
Outlook persistence is deliberately independent of a signal row: the
feature runs on its own hourly cadence and must never affect a scan.
"""
try:
if not app_config.signal_log_enabled():
return
path = app_config.signal_log_path()
with _loggers_lock:
lg = _loggers.get(path)
if lg is None:
lg = SignalLogger(path)
_loggers[path] = lg
lg.log_outlook(outlook, surface)
except Exception:
logger.warning("signal_log: failed to persist market outlook",
exc_info=True)
def reset_loggers_for_tests() -> None:
"""Close and drop all cached loggers (test isolation)."""
with _loggers_lock: