From 4dfd18050e02aca4fdbfa00f7f16808e50089c31 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 10 Jun 2026 15:03:41 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20resolve=20all=20CI=20mypy=20(265?= =?UTF-8?q?=E2=86=920)=20and=20ruff=20(26=E2=86=920)=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .claude/settings.local.json | 87 +++++++++++++++++++ pyproject.toml | 9 ++ src/easy_tdx/MyTT.py | 7 +- src/easy_tdx/backtest/cli.py | 21 +++-- src/easy_tdx/backtest/combo.py | 13 ++- src/easy_tdx/chanlun/analyser.py | 11 +-- src/easy_tdx/chanlun/beichi.py | 4 +- src/easy_tdx/cli/cmd_chanlun.py | 3 +- src/easy_tdx/cli/cmd_ex.py | 3 +- src/easy_tdx/cli/cmd_offline.py | 5 +- src/easy_tdx/client.py | 9 +- src/easy_tdx/codec/block.py | 22 ++--- src/easy_tdx/codec/datetime_.py | 9 +- src/easy_tdx/codec/frame.py | 6 +- src/easy_tdx/codec/industry.py | 3 +- src/easy_tdx/codec/volume.py | 5 +- src/easy_tdx/commands/company_info.py | 26 +++--- src/easy_tdx/commands/finance_info.py | 42 +++++++-- src/easy_tdx/commands/fund_flow.py | 40 +++++---- src/easy_tdx/commands/security_list.py | 4 +- src/easy_tdx/commands/setup.py | 5 +- src/easy_tdx/commands/transaction.py | 40 +++++---- src/easy_tdx/config.py | 24 +++--- src/easy_tdx/ex/commands/login.py | 26 +++--- src/easy_tdx/ex/mac_client.py | 16 ++-- src/easy_tdx/ex/transport/async_.py | 3 +- src/easy_tdx/ex/transport/sync.py | 1 - src/easy_tdx/indicator.py | 1 - src/easy_tdx/mac/client.py | 6 +- src/easy_tdx/mac/commands/goods_list.py | 5 +- src/easy_tdx/mac/commands/unusual.py | 4 +- src/easy_tdx/models/bar.py | 2 +- src/easy_tdx/models/finance.py | 110 ++++++++++++------------ src/easy_tdx/models/security.py | 8 +- src/easy_tdx/models/stats.py | 47 +++++----- src/easy_tdx/offline/write_daily.py | 2 +- src/easy_tdx/offline/write_ex_daily.py | 2 +- src/easy_tdx/transport/sync.py | 9 +- src/easy_tdx/unified.py | 28 ++++-- tests/unit/test_backtest_combo.py | 1 + tests/unit/test_backtest_engine.py | 6 +- tests/unit/test_backtest_performance.py | 62 +++++++------ tests/unit/test_chanlun.py | 4 +- tests/unit/test_chanlun_phase2.py | 4 +- tests/unit/test_chanlun_phase3.py | 11 +-- tests/unit/test_financial_data.py | 4 +- tests/unit/test_heartbeat.py | 21 ++--- tests/unit/test_indicator.py | 22 ++--- tests/unit/test_protocol_fixes.py | 67 ++++++++++----- tests/unit/test_screen.py | 2 +- tests/unit/test_sync_transport.py | 11 ++- 51 files changed, 548 insertions(+), 335 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..54a3148 --- /dev/null +++ b/.claude/settings.local.json @@ -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 *)" + ] + } +} diff --git a/pyproject.toml b/pyproject.toml index 44c33a5..16b9ac7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 diff --git a/src/easy_tdx/MyTT.py b/src/easy_tdx/MyTT.py index 3592c35..1103835 100644 --- a/src/easy_tdx/MyTT.py +++ b/src/easy_tdx/MyTT.py @@ -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作平滑因子,必须 0B & ) -# ------------------ 1级:应用层函数(通过0级核心函数实现)使用方法请参考通达信-------------------------------- +# -- 1级:应用层函数(通过0级核心函数实现)使用方法请参考通达信 -------------------- def COUNT(S, N): # COUNT(CLOSE>O, N): 最近N天满足S_BOO的天数 True的天数 return SUM(S, N) diff --git a/src/easy_tdx/backtest/cli.py b/src/easy_tdx/backtest/cli.py index 96c3b50..56134c8 100644 --- a/src/easy_tdx/backtest/cli.py +++ b/src/easy_tdx/backtest/cli.py @@ -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, diff --git a/src/easy_tdx/backtest/combo.py b/src/easy_tdx/backtest/combo.py index b582d6b..a838eb2 100644 --- a/src/easy_tdx/backtest/combo.py +++ b/src/easy_tdx/backtest/combo.py @@ -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)") diff --git a/src/easy_tdx/chanlun/analyser.py b/src/easy_tdx/chanlun/analyser.py index dc636b7..6961f1f 100644 --- a/src/easy_tdx/chanlun/analyser.py +++ b/src/easy_tdx/chanlun/analyser.py @@ -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, diff --git a/src/easy_tdx/chanlun/beichi.py b/src/easy_tdx/chanlun/beichi.py index 222af6c..2dbb759 100644 --- a/src/easy_tdx/chanlun/beichi.py +++ b/src/easy_tdx/chanlun/beichi.py @@ -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 面积,这里用幅度作为简化替代。 diff --git a/src/easy_tdx/cli/cmd_chanlun.py b/src/easy_tdx/cli/cmd_chanlun.py index 1e58716..43f4ad1 100644 --- a/src/easy_tdx/cli/cmd_chanlun.py +++ b/src/easy_tdx/cli/cmd_chanlun.py @@ -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']}") diff --git a/src/easy_tdx/cli/cmd_ex.py b/src/easy_tdx/cli/cmd_ex.py index 067c932..125b56b 100644 --- a/src/easy_tdx/cli/cmd_ex.py +++ b/src/easy_tdx/cli/cmd_ex.py @@ -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, diff --git a/src/easy_tdx/cli/cmd_offline.py b/src/easy_tdx/cli/cmd_offline.py index 2f83cde..cfa4607 100644 --- a/src/easy_tdx/cli/cmd_offline.py +++ b/src/easy_tdx/cli/cmd_offline.py @@ -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)) diff --git a/src/easy_tdx/client.py b/src/easy_tdx/client.py index 8cca5dc..d6860eb 100644 --- a/src/easy_tdx/client.py +++ b/src/easy_tdx/client.py @@ -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 diff --git a/src/easy_tdx/codec/block.py b/src/easy_tdx/codec/block.py index d29a1f0..e314447 100644 --- a/src/easy_tdx/codec/block.py +++ b/src/easy_tdx/codec/block.py @@ -21,7 +21,7 @@ def parse_block_dat(data: bytes, filename: str = "") -> list["TdxBlock"]: return [] pos = 384 - (count,) = struct.unpack(" list["TdxBlock"]: break # 板块元数据 (9 字节名称 + 2 字节股票数 + 2 字节类型) - name_b = data[pos:pos+9] - stock_count, _type = struct.unpack(" 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 diff --git a/src/easy_tdx/codec/datetime_.py b/src/easy_tdx/codec/datetime_.py index 78ad1fa..3c38254 100644 --- a/src/easy_tdx/codec/datetime_.py +++ b/src/easy_tdx/codec/datetime_.py @@ -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: diff --git a/src/easy_tdx/codec/frame.py b/src/easy_tdx/codec/frame.py index 40f1ef9..bccdbb3 100644 --- a/src/easy_tdx/codec/frame.py +++ b/src/easy_tdx/codec/frame.py @@ -25,9 +25,9 @@ _HEADER_FMT = " dict[str, tuple[str, str]]: """解析 tdxhy.cfg 字节内容。 - + 返回字典: { "code": (tdx_industry, sw_industry), ... } """ results = {} diff --git a/src/easy_tdx/codec/volume.py b/src/easy_tdx/codec/volume.py index c7b1b26..d7d6914 100644 --- a/src/easy_tdx/codec/volume.py +++ b/src/easy_tdx/codec/volume.py @@ -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 diff --git a/src/easy_tdx/commands/company_info.py b/src/easy_tdx/commands/company_info.py index 58e662f..112db33 100644 --- a/src/easy_tdx/commands/company_info.py +++ b/src/easy_tdx/commands/company_info.py @@ -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( " str: diff --git a/src/easy_tdx/commands/finance_info.py b/src/easy_tdx/commands/finance_info.py index be085ae..522fcfa 100644 --- a/src/easy_tdx/commands/finance_info.py +++ b/src/easy_tdx/commands/finance_info.py @@ -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 # 财务数据单位:万元/万股 diff --git a/src/easy_tdx/commands/fund_flow.py b/src/easy_tdx/commands/fund_flow.py index 40279d9..c11900b 100644 --- a/src/easy_tdx/commands/fund_flow.py +++ b/src/easy_tdx/commands/fund_flow.py @@ -41,35 +41,39 @@ class GetHistoryFundFlowCmd(BaseCommand[list[HistoricalFundFlow]]): # 响应格式:9字节头 + 2字节数量 + 每条记录 36 字节 if len(body) < 11: return [] - + (num,) = struct.unpack(" 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 diff --git a/src/easy_tdx/config.py b/src/easy_tdx/config.py index 55c1923..124096c 100644 --- a/src/easy_tdx/config.py +++ b/src/easy_tdx/config.py @@ -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)) # --------------------------------------------------------------------------- diff --git a/src/easy_tdx/ex/commands/login.py b/src/easy_tdx/ex/commands/login.py index 3e4aedb..173f876 100644 --- a/src/easy_tdx/ex/commands/login.py +++ b/src/easy_tdx/ex/commands/login.py @@ -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 = "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: """当日主力净流入。""" diff --git a/src/easy_tdx/offline/write_daily.py b/src/easy_tdx/offline/write_daily.py index 1756957..b23a94e 100644 --- a/src/easy_tdx/offline/write_daily.py +++ b/src/easy_tdx/offline/write_daily.py @@ -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: diff --git a/src/easy_tdx/offline/write_ex_daily.py b/src/easy_tdx/offline/write_ex_daily.py index 6d067d9..98caff5 100644 --- a/src/easy_tdx/offline/write_ex_daily.py +++ b/src/easy_tdx/offline/write_ex_daily.py @@ -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: diff --git a/src/easy_tdx/transport/sync.py b/src/easy_tdx/transport/sync.py index 4a82ddb..02b8a92 100644 --- a/src/easy_tdx/transport/sync.py +++ b/src/easy_tdx/transport/sync.py @@ -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: diff --git a/src/easy_tdx/unified.py b/src/easy_tdx/unified.py index 7bde9e2..5c60f0f 100644 --- a/src/easy_tdx/unified.py +++ b/src/easy_tdx/unified.py @@ -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() diff --git a/tests/unit/test_backtest_combo.py b/tests/unit/test_backtest_combo.py index 45c14c6..c702d06 100644 --- a/tests/unit/test_backtest_combo.py +++ b/tests/unit/test_backtest_combo.py @@ -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 diff --git a/tests/unit/test_backtest_engine.py b/tests/unit/test_backtest_engine.py index 06bc7a5..4927961 100644 --- a/tests/unit/test_backtest_engine.py +++ b/tests/unit/test_backtest_engine.py @@ -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: diff --git a/tests/unit/test_backtest_performance.py b/tests/unit/test_backtest_performance.py index 0f167ef..d9ee27b 100644 --- a/tests/unit/test_backtest_performance.py +++ b/tests/unit/test_backtest_performance.py @@ -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({ - "datetime": np.arange(n), - "total": total, - "drawdown": drawdown, - "drawdown_pct": drawdown_pct, - }) + 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({ - "direction": ["BUY", "SELL", "BUY", "SELL"], - "pnl": [0, 500, 0, -500], - "rejected": [False, False, False, False], - }) + 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([ - np.linspace(100000, 600000, 126), # 涨到 60 万 - np.linspace(600000, 300000, 126), # 跌到 30 万 - ]) + 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({ - "datetime": np.arange(252), - "total": total, - "drawdown": drawdown, - "drawdown_pct": drawdown_pct, - }) + 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({ - "direction": ["BUY", "SELL", "SELL", "SELL"], - "pnl": [0, 500, 0, -500], - "rejected": [False, False, True, False], - }) + 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() diff --git a/tests/unit/test_chanlun.py b/tests/unit/test_chanlun.py index 3d549eb..05db0bc 100644 --- a/tests/unit/test_chanlun.py +++ b/tests/unit/test_chanlun.py @@ -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: diff --git a/tests/unit/test_chanlun_phase2.py b/tests/unit/test_chanlun_phase2.py index 0409a92..3aaf43a 100644 --- a/tests/unit/test_chanlun_phase2.py +++ b/tests/unit/test_chanlun_phase2.py @@ -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: diff --git a/tests/unit/test_chanlun_phase3.py b/tests/unit/test_chanlun_phase3.py index b52e440..b38fc40 100644 --- a/tests/unit/test_chanlun_phase3.py +++ b/tests/unit/test_chanlun_phase3.py @@ -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( diff --git a/tests/unit/test_financial_data.py b/tests/unit/test_financial_data.py index 51c779f..e30865f 100644 --- a/tests/unit/test_financial_data.py +++ b/tests/unit/test_financial_data.py @@ -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) diff --git a/tests/unit/test_heartbeat.py b/tests/unit/test_heartbeat.py index 3029ee5..3ebb937 100644 --- a/tests/unit/test_heartbeat.py +++ b/tests/unit/test_heartbeat.py @@ -13,28 +13,29 @@ def test_heartbeat_sends_periodically(): mock_conn = mock_conn_cls.return_value mock_conn.connect = AsyncMock() mock_conn.close = AsyncMock() - + # 记录调用次数 call_count = 0 + async def mock_execute(cmd): nonlocal call_count call_count += 1 - return 5 # 模拟 get_security_count 返回值 + return 5 # 模拟 get_security_count 返回值 mock_conn.execute.side_effect = mock_execute # 设置非常短的心跳间隔以便测试 client = AsyncTdxClient("127.0.0.1", heartbeat_interval=0.1) await client.connect() - + # 等待几次心跳周期 await asyncio.sleep(0.35) - + await client.close() - + # 0.35s 应该触发约 3 次心跳 (0.1, 0.2, 0.3) assert call_count >= 3 - + asyncio.run(run_test()) @@ -45,14 +46,14 @@ def test_heartbeat_stops_on_close(): mock_conn.connect = AsyncMock() mock_conn.close = AsyncMock() mock_conn.execute = AsyncMock(return_value=5) - + client = AsyncTdxClient("127.0.0.1", heartbeat_interval=0.01) await client.connect() assert client._heartbeat_task is not None - + task = client._heartbeat_task await client.close() - + assert client._heartbeat_task is None assert task.done() or task.cancelled() @@ -65,5 +66,5 @@ if __name__ == "__main__": await test_heartbeat_sends_periodically() await test_heartbeat_stops_on_close() print("Heartbeat tests passed!") - + asyncio.run(run()) diff --git a/tests/unit/test_indicator.py b/tests/unit/test_indicator.py index f4757ec..1d9e3c4 100644 --- a/tests/unit/test_indicator.py +++ b/tests/unit/test_indicator.py @@ -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,15 +18,17 @@ 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({ - "datetime": pd.date_range("2024-01-01", periods=n, freq="D"), - "open": open_, - "high": high, - "low": low, - "close": close, - "vol": vol, - "amount": vol * close, - }) + return pd.DataFrame( + { + "datetime": pd.date_range("2024-01-01", periods=n, freq="D"), + "open": open_, + "high": high, + "low": low, + "close": close, + "vol": vol, + "amount": vol * close, + } + ) class TestRegistry: diff --git a/tests/unit/test_protocol_fixes.py b/tests/unit/test_protocol_fixes.py index 7f14b06..d6de902 100644 --- a/tests/unit/test_protocol_fixes.py +++ b/tests/unit/test_protocol_fixes.py @@ -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( " 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( - TdxConnection, - "_send_setup", - side_effect=TdxConnectionError("setup failed"), + with ( + patch("easy_tdx.transport.sync.socket.socket", return_value=sock), + patch.object( + TdxConnection, + "_send_setup", + side_effect=TdxConnectionError("setup failed"), + ), ): try: conn.connect()