mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 16:54:20 +08:00
feat: add chanlun (ChanLun) technical analysis module, bump to v1.7.0
- New chanlun/ subpackage: K-line merge, fractal, bi/xianduan/zhongshu/mmd/beichi - New 'easy-tdx chanlun' CLI command with JSON/table output - MACD calculation (pure numpy, no extra dependencies) - Multi-level analysis (MultiLevelAnalyser) - Pipeline: DataFrame -> merge -> fractal -> bi -> zhongshu -> xd -> mmd -> beichi - 49 offline unit tests covering all calculation steps - Detailed README docs with output explanation - Bump version: pyproject.toml 1.6.1 -> 1.7.0, cli 1.5.0 -> 1.7.0
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
"""缠论(ChanLun)技术分析模块。
|
||||
|
||||
基于缠论理论实现 K 线合并、分型识别、笔/线段/中枢/买卖点/背驰计算。
|
||||
|
||||
核心 API::
|
||||
|
||||
from easy_tdx.chanlun import ChanlunAnalyser, ChanlunConfig
|
||||
|
||||
analyser = ChanlunAnalyser("SZ000001", "DAILY")
|
||||
result = analyser.process_klines(df)
|
||||
print(result.to_dict())
|
||||
"""
|
||||
|
||||
from easy_tdx.chanlun.analyser import ChanlunAnalyser, ChanlunResult # noqa: F401
|
||||
from easy_tdx.chanlun.config import ChanlunConfig # noqa: F401
|
||||
from easy_tdx.chanlun.types import ( # noqa: F401
|
||||
BC,
|
||||
BI,
|
||||
FX,
|
||||
MMD,
|
||||
XD,
|
||||
ZS,
|
||||
BCType,
|
||||
CLKline,
|
||||
Direction,
|
||||
FXType,
|
||||
Kline,
|
||||
MMDType,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ChanlunAnalyser",
|
||||
"ChanlunConfig",
|
||||
"ChanlunResult",
|
||||
"BC",
|
||||
"BCType",
|
||||
"BI",
|
||||
"CLKline",
|
||||
"Direction",
|
||||
"FX",
|
||||
"FXType",
|
||||
"Kline",
|
||||
"MMD",
|
||||
"MMDType",
|
||||
"XD",
|
||||
"ZS",
|
||||
]
|
||||
@@ -0,0 +1,231 @@
|
||||
"""缠论分析器主入口。
|
||||
|
||||
ChanlunAnalyser 接收 easy_tdx 的 K 线 DataFrame,
|
||||
内部执行完整的缠论计算管道:
|
||||
K线合并 → 分型识别 → 笔计算 → 中枢计算 → 线段 → 买卖点 → 背驰。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from easy_tdx.chanlun.beichi import check_bi_beichi # noqa: F401
|
||||
from easy_tdx.chanlun.bi import find_bis
|
||||
from easy_tdx.chanlun.config import ChanlunConfig
|
||||
from easy_tdx.chanlun.fractal import find_fractals
|
||||
from easy_tdx.chanlun.kline_merge import merge_klines
|
||||
from easy_tdx.chanlun.macd import calc_macd # noqa: F401
|
||||
from easy_tdx.chanlun.mmd import find_mmds # noqa: F401
|
||||
from easy_tdx.chanlun.types import BC, BI, FX, MMD, XD, ZS, CLKline, Kline
|
||||
from easy_tdx.chanlun.xd import find_xds # noqa: F401
|
||||
from easy_tdx.chanlun.zs import find_zss
|
||||
|
||||
|
||||
def _df_to_klines(df: pd.DataFrame) -> list[Kline]:
|
||||
"""将 easy_tdx K 线 DataFrame 转为缠论 Kline 列表。
|
||||
|
||||
期望 DataFrame 包含列:datetime, open, close, high, low, vol
|
||||
"""
|
||||
klines: list[Kline] = []
|
||||
for i, row in enumerate(df.itertuples()):
|
||||
dt = getattr(row, "datetime", None) or getattr(row, "date", None)
|
||||
if dt is None:
|
||||
continue
|
||||
vol = getattr(row, "vol", 0.0) or 0.0
|
||||
klines.append(
|
||||
Kline(
|
||||
index=i,
|
||||
date=dt,
|
||||
open=float(row.open),
|
||||
close=float(row.close),
|
||||
high=float(row.high),
|
||||
low=float(row.low),
|
||||
amount=float(vol),
|
||||
)
|
||||
)
|
||||
return klines
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChanlunResult:
|
||||
"""缠论分析结果。"""
|
||||
|
||||
code: str = ""
|
||||
frequency: str = ""
|
||||
klines: list[Kline] = field(default_factory=list)
|
||||
cklines: list[CLKline] = field(default_factory=list)
|
||||
fractals: list[FX] = field(default_factory=list)
|
||||
bis: list[BI] = field(default_factory=list)
|
||||
zss: list[ZS] = field(default_factory=list)
|
||||
xds: list[XD] = field(default_factory=list)
|
||||
mmds: list[MMD] = field(default_factory=list)
|
||||
bcs: list[BC] = field(default_factory=list)
|
||||
macd: dict[str, list[float]] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""将结果转为可序列化的字典(用于 JSON 输出)。"""
|
||||
return {
|
||||
"code": self.code,
|
||||
"frequency": self.frequency,
|
||||
"kline_count": len(self.klines),
|
||||
"ckline_count": len(self.cklines),
|
||||
"fractal_count": len(self.fractals),
|
||||
"bi_count": len(self.bis),
|
||||
"zs_count": len(self.zss),
|
||||
"xd_count": len(self.xds),
|
||||
"mmd_count": len(self.mmds),
|
||||
"bc_count": len(self.bcs),
|
||||
"bis": [
|
||||
{
|
||||
"index": bi.index,
|
||||
"direction": bi.direction.value,
|
||||
"start_date": bi.start.k.date.strftime("%Y-%m-%d"),
|
||||
"end_date": bi.end.k.date.strftime("%Y-%m-%d"),
|
||||
"high": round(bi.high, 2),
|
||||
"low": round(bi.low, 2),
|
||||
"done": bi.is_done(),
|
||||
}
|
||||
for bi in self.bis
|
||||
],
|
||||
"zss": [
|
||||
{
|
||||
"index": zs.index,
|
||||
"zg": round(zs.zg, 2),
|
||||
"zd": round(zs.zd, 2),
|
||||
"gg": round(zs.gg, 2),
|
||||
"dd": round(zs.dd, 2),
|
||||
"line_count": zs.line_count,
|
||||
"done": zs.done,
|
||||
}
|
||||
for zs in self.zss
|
||||
],
|
||||
"xds": [
|
||||
{
|
||||
"index": xd.index,
|
||||
"direction": xd.direction.value,
|
||||
"start_date": xd.start.k.date.strftime("%Y-%m-%d"),
|
||||
"end_date": xd.end.k.date.strftime("%Y-%m-%d"),
|
||||
"high": round(xd.high, 2),
|
||||
"low": round(xd.low, 2),
|
||||
}
|
||||
for xd in self.xds
|
||||
],
|
||||
"mmds": [
|
||||
{
|
||||
"type": mmd.mmd_type.value,
|
||||
"msg": mmd.msg,
|
||||
}
|
||||
for mmd in self.mmds
|
||||
],
|
||||
"bcs": [
|
||||
{
|
||||
"type": bc.bc_type.value,
|
||||
"bc": bc.bc,
|
||||
"msg": bc.msg,
|
||||
}
|
||||
for bc in self.bcs
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class ChanlunAnalyser:
|
||||
"""缠论分析器。
|
||||
|
||||
接收 easy_tdx K 线 DataFrame,执行缠论计算管道。
|
||||
|
||||
用法:
|
||||
analyser = ChanlunAnalyser("SZ000001", "DAILY")
|
||||
analyser.process_klines(df)
|
||||
result = analyser.result
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
code: str = "",
|
||||
frequency: str = "",
|
||||
config: ChanlunConfig | None = None,
|
||||
) -> None:
|
||||
self._code = code
|
||||
self._frequency = frequency
|
||||
self._config = config or ChanlunConfig()
|
||||
self._result = ChanlunResult(
|
||||
code=code,
|
||||
frequency=frequency,
|
||||
)
|
||||
|
||||
@property
|
||||
def config(self) -> ChanlunConfig:
|
||||
return self._config
|
||||
|
||||
@property
|
||||
def result(self) -> ChanlunResult:
|
||||
return self._result
|
||||
|
||||
def process_klines(self, df: pd.DataFrame) -> ChanlunResult:
|
||||
"""处理 K 线 DataFrame,执行缠论计算管道。
|
||||
|
||||
Args:
|
||||
df: easy_tdx 返回的 K 线 DataFrame
|
||||
|
||||
Returns:
|
||||
ChanlunResult 包含所有缠论计算结果
|
||||
"""
|
||||
# Step 1: DataFrame → Kline 列表
|
||||
klines = _df_to_klines(df)
|
||||
self._result.klines = klines
|
||||
|
||||
if not klines:
|
||||
return self._result
|
||||
|
||||
# Step 2: K线包含处理
|
||||
cklines = merge_klines(klines)
|
||||
self._result.cklines = cklines
|
||||
|
||||
# Step 3: 分型识别
|
||||
fractals = find_fractals(cklines, self._config)
|
||||
self._result.fractals = fractals
|
||||
|
||||
# Step 4: 笔计算
|
||||
bis = find_bis(fractals, self._config)
|
||||
self._result.bis = bis
|
||||
|
||||
# Step 5: 中枢计算
|
||||
zss = find_zss(bis, self._config)
|
||||
self._result.zss = zss
|
||||
|
||||
# Step 6: MACD 计算
|
||||
closes = [k.close for k in klines]
|
||||
self._result.macd = calc_macd(
|
||||
closes, self._config.macd_fast, self._config.macd_slow, self._config.macd_signal
|
||||
)
|
||||
|
||||
# Step 7: 线段计算
|
||||
xds = find_xds(bis, self._config)
|
||||
self._result.xds = xds
|
||||
|
||||
# Step 8: 买卖点识别
|
||||
mmds = find_mmds(bis, zss, self._config)
|
||||
self._result.mmds = mmds
|
||||
|
||||
# Step 9: 背驰判断
|
||||
bcs = check_bi_beichi(bis, zss, self._config)
|
||||
self._result.bcs = bcs
|
||||
|
||||
return self._result
|
||||
|
||||
def get_bis(self) -> list[BI]:
|
||||
return self._result.bis
|
||||
|
||||
def get_zss(self) -> list[ZS]:
|
||||
return self._result.zss
|
||||
|
||||
def get_fxs(self) -> list[FX]:
|
||||
return self._result.fractals
|
||||
|
||||
def get_klines(self) -> list[Kline]:
|
||||
return self._result.klines
|
||||
|
||||
def get_cklines(self) -> list[CLKline]:
|
||||
return self._result.cklines
|
||||
@@ -0,0 +1,173 @@
|
||||
"""背驰判断。
|
||||
|
||||
背驰类型:
|
||||
- 笔背驰:相邻同向笔的力度比较(幅度或 MACD 面积减小)
|
||||
- 盘整背驰:中枢内最后一笔力度小于进入中枢的第一笔
|
||||
- 趋势背驰:两个同向中枢之间,离开中枢的笔力度减小
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from easy_tdx.chanlun.config import ChanlunConfig
|
||||
from easy_tdx.chanlun.types import BC, BI, ZS, BCType
|
||||
|
||||
|
||||
def check_bi_beichi(
|
||||
bis: list[BI],
|
||||
zss: list[ZS],
|
||||
config: ChanlunConfig | None = None,
|
||||
) -> list[BC]:
|
||||
"""检查笔级别的背驰。
|
||||
|
||||
简化算法:
|
||||
1. 笔背驰:比较相邻同向笔的幅度(后 < 前 = 背驰)
|
||||
2. 盘整背驰:中枢内最后一笔与进入笔比较
|
||||
3. 趋势背驰:连续两个同向中枢,离开力度减小
|
||||
|
||||
Args:
|
||||
bis: 笔列表
|
||||
zss: 中枢列表
|
||||
config: 缠论配置
|
||||
|
||||
Returns:
|
||||
背驰列表
|
||||
"""
|
||||
if config is None:
|
||||
config = ChanlunConfig()
|
||||
|
||||
if len(bis) < 2:
|
||||
return []
|
||||
|
||||
bcs: list[BC] = []
|
||||
|
||||
# 1. 笔背驰检查
|
||||
bcs.extend(_check_bi_level_beichi(bis))
|
||||
|
||||
# 2. 盘整背驰检查
|
||||
if len(zss) > 0:
|
||||
bcs.extend(_check_pz_beichi(bis, zss))
|
||||
|
||||
# 3. 趋势背驰检查
|
||||
if len(zss) >= 2:
|
||||
bcs.extend(_check_qs_beichi(bis, zss))
|
||||
|
||||
return bcs
|
||||
|
||||
|
||||
def _check_bi_level_beichi(bis: list[BI]) -> list[BC]:
|
||||
"""检查笔级别的力度背驰。"""
|
||||
bcs: list[BC] = []
|
||||
|
||||
# 按方向分组比较
|
||||
for i in range(1, len(bis)):
|
||||
curr = bis[i]
|
||||
# 向前找最近的同向笔
|
||||
for j in range(i - 1, -1, -1):
|
||||
prev = bis[j]
|
||||
if prev.direction == curr.direction:
|
||||
curr_force = _calc_bi_force(curr)
|
||||
prev_force = _calc_bi_force(prev)
|
||||
|
||||
# 力度衰减 = 背驰
|
||||
if curr_force < prev_force and curr_force > 0:
|
||||
bcs.append(
|
||||
BC(
|
||||
bc_type=BCType.BI,
|
||||
bc=True,
|
||||
zs=None,
|
||||
msg=(
|
||||
f"笔背驰: 笔[{curr.index}] 力度={curr_force:.2f} "
|
||||
f"< 笔[{prev.index}] 力度={prev_force:.2f}"
|
||||
),
|
||||
)
|
||||
)
|
||||
break # 只比较最近一个同向笔
|
||||
|
||||
return bcs
|
||||
|
||||
|
||||
def _check_pz_beichi(bis: list[BI], zss: list[ZS]) -> list[BC]:
|
||||
"""检查盘整背驰。"""
|
||||
bcs: list[BC] = []
|
||||
|
||||
for zs in zss:
|
||||
if zs.line_count < 3:
|
||||
continue
|
||||
|
||||
# 中枢内最后一笔 vs 进入中枢的第一笔
|
||||
first_bi = zs.lines[0]
|
||||
last_bi = zs.lines[-1]
|
||||
|
||||
if first_bi.direction == last_bi.direction:
|
||||
first_force = _calc_bi_force(first_bi)
|
||||
last_force = _calc_bi_force(last_bi)
|
||||
|
||||
if last_force < first_force and last_force > 0:
|
||||
bcs.append(
|
||||
BC(
|
||||
bc_type=BCType.PZ,
|
||||
bc=True,
|
||||
zs=zs,
|
||||
msg=(
|
||||
f"盘整背驰: 中枢[{zs.index}] 内末笔力度={last_force:.2f} "
|
||||
f"< 首笔力度={first_force:.2f}"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return bcs
|
||||
|
||||
|
||||
def _check_qs_beichi(bis: list[BI], zss: list[ZS]) -> list[BC]:
|
||||
"""检查趋势背驰。"""
|
||||
bcs: list[BC] = []
|
||||
|
||||
for i in range(1, len(zss)):
|
||||
prev_zs = zss[i - 1]
|
||||
curr_zs = zss[i]
|
||||
|
||||
# 判断两个中枢是否形成趋势(同向排列)
|
||||
if prev_zs.zg >= curr_zs.zg and prev_zs.zd >= curr_zs.zd:
|
||||
# 向下趋势
|
||||
prev_exit_force = _calc_bi_force(prev_zs.lines[-1])
|
||||
curr_exit_force = _calc_bi_force(curr_zs.lines[-1])
|
||||
|
||||
if curr_exit_force < prev_exit_force and curr_exit_force > 0:
|
||||
bcs.append(
|
||||
BC(
|
||||
bc_type=BCType.QS,
|
||||
bc=True,
|
||||
zs=curr_zs,
|
||||
msg=(
|
||||
f"趋势背驰(下): 中枢[{curr_zs.index}] 离开力度={curr_exit_force:.2f} "
|
||||
f"< 中枢[{prev_zs.index}] 离开力度={prev_exit_force:.2f}"
|
||||
),
|
||||
)
|
||||
)
|
||||
elif prev_zs.zg <= curr_zs.zg and prev_zs.zd <= curr_zs.zd:
|
||||
# 向上趋势
|
||||
prev_exit_force = _calc_bi_force(prev_zs.lines[-1])
|
||||
curr_exit_force = _calc_bi_force(curr_zs.lines[-1])
|
||||
|
||||
if curr_exit_force < prev_exit_force and curr_exit_force > 0:
|
||||
bcs.append(
|
||||
BC(
|
||||
bc_type=BCType.QS,
|
||||
bc=True,
|
||||
zs=curr_zs,
|
||||
msg=(
|
||||
f"趋势背驰(上): 中枢[{curr_zs.index}] 离开力度={curr_exit_force:.2f} "
|
||||
f"< 中枢[{prev_zs.index}] 离开力度={prev_exit_force:.2f}"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return bcs
|
||||
|
||||
|
||||
def _calc_bi_force(bi: BI) -> float:
|
||||
"""计算笔的力度(简化:用幅度表示)。
|
||||
|
||||
真正的力度应用 MACD 面积,这里用幅度作为简化替代。
|
||||
"""
|
||||
return abs(bi.high - bi.low)
|
||||
@@ -0,0 +1,102 @@
|
||||
"""笔计算。
|
||||
|
||||
笔的定义:
|
||||
- 由相邻的顶底分型连接而成
|
||||
- 顶→底 = 向下笔,底→顶 = 向上笔
|
||||
- 新笔规则:分型之间至少有 1 根独立缠论 K 线(即分型中间 K 线的 index 差 > 2)
|
||||
- 老笔规则:分型之间至少有 3 根缠论 K 线
|
||||
- 简单笔规则:只要顶底交替即可
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from easy_tdx.chanlun.config import ChanlunConfig
|
||||
from easy_tdx.chanlun.types import BI, FX, Direction, FXType
|
||||
|
||||
|
||||
def _can_form_bi(
|
||||
start: FX,
|
||||
end: FX,
|
||||
config: ChanlunConfig,
|
||||
) -> bool:
|
||||
"""判断两个分型是否可以构成一笔。"""
|
||||
# 顶底必须交替
|
||||
if start.fx_type == end.fx_type:
|
||||
return False
|
||||
|
||||
# 分型之间缠论 K 线的间距
|
||||
# 分型由三根 K 线组成:[left, mid, right]
|
||||
# 独立 K 线数 = end.left.index - start.right.index + 1(如果 >0)
|
||||
gap = end.klines[0].index - start.klines[2].index + 1
|
||||
|
||||
if config.bi_type == "new":
|
||||
# 新笔:至少1根独立K线
|
||||
return gap >= 1
|
||||
elif config.bi_type == "old":
|
||||
# 老笔:至少3根缠论K线在分型之间
|
||||
return gap >= 3
|
||||
else:
|
||||
# simple:只要顶底交替即可
|
||||
return True
|
||||
|
||||
|
||||
def find_bis(
|
||||
fxs: list[FX],
|
||||
config: ChanlunConfig | None = None,
|
||||
) -> list[BI]:
|
||||
"""从分型列表中计算笔。
|
||||
|
||||
算法(贪心):
|
||||
1. 遍历分型列表,维护最后一个有效分型
|
||||
2. 如果当前分型与最后一个有效分型可以成笔,形成新笔
|
||||
3. 如果当前分型与最后一个有效分型同类型(同为顶或同为底),
|
||||
取更极端的那个替换(顶取更高的,底取更低的)
|
||||
|
||||
Args:
|
||||
fxs: 分型列表
|
||||
config: 缠论配置
|
||||
|
||||
Returns:
|
||||
笔列表
|
||||
"""
|
||||
if config is None:
|
||||
config = ChanlunConfig()
|
||||
|
||||
if len(fxs) < 2:
|
||||
return []
|
||||
|
||||
bis: list[BI] = []
|
||||
|
||||
# 用一个指针追踪当前笔的起始分型
|
||||
start_fx = fxs[0]
|
||||
|
||||
for i in range(1, len(fxs)):
|
||||
current_fx = fxs[i]
|
||||
|
||||
# 同类型分型:取更极端的
|
||||
if current_fx.fx_type == start_fx.fx_type:
|
||||
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:
|
||||
start_fx = current_fx
|
||||
continue
|
||||
|
||||
# 异类型分型,检查是否可以成笔
|
||||
if _can_form_bi(start_fx, current_fx, config):
|
||||
direction = Direction.UP if start_fx.fx_type == FXType.DI else Direction.DOWN
|
||||
high = max(start_fx.val, current_fx.val)
|
||||
low = min(start_fx.val, current_fx.val)
|
||||
|
||||
bi = BI(
|
||||
start=start_fx,
|
||||
end=current_fx,
|
||||
direction=direction,
|
||||
index=len(bis),
|
||||
high=high,
|
||||
low=low,
|
||||
)
|
||||
bis.append(bi)
|
||||
start_fx = current_fx
|
||||
# 如果不能成笔(间距不够),继续搜索
|
||||
|
||||
return bis
|
||||
@@ -0,0 +1,56 @@
|
||||
"""缠论计算配置项。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChanlunConfig:
|
||||
"""缠论计算配置。
|
||||
|
||||
所有配置项均有默认值,开箱即用。
|
||||
"""
|
||||
|
||||
# ── 笔配置 ──────────────────────────────────────────────────────────
|
||||
# "new" 新笔(分型之间至少1根独立K线),"old" 老笔(分型之间至少3根),"simple" 简单笔
|
||||
bi_type: str = "new"
|
||||
|
||||
# ── 中枢配置 ────────────────────────────────────────────────────────
|
||||
# "standard" 标准中枢,"dn" 段内中枢
|
||||
zs_type: str = "standard"
|
||||
# 中枢最少重叠线段数(标准中枢 = 3)
|
||||
zs_min_lines: int = 3
|
||||
# 中枢区间来源: "dd" 用顶底点, "ck" 用缠论K线高低, "k" 用原始K线高低
|
||||
zs_qujian: str = "dd"
|
||||
|
||||
# ── 分型配置 ────────────────────────────────────────────────────────
|
||||
# 是否使用严格分型(顶底不能互相包含)
|
||||
fx_strict: bool = True
|
||||
|
||||
# ── 笔区间配置 ──────────────────────────────────────────────────────
|
||||
# "dd" 用顶底点, "ck" 用缠论K线高低, "k" 用原始K线高低
|
||||
bi_qujian: str = "dd"
|
||||
|
||||
# ── 线段配置 ────────────────────────────────────────────────────────
|
||||
# 是否支持笔破坏
|
||||
xd_bi_pohuai: bool = False
|
||||
|
||||
# ── MACD 配置 ───────────────────────────────────────────────────────
|
||||
macd_fast: int = 12
|
||||
macd_slow: int = 26
|
||||
macd_signal: int = 9
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"bi_type": self.bi_type,
|
||||
"zs_type": self.zs_type,
|
||||
"zs_min_lines": self.zs_min_lines,
|
||||
"zs_qujian": self.zs_qujian,
|
||||
"fx_strict": self.fx_strict,
|
||||
"bi_qujian": self.bi_qujian,
|
||||
"xd_bi_pohuai": self.xd_bi_pohuai,
|
||||
"macd_fast": self.macd_fast,
|
||||
"macd_slow": self.macd_slow,
|
||||
"macd_signal": self.macd_signal,
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
"""分型识别。
|
||||
|
||||
顶分型:中间 K 线的高点和低点均为三根中最高。
|
||||
底分型:中间 K 线的高点和低点均为三根中最低。
|
||||
|
||||
严格模式下不允许相等(使用 > 而非 >=)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from easy_tdx.chanlun.config import ChanlunConfig
|
||||
from easy_tdx.chanlun.types import FX, CLKline, FXType
|
||||
|
||||
|
||||
def find_fractals(
|
||||
cklines: list[CLKline],
|
||||
config: ChanlunConfig | None = None,
|
||||
) -> list[FX]:
|
||||
"""从缠论 K 线列表中识别分型。
|
||||
|
||||
扫描每三根相邻的缠论 K 线,判断是否构成顶分型或底分型。
|
||||
|
||||
Args:
|
||||
cklines: 合并后的缠论 K 线列表
|
||||
config: 缠论配置(默认使用 ChanlunConfig())
|
||||
|
||||
Returns:
|
||||
分型列表,按时间顺序排列
|
||||
"""
|
||||
if config is None:
|
||||
config = ChanlunConfig()
|
||||
|
||||
if len(cklines) < 3:
|
||||
return []
|
||||
|
||||
fxs: list[FX] = []
|
||||
|
||||
for i in range(len(cklines) - 2):
|
||||
left = cklines[i]
|
||||
mid = cklines[i + 1]
|
||||
right = cklines[i + 2]
|
||||
|
||||
if config.fx_strict:
|
||||
# 严格模式:中间 K 线的高/低必须严格大于/小于两边
|
||||
is_ding = mid.high > left.high and mid.high > right.high
|
||||
is_di = mid.low < left.low and mid.low < right.low
|
||||
else:
|
||||
# 非严格模式:允许等于
|
||||
is_ding = mid.high >= left.high and mid.high >= right.high
|
||||
is_di = mid.low <= left.low and mid.low <= right.low
|
||||
|
||||
if is_ding and is_di:
|
||||
# 同时满足顶底分型条件(如十字星),跳过
|
||||
continue
|
||||
elif is_ding:
|
||||
fx = FX(
|
||||
fx_type=FXType.DING,
|
||||
k=mid,
|
||||
klines=[left, mid, right],
|
||||
val=mid.high,
|
||||
index=len(fxs),
|
||||
done=True,
|
||||
)
|
||||
fxs.append(fx)
|
||||
elif is_di:
|
||||
fx = FX(
|
||||
fx_type=FXType.DI,
|
||||
k=mid,
|
||||
klines=[left, mid, right],
|
||||
val=mid.low,
|
||||
index=len(fxs),
|
||||
done=True,
|
||||
)
|
||||
fxs.append(fx)
|
||||
|
||||
return fxs
|
||||
@@ -0,0 +1,101 @@
|
||||
"""K 线包含处理(合并)。
|
||||
|
||||
缠论 K 线合并规则:
|
||||
1. 如果前一根 K 线方向向上,当前 K 线被包含时,取「高高」合并
|
||||
2. 如果前一根 K 线方向向下,当前 K 线被包含时,取「低低」合并
|
||||
3. 包含关系判定:K1.high >= K2.high AND K1.low <= K2.low
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from easy_tdx.chanlun.types import CLKline, Kline
|
||||
|
||||
|
||||
def _is_included(a: CLKline, b: CLKline) -> bool:
|
||||
"""判断 a 是否包含 b(a 的高低范围覆盖 b)。"""
|
||||
return a.high >= b.high and a.low <= b.low
|
||||
|
||||
|
||||
def _to_clkline(k: Kline, index: int) -> CLKline:
|
||||
"""将原始 Kline 转为 CLKline。"""
|
||||
return CLKline(
|
||||
k_index=k.index,
|
||||
date=k.date,
|
||||
open=k.open,
|
||||
close=k.close,
|
||||
high=k.high,
|
||||
low=k.low,
|
||||
amount=k.amount,
|
||||
index=index,
|
||||
merged_count=1,
|
||||
direction="",
|
||||
klines=[k],
|
||||
)
|
||||
|
||||
|
||||
def merge_klines(klines: list[Kline]) -> list[CLKline]:
|
||||
"""对原始 K 线列表进行包含处理,返回缠论 K 线列表。
|
||||
|
||||
算法:
|
||||
1. 第一根 K 线直接转为缠论 K 线
|
||||
2. 后续每根 K 线与前一根缠论 K 线比较:
|
||||
a. 如果存在包含关系,根据前一根的方向合并(向上取高高,向下取低低)
|
||||
b. 如果不存在包含关系,作为新的缠论 K 线追加
|
||||
"""
|
||||
if not klines:
|
||||
return []
|
||||
|
||||
result: list[CLKline] = [_to_clkline(klines[0], index=0)]
|
||||
result[0].klines = [klines[0]]
|
||||
|
||||
for i in range(1, len(klines)):
|
||||
k = klines[i]
|
||||
prev = result[-1]
|
||||
candidate = CLKline(
|
||||
k_index=k.index,
|
||||
date=k.date,
|
||||
open=k.open,
|
||||
close=k.close,
|
||||
high=k.high,
|
||||
low=k.low,
|
||||
amount=k.amount,
|
||||
klines=[k],
|
||||
)
|
||||
|
||||
# 判断包含关系(双向判断:prev 包含 candidate 或 candidate 包含 prev)
|
||||
if _is_included(prev, candidate) or _is_included(candidate, prev):
|
||||
# 确定合并方向
|
||||
# 向上:prev.high > prev_prev.high
|
||||
if len(result) >= 2:
|
||||
direction = "up" if prev.high > result[-2].high else "down"
|
||||
else:
|
||||
# 只有第一根,根据前一根 K 线本身的阴阳判断方向
|
||||
# 阳线(close >= open)→ 向上,阴线 → 向下
|
||||
direction = "up" if prev.close >= prev.open else "down"
|
||||
|
||||
if direction == "up":
|
||||
# 向上合并:取高高
|
||||
merged_high = max(prev.high, candidate.high)
|
||||
merged_low = max(prev.low, candidate.low)
|
||||
else:
|
||||
# 向下合并:取低低
|
||||
merged_high = min(prev.high, candidate.high)
|
||||
merged_low = min(prev.low, candidate.low)
|
||||
|
||||
prev.high = merged_high
|
||||
prev.low = merged_low
|
||||
prev.k_index = k.index
|
||||
prev.date = k.date
|
||||
prev.merged_count += 1
|
||||
prev.direction = direction
|
||||
prev.klines.append(k)
|
||||
# 更新 open/close 为最后一根 K 线的值
|
||||
prev.open = k.open
|
||||
prev.close = k.close
|
||||
prev.amount += k.amount
|
||||
else:
|
||||
# 无包含关系,追加新缠论 K 线
|
||||
ck = _to_clkline(k, index=len(result))
|
||||
result.append(ck)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,104 @@
|
||||
"""MACD 指标计算(纯 numpy 实现)。
|
||||
|
||||
MACD 由三部分组成:
|
||||
- DIF(快线): EMA(fast) - EMA(slow)
|
||||
- DEA(慢线): EMA(DIF, signal)
|
||||
- HIST(柱状图): 2 * (DIF - DEA)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def calc_macd(
|
||||
closes: list[float],
|
||||
fast: int = 12,
|
||||
slow: int = 26,
|
||||
signal: int = 9,
|
||||
) -> dict[str, list[float]]:
|
||||
"""计算 MACD 指标。
|
||||
|
||||
Args:
|
||||
closes: 收盘价序列
|
||||
fast: 快线周期
|
||||
slow: 慢线周期
|
||||
signal: 信号线周期
|
||||
|
||||
Returns:
|
||||
{"dif": [...], "dea": [...], "hist": [...]}
|
||||
"""
|
||||
n = len(closes)
|
||||
if n == 0:
|
||||
return {"dif": [], "dea": [], "hist": []}
|
||||
|
||||
# EMA 计算
|
||||
ema_fast = _calc_ema(closes, fast)
|
||||
ema_slow = _calc_ema(closes, slow)
|
||||
|
||||
# DIF = EMA(fast) - EMA(slow)
|
||||
dif = [ema_fast[i] - ema_slow[i] for i in range(n)]
|
||||
|
||||
# DEA = EMA(DIF, signal)
|
||||
dea = _calc_ema(dif, signal)
|
||||
|
||||
# HIST = 2 * (DIF - DEA)
|
||||
hist = [2.0 * (dif[i] - dea[i]) for i in range(n)]
|
||||
|
||||
return {"dif": dif, "dea": dea, "hist": hist}
|
||||
|
||||
|
||||
def _calc_ema(data: list[float], period: int) -> list[float]:
|
||||
"""计算指数移动平均线。
|
||||
|
||||
EMA(t) = price(t) * k + EMA(t-1) * (1 - k)
|
||||
k = 2 / (period + 1)
|
||||
"""
|
||||
n = len(data)
|
||||
if n == 0:
|
||||
return []
|
||||
|
||||
k = 2.0 / (period + 1)
|
||||
result = [0.0] * n
|
||||
|
||||
# 初始值:第一个数据点
|
||||
result[0] = data[0]
|
||||
|
||||
for i in range(1, n):
|
||||
result[i] = data[i] * k + result[i - 1] * (1 - k)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def calc_macd_force(
|
||||
closes: list[float],
|
||||
start_idx: int,
|
||||
end_idx: int,
|
||||
fast: int = 12,
|
||||
slow: int = 26,
|
||||
signal: int = 9,
|
||||
) -> dict[str, float]:
|
||||
"""计算区间内的 MACD 力度(用于背驰判断)。
|
||||
|
||||
Args:
|
||||
closes: 完整收盘价序列
|
||||
start_idx: 起始索引
|
||||
end_idx: 结束索引(含)
|
||||
fast, slow, signal: MACD 参数
|
||||
|
||||
Returns:
|
||||
{"hist_sum": 总柱子面积, "hist_up_sum": 红柱总和, "hist_down_sum": 绿柱总和}
|
||||
"""
|
||||
if start_idx > end_idx or end_idx >= len(closes):
|
||||
return {"hist_sum": 0.0, "hist_up_sum": 0.0, "hist_down_sum": 0.0}
|
||||
|
||||
macd = calc_macd(closes, fast, slow, signal)
|
||||
hist_slice = macd["hist"][start_idx : end_idx + 1]
|
||||
|
||||
hist_abs = [abs(h) for h in hist_slice]
|
||||
hist_up = [h for h in hist_slice if h > 0]
|
||||
hist_down = [h for h in hist_slice if h < 0]
|
||||
|
||||
return {
|
||||
"hist_sum": sum(hist_abs),
|
||||
"hist_up_sum": sum(hist_up),
|
||||
"hist_down_sum": abs(sum(hist_down)),
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
"""买卖点识别。
|
||||
|
||||
缠论三类买卖点:
|
||||
- 一类买点:下跌趋势中最后一个中枢下方的底背驰点
|
||||
- 二类买点:一类买点后回调不创新低的底分型
|
||||
- 三类买点:向上突破中枢后回调不跌破中枢上沿的底分型
|
||||
- 一类卖点:上涨趋势中最后一个中枢上方的顶背驰点(对称)
|
||||
- 二类卖点:一类卖点后反弹不创新高的顶分型
|
||||
- 三类卖点:向下跌破中枢后反弹不突破中枢下沿的顶分型
|
||||
|
||||
简化实现:基于笔和中枢的相对位置关系判断。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from easy_tdx.chanlun.config import ChanlunConfig
|
||||
from easy_tdx.chanlun.types import BI, MMD, ZS, MMDType
|
||||
|
||||
|
||||
def find_mmds(
|
||||
bis: list[BI],
|
||||
zss: list[ZS],
|
||||
config: ChanlunConfig | None = None,
|
||||
) -> list[MMD]:
|
||||
"""从笔和中枢中识别买卖点。
|
||||
|
||||
Args:
|
||||
bis: 笔列表
|
||||
zss: 中枢列表
|
||||
config: 缠论配置
|
||||
|
||||
Returns:
|
||||
买卖点列表
|
||||
"""
|
||||
if config is None:
|
||||
config = ChanlunConfig()
|
||||
|
||||
if len(bis) < 2 or len(zss) == 0:
|
||||
return []
|
||||
|
||||
mmds: list[MMD] = []
|
||||
|
||||
for bi in bis:
|
||||
# 寻找与该笔最近的中枢
|
||||
for zs in reversed(zss):
|
||||
mmd = _check_bi_mmd(bi, zs, bis)
|
||||
if mmd is not None:
|
||||
mmds.append(mmd)
|
||||
break # 每笔最多一个买卖点
|
||||
|
||||
return mmds
|
||||
|
||||
|
||||
def _check_bi_mmd(bi: BI, zs: ZS, all_bis: list[BI]) -> MMD | None:
|
||||
"""检查单笔是否在某中枢附近形成买卖点。"""
|
||||
if bi.direction.value == "down":
|
||||
return _check_buy_point(bi, zs, all_bis)
|
||||
else:
|
||||
return _check_sell_point(bi, zs, all_bis)
|
||||
|
||||
|
||||
def _check_buy_point(bi: BI, zs: ZS, all_bis: list[BI]) -> MMD | None:
|
||||
"""检查向下笔是否形成买点。"""
|
||||
# 一类买点:笔低点低于中枢下沿(中枢下方),且 MACD 力度衰减
|
||||
if bi.low < zs.zd:
|
||||
# 检查力度衰减(简化:比较相邻同向笔的幅度)
|
||||
if _check_force_decreasing(bi, all_bis, "down"):
|
||||
return MMD(
|
||||
mmd_type=MMDType.BUY_1,
|
||||
zs=zs,
|
||||
msg=f"中枢下方力度衰减,一类买点 (l={bi.low:.2f} < zd={zs.zd:.2f})",
|
||||
)
|
||||
|
||||
# 二类买点:前一个同类买点之后回调不创新低
|
||||
if bi.low > zs.zd and bi.low > zs.dd:
|
||||
# 检查是否在二买位置(简化判断)
|
||||
bi_idx = bi.index
|
||||
if bi_idx >= 2:
|
||||
prev_down_bi = all_bis[bi_idx - 2] if bi_idx - 2 < len(all_bis) else None
|
||||
if prev_down_bi and prev_down_bi.direction.value == "down":
|
||||
if bi.low > prev_down_bi.low:
|
||||
return MMD(
|
||||
mmd_type=MMDType.BUY_2,
|
||||
zs=zs,
|
||||
msg=f"回调不创新低,二类买点 (l={bi.low:.2f})",
|
||||
)
|
||||
|
||||
# 三类买点:回调不跌破中枢上沿
|
||||
if bi.low > zs.zg and bi.low > zs.zd:
|
||||
return MMD(
|
||||
mmd_type=MMDType.BUY_3,
|
||||
zs=zs,
|
||||
msg=f"回调不破中枢上沿,三类买点 (l={bi.low:.2f} > zg={zs.zg:.2f})",
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _check_sell_point(bi: BI, zs: ZS, all_bis: list[BI]) -> MMD | None:
|
||||
"""检查向上笔是否形成卖点。"""
|
||||
# 一类卖点:笔高点高于中枢上沿,且力度衰减
|
||||
if bi.high > zs.zg:
|
||||
if _check_force_decreasing(bi, all_bis, "up"):
|
||||
return MMD(
|
||||
mmd_type=MMDType.SELL_1,
|
||||
zs=zs,
|
||||
msg=f"中枢上方力度衰减,一类卖点 (h={bi.high:.2f} > zg={zs.zg:.2f})",
|
||||
)
|
||||
|
||||
# 二类卖点:反弹不创新高
|
||||
if bi.high < zs.zg and bi.high < zs.gg:
|
||||
bi_idx = bi.index
|
||||
if bi_idx >= 2:
|
||||
prev_up_bi = all_bis[bi_idx - 2] if bi_idx - 2 < len(all_bis) else None
|
||||
if prev_up_bi and prev_up_bi.direction.value == "up":
|
||||
if bi.high < prev_up_bi.high:
|
||||
return MMD(
|
||||
mmd_type=MMDType.SELL_2,
|
||||
zs=zs,
|
||||
msg=f"反弹不创新高,二类卖点 (h={bi.high:.2f})",
|
||||
)
|
||||
|
||||
# 三类卖点:反弹不突破中枢下沿
|
||||
if bi.high < zs.zd:
|
||||
return MMD(
|
||||
mmd_type=MMDType.SELL_3,
|
||||
zs=zs,
|
||||
msg=f"反弹不破中枢下沿,三类卖点 (h={bi.high:.2f} < zd={zs.zd:.2f})",
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _check_force_decreasing(bi: BI, all_bis: list[BI], direction: str) -> bool:
|
||||
"""检查力度是否衰减(简化版:比较相邻同向笔的幅度)。"""
|
||||
bi_idx = bi.index
|
||||
if bi_idx < 2 or bi_idx >= len(all_bis):
|
||||
return False
|
||||
|
||||
# 找前一个同向笔
|
||||
for j in range(bi_idx - 1, -1, -1):
|
||||
prev = all_bis[j]
|
||||
if prev.direction.value == direction:
|
||||
if direction == "down":
|
||||
# 比较低点是否创新低,但幅度减小
|
||||
curr_range = bi.high - bi.low
|
||||
prev_range = prev.high - prev.low
|
||||
return curr_range < prev_range and bi.low < prev.low
|
||||
else:
|
||||
curr_range = bi.high - bi.low
|
||||
prev_range = prev.high - prev.low
|
||||
return curr_range < prev_range and bi.high > prev.high
|
||||
|
||||
return False
|
||||
@@ -0,0 +1,125 @@
|
||||
"""多级别联立分析。
|
||||
|
||||
支持同时分析多个 K 线周期(如日线 + 30 分钟)的缠论数据,
|
||||
查看高级别笔在低级别中的走势结构,辅助判断买卖点的有效性。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from easy_tdx.chanlun.analyser import ChanlunAnalyser, ChanlunResult
|
||||
from easy_tdx.chanlun.types import BI
|
||||
|
||||
|
||||
class MultiLevelAnalyser:
|
||||
"""多级别缠论分析器。
|
||||
|
||||
管理多个 ChanlunAnalyser 实例,每个对应一个 K 线周期。
|
||||
支持跨级别查询:高级别笔对应的低级别走势信息。
|
||||
|
||||
用法::
|
||||
|
||||
mla = MultiLevelAnalyser()
|
||||
mla.add_level("daily", ChanlunAnalyser("SZ000001", "DAILY"))
|
||||
mla.add_level("30min", ChanlunAnalyser("SZ000001", "30MIN"))
|
||||
mla.process("daily", df_daily)
|
||||
mla.process("30min", df_30min)
|
||||
|
||||
# 查看日线最后一笔在 30 分钟级别中的走势
|
||||
info = mla.query_low_level_qs("daily", "30min", last_bi)
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._analysers: dict[str, ChanlunAnalyser] = {}
|
||||
|
||||
def add_level(self, name: str, analyser: ChanlunAnalyser) -> None:
|
||||
"""添加一个分析级别。
|
||||
|
||||
Args:
|
||||
name: 级别名称(如 "daily", "30min")
|
||||
analyser: 对应的 ChanlunAnalyser 实例
|
||||
"""
|
||||
self._analysers[name] = analyser
|
||||
|
||||
def process(self, level: str, df: object) -> ChanlunResult:
|
||||
"""处理指定级别的 K 线数据。
|
||||
|
||||
Args:
|
||||
level: 级别名称
|
||||
df: K 线 DataFrame
|
||||
|
||||
Returns:
|
||||
该级别的缠论分析结果
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
if level not in self._analysers:
|
||||
raise KeyError(f"未注册的级别: {level},可用: {list(self._analysers.keys())}")
|
||||
assert isinstance(df, pd.DataFrame)
|
||||
return self._analysers[level].process_klines(df)
|
||||
|
||||
def get_result(self, level: str) -> ChanlunResult | None:
|
||||
"""获取指定级别的分析结果。"""
|
||||
if level in self._analysers:
|
||||
return self._analysers[level].result
|
||||
return None
|
||||
|
||||
def results(self) -> dict[str, ChanlunResult]:
|
||||
"""获取所有级别的分析结果。"""
|
||||
return {name: a.result for name, a in self._analysers.items()}
|
||||
|
||||
def query_low_level_qs(
|
||||
self,
|
||||
high_level: str,
|
||||
low_level: str,
|
||||
high_bi: BI,
|
||||
) -> dict[str, int]:
|
||||
"""查询高级别笔在低级别中的走势信息。
|
||||
|
||||
查找低级别中时间范围落在高级别笔内的所有笔和中枢,
|
||||
统计形成趋势/盘整的情况。
|
||||
|
||||
Args:
|
||||
high_level: 高级别名称
|
||||
low_level: 低级别名称
|
||||
high_bi: 高级别的笔
|
||||
|
||||
Returns:
|
||||
{"bi_count": 低级别笔数, "zs_count": 低级别中枢数,
|
||||
"has_trend": 是否形成趋势, "has_consolidation": 是否形成盘整}
|
||||
"""
|
||||
high_result = self.get_result(high_level)
|
||||
low_result = self.get_result(low_level)
|
||||
|
||||
if high_result is None or low_result is None:
|
||||
return {"bi_count": 0, "zs_count": 0, "has_trend": False, "has_consolidation": False}
|
||||
|
||||
# 高级别笔的时间范围
|
||||
start_date = high_bi.start.k.date
|
||||
end_date = high_bi.end.k.date
|
||||
|
||||
# 筛选低级别中时间范围内的笔
|
||||
low_bis = [
|
||||
bi
|
||||
for bi in low_result.bis
|
||||
if bi.start.k.date >= start_date and bi.end.k.date <= end_date
|
||||
]
|
||||
|
||||
# 筛选低级别中枢
|
||||
low_zss = [
|
||||
zs
|
||||
for zs in low_result.zss
|
||||
if zs.start is not None
|
||||
and zs.end is not None
|
||||
and zs.start.k.date >= start_date
|
||||
and zs.end.k.date <= end_date
|
||||
]
|
||||
|
||||
has_trend = len(low_zss) >= 2
|
||||
has_consolidation = len(low_zss) >= 1
|
||||
|
||||
return {
|
||||
"bi_count": len(low_bis),
|
||||
"zs_count": len(low_zss),
|
||||
"has_trend": has_trend,
|
||||
"has_consolidation": has_consolidation,
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
"""缠论核心数据结构定义。
|
||||
|
||||
参考 chanlun-pro cl_interface.py,去除对 db/exchange 的依赖,
|
||||
使用纯 dataclass + 类型注解,保持 mypy strict 兼容。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
# ── K 线 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@dataclass
|
||||
class Kline:
|
||||
"""原始 K 线。"""
|
||||
|
||||
index: int
|
||||
date: datetime
|
||||
open: float
|
||||
close: float
|
||||
high: float
|
||||
low: float
|
||||
amount: float # 成交量(股数)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f"Kline(i={self.index} {self.date:%Y-%m-%d} "
|
||||
f"o={self.open:.2f} c={self.close:.2f} "
|
||||
f"h={self.high:.2f} l={self.low:.2f})"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CLKline:
|
||||
"""缠论 K 线(包含处理后的合并 K 线)。"""
|
||||
|
||||
k_index: int # 对应原始 K 线中最后一根的 index
|
||||
date: datetime # 合并 K 线最后一根的时间
|
||||
open: float
|
||||
close: float
|
||||
high: float
|
||||
low: float
|
||||
amount: float
|
||||
index: int = 0 # 在缠论 K 线列表中的序号
|
||||
merged_count: int = 1 # 合并了几根原始 K 线
|
||||
has_gap: bool = False # 是否有缺口
|
||||
direction: str = "" # 合并方向 "up" / "down" / ""
|
||||
klines: list[Kline] = field(default_factory=list) # 包含的原始 K 线
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f"CLKline(i={self.index} ki={self.k_index} {self.date:%Y-%m-%d} "
|
||||
f"h={self.high:.2f} l={self.low:.2f} n={self.merged_count})"
|
||||
)
|
||||
|
||||
|
||||
# ── 分型 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class FXType(str, Enum):
|
||||
"""分型类型。"""
|
||||
|
||||
DING = "ding" # 顶分型
|
||||
DI = "di" # 底分型
|
||||
|
||||
|
||||
@dataclass
|
||||
class FX:
|
||||
"""分型对象。"""
|
||||
|
||||
fx_type: FXType
|
||||
k: CLKline # 分型中间那根缠论 K 线
|
||||
klines: list[CLKline] # 构成分型的三根缠论 K 线 [左, 中, 右]
|
||||
val: float # 分型值(顶分型取 high,底分型取 low)
|
||||
index: int = 0 # 分型序号
|
||||
done: bool = True # 分型是否完成
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"FX(i={self.index} {self.fx_type.value} {self.k.date:%Y-%m-%d} val={self.val:.2f})"
|
||||
|
||||
|
||||
# ── 线(笔/线段基类)─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class Direction(str, Enum):
|
||||
"""方向。"""
|
||||
|
||||
UP = "up"
|
||||
DOWN = "down"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Line:
|
||||
"""线的基本定义,笔和线段的基类。"""
|
||||
|
||||
start: FX # 起始分型
|
||||
end: FX # 结束分型
|
||||
direction: Direction # 方向
|
||||
index: int = 0 # 序号
|
||||
high: float = 0.0 # 区间最高价
|
||||
low: float = 0.0 # 区间最低价
|
||||
|
||||
def is_done(self) -> bool:
|
||||
"""线是否完成(结束分型已完成)。"""
|
||||
return self.end.done
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f"Line(i={self.index} {self.direction.value} "
|
||||
f"{self.start.k.date:%Y-%m-%d}→{self.end.k.date:%Y-%m-%d} "
|
||||
f"h={self.high:.2f} l={self.low:.2f})"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BI(Line):
|
||||
"""笔。"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class XD(Line):
|
||||
"""线段。"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# ── 中枢 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@dataclass
|
||||
class ZS:
|
||||
"""中枢对象。"""
|
||||
|
||||
lines: list[BI | XD] = field(default_factory=list) # 构成中枢的线
|
||||
zg: float = 0.0 # 中枢上沿(重叠区间最高)
|
||||
zd: float = 0.0 # 中枢下沿(重叠区间最低)
|
||||
gg: float = 0.0 # 中枢最高点
|
||||
dd: float = 0.0 # 中枢最低点
|
||||
direction: str = "" # 中枢方向 "up"/"down"/""
|
||||
index: int = 0 # 序号
|
||||
done: bool = False # 中枢是否完成
|
||||
start: FX | None = None # 起始分型
|
||||
end: FX | None = None # 结束分型
|
||||
|
||||
def add_line(self, line: BI | XD) -> None:
|
||||
self.lines.append(line)
|
||||
|
||||
@property
|
||||
def line_count(self) -> int:
|
||||
return len(self.lines)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f"ZS(i={self.index} lines={self.line_count} "
|
||||
f"zg={self.zg:.2f} zd={self.zd:.2f} "
|
||||
f"gg={self.gg:.2f} dd={self.dd:.2f} "
|
||||
f"done={self.done})"
|
||||
)
|
||||
|
||||
|
||||
# ── 买卖点 / 背驰 ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class MMDType(str, Enum):
|
||||
"""买卖点类型。"""
|
||||
|
||||
BUY_1 = "1buy"
|
||||
BUY_2 = "2buy"
|
||||
BUY_3 = "3buy"
|
||||
SELL_1 = "1sell"
|
||||
SELL_2 = "2sell"
|
||||
SELL_3 = "3sell"
|
||||
|
||||
|
||||
@dataclass
|
||||
class MMD:
|
||||
"""买卖点。"""
|
||||
|
||||
mmd_type: MMDType
|
||||
zs: ZS | None = None
|
||||
msg: str = ""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"MMD({self.mmd_type.value} {self.msg})"
|
||||
|
||||
|
||||
class BCType(str, Enum):
|
||||
"""背驰类型。"""
|
||||
|
||||
BI = "bi" # 笔背驰
|
||||
PZ = "pz" # 盘整背驰
|
||||
QS = "qs" # 趋势背驰
|
||||
|
||||
|
||||
@dataclass
|
||||
class BC:
|
||||
"""背驰。"""
|
||||
|
||||
bc_type: BCType
|
||||
bc: bool = False # 是否背驰
|
||||
zs: ZS | None = None
|
||||
msg: str = ""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"BC({self.bc_type.value} {self.bc})"
|
||||
@@ -0,0 +1,128 @@
|
||||
"""线段计算。
|
||||
|
||||
线段定义:
|
||||
- 由至少3笔构成
|
||||
- 特征序列:将笔的高低点转化为特征序列
|
||||
- 特征序列分型:判断线段的转折
|
||||
- 简化实现:使用笔的方向和重叠关系判断线段
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from easy_tdx.chanlun.config import ChanlunConfig
|
||||
from easy_tdx.chanlun.types import BI, XD, Direction
|
||||
|
||||
|
||||
def find_xds(
|
||||
bis: list[BI],
|
||||
config: ChanlunConfig | None = None,
|
||||
) -> list[XD]:
|
||||
"""从笔列表中计算线段。
|
||||
|
||||
简化算法(基于中枢):
|
||||
1. 将笔序列划分为线段,每个线段对应一个中枢的形成和离开
|
||||
2. 从第一笔开始,累积笔直到形成中枢(至少3笔有重叠)
|
||||
3. 当后续笔离开中枢时,关闭当前线段,开始新线段
|
||||
|
||||
Args:
|
||||
bis: 笔列表
|
||||
config: 缠论配置
|
||||
|
||||
Returns:
|
||||
线段列表
|
||||
"""
|
||||
if config is None:
|
||||
config = ChanlunConfig()
|
||||
|
||||
if len(bis) < 3:
|
||||
return []
|
||||
|
||||
xds: list[XD] = []
|
||||
|
||||
# 简化线段划分:使用笔的重叠区域判断
|
||||
# 每个线段至少包含3笔(形成一个中枢)
|
||||
i = 0
|
||||
while i < len(bis):
|
||||
# 尝试从第 i 笔开始寻找线段
|
||||
xd_found = False
|
||||
|
||||
# 从3笔开始尝试,逐步扩展
|
||||
for end_offset in range(3, len(bis) - i + 1):
|
||||
segment_bis = bis[i : i + end_offset]
|
||||
|
||||
# 检查这段笔是否构成有意义的线段
|
||||
# 条件:中间的笔有价格重叠(类似中枢),且最后一笔离开重叠区
|
||||
if _forms_xd(segment_bis, config):
|
||||
xd = _create_xd(segment_bis, len(xds))
|
||||
xds.append(xd)
|
||||
i += end_offset - 1 # 下一笔从倒数第2笔开始(共享转折点)
|
||||
xd_found = True
|
||||
break
|
||||
|
||||
if not xd_found:
|
||||
i += 1
|
||||
|
||||
return xds
|
||||
|
||||
|
||||
def _forms_xd(bis: list[BI], config: ChanlunConfig) -> bool:
|
||||
"""判断一组笔是否构成线段。
|
||||
|
||||
简化条件:
|
||||
1. 至少3笔
|
||||
2. 中间的笔有价格重叠区域(类似中枢)
|
||||
3. 最后一笔与重叠区有明确的方向性突破
|
||||
"""
|
||||
if len(bis) < 3:
|
||||
return False
|
||||
|
||||
# 计算中间笔的重叠区域(排除第一笔和最后一笔)
|
||||
inner_bis = bis[1:-1]
|
||||
if not inner_bis:
|
||||
return False
|
||||
|
||||
# 重叠区域
|
||||
overlap_high = min(bi.high for bi in inner_bis)
|
||||
overlap_low = max(bi.low for bi in inner_bis)
|
||||
|
||||
if overlap_high <= overlap_low:
|
||||
return False
|
||||
|
||||
# 第一笔的方向决定线段的主方向
|
||||
main_direction = bis[0].direction
|
||||
|
||||
# 最后一笔应该离开重叠区域
|
||||
last_bi = bis[-1]
|
||||
if main_direction == Direction.UP:
|
||||
# 向上线段:最后一笔应向上突破
|
||||
return last_bi.high > overlap_high
|
||||
else:
|
||||
# 向下线段:最后一笔应向下跌破
|
||||
return last_bi.low < overlap_low
|
||||
|
||||
|
||||
def _create_xd(bis: list[BI], index: int) -> XD:
|
||||
"""从一组笔创建线段。"""
|
||||
start_bi = bis[0]
|
||||
end_bi = bis[-1]
|
||||
|
||||
# 线段方向由第一笔的方向决定
|
||||
direction = start_bi.direction
|
||||
|
||||
# 但如果第一笔向下,最后一笔向上,需要根据整体走势判断
|
||||
if end_bi.direction == Direction.UP and end_bi.high > start_bi.high:
|
||||
direction = Direction.UP
|
||||
elif end_bi.direction == Direction.DOWN and end_bi.low < start_bi.low:
|
||||
direction = Direction.DOWN
|
||||
|
||||
high = max(bi.high for bi in bis)
|
||||
low = min(bi.low for bi in bis)
|
||||
|
||||
return XD(
|
||||
start=start_bi.start,
|
||||
end=end_bi.end,
|
||||
direction=direction,
|
||||
index=index,
|
||||
high=high,
|
||||
low=low,
|
||||
)
|
||||
@@ -0,0 +1,103 @@
|
||||
"""中枢计算。
|
||||
|
||||
中枢定义:至少三笔(或线段)的价格区间有重叠。
|
||||
- zg(上沿)= 重叠区间的最高点
|
||||
- zd(下沿)= 重叠区间的最低点
|
||||
- gg = 中枢内所有笔的最高价
|
||||
- dd = 中枢内所有笔的最低价
|
||||
|
||||
标准中枢算法:
|
||||
1. 逐笔扫描,维护当前中枢的 zg/zd
|
||||
2. 新笔进入时,更新 gg/dd
|
||||
3. 新笔离开(不再与 [zd, zg] 重叠)时,关闭中枢
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from easy_tdx.chanlun.config import ChanlunConfig
|
||||
from easy_tdx.chanlun.types import BI, ZS
|
||||
|
||||
|
||||
def find_zss(
|
||||
bis: list[BI],
|
||||
config: ChanlunConfig | None = None,
|
||||
) -> list[ZS]:
|
||||
"""从笔列表中计算中枢。
|
||||
|
||||
算法:
|
||||
1. 逐笔扫描
|
||||
2. 维护当前中枢的 zg/zd(重叠区间)
|
||||
3. 新笔与 [zd, zg] 有重叠 → 加入中枢,更新重叠区间
|
||||
4. 新笔与 [zd, zg] 无重叠 → 关闭当前中枢,开始新中枢
|
||||
|
||||
Args:
|
||||
bis: 笔列表
|
||||
config: 缠论配置
|
||||
|
||||
Returns:
|
||||
中枢列表
|
||||
"""
|
||||
if config is None:
|
||||
config = ChanlunConfig()
|
||||
|
||||
if len(bis) < 3:
|
||||
return []
|
||||
|
||||
zss: list[ZS] = []
|
||||
current_zs: ZS | None = None
|
||||
|
||||
for bi in bis:
|
||||
if current_zs is None:
|
||||
# 尝试开始新中枢:需要至少前两笔有重叠
|
||||
# 中枢至少需要3笔,先累积
|
||||
if len(zss) == 0 or True:
|
||||
# 用当前笔初始化中枢候选
|
||||
current_zs = ZS(
|
||||
lines=[bi],
|
||||
zg=bi.high,
|
||||
zd=bi.low,
|
||||
gg=bi.high,
|
||||
dd=bi.low,
|
||||
start=bi.start,
|
||||
end=bi.end,
|
||||
index=len(zss),
|
||||
)
|
||||
continue
|
||||
|
||||
# 当前笔与中枢是否有重叠
|
||||
overlap_high = min(current_zs.zg, bi.high)
|
||||
overlap_low = max(current_zs.zd, bi.low)
|
||||
|
||||
if overlap_high > overlap_low:
|
||||
# 有重叠,加入中枢
|
||||
current_zs.add_line(bi)
|
||||
current_zs.zg = overlap_high
|
||||
current_zs.zd = overlap_low
|
||||
current_zs.gg = max(current_zs.gg, bi.high)
|
||||
current_zs.dd = min(current_zs.dd, bi.low)
|
||||
current_zs.end = bi.end
|
||||
else:
|
||||
# 无重叠
|
||||
if current_zs.line_count >= config.zs_min_lines:
|
||||
# 中枢成立
|
||||
current_zs.done = True
|
||||
zss.append(current_zs)
|
||||
current_zs = None
|
||||
# 当前笔作为新中枢的起始
|
||||
current_zs = ZS(
|
||||
lines=[bi],
|
||||
zg=bi.high,
|
||||
zd=bi.low,
|
||||
gg=bi.high,
|
||||
dd=bi.low,
|
||||
start=bi.start,
|
||||
end=bi.end,
|
||||
index=len(zss),
|
||||
)
|
||||
|
||||
# 处理最后一个中枢
|
||||
if current_zs is not None and current_zs.line_count >= config.zs_min_lines:
|
||||
current_zs.done = False # 最后一根K线未确定,中枢未完成
|
||||
zss.append(current_zs)
|
||||
|
||||
return zss
|
||||
@@ -0,0 +1,143 @@
|
||||
"""走势段/趋势段计算。
|
||||
|
||||
走势段(ZSD):由线段(XD)构成,类似于笔由分型构成。
|
||||
走势段的识别基于线段的方向和重叠关系。
|
||||
|
||||
趋势段(QSD):具有明确方向性的走势段,连续同向排列。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from easy_tdx.chanlun.config import ChanlunConfig
|
||||
from easy_tdx.chanlun.types import XD, Direction
|
||||
|
||||
|
||||
def find_zsds(
|
||||
xds: list[XD],
|
||||
config: ChanlunConfig | None = None,
|
||||
) -> list[XD]:
|
||||
"""从线段列表中计算走势段。
|
||||
|
||||
算法:
|
||||
1. 将相邻同向线段合并为走势段
|
||||
2. 当线段方向反转时,前一个走势段结束,新的走势段开始
|
||||
3. 走势段的方向由其中线段的主方向决定
|
||||
|
||||
Args:
|
||||
xds: 线段列表
|
||||
config: 缠论配置
|
||||
|
||||
Returns:
|
||||
走势段列表
|
||||
"""
|
||||
if config is None:
|
||||
config = ChanlunConfig()
|
||||
|
||||
if len(xds) < 1:
|
||||
return []
|
||||
|
||||
zsds: list[XD] = []
|
||||
current_start = xds[0]
|
||||
current_direction = xds[0].direction
|
||||
|
||||
for i in range(1, len(xds)):
|
||||
xd = xds[i]
|
||||
|
||||
if xd.direction != current_direction:
|
||||
# 方向反转,关闭当前走势段
|
||||
prev_xd = xds[i - 1]
|
||||
zsd = _create_zsd(current_start, prev_xd, current_direction, len(zsds))
|
||||
zsds.append(zsd)
|
||||
current_start = xd
|
||||
current_direction = xd.direction
|
||||
|
||||
# 处理最后一个走势段
|
||||
if len(xds) > 0:
|
||||
last_xd = xds[-1]
|
||||
zsd = _create_zsd(current_start, last_xd, current_direction, len(zsds))
|
||||
zsds.append(zsd)
|
||||
|
||||
return zsds
|
||||
|
||||
|
||||
def find_qsds(
|
||||
xds: list[XD],
|
||||
config: ChanlunConfig | None = None,
|
||||
) -> list[XD]:
|
||||
"""从线段列表中计算趋势段。
|
||||
|
||||
趋势段是具有明确趋势方向的走势段:
|
||||
- 向上趋势:每个线段的高点和低点逐步抬高
|
||||
- 向下趋势:每个线段的高点和低点逐步降低
|
||||
|
||||
Args:
|
||||
xds: 线段列表
|
||||
config: 缠论配置
|
||||
|
||||
Returns:
|
||||
趋势段列表
|
||||
"""
|
||||
if config is None:
|
||||
config = ChanlunConfig()
|
||||
|
||||
if len(xds) < 2:
|
||||
return []
|
||||
|
||||
# 先计算走势段
|
||||
zsds = find_zsds(xds, config)
|
||||
|
||||
# 从走势段中筛选趋势段
|
||||
qsds: list[XD] = []
|
||||
|
||||
for i in range(len(zsds)):
|
||||
zsd = zsds[i]
|
||||
|
||||
# 检查走势段内部是否形成趋势
|
||||
if _is_trending(zsd, zsds, i):
|
||||
qsds.append(zsd)
|
||||
|
||||
return qsds
|
||||
|
||||
|
||||
def _create_zsd(
|
||||
start_xd: XD,
|
||||
end_xd: XD,
|
||||
direction: Direction,
|
||||
index: int,
|
||||
) -> XD:
|
||||
"""从起止线段创建走势段。"""
|
||||
high = max(start_xd.high, end_xd.high)
|
||||
low = min(start_xd.low, end_xd.low)
|
||||
|
||||
return XD(
|
||||
start=start_xd.start,
|
||||
end=end_xd.end,
|
||||
direction=direction,
|
||||
index=index,
|
||||
high=high,
|
||||
low=low,
|
||||
)
|
||||
|
||||
|
||||
def _is_trending(
|
||||
zsd: XD,
|
||||
all_zsds: list[XD],
|
||||
zsd_index: int,
|
||||
) -> bool:
|
||||
"""判断走势段是否形成趋势。
|
||||
|
||||
简化判断:走势段跨越的幅度是否足够大(至少 2 个线段的范围)。
|
||||
"""
|
||||
# 单线段走势段不构成趋势
|
||||
if zsd.start == zsd.end:
|
||||
return False
|
||||
|
||||
# 向上趋势:走势段高点高于起点高点
|
||||
if zsd.direction == Direction.UP:
|
||||
return zsd.high > zsd.start.k.h if hasattr(zsd.start.k, "h") else True
|
||||
|
||||
# 向下趋势:走势段低点低于起点低点
|
||||
if zsd.direction == Direction.DOWN:
|
||||
return zsd.low < zsd.start.k.l if hasattr(zsd.start.k, "l") else True
|
||||
|
||||
return True
|
||||
@@ -8,6 +8,7 @@ from .cmd_admin import ping, version
|
||||
from .cmd_auction import auction
|
||||
from .cmd_board import belong_board, board_list, board_members, board_ranking, board_summary
|
||||
from .cmd_capital import capital_flow
|
||||
from .cmd_chanlun import chanlun
|
||||
from .cmd_ex import ex
|
||||
from .cmd_finance import f10, fund_flow
|
||||
from .cmd_indicator import indicator, indicator_list
|
||||
@@ -21,7 +22,7 @@ from .cmd_transaction import transaction
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version="1.5.0", prog_name="easy-tdx")
|
||||
@click.version_option(version="1.7.0", prog_name="easy-tdx")
|
||||
def cli() -> None:
|
||||
"""easy-tdx -- 通达信行情数据 CLI(默认 JSON 输出,适合 Agent 使用)。
|
||||
|
||||
@@ -64,3 +65,4 @@ cli.add_command(ex)
|
||||
cli.add_command(indicator)
|
||||
cli.add_command(indicator_list)
|
||||
cli.add_command(offline)
|
||||
cli.add_command(chanlun)
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""缠论分析命令。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import click
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument("market")
|
||||
@click.argument("code")
|
||||
@click.option(
|
||||
"--period", default="DAILY", help="K线周期: DAILY/5MIN/15MIN/30MIN/60MIN/1MIN/WEEKLY/MONTHLY"
|
||||
)
|
||||
@click.option("--count", default=800, type=int, help="K线数量")
|
||||
@click.option("--adjust", default="NONE", help="复权: NONE/QFQ/HFQ")
|
||||
@click.option("--table", "use_table", is_flag=True, help="表格输出")
|
||||
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
|
||||
def chanlun(
|
||||
market: str,
|
||||
code: str,
|
||||
period: str,
|
||||
count: int,
|
||||
adjust: str,
|
||||
use_table: bool,
|
||||
output_fmt: str,
|
||||
) -> None:
|
||||
"""缠论分析:计算 K 线的笔、中枢等缠论指标。
|
||||
|
||||
示例:
|
||||
|
||||
easy-tdx chanlun SZ 000001
|
||||
|
||||
easy-tdx chanlun SH 600519 --adjust QFQ --table
|
||||
|
||||
easy-tdx chanlun SZ 000001 --period 30MIN
|
||||
"""
|
||||
from ..chanlun.analyser import ChanlunAnalyser
|
||||
from .conn import get_mac_client
|
||||
from .parsers import parse_adjust, parse_market, parse_period
|
||||
|
||||
mkt = parse_market(market)
|
||||
with get_mac_client() as client:
|
||||
df = client.get_stock_kline(
|
||||
mkt,
|
||||
code,
|
||||
period=parse_period(period),
|
||||
start=0,
|
||||
count=count,
|
||||
adjust=parse_adjust(adjust),
|
||||
)
|
||||
|
||||
analyser = ChanlunAnalyser(code=code, frequency=period)
|
||||
result = analyser.process_klines(df)
|
||||
|
||||
result_dict = result.to_dict()
|
||||
|
||||
fmt = "table" if use_table else output_fmt
|
||||
if fmt == "json":
|
||||
click.echo(json.dumps(result_dict, ensure_ascii=False, indent=2))
|
||||
elif fmt == "table":
|
||||
_print_table(result_dict)
|
||||
else:
|
||||
click.echo(json.dumps(result_dict, ensure_ascii=False))
|
||||
|
||||
|
||||
def _print_table(result: dict) -> None:
|
||||
"""以表格形式输出缠论分析结果。"""
|
||||
click.echo(f"标的: {result['code']} 周期: {result['frequency']}")
|
||||
click.echo(f"原始K线: {result['kline_count']} 缠论K线: {result['ckline_count']}")
|
||||
click.echo(
|
||||
f"分型: {result['fractal_count']} 笔: {result['bi_count']} "
|
||||
f"中枢: {result['zs_count']} 线段: {result.get('xd_count', 0)}"
|
||||
)
|
||||
mmd_count = result.get("mmd_count", 0)
|
||||
bc_count = result.get("bc_count", 0)
|
||||
if mmd_count or bc_count:
|
||||
click.echo(f"买卖点: {mmd_count} 背驰: {bc_count}")
|
||||
click.echo()
|
||||
|
||||
if result["bis"]:
|
||||
click.echo("── 笔 ──")
|
||||
for bi in result["bis"]:
|
||||
direction = "↑" if bi["direction"] == "up" else "↓"
|
||||
done = "✓" if bi["done"] else "…"
|
||||
click.echo(
|
||||
f" [{bi['index']}] {direction} "
|
||||
f"{bi['start_date']} → {bi['end_date']} "
|
||||
f"h={bi['high']} l={bi['low']} {done}"
|
||||
)
|
||||
click.echo()
|
||||
|
||||
if result["zss"]:
|
||||
click.echo("── 中枢 ──")
|
||||
for zs in result["zss"]:
|
||||
done = "✓" if zs["done"] else "…"
|
||||
click.echo(
|
||||
f" [{zs['index']}] "
|
||||
f"zg={zs['zg']} zd={zs['zd']} "
|
||||
f"gg={zs['gg']} dd={zs['dd']} "
|
||||
f"lines={zs['line_count']} {done}"
|
||||
)
|
||||
click.echo()
|
||||
|
||||
if result.get("xds"):
|
||||
click.echo("── 线段 ──")
|
||||
for xd in result["xds"]:
|
||||
direction = "↑" if xd["direction"] == "up" else "↓"
|
||||
click.echo(
|
||||
f" [{xd['index']}] {direction} "
|
||||
f"{xd['start_date']} → {xd['end_date']} "
|
||||
f"h={xd['high']} l={xd['low']}"
|
||||
)
|
||||
click.echo()
|
||||
|
||||
if result.get("mmds"):
|
||||
click.echo("── 买卖点 ──")
|
||||
for mmd in result["mmds"]:
|
||||
click.echo(f" {mmd['type']}: {mmd['msg']}")
|
||||
click.echo()
|
||||
|
||||
if result.get("bcs"):
|
||||
click.echo("── 背驰 ──")
|
||||
for bc in result["bcs"]:
|
||||
status = "✓" if bc["bc"] else "✗"
|
||||
click.echo(f" [{status}] {bc['type']}: {bc['msg']}")
|
||||
Reference in New Issue
Block a user