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:
+25
-3
@@ -169,13 +169,24 @@ class ForexDataProvider:
|
|||||||
return cached
|
return cached
|
||||||
|
|
||||||
# Symbols not on Twelve Data (gold, silver) always use yfinance.
|
# Symbols not on Twelve Data (gold, silver) always use yfinance.
|
||||||
# D1 is intentionally skipped for these — yfinance daily for futures
|
# D1 is skipped (yfinance daily for futures is unreliable).
|
||||||
# is unreliable and the engine drops missing confirmation TFs cleanly.
|
# H4 is resampled from H1 since yfinance has no native 4h candle.
|
||||||
if symbol in _YF_FALLBACK_MAP:
|
if symbol in _YF_FALLBACK_MAP:
|
||||||
if timeframe == Timeframe.D1:
|
if timeframe == Timeframe.D1:
|
||||||
raise DataUnavailableError(
|
raise DataUnavailableError(
|
||||||
f"Daily chart skipped for {symbol} — gold/silver use M15/H1 only."
|
f"Daily chart skipped for {symbol} — gold/silver use M15/H1/H4 only."
|
||||||
)
|
)
|
||||||
|
if timeframe == Timeframe.H4:
|
||||||
|
h1_df = self._fetch_yfinance_fallback(symbol, Timeframe.H1)
|
||||||
|
if h1_df is None:
|
||||||
|
raise DataUnavailableError(
|
||||||
|
f"Yahoo Finance unavailable for {symbol} (H4 resample source H1)."
|
||||||
|
)
|
||||||
|
df = self._resample_h1_to_h4(h1_df)
|
||||||
|
df = self._validate_and_clean(df, symbol, timeframe)
|
||||||
|
self._cache[cache_key] = df
|
||||||
|
self._cache_timestamps[cache_key] = now
|
||||||
|
return df
|
||||||
yf_df = self._fetch_yfinance_fallback(symbol, timeframe)
|
yf_df = self._fetch_yfinance_fallback(symbol, timeframe)
|
||||||
if yf_df is None:
|
if yf_df is None:
|
||||||
raise DataUnavailableError(
|
raise DataUnavailableError(
|
||||||
@@ -277,6 +288,17 @@ class ForexDataProvider:
|
|||||||
}
|
}
|
||||||
return fallback_spreads.get(symbol, 0.00020)
|
return fallback_spreads.get(symbol, 0.00020)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _resample_h1_to_h4(h1_df: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""Resample 1-hour OHLCV to 4-hour bars (yfinance has no native 4h)."""
|
||||||
|
return h1_df.resample("4h", label="right", closed="right").agg({
|
||||||
|
"Open": "first",
|
||||||
|
"High": "max",
|
||||||
|
"Low": "min",
|
||||||
|
"Close": "last",
|
||||||
|
"Volume": "sum",
|
||||||
|
}).dropna()
|
||||||
|
|
||||||
def get_pair_info(self, symbol: str) -> Dict:
|
def get_pair_info(self, symbol: str) -> Dict:
|
||||||
"""Return display name and pip value for a pair."""
|
"""Return display name and pip value for a pair."""
|
||||||
display = SYMBOL_DISPLAY.get(symbol, symbol)
|
display = SYMBOL_DISPLAY.get(symbol, symbol)
|
||||||
|
|||||||
+25
-6
@@ -31,6 +31,16 @@ from news_engine import NewsEngine, FundamentalContext
|
|||||||
logger = logging.getLogger(__name__)
|
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
|
@dataclass
|
||||||
class TradeSetup:
|
class TradeSetup:
|
||||||
"""A complete trade setup recommendation."""
|
"""A complete trade setup recommendation."""
|
||||||
@@ -118,11 +128,12 @@ class SignalAggregator:
|
|||||||
reports = []
|
reports = []
|
||||||
context = self.news_engine.get_fundamental_context()
|
context = self.news_engine.get_fundamental_context()
|
||||||
|
|
||||||
# Batch-prefetch all pairs × all needed timeframes (3 API calls instead of 21)
|
# Batch-prefetch all pairs × all needed timeframes (3 API calls instead of 21).
|
||||||
all_timeframes = (
|
# Union of primary + per-symbol confirmation TFs across the watchlist.
|
||||||
[self.config.primary_timeframe] + self.config.confirmation_timeframes
|
tf_set = {self.config.primary_timeframe, *self.config.confirmation_timeframes}
|
||||||
)
|
for sym in self.config.watch_pairs:
|
||||||
self.data_provider.prefetch_batch(self.config.watch_pairs, all_timeframes)
|
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:
|
for symbol in self.config.watch_pairs:
|
||||||
try:
|
try:
|
||||||
@@ -179,7 +190,7 @@ class SignalAggregator:
|
|||||||
return report
|
return report
|
||||||
|
|
||||||
# ── 2. Confirmation Timeframe Analyses ─────────────────────
|
# ── 2. Confirmation Timeframe Analyses ─────────────────────
|
||||||
for tf in self.config.confirmation_timeframes:
|
for tf in self._confirmation_tfs(symbol):
|
||||||
try:
|
try:
|
||||||
cf_df = self.data_provider.fetch_ohlcv(symbol, tf)
|
cf_df = self.data_provider.fetch_ohlcv(symbol, tf)
|
||||||
cf_analysis = self.tech_engine.analyze(cf_df, symbol, tf.value)
|
cf_analysis = self.tech_engine.analyze(cf_df, symbol, tf.value)
|
||||||
@@ -220,6 +231,14 @@ class SignalAggregator:
|
|||||||
|
|
||||||
return report
|
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:
|
def _check_mtf_agreement(self, report: SignalReport) -> bool:
|
||||||
"""Check if multiple timeframes agree on direction."""
|
"""Check if multiple timeframes agree on direction."""
|
||||||
signals = list(report.mtf_signals.values())
|
signals = list(report.mtf_signals.values())
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# ── Config ──────────────────────────────────────────────────────────
|
# ── Config ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
SCAN_TIMEFRAMES = ["H1"] # engine pulls M15 + H1 + D1 via confirmations
|
SCAN_TIMEFRAMES = ["M15", "H1"] # two cards (M15 and H1) per /scan; cache shared
|
||||||
|
|
||||||
# Pairs analyzed by the /scan command. Limited to the most liquid majors
|
# Pairs analyzed by the /scan command. Limited to the most liquid majors
|
||||||
# plus gold to keep credits low and signal quality high.
|
# plus gold to keep credits low and signal quality high.
|
||||||
|
|||||||
Reference in New Issue
Block a user