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 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): client = MarketChartClient(FakeIfind(), EastmoneyChartClient()) 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): client = MarketChartClient(FakeIfindStalePreopen(), EastmoneyChartClient()) 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()