Tighten signal pipeline: yfinance-only XAU/XAG, leaner Telegram /scan
- XAU/XAG always route to Yahoo Finance and skip Twelve Data entirely (excluded from batch prefetch, served per-symbol via yfinance). - Daily chart cleanly skipped for gold/silver — engine drops it from MTF. - Telegram /scan limited to EURUSD, USDJPY, GBPUSD, XAUUSD. - SCAN_TIMEFRAMES reduced to single H1 primary; engine still pulls M15+H1+D1 internally via confirmation TFs (halves API credits per scan). - Default confirmation TFs swapped H4 -> D1 in config and main.py. - mtf_agreement_threshold kept at 2-of-3. - CLAUDE.md cleaned up — old XAUUSD/synthetic-data bug reports removed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,64 +1,10 @@
|
|||||||
# ForexBot2 — Known Bugs & Dev Notes
|
# ForexBot2 — Dev Notes
|
||||||
|
|
||||||
## Bug 1 — Concurrent TF scans exhaust API credits instantly
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
Batch prefetch failed for 1wk: You have run out of API credits for the current minute.
|
|
||||||
9 API credits were used, with the current limit at https://twelvedata.com/pricing
|
|
||||||
```
|
|
||||||
Warnings appear for every timeframe (1h, 4h, 1d, 1wk) in rapid succession.
|
|
||||||
|
|
||||||
**Root cause:**
|
|
||||||
`telegram_bot.py` runs all 4 `SCAN_TIMEFRAMES` concurrently via `ThreadPoolExecutor`.
|
|
||||||
Each `SignalAggregator` fires its own batch prefetch simultaneously.
|
|
||||||
Free tier limit is **8 credits/minute** — 4 parallel scans hit this in under a second.
|
|
||||||
|
|
||||||
**Impact:**
|
|
||||||
Confirmation timeframes fail and fall back to synthetic data.
|
|
||||||
Primary TF still has real data (first request wins the credits).
|
|
||||||
|
|
||||||
**Fix (not yet implemented):**
|
|
||||||
Run timeframes sequentially with a 60-second wait between each, or reduce
|
|
||||||
`SCAN_TIMEFRAMES` to a single primary TF and use the engine's built-in
|
|
||||||
multi-timeframe confirmation (which already batches efficiently in 3 calls).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Bug 2 — XAUUSD (Gold) shows wrong price (~1.01 instead of ~3300)
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
Entry: 1.01037
|
|
||||||
Take Profit: 1.00767 (+0.3 pips)
|
|
||||||
Stop Loss: 1.01172 (-0.1 pips)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Root cause:**
|
|
||||||
Two compounding issues:
|
|
||||||
|
|
||||||
1. XAUUSD is not available on the Twelve Data **free tier** — API returns no data.
|
|
||||||
2. `data_provider._generate_synthetic_data()` uses a `pair_params` dict for base
|
|
||||||
prices. XAUUSD either falls through to a wrong default (~1.0) instead of the
|
|
||||||
correct ~3300 baseline.
|
|
||||||
|
|
||||||
Check `data_provider.py` → `_generate_synthetic_data()` → `pair_params` dict.
|
|
||||||
Verify `"XAUUSD=X"` has a correct `"price"` entry around `3300.0`.
|
|
||||||
|
|
||||||
**Impact:**
|
|
||||||
All indicator values, entry, TP, and SL for XAUUSD are meaningless when
|
|
||||||
running on the free tier (synthetic data with wrong base price).
|
|
||||||
|
|
||||||
**Fix (not yet implemented):**
|
|
||||||
- Correct the base price in `pair_params` for `XAUUSD=X`.
|
|
||||||
- Add a bot-level guard: if `report.current_price < 100` for XAUUSD,
|
|
||||||
suppress the trade setup and show a "data unavailable" message instead.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Free tier: 8 credits/min, 800 credits/day. Each full scan = ~21 credits.
|
- Free tier: 8 credits/min, 800 credits/day. Each full scan = ~21 credits.
|
||||||
- Supported pairs (live data on free tier): EURUSD, GBPUSD, USDJPY, USDCHF,
|
- Supported pairs (live data on free tier): EURUSD, GBPUSD, USDJPY, USDCHF,
|
||||||
AUDUSD, USDCAD, NZDUSD, EURGBP, EURJPY, GBPJPY, AUDJPY, EURCHF, CADJPY.
|
AUDUSD, USDCAD, NZDUSD, EURGBP, EURJPY, GBPJPY, AUDJPY, EURCHF, CADJPY.
|
||||||
- All other symbols (XAU, XAG, commodities, indices) fall back to synthetic GBM data.
|
- XAU/XAG and other unsupported symbols fall back to yfinance.
|
||||||
|
- No synthetic data fallback — if both Twelve Data and yfinance fail, the
|
||||||
|
request errors out and no signal is generated.
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ class PlatformConfig:
|
|||||||
# Analysis timeframes (primary + confirmation)
|
# Analysis timeframes (primary + confirmation)
|
||||||
primary_timeframe: Timeframe = Timeframe.H1
|
primary_timeframe: Timeframe = Timeframe.H1
|
||||||
confirmation_timeframes: List[Timeframe] = field(
|
confirmation_timeframes: List[Timeframe] = field(
|
||||||
default_factory=lambda: [Timeframe.M15, Timeframe.H4]
|
default_factory=lambda: [Timeframe.M15, Timeframe.D1]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Data source
|
# Data source
|
||||||
|
|||||||
+27
-3
@@ -157,6 +157,24 @@ class ForexDataProvider:
|
|||||||
if cached is not None and not cached.empty:
|
if cached is not None and not cached.empty:
|
||||||
return cached
|
return cached
|
||||||
|
|
||||||
|
# Symbols not on Twelve Data (gold, silver) always use yfinance.
|
||||||
|
# D1 is intentionally skipped for these — yfinance daily for futures
|
||||||
|
# is unreliable and the engine drops missing confirmation TFs cleanly.
|
||||||
|
if symbol in _YF_FALLBACK_MAP:
|
||||||
|
if timeframe == Timeframe.D1:
|
||||||
|
raise DataUnavailableError(
|
||||||
|
f"Daily chart skipped for {symbol} — gold/silver use M15/H1 only."
|
||||||
|
)
|
||||||
|
yf_df = self._fetch_yfinance_fallback(symbol, timeframe)
|
||||||
|
if yf_df is None:
|
||||||
|
raise DataUnavailableError(
|
||||||
|
f"Yahoo Finance unavailable for {symbol} ({timeframe.value})."
|
||||||
|
)
|
||||||
|
df = self._validate_and_clean(yf_df, symbol, timeframe)
|
||||||
|
self._cache[cache_key] = df
|
||||||
|
self._cache_timestamps[cache_key] = now
|
||||||
|
return df
|
||||||
|
|
||||||
df = self._fetch_twelve_data(symbol, timeframe, lookback_days)
|
df = self._fetch_twelve_data(symbol, timeframe, lookback_days)
|
||||||
df = self._validate_and_clean(df, symbol, timeframe)
|
df = self._validate_and_clean(df, symbol, timeframe)
|
||||||
|
|
||||||
@@ -181,6 +199,12 @@ class ForexDataProvider:
|
|||||||
if lookback_days is None:
|
if lookback_days is None:
|
||||||
lookback_days = self.config.data_lookback_days
|
lookback_days = self.config.data_lookback_days
|
||||||
|
|
||||||
|
# XAU/XAG aren't on Twelve Data — exclude them from the batch call,
|
||||||
|
# they'll be fetched individually via yfinance on demand.
|
||||||
|
td_symbols = [s for s in symbols if s not in _YF_FALLBACK_MAP]
|
||||||
|
if not td_symbols:
|
||||||
|
return
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
for tf in timeframes:
|
for tf in timeframes:
|
||||||
ttl = self._cache_ttl(tf)
|
ttl = self._cache_ttl(tf)
|
||||||
@@ -188,13 +212,13 @@ class ForexDataProvider:
|
|||||||
f"{s}_{tf.value}" in self._cache_timestamps
|
f"{s}_{tf.value}" in self._cache_timestamps
|
||||||
and (now - self._cache_timestamps[f"{s}_{tf.value}"]) < ttl
|
and (now - self._cache_timestamps[f"{s}_{tf.value}"]) < ttl
|
||||||
and not self._cache.get(f"{s}_{tf.value}", pd.DataFrame()).empty
|
and not self._cache.get(f"{s}_{tf.value}", pd.DataFrame()).empty
|
||||||
for s in symbols
|
for s in td_symbols
|
||||||
)
|
)
|
||||||
if cached:
|
if cached:
|
||||||
logger.debug(f"Skipping prefetch for {tf.value} — all {len(symbols)} symbols cached (TTL {ttl}s)")
|
logger.debug(f"Skipping prefetch for {tf.value} — all {len(td_symbols)} symbols cached (TTL {ttl}s)")
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
self._batch_fetch_timeframe(symbols, tf, lookback_days)
|
self._batch_fetch_timeframe(td_symbols, tf, lookback_days)
|
||||||
time.sleep(1.0) # brief pause between TF requests
|
time.sleep(1.0) # brief pause between TF requests
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Batch prefetch failed for {tf.value}: {e}")
|
logger.warning(f"Batch prefetch failed for {tf.value}: {e}")
|
||||||
|
|||||||
@@ -403,17 +403,18 @@ def run_analysis(args) -> List[SignalReport]:
|
|||||||
}
|
}
|
||||||
config.primary_timeframe = tf_map[args.timeframe]
|
config.primary_timeframe = tf_map[args.timeframe]
|
||||||
|
|
||||||
# Configure confirmation timeframes based on primary
|
# Configure confirmation timeframes based on primary.
|
||||||
|
# H1 uses M15 + D1 for high-conviction confluence (3-of-3 threshold).
|
||||||
tf_confirmations = {
|
tf_confirmations = {
|
||||||
"M5": [Timeframe.M1, Timeframe.M15],
|
"M5": [Timeframe.M1, Timeframe.M15],
|
||||||
"M15": [Timeframe.M5, Timeframe.H1],
|
"M15": [Timeframe.H1, Timeframe.D1],
|
||||||
"M30": [Timeframe.M15, Timeframe.H1],
|
"M30": [Timeframe.H1, Timeframe.D1],
|
||||||
"H1": [Timeframe.M15, Timeframe.H4],
|
"H1": [Timeframe.M15, Timeframe.D1],
|
||||||
"H4": [Timeframe.H1, Timeframe.D1],
|
"H4": [Timeframe.H1, Timeframe.D1],
|
||||||
"D1": [Timeframe.H4, Timeframe.W1],
|
"D1": [Timeframe.H4, Timeframe.W1],
|
||||||
}
|
}
|
||||||
config.confirmation_timeframes = tf_confirmations.get(
|
config.confirmation_timeframes = tf_confirmations.get(
|
||||||
args.timeframe, [Timeframe.M15, Timeframe.H4]
|
args.timeframe, [Timeframe.M15, Timeframe.D1]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Configure pairs
|
# Configure pairs
|
||||||
|
|||||||
+7
-3
@@ -40,7 +40,11 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# ── Config ──────────────────────────────────────────────────────────
|
# ── Config ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
SCAN_TIMEFRAMES = ["M15", "H1"]
|
SCAN_TIMEFRAMES = ["H1"] # engine pulls M15 + H1 + D1 via confirmations
|
||||||
|
|
||||||
|
# Pairs analyzed by the /scan command. Limited to the most liquid majors
|
||||||
|
# plus gold to keep credits low and signal quality high.
|
||||||
|
SCAN_PAIRS = ["EURUSD=X", "USDJPY=X", "GBPUSD=X", "XAUUSD=X"]
|
||||||
|
|
||||||
_TF_MAP = {
|
_TF_MAP = {
|
||||||
"M5": Timeframe.M5, "M15": Timeframe.M15,
|
"M5": Timeframe.M5, "M15": Timeframe.M15,
|
||||||
@@ -268,7 +272,7 @@ class ForexTelegramBot:
|
|||||||
def _build_config(self, tf: str, pairs: List[str]) -> PlatformConfig:
|
def _build_config(self, tf: str, pairs: List[str]) -> PlatformConfig:
|
||||||
cfg = PlatformConfig()
|
cfg = PlatformConfig()
|
||||||
cfg.primary_timeframe = _TF_MAP[tf]
|
cfg.primary_timeframe = _TF_MAP[tf]
|
||||||
cfg.confirmation_timeframes = _TF_CONFIRMATIONS.get(tf, [Timeframe.M15, Timeframe.H4])
|
cfg.confirmation_timeframes = _TF_CONFIRMATIONS.get(tf, [Timeframe.M15, Timeframe.D1])
|
||||||
cfg.watch_pairs = list(pairs)
|
cfg.watch_pairs = list(pairs)
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
@@ -418,7 +422,7 @@ class ForexTelegramBot:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
results_by_tf = await self._analyze_all_tfs(list(MAJOR_PAIRS), progress=progress)
|
results_by_tf = await self._analyze_all_tfs(list(SCAN_PAIRS), progress=progress)
|
||||||
now_str = _now_est()
|
now_str = _now_est()
|
||||||
|
|
||||||
# Best grade per pair across all TFs
|
# Best grade per pair across all TFs
|
||||||
|
|||||||
Reference in New Issue
Block a user