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:
+34
-1
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user