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
+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:
+36 -26
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({
"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()
+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)
+11 -10
View File
@@ -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())
+12 -10
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,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:
+46 -21
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
@@ -56,16 +80,16 @@ def test_security_quotes_limit_mapping():
from easy_tdx.codec.price import put_price
cmd = GetSecurityQuotesCmd([(Market.SH, "600000")])
# 构造响应报文
body = bytearray(b"\x00\x00")
body.extend(struct.pack("<H", 1))
# Record: Market(B), Code(6s), Active1(H) + ...
body.extend(struct.pack("<B6sH", 1, b"600000", 0))
body.extend(put_price(1010)) # price_raw
body.extend(put_price(-5)) # last_close_diff
body.extend(put_price(-5)) # last_close_diff
body.extend(put_price(0))
body.extend(put_price(0))
body.extend(put_price(0))
@@ -86,7 +110,7 @@ def test_security_quotes_limit_mapping():
body.extend(put_price(0))
body.extend(put_price(0))
body.extend(struct.pack("<hH", 0, 0))
quotes = cmd.parse_response(bytes(body))
q = quotes[0]
assert q.limit_up is None
@@ -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
+7 -4
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(
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()