缠论(Chanlun) 完整实现 --- 从分型到买卖点 + 多级别联立
参考的最佳 GitHub 开源库
| 仓库 | Stars | 特点 |
|---|---|---|
| czsc (zengbin93) | ~5k+ | 最全面、工业级、持续维护 |
| chan.py (Vespa314) | ~1k+ | 代码清晰、学术严谨 |
| chanlun-pro | --- | 带 Web UI、专业级 |
以下代码综合了上述库的核心算法思路,重新整理为单文件、即插即用的实现。
完整源码:chanlun.py
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
chanlun.py --- 缠论核心引擎 (完整实现)
=====================================
层次: K线合并 → 分型 → 笔 → 线段 → 中枢 → 买卖点
参考: czsc / chan.py / chanlun-pro
用法:
from chanlun import ChanLun, MultiTimeframeChanLun
analyzer = ChanLun(df) # df: 含 OHLC 的 DataFrame
signals = analyzer.get_signals()
mtf = MultiTimeframeChanLun({'5min': df5, '1min': df1})
combined = mtf.get_combined_signals(higher='5min', lower='1min')
"""
from __future__ import annotations
import pandas as pd
import numpy as np
from dataclasses import dataclass, field
from enum import Enum, IntEnum
from typing import List, Optional, Dict
# ================================================================
# 1. 枚举 & 数据结构
# ================================================================
class Direction(IntEnum):
UP = 1
DOWN = -1
class FxType(IntEnum):
TOP = 1 # 顶分型
BOTTOM = -1 # 底分型
class Mark(Enum):
B1 = 'B1' # 第一类买点 --- 趋势背驰 / 跌破中枢后反转
B2 = 'B2' # 第二类买点 --- 回调不破前低 / 不破中枢下沿
B3 = 'B3' # 第三类买点 --- 回踩不破中枢上沿(强势)
S1 = 'S1' # 第一类卖点
S2 = 'S2' # 第二类卖点
S3 = 'S3' # 第三类卖点
@dataclass
class RawBar:
i: int; dt: str
o: float; h: float; l: float; c: float; v: float = 0.0
@dataclass
class NewBar:
"""合并后K线"""
i: int; dt: str
o: float; h: float; l: float; c: float
raw_start: int; raw_end: int
direction: Optional[Direction] = None
elements: int = 1 # 包含了多少根原始K线
@dataclass
class Fractal:
"""分型"""
fx_type: FxType
idx: int # 分型序号
ki: int # 中间K线在 merged 列表中的下标
dt: str
val: float # 顶分型=high, 底分型=low
high: float # 三根K线区间最高
low: float # 三根K线区间最低
@dataclass
class Bi:
"""笔"""
idx: int
direction: Direction # UP = 底→顶, DOWN = 顶→底
fx_a: Fractal # 起始分型
fx_b: Fractal # 结束分型
@property
def high(self): return max(self.fx_a.high, self.fx_b.high)
@property
def low(self): return min(self.fx_a.low, self.fx_b.low)
@property
def sdt(self): return self.fx_a.dt
@property
def edt(self): return self.fx_b.dt
@property
def power(self): return abs(self.fx_b.val - self.fx_a.val)
@dataclass
class Xd:
"""线段"""
idx: int
direction: Direction
bis: List[Bi] = field(default_factory=list)
@property
def high(self): return max(b.high for b in self.bis) if self.bis else 0
@property
def low(self): return min(b.low for b in self.bis) if self.bis else 0
@property
def sdt(self): return self.bis[0].sdt if self.bis else ''
@property
def edt(self): return self.bis[-1].edt if self.bis else ''
@dataclass
class ZhongShu:
"""中枢"""
idx: int
ZG: float # 中枢上沿 min(各笔high)
ZD: float # 中枢下沿 max(各笔low)
GG: float # 区间最高
DD: float # 区间最低
bis: List[Bi] = field(default_factory=list)
@property
def valid(self): return self.ZD < self.ZG
@property
def sdt(self): return self.bis[0].sdt if self.bis else ''
@property
def edt(self): return self.bis[-1].edt if self.bis else ''
@dataclass
class Signal:
"""买卖点"""
mark: Mark
dt: str
price: float
bi_idx: int
zs_ZG: float = 0
zs_ZD: float = 0
@property
def is_buy(self): return self.mark in (Mark.B1, Mark.B2, Mark.B3)
@property
def name_cn(self):
_m = {Mark.B1:'第一类买点', Mark.B2:'第二类买点', Mark.B3:'第三类买点',
Mark.S1:'第一类卖点', Mark.S2:'第二类卖点', Mark.S3:'第三类卖点'}
return _m[self.mark]
# ================================================================
# 2. 缠论参数
# ================================================================
@dataclass
class ChanConfig:
bi_min_gap: int = 4 # 笔: 两分型中间K线最小间隔 (新笔=4, 旧笔=3)
zs_min_bi: int = 3 # 中枢: 至少几笔
# ================================================================
# 3. 核心引擎
# ================================================================
class ChanLun:
"""
缠论分析器
----------
参数:
df : pd.DataFrame --- 必须含 open/high/low/close 列
config : ChanConfig
freq : 周期标签 (如 '5min')
"""
def __init__(self, df: pd.DataFrame,
config: ChanConfig | None = None,
freq: str = ''):
self.cfg = config or ChanConfig()
self.freq = freq
self.df = self._norm(df)
# ---------- 结果容器 ----------
self.bars: List[NewBar] = []
self.fractals: List[Fractal] = []
self.bis: List[Bi] = []
self.xds: List[Xd] = []
self.zhong_shus:List[ZhongShu] = []
self.signals: List[Signal] = []
self._run()
# ---------- 列名标准化 ----------
@staticmethod
def _norm(df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
alias = {
'日期':'datetime','时间':'datetime','date':'datetime',
'dt':'datetime','Date':'datetime','Datetime':'datetime',
'timestamp':'datetime','Timestamp':'datetime',
'开盘':'open','开盘价':'open','Open':'open',
'最高':'high','最高价':'high','High':'high',
'最低':'low','最低价':'low','Low':'low',
'收盘':'close','收盘价':'close','Close':'close',
'成交量':'volume','Volume':'volume','vol':'volume',
}
rn = {k: v for k, v in alias.items()
if k in df.columns and v not in df.columns}
if rn:
df = df.rename(columns=rn)
for c in ('open','high','low','close'):
if c not in df.columns:
raise ValueError(f"缺少列: {c}")
if 'datetime' not in df.columns:
if df.index.name and 'date' in str(df.index.name).lower():
df = df.reset_index()
df.rename(columns={df.columns[0]:'datetime'}, inplace=True)
else:
df['datetime'] = range(len(df))
if 'volume' not in df.columns:
df['volume'] = 0.0
return df.reset_index(drop=True)
# ---------- 主流程 ----------
def _run(self):
raws = self._build_raw()
self.bars = self._merge(raws)
self.fractals = self._find_fx()
self.bis = self._find_bi()
self.xds = self._find_xd()
self.zhong_shus= self._find_zs()
self.signals = self._find_signals()
# ============================================================
# Step 0 原始K线
# ============================================================
def _build_raw(self) -> List[RawBar]:
out = []
for i, r in self.df.iterrows():
out.append(RawBar(
i=int(i), dt=str(r['datetime']),
o=float(r['open']), h=float(r['high']),
l=float(r['low']), c=float(r['close']),
v=float(r.get('volume', 0))
))
return out
# ============================================================
# Step 1 K线合并 (处理包含关系)
# ============================================================
@staticmethod
def _merge(raws: List[RawBar]) -> List[NewBar]:
if not raws:
return []
def _incl(a: NewBar, b: NewBar) -> bool:
return (a.h >= b.h and a.l <= b.l) or (b.h >= a.h and b.l <= a.l)
res: List[NewBar] = []
first = raws[0]
res.append(NewBar(i=0, dt=first.dt,
o=first.o, h=first.h, l=first.l, c=first.c,
raw_start=0, raw_end=0))
for k in range(1, len(raws)):
r = raws[k]
cur = NewBar(i=0, dt=r.dt,
o=r.o, h=r.h, l=r.l, c=r.c,
raw_start=r.i, raw_end=r.i)
last = res[-1]
if _incl(last, cur):
# 判断方向
if len(res) >= 2:
d = Direction.UP if last.h >= res[-2].h else Direction.DOWN
else:
d = Direction.UP if cur.c >= cur.o else Direction.DOWN
if d == Direction.UP:
nh, nl = max(last.h, cur.h), max(last.l, cur.l)
else:
nh, nl = min(last.h, cur.h), min(last.l, cur.l)
res[-1] = NewBar(
i=last.i, dt=cur.dt,
o=last.o, h=nh, l=nl, c=cur.c,
raw_start=last.raw_start, raw_end=cur.raw_end,
direction=d, elements=last.elements + 1
)
else:
cur.direction = Direction.UP if cur.h > last.h else Direction.DOWN
cur.i = len(res)
res.append(cur)
for idx, b in enumerate(res):
b.i = idx
return res
# ============================================================
# Step 2 分型识别
# ============================================================
def _find_fx(self) -> List[Fractal]:
bars = self.bars
if len(bars) < 3:
return []
out: List[Fractal] = []
fi = 0
for i in range(1, len(bars) - 1):
L, M, R = bars[i-1], bars[i], bars[i+1]
if M.h > L.h and M.h > R.h:
out.append(Fractal(FxType.TOP, fi, M.i, M.dt,
M.h, max(L.h,M.h,R.h), min(L.l,M.l,R.l)))
fi += 1
elif M.l < L.l and M.l < R.l:
out.append(Fractal(FxType.BOTTOM, fi, M.i, M.dt,
M.l, max(L.h,M.h,R.h), min(L.l,M.l,R.l)))
fi += 1
return out
# ============================================================
# Step 3 笔
# ============================================================
def _find_bi(self) -> List[Bi]:
fxs = self._alternate(self.fractals)
if len(fxs) < 2:
return []
bis: List[Bi] = []
pending = fxs[0]
for f in fxs[1:]:
# 同类 → 保留极值
if f.fx_type == pending.fx_type:
if f.fx_type == FxType.TOP and f.val > pending.val:
pending = f
elif f.fx_type == FxType.BOTTOM and f.val < pending.val:
pending = f
continue
# 异类 → 检查最小间隔
if abs(f.ki - pending.ki) < self.cfg.bi_min_gap:
continue
# 检查价格关系
ok = False
if pending.fx_type == FxType.BOTTOM and f.fx_type == FxType.TOP:
ok = f.val > pending.val # 向上笔: 顶 > 底
elif pending.fx_type == FxType.TOP and f.fx_type == FxType.BOTTOM:
ok = f.val < pending.val # 向下笔: 底 < 顶
if ok:
d = Direction.UP if pending.fx_type == FxType.BOTTOM else Direction.DOWN
bis.append(Bi(idx=len(bis), direction=d, fx_a=pending, fx_b=f))
pending = f
return bis
@staticmethod
def _alternate(fxs: List[Fractal]) -> List[Fractal]:
"""确保分型严格交替: 顶-底-顶-底 ..."""
if not fxs:
return []
res = [fxs[0]]
for f in fxs[1:]:
if f.fx_type == res[-1].fx_type:
if f.fx_type == FxType.TOP and f.val >= res[-1].val:
res[-1] = f
elif f.fx_type == FxType.BOTTOM and f.val <= res[-1].val:
res[-1] = f
else:
res.append(f)
return res
# ============================================================
# Step 4 线段 (简化特征序列法)
# ============================================================
def _find_xd(self) -> List[Xd]:
bis = self.bis
if len(bis) < 3:
return []
xds: List[Xd] = []
start = 0
d = bis[0].direction
i = 2
while i < len(bis):
broken = False
seg_bis = bis[start:i+1]
if d == Direction.UP:
ups = [b for b in seg_bis if b.direction == Direction.UP]
dns = [b for b in seg_bis if b.direction == Direction.DOWN]
if len(ups) >= 2 and ups[-1].high < ups[-2].high:
if (len(dns) >= 2 and dns[-1].low < dns[-2].low) \
or bis[i].low < bis[start].low:
broken = True
else:
dns = [b for b in seg_bis if b.direction == Direction.DOWN]
ups = [b for b in seg_bis if b.direction == Direction.UP]
if len(dns) >= 2 and dns[-1].low > dns[-2].low:
if (len(ups) >= 2 and ups[-1].high > ups[-2].high) \
or bis[i].high > bis[start].high:
broken = True
if broken and (i - start) >= 2:
chunk = bis[start:i]
if len(chunk) >= 3:
xds.append(Xd(idx=len(xds), direction=d, bis=chunk))
start = i - 1
d = Direction.DOWN if d == Direction.UP else Direction.UP
i += 1
# 尾部
rest = bis[start:]
if len(rest) >= 3:
xds.append(Xd(idx=len(xds), direction=d, bis=rest))
return xds
# ============================================================
# Step 5 中枢
# ============================================================
def _find_zs(self) -> List[ZhongShu]:
bis = self.bis
n = self.cfg.zs_min_bi
if len(bis) < n:
return []
out: List[ZhongShu] = []
i = 0
while i <= len(bis) - n:
grp = bis[i:i+n]
zg = min(b.high for b in grp)
zd = max(b.low for b in grp)
if zd >= zg:
i += 1
continue
# 扩展中枢
members = list(grp)
gg = max(b.high for b in grp)
dd = min(b.low for b in grp)
j = i + n
while j < len(bis):
bj = bis[j]
if bj.high > zd and bj.low < zg:
members.append(bj)
gg = max(gg, bj.high)
dd = min(dd, bj.low)
j += 1
else:
break
out.append(ZhongShu(idx=len(out),
ZG=zg, ZD=zd, GG=gg, DD=dd,
bis=members))
i = j if j > i + n else i + n
return out
# ============================================================
# Step 6 买卖点
# ============================================================
def _find_signals(self) -> List[Signal]:
if not self.bis or not self.zhong_shus:
return []
all_sig: List[Signal] = []
for zs in self.zhong_shus:
last_bi_idx = zs.bis[-1].idx
post = [b for b in self.bis if b.idx > last_bi_idx]
if not post:
continue
# ---- 买点 ----
self._buy(zs, post, all_sig)
# ---- 卖点 ----
self._sell(zs, post, all_sig)
# 去重 & 排序
seen = set()
uniq = []
for s in all_sig:
key = (s.mark.value, s.dt, s.price)
if key not in seen:
seen.add(key)
uniq.append(s)
uniq.sort(key=lambda x: x.bi_idx)
return uniq
# ---- B1 / B2 / B3 ----
@staticmethod
def _buy(zs: ZhongShu, post: List[Bi], out: List[Signal]):
# B1: 向下笔跌破 ZD 后反转
for i, b in enumerate(post):
if b.direction == Direction.DOWN and b.low < zs.ZD:
if i + 1 < len(post) and post[i+1].direction == Direction.UP:
out.append(Signal(Mark.B1, b.fx_b.dt, b.low,
b.idx, zs.ZG, zs.ZD))
# B2: 后续第一次回调不破 ZD
for j in range(i+2, len(post)):
if post[j].direction == Direction.DOWN:
if post[j].low >= zs.ZD:
out.append(Signal(Mark.B2, post[j].fx_b.dt,
post[j].low, post[j].idx,
zs.ZG, zs.ZD))
break
break
# B3: 回踩不破 ZG (强势回踩)
for b in post:
if b.direction == Direction.DOWN and b.low > zs.ZG:
out.append(Signal(Mark.B3, b.fx_b.dt, b.low,
b.idx, zs.ZG, zs.ZD))
break
# ---- S1 / S2 / S3 ----
@staticmethod
def _sell(zs: ZhongShu, post: List[Bi], out: List[Signal]):
# S1: 向上笔突破 ZG 后反转
for i, b in enumerate(post):
if b.direction == Direction.UP and b.high > zs.ZG:
if i + 1 < len(post) and post[i+1].direction == Direction.DOWN:
out.append(Signal(Mark.S1, b.fx_b.dt, b.high,
b.idx, zs.ZG, zs.ZD))
# S2: 后续第一次反弹不过 ZG
for j in range(i+2, len(post)):
if post[j].direction == Direction.UP:
if post[j].high <= zs.ZG:
out.append(Signal(Mark.S2, post[j].fx_b.dt,
post[j].high, post[j].idx,
zs.ZG, zs.ZD))
break
break
# S3: 反弹不过 ZD (弱势反弹)
for b in post:
if b.direction == Direction.UP and b.high < zs.ZD:
out.append(Signal(Mark.S3, b.fx_b.dt, b.high,
b.idx, zs.ZG, zs.ZD))
break
# ============================================================
# 输出接口
# ============================================================
def get_signals(self) -> pd.DataFrame:
"""→ DataFrame[datetime, signal, name, price, zs_ZG, zs_ZD, is_buy, freq]"""
if not self.signals:
return pd.DataFrame(columns=[
'datetime','signal','name','price',
'zs_ZG','zs_ZD','is_buy','freq'])
rows = [{
'datetime': s.dt, 'signal': s.mark.value,
'name': s.name_cn, 'price': s.price,
'zs_ZG': s.zs_ZG, 'zs_ZD': s.zs_ZD,
'is_buy': s.is_buy, 'freq': self.freq
} for s in self.signals]
return pd.DataFrame(rows)
def get_fractals_df(self) -> pd.DataFrame:
return pd.DataFrame([
{'datetime':f.dt, 'type':'顶' if f.fx_type==FxType.TOP else '底',
'value':f.val, 'ki':f.ki} for f in self.fractals
]) if self.fractals else pd.DataFrame()
def get_bi_df(self) -> pd.DataFrame:
return pd.DataFrame([
{'idx':b.idx, 'dir':'↑' if b.direction==Direction.UP else '↓',
'sdt':b.sdt, 'edt':b.edt, 'high':b.high, 'low':b.low,
'power':b.power} for b in self.bis
]) if self.bis else pd.DataFrame()
def get_zs_df(self) -> pd.DataFrame:
return pd.DataFrame([
{'idx':z.idx, 'ZG':z.ZG, 'ZD':z.ZD, 'GG':z.GG, 'DD':z.DD,
'sdt':z.sdt, 'edt':z.edt, 'n_bi':len(z.bis)}
for z in self.zhong_shus
]) if self.zhong_shus else pd.DataFrame()
def trend(self) -> str:
"""当前趋势: uptrend / downtrend / consolidation"""
if not self.zhong_shus or not self.bis:
return 'unknown'
zs = self.zhong_shus[-1]
price = self.bis[-1].fx_b.val
if price > zs.ZG: return 'uptrend'
if price < zs.ZD: return 'downtrend'
return 'consolidation'
def summary(self) -> str:
tag = f' ({self.freq})' if self.freq else ''
lines = [
f"══════ 缠论分析{tag} ══════",
f"原始K线: {len(self.df)} 合并K线: {len(self.bars)}",
f"分型: {len(self.fractals)} "
f"(顶{sum(1 for f in self.fractals if f.fx_type==FxType.TOP)} / "
f"底{sum(1 for f in self.fractals if f.fx_type==FxType.BOTTOM)})",
f"笔: {len(self.bis)} 线段: {len(self.xds)} "
f"中枢: {len(self.zhong_shus)}",
f"买卖点: {len(self.signals)} 当前趋势: {self.trend()}",
]
if self.signals:
lines.append("------ 最近信号 ------")
for s in self.signals[-5:]:
lines.append(f" {s.dt} {s.mark.value} {s.name_cn} "
f"价格={s.price:.4f}")
return '\n'.join(lines)
# ================================================================
# 4. 多级别联立
# ================================================================
class MultiTimeframeChanLun:
"""
多级别缠论联立
──────────────
高级别定方向, 低级别找入场.
mtf = MultiTimeframeChanLun({'5min': df5, '1min': df1})
sig = mtf.get_combined_signals('5min', '1min')
"""
def __init__(self, dfs: Dict[str, pd.DataFrame],
config: ChanConfig | None = None):
self.analyzers: Dict[str, ChanLun] = {
tf: ChanLun(df, config=config, freq=tf)
for tf, df in dfs.items()
}
def get_combined_signals(self,
higher: str,
lower: str) -> pd.DataFrame:
"""
联立规则
--------
1. 高级别 uptrend + 低级别 Buy → STRONG_BUY
2. 高级别 downtrend + 低级别 Sell → STRONG_SELL
3. 高级别 consolidation → NORMAL
4. 方向相反 → WATCH (逆势)
"""
h = self.analyzers[higher]
l = self.analyzers[lower]
h_trend = h.trend()
h_sig_df = h.get_signals()
l_sig_df = l.get_signals()
if l_sig_df.empty:
return pd.DataFrame()
df = l_sig_df.copy()
df['higher_tf'] = higher
df['higher_trend'] = h_trend
# --- 信号强度 ---
def _strength(row):
buy = row['is_buy']
t = row['higher_trend']
if buy and t == 'uptrend': return 'strong'
if not buy and t == 'downtrend': return 'strong'
if t == 'consolidation': return 'medium'
return 'weak_counter'
df['strength'] = df.apply(_strength, axis=1)
# --- 优先级 / 综合评分 ---
prio = {'B1':5,'S1':5,'B2':4,'S2':4,'B3':3,'S3':3}
smap = {'strong':3,'medium':2,'weak_counter':1}
df['priority'] = df['signal'].map(prio).fillna(1)
df['score'] = df['strength'].map(smap) * df['priority']
# --- 高级别最新信号 ---
if not h_sig_df.empty:
last = h_sig_df.iloc[-1]
df['higher_last_signal'] = last['signal']
df['higher_last_signal_price'] = last['price']
else:
df['higher_last_signal'] = ''
df['higher_last_signal_price'] = np.nan
# --- 决策建议 ---
def _decide(row):
act = 'BUY' if row['is_buy'] else 'SELL'
if row['score'] >= 12: return f'STRONG_{act}'
if row['strength'] == 'weak_counter': return 'WATCH'
return f'NORMAL_{act}'
df['decision'] = df.apply(_decide, axis=1)
return df.sort_values('datetime').reset_index(drop=True)
def summary(self):
parts = ["══════ 多级别缠论联立 ══════\n"]
for tf, a in self.analyzers.items():
parts.append(a.summary()); parts.append('')
return '\n'.join(parts)
# ================================================================
# 5. 快速测试 / Demo
# ================================================================
def demo():
"""用合成数据快速验证"""
np.random.seed(42)
n = 300
px = [100.0]
for i in range(1, n):
drift = {i < 60: 0.12, 60 <= i < 130: -0.10,
130 <= i < 220: 0.14}.get(True, -0.06)
px.append(px[-1] + drift + np.random.randn() * 0.5)
df = pd.DataFrame({
'datetime': pd.date_range('2024-01-02 09:30', periods=n, freq='5min'),
'open': px,
'high': [p + abs(np.random.randn()*0.3) for p in px],
'low': [p - abs(np.random.randn()*0.3) for p in px],
'close': [p + np.random.randn()*0.1 for p in px],
'volume': np.random.randint(100, 8000, n),
})
print("=" * 50)
print(" 单级别 Demo (5min)")
print("=" * 50)
c = ChanLun(df, freq='5min')
print(c.summary())
print("\n分型 (前10):")
print(c.get_fractals_df().head(10))
print("\n笔:")
print(c.get_bi_df())
print("\n中枢:")
print(c.get_zs_df())
print("\n买卖点:")
print(c.get_signals())
# ---------- 模拟多级别 ----------
df1 = df.copy() # 假设同数据 (实际应为1min)
print("\n" + "=" * 50)
print(" 多级别 Demo (5min + 1min)")
print("=" * 50)
mtf = MultiTimeframeChanLun({'5min': df, '1min': df1})
print(mtf.summary())
combined = mtf.get_combined_signals('5min', '1min')
print("\n联立信号:")
print(combined[['datetime','signal','name','price',
'higher_trend','strength','score','decision']])
return c, mtf
if __name__ == '__main__':
demo()
使用示例
1. 单级别 --- 输入 CSV 直接拿买卖点
python
import pandas as pd
from chanlun import ChanLun
# -------- 读入任意 OHLCV 数据 --------
df = pd.read_csv('stock_5min.csv') # 须含 open/high/low/close
# 亦可传入中文列名: 开盘/最高/最低/收盘
analyzer = ChanLun(df, freq='5min')
print(analyzer.summary())
# -------- 各层级结果 --------
print(analyzer.get_fractals_df()) # 分型
print(analyzer.get_bi_df()) # 笔
print(analyzer.get_zs_df()) # 中枢
signals = analyzer.get_signals() # ★ 买卖点
print(signals)
# datetime signal name price zs_ZG zs_ZD is_buy freq
# 0 2024-01-03 B1 第一类买点 98.12 102.3 99.1 True 5min
# 1 2024-01-04 B2 第二类买点 99.45 102.3 99.1 True 5min
# 2 2024-01-05 S1 第一类卖点 107.88 102.3 99.1 False 5min
2. 多级别联立 --- 5 分钟定方向 + 1 分钟找入场
python
from chanlun import MultiTimeframeChanLun, ChanConfig
df_5m = pd.read_csv('stock_5min.csv')
df_1m = pd.read_csv('stock_1min.csv')
# 可自定义参数
cfg = ChanConfig(bi_min_gap=4, zs_min_bi=3)
mtf = MultiTimeframeChanLun(
dfs={'5min': df_5m, '1min': df_1m},
config=cfg
)
print(mtf.summary())
# ★ 联立信号
combined = mtf.get_combined_signals(higher='5min', lower='1min')
print(combined)
# -------- 只看强信号 --------
strong = combined[combined['decision'].str.startswith('STRONG')]
print(strong[['datetime','signal','name','price',
'higher_trend','strength','score','decision']])
3. 实时 / 回测框架中嵌入
python
def on_bar(new_df: pd.DataFrame):
"""每根K线结束时调用"""
c = ChanLun(new_df, freq='1min')
for sig in c.signals:
if sig.is_buy and sig.mark.value == 'B1':
print(f"[{sig.dt}] ★ 第一类买点 @ {sig.price:.2f}")
# → 下单逻辑
elif not sig.is_buy and sig.mark.value == 'S1':
print(f"[{sig.dt}] ★ 第一类卖点 @ {sig.price:.2f}")
# → 平仓逻辑
买卖点含义速查
| 信号 | 中文 | 触发条件 | 强度 |
|---|---|---|---|
| B1 | 第一类买点 | 下跌笔突破中枢下沿 (ZD) 后反转向上 | ★★★★★ 趋势反转 |
| B2 | 第二类买点 | B1 后第一次回调不破 ZD | ★★★★ 确认反转 |
| B3 | 第三类买点 | 回踩低点仍在中枢上沿 (ZG) 之上 | ★★★ 强势延续 |
| S1 | 第一类卖点 | 上涨笔突破中枢上沿 (ZG) 后反转向下 | ★★★★★ 趋势反转 |
| S2 | 第二类卖点 | S1 后第一次反弹不过 ZG | ★★★★ 确认反转 |
| S3 | 第三类卖点 | 反弹高点仍在中枢下沿 (ZD) 之下 | ★★★ 弱势延续 |
架构流水线
CSV / DataFrame
│
▼
┌─────────────┐ ┌──────────┐ ┌──────────┐
│ K线合并 │───▶│ 分型识别 │───▶│ 笔构建 │
│ (包含关系) │ │ 顶/底分型 │ │ 新笔标准 │
└─────────────┘ └──────────┘ └──────────┘
│
┌────────────────────────────────┘
▼
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ 线段识别 │───▶│ 中枢构建 │───▶│ 买卖点识别 │
│ 特征序列 │ │ 重叠区间 │ │ B1/B2/B3 │
│ (简化) │ │ ZG/ZD │ │ S1/S2/S3 │
└──────────┘ └──────────┘ └──────────────┘
│
▼
┌──────────────────┐
│ 多级别联立 │
│ 高TF趋势 × 低TF信号│
│ → 强/中/弱 决策 │
└──────────────────┘
💡 进阶优化建议 (点击展开)
- MACD 背驰辅助判定 B1/S1 --- 在
_buy()/_sell()中加入 DIF/DEA 背离检测,可大幅提高 B1/S1 准确率。 - 中枢级别升级 --- 当同一方向出现 2 个以上中枢时,可判定为趋势,B1 信号权重更高。
- 区间套 --- 先在 30 分钟级别找到中枢区间,再在 5 分钟/1 分钟里做精确进出场。
- 中枢扩展 / 中枢新生 --- 两个相邻中枢重叠时可合并成更大级别中枢。
- 笔的力度 --- 可用笔的
power(幅度)做 MACD 面积类比,辅助背驰判断。 - 特征序列标准化 --- 当前线段检测使用简化规则,可替换为完整特征序列+包含处理以提高准确性。