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:
Justin Gu
2026-06-07 23:29:52 +08:00
parent 4cab9aa325
commit fd4a1233b4
22 changed files with 3056 additions and 2 deletions
+47
View File
@@ -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",
]
+231
View File
@@ -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
+173
View File
@@ -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)
+102
View File
@@ -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
+56
View File
@@ -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,
}
+76
View File
@@ -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
+101
View File
@@ -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
+104
View File
@@ -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)),
}
+154
View File
@@ -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
+125
View File
@@ -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,
}
+210
View File
@@ -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})"
+128
View File
@@ -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,
)
+103
View File
@@ -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
+143
View File
@@ -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
+3 -1
View File
@@ -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)
+127
View File
@@ -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']}")