mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 14:34:18 +08:00
- 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>
266 lines
9.0 KiB
Python
266 lines
9.0 KiB
Python
"""缠论 Phase 2 单元测试:MACD、线段、买卖点、背驰。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
from easy_tdx.chanlun.bi import find_bis
|
|
from easy_tdx.chanlun.fractal import find_fractals
|
|
from easy_tdx.chanlun.types import CLKline, Direction, Kline
|
|
|
|
# ── helpers ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
def _k(
|
|
idx: int,
|
|
dt: str,
|
|
o: float,
|
|
c: float,
|
|
h: float,
|
|
l: float, # noqa: E741
|
|
a: float = 0.0,
|
|
) -> Kline:
|
|
return Kline(
|
|
index=idx,
|
|
date=datetime.strptime(dt, "%Y-%m-%d"),
|
|
open=o,
|
|
close=c,
|
|
high=h,
|
|
low=l,
|
|
amount=a,
|
|
)
|
|
|
|
|
|
def _ck(
|
|
idx: int,
|
|
dt: str,
|
|
o: float,
|
|
c: float,
|
|
h: float,
|
|
l: float, # noqa: E741
|
|
merged_count: int = 1,
|
|
direction: str = "",
|
|
) -> CLKline:
|
|
return CLKline(
|
|
k_index=idx,
|
|
date=datetime.strptime(dt, "%Y-%m-%d"),
|
|
open=o,
|
|
close=c,
|
|
high=h,
|
|
low=l,
|
|
amount=0.0,
|
|
index=0,
|
|
merged_count=merged_count,
|
|
direction=direction,
|
|
)
|
|
|
|
|
|
# ── MACD 测试 ────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestMacd:
|
|
"""calc_macd 测试。"""
|
|
|
|
def test_macd_output_length(self) -> None:
|
|
"""MACD 输出长度应与输入一致。"""
|
|
from easy_tdx.chanlun.macd import calc_macd
|
|
|
|
closes = [10.0 + i * 0.5 for i in range(50)]
|
|
result = calc_macd(closes)
|
|
assert "dif" in result
|
|
assert "dea" in result
|
|
assert "hist" in result
|
|
assert len(result["dif"]) == 50
|
|
assert len(result["dea"]) == 50
|
|
assert len(result["hist"]) == 50
|
|
|
|
def test_macd_short_input(self) -> None:
|
|
"""输入太短时应返回零数组。"""
|
|
from easy_tdx.chanlun.macd import calc_macd
|
|
|
|
result = calc_macd([10.0])
|
|
assert len(result["dif"]) == 1
|
|
assert result["dif"][0] == 0.0
|
|
|
|
def test_macd_uptrend_positive_dif(self) -> None:
|
|
"""持续上涨时 DIF 应为正。"""
|
|
from easy_tdx.chanlun.macd import calc_macd
|
|
|
|
closes = [float(i) for i in range(100)]
|
|
result = calc_macd(closes)
|
|
# 后半段 DIF 应为正
|
|
assert result["dif"][-1] > 0
|
|
|
|
def test_macd_downtrend_negative_dif(self) -> None:
|
|
"""持续下跌时 DIF 应为负。"""
|
|
from easy_tdx.chanlun.macd import calc_macd
|
|
|
|
closes = [100.0 - i for i in range(100)]
|
|
result = calc_macd(closes)
|
|
assert result["dif"][-1] < 0
|
|
|
|
def test_macd_hist_equals_2x_diff(self) -> None:
|
|
"""MACD 柱 = 2 * (DIF - DEA)。"""
|
|
from easy_tdx.chanlun.macd import calc_macd
|
|
|
|
closes = [10.0 + i * 0.3 for i in range(60)]
|
|
result = calc_macd(closes)
|
|
for i in range(len(closes)):
|
|
expected = 2 * (result["dif"][i] - result["dea"][i])
|
|
assert abs(result["hist"][i] - expected) < 1e-10
|
|
|
|
def test_macd_custom_params(self) -> None:
|
|
"""支持自定义 fast/slow/signal 参数。"""
|
|
from easy_tdx.chanlun.macd import calc_macd
|
|
|
|
closes = [float(i) for i in range(100)]
|
|
r1 = calc_macd(closes, fast=12, slow=26, signal=9)
|
|
r2 = calc_macd(closes, fast=6, slow=13, signal=5)
|
|
# 不同参数应产生不同结果
|
|
assert r1["dif"][-1] != r2["dif"][-1]
|
|
|
|
|
|
# ── 线段测试 ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestFindXds:
|
|
"""find_xds 测试。"""
|
|
|
|
def test_basic_xd_from_bis(self) -> None:
|
|
"""多笔应能形成至少一个线段。"""
|
|
from easy_tdx.chanlun.xd import find_xds
|
|
|
|
# 构造足够多的笔来形成线段
|
|
# 需要至少5笔(3笔形成中枢 + 2笔进出)
|
|
cks = [
|
|
_ck(0, "2025-01-02", 10, 8, 11, 8),
|
|
_ck(1, "2025-01-03", 9, 15, 16, 9), # 顶 h=16
|
|
_ck(2, "2025-01-06", 14, 11, 14, 10),
|
|
_ck(3, "2025-01-07", 10, 13, 14, 9), # 底 l=9
|
|
_ck(4, "2025-01-08", 12, 14, 15, 11),
|
|
_ck(5, "2025-01-09", 14, 12, 14, 11),
|
|
_ck(6, "2025-01-10", 11, 9, 12, 8), # 底
|
|
_ck(7, "2025-01-13", 10, 11, 12, 9),
|
|
_ck(8, "2025-01-14", 11, 6, 12, 5), # 大跌
|
|
_ck(9, "2025-01-15", 7, 8, 9, 6),
|
|
_ck(10, "2025-01-16", 8, 12, 13, 7),
|
|
_ck(11, "2025-01-17", 11, 10, 14, 9),
|
|
_ck(12, "2025-01-20", 10, 6, 11, 5),
|
|
_ck(13, "2025-01-21", 7, 8, 9, 6),
|
|
]
|
|
fxs = find_fractals(cks)
|
|
bis = find_bis(fxs)
|
|
xds = find_xds(bis)
|
|
# 有足够的笔时,应能形成线段
|
|
if len(bis) >= 5:
|
|
assert len(xds) >= 1
|
|
|
|
def test_empty_bis(self) -> None:
|
|
"""空笔列表应返回空线段。"""
|
|
from easy_tdx.chanlun.xd import find_xds
|
|
|
|
assert find_xds([]) == []
|
|
|
|
def test_xd_direction_alternates(self) -> None:
|
|
"""线段方向应与笔的方向一致:向上线段由向上笔主导。"""
|
|
from easy_tdx.chanlun.xd import find_xds
|
|
|
|
cks = [
|
|
_ck(0, "2025-01-02", 10, 8, 11, 8),
|
|
_ck(1, "2025-01-03", 9, 15, 16, 9),
|
|
_ck(2, "2025-01-06", 14, 11, 14, 10),
|
|
_ck(3, "2025-01-07", 10, 13, 14, 9),
|
|
_ck(4, "2025-01-08", 12, 14, 15, 11),
|
|
_ck(5, "2025-01-09", 14, 12, 14, 11),
|
|
_ck(6, "2025-01-10", 11, 9, 12, 8),
|
|
_ck(7, "2025-01-13", 10, 11, 12, 9),
|
|
_ck(8, "2025-01-14", 11, 6, 12, 5),
|
|
_ck(9, "2025-01-15", 7, 8, 9, 6),
|
|
_ck(10, "2025-01-16", 8, 12, 13, 7),
|
|
_ck(11, "2025-01-17", 11, 10, 14, 9),
|
|
_ck(12, "2025-01-20", 10, 6, 11, 5),
|
|
_ck(13, "2025-01-21", 7, 8, 9, 6),
|
|
]
|
|
fxs = find_fractals(cks)
|
|
bis = find_bis(fxs)
|
|
xds = find_xds(bis)
|
|
for xd in xds:
|
|
assert xd.direction in (Direction.UP, Direction.DOWN)
|
|
|
|
|
|
# ── 买卖点测试 ────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestFindMmds:
|
|
"""find_mmds 测试。"""
|
|
|
|
def test_first_buy_after_zs(self) -> None:
|
|
"""中枢下方出现底背驰应产生一类买点。"""
|
|
from easy_tdx.chanlun.mmd import find_mmds
|
|
|
|
cks = [
|
|
_ck(0, "2025-01-02", 10, 8, 11, 8),
|
|
_ck(1, "2025-01-03", 9, 15, 16, 9), # 顶
|
|
_ck(2, "2025-01-06", 14, 11, 14, 10),
|
|
_ck(3, "2025-01-07", 10, 13, 14, 9), # 底
|
|
_ck(4, "2025-01-08", 12, 14, 15, 11),
|
|
_ck(5, "2025-01-09", 14, 12, 14, 11),
|
|
_ck(6, "2025-01-10", 11, 9, 12, 8), # 底
|
|
_ck(7, "2025-01-13", 10, 11, 12, 9),
|
|
_ck(8, "2025-01-14", 11, 14, 15, 10), # 向上离开
|
|
_ck(9, "2025-01-15", 14, 12, 16, 11),
|
|
]
|
|
fxs = find_fractals(cks)
|
|
bis = find_bis(fxs)
|
|
# find_mmds 需要笔列表和中枢列表
|
|
from easy_tdx.chanlun.zs import find_zss
|
|
|
|
zss = find_zss(bis)
|
|
mmds = find_mmds(bis, zss)
|
|
# 结果应为列表(可能为空,取决于是否满足条件)
|
|
assert isinstance(mmds, list)
|
|
|
|
def test_empty_input(self) -> None:
|
|
"""空输入应返回空列表。"""
|
|
from easy_tdx.chanlun.mmd import find_mmds
|
|
|
|
assert find_mmds([], []) == []
|
|
|
|
|
|
# ── 背驰测试 ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestBeichi:
|
|
"""check_beichi 测试。"""
|
|
|
|
def test_divergence_detection(self) -> None:
|
|
"""力度衰减应被检测为背驰。"""
|
|
from easy_tdx.chanlun.beichi import check_bi_beichi
|
|
|
|
cks = [
|
|
_ck(0, "2025-01-02", 10, 8, 11, 8),
|
|
_ck(1, "2025-01-03", 9, 15, 16, 9),
|
|
_ck(2, "2025-01-06", 14, 11, 14, 10),
|
|
_ck(3, "2025-01-07", 10, 13, 14, 9),
|
|
_ck(4, "2025-01-08", 12, 14, 15, 11),
|
|
_ck(5, "2025-01-09", 14, 12, 14, 11),
|
|
_ck(6, "2025-01-10", 11, 9, 12, 8),
|
|
_ck(7, "2025-01-13", 10, 11, 12, 9),
|
|
_ck(8, "2025-01-14", 11, 14, 15, 10),
|
|
_ck(9, "2025-01-15", 14, 12, 16, 11),
|
|
]
|
|
fxs = find_fractals(cks)
|
|
bis = find_bis(fxs)
|
|
from easy_tdx.chanlun.zs import find_zss
|
|
|
|
zss = find_zss(bis)
|
|
# 至少不崩溃
|
|
result = check_bi_beichi(bis, zss)
|
|
assert isinstance(result, list)
|
|
|
|
def test_empty_input(self) -> None:
|
|
"""空输入应返回空列表。"""
|
|
from easy_tdx.chanlun.beichi import check_bi_beichi
|
|
|
|
assert check_bi_beichi([], []) == []
|