Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
194 lines
6.9 KiB
Python
194 lines
6.9 KiB
Python
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")
|