Skip to content

Commit 52c6f45

Browse files
committed
fix: add runtime fallback when primary data source returns empty
When a loader (e.g. OKX) is reachable but returns no data at runtime (network block, SSL failure, regional restriction), the backtest runner now walks the FALLBACK_CHAINS to try the next available source (e.g. ccxt/Binance). Covers both explicit source and auto-detect paths.
1 parent 2ef5cab commit 52c6f45

1 file changed

Lines changed: 35 additions & 0 deletions

File tree

agent/backtest/runner.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
pass
2626

2727
from backtest.loaders.registry import (
28+
FALLBACK_CHAINS,
2829
LOADER_REGISTRY,
2930
get_loader_cls_with_fallback,
3031
resolve_loader,
@@ -302,6 +303,25 @@ def main(run_dir: Path) -> None:
302303
fields=config.get("extra_fields") or None,
303304
interval=interval,
304305
)
306+
# Runtime fallback: try next sources in chain when primary returns empty
307+
if not data_map and codes:
308+
market = _detect_market(codes[0])
309+
for fb_name in FALLBACK_CHAINS.get(market, []):
310+
if fb_name == source or fb_name not in LOADER_REGISTRY:
311+
continue
312+
fb_loader = LOADER_REGISTRY[fb_name]()
313+
if not fb_loader.is_available():
314+
continue
315+
fb_codes = _normalize_codes(codes, fb_name)
316+
data_map = fb_loader.fetch(
317+
fb_codes, config.get("start_date", ""),
318+
config.get("end_date", ""), interval=interval,
319+
)
320+
if data_map:
321+
logger.info("Runtime fallback: %s -> %s", source, fb_name)
322+
source = fb_name
323+
loader = fb_loader
324+
break
305325
if not data_map:
306326
print(json.dumps({"error": "No data fetched"}))
307327
sys.exit(1)
@@ -476,6 +496,21 @@ def _fetch_auto(codes: List[str], config: dict, interval: str = "1D") -> dict:
476496
normalized_codes = _normalize_codes(market_codes, src_name)
477497
fields = config.get("extra_fields") if src_name == "tushare" else None
478498
result = loader.fetch(normalized_codes, start_date, end_date, fields=fields, interval=interval)
499+
500+
# Runtime fallback: try remaining sources when primary returns empty
501+
if not result:
502+
for fb_name in FALLBACK_CHAINS.get(market, []):
503+
if fb_name == src_name or fb_name not in LOADER_REGISTRY:
504+
continue
505+
fb_loader = LOADER_REGISTRY[fb_name]()
506+
if not fb_loader.is_available():
507+
continue
508+
fb_codes = _normalize_codes(market_codes, fb_name)
509+
result = fb_loader.fetch(fb_codes, start_date, end_date, interval=interval)
510+
if result:
511+
logger.info("Runtime fallback: %s -> %s for %s", src_name, fb_name, market)
512+
break
513+
479514
merged.update(result)
480515

481516
return merged

0 commit comments

Comments
 (0)