fix(sector-radar): 支持当前上市股票资金流补拉
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import replace
|
||||
from datetime import UTC, date, datetime, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
@@ -51,6 +52,7 @@ class FakeRadarSource:
|
||||
self.net_scale = net_scale
|
||||
self.fail_daily = False
|
||||
self.calls: list[str] = []
|
||||
self.moneyflow_candidate_codes: list[tuple[str, ...]] = []
|
||||
|
||||
def _result[T](
|
||||
self, api_name: str, target: date | None, rows: tuple[T, ...]
|
||||
@@ -239,8 +241,13 @@ class FakeRadarSource:
|
||||
)
|
||||
return self._result("daily", trade_date, rows)
|
||||
|
||||
def fetch_moneyflow_dc(self, trade_date: date) -> SourceResult[MoneyflowDcRow]:
|
||||
def fetch_moneyflow_dc(
|
||||
self,
|
||||
trade_date: date,
|
||||
candidate_codes: Sequence[str],
|
||||
) -> SourceResult[MoneyflowDcRow]:
|
||||
self.calls.append("moneyflow_dc")
|
||||
self.moneyflow_candidate_codes.append(tuple(candidate_codes))
|
||||
count = 4 if self.missing_moneyflow else 5
|
||||
rows = tuple(
|
||||
MoneyflowDcRow(
|
||||
@@ -288,6 +295,28 @@ def test_successful_build_is_idempotent_and_failed_retry_preserves_last_good() -
|
||||
assert any(item.status is PublicationStatus.FAILED for item in repository.publications.values())
|
||||
|
||||
|
||||
def test_build_passes_stable_current_listing_member_intersection_to_moneyflow() -> None:
|
||||
class FutureListingSource(FakeRadarSource):
|
||||
def fetch_stock_basics(self) -> SourceResult[StockBasicRow]:
|
||||
result = super().fetch_stock_basics()
|
||||
rows = result.rows[:-1] + (replace(result.rows[-1], list_date=date(2027, 1, 1)),)
|
||||
return self._result("stock_basic", None, rows)
|
||||
|
||||
source = FutureListingSource()
|
||||
|
||||
summary = BuildSectorRadar(
|
||||
source,
|
||||
InMemorySectorRadarRepository(),
|
||||
today=TARGET_DATE,
|
||||
now_fn=lambda: NOW,
|
||||
).execute(BuildSectorRadarCommand(trade_date=TARGET_DATE))
|
||||
|
||||
assert summary.status == "success"
|
||||
assert source.moneyflow_candidate_codes == [
|
||||
("000001.SZ", "000002.SZ", "000003.SZ", "000004.SZ")
|
||||
]
|
||||
|
||||
|
||||
def test_source_contract_failure_is_logged_with_safe_build_context(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
@@ -386,6 +415,85 @@ def test_unknown_membership_is_persisted_as_partial_and_retried_independently()
|
||||
assert source.calls == ["members"]
|
||||
|
||||
|
||||
def test_membership_retry_refreshes_moneyflow_when_replay_misses_new_candidates() -> None:
|
||||
class ExpandingMembershipSource(FakeRadarSource):
|
||||
def fetch_sector_members(
|
||||
self,
|
||||
trade_date: date,
|
||||
sector_codes: Sequence[str],
|
||||
) -> SourceResult[SectorMemberRow]:
|
||||
self.calls.append("members")
|
||||
rows: list[SectorMemberRow] = []
|
||||
snapshots: list[SourceSnapshot] = []
|
||||
for index, sector_code in enumerate(sector_codes, start=1):
|
||||
sector_rows = (
|
||||
()
|
||||
if self.missing_membership and index == len(sector_codes)
|
||||
else (
|
||||
SectorMemberRow(
|
||||
trade_date,
|
||||
sector_code,
|
||||
f"00000{index}.SZ",
|
||||
f"股票{index}",
|
||||
),
|
||||
)
|
||||
)
|
||||
snapshots.append(
|
||||
build_source_snapshot(
|
||||
api_name="dc_member",
|
||||
params={
|
||||
"trade_date": trade_date.isoformat(),
|
||||
"ts_code": sector_code,
|
||||
},
|
||||
rows=tuple(self._raw_row(row) for row in sector_rows),
|
||||
target_trade_date=trade_date,
|
||||
partition_key=sector_code,
|
||||
observed_at=NOW,
|
||||
)
|
||||
)
|
||||
rows.extend(sector_rows)
|
||||
return SourceResult(tuple(snapshots), tuple(rows))
|
||||
|
||||
def fetch_moneyflow_dc(
|
||||
self,
|
||||
trade_date: date,
|
||||
candidate_codes: Sequence[str],
|
||||
) -> SourceResult[MoneyflowDcRow]:
|
||||
self.calls.append("moneyflow_dc")
|
||||
self.moneyflow_candidate_codes.append(tuple(candidate_codes))
|
||||
rows = tuple(
|
||||
MoneyflowDcRow(
|
||||
trade_date,
|
||||
code,
|
||||
code,
|
||||
Decimal(1),
|
||||
Decimal(0),
|
||||
Decimal(0),
|
||||
Decimal(10),
|
||||
)
|
||||
for code in candidate_codes
|
||||
)
|
||||
return self._result("moneyflow_dc", trade_date, rows)
|
||||
|
||||
repository = InMemorySectorRadarRepository()
|
||||
source = ExpandingMembershipSource(missing_membership=True)
|
||||
use_case = BuildSectorRadar(source, repository, today=TARGET_DATE, now_fn=lambda: NOW)
|
||||
|
||||
partial = use_case.execute(BuildSectorRadarCommand(trade_date=TARGET_DATE))
|
||||
partial_id = partial.outcomes[0].publication_id
|
||||
assert partial.status == "partial"
|
||||
assert partial_id is not None
|
||||
assert source.moneyflow_candidate_codes == [("000001.SZ",)]
|
||||
|
||||
source.missing_membership = False
|
||||
source.calls.clear()
|
||||
retried = use_case.execute(BuildSectorRadarCommand(retry_publication_id=partial_id))
|
||||
|
||||
assert retried.status == "success"
|
||||
assert source.calls == ["members", "moneyflow_dc"]
|
||||
assert source.moneyflow_candidate_codes[-1] == ("000001.SZ", "000002.SZ")
|
||||
|
||||
|
||||
def test_range_builds_dates_in_order_and_retry_uses_old_target() -> None:
|
||||
repository = InMemorySectorRadarRepository()
|
||||
source = FakeRadarSource(missing_moneyflow=True)
|
||||
|
||||
Reference in New Issue
Block a user