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
+87
View File
@@ -0,0 +1,87 @@
{
"permissions": {
"allow": [
"Skill(update-config)",
"Bash(echo '{\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"D:\\\\\\\\python\\\\\\\\xmtdx\\\\\\\\src\\\\\\\\xmtdx\\\\\\\\__init__.py\"}}')",
"Bash(python -c \"import sys,json,subprocess;d=json.load\\(sys.stdin\\);f=d.get\\('tool_input',{}\\).get\\('file_path',''\\);exec\\('' if not f.endswith\\(\\\\\\\\'.py\\\\\\\\'\\) else 'subprocess.run\\([\\\\\\\\'ruff\\\\\\\\',\\\\\\\\'check\\\\\\\\',\\\\\\\\'--fix\\\\\\\\',f],capture_output=True\\);subprocess.run\\([\\\\\\\\'ruff\\\\\\\\',\\\\\\\\'format\\\\\\\\',f],capture_output=True\\)'\\);print\\(f'done: {f}'\\)\")",
"Bash(python *)",
"Bash(ruff check *)",
"Bash(git add *)",
"Bash(git commit *)",
"Bash(git push *)",
"mcp__zread__get_repo_structure",
"Bash(pip install *)",
"mcp__web-reader__webReader",
"Bash(python3 *)",
"Bash(where mypy *)",
"Bash(pip list *)",
"Bash(uv run *)",
"Bash(git stash *)",
"Bash(dir /s /b src\\\\xmtdx)",
"Bash(git remote *)",
"Bash(uv --version)",
"Bash(git rm *)",
"mcp__plugin_context7_context7__resolve-library-id",
"mcp__plugin_context7_context7__query-docs",
"Bash(grep -E \"\\\\.py$\")",
"Bash(awk '{added+=$1; deleted+=$2} END {print \"+\" added \" -\" deleted}')",
"Bash(find D:/python/easty_tdx/examples/1[1-7]* -name \"*.py\")",
"Bash(easy-tdx ping *)",
"Bash(easy-tdx version *)",
"Bash(easy-tdx kline *)",
"Bash(easy-tdx quote *)",
"Bash(easy-tdx tick *)",
"Bash(easy-tdx transaction *)",
"Bash(easy-tdx auction *)",
"Bash(easy-tdx quote-list *)",
"Bash(easy-tdx board-list *)",
"Bash(easy-tdx board-members *)",
"Bash(easy-tdx belong-board *)",
"Bash(easy-tdx capital-flow *)",
"Bash(echo \"EXIT CODE: $?\")",
"Bash(easy-tdx unusual *)",
"Bash(easy-tdx market-stat *)",
"Bash(easy-tdx server-info *)",
"Bash(easy-tdx symbol-info *)",
"Bash(easy-tdx ex *)",
"WebSearch",
"mcp__zread__search_doc",
"mcp__zread__read_file",
"Bash(git status *)",
"Bash(pip show *)",
"Bash(git tag *)",
"Bash(twine upload *)",
"Bash(pip index *)",
"Bash(curl -s https://pypi.org/pypi/easy-tdx/json)",
"Bash(curl -sI \"https://files.pythonhosted.org/packages/py3/e/easy-tdx/easy_tdx-1.2.0-py3-none-any.whl\")",
"Bash(gh run *)",
"Bash(easy-tdx *)",
"mcp__web-search-prime__web_search_prime",
"mcp__plugin_compound-engineering_context7__resolve-library-id",
"mcp__plugin_compound-engineering_context7__query-docs",
"Bash(pip uninstall *)",
"Bash(xargs grep -l \"Command\\\\|Request\\\\|Response\")",
"Bash(quotes/stock_xdxr_info.go)",
"Bash(mypy src/)",
"Bash(ruff format *)",
"mcp__plugin_episodic-memory_episodic-memory__search",
"Bash(echo \"FK/\")",
"Bash(git pull *)",
"mcp__playwright__browser_navigate",
"mcp__playwright__browser_snapshot",
"Bash(gh auth *)",
"Bash(rm -rf easy_tdx.wiki)",
"Bash(gh repo *)",
"Bash(gh api *)",
"Bash(mypy src/easy_tdx/backtest/types.py)",
"Bash(mypy tests/unit/test_backtest_types.py)",
"Bash(mypy src/easy_tdx/backtest/types.py tests/unit/test_backtest_types.py)",
"Bash(mypy *)",
"Bash(git diff *)",
"Bash(echo \"exit: $?\")",
"Bash(where easy-tdx *)",
"mcp__zai-mcp-server__analyze_image",
"Bash(git ls-remote *)"
]
}
}
+9
View File
@@ -23,6 +23,15 @@ packages = ["src/easy_tdx"]
strict = true
python_version = "3.10"
[[tool.mypy.overrides]]
module = ["pandas", "tabulate", "matplotlib", "matplotlib.*"]
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "easy_tdx.MyTT"
disallow_untyped_defs = false
disallow_untyped_calls = false
[tool.ruff]
target-version = "py310"
line-length = 100
+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)
+8 -9
View File
@@ -79,14 +79,13 @@ 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:
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)
@@ -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
+4 -2
View File
@@ -56,12 +56,14 @@ def parse_block_dat(data: bytes, filename: str = "") -> list["TdxBlock"]:
if code:
codes.append(code)
results.append(TdxBlock(
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:
+1
View File
@@ -1,5 +1,6 @@
"""通达信行业配置文件 (tdxhy.cfg) 解析器。"""
def parse_tdxhy_cfg(content: bytes) -> dict[str, tuple[str, str]]:
"""解析 tdxhy.cfg 字节内容。
-1
View File
@@ -9,7 +9,6 @@
警告:此函数专为成交量设计,不可用于价格字段(pytdx Bug #3)。
"""
from .._binary import unpack_from
+12 -6
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(
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 # 财务数据单位:万元/万股
+7 -3
View File
@@ -59,8 +59,11 @@ class GetHistoryFundFlowCmd(BaseCommand[list[HistoricalFundFlow]]):
month = (raw_date // 100) % 100
day = raw_date % 100
results.append(HistoricalFundFlow(
year=year, month=month, day=day,
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]),
@@ -69,7 +72,8 @@ class GetHistoryFundFlowCmd(BaseCommand[list[HistoricalFundFlow]]):
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
+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)
+23 -15
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
@@ -96,10 +98,16 @@ def _parse_history_transaction_body(body: bytes) -> list[TransactionRecord]:
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))
# ---------------------------------------------------------------------------
+4 -2
View File
@@ -12,7 +12,8 @@ _MSG_ID = 0x2454
_HEAD_FLAG = 0x01
# 80 字节 Login body,来自 opentdx 参考实现,已通过实际测试验证。
_LOGIN_BODY = bytes(bytearray.fromhex(
_LOGIN_BODY = bytes(
bytearray.fromhex(
"e5bb1c2fafe52594"
"1f32c6e5d53dfb41"
"5b734cc9cdbf0ac9"
@@ -23,7 +24,8 @@ _LOGIN_BODY = bytes(bytearray.fromhex(
"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(
+5 -2
View File
@@ -1,10 +1,12 @@
"""验证市场概况模型。"""
from dataclasses import dataclass
@dataclass
class MarketStat:
"""全市场涨跌统计概况。"""
up_count: int # 上涨家数
down_count: int # 下跌家数
neutral_count: int # 平盘家数
@@ -41,8 +43,9 @@ class FundFlow:
@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
+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()
+1
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import numpy as np
import pandas as pd
import pytest
from easy_tdx import MyTT
from easy_tdx.backtest.strategy import Strategy
+2 -4
View File
@@ -318,7 +318,7 @@ def test_position_aware_buy_sell_alternation():
engine = BacktestEngine(PositionAwareStrategy, cash=100000)
result = engine.run(df)
trades = result.trades[result.trades["rejected"] == False]
trades = result.trades[~result.trades["rejected"]]
directions = trades["direction"].tolist()
# Must have both BUYs and SELLs
@@ -339,9 +339,7 @@ def test_position_aware_no_duplicate_buys():
engine = BacktestEngine(PositionAwareStrategy, cash=100000)
result = engine.run(df)
buy_trades = result.trades[
(result.trades["direction"] == "BUY") & (result.trades["rejected"] == False)
]
buy_trades = result.trades[(result.trades["direction"] == "BUY") & (~result.trades["rejected"])]
# Each BUY's size should be reasonable (not tiny leftover from exhausted cash)
if len(buy_trades) > 1:
+20 -10
View File
@@ -33,12 +33,14 @@ def _make_equity_curve(n: int = 252, total_return: float = 0.1) -> pd.DataFrame:
drawdown = peak - total
drawdown_pct = np.divide(drawdown, peak, out=np.zeros_like(drawdown), where=(peak != 0))
return pd.DataFrame({
return pd.DataFrame(
{
"datetime": np.arange(n),
"total": total,
"drawdown": drawdown,
"drawdown_pct": drawdown_pct,
})
}
)
def _make_trades() -> pd.DataFrame:
@@ -48,11 +50,13 @@ def _make_trades() -> pd.DataFrame:
包含 direction, pnl, rejected 的 DataFrame
4 条交易: BUY@100, SELL@105(pnl=500), BUY@95, SELL@90(pnl=-500)
"""
return pd.DataFrame({
return pd.DataFrame(
{
"direction": ["BUY", "SELL", "BUY", "SELL"],
"pnl": [0, 500, 0, -500],
"rejected": [False, False, False, False],
})
}
)
def test_total_return() -> None:
@@ -70,20 +74,24 @@ def test_total_return() -> None:
def test_max_drawdown_never_exceeds_100_pct() -> None:
"""测试最大回撤永远不超过 100%(从峰值的跌幅)。"""
# 模拟先涨 5 倍再腰斩的资金曲线
total = np.concatenate([
total = np.concatenate(
[
np.linspace(100000, 600000, 126), # 涨到 60 万
np.linspace(600000, 300000, 126), # 跌到 30 万
])
]
)
peak = np.maximum.accumulate(total)
drawdown = peak - total
drawdown_pct = np.divide(drawdown, peak, out=np.zeros_like(drawdown), where=(peak != 0))
equity = pd.DataFrame({
equity = pd.DataFrame(
{
"datetime": np.arange(252),
"total": total,
"drawdown": drawdown,
"drawdown_pct": drawdown_pct,
})
}
)
trades = _make_trades()
analyzer = PerformanceAnalyzer(equity, trades)
@@ -287,11 +295,13 @@ def test_rejected_trades() -> None:
equity = _make_equity_curve(n=252, total_return=0.1)
# 创建包含被拒绝交易的记录
trades = pd.DataFrame({
trades = pd.DataFrame(
{
"direction": ["BUY", "SELL", "SELL", "SELL"],
"pnl": [0, 500, 0, -500],
"rejected": [False, False, True, False],
})
}
)
analyzer = PerformanceAnalyzer(equity, trades)
metrics = analyzer.compute()
+2 -2
View File
@@ -19,7 +19,7 @@ def _k(
o: float,
c: float,
h: float,
l: float,
l: float, # noqa: E741
a: float = 0.0,
) -> Kline:
"""快速构造 Kline。"""
@@ -40,7 +40,7 @@ def _ck(
o: float,
c: float,
h: float,
l: float,
l: float, # noqa: E741
merged_count: int = 1,
direction: str = "",
) -> CLKline:
+2 -2
View File
@@ -17,7 +17,7 @@ def _k(
o: float,
c: float,
h: float,
l: float,
l: float, # noqa: E741
a: float = 0.0,
) -> Kline:
return Kline(
@@ -37,7 +37,7 @@ def _ck(
o: float,
c: float,
h: float,
l: float,
l: float, # noqa: E741
merged_count: int = 1,
direction: str = "",
) -> CLKline:
+4 -7
View File
@@ -13,7 +13,7 @@ from easy_tdx.chanlun.types import CLKline, Kline
# ── helpers ──────────────────────────────────────────────────────────────
def _k(idx: int, dt: str, o: float, c: float, h: float, l: float, a: float = 0.0) -> Kline:
def _k(idx: int, dt: str, o: float, c: float, h: float, l: float, a: float = 0.0) -> Kline: # noqa: E741
return Kline(
index=idx,
date=datetime.strptime(dt, "%Y-%m-%d"),
@@ -55,9 +55,8 @@ class TestMultiLevel:
def test_multi_level_basic(self) -> None:
"""多级别分析应返回各级别结果。"""
from easy_tdx.chanlun.multi_level import MultiLevelAnalyser
from easy_tdx.chanlun.analyser import ChanlunAnalyser
from easy_tdx.chanlun.multi_level import MultiLevelAnalyser
df_daily = _make_df(100)
df_30min = _make_df(200)
@@ -77,9 +76,8 @@ class TestMultiLevel:
def test_multi_level_low_level_qs(self) -> None:
"""高级别笔对应的低级别趋势信息。"""
from easy_tdx.chanlun.multi_level import MultiLevelAnalyser
from easy_tdx.chanlun.analyser import ChanlunAnalyser
from easy_tdx.chanlun.multi_level import MultiLevelAnalyser
df_daily = _make_df(100)
df_30min = _make_df(200)
@@ -158,9 +156,8 @@ class TestZsd:
def test_zsd_from_xds(self) -> None:
"""线段应能组合为走势段。"""
from easy_tdx.chanlun.zsd import find_zsds
from easy_tdx.chanlun.xd import find_xds
from easy_tdx.chanlun.zsd import find_zsds
cks = [
CLKline(
+1 -3
View File
@@ -61,9 +61,7 @@ class TestParseFinancialDat:
index_entries: list[bytes] = []
for i, (code, market_byte, _) in enumerate(stocks):
index_entries.append(
struct.pack(
index_fmt, code.encode("ascii"), bytes([market_byte]), offsets[i]
)
struct.pack(index_fmt, code.encode("ascii"), bytes([market_byte]), offsets[i])
)
return header + b"".join(index_entries) + b"".join(data_chunks)
+1
View File
@@ -16,6 +16,7 @@ def test_heartbeat_sends_periodically():
# 记录调用次数
call_count = 0
async def mock_execute(cmd):
nonlocal call_count
call_count += 1
+5 -3
View File
@@ -8,7 +8,7 @@ import numpy as np
import pandas as pd
import pytest
from easy_tdx.indicator import compute_indicators, list_indicators, _REGISTRY
from easy_tdx.indicator import _REGISTRY, compute_indicators, list_indicators
def _make_ohlcv(n: int = 200, seed: int = 42) -> pd.DataFrame:
@@ -18,7 +18,8 @@ def _make_ohlcv(n: int = 200, seed: int = 42) -> pd.DataFrame:
low = close - np.abs(rng.standard_normal(n))
open_ = low + (high - low) * rng.random(n)
vol = (rng.random(n) * 1e6).astype(float)
return pd.DataFrame({
return pd.DataFrame(
{
"datetime": pd.date_range("2024-01-01", periods=n, freq="D"),
"open": open_,
"high": high,
@@ -26,7 +27,8 @@ def _make_ohlcv(n: int = 200, seed: int = 42) -> pd.DataFrame:
"close": close,
"vol": vol,
"amount": vol * close,
})
}
)
class TestRegistry:
+41 -16
View File
@@ -19,8 +19,20 @@ def test_security_bars_exact_layout():
# 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
0x010C,
0x01016408,
0x001C,
0x001C,
0x052D,
1,
b"600000",
4,
1,
0,
10,
0,
0,
0,
)
assert req == expected
assert len(req) == 38
@@ -34,8 +46,20 @@ def test_history_fund_flow_exact_layout():
# 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
0x010C,
0x01016408,
0x001C,
0x001C,
0x052D,
1,
b"600000",
22,
1,
0,
10,
0,
0,
0,
)
assert req == expected
assert len(req) == 38
@@ -120,18 +144,19 @@ def test_compute_price_limits_for_indices():
def test_compute_price_limits_for_newly_listed_stocks():
"""上市初期限价窗口应返回 None。"""
assert compute_price_limits(
Market.SH, "600001", "主板新股", 10.0, listed_days=5
) == (None, None)
assert compute_price_limits(
Market.SH, "600001", "主板新股", 10.0, listed_days=6
) == (11.0, 9.0)
assert compute_price_limits(
Market.BJ, "920002", "北交所新股", 84.36, listed_days=1
) == (None, None)
assert compute_price_limits(
Market.BJ, "920002", "北交所新股", 84.36, listed_days=2
) == (109.67, 59.05)
assert compute_price_limits(Market.SH, "600001", "主板新股", 10.0, listed_days=5) == (
None,
None,
)
assert compute_price_limits(Market.SH, "600001", "主板新股", 10.0, listed_days=6) == (11.0, 9.0)
assert compute_price_limits(Market.BJ, "920002", "北交所新股", 84.36, listed_days=1) == (
None,
None,
)
assert compute_price_limits(Market.BJ, "920002", "北交所新股", 84.36, listed_days=2) == (
109.67,
59.05,
)
def test_history_fund_flow_uses_uint32_volume_words():
+1 -1
View File
@@ -10,6 +10,7 @@ from unittest.mock import MagicMock, patch
import pandas as pd
import pytest
from easy_tdx.models.bar import SecurityBar
# ── 辅助:构造 SecurityBar ────────────────────────────────────────────────
@@ -300,7 +301,6 @@ class TestScanOne:
scanner._cash = 100000.0
scanner._commission = 0.0003
bars = _make_bars(100)
with patch.object(scanner, "_scan_one") as mock_scan:
# 不产生信号时返回 None
mock_scan.return_value = None
+4 -1
View File
@@ -26,10 +26,13 @@ def test_sync_connection_closes_socket_when_setup_fails() -> None:
sock = _FakeSocket()
conn = TdxConnection("127.0.0.1", port=7709, timeout=0.2)
with patch("easy_tdx.transport.sync.socket.socket", return_value=sock), patch.object(
with (
patch("easy_tdx.transport.sync.socket.socket", return_value=sock),
patch.object(
TdxConnection,
"_send_setup",
side_effect=TdxConnectionError("setup failed"),
),
):
try:
conn.connect()