fix: chanlun bi algorithm stuck in fractal trap during sustained trends

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
This commit is contained in:
Justin Gu
2026-06-08 03:08:12 +08:00
parent 0175acec43
commit 112ba7849f
4 changed files with 78 additions and 7 deletions
+10
View File
@@ -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 提供个股缠论分析。
+1 -1
View File
@@ -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"
+22 -6
View File
@@ -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
+45
View File
@@ -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}),笔的方向应该交替。"
)
# ── 中枢计算测试 ──────────────────────────────────────────────────────────