Skip to content

fix(loaders): route tushare ETF/index/HK to correct API endpoints instead of silent fallback#315

Merged
warren618 merged 1 commit into
HKUDS:mainfrom
shadowinlife:fix/tushare-etf-index-routing-310
Jun 26, 2026
Merged

fix(loaders): route tushare ETF/index/HK to correct API endpoints instead of silent fallback#315
warren618 merged 1 commit into
HKUDS:mainfrom
shadowinlife:fix/tushare-etf-index-routing-310

Conversation

@shadowinlife

@shadowinlife shadowinlife commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fix tushare loader _fetch_daily_frame() to route ETF/LOF → fund_daily(), index → index_daily(), HK equity → hk_daily() instead of always calling daily() which returns empty for non-stock symbols
  • Add per-symbol empty-result warning in tushare loader and partial-fetch warning in runner when some symbols are silently dropped in mixed batches
  • Add 42 unit/mock tests + 4 E2E tests (real tushare API) covering all symbol types

Why

Tushare's daily() endpoint only serves A-share stocks. ETF/LOF symbols (510050.SH, 159915.SZ), indices (000001.SH, 399001.SZ), and HK equities (00700.HK) return empty through daily(), causing silent fallback to tencent.

Worse, in mixed batches (e.g. ["600519.SH", "510050.SH"]), the runner's runtime fallback only triggers when ALL symbols are empty — so the ETF is silently dropped from the backtest with no warning at all.

Closes #310

Changes

  • agent/backtest/loaders/tushare.py: Add _ETF_PREFIXES frozenset + _is_etf_listed(), _is_index(), _is_hk_equity(), _is_us_equity(), _is_crypto() detection helpers. Modify _fetch_daily_frame() to route each symbol type to the correct tushare endpoint. Warn and skip US/crypto (not in tushare). Add per-symbol empty-result warning.
  • agent/backtest/runner.py: Add partial-fetch warning when source != "auto" and some requested codes are missing from data_map (catches mixed-batch silent data loss).
  • agent/tests/test_tushare_loader.py (new): 42 unit/mock tests for all detection helpers + routing verification via unittest.mock. 4 E2E tests gated behind TUSHARE_TOKEN env var (auto-skip when absent).

Test Plan

Automated tests

  • Existing tests pass (pytest tests/test_baostock_loader.py tests/test_akshare_loader.py tests/test_loader_retry_helpers.py tests/test_local_loader.py -q54 passed in 1.11s)
  • New unit + mock tests (pytest tests/test_tushare_loader.py -v42 passed, 4 skipped in 0.47s)
  • New E2E tests with real tushare token (TUSHARE_TOKEN=... pytest tests/test_tushare_loader.py::TestTushareE2E -v4 passed in 0.98s)

E2E / manual test cases (real tushare API)

All tests use TUSHARE_TOKEN env var, date range 2025-01-02 to 2025-01-10.

# Test case Symbol(s) Endpoint used Expected Result
E1 A-share stock via daily() 000001.SZ api.daily() Non-empty DataFrame 7 rows
E2 ETF via fund_daily() 510050.SH api.fund_daily() Non-empty DataFrame 7 rows
E3 Index via index_daily() 000001.SH api.index_daily() Non-empty DataFrame 7 rows
E4 Mixed batch (stock + ETF + index) 600519.SH, 510050.SH, 000001.SH per-symbol routing All 3 symbols returned 3/3 symbols, 7 rows each

Pre-fix behavior (verified against upstream main):

Symbol api.daily() rows Correct endpoint rows Bug?
000001.SZ (stock) 7 7
600519.SH (stock) 7 7
510050.SH (ETF) 0 7 (fund_daily) Confirmed
510300.SH (ETF) 0 7 (fund_daily) Confirmed
159915.SZ (ETF) 0 7 (fund_daily) Confirmed
588000.SH (STAR ETF) 0 7 (fund_daily) Confirmed
161725.SZ (LOF) 0 7 (fund_daily) Confirmed
000001.SH (index) 0 7 (index_daily) Confirmed
399001.SZ (index) 0 7 (index_daily) Confirmed
00700.HK (HK equity) 0 7 (hk_daily) Confirmed

Unit test breakdown (42 tests, no network required)

Class Count Coverage
TestIsEtfListed 18 7 ETF prefixes (51/52/56/58/15/16/50) + 11 non-ETF (stocks, US, HK, crypto, empty, malformed)
TestIsIndex 11 5 indices (000001.SH, 000300.SH, 000016.SH, 399001.SZ, 399006.SZ) + 6 non-indices (stocks, ETF, US, empty)
TestIsHkEquity 2 HK match + non-HK skip
TestIsUsEquity 2 US match + non-US skip
TestIsCrypto 2 Crypto match + non-crypto skip
TestFetchDailyFrameRouting 7 stock→daily, ETF→fund_daily, index→index_daily, HK→hk_daily, US→None+warn, crypto→None+warn, empty→warn

Checklist

  • No changes to protected areas (src/agent/, src/session/, src/providers/) without prior discussion
  • No hardcoded values (API keys, file paths, magic numbers)
  • Code follows CONTRIBUTING.md guidelines
  • Documentation updated (if user-facing change)
)

Tushare's daily() only serves A-share stocks. ETF/LOF symbols need
fund_daily(), indices need index_daily(), and HK equities need
hk_daily(). Previously all codes went through daily(), returning
empty for non-stock types and silently falling back to tencent.

- Add _is_etf_listed/_is_index/_is_hk_equity detection helpers
- Route _fetch_daily_frame to correct endpoint per symbol type
- Warn and skip US/crypto (not in tushare)
- Add runner partial-fetch warning for mixed batches
- Unit + mock + E2E tests

Closes HKUDS#310

Co-Authored-By: OpenCode <noreply@opencode.ai>
AI-Model: qwen3.7-max
Co-Authored-By: opencode <noreply@ai-tool.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
AI-Contributed/Feature: 70/70
AI-Contributed/UT: 237/237
@shadowinlife shadowinlife force-pushed the fix/tushare-etf-index-routing-310 branch from 0663985 to 8323cb3 Compare June 26, 2026 03:32
@shadowinlife

Copy link
Copy Markdown
Contributor Author

Post-implementation review follow-up fixes

After the 5-agent parallel review (/review-work), the Context Mining agent identified two sibling routing gaps in the same file. Both have been fixed in the amended commit:

B1. _fetch_minutes stk_mins routing gap (fixed)

_fetch_minutes() had the same structural bug: stk_mins() is stock-only in tushare, but was called for all symbol types. Since tushare has no fund_mins endpoint (verified empirically), ETF/index/HK/US/crypto intraday now explicitly warn+skip instead of returning empty with a misleading "points >= 2000 required" message.

B2. _merge_basic_fields daily_basic routing gap (fixed)

_merge_basic_fields() called daily_basic() for all codes, but daily_basic is stock-only. Added an explicit guard that skips non-stock symbols (ETF/index/HK/US/crypto) before calling daily_basic, replacing the silent try/except swallow with a clear code path.

M3. Shared _symbol_utils.py extraction (fixed)

Extracted _ETF_PREFIXES and _is_etf_listed() into agent/backtest/loaders/_symbol_utils.py. Both tushare.py and akshare_loader.py now import from the shared module, eliminating the DRY violation.

Test additions

  • 7 new _fetch_minutes routing mock tests (ETF/index/HK/US/crypto warn+skip, stock keeps stk_mins)
  • 6 new _merge_basic_fields guard tests (non-stock codes skip daily_basic)
  • 1 stock baseline test

Verified test results

Suite Result
test_tushare_loader.py (unit+mock) 55 passed
test_tushare_loader.py::TestTushareE2E (real API) 4 passed
test_akshare_loader.py (regression after shared extraction) 21 passed
Other loader tests (baostock + retry + local) 33 passed
@warren618 warren618 merged commit 5106b7b into HKUDS:main Jun 26, 2026
1 check passed
@warren618

Copy link
Copy Markdown
Collaborator

Merged 🎉 Thanks @shadowinlife — routing ETF/LOF → fund_daily(), indices → index_daily(), HK → hk_daily() instead of letting daily() silently return empty is a clean fix, and the shared _symbol_utils plus the per-symbol / partial-fetch warnings make the old silent-drop failure mode visible. Closes #310. Shipped to main in 5106b7b (with a tiny follow-up 4a57c79 dropping the now-unused _ETF_PREFIXES import).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants