fix(market-data): 完善 Tushare 全量同步配置

This commit is contained in:
yuxuanhui
2026-08-06 22:45:18 +08:00
parent afdc5ca909
commit 039a81fe57
6 changed files with 119 additions and 7 deletions
@@ -24,7 +24,8 @@ def normalize_decimal(value: object | None) -> Decimal | None:
Tushare may return a float, a decimal, a string, or ``None`` depending on
the transport. Decimal constructed from the textual representation keeps
those variants from changing snapshot fingerprints.
those variants from changing snapshot fingerprints. NaN representations
are treated as missing, while other non-finite values remain invalid.
"""
if value is None or str(value).strip() == "":
@@ -33,6 +34,8 @@ def normalize_decimal(value: object | None) -> Decimal | None:
result = Decimal(str(value).strip())
except (InvalidOperation, ValueError) as exc:
raise ValueError(f"invalid numeric value: {value!r}") from exc
if result.is_nan():
return None
if not result.is_finite():
raise ValueError(f"numeric value must be finite: {value!r}")
return Decimal(0) if result == 0 else result.normalize()
@@ -57,13 +57,18 @@ class TushareAdapter:
raise ValueError("ZHIXING_TUSHARE_TOKEN is required for market sync")
import tushare as ts # pyright: ignore[reportMissingTypeStubs]
pro_bar_function = cast(
client = cast(object, ts.pro_api(token))
pro_bar = cast(
Callable[..., object],
ts.pro_bar, # pyright: ignore[reportUnknownMemberType]
)
def pro_bar_with_client(**kwargs: object) -> object:
return pro_bar(api=client, **kwargs)
return cls(
cast(object, ts.pro_api(token)),
pro_bar=pro_bar_function,
client,
pro_bar=pro_bar_with_client,
max_retries=max_retries,
backoff_seconds=backoff_seconds,
request_interval_seconds=request_interval_seconds,
@@ -1,12 +1,19 @@
from datetime import date
from decimal import Decimal
import pytest
from zhixing_server.modules.market_data.domain.fingerprint import (
SnapshotChange,
compare_snapshots,
snapshot_fingerprint,
)
from zhixing_server.modules.market_data.domain.models import Bar, Stock, SyncWindow
from zhixing_server.modules.market_data.domain.models import (
Bar,
DailyBasic,
Stock,
SyncWindow,
)
from zhixing_server.modules.market_data.domain.rules import filter_current_hs_a_stocks
@@ -34,6 +41,30 @@ def test_window_uses_inclusive_calendar_boundary() -> None:
assert window.contains(date(2018, 2, 28))
def test_daily_basic_maps_nan_to_none_but_rejects_infinite_values() -> None:
row = DailyBasic.from_mapping(
{
"ts_code": "000001.SZ",
"trade_date": "20240102",
"pe": float("nan"),
"pb": "NaN",
}
)
assert row.pe is None
assert row.pb is None
for infinite_value in (float("inf"), float("-inf")):
with pytest.raises(ValueError, match="must be finite"):
DailyBasic.from_mapping(
{
"ts_code": "000001.SZ",
"trade_date": "20240102",
"pe": infinite_value,
}
)
def test_universe_keeps_current_non_st_hs_a_stocks() -> None:
stocks = (
Stock("000001.SZ", "平安银行", exchange="SZSE", list_status="L"),
@@ -0,0 +1,46 @@
from datetime import date
import pytest
import tushare as ts # pyright: ignore[reportMissingTypeStubs]
from zhixing_server.modules.market_data.domain.models import SyncWindow
from zhixing_server.modules.market_data.infrastructure.tushare import TushareAdapter
def test_from_token_reuses_api_client_for_pro_bar(
monkeypatch: pytest.MonkeyPatch,
) -> None:
created_client = object()
calls: list[dict[str, object]] = []
def fake_pro_api(token: str) -> object:
assert token
return created_client
def fake_pro_bar(**kwargs: object) -> list[dict[str, object]]:
calls.append(kwargs)
return [
{
"ts_code": "000001.SZ",
"trade_date": "20240102",
"close": "10",
}
]
monkeypatch.setattr(ts, "pro_api", fake_pro_api)
monkeypatch.setattr(ts, "pro_bar", fake_pro_bar)
adapter = TushareAdapter.from_token(
"test-token",
request_interval_seconds=0,
)
bars = adapter.fetch_bars(
"000001.SZ",
SyncWindow(start=date(2024, 1, 2), end=date(2024, 1, 2)),
)
assert bars[0].ts_code == "000001.SZ"
assert calls
assert calls[0]["api"] is created_client
assert calls[0]["adj"] == "qfq"