Files
easy_tdx_max/src/easy_tdx/commands/security_bars.py
T
GitHub e3e8dd492e fix: K线响应截断容错 + 一键寻优并发默认8进程
- security_bars: parse_response 遇末尾残缺记录时丢弃并返回前 N-1 条,
  避免 TDX 服务端截断响应导致整页 500(如 000408 count=800 日线)
- OptimizeView: 一键寻优并发默认 8 进程,用户选择持久化到 localStorage

bump version to 1.18.3
2026-07-06 13:23:37 +08:00

192 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""获取 K 线数据命令(支持全部周期)。"""
import logging
import struct
from .._binary import unpack_from
from ..codec.datetime_ import get_datetime
from ..codec.price import get_price
from ..codec.volume import get_volume
from ..exceptions import TdxDecodeError
from ..models.bar import SecurityBar
from ..models.enums import KlineCategory, Market
from .base import BaseCommand
_log = logging.getLogger(__name__)
class GetSecurityBarsCmd(BaseCommand[list[SecurityBar]]):
"""获取指定股票的 K 线数据。
Args:
market: 市场(SH/SZ
code: 6位股票代码(字符串)
category: K线周期
start: 起始行(0 = 最新;分页时递增)
count: 返回条数(最多 800
"""
def __init__(
self,
market: Market,
code: str,
category: KlineCategory,
start: int,
count: int = 800,
) -> None:
self.market = market
self.code = code.encode("utf-8")
self.category = category
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,
int(self.category),
1,
self.start,
self.count,
0,
0,
0,
)
def parse_response(self, body: bytes) -> list[SecurityBar]:
(ret_count,) = unpack_from("<H", body, 0, "security_bars header")
pos = 2
bars: list[SecurityBar] = []
pre_diff_base = 0
cat = int(self.category)
for i in range(ret_count):
record_start = pos
try:
year, month, day, hour, minute, pos = get_datetime(cat, body, pos)
open_diff, pos = get_price(body, pos)
close_diff, pos = get_price(body, pos)
high_diff, pos = get_price(body, pos)
low_diff, pos = get_price(body, pos)
vol, pos = get_volume(body, pos)
amount, pos = get_volume(body, pos)
except TdxDecodeError as e:
# TDX 服务端偶发截断:响应头声称有 N 条,但 body 末尾若干条
# 被切掉(停牌/退市/分页边界常见)。丢弃残缺的末条,保留已
# 成功解析的前若干条,避免一条坏数据让整页 500。
if bars:
_log.warning(
"K线响应在第 %d/%d 条处被截断(%s),已丢弃末尾残缺记录,"
"返回前 %d 条",
i + 1,
ret_count,
e,
len(bars),
)
return bars
raise
# 差分还原(与 pytdx 完全一致)
open_abs = open_diff + pre_diff_base
close_abs = open_abs + close_diff
high_abs = open_abs + high_diff
low_abs = open_abs + low_diff
pre_diff_base = open_abs + close_diff
bars.append(
SecurityBar(
open=open_abs / 1000.0,
close=close_abs / 1000.0,
high=high_abs / 1000.0,
low=low_abs / 1000.0,
vol=vol,
amount=amount,
year=year,
month=month,
day=day,
hour=hour,
minute=minute,
_raw=body[record_start:pos],
)
)
return bars
class GetIndexBarsCmd(GetSecurityBarsCmd):
"""获取指数 K 线。
请求格式与股票 K 线相同,但响应每条记录在 vol+amt 后多 4 字节
(上涨家数 uint16 + 下跌家数 uint16),必须跳过否则后续记录错位。
"""
def parse_response(self, body: bytes) -> list[SecurityBar]:
(ret_count,) = unpack_from("<H", body, 0, "security_bars header")
pos = 2
bars: list[SecurityBar] = []
pre_diff_base = 0
cat = int(self.category)
for i in range(ret_count):
record_start = pos
try:
year, month, day, hour, minute, pos = get_datetime(cat, body, pos)
open_diff, pos = get_price(body, pos)
close_diff, pos = get_price(body, pos)
high_diff, pos = get_price(body, pos)
low_diff, pos = get_price(body, pos)
vol, pos = get_volume(body, pos)
amount, pos = get_volume(body, pos)
# 指数记录额外 4 字节:上涨家数 + 下跌家数(各 uint16 LE
pos += 4
except TdxDecodeError as e:
if bars:
_log.warning(
"指数K线响应在第 %d/%d 条处被截断(%s),已丢弃末尾残缺记录,"
"返回前 %d 条",
i + 1,
ret_count,
e,
len(bars),
)
return bars
raise
# 差分还原(与 pytdx 完全一致)
open_abs = open_diff + pre_diff_base
close_abs = open_abs + close_diff
high_abs = open_abs + high_diff
low_abs = open_abs + low_diff
pre_diff_base = open_abs + close_diff
bars.append(
SecurityBar(
open=open_abs / 1000.0,
close=close_abs / 1000.0,
high=high_abs / 1000.0,
low=low_abs / 1000.0,
vol=vol,
amount=amount,
year=year,
month=month,
day=day,
hour=hour,
minute=minute,
_raw=body[record_start:pos],
)
)
return bars