rebuild(stage-6): deliver emotion and market pools

This commit is contained in:
leefer
2026-07-30 03:03:53 +08:00
parent 889963862a
commit 59f6011ae8
30 changed files with 1751 additions and 9 deletions
+150
View File
@@ -19,9 +19,13 @@ from backend.data.contracts import (
from backend.data.gateway import DataGateway
from backend.data.policy import DataPolicyError, DataSourcePolicy
from backend.data.providers.ifind import IfindProvider
from backend.data.providers.tushare import TushareProvider
from backend.data.repository import MarketRepository
from backend.database.connection import Database
from backend.database.migrations import MIGRATIONS, MigrationRunner
from backend.features.market.sentiment import calculate_sentiment
from backend.features.market.snapshot import build_snapshot
from backend.features.market.sync import MarketSnapshotService, SnapshotSyncError
from tests.support import run_scenario
SHANGHAI = ZoneInfo("Asia/Shanghai")
@@ -256,3 +260,149 @@ def test_search_groups_are_fixed_and_require_authentication(tmp_path) -> None:
assert groups[-1]["items"][0]["name"] == "上证指数"
run_scenario(application, scenario)
def calculation_result(rows) -> ProviderResult:
return ProviderResult(
tuple(rows),
ObservationMetadata(
source=DataSource.TUSHARE,
observed_at=datetime(2026, 7, 29, 15, tzinfo=SHANGHAI),
unit="mixed",
adjustment="not_applicable",
freshness_seconds=0,
coverage=1,
state=SnapshotState.FINAL,
usage=DataUsage.CALCULATION,
),
)
def test_market_snapshot_units_and_yesterday_outcomes_are_deterministic() -> None:
daily = [
{
"ts_code": f"00000{index}.SZ",
"close": 10 + index,
"pct_chg": change,
"amount": 100,
}
for index, change in enumerate((10, 4, -10, 2, -2), start=1)
]
def event(index, streak=1):
return {
"ts_code": f"00000{index}.SZ",
"name": f"样本{index}",
"industry": "测试行业",
"close": 10 + index,
"pct_chg": daily[index - 1]["pct_chg"],
"amount": 100,
"limit_times": streak,
}
snapshot = build_snapshot(
"2026-07-29",
"2026-07-28",
{
"daily": calculation_result(daily),
"limit_up": calculation_result([event(1, 2)]),
"broken": calculation_result([event(2)]),
"limit_down": calculation_result([event(3)]),
"previous_limit_up": calculation_result([event(index) for index in range(1, 6)]),
"price_limits": calculation_result(
[{"ts_code": "000002.SZ", "up_limit": 15, "down_limit": 9}]
),
},
)
assert snapshot["overview"]["amount"] == 500_000
assert snapshot["broken"][0]["distance_to_limit"] == 20
assert [row["outcome"] for row in snapshot["yesterday_limits"]] == [
"晋级",
"炸板",
"跌停",
"红盘",
"断板",
]
performance = snapshot["limit_performance"][0]
assert performance == {
"level": 1,
"count": 5,
"advanced": 1,
"red": 1,
"broken": 1,
"opened": 1,
"limit_down": 1,
"advance_rate": 20.0,
"positive_rate": 60.0,
"average_change": 0.8,
}
def test_sentiment_has_all_weighted_components_and_extreme_risk_cap() -> None:
snapshot = {
"overview": {
"up_count": 10,
"down_count": 90,
"limit_up": 10,
"limit_down": 100,
"broken": 20,
"seal_rate": 33.3,
"amount": 100_000_000_000,
},
"limits": [{"streak": 1, "amount": 100_000_000} for _ in range(10)],
"yesterday_limits": [],
}
sentiment = calculate_sentiment(snapshot, [])
assert sentiment["score"] <= 15
assert sentiment["phase"] == "冰点"
assert {item["key"]: item["weight"] for item in sentiment["components"]} == {
"breadth": 20,
"limit_ecology": 25,
"profit_effect": 30,
"ladder_structure": 15,
"liquidity": 10,
}
def test_incomplete_daily_snapshot_is_rejected_without_overwriting(tmp_path) -> None:
database = Database(tmp_path / "sync.db")
MigrationRunner(database).upgrade(MIGRATIONS)
repository = MarketRepository()
with database.transaction() as connection:
repository.replace_calendar(
connection,
(
{"cal_date": "20260728", "is_open": 1, "pretrade_date": "20260727"},
{"cal_date": "20260729", "is_open": 1, "pretrade_date": "20260728"},
),
"tushare",
"2026-07-29T15:00:00+08:00",
)
repository.replace_stocks(
connection,
tuple(
{
"ts_code": f"{index:06d}.SZ",
"symbol": f"{index:06d}",
"name": f"样本{index}",
"industry": "测试",
"list_status": "L",
}
for index in range(1, 101)
),
"tushare",
"2026-07-29T15:00:00+08:00",
)
provider = TushareProvider("test-token")
provider.snapshot_inputs = lambda *_: {
"daily": calculation_result([{"ts_code": "000001.SZ"}]),
"limit_up": calculation_result([]),
"limit_down": calculation_result([]),
"broken": calculation_result([]),
"previous_limit_up": calculation_result([]),
"price_limits": calculation_result([]),
}
service = MarketSnapshotService(database, repository, provider)
with pytest.raises(SnapshotSyncError, match="覆盖率"):
service.sync("2026-07-29", datetime(2026, 7, 30, 16, tzinfo=SHANGHAI))
with database.read() as connection:
assert repository.latest_summary(connection, "2026-07-29") is None