fix: resolve all CI mypy (265→0) and ruff (26→0) errors

- pyproject.toml: add mypy overrides for pandas/tabulate/matplotlib stubs,
  disable strict checking for vendored MyTT library
- config.py: use cast() for dict[str, Any] .get() returns
- beichi.py: widen _calc_bi_force param to BI | XD, import XD
- backtest/cli.py: split combo/single strategy into separate typed variables
- backtest/combo.py: add bool_array() helper for numpy return types
- chanlun/analyser.py: type ignore for pandas row access, fix dict type arg
- unified.py: change fields param from object to Any
- ex/mac_client.py: add type args to list literals
- cli/cmd_offline.py: wrap int market as Market enum before API call
- cli/cmd_chanlun.py: fix dict type arg
- offline/write_*.py: explicit int() cast for struct.unpack returns
- MyTT.py: fix line-too-long comments, UP038 isinstance syntax
- tests: fix E712 (==False → ~mask), E741 (noqa), F841, import sorting
- ruff format applied across codebase

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
GitHub
2026-06-10 15:03:41 +08:00
co-authored by Claude Opus 4.8
parent 5aac7d3a39
commit 4dfd18050e
51 changed files with 548 additions and 335 deletions
+4 -3
View File
@@ -4,7 +4,8 @@
# V2.1 2021-6-6 新增 BARSLAST函数 SLOPE,FORCAST线性回归预测函数
# V2.3 2021-6-13 新增 TRIX,DPO,BRAR,DMA,MTM,MASS,ROC,VR,ASI等指标
# V2.4 2021-6-27 新增 EXPMA,OBV,MFI指标, 改进SMA核心函数(核心函数彻底无循环)
# V2.7 2021-11-21 修正 SLOPE,BARSLAST,函数,新加FILTER,LONGCROSS, 感谢qzhjiang对SLOPE,SMA等函数的指正
# V2.7 2021-11-21 修正 SLOPE,BARSLAST,函数,新加FILTER,LONGCROSS,
# 感谢qzhjiang对SLOPE,SMA等函数的指正
# V2.8 2021-11-23 修正 FORCAST,WMA函数,欢迎qzhjiang,stanene,bcq加入社群,一起来完善myTT库
# V2.9 2021-11-29 新增 HHVBARS,LLVBARS,CONST, VALUEWHEN功能函数
# V2.92 2021-11-30 新增 BARSSINCEN函数,现在可以 pip install MyTT 完成安装
@@ -128,7 +129,7 @@ def WMA(S, N): # 通达信S序列的N日加权移动平均 Yn = (1*X1+2*X2+3*X3
def DMA(S, A): # 求S的动态移动平均,A作平滑因子,必须 0<A<1 (此为核心函数,非指标)
if isinstance(A, (int, float)):
if isinstance(A, int | float):
return pd.Series(S).ewm(alpha=A, adjust=False).mean().values
A = np.array(A)
A[np.isnan(A)] = 1.0
@@ -164,7 +165,7 @@ def LAST(S, A, B): # 从前A日到前B日一直满足S_BOOL条件, 要求A>B &
)
# ------------------ 1级:应用层函数(通过0级核心函数实现)使用方法请参考通达信--------------------------------
# -- 1级:应用层函数(通过0级核心函数实现)使用方法请参考通达信 --------------------
def COUNT(S, N): # COUNT(CLOSE>O, N): 最近N天满足S_BOO的天数 True的天数
return SUM(S, N)
+10 -11
View File
@@ -79,16 +79,15 @@ def backtest(
# 1. 加载策略(单策略 or 多因子组合)
is_combo = combo_strategies is not None
strategy = None
if is_combo:
strategy = _load_combo_strategies(combo_strategies)
assert combo_strategies is not None # narrowed by is_combo
combo_classes = _load_combo_strategies(combo_strategies)
else:
strategy = _load_strategy(strategy_str, strategy_file)
if strategy is None:
click.echo("错误: 必须指定 --strategy-file / --combo-strategies / --strategy", err=True)
raise SystemExit(1)
strategy_cls = _load_strategy(strategy_str, strategy_file)
if strategy_cls is None:
click.echo("错误: 必须指定 --strategy-file / --combo-strategies / --strategy", err=True)
raise SystemExit(1)
# 2. 获取数据
mkt = parse_market(market)
@@ -111,21 +110,21 @@ def backtest(
if is_combo:
from ..backtest.combo import CombinationRunner
assert strategy is not None # for type checker
runner = CombinationRunner(
strategy_classes=strategy,
strategy_classes=combo_classes,
df=df,
cash=cash,
commission=commission,
execution=execution,
)
result = runner.run_combination(
indices=list(range(len(strategy))),
indices=list(range(len(combo_classes))),
mode=combo_mode.upper(),
)
else:
assert strategy_cls is not None # guarded above by SystemExit
engine = BacktestEngine(
strategy=strategy,
strategy=strategy_cls,
cash=cash,
commission=commission,
execution=execution,
+9 -4
View File
@@ -39,6 +39,11 @@ NDArray = np.ndarray
BoolArray = npt.NDArray[np.bool_]
def bool_array(x: Any) -> BoolArray:
"""Ensure the result is a BoolArray (not a scalar bool_)."""
return np.asarray(x, dtype=np.bool_)
# ── 数据结构 ────────────────────────────────────────────────────────────────
@@ -200,15 +205,15 @@ def combine_masks(
sell_stack = np.stack(sell_arrays)
if mode == "AND":
return np.all(buy_stack, axis=0), np.all(sell_stack, axis=0)
return bool_array(np.all(buy_stack, axis=0)), bool_array(np.all(sell_stack, axis=0))
elif mode == "OR":
return np.any(buy_stack, axis=0), np.any(sell_stack, axis=0)
return bool_array(np.any(buy_stack, axis=0)), bool_array(np.any(sell_stack, axis=0))
elif mode == "MAJORITY":
n_factors = len(signals_list)
threshold = n_factors / 2
return (
np.sum(buy_stack, axis=0) > threshold,
np.sum(sell_stack, axis=0) > threshold,
bool_array(np.sum(buy_stack, axis=0) > threshold),
bool_array(np.sum(sell_stack, axis=0) > threshold),
)
else:
raise ValueError(f"不支持的合并模式: {mode!r}(可选: AND, OR, MAJORITY")
+6 -5
View File
@@ -8,6 +8,7 @@ K线合并 → 分型识别 → 笔计算 → 中枢计算 → 线段 → 买卖
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
import pandas as pd
@@ -38,10 +39,10 @@ def _df_to_klines(df: pd.DataFrame) -> list[Kline]:
Kline(
index=i,
date=dt,
open=float(row.open),
close=float(row.close),
high=float(row.high),
low=float(row.low),
open=float(row.open), # type: ignore[arg-type]
close=float(row.close), # type: ignore[arg-type]
high=float(row.high), # type: ignore[arg-type]
low=float(row.low), # type: ignore[arg-type]
amount=float(vol),
)
)
@@ -64,7 +65,7 @@ class ChanlunResult:
bcs: list[BC] = field(default_factory=list)
macd: dict[str, list[float]] = field(default_factory=dict)
def to_dict(self) -> dict:
def to_dict(self) -> dict[str, Any]:
"""将结果转为可序列化的字典(用于 JSON 输出)。"""
return {
"code": self.code,
+2 -2
View File
@@ -9,7 +9,7 @@
from __future__ import annotations
from easy_tdx.chanlun.config import ChanlunConfig
from easy_tdx.chanlun.types import BC, BI, ZS, BCType
from easy_tdx.chanlun.types import BC, BI, XD, ZS, BCType
def check_bi_beichi(
@@ -165,7 +165,7 @@ def _check_qs_beichi(bis: list[BI], zss: list[ZS]) -> list[BC]:
return bcs
def _calc_bi_force(bi: BI) -> float:
def _calc_bi_force(bi: BI | XD) -> float:
"""计算笔的力度(简化:用幅度表示)。
真正的力度应用 MACD 面积,这里用幅度作为简化替代。
+2 -1
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import json
from typing import Any
import click
@@ -65,7 +66,7 @@ def chanlun(
click.echo(json.dumps(result_dict, ensure_ascii=False))
def _print_table(result: dict) -> None:
def _print_table(result: dict[str, Any]) -> None:
"""以表格形式输出缠论分析结果。"""
click.echo(f"标的: {result['code']} 周期: {result['frequency']}")
click.echo(f"原始K线: {result['kline_count']} 缠论K线: {result['ckline_count']}")
+2 -1
View File
@@ -57,7 +57,8 @@ def kline(
mkt = parse_ex_market(market)
with get_mac_ex_client() as client:
df = client.goods_kline(
mkt, code,
mkt,
code,
period=parse_period(period),
start=start,
count=count,
+3 -2
View File
@@ -493,8 +493,9 @@ def _fetch_all_daily_bars(
Returns:
SecurityBar 列表(按日期升序)。
"""
from ..models.enums import KlineCategory
from ..models.enums import KlineCategory, Market
mkt = Market(market)
fetch_fn = client.get_index_bars if is_index else client.get_security_bars
all_bars: list[SecurityBar] = []
@@ -503,7 +504,7 @@ def _fetch_all_daily_bars(
max_pages = 50 if need_full else 1 # 50 页 = 40000 条,足够覆盖 A 股全部历史
for _ in range(max_pages):
df = fetch_fn(market, code, KlineCategory.DAY, start, page_size)
df = fetch_fn(mkt, code, KlineCategory.DAY, start, page_size)
if df.empty:
break
all_bars.extend(_df_to_bars(df))
+8 -1
View File
@@ -32,7 +32,14 @@ from .commands.security_list import GetSecurityListCmd
from .commands.security_quotes import GetSecurityQuotesCmd
from .commands.transaction import GetHistoryTransactionDataCmd, GetTransactionDataCmd
from .commands.xdxr_info import GetXdxrInfoCmd
from .config import get_best_host, get_calc_hosts, get_known_hosts, get_port, get_timeout, save_best_host
from .config import (
get_best_host,
get_calc_hosts,
get_known_hosts,
get_port,
get_timeout,
save_best_host,
)
from .exceptions import TdxConnectionError
from .models.bar import SecurityBar
from .models.enums import KlineCategory, Market
+12 -10
View File
@@ -21,7 +21,7 @@ def parse_block_dat(data: bytes, filename: str = "") -> list["TdxBlock"]:
return []
pos = 384
(count,) = struct.unpack("<H", data[pos:pos+2])
(count,) = struct.unpack("<H", data[pos : pos + 2])
pos += 2
results: list[TdxBlock] = []
@@ -40,8 +40,8 @@ def parse_block_dat(data: bytes, filename: str = "") -> list["TdxBlock"]:
break
# 板块元数据 (9 字节名称 + 2 字节股票数 + 2 字节类型)
name_b = data[pos:pos+9]
stock_count, _type = struct.unpack("<HH", data[pos+9:pos+13])
name_b = data[pos : pos + 9]
stock_count, _type = struct.unpack("<HH", data[pos + 9 : pos + 13])
name = name_b.decode("gbk", errors="replace").strip("\x00")
# 股票代码区 (2800 字节,每只股票 7 字节)
@@ -51,17 +51,19 @@ def parse_block_dat(data: bytes, filename: str = "") -> list["TdxBlock"]:
actual_count = min(stock_count, 400)
for i in range(actual_count):
c_start = codes_start + i * 7
c_raw = data[c_start:c_start+7]
c_raw = data[c_start : c_start + 7]
code = c_raw.decode("ascii", errors="replace").strip("\x00")
if code:
codes.append(code)
results.append(TdxBlock(
name=name,
category=category,
count=stock_count,
codes=codes,
))
results.append(
TdxBlock(
name=name,
category=category,
count=stock_count,
codes=codes,
)
)
# 跳过整个 2813 字节的记录块
pos += 2813
+2 -7
View File
@@ -7,13 +7,10 @@
日线及以上(其余 category):4 字节 YYYYMMDD 整数
"""
from .._binary import unpack_from
def get_datetime_minute(
data: bytes | bytearray, pos: int
) -> tuple[int, int, int, int, int, int]:
def get_datetime_minute(data: bytes | bytearray, pos: int) -> tuple[int, int, int, int, int, int]:
"""解析分钟级时间戳(4 字节)。
Returns:
@@ -28,9 +25,7 @@ def get_datetime_minute(
return year, month, day, hour, minute, pos + 4
def get_datetime_day(
data: bytes | bytearray, pos: int
) -> tuple[int, int, int, int]:
def get_datetime_day(data: bytes | bytearray, pos: int) -> tuple[int, int, int, int]:
"""解析日期(4 字节 YYYYMMDD)。
Returns:
+3 -3
View File
@@ -25,9 +25,9 @@ _HEADER_FMT = "<IIIHH"
@dataclass(frozen=True)
class FrameHeader:
magic: int # 协议魔数,恒为 7654321
seq_id: int # ZipFlag(1B) + 请求 bytes 1-4 回显(3B)
method: int # 请求回显(1B) + 保留(1B) + Method(2B)
magic: int # 协议魔数,恒为 7654321
seq_id: int # ZipFlag(1B) + 请求 bytes 1-4 回显(3B)
method: int # 请求回显(1B) + 保留(1B) + Method(2B)
zipsize: int
unzipsize: int
+2 -1
View File
@@ -1,8 +1,9 @@
"""通达信行业配置文件 (tdxhy.cfg) 解析器。"""
def parse_tdxhy_cfg(content: bytes) -> dict[str, tuple[str, str]]:
"""解析 tdxhy.cfg 字节内容。
返回字典: { "code": (tdx_industry, sw_industry), ... }
"""
results = {}
+2 -3
View File
@@ -9,7 +9,6 @@
警告:此函数专为成交量设计,不可用于价格字段(pytdx Bug #3)。
"""
from .._binary import unpack_from
@@ -53,5 +52,5 @@ def _decode_volume(ivol: int) -> float:
def _pow2(exp: int) -> float:
if exp >= 0:
return float(1 << exp) if exp < 63 else 2.0 ** exp
return 1.0 / (1 << (-exp)) if -exp < 63 else 2.0 ** exp
return float(1 << exp) if exp < 63 else 2.0**exp
return 1.0 / (1 << (-exp)) if -exp < 63 else 2.0**exp
+16 -10
View File
@@ -39,12 +39,14 @@ class GetCompanyInfoCategoryCmd(BaseCommand[list[CompanyInfoCategory]]):
raw = b[:nul] if nul != -1 else b
return raw.decode("gbk", errors="replace")
results.append(CompanyInfoCategory(
name=_decode(name_b),
filename=_decode(filename_b),
start=start,
length=length,
))
results.append(
CompanyInfoCategory(
name=_decode(name_b),
filename=_decode(filename_b),
start=start,
length=length,
)
)
return results
@@ -52,9 +54,7 @@ class GetCompanyInfoCategoryCmd(BaseCommand[list[CompanyInfoCategory]]):
class GetCompanyInfoContentCmd(BaseCommand[str]):
"""按文件名、偏移、长度读取公司信息文本(GBK 编码)。"""
def __init__(
self, market: Market, code: str, filename: str, offset: int, length: int
) -> None:
def __init__(self, market: Market, code: str, filename: str, offset: int, length: int) -> None:
self.market = market
self.code = code.encode("utf-8")
self.filename = filename.encode("gbk")
@@ -66,7 +66,13 @@ class GetCompanyInfoContentCmd(BaseCommand[str]):
header = bytes.fromhex("0c07109c0001680068 00d002".replace(" ", ""))
return header + struct.pack(
"<H6sH80sIII",
int(self.market), self.code, 0, fname_padded, self.offset, self.length, 0,
int(self.market),
self.code,
0,
fname_padded,
self.offset,
self.length,
0,
)
def parse_response(self, body: bytes) -> str:
+34 -8
View File
@@ -31,15 +31,41 @@ class GetFinanceInfoCmd(BaseCommand[FinanceInfo]):
fields = struct.unpack(_FIN_FMT, slice_bytes(body, pos, _FIN_SIZE, "finance_info body"))
(
liutong_guben, province, industry, updated_date, ipo_date,
zong_guben, guojia_gu, faqiren_faren_gu, faren_gu, b_gu, h_gu, zhigong_gu,
zong_zichan, liudong_zichan, guding_zichan, wuxing_zichan,
liutong_guben,
province,
industry,
updated_date,
ipo_date,
zong_guben,
guojia_gu,
faqiren_faren_gu,
faren_gu,
b_gu,
h_gu,
zhigong_gu,
zong_zichan,
liudong_zichan,
guding_zichan,
wuxing_zichan,
gudong_renshu,
liudong_fuzhai, changqi_fuzhai, ziben_gongjijin, jing_zichan,
zhuying_shouru, zhuying_lirun, yingshou_zhangkuan, yingye_lirun,
touzi_shouyu, jingying_xianjinliu, zong_xianjinliu,
cunhuo, lirun_zonghe, shuihou_lirun, jing_lirun, weifen_lirun,
meigujing_zichan, reserve2,
liudong_fuzhai,
changqi_fuzhai,
ziben_gongjijin,
jing_zichan,
zhuying_shouru,
zhuying_lirun,
yingshou_zhangkuan,
yingye_lirun,
touzi_shouyu,
jingying_xianjinliu,
zong_xianjinliu,
cunhuo,
lirun_zonghe,
shuihou_lirun,
jing_lirun,
weifen_lirun,
meigujing_zichan,
reserve2,
) = fields
_SCALE = 10000.0 # 财务数据单位:万元/万股
+22 -18
View File
@@ -41,35 +41,39 @@ class GetHistoryFundFlowCmd(BaseCommand[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_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]),
))
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
+2 -2
View File
@@ -39,10 +39,10 @@ class GetSecurityListCmd(BaseCommand[list[SecurityInfo]]):
code_bytes,
volunit,
name_bytes,
_unknown1, # 4字节,排序/分组字段(非用户可见数据)
_unknown1, # 4字节,排序/分组字段(非用户可见数据)
decimal_point,
pre_close_raw,
_unknown2, # 4字节,私有时间戳(非用户可见数据)
_unknown2, # 4字节,私有时间戳(非用户可见数据)
) = struct.unpack("<6sH8s4sBI4s", raw)
code = code_bytes.decode("utf-8", errors="replace").rstrip("\x00")
+1 -4
View File
@@ -9,10 +9,7 @@ from typing import Final
SETUP_CMD1: Final[bytes] = bytes.fromhex("0c0218930001030003000d0001")
SETUP_CMD2: Final[bytes] = bytes.fromhex("0c0218940001030003000d0002")
SETUP_CMD3: Final[bytes] = bytes.fromhex(
"0c031899000120002000db0f"
"d5d0c9ccd6a4a8af0000008f"
"c22540130000d500c9ccbdf0"
"d7ea00000002"
"0c031899000120002000db0fd5d0c9ccd6a4a8af0000008fc22540130000d500c9ccbdf0d7ea00000002"
)
SETUP_COMMANDS: Final[tuple[bytes, ...]] = (SETUP_CMD1, SETUP_CMD2, SETUP_CMD3)
+24 -16
View File
@@ -26,8 +26,6 @@ class GetTransactionDataCmd(BaseCommand[list[TransactionRecord]]):
header = bytes.fromhex("0c170801010 10e000e00c50f".replace(" ", ""))
return header + struct.pack("<H6sHH", int(self.market), self.code, self.start, self.count)
def parse_response(self, body: bytes) -> list[TransactionRecord]:
return _parse_transaction_body(body)
@@ -35,9 +33,7 @@ class GetTransactionDataCmd(BaseCommand[list[TransactionRecord]]):
class GetHistoryTransactionDataCmd(BaseCommand[list[TransactionRecord]]):
"""获取历史某日逐笔成交(date 格式 YYYYMMDD,分页)。"""
def __init__(
self, market: Market, code: str, date: int, start: int, count: int = 800
) -> None:
def __init__(self, market: Market, code: str, date: int, start: int, count: int = 800) -> None:
self.market = market
self.code = code.encode("utf-8")
self.date = date
@@ -72,11 +68,17 @@ def _parse_transaction_body(body: bytes) -> list[TransactionRecord]:
buyorsell, pos = get_price(body, pos)
unknown_last, pos = get_price(body, pos) # Bug #4 修复:不再丢弃
last_price += price_diff
records.append(TransactionRecord(
hour=hour, minute=minute,
price=last_price / 100.0, vol=vol, buyorsell=buyorsell,
unknown_last=unknown_last, _raw=body[record_start:pos],
))
records.append(
TransactionRecord(
hour=hour,
minute=minute,
price=last_price / 100.0,
vol=vol,
buyorsell=buyorsell,
unknown_last=unknown_last,
_raw=body[record_start:pos],
)
)
return records
@@ -93,13 +95,19 @@ def _parse_history_transaction_body(body: bytes) -> list[TransactionRecord]:
hour, minute, pos = get_time(body, pos)
price_diff, pos = get_price(body, pos)
vol, pos = get_price(body, pos)
buyorsell, pos = get_price(body, pos) # 历史无 num_orders
buyorsell, pos = get_price(body, pos) # 历史无 num_orders
unknown_last, pos = get_price(body, pos)
last_price += price_diff
records.append(TransactionRecord(
hour=hour, minute=minute,
price=last_price / 100.0, vol=vol, buyorsell=buyorsell,
unknown_last=unknown_last, _raw=body[record_start:pos],
))
records.append(
TransactionRecord(
hour=hour,
minute=minute,
price=last_price / 100.0,
vol=vol,
buyorsell=buyorsell,
unknown_last=unknown_last,
_raw=body[record_start:pos],
)
)
return records
+12 -12
View File
@@ -27,7 +27,7 @@ import json
import os
from datetime import datetime
from pathlib import Path
from typing import Any
from typing import Any, cast
_CONFIG_DIR = Path(os.environ.get("EASY_TDX_CONFIG_DIR", str(Path.home() / ".easy_tdx")))
_CONFIG_FILE = _CONFIG_DIR / "config.json"
@@ -137,7 +137,7 @@ _FALLBACK_TIMEOUT = 15.0
def _load() -> dict[str, Any]:
try:
if _CONFIG_FILE.exists():
return json.loads(_CONFIG_FILE.read_text("utf-8"))
return cast(dict[str, Any], json.loads(_CONFIG_FILE.read_text("utf-8")))
except Exception:
pass
return {}
@@ -161,7 +161,7 @@ def get_best_host() -> str:
if env:
return env
cfg = _load()
return cfg.get("best_host", _FALLBACK_HOSTS[0])
return cast(str, cfg.get("best_host", _FALLBACK_HOSTS[0]))
def get_known_hosts() -> list[str]:
@@ -170,25 +170,25 @@ def get_known_hosts() -> list[str]:
if env:
return [h.strip() for h in env.split(",") if h.strip()]
cfg = _load()
return cfg.get("known_hosts", list(_FALLBACK_HOSTS))
return cast(list[str], cfg.get("known_hosts", list(_FALLBACK_HOSTS)))
def get_calc_hosts() -> list[str]:
"""返回计算服务器列表。"""
cfg = _load()
return cfg.get("calc_hosts", list(_FALLBACK_CALC_HOSTS))
return cast(list[str], cfg.get("calc_hosts", list(_FALLBACK_CALC_HOSTS)))
def get_mac_hosts() -> list[str]:
"""返回 MAC 行情服务器列表。"""
cfg = _load()
return cfg.get("mac_hosts", list(_FALLBACK_MAC_HOSTS))
return cast(list[str], cfg.get("mac_hosts", list(_FALLBACK_MAC_HOSTS)))
def get_ex_hosts() -> list[str]:
"""返回扩展行情服务器列表。"""
cfg = _load()
return cfg.get("ex_hosts", list(_FALLBACK_EX_HOSTS))
return cast(list[str], cfg.get("ex_hosts", list(_FALLBACK_EX_HOSTS)))
def get_best_ex_host() -> str:
@@ -197,13 +197,13 @@ def get_best_ex_host() -> str:
if env:
return env
cfg = _load()
return cfg.get("best_ex_host", _FALLBACK_EX_HOSTS[0])
return cast(str, cfg.get("best_ex_host", _FALLBACK_EX_HOSTS[0]))
def get_mac_ex_hosts() -> list[str]:
"""返回 MAC 协议扩展行情服务器列表。"""
cfg = _load()
return cfg.get("mac_ex_hosts", list(_FALLBACK_MAC_EX_HOSTS))
return cast(list[str], cfg.get("mac_ex_hosts", list(_FALLBACK_MAC_EX_HOSTS)))
def get_best_mac_ex_host() -> str:
@@ -212,7 +212,7 @@ def get_best_mac_ex_host() -> str:
if env:
return env
cfg = _load()
return cfg.get("best_mac_ex_host", _FALLBACK_MAC_EX_HOSTS[0])
return cast(str, cfg.get("best_mac_ex_host", _FALLBACK_MAC_EX_HOSTS[0]))
def get_port() -> int:
@@ -221,7 +221,7 @@ def get_port() -> int:
if env:
return int(env)
cfg = _load()
return cfg.get("port", _FALLBACK_PORT)
return cast(int, cfg.get("port", _FALLBACK_PORT))
def get_timeout() -> float:
@@ -230,7 +230,7 @@ def get_timeout() -> float:
if env:
return float(env)
cfg = _load()
return cfg.get("timeout", _FALLBACK_TIMEOUT)
return cast(float, cfg.get("timeout", _FALLBACK_TIMEOUT))
# ---------------------------------------------------------------------------
+14 -12
View File
@@ -12,18 +12,20 @@ _MSG_ID = 0x2454
_HEAD_FLAG = 0x01
# 80 字节 Login body,来自 opentdx 参考实现,已通过实际测试验证。
_LOGIN_BODY = bytes(bytearray.fromhex(
"e5bb1c2fafe52594"
"1f32c6e5d53dfb41"
"5b734cc9cdbf0ac9"
"2021bfdd1eb06d22"
"d008884c1611cb13"
"78f6abd824d899d2"
"1f32c6e5d53dfb41"
"1f32c6e5d53dfb41"
"a9325ac935dc0837"
"335a16e4ce17c1bb"
))
_LOGIN_BODY = bytes(
bytearray.fromhex(
"e5bb1c2fafe52594"
"1f32c6e5d53dfb41"
"5b734cc9cdbf0ac9"
"2021bfdd1eb06d22"
"d008884c1611cb13"
"78f6abd824d899d2"
"1f32c6e5d53dfb41"
"1f32c6e5d53dfb41"
"a9325ac935dc0837"
"335a16e4ce17c1bb"
)
)
# EX 协议帧头格式: head_flag(1B) + customize(4B) + version(1B) + zipsize(2B) + unzipsize(2B)
_EX_HEADER_FMT = "<BIBHH"
+9 -7
View File
@@ -13,18 +13,18 @@ import pandas as pd
from .._df import _to_df
from ..commands.base import BaseCommand
from ..config import get_best_mac_ex_host, get_mac_ex_hosts, save_best_mac_ex_host
from ..exceptions import TdxConnectionError
from .commands.login import MacExLoginCmd
from .commands.get_instrument_count import GetExInstrumentCountCmd
from .commands.get_instrument_info import GetExInstrumentInfoCmd
from ..mac.commands.chart_sampling import ChartSamplingCmd
from ..mac.commands.symbol_bar import SymbolBarCmd
from ..mac.commands.symbol_quotes import SymbolQuotesCmd
from ..mac.commands.symbol_tick_chart import SymbolTickChartCmd
from ..mac.commands.symbol_transaction import SymbolTransactionCmd
from ..mac.enums import Adjust, Period, SortOrder, SortType
from ..config import get_best_mac_ex_host, get_mac_ex_hosts, save_best_mac_ex_host
from ..mac.models import MacQuoteField
from .commands.get_instrument_count import GetExInstrumentCountCmd
from .commands.get_instrument_info import GetExInstrumentInfoCmd
from .commands.login import MacExLoginCmd
from .transport.async_ import AsyncExTdxConnection
from .transport.sync import ExTdxConnection, ping_ex_all
@@ -194,7 +194,7 @@ class MacExClient:
return pd.DataFrame()
total = self._execute(GetExInstrumentCountCmd())
page_size = 1000
collected: list = []
collected: list[Any] = []
skipped = 0
pos = offset
while pos < total and len(collected) < count:
@@ -532,7 +532,9 @@ class AsyncMacExClient:
if not self._auto_reconnect:
raise
await self._conn.close()
self._conn = AsyncExTdxConnection(self._host, self._port, self._timeout, mac_ex_mode=True)
self._conn = AsyncExTdxConnection(
self._host, self._port, self._timeout, mac_ex_mode=True
)
await self._conn.connect()
await self._login()
return await self._conn.execute(cmd)
@@ -571,7 +573,7 @@ class AsyncMacExClient:
return pd.DataFrame()
total = await self._execute(GetExInstrumentCountCmd())
page_size = 1000
collected: list = []
collected: list[Any] = []
skipped = 0
pos = offset
while pos < total and len(collected) < count:
+1 -2
View File
@@ -5,9 +5,8 @@ from types import TracebackType
from typing import TYPE_CHECKING, TypeVar
from ...codec.frame import HEADER_SIZE, decompress_body, parse_header
from ...config import get_best_ex_host, get_ex_hosts
from ...config import get_best_ex_host
from ...exceptions import TdxConnectionError
from ..models import KNOWN_EX_HOSTS
if TYPE_CHECKING:
from ...commands.base import BaseCommand
-1
View File
@@ -10,7 +10,6 @@ from ...codec.frame import HEADER_SIZE, decompress_body, parse_header
from ...config import get_best_ex_host, get_ex_hosts
from ...exceptions import TdxConnectionError
from ..commands.get_instrument_count import GetExInstrumentCountCmd
from ..models import KNOWN_EX_HOSTS
if TYPE_CHECKING:
from ...commands.base import BaseCommand
-1
View File
@@ -6,7 +6,6 @@ import warnings
from collections.abc import Callable
from dataclasses import dataclass
import numpy as np
import pandas as pd
from . import MyTT
+5 -1
View File
@@ -1189,7 +1189,11 @@ class AsyncMacClient:
fetch_count = max(120 + count, 200)
df = await self.get_stock_kline(
market, code, period=period, count=fetch_count, adjust=adjust,
market,
code,
period=period,
count=fetch_count,
adjust=adjust,
)
if df.empty:
return df
+4 -1
View File
@@ -59,7 +59,10 @@ class GoodsListCmd(BaseCommand[list[GoodsItem]]):
offset = 2 + i * _RECORD_SIZE
require_bytes(body, offset, _RECORD_SIZE, f"GoodsListCmd record[{i}]")
category, raw_name, u, index, switch, v1, v2, v3, c1, c2 = unpack_from(
_RECORD_FMT, body, offset, f"GoodsListCmd record[{i}]",
_RECORD_FMT,
body,
offset,
f"GoodsListCmd record[{i}]",
)
name = raw_name.decode("gbk", errors="replace").rstrip("\x00")
items.append(
+1 -3
View File
@@ -131,9 +131,7 @@ class UnusualCmd(BaseCommand[list[UnusualItem]]):
desc, value = _describe_unusual(unusual_type, body[offset + 15 : offset + 28])
hour, minute_sec = unpack_from(
"<BH", body, offset + 29, f"unusual time[{i}]"
)
hour, minute_sec = unpack_from("<BH", body, offset + 29, f"unusual time[{i}]")
results.append(
UnusualItem(
+1 -1
View File
@@ -11,7 +11,7 @@ class SecurityBar:
close: float
high: float
low: float
vol: float # 成交量(股)
vol: float # 成交量(股)
amount: float # 成交额(元)
year: int
+55 -55
View File
@@ -18,27 +18,27 @@ class XdxrRecord:
year: int
month: int
day: int
category: int # 事件类型(见下方 CATEGORY_NAMES
name: str # 事件类型名称
category: int # 事件类型(见下方 CATEGORY_NAMES
name: str # 事件类型名称
# category == 1(除权除息)
fenhong: float | None = None # 每股分红(元;协议原值按每10股)
peigujia: float | None = None # 配股价(元/股)
fenhong: float | None = None # 每股分红(元;协议原值按每10股)
peigujia: float | None = None # 配股价(元/股)
songzhuangu: float | None = None # 每股送转股比例(协议原值按每10股)
peigu: float | None = None # 每股配股比例(协议原值按每10股)
peigu: float | None = None # 每股配股比例(协议原值按每10股)
# category in [11, 12](扩缩股)
suogu: float | None = None # 缩股比例
suogu: float | None = None # 缩股比例
# category in [13, 14](权证)
xingquanjia: float | None = None # 行权价
fenshu: float | None = None # 分数
fenshu: float | None = None # 分数
# category in [2..10](股本变动类,单位:万股)
panqian_liutong: float | None = None # 盘前流通股本(万股)
panhou_liutong: float | None = None # 盘后流通股本(万股)
qian_zongguben: float | None = None # 前总股本(万股)
hou_zongguben: float | None = None # 后总股本(万股)
panqian_liutong: float | None = None # 盘前流通股本(万股)
panhou_liutong: float | None = None # 盘后流通股本(万股)
qian_zongguben: float | None = None # 前总股本(万股)
hou_zongguben: float | None = None # 后总股本(万股)
_raw: bytes = field(default=b"", repr=False, compare=False)
@@ -69,45 +69,45 @@ class FinanceInfo:
code: str
# 股本(万股)
liutong_guben: float # 流通股本
zong_guben: float # 总股本
guojia_gu: float # 国家股
liutong_guben: float # 流通股本
zong_guben: float # 总股本
guojia_gu: float # 国家股
faqiren_faren_gu: float # 发起人法人股
faren_gu: float # 法人股
b_gu: float # B股
h_gu: float # H股
zhigong_gu: float # 职工股
faren_gu: float # 法人股
b_gu: float # B股
h_gu: float # H股
zhigong_gu: float # 职工股
# 基本信息
province: int # 所属省份代码
industry: int # 所属行业代码
updated_date: int # 财务更新日期 YYYYMMDD
ipo_date: int # 上市日期 YYYYMMDD
gudong_renshu: float # 股东人数
province: int # 所属省份代码
industry: int # 所属行业代码
updated_date: int # 财务更新日期 YYYYMMDD
ipo_date: int # 上市日期 YYYYMMDD
gudong_renshu: float # 股东人数
# 资产负债(元)
zong_zichan: float # 总资产
liudong_zichan: float # 流动资产
guding_zichan: float # 固定资产
wuxing_zichan: float # 无形资产
liudong_fuzhai: float # 流动负债
changqi_fuzhai: float # 长期负债
ziben_gongjijin: float # 资本公积金
jing_zichan: float # 净资产
zong_zichan: float # 总资产
liudong_zichan: float # 流动资产
guding_zichan: float # 固定资产
wuxing_zichan: float # 无形资产
liudong_fuzhai: float # 流动负债
changqi_fuzhai: float # 长期负债
ziben_gongjijin: float # 资本公积金
jing_zichan: float # 净资产
# 利润(元)
zhuying_shouru: float # 主营收入
zhuying_lirun: float # 主营利润
zhuying_shouru: float # 主营收入
zhuying_lirun: float # 主营利润
yingshou_zhangkuan: float # 应收账款
yingye_lirun: float # 营业利润
touzi_shouyu: float # 投资收益
yingye_lirun: float # 营业利润
touzi_shouyu: float # 投资收益
jingying_xianjinliu: float # 经营现金流
zong_xianjinliu: float # 总现金流
cunhuo: float # 存货
lirun_zonghe: float # 利润总额
shuihou_lirun: float # 税后利润
jing_lirun: float # 净利润
weifen_lirun: float # 未分配利润
zong_xianjinliu: float # 总现金流
cunhuo: float # 存货
lirun_zonghe: float # 利润总额
shuihou_lirun: float # 税后利润
jing_lirun: float # 净利润
weifen_lirun: float # 未分配利润
# 每股指标
meigujing_zichan: float # 每股净资产(原 baoliu1
@@ -122,36 +122,36 @@ class FinanceInfo:
class CompanyInfoCategory:
"""公司信息文件目录条目"""
name: str = "" # 目录名(如“最新提示”)
name: str = "" # 目录名(如“最新提示”)
filename: str = "" # 文件名(如 '600000.txt'
start: int = 0 # 内容起始偏移
length: int = 0 # 内容长度(字节)
start: int = 0 # 内容起始偏移
length: int = 0 # 内容长度(字节)
@dataclass
class FinancialFileInfo:
"""财报 zip 文件索引条目(来自 tdxfin/gpcw.txt)。"""
filename: str # "gpcw20260331.zip"
hash: str # MD5 hex digest
filesize: int # 字节
filename: str # "gpcw20260331.zip"
hash: str # MD5 hex digest
filesize: int # 字节
@dataclass
class FinancialRecord:
"""单只股票的一期历史专业财报记录。"""
code: str # 6 位股票代码
market: Market # 市场
report_date: int # 报告期 YYYYMMDD
fields: list[float] # N 个浮点字段(N = report_size / 4
code: str # 6 位股票代码
market: Market # 市场
report_date: int # 报告期 YYYYMMDD
fields: list[float] # N 个浮点字段(N = report_size / 4
@dataclass
class TdxBlock:
"""通达信板块信息(行业、概念、风格等)"""
name: str # 板块名称(如“房地产”)
category: int # 板块分类(0=行业, 1=地域, 2=概念, 3=风格, 等)
count: int # 板块包含股票数量
codes: list[str] # 股票代码列表(6位数字代码)
name: str # 板块名称(如“房地产”)
category: int # 板块分类(0=行业, 1=地域, 2=概念, 3=风格, 等)
count: int # 板块包含股票数量
codes: list[str] # 股票代码列表(6位数字代码)
+4 -4
View File
@@ -11,13 +11,13 @@ class SecurityInfo:
market: Market
code: str
name: str # 股票名称(GBK 解码,截断字节用 replacement char 替代)
volunit: int # 成交量单位(手 = volunit 股)
name: str # 股票名称(GBK 解码,截断字节用 replacement char 替代)
volunit: int # 成交量单位(手 = volunit 股)
decimal_point: int # 价格小数位数
pre_close: float # 昨收价(通达信自定义浮点解码)
pre_close: float # 昨收价(通达信自定义浮点解码)
# 扩展字段(通过 get_security_list_all 关联 tdxhy.cfg 获得)
industry_tdx: str = "" # 通达信行业代码 (如 T1001)
industry_sw: str = "" # 申万行业代码 (如 X500102)
industry_sw: str = "" # 申万行业代码 (如 X500102)
_raw: bytes = field(default=b"", repr=False, compare=False)
+25 -22
View File
@@ -1,58 +1,61 @@
"""验证市场概况模型。"""
from dataclasses import dataclass
@dataclass
class MarketStat:
"""全市场涨跌统计概况。"""
up_count: int # 上涨家数
down_count: int # 下跌家数
neutral_count: int # 平盘家数
suspended_count: int # 由 total-(up+down+neutral) 得到的残差项,近似表示停牌/未参与统计家数
total_count: int # 总计(包含停牌)
total_amount: float # 总成交额
total_volume: float # 总成交
total_market_cap: float # 总市值(亿元),来自 880001 收盘价,÷100 得万亿
limit_up_count: int # 涨停家数,来自 880006 close
limit_down_count: int # 停家数,来自 880006 open
up_count: int # 上涨家数
down_count: int # 下跌家数
neutral_count: int # 平盘家数
suspended_count: int # 由 total-(up+down+neutral) 得到的残差项,近似表示停牌/未参与统计家数
total_count: int # 总计(包含停牌)
total_amount: float # 总成交
total_volume: float # 总成交量
total_market_cap: float # 总市值(亿元),来自 880001 收盘价,÷100 得万亿
limit_up_count: int # 停家数,来自 880006 close
limit_down_count: int # 跌停家数,来自 880006 open
@dataclass
class FundFlow:
"""个股资金流向统计(基于 Tick 数据加权计算)。"""
# 流入项 (Buy)
super_in: float # 超大单流入 (>100万)
large_in: float # 大单流入 (>20万 且 <=100万)
super_in: float # 超大单流入 (>100万)
large_in: float # 大单流入 (>20万 且 <=100万)
medium_in: float # 中单流入 (>4万 且 <=20万)
small_in: float # 小单流入 (<=4万)
small_in: float # 小单流入 (<=4万)
# 流出项 (Sell)
super_out: float
large_out: float
medium_out: float
small_out: float
@property
def main_net_inflow(self) -> float:
"""主力净流入 (超大单 + 大单)。"""
return (self.super_in + self.large_in) - (self.super_out + self.large_out)
@property
def total_net_inflow(self) -> float:
"""全单净流入。"""
return (self.super_in + self.large_in + self.medium_in + self.small_in) - \
(self.super_out + self.large_out + self.medium_out + self.small_out)
return (self.super_in + self.large_in + self.medium_in + self.small_in) - (
self.super_out + self.large_out + self.medium_out + self.small_out
)
@dataclass
class HistoricalFundFlow:
"""历史日线资金流向条目。"""
year: int
month: int
day: int
# 金额项 (单位:元)
super_in: float
super_out: float
@@ -62,7 +65,7 @@ class HistoricalFundFlow:
medium_out: float
small_in: float
small_out: float
@property
def main_net_inflow(self) -> float:
"""当日主力净流入。"""
+1 -1
View File
@@ -69,7 +69,7 @@ def get_last_bar_date(filepath: str | Path) -> int | None:
f.seek(size - _DAILY_FMT.size)
last_record = f.read(_DAILY_FMT.size)
(date_int, *_) = _DAILY_FMT.unpack(last_record)
return date_int
return int(date_int)
def _bar_date_int(bar: SecurityBar) -> int:
+1 -1
View File
@@ -48,7 +48,7 @@ def get_last_ex_bar_date(filepath: str | Path) -> int | None:
f.seek(size - _EX_DAILY_FMT.size)
last_record = f.read(_EX_DAILY_FMT.size)
(date_int, *_) = _EX_DAILY_FMT.unpack(last_record)
return date_int
return int(date_int)
def _bar_date_int(bar: ExDailyBar) -> int:
+8 -1
View File
@@ -8,7 +8,14 @@ from typing import TYPE_CHECKING, TypeVar
from ..codec.frame import HEADER_SIZE, decompress_body, parse_header
from ..commands.setup import SETUP_COMMANDS
from ..config import get_best_host, get_calc_hosts, get_known_hosts, get_mac_hosts, get_port, get_timeout
from ..config import (
get_best_host,
get_calc_hosts,
get_known_hosts,
get_mac_hosts,
get_port,
get_timeout,
)
from ..exceptions import TdxConnectionError
if TYPE_CHECKING:
+20 -8
View File
@@ -95,7 +95,7 @@ class UnifiedTdxClient:
def get_stock_quotes(
self,
stocks: list[tuple[int, str]],
fields: object = None,
fields: Any = None,
) -> pd.DataFrame:
return self._ensure_mac().get_stock_quotes(stocks, fields)
@@ -107,7 +107,7 @@ class UnifiedTdxClient:
sort_type: SortType = SortType.CHANGE_PCT,
sort_order: SortOrder = SortOrder.DESC,
exclude_flags: list[FilterType] | None = None,
fields: object = None,
fields: Any = None,
) -> pd.DataFrame:
return self._ensure_mac().get_stock_quotes_list(
category, start, count, sort_type, sort_order, exclude_flags, fields
@@ -136,7 +136,13 @@ class UnifiedTdxClient:
params: dict[str, dict[str, int | float]] | None = None,
) -> pd.DataFrame:
return self._ensure_mac().get_stock_kline_with_indicators(
market, code, indicators, period, count, adjust, params,
market,
code,
indicators,
period,
count,
adjust,
params,
)
def get_tick_chart(
@@ -185,7 +191,7 @@ class UnifiedTdxClient:
count: int = 100000,
sort_type: SortType = SortType.CHANGE_PCT,
sort_order: SortOrder = SortOrder.DESC,
fields: object = None,
fields: Any = None,
exclude_flags: list[FilterType] | None = None,
) -> pd.DataFrame:
return self._ensure_mac().get_board_members(
@@ -378,7 +384,7 @@ class AsyncUnifiedTdxClient:
async def get_stock_quotes(
self,
stocks: list[tuple[int, str]],
fields: object = None,
fields: Any = None,
) -> pd.DataFrame:
mac = await self._ensure_mac()
return await mac.get_stock_quotes(stocks, fields)
@@ -391,7 +397,7 @@ class AsyncUnifiedTdxClient:
sort_type: SortType = SortType.CHANGE_PCT,
sort_order: SortOrder = SortOrder.DESC,
exclude_flags: list[FilterType] | None = None,
fields: object = None,
fields: Any = None,
) -> pd.DataFrame:
mac = await self._ensure_mac()
return await mac.get_stock_quotes_list(
@@ -423,7 +429,13 @@ class AsyncUnifiedTdxClient:
) -> pd.DataFrame:
mac = await self._ensure_mac()
return await mac.get_stock_kline_with_indicators(
market, code, indicators, period, count, adjust, params,
market,
code,
indicators,
period,
count,
adjust,
params,
)
async def get_tick_chart(
@@ -478,7 +490,7 @@ class AsyncUnifiedTdxClient:
count: int = 100000,
sort_type: SortType = SortType.CHANGE_PCT,
sort_order: SortOrder = SortOrder.DESC,
fields: object = None,
fields: Any = None,
exclude_flags: list[FilterType] | None = None,
) -> pd.DataFrame:
mac = await self._ensure_mac()