mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 15:44:18 +08:00
Fix protocol regressions and clarify experimental APIs
This commit is contained in:
@@ -96,7 +96,7 @@ client = AsyncTdxClient.from_best_host(ping_timeout=5.0)
|
|||||||
|------|------|
|
|------|------|
|
||||||
| `get_security_count(market)` | 市场证券总数 |
|
| `get_security_count(market)` | 市场证券总数 |
|
||||||
| `get_security_list(market, start)` | 证券列表(每页 ~1000 条) |
|
| `get_security_list(market, start)` | 证券列表(每页 ~1000 条) |
|
||||||
| `get_security_list_all()` | 全市场 A 股列表(自动挂载行业信息) |
|
| `get_security_list_all()` | 沪深 A 股列表(自动挂载行业信息;BJ 暂未纳入) |
|
||||||
| `get_market_stat()` | 全市场 A 股涨跌统计(家数、成交额) |
|
| `get_market_stat()` | 全市场 A 股涨跌统计(家数、成交额) |
|
||||||
| `get_security_quotes([(market, code), ...])` | 批量实时五档行情(最多 80 只/次) |
|
| `get_security_quotes([(market, code), ...])` | 批量实时五档行情(最多 80 只/次) |
|
||||||
| `get_security_bars(market, code, category, start, count=800)` | K 线(股票) |
|
| `get_security_bars(market, code, category, start, count=800)` | K 线(股票) |
|
||||||
@@ -106,7 +106,7 @@ client = AsyncTdxClient.from_best_host(ping_timeout=5.0)
|
|||||||
| `get_transaction_data(market, code, start, count=800)` | 当日逐笔成交(分页) |
|
| `get_transaction_data(market, code, start, count=800)` | 当日逐笔成交(分页) |
|
||||||
| `get_history_transaction_data(market, code, date, start, count=800)` | 历史逐笔成交 |
|
| `get_history_transaction_data(market, code, date, start, count=800)` | 历史逐笔成交 |
|
||||||
| `get_fund_flow(market, code)` | 当日资金流向统计(超大/大/中/小单) |
|
| `get_fund_flow(market, code)` | 当日资金流向统计(超大/大/中/小单) |
|
||||||
| `get_history_fund_flow(market, code, start, count)` | 历史日线资金流向序列(Category 22) |
|
| `get_history_fund_flow(market, code, start, count)` | 历史日线资金流向序列(Category 22,实验性) |
|
||||||
| `get_xdxr_info(market, code)` | 除权除息历史 |
|
| `get_xdxr_info(market, code)` | 除权除息历史 |
|
||||||
| `get_finance_info(market, code)` | 最新财务数据 |
|
| `get_finance_info(market, code)` | 最新财务数据 |
|
||||||
| `get_company_info_category(market, code)` | 公司信息文件目录 |
|
| `get_company_info_category(market, code)` | 公司信息文件目录 |
|
||||||
@@ -148,6 +148,9 @@ unknown_2..unknown_3 unknown_5..unknown_8
|
|||||||
_raw
|
_raw
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`limit_up` / `limit_down` 当前不再直接由协议字段映射,默认保留为 `None`;
|
||||||
|
建议通过 `xmtdx.codec.price_rules.compute_price_limits(...)` 按业务规则计算。
|
||||||
|
|
||||||
### MinuteBar(分时)
|
### MinuteBar(分时)
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -225,7 +228,7 @@ main_net_inflow
|
|||||||
| 4 | `transaction` | 最后一个字段被 `_` 丢弃 | 保留为 `unknown_last` |
|
| 4 | `transaction` | 最后一个字段被 `_` 丢弃 | 保留为 `unknown_last` |
|
||||||
| 5 | `minute_time` | `reversed1` 字段被丢弃 | 保留为 `unknown_1` |
|
| 5 | `minute_time` | `reversed1` 字段被丢弃 | 保留为 `unknown_1` |
|
||||||
| 6 | `xdxr_info` | 股本字段用 `float(uint32)` 直解,差约 374 倍 | 改用 `_decode_volume`(通达信自定义浮点),单位万股,与 `FinanceInfo` 完全吻合 |
|
| 6 | `xdxr_info` | 股本字段用 `float(uint32)` 直解,差约 374 倍 | 改用 `_decode_volume`(通达信自定义浮点),单位万股,与 `FinanceInfo` 完全吻合 |
|
||||||
| 7 | `security_quotes` | 涨停/跌停价映射错误或缺失 | 解析 `unknown_2/3` 为绝对价格字段 `limit_up/down` |
|
| 7 | `security_quotes` | 涨停/跌停价映射错误或缺失 | 停止使用不可信协议位,改由业务规则计算 |
|
||||||
|
|
||||||
## 架构
|
## 架构
|
||||||
|
|
||||||
|
|||||||
+65
-147
@@ -1,188 +1,106 @@
|
|||||||
"""未知字段探测脚本:通过批量拉取多只股票数据,尝试推断各 unknown_N 字段的含义。
|
"""探测未知字段含义的辅助脚本。"""
|
||||||
|
|
||||||
用法:
|
|
||||||
cd /home/m/xmtdx
|
|
||||||
python3 scripts/probe_unknowns.py
|
|
||||||
|
|
||||||
输出:
|
|
||||||
1. MinuteBar.unknown_1 vs 分钟均价(累计成交额 / 累计成交量)
|
|
||||||
2. SecurityQuote.unknown_2/3/5/6/7/8 与已知行情指标的相关关系
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import pathlib
|
|
||||||
|
|
||||||
sys.path.insert(0, str(pathlib.Path(__file__).parent.parent / "src"))
|
from xmtdx import Market, TdxClient
|
||||||
|
|
||||||
from xmtdx import TdxClient, Market, KlineCategory
|
|
||||||
|
|
||||||
HOST = "180.153.18.170"
|
|
||||||
|
|
||||||
# 沪深各取若干活跃股票
|
|
||||||
SH_CODES = ["600000", "600036", "601318", "600519", "601628"]
|
|
||||||
SZ_CODES = ["000001", "000002", "000858", "002415", "300750"]
|
|
||||||
|
|
||||||
SEP = "-" * 72
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
def probe_minute_averages(client, market, code):
|
||||||
# Part 1: MinuteBar.unknown_1 — 是否为分钟均价?
|
"""探测分时数据中 unknown_1 的含义(疑似均价)。"""
|
||||||
# ---------------------------------------------------------------------------
|
print(f"\nProbing {code} Minute Time unknown_1:")
|
||||||
|
bars = client.get_minute_time_data(market, code)
|
||||||
|
if not bars:
|
||||||
|
return
|
||||||
|
|
||||||
def probe_minute_unknown_1(c: TdxClient) -> None:
|
|
||||||
print(SEP)
|
|
||||||
print("Part 1: MinuteBar.unknown_1 vs 分钟均价 (历史某日)")
|
|
||||||
print(SEP)
|
|
||||||
|
|
||||||
# 使用历史分时,数据确定(不随时间变化)
|
|
||||||
DATE = 20250108
|
|
||||||
code, market = "600000", Market.SH
|
|
||||||
|
|
||||||
bars = c.get_history_minute_time_data(market, code, DATE)
|
|
||||||
print(f" {market.name} {code} 日期={DATE} 共 {len(bars)} 条分时\n")
|
|
||||||
|
|
||||||
# 同时拉取当日日线 K 作为参考(含 amount/vol 可算均价)
|
|
||||||
# 分时数据无直接成交额,需要用 price × vol 近似
|
# 分时数据无直接成交额,需要用 price × vol 近似
|
||||||
# 若 unknown_1 == round(price × 100) 则为原始价格单位均价
|
# 若 unknown_1 == round(price × 100) 则为原始价格单位均价
|
||||||
print(f" {'分钟':>6} {'price':>8} {'vol':>8} {'unknown_1':>12} {'price*100':>10} {'diff':>8}")
|
print(f" {'分钟':>6} {'price':>8} {'vol':>8} "
|
||||||
print(f" {'':-<6} {'':-<8} {'':-<8} {'':-<12} {'':-<10} {'':-<8}")
|
f"{'unknown_1':>12} {'price*100':>10} {'diff':>8}")
|
||||||
|
print(f" {'':-<6} {'':-<8} {'':-<8} "
|
||||||
|
f"{'':-<12} {'':-<10} {'':-<8}")
|
||||||
|
|
||||||
exact_match = 0
|
all_exact = 0
|
||||||
close_match = 0
|
all_close = 0
|
||||||
|
for i, b in enumerate(bars):
|
||||||
for i, b in enumerate(bars[:30]): # 只打印前30条
|
price_x100 = int(round(b.price * 100))
|
||||||
price_x100 = round(b.price * 100)
|
|
||||||
diff = b.unknown_1 - price_x100
|
diff = b.unknown_1 - price_x100
|
||||||
exact = b.unknown_1 == price_x100
|
exact = (diff == 0)
|
||||||
close = abs(diff) <= 2
|
close = (abs(diff) <= 2)
|
||||||
|
|
||||||
if exact:
|
if exact:
|
||||||
exact_match += 1
|
all_exact += 1
|
||||||
if close:
|
if close:
|
||||||
close_match += 1
|
all_close += 1
|
||||||
|
|
||||||
flag = " <<< exact" if exact else (" ≈" if close else "")
|
flag = " <<< exact" if exact else (" ≈" if close else "")
|
||||||
print(f" {i+1:>6} {b.price:>8.2f} {b.vol:>8} {b.unknown_1:>12} {price_x100:>10} {diff:>+8}{flag}")
|
print(f" {i+1:>6} {b.price:>8.2f} {b.vol:>8} "
|
||||||
|
f"{b.unknown_1:>12} {price_x100:>10} {diff:>+8}{flag}")
|
||||||
|
|
||||||
# Count across all bars
|
# Count across all bars
|
||||||
all_exact = sum(1 for b in bars if b.unknown_1 == round(b.price * 100))
|
|
||||||
all_close = sum(1 for b in bars if abs(b.unknown_1 - round(b.price * 100)) <= 2)
|
|
||||||
|
|
||||||
print(f"\n 全部 {len(bars)} 条:")
|
print(f"\n 全部 {len(bars)} 条:")
|
||||||
print(f" unknown_1 == price*100 (精确): {all_exact}/{len(bars)} ({100*all_exact/len(bars):.1f}%)")
|
print(f" unknown_1 == price*100 (精确): "
|
||||||
print(f" unknown_1 ≈ price*100 (±2): {all_close}/{len(bars)} ({100*all_close/len(bars):.1f}%)")
|
f"{all_exact}/{len(bars)} ({100*all_exact/len(bars):.1f}%)")
|
||||||
|
print(f" unknown_1 ≈ price*100 (±2): "
|
||||||
|
f"{all_close}/{len(bars)} ({100*all_close/len(bars):.1f}%)")
|
||||||
|
|
||||||
# Try another hypothesis: unknown_1 is a cumulative average price (均价)
|
# Try another hypothesis: unknown_1 is a cumulative average price (均价)
|
||||||
# Compute running avg: sum(price*vol)/sum(vol)
|
total_vol = 0
|
||||||
print(f"\n 另一假设:unknown_1 = 当日累计均价×100")
|
total_amount = 0.0
|
||||||
cum_pv = 0.0
|
|
||||||
cum_v = 0
|
|
||||||
correct_avg = 0
|
correct_avg = 0
|
||||||
for b in bars:
|
for b in bars:
|
||||||
cum_pv += b.price * b.vol
|
total_vol += b.vol
|
||||||
cum_v += b.vol
|
total_amount += b.price * b.vol
|
||||||
if cum_v > 0:
|
if total_vol > 0:
|
||||||
avg = cum_pv / cum_v
|
avg_x100 = int(round((total_amount / total_vol) * 100))
|
||||||
expected = round(avg * 100)
|
if abs(b.unknown_1 - avg_x100) <= 2:
|
||||||
if abs(b.unknown_1 - expected) <= 2:
|
|
||||||
correct_avg += 1
|
correct_avg += 1
|
||||||
|
|
||||||
print(f" unknown_1 ≈ 累计均价×100 (±2): {correct_avg}/{len(bars)} ({100*correct_avg/len(bars):.1f}%)")
|
print(f" unknown_1 ≈ 累计均价×100 (±2): "
|
||||||
|
f"{correct_avg}/{len(bars)} ({100*correct_avg/len(bars):.1f}%)")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
def probe_quote_limits(client, market, code):
|
||||||
# Part 2: SecurityQuote.unknown_N fields
|
"""探测实时行情中 unknown_5/6 的含义(疑似涨跌停)。"""
|
||||||
# ---------------------------------------------------------------------------
|
print(f"\nProbing {code} Quote unknown_5/6:")
|
||||||
|
quotes = client.get_security_quotes([(market, code)])
|
||||||
def probe_quote_unknowns(c: TdxClient) -> None:
|
if not quotes:
|
||||||
print(f"\n{SEP}")
|
return
|
||||||
print("Part 2: SecurityQuote.unknown_2/3/5/6/7/8 — 与已知字段的关系")
|
|
||||||
print(SEP)
|
|
||||||
|
|
||||||
pairs = [(Market.SH, code) for code in SH_CODES] + [(Market.SZ, code) for code in SZ_CODES]
|
|
||||||
quotes = c.get_security_quotes(pairs)
|
|
||||||
|
|
||||||
print(f" {'market':>6} {'code':>8} {'pre_close':>10} {'price':>8} "
|
|
||||||
f"{'u2':>6} {'u3':>8} {'u5':>6} {'u6':>6} {'u7':>6} {'u8':>6} {'rise_spd':>10}")
|
|
||||||
print(f" {'':-<6} {'':-<8} {'':-<10} {'':-<8} "
|
|
||||||
f"{'':-<6} {'':-<8} {'':-<6} {'':-<6} {'':-<6} {'':-<6} {'':-<10}")
|
|
||||||
|
|
||||||
for q in quotes:
|
for q in quotes:
|
||||||
pct = (q.price - q.pre_close) / q.pre_close * 100 if q.pre_close else 0
|
|
||||||
print(
|
print(
|
||||||
f" {q.market.name:>6} {q.code:>8} {q.pre_close:>10.2f} {q.price:>8.2f} "
|
f" {q.market.name:>6} {q.code:>8} {q.pre_close:>10.2f} {q.price:>8.2f} "
|
||||||
f"{q.unknown_2:>6} {q.unknown_3:>8} {q.unknown_5:>6} "
|
f"u5:{q.unknown_5:>6} u6:{q.unknown_6:>6}"
|
||||||
f"{q.unknown_6:>6} {q.unknown_7:>6} {q.unknown_8:>6} {q.rise_speed:>10.4f}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"\n 注:rise_speed = reversed_bytes9/100(已确认 = 涨速)")
|
|
||||||
|
|
||||||
# Hypothesis: unknown_3 might relate to 涨停/跌停 price
|
|
||||||
# 涨停 = pre_close * 1.10 (rounded to 2 decimal)
|
|
||||||
print(f"\n 假设 unknown_3 = 涨停价×100:")
|
|
||||||
print(f" {'code':>8} {'涨停价×100 预期':>16} {'unknown_3':>10} {'diff':>6}")
|
|
||||||
for q in quotes:
|
|
||||||
if q.pre_close > 0:
|
|
||||||
limit_up = round(q.pre_close * 1.10 * 100)
|
|
||||||
diff = q.unknown_3 - limit_up
|
|
||||||
print(f" {q.code:>8} {limit_up:>16} {q.unknown_3:>10} {diff:>+6}")
|
|
||||||
|
|
||||||
print(f"\n 假设 unknown_3 = 跌停价×100:")
|
|
||||||
print(f" {'code':>8} {'跌停价×100 预期':>16} {'unknown_3':>10} {'diff':>6}")
|
|
||||||
for q in quotes:
|
|
||||||
if q.pre_close > 0:
|
|
||||||
limit_dn = round(q.pre_close * 0.90 * 100)
|
|
||||||
diff = q.unknown_3 - limit_dn
|
|
||||||
print(f" {q.code:>8} {limit_dn:>16} {q.unknown_3:>10} {diff:>+6}")
|
|
||||||
|
|
||||||
# unknown_2: often -1 or small value — check if it's 换手率×10000 or similar
|
|
||||||
print(f"\n unknown_2 raw values: {[q.unknown_2 for q in quotes]}")
|
|
||||||
print(f" unknown_5 raw values: {[q.unknown_5 for q in quotes]}")
|
|
||||||
print(f" unknown_6 raw values: {[q.unknown_6 for q in quotes]}")
|
|
||||||
print(f" unknown_7 raw values: {[q.unknown_7 for q in quotes]}")
|
|
||||||
print(f" unknown_8 raw values: {[q.unknown_8 for q in quotes]}")
|
|
||||||
|
|
||||||
# Print raw bytes for manual inspection
|
|
||||||
print(f"\n 原始字节(前20字节 hex):")
|
|
||||||
for q in quotes:
|
|
||||||
print(f" {q.code}: {q._raw[:20].hex()}")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Part 3: TransactionRecord.unknown_last — 是否为秒数?
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def probe_transaction_unknown_last(c: TdxClient) -> None:
|
|
||||||
print(f"\n{SEP}")
|
|
||||||
print("Part 3: TransactionRecord.unknown_last — 是否为秒或序号?")
|
|
||||||
print(SEP)
|
|
||||||
|
|
||||||
recs = c.get_history_transaction_data(Market.SH, "600000", 20250108, 0, 30)
|
|
||||||
print(f" {'序号':>4} {'时间':>6} {'price':>8} {'vol':>6} {'buy':>4} {'unknown_last':>14}")
|
|
||||||
print(f" {'':-<4} {'':-<6} {'':-<8} {'':-<6} {'':-<4} {'':-<14}")
|
|
||||||
|
|
||||||
|
def probe_fund_flow_raw(client, market, code):
|
||||||
|
"""探测资金流原始数据分布。"""
|
||||||
|
print(f"\nProbing {code} Transaction raw unknown_last:")
|
||||||
|
# 直接用 get_transaction_data 获取原始记录
|
||||||
|
recs = client.get_transaction_data(market, code, 0, 50)
|
||||||
|
print(f" {'idx':>4} {'time':>5} {'price':>8} {'vol':>6} {'b/s':>4} {'unknown_last':>14}")
|
||||||
for i, r in enumerate(recs):
|
for i, r in enumerate(recs):
|
||||||
print(f" {i+1:>4} {r.hour:02d}:{r.minute:02d} {r.price:>8.2f} {r.vol:>6} {r.buyorsell:>4} {r.unknown_last:>14}")
|
print(f" {i+1:>4} {r.hour:02d}:{r.minute:02d} {r.price:>8.2f} "
|
||||||
|
f"{r.vol:>6} {r.buyorsell:>4} {r.unknown_last:>14}")
|
||||||
|
|
||||||
unique = len({r.unknown_last for r in recs})
|
unique = len({r.unknown_last for r in recs})
|
||||||
print(f"\n unknown_last 唯一值数量: {unique}/{len(recs)}")
|
print(f"\n Unique unknown_last in 50 recs: {unique}")
|
||||||
print(f" 值分布: {sorted({r.unknown_last for r in recs})}")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
def main():
|
||||||
# main
|
host = "180.153.18.170"
|
||||||
# ---------------------------------------------------------------------------
|
if len(sys.argv) > 1:
|
||||||
|
host = sys.argv[1]
|
||||||
|
|
||||||
def main() -> None:
|
with TdxClient(host) as client:
|
||||||
print(f"连接 {HOST}:7709 ...")
|
# 1. 均价探测
|
||||||
with TdxClient(HOST) as c:
|
probe_minute_averages(client, Market.SH, "600000")
|
||||||
probe_minute_unknown_1(c)
|
probe_minute_averages(client, Market.SZ, "000001")
|
||||||
probe_quote_unknowns(c)
|
|
||||||
probe_transaction_unknown_last(c)
|
|
||||||
|
|
||||||
print(f"\n{SEP}")
|
# 2. 涨跌停探测
|
||||||
print("探测完成。根据以上输出可判断各字段含义,更新 models/ 文档注释。")
|
probe_quote_limits(client, Market.SH, "600000")
|
||||||
|
probe_quote_limits(client, Market.SZ, "000001")
|
||||||
|
|
||||||
|
# 3. 资金流探测
|
||||||
|
probe_fund_flow_raw(client, Market.SH, "600000")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
"""实测验证脚本 (2026-04-15 修复验证)。"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from xmtdx import Market, TdxClient
|
||||||
|
from xmtdx.codec.price_rules import compute_price_limits
|
||||||
|
from xmtdx.models.enums import KlineCategory
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
hosts = ["115.238.56.198", "180.153.18.170", "124.71.187.122"]
|
||||||
|
host = hosts[0]
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
host = sys.argv[1]
|
||||||
|
|
||||||
|
print(f"Connecting to {host}...")
|
||||||
|
success = True
|
||||||
|
|
||||||
|
with TdxClient(host) as client:
|
||||||
|
# 1. 验证 K 线请求已恢复
|
||||||
|
print("\n[1] Security/Index Bars:")
|
||||||
|
try:
|
||||||
|
bars = client.get_security_bars(Market.SH, "600000", KlineCategory.DAY, 0, 3)
|
||||||
|
ibars = client.get_index_bars(Market.SH, "999999", KlineCategory.DAY, 0, 3)
|
||||||
|
print(f" 600000 bars: {len(bars)}")
|
||||||
|
print(f" 999999 index bars: {len(ibars)}")
|
||||||
|
if not bars or not ibars:
|
||||||
|
print(" Result: FAIL (Bars request returned empty)")
|
||||||
|
success = False
|
||||||
|
else:
|
||||||
|
print(" Result: SUCCESS")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error: {e}")
|
||||||
|
success = False
|
||||||
|
|
||||||
|
# 2. 验证 get_market_stat (880005)
|
||||||
|
print("\n[2] Market Stat (880005):")
|
||||||
|
try:
|
||||||
|
stat = client.get_market_stat()
|
||||||
|
print(
|
||||||
|
f" Up: {stat.up_count}, Down: {stat.down_count}, "
|
||||||
|
f"Neutral: {stat.neutral_count}, Suspended: {stat.suspended_count}, "
|
||||||
|
f"Total: {stat.total_count}"
|
||||||
|
)
|
||||||
|
stat_sum = (
|
||||||
|
stat.up_count
|
||||||
|
+ stat.down_count
|
||||||
|
+ stat.neutral_count
|
||||||
|
+ stat.suspended_count
|
||||||
|
)
|
||||||
|
print(f" Sum (U+D+N+S): {stat_sum}")
|
||||||
|
if stat_sum == stat.total_count:
|
||||||
|
print(" Result: SUCCESS (residual-balanced total)")
|
||||||
|
else:
|
||||||
|
print(" Result: FAIL (Sum != Total)")
|
||||||
|
success = False
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error: {e}")
|
||||||
|
success = False
|
||||||
|
|
||||||
|
# 3. 验证价格规则引擎
|
||||||
|
print("\n[3] Price Limits (Rule Engine):")
|
||||||
|
samples = [
|
||||||
|
("600000", Market.SH, "浦发银行"),
|
||||||
|
("300750", Market.SZ, "宁德时代"),
|
||||||
|
("688981", Market.SH, "中芯国际"),
|
||||||
|
("999999", Market.SH, "上证指数"),
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
quotes = client.get_security_quotes([(market, code) for code, market, _name in samples])
|
||||||
|
for q, (_code, _market, name) in zip(quotes, samples, strict=True):
|
||||||
|
lu, ld = compute_price_limits(q.market, q.code, name, q.pre_close)
|
||||||
|
print(
|
||||||
|
f" {q.code}: Price={q.price:.2f}, PreClose={q.pre_close:.2f}, "
|
||||||
|
f"LimitUp={lu}, LimitDown={ld}"
|
||||||
|
)
|
||||||
|
if q.code == "999999":
|
||||||
|
if lu is not None or ld is not None:
|
||||||
|
print(" Result: FAIL (Index should not have price limits)")
|
||||||
|
success = False
|
||||||
|
elif lu is None or ld is None:
|
||||||
|
print(f" Result: FAIL (Limit calculation returned None for {q.code})")
|
||||||
|
success = False
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error: {e}")
|
||||||
|
success = False
|
||||||
|
|
||||||
|
# 4. 验证 get_history_fund_flow (Category 22)
|
||||||
|
print("\n[4] History Fund Flow (Category 22, experimental):")
|
||||||
|
try:
|
||||||
|
h_flow = client.get_history_fund_flow(Market.SH, "600000", 0, 1)
|
||||||
|
if h_flow:
|
||||||
|
f = h_flow[0]
|
||||||
|
print(f" Date: {f.year}-{f.month}-{f.day}, SuperIn: {f.super_in:.2f}")
|
||||||
|
print(" Result: SUCCESS")
|
||||||
|
else:
|
||||||
|
print(" Result: INFO (No data returned; interface remains experimental)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error: {e} (Experimental interface; not counted as hard failure)")
|
||||||
|
|
||||||
|
# 5. 验证 get_fund_flow 分页
|
||||||
|
print("\n[5] Fund Flow Pagination (600000):")
|
||||||
|
try:
|
||||||
|
flow = client.get_fund_flow(Market.SH, "600000")
|
||||||
|
total_in = flow.super_in + flow.large_in + flow.medium_in + flow.small_in
|
||||||
|
total_out = flow.super_out + flow.large_out + flow.medium_out + flow.small_out
|
||||||
|
print(f" 600000 Classified Total: {total_in + total_out:.2f}")
|
||||||
|
# 获取实时成交额对比
|
||||||
|
q = client.get_security_quotes([(Market.SH, "600000")])[0]
|
||||||
|
print(f" 600000 Real Amount: {q.amount:.2f}")
|
||||||
|
coverage = (total_in + total_out) / q.amount if q.amount > 0 else 0
|
||||||
|
print(f" Coverage: {coverage * 100:.1f}%")
|
||||||
|
if coverage < 0.90:
|
||||||
|
print(" Result: FAIL (Coverage too low)")
|
||||||
|
success = False
|
||||||
|
else:
|
||||||
|
print(" Result: SUCCESS")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error: {e}")
|
||||||
|
success = False
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+132
-36
@@ -4,21 +4,21 @@ import asyncio
|
|||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
|
from .codec.block import parse_block_dat
|
||||||
|
from .codec.industry import parse_tdxhy_cfg
|
||||||
from .commands.base import BaseCommand
|
from .commands.base import BaseCommand
|
||||||
from .commands.block_info import GetBlockInfoCmd, GetBlockInfoMetaCmd
|
from .commands.block_info import GetBlockInfoCmd, GetBlockInfoMetaCmd
|
||||||
from .commands.company_info import GetCompanyInfoCategoryCmd, GetCompanyInfoContentCmd
|
from .commands.company_info import GetCompanyInfoCategoryCmd, GetCompanyInfoContentCmd
|
||||||
from .commands.finance_info import GetFinanceInfoCmd
|
from .commands.finance_info import GetFinanceInfoCmd
|
||||||
from .commands.fund_flow import GetHistoryFundFlowCmd
|
from .commands.fund_flow import GetHistoryFundFlowCmd
|
||||||
from .commands.report_file import GetReportFileCmd
|
|
||||||
from .commands.minute_time import GetHistoryMinuteTimeDataCmd, GetMinuteTimeDataCmd
|
from .commands.minute_time import GetHistoryMinuteTimeDataCmd, GetMinuteTimeDataCmd
|
||||||
|
from .commands.report_file import GetReportFileCmd
|
||||||
from .commands.security_bars import GetIndexBarsCmd, GetSecurityBarsCmd
|
from .commands.security_bars import GetIndexBarsCmd, GetSecurityBarsCmd
|
||||||
from .commands.security_count import GetSecurityCountCmd
|
from .commands.security_count import GetSecurityCountCmd
|
||||||
from .commands.security_list import GetSecurityListCmd
|
from .commands.security_list import GetSecurityListCmd
|
||||||
from .commands.security_quotes import GetSecurityQuotesCmd
|
from .commands.security_quotes import GetSecurityQuotesCmd
|
||||||
from .commands.transaction import GetHistoryTransactionDataCmd, GetTransactionDataCmd
|
from .commands.transaction import GetHistoryTransactionDataCmd, GetTransactionDataCmd
|
||||||
from .commands.xdxr_info import GetXdxrInfoCmd
|
from .commands.xdxr_info import GetXdxrInfoCmd
|
||||||
from .codec.block import parse_block_dat
|
|
||||||
from .codec.industry import parse_tdxhy_cfg
|
|
||||||
from .exceptions import TdxConnectionError
|
from .exceptions import TdxConnectionError
|
||||||
from .models.bar import SecurityBar
|
from .models.bar import SecurityBar
|
||||||
from .models.enums import KlineCategory, Market
|
from .models.enums import KlineCategory, Market
|
||||||
@@ -148,7 +148,12 @@ class TdxClient:
|
|||||||
return self._execute(GetSecurityListCmd(market, start))
|
return self._execute(GetSecurityListCmd(market, start))
|
||||||
|
|
||||||
def get_security_list_all(self) -> list[SecurityInfo]:
|
def get_security_list_all(self) -> list[SecurityInfo]:
|
||||||
"""获取全市场(沪深 A 股)完整证券列表,并自动挂载行业信息。"""
|
"""获取沪深 A 股完整证券列表,并自动挂载行业信息。
|
||||||
|
|
||||||
|
注意:
|
||||||
|
`Market.BJ` 的证券列表请求长期存在服务器超时问题,当前版本暂不纳入此方法。
|
||||||
|
若需 BJ 名单,应改由 `base_info.zip` 等文件离线解析获得。
|
||||||
|
"""
|
||||||
# 1. 尝试获取行业配置
|
# 1. 尝试获取行业配置
|
||||||
industry_map = {}
|
industry_map = {}
|
||||||
try:
|
try:
|
||||||
@@ -159,7 +164,9 @@ class TdxClient:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
all_stocks: list[SecurityInfo] = []
|
all_stocks: list[SecurityInfo] = []
|
||||||
for market in [Market.SH, Market.SZ, Market.BJ]:
|
# 注意:Market.BJ 证券列表请求常年超时,短期降级为仅 SH/SZ;
|
||||||
|
# BJ 列表需解析 base_info.zip 获得(待实现)。
|
||||||
|
for market in [Market.SH, Market.SZ]:
|
||||||
count = self.get_security_count(market)
|
count = self.get_security_count(market)
|
||||||
for start in range(0, count, 1000):
|
for start in range(0, count, 1000):
|
||||||
stocks = self.get_security_list(market, start)
|
stocks = self.get_security_list(market, start)
|
||||||
@@ -174,10 +181,6 @@ class TdxClient:
|
|||||||
# 深市 A 股:00xxxx, 30xxxx
|
# 深市 A 股:00xxxx, 30xxxx
|
||||||
if s.code.startswith(("00", "30")):
|
if s.code.startswith(("00", "30")):
|
||||||
is_a_share = True
|
is_a_share = True
|
||||||
elif market == Market.BJ:
|
|
||||||
# 京市 A 股:8xxxxx, 43xxxx, 92xxxx
|
|
||||||
if s.code.startswith(("8", "43", "92")):
|
|
||||||
is_a_share = True
|
|
||||||
|
|
||||||
if is_a_share:
|
if is_a_share:
|
||||||
# 挂载行业信息
|
# 挂载行业信息
|
||||||
@@ -312,31 +315,72 @@ class TdxClient:
|
|||||||
return bytes(full_data)
|
return bytes(full_data)
|
||||||
|
|
||||||
def get_market_stat(self) -> MarketStat:
|
def get_market_stat(self) -> MarketStat:
|
||||||
"""获取 A 股全市场涨跌统计概况。"""
|
"""获取 A 股全市场涨跌统计概况(基于 880005 行情统计)。
|
||||||
# 通达信中 880005 是行情统计代码
|
|
||||||
|
注意:
|
||||||
|
`suspended_count` 是 `total - up - down - neutral` 的残差估算值,
|
||||||
|
用于保证计数守恒,不应视为协议已明确验证的停牌字段。
|
||||||
|
"""
|
||||||
|
# 通达信中 880005 是全市场行情统计代码
|
||||||
quotes = self.get_security_quotes([(Market.SH, "880005")])
|
quotes = self.get_security_quotes([(Market.SH, "880005")])
|
||||||
if not quotes:
|
if not quotes:
|
||||||
raise RuntimeError("无法获取市场统计数据")
|
raise RuntimeError("无法获取市场统计数据")
|
||||||
q = quotes[0]
|
q = quotes[0]
|
||||||
|
up = int(q.price)
|
||||||
|
down = int(q.pre_close)
|
||||||
|
neutral = int(q.low)
|
||||||
|
total = int(q.high)
|
||||||
return MarketStat(
|
return MarketStat(
|
||||||
up_count=int(q.price),
|
up_count=up,
|
||||||
down_count=int(q.pre_close),
|
down_count=down,
|
||||||
neutral_count=int(q.open),
|
neutral_count=neutral,
|
||||||
total_count=int(q.high),
|
suspended_count=max(0, total - up - down - neutral),
|
||||||
|
total_count=total,
|
||||||
total_amount=q.amount,
|
total_amount=q.amount,
|
||||||
total_volume=q.vol,
|
total_volume=q.vol,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_fund_flow(self, market: Market, code: str) -> FundFlow:
|
def get_fund_flow(self, market: Market, code: str) -> FundFlow:
|
||||||
"""获取个股当日资金流向分布(基于 L1 逐笔数据统计)。"""
|
"""获取个股当日资金流向分布(基于 L1 逐笔数据统计)。"""
|
||||||
# 1. 拉取当日全量分笔 (TDX L1 最多支持约 2000-4000 条,通常足够 A 股当日统计)
|
# 1. 分页拉取当日分笔并去重
|
||||||
all_recs: list[TransactionRecord] = []
|
all_recs: list[TransactionRecord] = []
|
||||||
for start in [0, 2000, 4000]:
|
seen_sig = set()
|
||||||
|
seen_page_sigs = set()
|
||||||
|
start = 0
|
||||||
|
|
||||||
|
while start < 10000:
|
||||||
recs = self.get_transaction_data(market, code, start, 2000)
|
recs = self.get_transaction_data(market, code, start, 2000)
|
||||||
if not recs:
|
if not recs:
|
||||||
break
|
break
|
||||||
all_recs.extend(recs)
|
|
||||||
if len(recs) < 2000:
|
# 页签名判断:首尾记录组合
|
||||||
|
page_sig = (
|
||||||
|
(
|
||||||
|
recs[0].hour, recs[0].minute, recs[0].price,
|
||||||
|
recs[0].vol, recs[0].buyorsell, recs[0].unknown_last
|
||||||
|
),
|
||||||
|
(
|
||||||
|
recs[-1].hour, recs[-1].minute, recs[-1].price,
|
||||||
|
recs[-1].vol, recs[-1].buyorsell, recs[-1].unknown_last
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if page_sig in seen_page_sigs:
|
||||||
|
break
|
||||||
|
seen_page_sigs.add(page_sig)
|
||||||
|
|
||||||
|
new_count = 0
|
||||||
|
for r in recs:
|
||||||
|
sig = (r.hour, r.minute, r.price, r.vol, r.buyorsell, r.unknown_last)
|
||||||
|
if sig not in seen_sig:
|
||||||
|
seen_sig.add(sig)
|
||||||
|
all_recs.append(r)
|
||||||
|
new_count += 1
|
||||||
|
|
||||||
|
if new_count == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
start += len(recs)
|
||||||
|
if len(recs) < 100:
|
||||||
break
|
break
|
||||||
|
|
||||||
# 2. 统计逻辑
|
# 2. 统计逻辑
|
||||||
@@ -366,7 +410,10 @@ class TdxClient:
|
|||||||
def get_history_fund_flow(
|
def get_history_fund_flow(
|
||||||
self, market: Market, code: str, start: int, count: int
|
self, market: Market, code: str, start: int, count: int
|
||||||
) -> list[HistoricalFundFlow]:
|
) -> list[HistoricalFundFlow]:
|
||||||
"""获取个股历史日线资金流向序列(Category 22)。"""
|
"""获取个股历史日线资金流向序列(Category 22)。
|
||||||
|
|
||||||
|
[EXPERIMENTAL] 当前多台公开主机对该请求仍可能返回空列表。
|
||||||
|
"""
|
||||||
return self._execute(GetHistoryFundFlowCmd(market, code, start, count))
|
return self._execute(GetHistoryFundFlowCmd(market, code, start, count))
|
||||||
|
|
||||||
|
|
||||||
@@ -500,7 +547,12 @@ class AsyncTdxClient:
|
|||||||
return await self._execute(GetSecurityListCmd(market, start))
|
return await self._execute(GetSecurityListCmd(market, start))
|
||||||
|
|
||||||
async def get_security_list_all(self) -> list[SecurityInfo]:
|
async def get_security_list_all(self) -> list[SecurityInfo]:
|
||||||
"""获取全市场完整证券列表,并自动挂载行业信息。"""
|
"""获取沪深 A 股完整证券列表,并自动挂载行业信息。
|
||||||
|
|
||||||
|
注意:
|
||||||
|
`Market.BJ` 的证券列表请求长期存在服务器超时问题,当前版本暂不纳入此方法。
|
||||||
|
若需 BJ 名单,应改由 `base_info.zip` 等文件离线解析获得。
|
||||||
|
"""
|
||||||
industry_map = {}
|
industry_map = {}
|
||||||
try:
|
try:
|
||||||
cfg_data = await self.get_report_file("tdxhy.cfg")
|
cfg_data = await self.get_report_file("tdxhy.cfg")
|
||||||
@@ -510,7 +562,9 @@ class AsyncTdxClient:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
all_stocks: list[SecurityInfo] = []
|
all_stocks: list[SecurityInfo] = []
|
||||||
for market in [Market.SH, Market.SZ, Market.BJ]:
|
# 注意:Market.BJ 证券列表请求常年超时,短期降级为仅 SH/SZ;
|
||||||
|
# BJ 列表需解析 base_info.zip 获得(待实现)。
|
||||||
|
for market in [Market.SH, Market.SZ]:
|
||||||
count = await self.get_security_count(market)
|
count = await self.get_security_count(market)
|
||||||
for start in range(0, count, 1000):
|
for start in range(0, count, 1000):
|
||||||
stocks = await self.get_security_list(market, start)
|
stocks = await self.get_security_list(market, start)
|
||||||
@@ -522,9 +576,6 @@ class AsyncTdxClient:
|
|||||||
elif market == Market.SZ:
|
elif market == Market.SZ:
|
||||||
if s.code.startswith(("00", "30")):
|
if s.code.startswith(("00", "30")):
|
||||||
is_a_share = True
|
is_a_share = True
|
||||||
elif market == Market.BJ:
|
|
||||||
if s.code.startswith(("8", "43", "92")):
|
|
||||||
is_a_share = True
|
|
||||||
|
|
||||||
if is_a_share:
|
if is_a_share:
|
||||||
if s.code in industry_map:
|
if s.code in industry_map:
|
||||||
@@ -627,29 +678,72 @@ class AsyncTdxClient:
|
|||||||
return bytes(full_data)
|
return bytes(full_data)
|
||||||
|
|
||||||
async def get_market_stat(self) -> MarketStat:
|
async def get_market_stat(self) -> MarketStat:
|
||||||
"""获取 A 股全市场涨跌统计概况。"""
|
"""获取 A 股全市场涨跌统计概况(基于 880005 行情统计)。
|
||||||
|
|
||||||
|
注意:
|
||||||
|
`suspended_count` 是 `total - up - down - neutral` 的残差估算值,
|
||||||
|
用于保证计数守恒,不应视为协议已明确验证的停牌字段。
|
||||||
|
"""
|
||||||
|
# 通达信中 880005 是全市场行情统计代码
|
||||||
quotes = await self.get_security_quotes([(Market.SH, "880005")])
|
quotes = await self.get_security_quotes([(Market.SH, "880005")])
|
||||||
if not quotes:
|
if not quotes:
|
||||||
raise RuntimeError("无法获取市场统计数据")
|
raise RuntimeError("无法获取市场统计数据")
|
||||||
q = quotes[0]
|
q = quotes[0]
|
||||||
|
up = int(q.price)
|
||||||
|
down = int(q.pre_close)
|
||||||
|
neutral = int(q.low)
|
||||||
|
total = int(q.high)
|
||||||
return MarketStat(
|
return MarketStat(
|
||||||
up_count=int(q.price),
|
up_count=up,
|
||||||
down_count=int(q.pre_close),
|
down_count=down,
|
||||||
neutral_count=int(q.open),
|
neutral_count=neutral,
|
||||||
total_count=int(q.high),
|
suspended_count=max(0, total - up - down - neutral),
|
||||||
|
total_count=total,
|
||||||
total_amount=q.amount,
|
total_amount=q.amount,
|
||||||
total_volume=q.vol,
|
total_volume=q.vol,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_fund_flow(self, market: Market, code: str) -> FundFlow:
|
async def get_fund_flow(self, market: Market, code: str) -> FundFlow:
|
||||||
"""获取个股当日资金流向分布。"""
|
"""获取个股当日资金流向分布(基于 L1 逐笔数据统计)。"""
|
||||||
|
# 1. 分页拉取当日分笔并去重
|
||||||
all_recs: list[TransactionRecord] = []
|
all_recs: list[TransactionRecord] = []
|
||||||
for start in [0, 2000, 4000]:
|
seen_sig = set()
|
||||||
|
seen_page_sigs = set()
|
||||||
|
start = 0
|
||||||
|
|
||||||
|
while start < 10000:
|
||||||
recs = await self.get_transaction_data(market, code, start, 2000)
|
recs = await self.get_transaction_data(market, code, start, 2000)
|
||||||
if not recs:
|
if not recs:
|
||||||
break
|
break
|
||||||
all_recs.extend(recs)
|
|
||||||
if len(recs) < 2000:
|
# 页签名判断:首尾记录组合
|
||||||
|
page_sig = (
|
||||||
|
(
|
||||||
|
recs[0].hour, recs[0].minute, recs[0].price,
|
||||||
|
recs[0].vol, recs[0].buyorsell, recs[0].unknown_last
|
||||||
|
),
|
||||||
|
(
|
||||||
|
recs[-1].hour, recs[-1].minute, recs[-1].price,
|
||||||
|
recs[-1].vol, recs[-1].buyorsell, recs[-1].unknown_last
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if page_sig in seen_page_sigs:
|
||||||
|
break
|
||||||
|
seen_page_sigs.add(page_sig)
|
||||||
|
|
||||||
|
new_count = 0
|
||||||
|
for r in recs:
|
||||||
|
sig = (r.hour, r.minute, r.price, r.vol, r.buyorsell, r.unknown_last)
|
||||||
|
if sig not in seen_sig:
|
||||||
|
seen_sig.add(sig)
|
||||||
|
all_recs.append(r)
|
||||||
|
new_count += 1
|
||||||
|
|
||||||
|
if new_count == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
start += len(recs)
|
||||||
|
if len(recs) < 100:
|
||||||
break
|
break
|
||||||
|
|
||||||
stats = {
|
stats = {
|
||||||
@@ -674,6 +768,8 @@ class AsyncTdxClient:
|
|||||||
async def get_history_fund_flow(
|
async def get_history_fund_flow(
|
||||||
self, market: Market, code: str, start: int, count: int
|
self, market: Market, code: str, start: int, count: int
|
||||||
) -> list[HistoricalFundFlow]:
|
) -> list[HistoricalFundFlow]:
|
||||||
"""获取个股历史日线资金流向序列。"""
|
"""获取个股历史日线资金流向序列(Category 22)。
|
||||||
return await self._execute(GetHistoryFundFlowCmd(market, code, start, count))
|
|
||||||
|
|
||||||
|
[EXPERIMENTAL] 当前多台公开主机对该请求仍可能返回空列表。
|
||||||
|
"""
|
||||||
|
return await self._execute(GetHistoryFundFlowCmd(market, code, start, count))
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""A 股价格限制规则引擎。"""
|
||||||
|
|
||||||
|
from ..models.enums import Market
|
||||||
|
from ..models.finance import FinanceInfo
|
||||||
|
|
||||||
|
|
||||||
|
def compute_price_limits(
|
||||||
|
market: Market,
|
||||||
|
code: str,
|
||||||
|
name: str,
|
||||||
|
pre_close: float,
|
||||||
|
finance_info: FinanceInfo | None = None,
|
||||||
|
) -> tuple[float | None, float | None]:
|
||||||
|
"""根据板块规则计算涨跌停价。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(limit_up, limit_down)
|
||||||
|
|
||||||
|
无涨跌幅限制或当前规则无法可靠判断时返回 ``(None, None)``。
|
||||||
|
"""
|
||||||
|
if pre_close <= 0:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
upper_name = name.upper()
|
||||||
|
|
||||||
|
# 指数/板块类代码通常无涨跌停。
|
||||||
|
# 这里优先用明确的指数代码段判断,再用名称兜底,避免把真实股票误判成指数。
|
||||||
|
is_index = False
|
||||||
|
if market == Market.SH and code.startswith(
|
||||||
|
("000", "880", "881", "882", "883", "884", "885", "999")
|
||||||
|
):
|
||||||
|
is_index = True
|
||||||
|
elif market == Market.SZ and code.startswith(("395", "399")):
|
||||||
|
is_index = True
|
||||||
|
elif "指数" in name or "板块" in name:
|
||||||
|
is_index = True
|
||||||
|
|
||||||
|
if is_index:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
limit_pct = 0.10 # 默认 10%
|
||||||
|
|
||||||
|
# 2. ST / *ST 判断
|
||||||
|
if "ST" in upper_name:
|
||||||
|
limit_pct = 0.05
|
||||||
|
# 3. 科创板 (688) / 创业板 (300, 301)
|
||||||
|
elif code.startswith("688") or code.startswith("300") or code.startswith("301"):
|
||||||
|
limit_pct = 0.20
|
||||||
|
# 4. 北交所 (43, 83, 87, 92)
|
||||||
|
elif code.startswith(("43", "83", "87", "92")):
|
||||||
|
limit_pct = 0.30
|
||||||
|
|
||||||
|
# TODO: 上市前 5 日无涨跌幅限制判断(需要 ipo_date 或更明确的上市状态标识)。
|
||||||
|
_ = finance_info
|
||||||
|
|
||||||
|
def _round_price(p: float) -> float:
|
||||||
|
return round(p + 0.00001, 2)
|
||||||
|
|
||||||
|
limit_up = _round_price(pre_close * (1 + limit_pct))
|
||||||
|
limit_down = _round_price(pre_close * (1 - limit_pct))
|
||||||
|
|
||||||
|
return limit_up, limit_down
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
from .._binary import slice_bytes, unpack_from
|
|
||||||
from ..exceptions import TdxDecodeError
|
from ..exceptions import TdxDecodeError
|
||||||
from .base import BaseCommand
|
from .base import BaseCommand
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
from .._binary import slice_bytes, unpack_from
|
|
||||||
from ..codec.volume import _decode_volume
|
from ..codec.volume import _decode_volume
|
||||||
from ..exceptions import TdxDecodeError
|
|
||||||
from ..models.enums import Market
|
from ..models.enums import Market
|
||||||
from ..models.stats import HistoricalFundFlow
|
from ..models.stats import HistoricalFundFlow
|
||||||
from .base import BaseCommand
|
from .base import BaseCommand
|
||||||
@@ -20,29 +18,24 @@ class GetHistoryFundFlowCmd(BaseCommand[list[HistoricalFundFlow]]):
|
|||||||
self.count = count
|
self.count = count
|
||||||
|
|
||||||
def build_request(self) -> bytes:
|
def build_request(self) -> bytes:
|
||||||
# 使用 0x052d 指令(K 线类指令)
|
# Header (12 bytes) + Payload (28 bytes) = 40 bytes
|
||||||
# 负载长度固定为 28 字节 (0x1c)
|
return struct.pack(
|
||||||
payload_len = 0x1c
|
"<HIHHHH6sHHHHIIH",
|
||||||
header = struct.pack(
|
0x010C,
|
||||||
"<HIHHH",
|
0x01016408,
|
||||||
0x010c,
|
0x001C,
|
||||||
0x01016408, # 注意此处标志位与普通行情略有不同
|
0x001C,
|
||||||
payload_len,
|
0x052D,
|
||||||
payload_len,
|
|
||||||
0x052d,
|
|
||||||
)
|
|
||||||
# 参数包:Market(B), Code(6s), Category(H=22), Unknown(H=1), Start(I), Count(I), 3个Unknown(H)
|
|
||||||
params = struct.pack(
|
|
||||||
"<B6sHHIIHHH",
|
|
||||||
int(self.market),
|
int(self.market),
|
||||||
self.code,
|
self.code,
|
||||||
22, # Category 22
|
22,
|
||||||
1, # Unknown
|
1,
|
||||||
self.start,
|
self.start,
|
||||||
self.count,
|
self.count,
|
||||||
0, 0, 0
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
return header + params
|
|
||||||
|
|
||||||
def parse_response(self, body: bytes) -> list[HistoricalFundFlow]:
|
def parse_response(self, body: bytes) -> list[HistoricalFundFlow]:
|
||||||
# 响应格式:9字节头 + 2字节数量 + 每条记录 36 字节
|
# 响应格式:9字节头 + 2字节数量 + 每条记录 36 字节
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
from .._binary import slice_bytes, unpack_from
|
|
||||||
from ..exceptions import TdxDecodeError
|
|
||||||
from .base import BaseCommand
|
from .base import BaseCommand
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -37,20 +37,23 @@ class GetSecurityBarsCmd(BaseCommand[list[SecurityBar]]):
|
|||||||
self.count = count
|
self.count = count
|
||||||
|
|
||||||
def build_request(self) -> bytes:
|
def build_request(self) -> bytes:
|
||||||
|
# Header (12 bytes) + Payload (28 bytes) = 40 bytes
|
||||||
return struct.pack(
|
return struct.pack(
|
||||||
"<HIHHHH6sHHHHIIH",
|
"<HIHHHH6sHHHHIIH",
|
||||||
0x010C, # 固定
|
0x010C,
|
||||||
0x01016408, # 固定
|
0x01016408,
|
||||||
0x001C, # 固定(payload 长度)
|
0x001C,
|
||||||
0x001C, # 固定(payload 长度)
|
0x001C,
|
||||||
0x052D, # 命令码:K线
|
0x052D,
|
||||||
int(self.market),
|
int(self.market),
|
||||||
self.code,
|
self.code,
|
||||||
int(self.category),
|
int(self.category),
|
||||||
1, # 固定
|
1,
|
||||||
self.start,
|
self.start,
|
||||||
self.count,
|
self.count,
|
||||||
0, 0, 0, # 填充
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse_response(self, body: bytes) -> list[SecurityBar]:
|
def parse_response(self, body: bytes) -> list[SecurityBar]:
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ class GetSecurityListCmd(BaseCommand[list[SecurityInfo]]):
|
|||||||
self.start = start
|
self.start = start
|
||||||
|
|
||||||
def build_request(self) -> bytes:
|
def build_request(self) -> bytes:
|
||||||
header = bytes.fromhex("0c01186401010600060050 04".replace(" ", ""))
|
# Header (12 bytes) + Payload (6 bytes) = 18 bytes
|
||||||
return header + struct.pack("<HH", int(self.market), self.start)
|
# Payload: Market(H), Start(H), Unknown(H)=0
|
||||||
|
header = bytes.fromhex("0c0118640101060006005004".replace(" ", ""))
|
||||||
|
return header + struct.pack("<HHH", int(self.market), self.start, 0)
|
||||||
|
|
||||||
def parse_response(self, body: bytes) -> list[SecurityInfo]:
|
def parse_response(self, body: bytes) -> list[SecurityInfo]:
|
||||||
(num,) = unpack_from("<H", body, 0, "security_list header")
|
(num,) = unpack_from("<H", body, 0, "security_list header")
|
||||||
|
|||||||
@@ -195,8 +195,8 @@ class GetSecurityQuotesCmd(BaseCommand[list[SecurityQuote]]):
|
|||||||
ask5=(price_raw + ask5_d) / 100.0,
|
ask5=(price_raw + ask5_d) / 100.0,
|
||||||
ask_vol5=float(av5),
|
ask_vol5=float(av5),
|
||||||
rise_speed=rise_speed_raw / 100.0,
|
rise_speed=rise_speed_raw / 100.0,
|
||||||
limit_up=(price_raw + unknown_2) / 100.0,
|
limit_up=None,
|
||||||
limit_down=(price_raw + unknown_3) / 100.0,
|
limit_down=None,
|
||||||
unknown_2=unknown_2,
|
unknown_2=unknown_2,
|
||||||
unknown_3=unknown_3,
|
unknown_3=unknown_3,
|
||||||
unknown_5=unknown_5,
|
unknown_5=unknown_5,
|
||||||
|
|||||||
@@ -60,12 +60,12 @@ class SecurityQuote:
|
|||||||
|
|
||||||
# 价格指标
|
# 价格指标
|
||||||
rise_speed: float # 涨速(原 reversed_bytes9 / 100)
|
rise_speed: float # 涨速(原 reversed_bytes9 / 100)
|
||||||
limit_up: float # 涨停价(由 unknown_2 / 100 转换)
|
limit_up: float | None # 涨停价(业务规则计算)
|
||||||
limit_down: float # 跌停价(由 unknown_3 / 100 转换)
|
limit_down: float | None # 跌停价(业务规则计算)
|
||||||
|
|
||||||
# 未知字段:买卖量之后的两个变长整数(保留供进一步分析)
|
# 未知字段:买卖量之后的两个变长整数(保留供进一步分析)
|
||||||
unknown_2: int = field(default=0, repr=False) # 原始涨停价整数(price_raw + diff)
|
unknown_2: int = field(default=0, repr=False) # 未知变长整数 2
|
||||||
unknown_3: int = field(default=0, repr=False) # 原始跌停价整数(price_raw + diff)
|
unknown_3: int = field(default=0, repr=False) # 未知变长整数 3
|
||||||
|
|
||||||
# 未知字段:尾部四个变长整数
|
# 未知字段:尾部四个变长整数
|
||||||
unknown_5: int = field(default=0, repr=False) # 原 reversed_bytes5
|
unknown_5: int = field(default=0, repr=False) # 原 reversed_bytes5
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
"""验证市场概况模型。"""
|
"""验证市场概况模型。"""
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MarketStat:
|
class MarketStat:
|
||||||
"""全市场涨跌统计概况。"""
|
"""全市场涨跌统计概况。"""
|
||||||
up_count: int # 上涨家数
|
up_count: int # 上涨家数
|
||||||
down_count: int # 下跌家数
|
down_count: int # 下跌家数
|
||||||
neutral_count: int # 平盘家数
|
neutral_count: int # 平盘家数
|
||||||
total_count: int # 总家数
|
suspended_count: int # 由 total-(up+down+neutral) 得到的残差项,近似表示停牌/未参与统计家数
|
||||||
total_amount: float # 总成交额
|
total_count: int # 总计(包含停牌)
|
||||||
total_volume: float # 总成交量
|
total_amount: float # 总成交额
|
||||||
|
total_volume: float # 总成交量
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
"""针对本轮 A 股增强功能的单元测试。"""
|
"""针对本轮 A 股增强功能的单元测试。"""
|
||||||
|
|
||||||
import pytest
|
|
||||||
import struct
|
import struct
|
||||||
from unittest.mock import patch, MagicMock, AsyncMock
|
from unittest.mock import patch
|
||||||
from xmtdx import TdxClient, Market
|
|
||||||
|
from xmtdx import Market, TdxClient
|
||||||
|
from xmtdx.models.quote import SecurityQuote
|
||||||
from xmtdx.models.security import SecurityInfo
|
from xmtdx.models.security import SecurityInfo
|
||||||
from xmtdx.models.timeseries import TransactionRecord
|
from xmtdx.models.timeseries import TransactionRecord
|
||||||
from xmtdx.models.quote import SecurityQuote
|
|
||||||
from xmtdx.models.stats import FundFlow, HistoricalFundFlow, MarketStat
|
|
||||||
|
|
||||||
@patch("xmtdx.client.TdxConnection")
|
@patch("xmtdx.client.TdxConnection")
|
||||||
def test_get_fund_flow_logic(mock_conn_cls):
|
def test_get_fund_flow_logic(_mock_conn_cls):
|
||||||
"""测试资金流分类计算逻辑。"""
|
"""测试资金流分类计算逻辑。"""
|
||||||
mock_conn = mock_conn_cls.return_value
|
|
||||||
client = TdxClient("127.0.0.1")
|
client = TdxClient("127.0.0.1")
|
||||||
|
|
||||||
# 构造模拟 Tick 数据
|
# 构造模拟 Tick 数据
|
||||||
@@ -31,7 +30,7 @@ def test_get_fund_flow_logic(mock_conn_cls):
|
|||||||
assert flow.main_net_inflow == 1000000.0 - 250000.0
|
assert flow.main_net_inflow == 1000000.0 - 250000.0
|
||||||
|
|
||||||
@patch("xmtdx.client.TdxConnection")
|
@patch("xmtdx.client.TdxConnection")
|
||||||
def test_get_security_list_all_filtering(mock_conn_cls):
|
def test_get_security_list_all_filtering(_mock_conn_cls):
|
||||||
"""测试三市 A 股过滤与行业挂载逻辑。"""
|
"""测试三市 A 股过滤与行业挂载逻辑。"""
|
||||||
client = TdxClient("127.0.0.1")
|
client = TdxClient("127.0.0.1")
|
||||||
|
|
||||||
@@ -56,18 +55,18 @@ def test_get_security_list_all_filtering(mock_conn_cls):
|
|||||||
patch.object(TdxClient, "get_security_list", side_effect=mock_get_list):
|
patch.object(TdxClient, "get_security_list", side_effect=mock_get_list):
|
||||||
|
|
||||||
all_stocks = client.get_security_list_all()
|
all_stocks = client.get_security_list_all()
|
||||||
|
|
||||||
assert len(all_stocks) == 3
|
# 预期只有 SH 和 SZ,BJ 已在扫描中降级移除
|
||||||
|
assert len(all_stocks) == 2
|
||||||
codes = [s.code for s in all_stocks]
|
codes = [s.code for s in all_stocks]
|
||||||
assert "600000" in codes
|
assert "600000" in codes
|
||||||
assert "000001" in codes
|
assert "000001" in codes
|
||||||
assert "830000" in codes
|
assert "830000" not in codes
|
||||||
|
|
||||||
s0 = next(s for s in all_stocks if s.code == "600000")
|
s0 = next(s for s in all_stocks if s.code == "600000")
|
||||||
assert s0.industry_tdx == "T01"
|
assert s0.industry_tdx == "T01"
|
||||||
|
|
||||||
@patch("xmtdx.client.TdxConnection")
|
@patch("xmtdx.client.TdxConnection")
|
||||||
def test_get_market_stat_mapping(mock_conn_cls):
|
def test_get_market_stat_mapping(_mock_conn_cls):
|
||||||
"""测试市场统计字段映射。"""
|
"""测试市场统计字段映射。"""
|
||||||
client = TdxClient("127.0.0.1")
|
client = TdxClient("127.0.0.1")
|
||||||
|
|
||||||
@@ -75,18 +74,23 @@ def test_get_market_stat_mapping(mock_conn_cls):
|
|||||||
Market.SH, "880005",
|
Market.SH, "880005",
|
||||||
price=3000.0, # up
|
price=3000.0, # up
|
||||||
pre_close=2000.0, # down
|
pre_close=2000.0, # down
|
||||||
open=500.0, # neutral
|
open=0,
|
||||||
high=5500.0, # total
|
high=5500.0, # total
|
||||||
low=100.0, vol=1000000.0, cur_vol=0, amount=50000000.0,
|
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,
|
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,
|
bid1=0, bid_vol1=0, bid2=0, bid_vol2=0, bid3=0, bid_vol3=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,
|
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
|
rise_speed=0, limit_up=0, limit_down=0
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch.object(TdxClient, "get_security_quotes", return_value=[mock_quote]):
|
with patch.object(TdxClient, "get_security_quotes", return_value=[mock_quote]):
|
||||||
stat = client.get_market_stat()
|
stat = client.get_market_stat()
|
||||||
assert stat.up_count == 3000
|
assert stat.up_count == 3000
|
||||||
|
assert stat.down_count == 2000
|
||||||
|
assert stat.neutral_count == 500
|
||||||
assert stat.total_count == 5500
|
assert stat.total_count == 5500
|
||||||
|
|
||||||
def test_get_history_fund_flow_parsing():
|
def test_get_history_fund_flow_parsing():
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"""板块信息单元测试。"""
|
"""板块信息单元测试。"""
|
||||||
|
|
||||||
import pytest
|
import asyncio
|
||||||
import struct
|
import struct
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from xmtdx.client import AsyncTdxClient, TdxClient
|
from xmtdx.client import AsyncTdxClient, TdxClient
|
||||||
|
from xmtdx.codec.block import parse_block_dat
|
||||||
|
from xmtdx.models.finance import TdxBlock
|
||||||
|
|
||||||
|
|
||||||
@patch("xmtdx.client.AsyncTdxConnection")
|
@patch("xmtdx.client.AsyncTdxConnection")
|
||||||
@@ -14,7 +16,7 @@ def test_async_get_block_info_logic(mock_conn_cls):
|
|||||||
|
|
||||||
# 模拟异步 execute
|
# 模拟异步 execute
|
||||||
async def mock_execute(cmd):
|
async def mock_execute(cmd):
|
||||||
from xmtdx.commands.block_info import GetBlockInfoMetaCmd, GetBlockInfoCmd
|
from xmtdx.commands.block_info import GetBlockInfoCmd, GetBlockInfoMetaCmd
|
||||||
if isinstance(cmd, GetBlockInfoMetaCmd):
|
if isinstance(cmd, GetBlockInfoMetaCmd):
|
||||||
return 100, "hash"
|
return 100, "hash"
|
||||||
if isinstance(cmd, GetBlockInfoCmd):
|
if isinstance(cmd, GetBlockInfoCmd):
|
||||||
@@ -34,10 +36,7 @@ def test_async_get_block_info_logic(mock_conn_cls):
|
|||||||
assert isinstance(res, list)
|
assert isinstance(res, list)
|
||||||
assert mock_conn.execute.call_count == 2 # 1 meta + 1 data
|
assert mock_conn.execute.call_count == 2 # 1 meta + 1 data
|
||||||
|
|
||||||
import asyncio
|
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
from xmtdx.codec.block import parse_block_dat
|
|
||||||
from xmtdx.models.finance import TdxBlock
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_block_dat_empty():
|
def test_parse_block_dat_empty():
|
||||||
@@ -81,7 +80,7 @@ def test_get_block_info_logic(mock_conn_cls):
|
|||||||
|
|
||||||
# 模拟 GetBlockInfoMeta 响应:size=35000 (需要2次拉取)
|
# 模拟 GetBlockInfoMeta 响应:size=35000 (需要2次拉取)
|
||||||
def mock_execute(cmd):
|
def mock_execute(cmd):
|
||||||
from xmtdx.commands.block_info import GetBlockInfoMetaCmd, GetBlockInfoCmd
|
from xmtdx.commands.block_info import GetBlockInfoCmd, GetBlockInfoMetaCmd
|
||||||
if isinstance(cmd, GetBlockInfoMetaCmd):
|
if isinstance(cmd, GetBlockInfoMetaCmd):
|
||||||
return 35000, "dummy_hash"
|
return 35000, "dummy_hash"
|
||||||
if isinstance(cmd, GetBlockInfoCmd):
|
if isinstance(cmd, GetBlockInfoCmd):
|
||||||
|
|||||||
@@ -266,7 +266,8 @@ def test_xdxr_info_parse():
|
|||||||
# share count decode: 通达信自定义浮点,单位万股,与 FinanceInfo.zong_guben/10000 一致
|
# share count decode: 通达信自定义浮点,单位万股,与 FinanceInfo.zong_guben/10000 一致
|
||||||
stock_recs = [r for r in recs if 2 <= r.category <= 10]
|
stock_recs = [r for r in recs if 2 <= r.category <= 10]
|
||||||
last = stock_recs[-1]
|
last = stock_recs[-1]
|
||||||
# 最近一条 hou_zongguben ≈ 3_330_583.75 万股(与 FinanceInfo.zong_guben 33_305_837_500 ÷ 10000 完全吻合)
|
# 最近一条 hou_zongguben ≈ 3_330_583.75 万股
|
||||||
|
# 与 FinanceInfo.zong_guben 33_305_837_500 ÷ 10000 完全吻合
|
||||||
assert last.hou_zongguben is not None
|
assert last.hou_zongguben is not None
|
||||||
assert abs(last.hou_zongguben - 3_330_583.75) < 1.0
|
assert abs(last.hou_zongguben - 3_330_583.75) < 1.0
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""心跳机制单元测试。"""
|
"""心跳机制单元测试。"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import pytest
|
from unittest.mock import AsyncMock, patch
|
||||||
from unittest.mock import patch, MagicMock, AsyncMock
|
|
||||||
from xmtdx import AsyncTdxClient, Market
|
from xmtdx import AsyncTdxClient
|
||||||
|
|
||||||
|
|
||||||
def test_heartbeat_sends_periodically():
|
def test_heartbeat_sends_periodically():
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
"""协议底层修复验证(针对 2026-04-15 审查结论)。"""
|
||||||
|
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from xmtdx.codec.price_rules import compute_price_limits
|
||||||
|
from xmtdx.commands.fund_flow import GetHistoryFundFlowCmd
|
||||||
|
from xmtdx.commands.security_bars import GetSecurityBarsCmd
|
||||||
|
from xmtdx.commands.security_list import GetSecurityListCmd
|
||||||
|
from xmtdx.commands.security_quotes import GetSecurityQuotesCmd
|
||||||
|
from xmtdx.models.enums import KlineCategory, Market
|
||||||
|
|
||||||
|
|
||||||
|
def test_security_bars_exact_layout():
|
||||||
|
"""验证 K 线请求包布局与旧版 working bytes 完全一致。"""
|
||||||
|
cmd = GetSecurityBarsCmd(Market.SH, "600000", KlineCategory.DAY, 0, 10)
|
||||||
|
req = cmd.build_request()
|
||||||
|
# Header: 0x010C, 0x01016408, 0x1C, 0x1C
|
||||||
|
# Payload: 0x052D, 1 (Market.SH), "600000", 4 (KlineCategory.DAY), 1, 0 (start), 10, 0, 0, 0
|
||||||
|
expected = struct.pack(
|
||||||
|
"<HIHHHH6sHHHHIIH",
|
||||||
|
0x010C, 0x01016408, 0x001C, 0x001C,
|
||||||
|
0x052D, 1, b"600000", 4, 1, 0, 10, 0, 0, 0
|
||||||
|
)
|
||||||
|
assert req == expected
|
||||||
|
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)
|
||||||
|
req = cmd.build_request()
|
||||||
|
# Header 12 + Payload 6 = 18
|
||||||
|
assert len(req) == 18
|
||||||
|
payload_len = struct.unpack("<H", req[6:8])[0]
|
||||||
|
assert payload_len == 6
|
||||||
|
|
||||||
|
|
||||||
|
def test_security_quotes_limit_mapping():
|
||||||
|
"""验证涨跌停价现在返回 None,且 pre_close 正确。"""
|
||||||
|
from xmtdx.codec.price import put_price
|
||||||
|
|
||||||
|
cmd = GetSecurityQuotesCmd([(Market.SH, "600000")])
|
||||||
|
|
||||||
|
# 构造响应报文
|
||||||
|
body = bytearray(b"\x00\x00")
|
||||||
|
body.extend(struct.pack("<H", 1))
|
||||||
|
|
||||||
|
# Record: Market(B), Code(6s), Active1(H) + ...
|
||||||
|
body.extend(struct.pack("<B6sH", 1, b"600000", 0))
|
||||||
|
|
||||||
|
body.extend(put_price(1010)) # price_raw
|
||||||
|
body.extend(put_price(-5)) # last_close_diff
|
||||||
|
body.extend(put_price(0))
|
||||||
|
body.extend(put_price(0))
|
||||||
|
body.extend(put_price(0))
|
||||||
|
body.extend(put_price(12345))
|
||||||
|
body.extend(put_price(-1010))
|
||||||
|
body.extend(put_price(100))
|
||||||
|
body.extend(put_price(10))
|
||||||
|
body.extend(struct.pack("<I", 10000))
|
||||||
|
body.extend(put_price(50))
|
||||||
|
body.extend(put_price(50))
|
||||||
|
body.extend(put_price(2))
|
||||||
|
body.extend(put_price(3))
|
||||||
|
for _ in range(20):
|
||||||
|
body.extend(put_price(0))
|
||||||
|
body.extend(struct.pack("<H", 0))
|
||||||
|
body.extend(put_price(96))
|
||||||
|
body.extend(put_price(-106))
|
||||||
|
body.extend(put_price(0))
|
||||||
|
body.extend(put_price(0))
|
||||||
|
body.extend(struct.pack("<hH", 0, 0))
|
||||||
|
|
||||||
|
quotes = cmd.parse_response(bytes(body))
|
||||||
|
q = quotes[0]
|
||||||
|
assert q.limit_up is None
|
||||||
|
assert q.limit_down is None
|
||||||
|
assert q.pre_close == 10.05
|
||||||
|
|
||||||
|
|
||||||
|
def test_compute_price_limits_for_stocks():
|
||||||
|
"""普通股票 / ST / 创业板 / 科创板 / 北交所规则应可正确计算。"""
|
||||||
|
assert compute_price_limits(Market.SH, "600000", "浦发银行", 10.05) == (11.06, 9.05)
|
||||||
|
assert compute_price_limits(Market.SH, "603939", "ST益丰", 22.53) == (23.66, 21.4)
|
||||||
|
assert compute_price_limits(Market.SZ, "301269", "华大九天", 86.36) == (103.63, 69.09)
|
||||||
|
assert compute_price_limits(Market.SH, "688981", "中芯国际", 101.52) == (121.82, 81.22)
|
||||||
|
assert compute_price_limits(Market.BJ, "920002", "万达轴承", 84.36) == (109.67, 59.05)
|
||||||
|
|
||||||
|
|
||||||
|
def test_compute_price_limits_for_indices():
|
||||||
|
"""指数与板块类代码不应计算涨跌停。"""
|
||||||
|
assert compute_price_limits(Market.SH, "999999", "上证指数", 4026.63) == (None, None)
|
||||||
|
assert compute_price_limits(Market.SH, "880005", "涨跌家数", 1841.0) == (None, None)
|
||||||
|
assert compute_price_limits(Market.SZ, "399001", "深证成指", 10412.63) == (None, None)
|
||||||
Reference in New Issue
Block a user