mirror of
https://ghfast.top/https://github.com/aeroxw/easy-tdx.git
synced 2026-09-12 19:14:16 +08:00
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:
co-authored by
Claude Opus 4.8
parent
5aac7d3a39
commit
4dfd18050e
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user