fix(HEL-494): 修复个股缺失指标、问天遮罩、四爻外显并回补250日K
Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
c8a9376adb
commit
3e828b346c
@@ -459,6 +459,19 @@ class FrontendContractTests(unittest.TestCase):
|
||||
self.assertIn('payload.question_preset = state.heartQuestionPreset;', self.script)
|
||||
self.assertIn('payload.cast_at = state.heartCastAt;', self.script)
|
||||
|
||||
def test_heaven_loading_timeout_clears_dimmed_state(self):
|
||||
self.assertIn("controller.abort()", self.script)
|
||||
self.assertIn('heavenView?.classList.remove("heaven-data-loading")', self.script)
|
||||
self.assertIn("问天数据仍在准备,页面可继续输入和操作", self.script)
|
||||
self.assertIn("const blocking = !state.heavenSetup;", self.script)
|
||||
self.assertIn("payload?.aborted", self.script)
|
||||
|
||||
def test_stock_detail_does_not_display_missing_metrics_as_zero(self):
|
||||
self.assertIn("function setStockBoardFields(row)", self.script)
|
||||
self.assertIn("function presentMetric(value)", self.script)
|
||||
self.assertIn("payload.available !== false", self.script)
|
||||
self.assertIn('element.textContent = "--"', self.script)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from backend.data.providers.tushare_client import TushareClient, TushareError
|
||||
from backend.data.providers.tushare_helpers import _moneyflow_payload
|
||||
from backend.data.realtime import (
|
||||
WebRealtimeAggregator,
|
||||
_normalize_eastmoney_limit_row,
|
||||
_normalize_eastmoney_stock_quote,
|
||||
)
|
||||
from backend.data.providers.tushare_daily import DailyMarketMixin
|
||||
|
||||
|
||||
class MoneyflowPayloadTests(unittest.TestCase):
|
||||
def test_missing_row_is_not_zero(self) -> None:
|
||||
payload = _moneyflow_payload(None)
|
||||
self.assertFalse(payload["available"])
|
||||
self.assertIsNone(payload["net_million"])
|
||||
self.assertIsNone(payload["large_million"])
|
||||
|
||||
def test_empty_row_is_not_zero(self) -> None:
|
||||
payload = _moneyflow_payload({})
|
||||
self.assertFalse(payload["available"])
|
||||
self.assertIsNone(payload["net_million"])
|
||||
|
||||
def test_real_zero_net_is_kept_when_source_exists(self) -> None:
|
||||
payload = _moneyflow_payload(
|
||||
{
|
||||
"net_mf_amount": 0,
|
||||
"buy_lg_amount": 1,
|
||||
"sell_lg_amount": 1,
|
||||
"buy_elg_amount": 0,
|
||||
"sell_elg_amount": 0,
|
||||
"buy_md_amount": 0,
|
||||
"sell_md_amount": 0,
|
||||
"buy_sm_amount": 0,
|
||||
"sell_sm_amount": 0,
|
||||
}
|
||||
)
|
||||
self.assertTrue(payload["available"])
|
||||
self.assertEqual(payload["net_million"], 0)
|
||||
|
||||
|
||||
class LimitOverlayTests(unittest.TestCase):
|
||||
def test_normalize_limit_keeps_missing_seal_as_none(self) -> None:
|
||||
row = DailyMarketMixin._normalize_limit(
|
||||
{
|
||||
"ts_code": "000737.SZ",
|
||||
"name": "北方铜业",
|
||||
"close": 12.3,
|
||||
"pct_chg": 10,
|
||||
"amount": 1e8,
|
||||
"amount_unit": "yuan",
|
||||
},
|
||||
"涨停",
|
||||
)
|
||||
self.assertIsNone(row["seal_amount_million"])
|
||||
self.assertEqual(row["first_time"], "--")
|
||||
|
||||
def test_overlay_fills_board_times_from_official_list(self) -> None:
|
||||
mixin = DailyMarketMixin()
|
||||
mixin._load_limit_lists = lambda trade_date: [
|
||||
{
|
||||
"ts_code": "000737.SZ",
|
||||
"first_time": "09:31:02",
|
||||
"last_time": "10:18:11",
|
||||
"fd_amount": 82000000,
|
||||
"open_times": 1,
|
||||
"turnover_ratio": 18.4,
|
||||
}
|
||||
]
|
||||
mixin.realtime_aggregator = None
|
||||
rows = mixin._overlay_board_fields(
|
||||
[{"ts_code": "000737.SZ", "close": 12.3, "limit_type": "U"}],
|
||||
"20260908",
|
||||
)
|
||||
self.assertEqual(rows[0]["first_time"], "09:31:02")
|
||||
self.assertEqual(rows[0]["fd_amount"], 82000000)
|
||||
self.assertEqual(rows[0]["turnover_ratio"], 18.4)
|
||||
|
||||
|
||||
class ShenwanRealtimeSourceTests(unittest.TestCase):
|
||||
def test_transport_refuses_rt_sw_k(self) -> None:
|
||||
client = TushareClient(token="demo")
|
||||
with self.assertRaisesRegex(TushareError, "rt_sw_k is disabled"):
|
||||
client.query("rt_sw_k", {"ts_code": "801074.SI"})
|
||||
|
||||
def test_outer_realtime_uses_eastmoney_shenwan_not_rt_sw_k(self) -> None:
|
||||
client = TushareClient(token="demo")
|
||||
client.query = MagicMock(side_effect=AssertionError("should not call tushare"))
|
||||
client.realtime_aggregator = MagicMock()
|
||||
client.realtime_aggregator.eastmoney_shenwan_quote.return_value = {
|
||||
"code": "801074.SI",
|
||||
"name": "工业金属",
|
||||
"close": 1234.5,
|
||||
"pre_close": 1200,
|
||||
"change": 2.88,
|
||||
"pct_change": 2.88,
|
||||
"quote_date": "20260908",
|
||||
"quote_time": "2026-09-08T14:50:00+08:00",
|
||||
"source": "eastmoney_sw",
|
||||
}
|
||||
row, source, error = client._sw_outer_realtime("801074.SI", "工业金属", "20260908")
|
||||
self.assertEqual(source, "eastmoney_sw")
|
||||
self.assertEqual(error, "")
|
||||
self.assertEqual(row["change"], 2.88)
|
||||
client.query.assert_not_called()
|
||||
|
||||
def test_outer_waiting_state_has_no_permission_error(self) -> None:
|
||||
client = TushareClient(token="demo")
|
||||
client.realtime_aggregator = None
|
||||
row, source, error = client._sw_outer_realtime(
|
||||
"801074.SI", "工业金属", "20260908", finalized=True
|
||||
)
|
||||
self.assertEqual(row, {})
|
||||
self.assertIn("尚未入库", error)
|
||||
self.assertNotIn("权限", error)
|
||||
self.assertNotIn("rt_sw_k", error)
|
||||
|
||||
|
||||
class EastmoneyHelperTests(unittest.TestCase):
|
||||
def test_limit_pool_row_keeps_board_clock(self) -> None:
|
||||
parsed = _normalize_eastmoney_limit_row(
|
||||
{
|
||||
"c": "000737",
|
||||
"m": 0,
|
||||
"n": "北方铜业",
|
||||
"fbt": 93102,
|
||||
"lbt": 101811,
|
||||
"zbc": 1,
|
||||
"lbc": 2,
|
||||
"hs": 18.4,
|
||||
"fund": 82000000,
|
||||
},
|
||||
"U",
|
||||
)
|
||||
self.assertEqual(parsed["ts_code"], "000737.SZ")
|
||||
self.assertEqual(parsed["first_time"], "09:31:02")
|
||||
self.assertEqual(parsed["last_time"], "10:18:11")
|
||||
self.assertEqual(parsed["fd_amount"], 82000000)
|
||||
|
||||
def test_stock_quote_keeps_moneyflow_when_present(self) -> None:
|
||||
quote = _normalize_eastmoney_stock_quote(
|
||||
{
|
||||
"f43": 12.3,
|
||||
"f60": 11.18,
|
||||
"f46": 11.2,
|
||||
"f44": 12.3,
|
||||
"f45": 11.1,
|
||||
"f47": 1000,
|
||||
"f48": 150000000,
|
||||
"f58": "北方铜业",
|
||||
"f86": 0,
|
||||
"f168": 8.5,
|
||||
"f62": 25000000,
|
||||
"f78": 3000000,
|
||||
"f84": -1000000,
|
||||
},
|
||||
"000737.SZ",
|
||||
)
|
||||
self.assertEqual(quote["net_mf_amount"], 2500)
|
||||
payload = _moneyflow_payload(quote)
|
||||
self.assertTrue(payload["available"])
|
||||
self.assertEqual(payload["net_million"], 25)
|
||||
|
||||
@patch.object(WebRealtimeAggregator, "_get_json")
|
||||
def test_shenwan_quote_uses_eastmoney_90_prefix(self, get_json: MagicMock) -> None:
|
||||
get_json.return_value = {
|
||||
"rc": 0,
|
||||
"data": {
|
||||
"diff": [
|
||||
{
|
||||
"f12": "801074",
|
||||
"f14": "工业金属",
|
||||
"f2": 1234.5,
|
||||
"f3": 2.88,
|
||||
"f18": 1200,
|
||||
"f17": 1205,
|
||||
"f15": 1240,
|
||||
"f16": 1198,
|
||||
"f6": 1,
|
||||
"f124": 1757319000,
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
quote = WebRealtimeAggregator().eastmoney_shenwan_quote("801074.SI")
|
||||
self.assertEqual(quote["source"], "eastmoney_sw")
|
||||
self.assertAlmostEqual(quote["change"], 2.88)
|
||||
params = get_json.call_args.args[1]
|
||||
self.assertEqual(params["secids"], "90.801074")
|
||||
@@ -319,7 +319,12 @@ class StockDetailRealtimeTests(unittest.TestCase):
|
||||
def test_today_detail_falls_back_to_eastmoney_then_intraday(self):
|
||||
today = FixedMarketDatetime.fixed_now.strftime("%Y%m%d")
|
||||
aggregator = FreeQuoteAggregator(
|
||||
_free_quote("eastmoney_stock", ts_code="600000.SH", name="浦发银行"),
|
||||
_free_quote(
|
||||
"eastmoney_stock",
|
||||
ts_code="600000.SH",
|
||||
name="浦发银行",
|
||||
net_mf_amount=12,
|
||||
),
|
||||
)
|
||||
self.service.realtime_aggregator = aggregator
|
||||
DeniedRealtimeClientStub.quote_calls = 0
|
||||
|
||||
Reference in New Issue
Block a user