mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 15:44:18 +08:00
fix(client): 历史资金流当日行全零 + 主力净额列缺失(issue #52)
三个根因(全部实测核实): 1. Category 22 直连接口为虚构协议——52 台已知服务器中 46 台可达的 全部仅回 2 字节空包,从未成功过;移除死代码与臆造解析格式。 2. 历史逐笔接口当日数据要收盘清算后才有,日 K 盘中已含当日 bar, 故 start=0 的最新一行恒为全 0——当日 bar 改走当日实时逐笔接口。 3. main_net_inflow 此前仅为 dataclass property,asdict 静默丢弃, 返回 DataFrame 无主力净额列——新增 _fund_flow_df_with_net 物化 (history 紧随 date 列、当日快照放首列)。 sync + async 双客户端同步修改;更新示例与三份文档;重写/新增回归 测试(当日实时逐笔路径、主力净额列断言)。
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
"""针对本轮 A 股增强功能的单元测试。"""
|
||||
|
||||
import asyncio
|
||||
import struct
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import patch
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import pandas as pd
|
||||
|
||||
@@ -46,6 +47,8 @@ def test_get_fund_flow_logic(_mock_conn_cls):
|
||||
assert flow["super_in"].iloc[0] == 1010000.0
|
||||
assert flow["large_out"].iloc[0] == 250000.0
|
||||
assert flow["small_in"].iloc[0] == 10000.0
|
||||
# 当日资金流同样物化主力净额列(Issue #52)
|
||||
assert flow["main_net_inflow"].iloc[0] == 1010000.0 - 250000.0
|
||||
|
||||
|
||||
def test_classify_fund_flow_exact_thresholds_use_lower_bucket():
|
||||
@@ -194,31 +197,9 @@ def test_get_market_stat_mapping(_mock_conn_cls):
|
||||
assert stat["total_market_cap"].iloc[0] == 1186.579 * 1e10
|
||||
|
||||
|
||||
def test_get_history_fund_flow_parsing():
|
||||
"""测试历史资金流序列解析逻辑。"""
|
||||
from easy_tdx.commands.fund_flow import GetHistoryFundFlowCmd
|
||||
|
||||
body = bytearray(9)
|
||||
body.extend(struct.pack("<H", 1))
|
||||
|
||||
date = 20250108
|
||||
record = struct.pack("<IIIIIIIII", date, 100, 200, 300, 400, 500, 600, 700, 800)
|
||||
body.extend(record)
|
||||
|
||||
cmd = GetHistoryFundFlowCmd(Market.SH, "600000", 0, 1)
|
||||
res = cmd.parse_response(bytes(body))
|
||||
|
||||
assert len(res) == 1
|
||||
assert res[0].year == 2025
|
||||
assert res[0].month == 1
|
||||
assert res[0].day == 8
|
||||
|
||||
|
||||
@patch("easy_tdx.client.TdxConnection")
|
||||
def test_get_history_fund_flow_fallback(_mock_conn_cls):
|
||||
"""Category 22 空回包时,自动回退到历史逐笔重算。"""
|
||||
from easy_tdx.commands.fund_flow import GetHistoryFundFlowCmd
|
||||
|
||||
"""资金流由日K取日期 + 历史逐笔重算;返回含 main_net_inflow 列。"""
|
||||
client = TdxClient("127.0.0.1")
|
||||
|
||||
bars = [
|
||||
@@ -236,8 +217,6 @@ def test_get_history_fund_flow_fallback(_mock_conn_cls):
|
||||
}
|
||||
|
||||
def mock_execute(cmd):
|
||||
if isinstance(cmd, GetHistoryFundFlowCmd):
|
||||
return []
|
||||
if isinstance(cmd, GetSecurityBarsCmd):
|
||||
return bars
|
||||
if isinstance(cmd, GetHistoryTransactionDataCmd):
|
||||
@@ -251,11 +230,65 @@ def test_get_history_fund_flow_fallback(_mock_conn_cls):
|
||||
|
||||
assert isinstance(flows, pd.DataFrame)
|
||||
assert len(flows) == 2
|
||||
# 主力净额列必须存在(Issue #52:asdict 丢弃 property 导致此前无此列)
|
||||
assert "main_net_inflow" in flows.columns
|
||||
assert flows.columns[1] == "main_net_inflow"
|
||||
row0 = flows.iloc[0]
|
||||
assert row0["super_in"] == 1010000.0
|
||||
assert row0["large_out"] == 250000.0
|
||||
assert row0["main_net_inflow"] == (1010000.0 + 0.0) - (0.0 + 250000.0)
|
||||
row1 = flows.iloc[1]
|
||||
assert row1["small_in"] == 10000.0
|
||||
# 仅小单流入,不计入主力净额
|
||||
assert row1["main_net_inflow"] == 0.0
|
||||
|
||||
|
||||
@patch("easy_tdx.client.TdxConnection")
|
||||
def test_get_history_fund_flow_today_uses_realtime_ticks(_mock_conn_cls):
|
||||
"""当日 bar 盘中取当日实时逐笔(Issue #52:历史逐笔当日恒空致整行为 0)。"""
|
||||
now = datetime.now(ZoneInfo("Asia/Shanghai"))
|
||||
|
||||
client = TdxClient("127.0.0.1")
|
||||
yesterday = now - timedelta(days=1)
|
||||
bars = [
|
||||
# 顺序与服务器一致:旧 → 新,最新一根是今天
|
||||
SecurityBar(10, 10, 10, 10, 0, 0, yesterday.year, yesterday.month, yesterday.day, 15, 0),
|
||||
SecurityBar(10, 10, 10, 10, 0, 0, now.year, now.month, now.day, 15, 0),
|
||||
]
|
||||
history_txn = {
|
||||
yesterday.year * 10000 + yesterday.month * 100 + yesterday.day: [
|
||||
TransactionRecord(10, 0, 10.0, 10, 0, 0)
|
||||
]
|
||||
}
|
||||
realtime_txn = [TransactionRecord(13, 0, 100.0, 101, 0, 0)]
|
||||
|
||||
seen_cmds = []
|
||||
|
||||
def mock_execute(cmd):
|
||||
seen_cmds.append(type(cmd).__name__)
|
||||
if isinstance(cmd, GetSecurityBarsCmd):
|
||||
return bars
|
||||
if isinstance(cmd, GetTransactionDataCmd):
|
||||
if cmd.start > 0:
|
||||
return []
|
||||
return realtime_txn
|
||||
if isinstance(cmd, GetHistoryTransactionDataCmd):
|
||||
if cmd.start > 0:
|
||||
return []
|
||||
return history_txn.get(cmd.date, [])
|
||||
return []
|
||||
|
||||
with patch.object(TdxClient, "_execute", side_effect=mock_execute):
|
||||
flows = client.get_history_fund_flow(Market.SH, "600000", 0, 2)
|
||||
|
||||
assert len(flows) == 2
|
||||
assert "GetTransactionDataCmd" in seen_cmds
|
||||
today_row = flows.iloc[-1]
|
||||
# 今日行来自实时逐笔:100 元 × 101 手 × 100 = 超大单流入 1010000
|
||||
assert today_row["super_in"] == 1010000.0
|
||||
assert today_row["main_net_inflow"] == 1010000.0
|
||||
# 昨日行来自历史逐笔:小单流入 10000
|
||||
assert flows.iloc[0]["small_in"] == 10000.0
|
||||
|
||||
|
||||
@patch("easy_tdx.client.TdxConnection")
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
"""协议底层修复验证(针对 2026-04-15 审查结论)。"""
|
||||
|
||||
import struct
|
||||
from unittest.mock import patch
|
||||
|
||||
from easy_tdx.codec.price_rules import compute_price_limits
|
||||
from easy_tdx.commands.fund_flow import GetHistoryFundFlowCmd
|
||||
from easy_tdx.commands.security_bars import GetSecurityBarsCmd
|
||||
from easy_tdx.commands.security_list import GetSecurityListCmd
|
||||
from easy_tdx.commands.security_quotes import GetSecurityQuotesCmd
|
||||
@@ -38,33 +36,6 @@ def test_security_bars_exact_layout():
|
||||
assert len(req) == 38
|
||||
|
||||
|
||||
def test_history_fund_flow_exact_layout():
|
||||
"""验证历史资金流请求包布局与 K 线一致,只差 category=22。"""
|
||||
cmd = GetHistoryFundFlowCmd(Market.SH, "600000", 0, 10)
|
||||
req = cmd.build_request()
|
||||
# Header: 0x010C, 0x01016408, 0x1C, 0x1C
|
||||
# Payload: 0x052D, 1 (Market.SH), "600000", 22, 1, 0, 10, 0, 0, 0
|
||||
expected = struct.pack(
|
||||
"<HIHHHH6sHHHHIIH",
|
||||
0x010C,
|
||||
0x01016408,
|
||||
0x001C,
|
||||
0x001C,
|
||||
0x052D,
|
||||
1,
|
||||
b"600000",
|
||||
22,
|
||||
1,
|
||||
0,
|
||||
10,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
assert req == expected
|
||||
assert len(req) == 38
|
||||
|
||||
|
||||
def test_security_list_request_length():
|
||||
"""验证证券列表请求包载荷长度为 6 字节。"""
|
||||
cmd = GetSecurityListCmd(Market.SH, 0)
|
||||
@@ -157,32 +128,3 @@ def test_compute_price_limits_for_newly_listed_stocks():
|
||||
109.67,
|
||||
59.05,
|
||||
)
|
||||
|
||||
|
||||
def test_history_fund_flow_uses_uint32_volume_words():
|
||||
"""历史资金流金额字段必须按 uint32 传给 _decode_volume。"""
|
||||
raw_words = [
|
||||
0x80000001,
|
||||
0xFFFFFFFF,
|
||||
0x7FFFFFFF,
|
||||
0x90000000,
|
||||
0xA0000000,
|
||||
0xB0000000,
|
||||
0xC0000000,
|
||||
0xD0000000,
|
||||
]
|
||||
body = bytearray(9)
|
||||
body.extend(struct.pack("<H", 1))
|
||||
body.extend(struct.pack("<IIIIIIIII", 20250108, *raw_words))
|
||||
|
||||
seen: list[int] = []
|
||||
|
||||
def fake_decode(raw: int) -> float:
|
||||
seen.append(raw)
|
||||
return float(raw)
|
||||
|
||||
with patch("easy_tdx.commands.fund_flow._decode_volume", side_effect=fake_decode):
|
||||
records = GetHistoryFundFlowCmd(Market.SH, "600000", 0, 1).parse_response(bytes(body))
|
||||
|
||||
assert seen == raw_words
|
||||
assert records[0].small_out == float(raw_words[-1])
|
||||
|
||||
Reference in New Issue
Block a user