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:
GitHub
2026-08-26 15:00:53 +08:00
parent 7c9e19de93
commit 574ffdd2a4
8 changed files with 158 additions and 227 deletions
+6 -4
View File
@@ -329,10 +329,10 @@ c.get_block_info(filename: str) -> list[TdxBlock]
### get_fund_flow
```python
c.get_fund_flow(market: Market, code: str) -> FundFlow
c.get_fund_flow(market: Market, code: str) -> pd.DataFrame
```
获取个股当日资金流向(基于 L1 逐笔数据统计)。
获取个股当日资金流向(基于 L1 逐笔数据统计)。返回含 `main_net_inflow`(主力净流入)列。
**资金分级**
| 级别 | 单笔成交额 |
@@ -346,10 +346,12 @@ c.get_fund_flow(market: Market, code: str) -> FundFlow
```python
c.get_history_fund_flow(market: Market, code: str,
start: int, count: int) -> list[HistoricalFundFlow]
start: int, count: int) -> pd.DataFrame
```
获取历史日线资金流向序列。优先走直连接口,若服务器不支持则自动回退为逐笔成交重算。
获取历史日线资金流向序列,由"日K线取日期 + 逐笔成交重算"实现(标准服务器
无资金流专用指令,Issue #52)。当日 bar 盘中取当日实时逐笔。返回列含
`main_net_inflow`(主力净流入,单位元)。
---
+2 -2
View File
@@ -260,6 +260,7 @@
| 字段名 | 中文 | 类型 | 说明 |
|--------|------|------|------|
| `main_net_inflow` | 主力净流入 | `float` | 列:超大+大净额(Issue #52 起物化为列) |
| `super_in` | 超大单流入 | `float` | 单笔 >100 万 |
| `super_out` | 超大单流出 | `float` | |
| `large_in` | 大单流入 | `float` | 20~100 万 |
@@ -268,7 +269,6 @@
| `medium_out` | 中单流出 | `float` | |
| `small_in` | 小单流入 | `float` | ≤4 万 |
| `small_out` | 小单流出 | `float` | |
| `main_net_inflow` | 主力净流入 | `float` | 属性:超大+大 |
| `total_net_inflow` | 全单净流入 | `float` | 属性:全部 |
---
@@ -277,7 +277,7 @@
来源:`get_history_fund_flow()`
字段同 FundFlow,额外包含:
字段同 FundFlow`main_net_inflow` 同样物化为列,紧随 `date` 之后),额外包含:
| 字段名 | 中文 | 类型 |
|--------|------|------|
+8 -5
View File
@@ -231,12 +231,15 @@ gotdx 同样命名为 `Unknown`pytdx 直接 `pos += 9`。
fixture 中 87 条记录全部为 0x00(87/87)。无其他可能值样本,推测为保留/对齐字节。
gotdx 命名为 `Unknown`pytdx 注释 `# noused`
### 4.3 fund_flow 9字节响应头部(已确认:同 xdxr_info 格式
### 4.3 fund_flowCategory 22)——已证伪并移除(Issue #52
**位置**`commands/fund_flow.py:45`
**位置**`commands/fund_flow.py`(已删除)
与 xdxr_info 相同的 9 字节头部格式:prefix(2) + market(1) + code(6)。
后接 uint16 num(记录数量),然后是 36 字节/条的固定记录。
曾有实现假设 0x052D + category=22 为"历史资金流向"指令,并按
"9 字节头 + uint16 num + 36 字节/条"解析。**2026-08-26 实测**Issue #52):
全部 46 台可达标准行情服务器对该请求仅回 2 字节 body(`0000``2003`),
即 0 条记录 / ret_count 撒谎空包,从未在任何环境返回过有效数据。
响应格式属臆造,该命令已删除;资金流改为"日K取日期 + 逐笔成交重算"。
gotdx 未实现此命令。
@@ -374,7 +377,7 @@ xdxr_info 9字节头部(fixture 验证):
xdxr_info 每条1字节 padding
- 87 条记录全部为 0x00,保留/对齐字节
fund_flow 9字节头部:同 xdxr_info 格式prefix + market + code
fund_flow 9字节头部:~~同 xdxr_info 格式~~ 2026-08-26 证伪(见 4.3,命令已删除
block 384字节头部:gotdx 命名"头信息, 忽略",所有实现跳过
+13 -11
View File
@@ -2,18 +2,20 @@
使用 TdxClient 标准协议客户端,调用 get_history_fund_flow() 获取个股历史每日资金流向。
返回 HistoricalFundFlow DataFrame,每行代表一个交易日的资金流向数据。
优先走 Category 22 直连接口;若服务器返回空,自动回退为日K线+逐笔重算。
实现为"日K线取日期 + 逐笔成交重算"(通达信标准服务器无资金流专用指令,
Category 22 实测全部服务器仅回空包,见 Issue #52)。
DataFrame 列说明:
date str 交易日期(datetime
super_in float 超大单流入(元)
super_out float 超大单流(元)
large_in float 大单流(元)
large_out float 大单流(元)
medium_in float 单流(元)
medium_out float 中单流(元)
small_in float 单流(元)
small_out float 小单流(元)
date str 交易日期(datetime
main_net_inflow float 主力净流入 = (超大单+大单)流入 - 流出(元)
super_in float 超大单流(元)
super_out float 大单流(元)
large_in float 大单流(元)
large_out float 单流(元)
medium_in float 中单流(元)
medium_out float 单流(元)
small_in float 小单流(元)
small_out float 小单流出(元)
资金级别划分(按单笔成交金额):
超大单: > 100 万元
@@ -24,7 +26,7 @@ DataFrame 列说明:
数据特点:
- start 为偏移量,0=最近交易日,count 为请求数量
- 金额单位为元
- 部分服务器不支持 Category 22,此时自动回退到逐笔重算模式(较慢
- 当日行情:盘中走当日实时逐笔接口(收盘清算后历史逐笔接口才有当日数据
"""
from easy_tdx import Market, TdxClient
+70 -42
View File
@@ -39,7 +39,6 @@ from .commands.base import BaseCommand
from .commands.block_info import GetBlockInfoCmd, GetBlockInfoMetaCmd
from .commands.company_info import GetCompanyInfoCategoryCmd, GetCompanyInfoContentCmd
from .commands.finance_info import GetFinanceInfoCmd
from .commands.fund_flow import GetHistoryFundFlowCmd
from .commands.minute_time import GetHistoryMinuteTimeDataCmd
from .commands.report_file import GetReportFileCmd
from .commands.security_bars import GetIndexBarsCmd, GetSecurityBarsCmd
@@ -87,6 +86,26 @@ def _today_in_shanghai() -> int:
return int(datetime.now(_SHANGHAI_TZ).strftime("%Y%m%d"))
def _fund_flow_df_with_net(df: pd.DataFrame) -> pd.DataFrame:
"""为资金流 DataFrame 物化主力净额列。
``HistoricalFundFlow.main_net_inflow`` / ``FundFlow.main_net_inflow`` 是
dataclass property``_to_df`` 的 asdict 会静默丢弃(Issue #52:用户
"取不到主力净额"的直接原因),这里显式物化为 ``main_net_inflow`` 列
(单位:元,正=净流入)。放在 date 列之后(无 date 时放首列)。
"""
if df.empty or "super_in" not in df.columns:
return df
out = df.copy()
pos = 1 if "date" in out.columns else 0
out.insert(
pos,
"main_net_inflow",
(out["super_in"] + out["large_in"]) - (out["super_out"] + out["large_out"]),
)
return out
def _record_signature(
record: TransactionRecord,
) -> tuple[int, int, float, int, int, int]:
@@ -871,31 +890,32 @@ class TdxClient:
return all_recs
def get_fund_flow(self, market: Market, code: str) -> pd.DataFrame:
"""获取个股当日资金流向分布(基于 L1 逐笔数据统计)。"""
"""获取个股当日资金流向分布(基于 L1 逐笔数据统计)。
返回列含 ``main_net_inflow``(主力净流入,单位元)。
"""
records = self._collect_transaction_records(
lambda start, page_size: self._execute(
GetTransactionDataCmd(market, code, start, page_size)
),
2000,
)
return _to_df(_classify_fund_flow(records))
return _fund_flow_df_with_net(_to_df(_classify_fund_flow(records)))
def _fetch_fund_flow_records(
self, market: Market, code: str, start: int, count: int
) -> list[HistoricalFundFlow]:
"""在当前 host 上获取资金流记录(直连 + K 线回退)。
"""在当前 host 上获取资金流记录(日 K 线取日期 + 逐笔成交重算)。
优先走 Category 22 直连接口;空则回退为"日 K 线取日期 + 历史逐笔成交重算"
返回空列表代表该 host 既无直连数据也无 K 线数据(或解析失败)
通达信标准行情服务器没有"历史资金流向"专用指令:曾经的 Category 22
直连请求实测在全部已知服务器上仅返回 2 字节空包(Issue #52),已移除
资金流一律由逐笔成交重算:历史日期走历史逐笔接口;当日成交在历史逐笔
接口里要收盘清算后才有,当日 bar 盘中改走当日实时逐笔接口——此前当日
行恒为全 0,用户"取不到最新主力净额"的直接原因(Issue #52)。
返回空列表代表该 host 无 K 线数据(或解析失败)。
"""
try:
direct = self._execute(GetHistoryFundFlowCmd(market, code, start, count))
except Exception:
direct = []
if direct:
return list(direct)
bars = self._execute(GetSecurityBarsCmd(market, code, KlineCategory.DAY, start, count))
today = _today_in_shanghai()
results: list[HistoricalFundFlow] = []
for bar in bars:
date = _date_from_bar(bar)
@@ -904,9 +924,12 @@ class TdxClient:
def _fetch_page(
page_start: int, page_size: int, _d: int = date
) -> list[TransactionRecord]:
return self._execute(
GetHistoryTransactionDataCmd(market, code, _d, page_start, page_size)
)
cmd: BaseCommand[list[TransactionRecord]]
if _d == today:
cmd = GetTransactionDataCmd(market, code, page_start, page_size)
else:
cmd = GetHistoryTransactionDataCmd(market, code, _d, page_start, page_size)
return self._execute(cmd)
records = self._collect_transaction_records(_fetch_page, 800)
results.append(_historical_fund_flow_from_records(date, records))
@@ -917,10 +940,13 @@ class TdxClient:
) -> pd.DataFrame:
"""获取个股历史日线资金流向序列。
优先走 Category 22 直连接口;若服务器返回空列表,则自动回退为
"日 K 线取日期 + 历史逐笔成交重算资金流"的兼容实现。
实现:"日 K 线取日期 + 逐笔成交重算资金流"。通达信标准服务器无
资金流专用指令(Category 22 实测全空,见 Issue #52);当日 bar 盘中
走当日实时逐笔接口,收盘清算后走历史逐笔接口。
空数据故障转移(v1.20.5Issue #41):当前 host 直连与 K 线回退均空时,
返回列含 ``main_net_inflow``(主力净流入 = 超大单+大单净额,单位元)。
空数据故障转移(v1.20.5Issue #41):当前 host 无 K 线数据时,
按延迟顺序逐台实测找首台返回有效数据的服务器。部分服务器对常见标的也会
返回 ret_count 撒谎的空 body(日志"K线响应为空(声称 800 条..."),
此前直接返回空 DataFrame,用户拿不到数据;现复用 K 线故障转移的同源逻辑。
@@ -931,14 +957,14 @@ class TdxClient:
# 空数据故障转移:与 get_security_bars / get_index_bars 同源逻辑。
if not results and self._auto_reconnect:
results = self._fund_flow_failover(market, code, start, count)
return _to_df(results)
return _fund_flow_df_with_net(_to_df(results))
def _fund_flow_failover(
self, market: Market, code: str, start: int, count: int
) -> list[HistoricalFundFlow]:
"""资金流空数据故障转移:逐台实测找首台返回有效数据的服务器。
与 ``_find_host_returning_data`` 区别:资金流获取涉及多命令(直连 / K 线 +
与 ``_find_host_returning_data`` 区别:资金流获取涉及多命令(K 线 +
逐笔),无法用单个 cmd 复用泛化版;这里以内联 ``_try`` 在每台候选上跑完
整 ``_fetch_fund_flow_records``,返回首台非空结果。全失败返回空列表。
"""
@@ -1536,33 +1562,31 @@ class AsyncTdxClient(AsyncHeartbeatMixin):
return all_recs
async def get_fund_flow(self, market: Market, code: str) -> pd.DataFrame:
"""获取个股当日资金流向分布(基于 L1 逐笔数据统计)。"""
"""获取个股当日资金流向分布(基于 L1 逐笔数据统计)。
返回列含 ``main_net_inflow``(主力净流入,单位元)。
"""
records = await self._collect_transaction_records(
lambda start, page_size: self._execute(
GetTransactionDataCmd(market, code, start, page_size)
),
2000,
)
return _to_df(_classify_fund_flow(records))
return _fund_flow_df_with_net(_to_df(_classify_fund_flow(records)))
async def _fetch_fund_flow_records(
self, market: Market, code: str, start: int, count: int
) -> list[HistoricalFundFlow]:
"""在当前 host 上获取资金流记录(直连 + K 线回退async)。
"""在当前 host 上获取资金流记录(日 K 线取日期 + 逐笔成交重算async)。
优先走 Category 22 直连接口;空则回退为"日 K 线取日期 + 历史逐笔成交重算"
返回空列表代表该 host 既无直连数据也无 K 线数据(或解析失败)
同步版说明:无 Category 22 直连(实测全空,Issue #52);当日 bar 盘中
走当日实时逐笔接口,收盘清算后走历史逐笔接口
返回空列表代表该 host 无 K 线数据(或解析失败)。
"""
try:
direct = await self._execute(GetHistoryFundFlowCmd(market, code, start, count))
except Exception:
direct = []
if direct:
return list(direct)
bars = await self._execute(
GetSecurityBarsCmd(market, code, KlineCategory.DAY, start, count)
)
today = _today_in_shanghai()
results: list[HistoricalFundFlow] = []
for bar in bars:
date = _date_from_bar(bar)
@@ -1571,9 +1595,12 @@ class AsyncTdxClient(AsyncHeartbeatMixin):
async def _fetch_page(
page_start: int, page_size: int, _d: int = date
) -> list[TransactionRecord]:
return await self._execute(
GetHistoryTransactionDataCmd(market, code, _d, page_start, page_size)
)
cmd: BaseCommand[list[TransactionRecord]]
if _d == today:
cmd = GetTransactionDataCmd(market, code, page_start, page_size)
else:
cmd = GetHistoryTransactionDataCmd(market, code, _d, page_start, page_size)
return await self._execute(cmd)
records = await self._collect_transaction_records(_fetch_page, 800)
results.append(_historical_fund_flow_from_records(date, records))
@@ -1584,24 +1611,25 @@ class AsyncTdxClient(AsyncHeartbeatMixin):
) -> pd.DataFrame:
"""获取个股历史日线资金流向序列。
优先走 Category 22 直连接口;若服务器返回空列表,则自动回退为
"日 K 线取日期 + 历史逐笔成交重算资金流"的兼容实现
实现:"日 K 线取日期 + 逐笔成交重算资金流";当日 bar 盘中走当日实时
逐笔接口。返回列含 ``main_net_inflow``(主力净流入,单位元)
空数据故障转移(v1.20.5Issue #41):当前 host 直连与 K 线回退均空时,
空数据故障转移(v1.20.5Issue #41):当前 host K 线数据时,
按延迟顺序逐台实测找首台返回有效数据的服务器。
"""
results = await self._fetch_fund_flow_records(market, code, start, count)
if not results and self._auto_reconnect:
results = await self._fund_flow_failover(market, code, start, count)
return _to_df(results)
return _fund_flow_df_with_net(_to_df(results))
async def _fund_flow_failover(
self, market: Market, code: str, start: int, count: int
) -> list[HistoricalFundFlow]:
"""资金流空数据故障转移(async):逐台实测找首台返回有效数据的服务器。
与 ``_find_host_returning_data`` 区别:资金流获取涉及多命令,无法用单个
cmd 复用泛化版;这里以内联 ``_try`` 在每台候选上跑完整 ``_fetch_fund_flow_records``。
与 ``_find_host_returning_data`` 区别:资金流获取涉及多命令K 线 +
逐笔),无法用单个 cmd 复用泛化版;这里以内联 ``_try`` 在每台候选上
跑完整 ``_fetch_fund_flow_records``。
"""
bad_host = self._host
ranked = await asyncio.to_thread(ping_all, get_known_hosts(), self._port, 5.0)
-79
View File
@@ -1,79 +0,0 @@
"""历史资金流向命令 (Category 22)。"""
import struct
from ..codec.volume import _decode_volume
from ..models.enums import Market
from ..models.stats import HistoricalFundFlow
from .base import BaseCommand
class GetHistoryFundFlowCmd(BaseCommand[list[HistoricalFundFlow]]):
"""获取历史日线资金流向序列。"""
def __init__(self, market: Market, code: str, start: int, count: int) -> None:
self.market = market
self.code = code.encode("utf-8")
self.start = start
self.count = count
def build_request(self) -> bytes:
# Header (12 bytes) + Payload (28 bytes) = 40 bytes
return struct.pack(
"<HIHHHH6sHHHHIIH",
0x010C,
0x01016408,
0x001C,
0x001C,
0x052D,
int(self.market),
self.code,
22,
1,
self.start,
self.count,
0,
0,
0,
)
def parse_response(self, body: bytes) -> list[HistoricalFundFlow]:
# 响应格式:9字节头 + 2字节数量 + 每条记录 36 字节
if len(body) < 11:
return []
(num,) = struct.unpack("<H", body[9:11])
pos = 11
results = []
for _ in range(num):
if len(body) < pos + 36:
break
# 记录格式:4字节日期 + 8个4字节自定义浮点金额
# [0]日期, [1..4]流入(超/大/中/小), [5..8]流出(超/大/中/小)
raw_data = struct.unpack("<IIIIIIIII", body[pos : pos + 36])
raw_date = raw_data[0]
year = raw_date // 10000
month = (raw_date // 100) % 100
day = raw_date % 100
results.append(
HistoricalFundFlow(
year=year,
month=month,
day=day,
super_in=_decode_volume(raw_data[1]),
large_in=_decode_volume(raw_data[2]),
medium_in=_decode_volume(raw_data[3]),
small_in=_decode_volume(raw_data[4]),
super_out=_decode_volume(raw_data[5]),
large_out=_decode_volume(raw_data[6]),
medium_out=_decode_volume(raw_data[7]),
small_out=_decode_volume(raw_data[8]),
)
)
pos += 36
return results
+59 -26
View File
@@ -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 #52asdict 丢弃 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")
-58
View File
@@ -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])