Files
xiaobai-review/tests/test_ifind_features.py
T
0b8419abca fix(HEL-494): 数据中枢独占调度,主网站不再回退旧接口
主网站只向中枢要业务数据;来源选择、切源、补数全部在中枢内部完成,失败不再走东财/腾讯/Tushare 保底。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-08 21:43:31 +08:00

254 lines
8.0 KiB
Python

from __future__ import annotations
import tempfile
import unittest
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import patch
from backend.features.market.charts import EastmoneyChartClient, MarketChartClient
from database import ReviewDatabase
from backend.features.market.insights import MarketInsightsService
from server import DashboardService
class _FakeDailyHub:
def __init__(self, rows: list) -> None:
self.rows = rows
def try_daily_chart(self, code, end_date, limit, dataset="daily"):
return list(self.rows)
def try_quotes(self, codes):
return None
def try_index_quotes(self):
return None
class FakeIfind:
configured = True
def history(self, codes, indicators, start_date, end_date, cache_ttl=0):
return [
{
"time": "2026-07-27",
"thscode": "000001.SZ",
"open": 10,
"high": 10.5,
"low": 9.8,
"close": 10.2,
"volume": 100,
"amount": 1_000_000,
},
{
"time": "2026-07-28",
"thscode": "000001.SZ",
"open": 10.2,
"high": 10.8,
"low": 10.1,
"close": 10.5,
"volume": 120,
"amount": 1_200_000,
},
]
def real_time(self, codes, indicators, cache_ttl=0):
return []
class FakeIfindStalePreopen(FakeIfind):
def history(self, codes, indicators, start_date, end_date, cache_ttl=0):
return [
*super().history(codes, indicators, start_date, end_date, cache_ttl),
{
"time": "2026-07-29",
"thscode": "000001.SZ",
"open": 10.5,
"high": 10.5,
"low": 10.5,
"close": 10.5,
"volume": 0,
"amount": 0,
},
]
def real_time(self, codes, indicators, cache_ttl=0):
return [
{
"time": "2026-07-28 15:00:00",
"open": 10.2,
"high": 10.8,
"low": 10.1,
"latest": 10.5,
"preClose": 10.2,
"volume": 120,
"amount": 1_200_000,
}
]
class FixedPreopenDatetime(datetime):
fixed_now = datetime(2026, 7, 29, 8, 45, tzinfo=timezone(timedelta(hours=8)))
@classmethod
def now(cls, tz=None):
return cls.fixed_now
class FakeIfindSnapshots:
configured = True
def __init__(self):
self.calls = []
def snapshots(self, codes, indicators, start_time, end_time, cache_ttl=0):
self.calls.append(
{
"codes": codes,
"indicators": indicators,
"start_time": start_time,
"end_time": end_time,
"cache_ttl": cache_ttl,
}
)
return [
{
"time": "2026-07-28 09:21:00",
"thscode": "000001.SZ",
"latest": 10.5,
"preClose": 10,
"volume": 2000,
"amount": 21000,
"bidSize1": 1200,
"askSize1": 800,
}
]
class FakeTushare:
pass
class IfindFeatureTests(unittest.TestCase):
def test_wencai_saved_queries_are_isolated_by_user(self):
with tempfile.TemporaryDirectory() as temporary:
database = ReviewDatabase(Path(temporary) / "review.db")
first = database.create_user("first-user", "salt", "hash")
second = database.create_user("second-user", "salt", "hash")
database.save_wencai_query(first["id"], "高质量", "ROE大于15%", "stock")
self.assertEqual(len(database.list_wencai_saved_queries(first["id"])), 1)
self.assertEqual(database.list_wencai_saved_queries(second["id"]), [])
def test_ifind_daily_chart_normalizes_change(self):
hub = _FakeDailyHub(
[
{
"trade_date": "2026-07-27",
"open": 10,
"high": 10.5,
"low": 9.8,
"close": 10.2,
"volume": 100,
"amount_billion": 0.01,
"change": 0,
},
{
"trade_date": "2026-07-28",
"open": 10.2,
"high": 10.8,
"low": 10.1,
"close": 10.5,
"volume": 120,
"amount_billion": 0.012,
"change": 2.9412,
},
]
)
client = MarketChartClient(FakeIfind(), EastmoneyChartClient(), hub)
rows = client.stock_daily("000001", "20260728")
self.assertEqual(rows[-1]["trade_date"], "2026-07-28")
self.assertAlmostEqual(rows[-1]["change"], 2.9412, places=4)
def test_ifind_daily_chart_keeps_last_traded_bar_before_market_open(self):
hub = _FakeDailyHub(
[
{
"trade_date": "2026-07-28",
"open": 10.2,
"high": 10.8,
"low": 10.1,
"close": 10.5,
"volume": 120,
"amount_billion": 0.012,
}
]
)
client = MarketChartClient(FakeIfindStalePreopen(), EastmoneyChartClient(), hub)
with patch("backend.features.market.charts.datetime", FixedPreopenDatetime):
rows = client.stock_daily("000001", "20260729")
self.assertEqual(rows[-1]["trade_date"], "2026-07-28")
self.assertFalse(rows[-1].get("realtime", False))
def test_event_enrichment_keeps_blank_broken_reason_blank(self):
dashboard = {"broken": [{"code": "000001", "reason": "原原因"}]}
DashboardService._merge_ifind_event_enrichment(
dashboard,
{
"broken": {
"000001": {
"reason": "",
"first_time": "09:42:00",
"last_time": "",
"open_times": 3,
}
}
},
)
self.assertEqual(dashboard["broken"][0]["reason"], "原原因")
self.assertEqual(dashboard["broken"][0]["open_times"], 3)
def test_dynamic_auction_uses_ifind_snapshot_window_and_normalizes_rows(self):
with tempfile.TemporaryDirectory() as temporary:
database = ReviewDatabase(Path(temporary) / "review.db")
database.upsert_stock_master(
[
{
"ts_code": "000001.SZ",
"name": "Ping An Bank",
"industry": "Bank",
"market": "MainBoard",
"list_date": "19910403",
}
]
)
ifind = FakeIfindSnapshots()
service = MarketInsightsService(
database,
FakeTushare(),
now_provider=lambda: datetime(
2026, 7, 28, 9, 22, tzinfo=timezone(timedelta(hours=8))
),
ifind=ifind,
)
service._auction_candidates = lambda rows, baseline: (
[{"ts_code": "000001.SZ"}],
{},
[],
)
rows = service._dynamic_auction_rows("20260728", "20260727", 0)
self.assertEqual(ifind.calls[0]["start_time"], "2026-07-28 09:15:00")
self.assertEqual(ifind.calls[0]["end_time"], "2026-07-28 09:22:00")
self.assertEqual(rows[0]["ts_code"], "000001.SZ")
self.assertEqual(rows[0]["price"], 10.5)
self.assertEqual(rows[0]["snapshot_time"], "2026-07-28 09:21:00")
self.assertTrue(rows[0]["dynamic"])
if __name__ == "__main__":
unittest.main()