From 112ba7849fdb265448f9abb04946950339bce77f Mon Sep 17 00:00:00 2001 From: Justin Gu <97915@qq.com> Date: Mon, 8 Jun 2026 03:08:12 +0800 Subject: [PATCH] fix: chanlun bi algorithm stuck in fractal trap during sustained trends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix find_bis() greedy algorithm terminating early when dense alternating fractals cause gap=0 for every opposite-type fractal. The root cause was blindly replacing start_fx with more extreme same-type fractals, pushing right_kline_index forward and making subsequent gaps permanently 0. Solution: add pending_opposite guard — when an opposite-type fractal fails the gap check, freeze start_fx replacement until a valid bi is formed. - Affects: sustained up/down trends with dense fractals (e.g. high-price stocks) - 600519: 114 bi (ending 04-28) -> 142 bi (ending 05-27) - 601088: 131 bi -> 147 bi (end date unchanged) - New regression test: test_fractal_trap_regression - Bump version to 1.7.1 --- README.md | 10 +++++++++ pyproject.toml | 2 +- src/easy_tdx/chanlun/bi.py | 28 +++++++++++++++++++----- tests/unit/test_chanlun.py | 45 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 37122e1..a1a51a2 100644 --- a/README.md +++ b/README.md @@ -937,6 +937,16 @@ ruff format --check src/ tests/ # format check ## Changelog +### 1.7.1 (2026-06-08) + +**Bug 修复** — 修复缠论笔计算在持续下跌/上涨走势中因"分型陷阱"导致近期笔丢失的问题。 + +- 修复 `find_bis()` 贪心算法在密集交替分型场景下提前终止的 bug +- 根因:当异类型分型 gap=0 时,算法仍用更极端的同类型分型替换 start_fx,导致 right_kline_index 不断前推,后续所有异类型分型 gap 永远为 0 +- 新增 `pending_opposite` 保护机制:存在未配对异类型分型时冻结替换,保留 start_fx 较前位置 +- 影响范围:持续下跌/上涨中的高价股(如贵州茅台)或分型密度高的股票 +- 新增回归测试 `test_fractal_trap_regression` + ### 1.7.0 (2026-06-07) **缠论技术分析模块** — 新增完整的缠论(ChanLun)计算引擎,通过 CLI 和 Python API 提供个股缠论分析。 diff --git a/pyproject.toml b/pyproject.toml index 2585c26..9831032 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "easy-tdx" -version = "1.7.0" +version = "1.7.1" description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步" readme = "README.md" requires-python = ">=3.10" diff --git a/src/easy_tdx/chanlun/bi.py b/src/easy_tdx/chanlun/bi.py index 5769777..eb492a9 100644 --- a/src/easy_tdx/chanlun/bi.py +++ b/src/easy_tdx/chanlun/bi.py @@ -46,11 +46,16 @@ def find_bis( ) -> list[BI]: """从分型列表中计算笔。 - 算法(贪心): - 1. 遍历分型列表,维护最后一个有效分型 - 2. 如果当前分型与最后一个有效分型可以成笔,形成新笔 - 3. 如果当前分型与最后一个有效分型同类型(同为顶或同为底), - 取更极端的那个替换(顶取更高的,底取更低的) + 算法(贪心,带 pending 保护): + 1. 遍历分型列表,维护最后一个有效分型(start_fx) + 2. 如果当前分型与 start_fx 可以成笔,形成新笔 + 3. 如果当前分型与 start_fx 同类型,取更极端的替换(顶取更高,底取更低) + 4. 如果当前分型与 start_fx 异类型但 gap 不够,记录为 pending_opposite + 5. 当存在 pending_opposite 时,不替换 start_fx,保留其在较前的位置, + 使后续异类型分型自然满足 gap 要求(避免"分型陷阱") + + "分型陷阱"场景:持续下跌/上涨中,密集交替分型导致每次替换 start_fx + 都把 right_kline_index 前推,使下一个异类型分型的 gap 永远为 0。 Args: fxs: 分型列表 @@ -69,12 +74,20 @@ def find_bis( # 用一个指针追踪当前笔的起始分型 start_fx = fxs[0] + # 记录最近一个因 gap 不足而跳过的异类型分型 + pending_opposite: FX | None = None for i in range(1, len(fxs)): current_fx = fxs[i] # 同类型分型:取更极端的 if current_fx.fx_type == start_fx.fx_type: + # 当存在 pending_opposite 时,不替换 start_fx。 + # 保留 start_fx 在较早位置,让后续异类型分型有机会满足 gap。 + # 若替换,right_kline_index 前推会导致 gap 反复为 0,卡死算法。 + if pending_opposite is not None: + continue + if start_fx.fx_type == FXType.DING and current_fx.val > start_fx.val: start_fx = current_fx elif start_fx.fx_type == FXType.DI and current_fx.val < start_fx.val: @@ -97,6 +110,9 @@ def find_bis( ) bis.append(bi) start_fx = current_fx - # 如果不能成笔(间距不够),继续搜索 + pending_opposite = None + else: + # gap 不够,记录为 pending + pending_opposite = current_fx return bis diff --git a/tests/unit/test_chanlun.py b/tests/unit/test_chanlun.py index 41f7f02..3d549eb 100644 --- a/tests/unit/test_chanlun.py +++ b/tests/unit/test_chanlun.py @@ -360,6 +360,51 @@ class TestFindBis: bis = find_bis(fxs) assert len(bis) >= 1 + def test_fractal_trap_regression(self) -> None: + """回归测试:密集交替分型不应卡死笔算法。 + + 场景:持续下跌走势中,分型在相邻 CKline 位置交替出现(mid_gap=1), + 导致每个异类型分型与前一个同类型分型共享 2 根 CKline(gap=0)。 + + 修复前:贪心算法用更极端的同类型分型替换 start_fx, + 推进 right_kline_index,使后续所有异类型分型 gap 永远为 0,卡死算法。 + 修复后:存在 pending 异类型分型时不替换 start_fx,保留较早位置使 gap 自然递增。 + """ + # 下跌锯齿形:分型在连续 CKline 位置交替(ding/di mid_gap=1) + # 每个 di 比前一个低,每个 ding 也比前一个低 → 持续下跌 + cks = [ + _ck(0, "2025-01-02", 145, 145, 150, 140), + _ck(1, "2025-01-03", 130, 130, 135, 125), # di + _ck(2, "2025-01-06", 140, 140, 145, 135), # ding + _ck(3, "2025-01-07", 125, 125, 130, 120), # di + _ck(4, "2025-01-08", 133, 133, 138, 128), # ding + _ck(5, "2025-01-09", 120, 120, 125, 115), # di + _ck(6, "2025-01-10", 127, 127, 132, 122), # ding + _ck(7, "2025-01-13", 113, 113, 118, 108), # di + _ck(8, "2025-01-14", 121, 121, 126, 116), # ding + _ck(9, "2025-01-15", 107, 107, 112, 102), # di (overlap ends) + _ck(10, "2025-01-16", 103, 103, 108, 98), + _ck(11, "2025-01-17", 113, 113, 118, 108), # ding + _ck(12, "2025-01-20", 101, 101, 106, 96), + ] + + fxs = find_fractals(cks) + assert len(fxs) >= 6, f"应产生至少6个分型,实际 {len(fxs)}" + + bis = find_bis(fxs) + + # 关键断言:密集交替分型不应导致算法卡死 + assert len(bis) >= 2, ( + f"密集交替分型场景应产出至少2笔,实际只有 {len(bis)} 笔。" + f"分型数: {len(fxs)},可能触发了分型陷阱 bug。" + ) + + # 验证方向交替 + for i in range(1, len(bis)): + assert bis[i].direction != bis[i - 1].direction, ( + f"笔 {i - 1} 和笔 {i} 方向相同 ({bis[i].direction.value}),笔的方向应该交替。" + ) + # ── 中枢计算测试 ──────────────────────────────────────────────────────────