From 6b607027e7d6c38e0886d282e6fb288672705d19 Mon Sep 17 00:00:00 2001 From: Shuhao Qing Date: Mon, 13 Apr 2026 23:09:35 +0800 Subject: [PATCH 1/2] fix(market): route 5xxxxx ETF codes to Shanghai exchange prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 56xxxx cross-market ETFs (e.g. 563020 红利低波) were incorrectly routed with 'sz' prefix on Sina/Tencent APIs, hitting a different security (bond "新疆2437") and returning price=100.0 instead of ~1.19. This caused guardrails alerts to report 8303% phantom gains. All 5xxxxx codes (51/56 ETFs, 50 funds) belong to Shanghai exchange on quote APIs. Extract _is_sh() helper and include '5' prefix alongside '6' and '9'. Co-Authored-By: Claude Opus 4.6 (1M context) --- haoinvest/market/sources/_common.py | 17 ++-- tests/test_market/test_sources/test_common.py | 78 +++++++++++++++++++ 2 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 tests/test_market/test_sources/test_common.py diff --git a/haoinvest/market/sources/_common.py b/haoinvest/market/sources/_common.py index 54a24ae..42c8af2 100644 --- a/haoinvest/market/sources/_common.py +++ b/haoinvest/market/sources/_common.py @@ -27,21 +27,28 @@ def bypass_proxy(): os.environ.update(saved) +def _is_sh(symbol: str) -> bool: + """Check if symbol belongs to Shanghai Exchange. + + Shanghai codes: 6xxxxx (main/STAR), 9xxxxx, 5xxxxx (ETF/funds including + 51xxxx, 56xxxx cross-market ETFs that route via SH on quote APIs). + """ + return symbol.startswith(("5", "6", "9")) + + def market_prefix(symbol: str) -> str: """Return 'sh' or 'sz' based on A-share stock code convention.""" - if symbol.startswith(("6", "9")): - return "sh" - return "sz" + return "sh" if _is_sh(symbol) else "sz" def secid(symbol: str) -> str: """Return eastmoney secid like '1.603618' (1=SH, 0=SZ).""" - return f"1.{symbol}" if symbol.startswith(("6", "9")) else f"0.{symbol}" + return f"1.{symbol}" if _is_sh(symbol) else f"0.{symbol}" def exchange_prefix(symbol: str) -> str: """Return 'SH' or 'SZ' for eastmoney web API code parameter.""" - return "SH" if symbol.startswith(("6", "9")) else "SZ" + return "SH" if _is_sh(symbol) else "SZ" def parse_float(value: Any) -> float | None: diff --git a/tests/test_market/test_sources/test_common.py b/tests/test_market/test_sources/test_common.py new file mode 100644 index 0000000..48288df --- /dev/null +++ b/tests/test_market/test_sources/test_common.py @@ -0,0 +1,78 @@ +"""Tests for market source common utilities — prefix routing.""" + +import pytest + +from haoinvest.market.sources._common import ( + exchange_prefix, + market_prefix, + secid, +) + + +class TestMarketPrefix: + """Verify symbol-to-exchange prefix routing.""" + + @pytest.mark.parametrize( + "symbol,expected", + [ + # Shanghai main board + ("600519", "sh"), + ("601877", "sh"), + # Shanghai STAR board + ("688001", "sh"), + # Shanghai ETF (51xxxx) + ("511360", "sh"), + ("513130", "sh"), + ("518880", "sh"), + # Cross-market ETF (56xxxx) — must route via sh + ("563020", "sh"), + ("560010", "sh"), + # Shenzhen main board + ("000001", "sz"), + ("000988", "sz"), + # Shenzhen SME + ("002001", "sz"), + ("002463", "sz"), + # Shenzhen ChiNext + ("300750", "sz"), + # Shenzhen ETF (15xxxx) + ("159915", "sz"), + ], + ) + def test_market_prefix(self, symbol: str, expected: str) -> None: + assert market_prefix(symbol) == expected + + +class TestSecid: + """Verify eastmoney secid mapping.""" + + @pytest.mark.parametrize( + "symbol,expected", + [ + ("600519", "1.600519"), + ("563020", "1.563020"), + ("518880", "1.518880"), + ("000988", "0.000988"), + ("002463", "0.002463"), + ("300750", "0.300750"), + ], + ) + def test_secid(self, symbol: str, expected: str) -> None: + assert secid(symbol) == expected + + +class TestExchangePrefix: + """Verify eastmoney exchange prefix mapping.""" + + @pytest.mark.parametrize( + "symbol,expected", + [ + ("600519", "SH"), + ("563020", "SH"), + ("518880", "SH"), + ("000988", "SZ"), + ("002463", "SZ"), + ], + ) + def test_exchange_prefix(self, symbol: str, expected: str) -> None: + assert exchange_prefix(symbol) == expected From 8bbebe510184ec8e92fa9e1894d65bb6e7d05960 Mon Sep 17 00:00:00 2001 From: Shuhao Qing Date: Mon, 13 Apr 2026 23:16:32 +0800 Subject: [PATCH 2/2] chore(market): remove unused secid(), add B-share test case Address code review feedback: - Remove secid() which had no consumers in application code - Add 900001 (Shanghai B-share) test case for complete _is_sh() coverage Co-Authored-By: Claude Opus 4.6 (1M context) --- haoinvest/market/sources/_common.py | 5 ----- tests/test_market/test_sources/test_common.py | 21 ++----------------- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/haoinvest/market/sources/_common.py b/haoinvest/market/sources/_common.py index 42c8af2..853704f 100644 --- a/haoinvest/market/sources/_common.py +++ b/haoinvest/market/sources/_common.py @@ -41,11 +41,6 @@ def market_prefix(symbol: str) -> str: return "sh" if _is_sh(symbol) else "sz" -def secid(symbol: str) -> str: - """Return eastmoney secid like '1.603618' (1=SH, 0=SZ).""" - return f"1.{symbol}" if _is_sh(symbol) else f"0.{symbol}" - - def exchange_prefix(symbol: str) -> str: """Return 'SH' or 'SZ' for eastmoney web API code parameter.""" return "SH" if _is_sh(symbol) else "SZ" diff --git a/tests/test_market/test_sources/test_common.py b/tests/test_market/test_sources/test_common.py index 48288df..6107a89 100644 --- a/tests/test_market/test_sources/test_common.py +++ b/tests/test_market/test_sources/test_common.py @@ -5,7 +5,6 @@ from haoinvest.market.sources._common import ( exchange_prefix, market_prefix, - secid, ) @@ -35,6 +34,8 @@ class TestMarketPrefix: ("002463", "sz"), # Shenzhen ChiNext ("300750", "sz"), + # Shanghai B-share + ("900001", "sh"), # Shenzhen ETF (15xxxx) ("159915", "sz"), ], @@ -43,24 +44,6 @@ def test_market_prefix(self, symbol: str, expected: str) -> None: assert market_prefix(symbol) == expected -class TestSecid: - """Verify eastmoney secid mapping.""" - - @pytest.mark.parametrize( - "symbol,expected", - [ - ("600519", "1.600519"), - ("563020", "1.563020"), - ("518880", "1.518880"), - ("000988", "0.000988"), - ("002463", "0.002463"), - ("300750", "0.300750"), - ], - ) - def test_secid(self, symbol: str, expected: str) -> None: - assert secid(symbol) == expected - - class TestExchangePrefix: """Verify eastmoney exchange prefix mapping."""