release: v1.14.2 — 缠论 JSON 可视化字段增强(中枢/买卖点/背驰补日期)

This commit is contained in:
Justin Gu
2026-06-16 02:51:52 +08:00
parent c54071e85e
commit 28aad8a84c
7 changed files with 114 additions and 1 deletions
+12
View File
@@ -1618,6 +1618,18 @@ ruff format --check src/ tests/ # format check
## Changelog
### 1.14.2 (2026-06-16)
**缠论结果可视化字段增强** — 响应 [Discussion #2](https://github.com/handsomejustin/easy-tdx/discussions/2),为缠论分析 JSON 输出(`ChanlunResult.to_dict()`)中的中枢 / 买卖点 / 背驰补上对应 K 线日期,方便前端/可视化工具直接用来标点画图。纯增量、向后兼容,不破坏任何已有 JSON 字段。
新增字段:
- **中枢 `zss`**:输出起始笔与结束笔的日期 `start_date` / `end_date`(第一笔起点 → 最后一笔终点)。
- **买卖点 `mmds`**:输出触发该买卖点的笔确认日期 `date`(买卖点确立时刻的 K 线日期)。
- **背驰 `bcs`**:输出背驰对照两笔的日期 `curr_date`(当前背驰笔)/ `prev_date`(对照基准笔)。
日期统一采用 `YYYY-MM-DD` 格式(与已有 `bis` / `xds` 输出一致),全部字段对 `None` 做了兜底。三层接入(Python API / CLI `easy-tdx chanlun` / Web `/chanlun/analyze`)同步生效,Web 接口直接返回新字段无需改动。
### 1.14.1 (2026-06-15)
**高级回测 ExecutionModel 路径 3 个真实数据兼容 Bug 修复** — 实测 `601088` 高级回测(方根滑点 + TWAP)暴露:权益曲线恒定、收益归零。根因为 ExecutionModel 路径与真实行情数据的格式/列名/类型脱节。
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "easy-tdx"
version = "1.14.1"
version = "1.14.2"
description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步"
readme = "README.md"
requires-python = ">=3.10"
+5
View File
@@ -99,6 +99,8 @@ class ChanlunResult:
"gg": round(zs.gg, 2),
"dd": round(zs.dd, 2),
"line_count": zs.line_count,
"start_date": zs.start.k.date.strftime("%Y-%m-%d") if zs.start else None,
"end_date": zs.end.k.date.strftime("%Y-%m-%d") if zs.end else None,
"done": zs.done,
}
for zs in self.zss
@@ -117,6 +119,7 @@ class ChanlunResult:
"mmds": [
{
"type": mmd.mmd_type.value,
"date": mmd.bi.end.k.date.strftime("%Y-%m-%d") if mmd.bi else None,
"msg": mmd.msg,
}
for mmd in self.mmds
@@ -125,6 +128,8 @@ class ChanlunResult:
{
"type": bc.bc_type.value,
"bc": bc.bc,
"curr_date": bc.curr.end.k.date.strftime("%Y-%m-%d") if bc.curr else None,
"prev_date": bc.prev.end.k.date.strftime("%Y-%m-%d") if bc.prev else None,
"msg": bc.msg,
}
for bc in self.bcs
+8
View File
@@ -75,6 +75,8 @@ def _check_bi_level_beichi(bis: list[BI]) -> list[BC]:
bc_type=BCType.BI,
bc=True,
zs=None,
curr=curr,
prev=prev,
msg=(
f"笔背驰: 笔[{curr.index}] 力度={curr_force:.2f} "
f"< 笔[{prev.index}] 力度={prev_force:.2f}"
@@ -108,6 +110,8 @@ def _check_pz_beichi(bis: list[BI], zss: list[ZS]) -> list[BC]:
bc_type=BCType.PZ,
bc=True,
zs=zs,
curr=last_bi,
prev=first_bi,
msg=(
f"盘整背驰: 中枢[{zs.index}] 内末笔力度={last_force:.2f} "
f"< 首笔力度={first_force:.2f}"
@@ -138,6 +142,8 @@ def _check_qs_beichi(bis: list[BI], zss: list[ZS]) -> list[BC]:
bc_type=BCType.QS,
bc=True,
zs=curr_zs,
curr=curr_zs.lines[-1],
prev=prev_zs.lines[-1],
msg=(
f"趋势背驰(下): 中枢[{curr_zs.index}] 离开力度={curr_exit_force:.2f} "
f"< 中枢[{prev_zs.index}] 离开力度={prev_exit_force:.2f}"
@@ -155,6 +161,8 @@ def _check_qs_beichi(bis: list[BI], zss: list[ZS]) -> list[BC]:
bc_type=BCType.QS,
bc=True,
zs=curr_zs,
curr=curr_zs.lines[-1],
prev=prev_zs.lines[-1],
msg=(
f"趋势背驰(上): 中枢[{curr_zs.index}] 离开力度={curr_exit_force:.2f} "
f"< 中枢[{prev_zs.index}] 离开力度={prev_exit_force:.2f}"
+6
View File
@@ -68,6 +68,7 @@ def _check_buy_point(bi: BI, zs: ZS, all_bis: list[BI]) -> MMD | None:
return MMD(
mmd_type=MMDType.BUY_1,
zs=zs,
bi=bi,
msg=f"中枢下方力度衰减,一类买点 (l={bi.low:.2f} < zd={zs.zd:.2f})",
)
@@ -82,6 +83,7 @@ def _check_buy_point(bi: BI, zs: ZS, all_bis: list[BI]) -> MMD | None:
return MMD(
mmd_type=MMDType.BUY_2,
zs=zs,
bi=bi,
msg=f"回调不创新低,二类买点 (l={bi.low:.2f})",
)
@@ -90,6 +92,7 @@ def _check_buy_point(bi: BI, zs: ZS, all_bis: list[BI]) -> MMD | None:
return MMD(
mmd_type=MMDType.BUY_3,
zs=zs,
bi=bi,
msg=f"回调不破中枢上沿,三类买点 (l={bi.low:.2f} > zg={zs.zg:.2f})",
)
@@ -104,6 +107,7 @@ def _check_sell_point(bi: BI, zs: ZS, all_bis: list[BI]) -> MMD | None:
return MMD(
mmd_type=MMDType.SELL_1,
zs=zs,
bi=bi,
msg=f"中枢上方力度衰减,一类卖点 (h={bi.high:.2f} > zg={zs.zg:.2f})",
)
@@ -117,6 +121,7 @@ def _check_sell_point(bi: BI, zs: ZS, all_bis: list[BI]) -> MMD | None:
return MMD(
mmd_type=MMDType.SELL_2,
zs=zs,
bi=bi,
msg=f"反弹不创新高,二类卖点 (h={bi.high:.2f})",
)
@@ -125,6 +130,7 @@ def _check_sell_point(bi: BI, zs: ZS, all_bis: list[BI]) -> MMD | None:
return MMD(
mmd_type=MMDType.SELL_3,
zs=zs,
bi=bi,
msg=f"反弹不破中枢下沿,三类卖点 (h={bi.high:.2f} < zd={zs.zd:.2f})",
)
+3
View File
@@ -183,6 +183,7 @@ class MMD:
mmd_type: MMDType
zs: ZS | None = None
bi: BI | None = None # 触发该买卖点的笔(用于可视化锚定日期)
msg: str = ""
def __str__(self) -> str:
@@ -204,6 +205,8 @@ class BC:
bc_type: BCType
bc: bool = False # 是否背驰
zs: ZS | None = None
curr: BI | XD | None = None # 当前背驰笔/线段(用于可视化锚定日期)
prev: BI | XD | None = None # 前一同向笔/线段(力度对照基准)
msg: str = ""
def __str__(self) -> str:
+79
View File
@@ -549,3 +549,82 @@ class TestChanlunAnalyser:
assert "zs_count" in d
assert "bis" in d
assert "zss" in d
# 可视化字段:中枢/买卖点/背驰应携带对应 K 线日期(若该样本产出了它们)
import re
date_re = re.compile(r"^\d{4}-\d{2}-\d{2}$")
for zs in d["zss"]:
assert "start_date" in zs
assert "end_date" in zs
if zs["start_date"] is not None:
assert date_re.match(zs["start_date"])
if zs["end_date"] is not None:
assert date_re.match(zs["end_date"])
for mmd in d["mmds"]:
assert "date" in mmd
if mmd["date"] is not None:
assert date_re.match(mmd["date"])
for bc in d["bcs"]:
assert "curr_date" in bc
assert "prev_date" in bc
if bc["curr_date"] is not None:
assert date_re.match(bc["curr_date"])
if bc["prev_date"] is not None:
assert date_re.match(bc["prev_date"])
def test_result_to_dict_with_visual_dates(self) -> None:
"""可视化字段:足够数据下 zss/mmds/bcs 应携带合法 K 线日期。
用一段振荡+趋势的数据,确保能确定性产出中枢/买卖点/背驰,
从而真正覆盖 to_dict() 的日期输出分支。
"""
import math
import re
import pandas as pd
from easy_tdx.chanlun.analyser import ChanlunAnalyser
dates = pd.date_range("2025-01-02", periods=40, freq="B")
highs = [15 + 5 * math.sin(i / 2) + i * 0.2 for i in range(40)]
lows = [highs[i] - 4 for i in range(40)]
df = pd.DataFrame(
{
"datetime": dates,
"open": [h - 2 for h in highs],
"close": [h - 1 for h in highs],
"high": highs,
"low": lows,
"vol": [1000] * 40,
}
)
analyser = ChanlunAnalyser(code="SZ000001")
d = analyser.process_klines(df).to_dict()
date_re = re.compile(r"^\d{4}-\d{2}-\d{2}$")
# 中枢必须有起止日期
assert len(d["zss"]) > 0
for zs in d["zss"]:
assert zs["start_date"] is not None
assert zs["end_date"] is not None
assert date_re.match(zs["start_date"])
assert date_re.match(zs["end_date"])
# 买卖点必须有触发日期
assert len(d["mmds"]) > 0
for mmd in d["mmds"]:
assert mmd["date"] is not None
assert date_re.match(mmd["date"])
# 背驰必须有当前笔 + 对照笔日期
assert len(d["bcs"]) > 0
for bc in d["bcs"]:
assert bc["curr_date"] is not None
assert bc["prev_date"] is not None
assert date_re.match(bc["curr_date"])
assert date_re.match(bc["prev_date"])