fix(market-data): 完善 Tushare 全量同步配置
This commit is contained in:
@@ -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"
|
||||
Reference in New Issue
Block a user