signal_log: persist every live scan (signal, grade, price, news) to SQLite

New signal_log.py logs each SignalReport to logs/signals.db via a
fail-safe hook in SignalAggregator.analyze_pair — the live-only entry
point used by the bot, daemon, CLI and dashboard. Backtester calls
analyze_from_dataframes directly, so live==backtest parity is preserved
(test-locked). Rows carry signal/grade/price/spread, MTF + trade-setup +
reasons/warnings JSON, and news-at-scan-time (calendar events + scored
headlines JSON via NewsEngine class-level caches).

Config: signal_log block (enabled, path; env SIGNAL_LOG_ENABLED/PATH),
source tagged per consumer (telegram/oanda_daemon/cli/dashboard).
381 tests pass (24 modules).
This commit is contained in:
Harwinder Sekhon
2026-08-13 10:24:49 -04:00
parent 887aaed6f2
commit e609b433b0
10 changed files with 685 additions and 7 deletions
+34 -1
View File
@@ -21,6 +21,7 @@ from config import PlatformConfig, SignalStrength, Timeframe
from providers import BaseDataProvider, make_data_provider
from engine import TechnicalEngine, TechnicalAnalysis
from news_engine import NewsEngine, FundamentalContext
import signal_log
logger = logging.getLogger(__name__)
@@ -142,8 +143,14 @@ class SignalAggregator:
self,
symbol: Optional[str] = None,
context: Optional[FundamentalContext] = None,
log_source: Optional[str] = None,
) -> SignalReport:
"""Live-mode entry: fetch all needed timeframes then delegate."""
"""Live-mode entry: fetch all needed timeframes then delegate.
Every report produced here (including weekend/day-filter early
returns) is persisted to the signal-history DB via _log_signal —
fail-safe, and never reached by the backtester, which calls
analyze_from_dataframes directly (live == backtest parity)."""
if symbol is None:
symbol = self.config.symbol
@@ -164,6 +171,7 @@ class SignalAggregator:
report.trade_setup = None
report.warnings.append("WEEKEND FILTER: XAU/USD market closed — no liquidity")
report.action_recommendation = "NO TRADE — Weekend (market closed)"
self._log_signal(report, log_source)
return report
if xau_cfg.allowed_weekdays and now_utc.weekday() not in xau_cfg.allowed_weekdays:
@@ -181,6 +189,7 @@ class SignalAggregator:
f"DAY FILTER: {day_names.get(now_utc.weekday(), '?')} not in allowed days ({allowed})"
)
report.action_recommendation = f"NO TRADE — Only trading {allowed}"
self._log_signal(report, log_source)
return report
timeframes = [self.config.primary_timeframe] + list(
@@ -258,8 +267,32 @@ class SignalAggregator:
f"{symbol}: live session filter errored — filter SKIPPED this scan"
)
self._log_signal(report, log_source)
return report
def _log_signal(
self, report: SignalReport, source: Optional[str]
) -> None:
"""Persist the scan to the signal-history DB (signal_log.py).
Fail-safe by contract — a logging failure must never break a
live scan. The news context re-fetch rides the NewsEngine's
class-level caches, so it costs nothing within a scan and
matches the context the signal just used; if it fails, the row
is still written, just without the news JSON blobs."""
context = None
try:
context = self._get_news_engine().get_fundamental_context()
except Exception:
logger.debug("signal log: news context unavailable",
exc_info=True)
signal_log.log_signal(
report,
context=context,
source=source or "live",
timeframe=self.config.primary_timeframe.value,
)
def _apply_regime_filter_live(
self, report: SignalReport, dfs: Dict[Timeframe, pd.DataFrame]
) -> None: