XAU/XAG: 3-TF analysis (M15+H1+H4 resampled) via per-symbol override

- New _PER_SYMBOL_TFS map in signal_aggregator routes gold/silver to
  M15+H1+H4 confirmation; forex pairs untouched.
- H4 for XAU/XAG is resampled from yfinance H1 bars (yfinance has no
  native 4h candle). New _resample_h1_to_h4 helper.
- Batch prefetch unions per-symbol TFs so future per-pair overrides
  work without extra plumbing. XAU/XAG still excluded from Twelve Data
  batch — H4 resample triggers a single yfinance H1 fetch.
- Telegram SCAN_TIMEFRAMES restored to [M15, H1] so /scan emits both
  M15-primary and H1-primary cards. Shared cache means no extra credits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Harwinder
2026-05-05 20:12:22 -04:00
parent 2700b0845d
commit f5d6d35063
3 changed files with 51 additions and 10 deletions
+25 -6
View File
@@ -31,6 +31,16 @@ from news_engine import NewsEngine, FundamentalContext
logger = logging.getLogger(__name__)
# Per-symbol full TF list (primary + confirmations). Overrides the global
# config.confirmation_timeframes for these symbols only — forex pairs are
# unaffected. XAU/XAG use M15+H1+H4 (no D1 since yfinance daily for futures
# is unreliable; H4 is resampled from H1 in data_provider).
_PER_SYMBOL_TFS: Dict[str, List[Timeframe]] = {
"XAUUSD=X": [Timeframe.M15, Timeframe.H1, Timeframe.H4],
"XAGUSD=X": [Timeframe.M15, Timeframe.H1, Timeframe.H4],
}
@dataclass
class TradeSetup:
"""A complete trade setup recommendation."""
@@ -118,11 +128,12 @@ class SignalAggregator:
reports = []
context = self.news_engine.get_fundamental_context()
# Batch-prefetch all pairs × all needed timeframes (3 API calls instead of 21)
all_timeframes = (
[self.config.primary_timeframe] + self.config.confirmation_timeframes
)
self.data_provider.prefetch_batch(self.config.watch_pairs, all_timeframes)
# Batch-prefetch all pairs × all needed timeframes (3 API calls instead of 21).
# Union of primary + per-symbol confirmation TFs across the watchlist.
tf_set = {self.config.primary_timeframe, *self.config.confirmation_timeframes}
for sym in self.config.watch_pairs:
tf_set.update(self._confirmation_tfs(sym))
self.data_provider.prefetch_batch(self.config.watch_pairs, list(tf_set))
for symbol in self.config.watch_pairs:
try:
@@ -179,7 +190,7 @@ class SignalAggregator:
return report
# ── 2. Confirmation Timeframe Analyses ─────────────────────
for tf in self.config.confirmation_timeframes:
for tf in self._confirmation_tfs(symbol):
try:
cf_df = self.data_provider.fetch_ohlcv(symbol, tf)
cf_analysis = self.tech_engine.analyze(cf_df, symbol, tf.value)
@@ -220,6 +231,14 @@ class SignalAggregator:
return report
def _confirmation_tfs(self, symbol: str) -> List[Timeframe]:
"""Return confirmation TFs for a symbol, honouring per-symbol overrides."""
override = _PER_SYMBOL_TFS.get(symbol)
if override is None:
return self.config.confirmation_timeframes
primary = self.config.primary_timeframe
return [tf for tf in override if tf != primary]
def _check_mtf_agreement(self, report: SignalReport) -> bool:
"""Check if multiple timeframes agree on direction."""
signals = list(report.mtf_signals.values())