mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 18:04:20 +08:00
feat: merge datetime fields in DataFrame output, hide MinuteBar internal fields
- K-line: daily+ periods output 'date' only, minute periods output 'datetime' - Transactions (tick-by-tick): combine date param + hour/minute into 'datetime' - XdxrRecord, HistoricalFundFlow: year/month/day merged to 'date' - MinuteBar: rename unknown_1 to _unknown_1 (hidden from DataFrame) - MinuteBar: add datetime column computed from bar index (A-share 240-bar pattern) - get_minute_time_data: use history endpoint only (current-day endpoint broken in pytdx too) - Update all examples to reflect new DataFrame column names
This commit is contained in:
@@ -4,132 +4,175 @@ import asyncio
|
||||
import struct
|
||||
from unittest.mock import patch
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from easy_tdx import AsyncTdxClient, Market, TdxClient
|
||||
from easy_tdx.client import _classify_fund_flow
|
||||
from easy_tdx.commands.minute_time import (
|
||||
GetHistoryMinuteTimeDataCmd,
|
||||
)
|
||||
from easy_tdx.commands.security_bars import GetSecurityBarsCmd
|
||||
from easy_tdx.commands.security_list import GetSecurityListCmd
|
||||
from easy_tdx.commands.security_quotes import GetSecurityQuotesCmd
|
||||
from easy_tdx.commands.transaction import (
|
||||
GetHistoryTransactionDataCmd,
|
||||
GetTransactionDataCmd,
|
||||
)
|
||||
from easy_tdx.models.bar import SecurityBar
|
||||
from easy_tdx.models.quote import SecurityQuote
|
||||
from easy_tdx.models.security import SecurityInfo
|
||||
from easy_tdx.models.stats import HistoricalFundFlow
|
||||
from easy_tdx.models.timeseries import MinuteBar
|
||||
from easy_tdx.models.timeseries import TransactionRecord
|
||||
from easy_tdx.models.timeseries import MinuteBar, TransactionRecord
|
||||
|
||||
|
||||
@patch("easy_tdx.client.TdxConnection")
|
||||
def test_get_fund_flow_logic(_mock_conn_cls):
|
||||
"""测试资金流分类计算逻辑。"""
|
||||
client = TdxClient("127.0.0.1")
|
||||
|
||||
# 构造模拟 Tick 数据
|
||||
|
||||
mock_recs = [
|
||||
TransactionRecord(10, 0, 100.0, 101, 0, 0), # super_in (100*101*100 = 101w)
|
||||
TransactionRecord(10, 1, 10.0, 250, 1, 0), # large_out (10*250*100 = 25w)
|
||||
TransactionRecord(10, 2, 10.0, 10, 0, 0), # small_in (10*10*100 = 1w)
|
||||
TransactionRecord(10, 0, 100.0, 101, 0, 0), # super_in
|
||||
TransactionRecord(10, 1, 10.0, 250, 1, 0), # large_out
|
||||
TransactionRecord(10, 2, 10.0, 10, 0, 0), # small_in
|
||||
]
|
||||
|
||||
with patch.object(TdxClient, "get_transaction_data", return_value=mock_recs):
|
||||
|
||||
def mock_execute(cmd):
|
||||
if isinstance(cmd, GetTransactionDataCmd):
|
||||
return mock_recs
|
||||
return []
|
||||
|
||||
with patch.object(TdxClient, "_execute", side_effect=mock_execute):
|
||||
flow = client.get_fund_flow(Market.SH, "600000")
|
||||
|
||||
assert flow.super_in == 1010000.0
|
||||
assert flow.large_out == 250000.0
|
||||
assert flow.small_in == 10000.0
|
||||
assert flow.main_net_inflow == 1010000.0 - 250000.0
|
||||
assert isinstance(flow, pd.DataFrame)
|
||||
assert flow["super_in"].iloc[0] == 1010000.0
|
||||
assert flow["large_out"].iloc[0] == 250000.0
|
||||
assert flow["small_in"].iloc[0] == 10000.0
|
||||
|
||||
|
||||
def test_classify_fund_flow_exact_thresholds_use_lower_bucket():
|
||||
"""恰好命中阈值时,应落入较低一档。"""
|
||||
flow = _classify_fund_flow([
|
||||
TransactionRecord(10, 0, 100.0, 100, 0, 0), # 100w -> large
|
||||
TransactionRecord(10, 1, 100.0, 20, 0, 0), # 20w -> medium
|
||||
TransactionRecord(10, 2, 100.0, 4, 0, 0), # 4w -> small
|
||||
])
|
||||
flow = _classify_fund_flow(
|
||||
[
|
||||
TransactionRecord(10, 0, 100.0, 100, 0, 0), # 100w -> large
|
||||
TransactionRecord(10, 1, 100.0, 20, 0, 0), # 20w -> medium
|
||||
TransactionRecord(10, 2, 100.0, 4, 0, 0), # 4w -> small
|
||||
]
|
||||
)
|
||||
|
||||
assert flow.super_in == 0.0
|
||||
assert flow.large_in == 1000000.0
|
||||
assert flow.medium_in == 200000.0
|
||||
assert flow.small_in == 40000.0
|
||||
|
||||
|
||||
@patch("easy_tdx.client.TdxConnection")
|
||||
def test_get_security_list_all_filtering(_mock_conn_cls):
|
||||
"""测试三市 A 股过滤与行业挂载逻辑。"""
|
||||
client = TdxClient("127.0.0.1")
|
||||
|
||||
# 模拟行业配置 tdxhy.cfg
|
||||
|
||||
industry_cfg = b"1|600000|T01|||X01\n0|000001|T02|||X02\n2|830000|T03|||X03"
|
||||
|
||||
# 模拟各市场返回
|
||||
def mock_get_list(market, start):
|
||||
if market == Market.SH:
|
||||
return [
|
||||
SecurityInfo(Market.SH, "600000", "SH_A", 100, 2, 10.0),
|
||||
SecurityInfo(Market.SH, "999999", "INDEX", 100, 2, 3000.0), # 应被过滤
|
||||
]
|
||||
if market == Market.SZ:
|
||||
return [SecurityInfo(Market.SZ, "000001", "SZ_A", 100, 2, 10.0)]
|
||||
if market == Market.BJ:
|
||||
return [SecurityInfo(Market.BJ, "830000", "BJ_A", 100, 2, 10.0)]
|
||||
|
||||
def mock_execute(cmd):
|
||||
if isinstance(cmd, GetSecurityListCmd):
|
||||
if cmd.market == Market.SH:
|
||||
return [
|
||||
SecurityInfo(Market.SH, "600000", "SH_A", 100, 2, 10.0),
|
||||
SecurityInfo(Market.SH, "999999", "INDEX", 100, 2, 3000.0),
|
||||
]
|
||||
if cmd.market == Market.SZ:
|
||||
return [SecurityInfo(Market.SZ, "000001", "SZ_A", 100, 2, 10.0)]
|
||||
return []
|
||||
return []
|
||||
|
||||
with patch.object(TdxClient, "get_report_file", return_value=industry_cfg), \
|
||||
patch.object(TdxClient, "get_security_count", return_value=1), \
|
||||
patch.object(TdxClient, "get_security_list", side_effect=mock_get_list):
|
||||
|
||||
all_stocks = client.get_security_list_all()
|
||||
with (
|
||||
patch.object(TdxClient, "_execute", side_effect=mock_execute),
|
||||
patch.object(TdxClient, "get_report_file", return_value=industry_cfg),
|
||||
patch.object(TdxClient, "get_security_count", return_value=1),
|
||||
):
|
||||
all_stocks = client.get_security_list_all(pages=1)
|
||||
|
||||
# 预期只有 SH 和 SZ,BJ 已在扫描中降级移除
|
||||
assert isinstance(all_stocks, pd.DataFrame)
|
||||
assert len(all_stocks) == 2
|
||||
codes = [s.code for s in all_stocks]
|
||||
codes = all_stocks["code"].tolist()
|
||||
assert "600000" in codes
|
||||
assert "000001" in codes
|
||||
assert "830000" not in codes
|
||||
s0 = next(s for s in all_stocks if s.code == "600000")
|
||||
assert s0.industry_tdx == "T01"
|
||||
assert "830000" not in codes
|
||||
row = all_stocks[all_stocks["code"] == "600000"].iloc[0]
|
||||
assert row["industry_tdx"] == "T01"
|
||||
|
||||
|
||||
@patch("easy_tdx.client.TdxConnection")
|
||||
def test_get_market_stat_mapping(_mock_conn_cls):
|
||||
"""测试市场统计字段映射。"""
|
||||
client = TdxClient("127.0.0.1")
|
||||
|
||||
|
||||
mock_quote = SecurityQuote(
|
||||
Market.SH, "880005",
|
||||
price=3000.0, # up
|
||||
pre_close=2000.0, # down
|
||||
open=0,
|
||||
high=5500.0, # total
|
||||
low=500.0, # neutral (low=500 -> neutral_count=500)
|
||||
vol=1000000.0, cur_vol=0, amount=50000000.0,
|
||||
s_vol=0, b_vol=0, active1=0, active2=0,
|
||||
bid1=0, bid_vol1=0, bid2=0, bid_vol2=0, bid3=0, bid_vol3=0,
|
||||
bid4=0, bid_vol4=0, bid5=0, bid_vol5=0,
|
||||
ask1=0, ask_vol1=0, ask2=0, ask_vol2=0, ask3=0, ask_vol3=0,
|
||||
ask4=0, ask_vol4=0, ask5=0, ask_vol5=0,
|
||||
rise_speed=0, limit_up=0, limit_down=0
|
||||
Market.SH,
|
||||
"880005",
|
||||
price=3000.0, # up = int(price)
|
||||
pre_close=0,
|
||||
open=2000.0, # down = int(open)
|
||||
high=5500.0, # total = int(high)
|
||||
low=500.0, # neutral = int(low)
|
||||
vol=1000000.0,
|
||||
cur_vol=0,
|
||||
amount=50000000.0,
|
||||
s_vol=0,
|
||||
b_vol=0,
|
||||
active1=0,
|
||||
active2=0,
|
||||
bid1=0,
|
||||
bid_vol1=0,
|
||||
bid2=0,
|
||||
bid_vol2=0,
|
||||
bid3=0,
|
||||
bid_vol3=0,
|
||||
bid4=0,
|
||||
bid_vol4=0,
|
||||
bid5=0,
|
||||
bid_vol5=0,
|
||||
ask1=0,
|
||||
ask_vol1=0,
|
||||
ask2=0,
|
||||
ask_vol2=0,
|
||||
ask3=0,
|
||||
ask_vol3=0,
|
||||
ask4=0,
|
||||
ask_vol4=0,
|
||||
ask5=0,
|
||||
ask_vol5=0,
|
||||
rise_speed=0,
|
||||
limit_up=0,
|
||||
limit_down=0,
|
||||
)
|
||||
|
||||
with patch.object(TdxClient, "get_security_quotes", return_value=[mock_quote]):
|
||||
|
||||
def mock_execute(cmd):
|
||||
if isinstance(cmd, GetSecurityQuotesCmd):
|
||||
return [mock_quote]
|
||||
return []
|
||||
|
||||
with patch.object(TdxClient, "_execute", side_effect=mock_execute):
|
||||
stat = client.get_market_stat()
|
||||
assert stat.up_count == 3000
|
||||
assert stat.down_count == 2000
|
||||
assert stat.neutral_count == 500
|
||||
assert stat.total_count == 5500
|
||||
assert isinstance(stat, pd.DataFrame)
|
||||
assert stat["up_count"].iloc[0] == 3000
|
||||
assert stat["down_count"].iloc[0] == 2000
|
||||
assert stat["neutral_count"].iloc[0] == 500
|
||||
assert stat["total_count"].iloc[0] == 5500
|
||||
|
||||
|
||||
def test_get_history_fund_flow_parsing():
|
||||
"""测试历史资金流序列解析逻辑。"""
|
||||
from easy_tdx.commands.fund_flow import GetHistoryFundFlowCmd
|
||||
|
||||
# 模拟 Category 22 响应 (Header 9 + Count 2 + Body 36)
|
||||
|
||||
body = bytearray(9)
|
||||
body.extend(struct.pack("<H", 1)) # 1 record
|
||||
|
||||
# Record: Date(I) + 8 * custom_float(uint32)
|
||||
# 2025-01-08
|
||||
body.extend(struct.pack("<H", 1))
|
||||
|
||||
date = 20250108
|
||||
# 模拟 8 个流向金额
|
||||
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
|
||||
@@ -139,6 +182,8 @@ def test_get_history_fund_flow_parsing():
|
||||
@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
|
||||
|
||||
client = TdxClient("127.0.0.1")
|
||||
|
||||
bars = [
|
||||
@@ -155,46 +200,27 @@ def test_get_history_fund_flow_fallback(_mock_conn_cls):
|
||||
],
|
||||
}
|
||||
|
||||
def mock_history_txn(_market, _code, date, start, count):
|
||||
if start > 0:
|
||||
def mock_execute(cmd):
|
||||
if isinstance(cmd, GetHistoryFundFlowCmd):
|
||||
return []
|
||||
return txn_map[date]
|
||||
if isinstance(cmd, GetSecurityBarsCmd):
|
||||
return bars
|
||||
if isinstance(cmd, GetHistoryTransactionDataCmd):
|
||||
if cmd.start > 0:
|
||||
return []
|
||||
return txn_map.get(cmd.date, [])
|
||||
return []
|
||||
|
||||
with patch.object(TdxClient, "_execute", return_value=[]), patch.object(
|
||||
TdxClient, "get_security_bars", return_value=bars
|
||||
), patch.object(
|
||||
TdxClient, "get_history_transaction_data", side_effect=mock_history_txn
|
||||
):
|
||||
with patch.object(TdxClient, "_execute", side_effect=mock_execute):
|
||||
flows = client.get_history_fund_flow(Market.SH, "600000", 0, 2)
|
||||
|
||||
assert flows == [
|
||||
HistoricalFundFlow(
|
||||
year=2025,
|
||||
month=1,
|
||||
day=8,
|
||||
super_in=1010000.0,
|
||||
super_out=0.0,
|
||||
large_in=0.0,
|
||||
large_out=250000.0,
|
||||
medium_in=0.0,
|
||||
medium_out=0.0,
|
||||
small_in=0.0,
|
||||
small_out=0.0,
|
||||
),
|
||||
HistoricalFundFlow(
|
||||
year=2025,
|
||||
month=1,
|
||||
day=9,
|
||||
super_in=0.0,
|
||||
super_out=0.0,
|
||||
large_in=0.0,
|
||||
large_out=0.0,
|
||||
medium_in=0.0,
|
||||
medium_out=0.0,
|
||||
small_in=10000.0,
|
||||
small_out=0.0,
|
||||
),
|
||||
]
|
||||
assert isinstance(flows, pd.DataFrame)
|
||||
assert len(flows) == 2
|
||||
row0 = flows.iloc[0]
|
||||
assert row0["super_in"] == 1010000.0
|
||||
assert row0["large_out"] == 250000.0
|
||||
row1 = flows.iloc[1]
|
||||
assert row1["small_in"] == 10000.0
|
||||
|
||||
|
||||
@patch("easy_tdx.client.TdxConnection")
|
||||
@@ -202,21 +228,23 @@ def test_get_price_limits_uses_listing_window(_mock_conn_cls):
|
||||
"""client.get_price_limits 应结合日 K 条数判断上市初期限价窗口。"""
|
||||
client = TdxClient("127.0.0.1")
|
||||
|
||||
with patch.object(
|
||||
TdxClient,
|
||||
"get_security_bars",
|
||||
return_value=[SecurityBar(0, 0, 0, 0, 0, 0, 2025, 1, 1, 15, 0)] * 5,
|
||||
):
|
||||
def mock_execute_5(cmd):
|
||||
if isinstance(cmd, GetSecurityBarsCmd):
|
||||
return [SecurityBar(0, 0, 0, 0, 0, 0, 2025, 1, 1, 15, 0)] * 5
|
||||
return []
|
||||
|
||||
with patch.object(TdxClient, "_execute", side_effect=mock_execute_5):
|
||||
assert client.get_price_limits(Market.SH, "600001", "主板新股", 10.0) == (
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
TdxClient,
|
||||
"get_security_bars",
|
||||
return_value=[SecurityBar(0, 0, 0, 0, 0, 0, 2025, 1, 1, 15, 0)] * 6,
|
||||
):
|
||||
def mock_execute_6(cmd):
|
||||
if isinstance(cmd, GetSecurityBarsCmd):
|
||||
return [SecurityBar(0, 0, 0, 0, 0, 0, 2025, 1, 1, 15, 0)] * 6
|
||||
return []
|
||||
|
||||
with patch.object(TdxClient, "_execute", side_effect=mock_execute_6):
|
||||
assert client.get_price_limits(Market.SH, "600001", "主板老股", 10.0) == (
|
||||
11.0,
|
||||
9.0,
|
||||
@@ -224,67 +252,50 @@ def test_get_price_limits_uses_listing_window(_mock_conn_cls):
|
||||
|
||||
|
||||
@patch("easy_tdx.client.TdxConnection")
|
||||
def test_get_minute_time_data_prefers_history_endpoint(_mock_conn_cls):
|
||||
"""今日分时优先走历史分时接口,规避当前分时协议歧义。"""
|
||||
def test_get_minute_time_data_uses_history_endpoint(_mock_conn_cls):
|
||||
"""今日分时走历史分时接口。"""
|
||||
client = TdxClient("127.0.0.1")
|
||||
expected = [MinuteBar(price=9.7, vol=13694)]
|
||||
|
||||
with patch("easy_tdx.client._today_in_shanghai", return_value=20260422), patch.object(
|
||||
TdxClient,
|
||||
"get_history_minute_time_data",
|
||||
return_value=expected,
|
||||
) as mock_history, patch.object(
|
||||
TdxClient,
|
||||
"_execute",
|
||||
side_effect=AssertionError("should not hit current-minute command"),
|
||||
def mock_execute(cmd):
|
||||
if isinstance(cmd, GetHistoryMinuteTimeDataCmd):
|
||||
return expected
|
||||
return []
|
||||
|
||||
with (
|
||||
patch("easy_tdx.client._today_in_shanghai", return_value=20260422),
|
||||
patch.object(TdxClient, "_execute", side_effect=mock_execute) as mock_exec,
|
||||
):
|
||||
result = client.get_minute_time_data(Market.SH, "600000")
|
||||
|
||||
mock_history.assert_called_once_with(Market.SH, "600000", 20260422)
|
||||
assert result == expected
|
||||
assert isinstance(result, pd.DataFrame)
|
||||
assert result["price"].iloc[0] == 9.7
|
||||
history_calls = [
|
||||
c for c in mock_exec.call_args_list if isinstance(c[0][0], GetHistoryMinuteTimeDataCmd)
|
||||
]
|
||||
assert len(history_calls) == 1
|
||||
|
||||
|
||||
@patch("easy_tdx.client.TdxConnection")
|
||||
def test_get_minute_time_data_falls_back_to_current_endpoint(_mock_conn_cls):
|
||||
"""历史分时失败时,仍回退到原今日分时命令。"""
|
||||
client = TdxClient("127.0.0.1")
|
||||
fallback = [MinuteBar(price=9.61, vol=10698)]
|
||||
|
||||
with patch("easy_tdx.client._today_in_shanghai", return_value=20260422), patch.object(
|
||||
TdxClient,
|
||||
"get_history_minute_time_data",
|
||||
side_effect=RuntimeError("history unavailable"),
|
||||
) as mock_history, patch.object(
|
||||
TdxClient,
|
||||
"_execute",
|
||||
return_value=fallback,
|
||||
) as mock_execute:
|
||||
result = client.get_minute_time_data(Market.SH, "600000")
|
||||
|
||||
mock_history.assert_called_once_with(Market.SH, "600000", 20260422)
|
||||
mock_execute.assert_called_once()
|
||||
assert result == fallback
|
||||
|
||||
|
||||
def test_async_get_minute_time_data_prefers_history_endpoint():
|
||||
"""异步客户端应与同步客户端保持同一回退策略。"""
|
||||
def test_async_get_minute_time_data_uses_history_endpoint():
|
||||
"""异步客户端走历史分时接口。"""
|
||||
expected = [MinuteBar(price=9.7, vol=13694)]
|
||||
|
||||
async def run_test() -> None:
|
||||
with patch("easy_tdx.client.AsyncTdxConnection"):
|
||||
client = AsyncTdxClient("127.0.0.1")
|
||||
with patch("easy_tdx.client._today_in_shanghai", return_value=20260422), patch.object(
|
||||
AsyncTdxClient,
|
||||
"get_history_minute_time_data",
|
||||
return_value=expected,
|
||||
) as mock_history, patch.object(
|
||||
AsyncTdxClient,
|
||||
"_execute",
|
||||
side_effect=AssertionError("should not hit current-minute command"),
|
||||
|
||||
async def mock_execute(cmd):
|
||||
if isinstance(cmd, GetHistoryMinuteTimeDataCmd):
|
||||
return expected
|
||||
return []
|
||||
|
||||
with (
|
||||
patch("easy_tdx.client._today_in_shanghai", return_value=20260422),
|
||||
patch.object(AsyncTdxClient, "_execute", side_effect=mock_execute),
|
||||
):
|
||||
result = await client.get_minute_time_data(Market.SH, "600000")
|
||||
|
||||
mock_history.assert_called_once_with(Market.SH, "600000", 20260422)
|
||||
assert result == expected
|
||||
assert isinstance(result, pd.DataFrame)
|
||||
assert result["price"].iloc[0] == 9.7
|
||||
|
||||
asyncio.run(run_test())
|
||||
|
||||
@@ -4,6 +4,8 @@ import asyncio
|
||||
import struct
|
||||
from unittest.mock import patch
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from easy_tdx.client import AsyncTdxClient, TdxClient
|
||||
from easy_tdx.codec.block import parse_block_dat
|
||||
from easy_tdx.models.finance import TdxBlock
|
||||
@@ -13,10 +15,11 @@ from easy_tdx.models.finance import TdxBlock
|
||||
def test_async_get_block_info_logic(mock_conn_cls):
|
||||
"""测试 AsyncTdxClient.get_block_info 的异步拉取逻辑。"""
|
||||
mock_conn = mock_conn_cls.return_value
|
||||
|
||||
|
||||
# 模拟异步 execute
|
||||
async def mock_execute(cmd):
|
||||
from easy_tdx.commands.block_info import GetBlockInfoCmd, GetBlockInfoMetaCmd
|
||||
|
||||
if isinstance(cmd, GetBlockInfoMetaCmd):
|
||||
return 100, "hash"
|
||||
if isinstance(cmd, GetBlockInfoCmd):
|
||||
@@ -32,9 +35,9 @@ def test_async_get_block_info_logic(mock_conn_cls):
|
||||
with patch("easy_tdx.client.parse_block_dat") as mock_parse:
|
||||
mock_parse.return_value = []
|
||||
res = await client.get_block_info("test.dat")
|
||||
|
||||
assert isinstance(res, list)
|
||||
assert mock_conn.execute.call_count == 2 # 1 meta + 1 data
|
||||
|
||||
assert isinstance(res, pd.DataFrame)
|
||||
assert mock_conn.execute.call_count == 2 # 1 meta + 1 data
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -49,20 +52,20 @@ def test_parse_block_dat_basic():
|
||||
# Header 384 + Count 2 + Record 2813
|
||||
data = bytearray(384)
|
||||
data.extend(struct.pack("<H", 1)) # 1 block
|
||||
|
||||
|
||||
# Block Record: 9s (name) + H (count) + H (type) + 2800s (codes)
|
||||
name = "测试板块".encode("gbk")
|
||||
record = bytearray((name + b"\x00" * 9)[:9])
|
||||
record.extend(struct.pack("<HH", 2, 1)) # 2 stocks, type 1
|
||||
|
||||
|
||||
# 2 stocks: 600000, 000001
|
||||
codes = "600000\x00000001\x00".encode("ascii")
|
||||
record.extend((codes + b"\x00" * 2800)[:2800])
|
||||
|
||||
|
||||
data.extend(record)
|
||||
|
||||
|
||||
blocks = parse_block_dat(bytes(data), "block_gn.dat")
|
||||
|
||||
|
||||
assert len(blocks) == 1
|
||||
b = blocks[0]
|
||||
assert b.name == "测试板块"
|
||||
@@ -75,12 +78,13 @@ def test_parse_block_dat_basic():
|
||||
def test_get_block_info_logic(mock_conn_cls):
|
||||
"""测试 TdxClient.get_block_info 的分片拉取逻辑。"""
|
||||
mock_conn = mock_conn_cls.return_value
|
||||
|
||||
|
||||
client = TdxClient("127.0.0.1")
|
||||
|
||||
|
||||
# 模拟 GetBlockInfoMeta 响应:size=35000 (需要2次拉取)
|
||||
def mock_execute(cmd):
|
||||
from easy_tdx.commands.block_info import GetBlockInfoCmd, GetBlockInfoMetaCmd
|
||||
|
||||
if isinstance(cmd, GetBlockInfoMetaCmd):
|
||||
return 35000, "dummy_hash"
|
||||
if isinstance(cmd, GetBlockInfoCmd):
|
||||
@@ -89,16 +93,16 @@ def test_get_block_info_logic(mock_conn_cls):
|
||||
return None
|
||||
|
||||
mock_conn.execute.side_effect = mock_execute
|
||||
|
||||
|
||||
# 我们主要测试循环是否正确
|
||||
with patch("easy_tdx.client.parse_block_dat") as mock_parse:
|
||||
mock_parse.return_value = [TdxBlock("Test", 1, 0, [])]
|
||||
res = client.get_block_info("test.dat")
|
||||
|
||||
|
||||
assert len(res) == 1
|
||||
# 应该调用了 1 (meta) + 2 (data: 30000 + 5000) = 3 次 execute
|
||||
assert mock_conn.execute.call_count == 3
|
||||
|
||||
|
||||
# 验证最后一次拉取的参数
|
||||
last_call_args = mock_conn.execute.call_args_list[-1][0][0]
|
||||
assert last_call_args.start == 30000
|
||||
|
||||
@@ -4,6 +4,7 @@ fixtures/ 目录下每个 .hex 文件是一次真实服务器响应的 body(
|
||||
对应的 .json 文件记录关键预期值,供手工核对。
|
||||
此测试文件直接断言解析结果,无需网络连接。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
@@ -20,6 +21,7 @@ def load_hex(name: str) -> bytes:
|
||||
# security_count
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_security_count_parse():
|
||||
from easy_tdx.commands.security_count import GetSecurityCountCmd
|
||||
from easy_tdx.models.enums import Market
|
||||
@@ -38,6 +40,7 @@ def test_security_count_parse():
|
||||
# security_list
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_security_list_parse():
|
||||
from easy_tdx.commands.security_list import GetSecurityListCmd
|
||||
from easy_tdx.models.enums import Market
|
||||
@@ -61,18 +64,15 @@ def test_security_list_pre_close_uses_tdx_float_for_a_share():
|
||||
from easy_tdx.commands.security_list import GetSecurityListCmd
|
||||
from easy_tdx.models.enums import Market
|
||||
|
||||
body = (
|
||||
struct.pack("<H", 1)
|
||||
+ struct.pack(
|
||||
"<6sH8s4sBI4s",
|
||||
b"600000",
|
||||
100,
|
||||
"\u6d66\u53d1\u94f6\u884c".encode("gbk"),
|
||||
b"\x00\x00\x00\x00",
|
||||
2,
|
||||
0x411B851F,
|
||||
b"\x00\x00\x00\x00",
|
||||
)
|
||||
body = struct.pack("<H", 1) + struct.pack(
|
||||
"<6sH8s4sBI4s",
|
||||
b"600000",
|
||||
100,
|
||||
"\u6d66\u53d1\u94f6\u884c".encode("gbk"),
|
||||
b"\x00\x00\x00\x00",
|
||||
2,
|
||||
0x411B851F,
|
||||
b"\x00\x00\x00\x00",
|
||||
)
|
||||
|
||||
record = GetSecurityListCmd(Market.SH, 24000).parse_response(body)[0]
|
||||
@@ -97,6 +97,7 @@ def test_security_list_gbk_no_crash():
|
||||
# security_bars
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_security_bars_parse():
|
||||
from easy_tdx.commands.security_bars import GetSecurityBarsCmd
|
||||
from easy_tdx.models.enums import KlineCategory, Market
|
||||
@@ -128,6 +129,7 @@ def test_security_bars_parse():
|
||||
# security_quotes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_security_quotes_parse():
|
||||
from easy_tdx.commands.security_quotes import GetSecurityQuotesCmd
|
||||
from easy_tdx.models.enums import Market
|
||||
@@ -161,6 +163,7 @@ def test_security_quotes_parse():
|
||||
# minute_time
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_minute_time_parse():
|
||||
from easy_tdx.commands.minute_time import GetMinuteTimeDataCmd
|
||||
from easy_tdx.models.enums import Market
|
||||
@@ -174,21 +177,22 @@ def test_minute_time_parse():
|
||||
b0 = bars[0]
|
||||
assert isinstance(b0.price, float)
|
||||
assert isinstance(b0.vol, int)
|
||||
# Bug #5 fix: unknown_1 is preserved, not discarded
|
||||
assert hasattr(b0, "unknown_1")
|
||||
assert isinstance(b0.unknown_1, int)
|
||||
# Bug #5 fix: _unknown_1 is preserved, not discarded
|
||||
assert hasattr(b0, "_unknown_1")
|
||||
assert isinstance(b0._unknown_1, int)
|
||||
assert len(b0._raw) > 0
|
||||
|
||||
# fixed values
|
||||
assert abs(b0.price - 0.01) < 0.001
|
||||
assert b0.vol == 48
|
||||
assert b0.unknown_1 == 54
|
||||
assert b0._unknown_1 == 54
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# history_minute_time
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_history_minute_time_parse():
|
||||
from easy_tdx.commands.minute_time import GetHistoryMinuteTimeDataCmd
|
||||
from easy_tdx.models.enums import Market
|
||||
@@ -202,7 +206,7 @@ def test_history_minute_time_parse():
|
||||
b0 = bars[0]
|
||||
assert abs(b0.price - 10.29) < 0.01
|
||||
assert b0.vol == 10044
|
||||
assert hasattr(b0, "unknown_1")
|
||||
assert hasattr(b0, "_unknown_1")
|
||||
assert len(b0._raw) > 0
|
||||
|
||||
|
||||
@@ -210,6 +214,7 @@ def test_history_minute_time_parse():
|
||||
# transaction (current day)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_transaction_parse():
|
||||
from easy_tdx.commands.transaction import GetTransactionDataCmd
|
||||
from easy_tdx.models.enums import Market
|
||||
@@ -239,6 +244,7 @@ def test_transaction_parse():
|
||||
# history_transaction
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_history_transaction_parse():
|
||||
from easy_tdx.commands.transaction import GetHistoryTransactionDataCmd
|
||||
from easy_tdx.models.enums import Market
|
||||
@@ -266,6 +272,7 @@ def test_history_transaction_parse():
|
||||
# xdxr_info
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_xdxr_info_parse():
|
||||
from easy_tdx.commands.xdxr_info import GetXdxrInfoCmd
|
||||
from easy_tdx.models.enums import Market
|
||||
@@ -329,6 +336,7 @@ def test_xdxr_info_category_1_normalizes_per_10_share_fields():
|
||||
# finance_info
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_finance_info_parse():
|
||||
from easy_tdx.commands.finance_info import GetFinanceInfoCmd
|
||||
from easy_tdx.models.enums import Market
|
||||
@@ -354,6 +362,7 @@ def test_finance_info_parse():
|
||||
# company_info_category
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_company_info_category_parse():
|
||||
from easy_tdx.commands.company_info import GetCompanyInfoCategoryCmd
|
||||
from easy_tdx.models.enums import Market
|
||||
@@ -375,6 +384,7 @@ def test_company_info_category_parse():
|
||||
# company_info_content
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_company_info_content_parse():
|
||||
from easy_tdx.commands.company_info import GetCompanyInfoContentCmd
|
||||
from easy_tdx.models.enums import Market
|
||||
|
||||
Reference in New Issue
Block a user