mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 15:44:18 +08:00
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:
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}),笔的方向应该交替。"
|
||||
)
|
||||
|
||||
|
||||
# ── 中枢计算测试 ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user