refactor: 缠论引擎包化与 Web 分层(ECR-001)

将根目录引擎迁入 chanlun/ 并保留兼容 shim;拆分 TF_DF 与 web 服务;
前端模块化;strategies 改用 chanlun 导入;补充 ESS 文档与 golden 回归。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-05 18:48:20 +08:00
co-authored by Cursor
parent e2e45bc1bc
commit 74dec4e50b
160 changed files with 25576 additions and 23104 deletions
+132
View File
@@ -0,0 +1,132 @@
from decimal import Decimal
import chanlun.core.ChanKLC as ChanKLC
from chanlun.core.ChanEnum import Chan_BI_DIR
class ChanBI():
def __init__(self, klc: ChanKLC, index, ddir=Chan_BI_DIR.UP):
self.start_klc = klc
self.end_klc = klc
self.next = None
self.pre = None
self.dir = ddir
self.index = index
self.is_sure = False
self.high = klc.high
self.low = klc.low
self.sure_time = None
self.klc_list = []
self.klc_list.append(klc)
self.end_time = klc.end_time
self.start_time = klc.start_time
self.macd_hist = 0
self.macd_div = 0
self.seg = None
self.height = 0
self.width = 0
self.slop = 0
self.fib_list = []
self.seg_index = 0
self.bi_zs = None
self.seg_zs = None
def set_bi_zs(self, bi_zs):
for klc in self.klc_list:
klc.set_bi_zs(bi_zs)
def set_seg(self, seg):
self.seg = seg
self.seg_index = len(seg.bi_list)-1
def set_macdhist(self, macd_hist):
self.macd_hist = macd_hist
def set_macd_div(self, macd_div):
self.macd_div = macd_div
def cal_macd_div(self):
self.macd_div = 0.0
if self.pre and self.pre.pre:
if self.pre.pre.macd_hist == 0:
self.macd_div = 0.0
else:
self.macd_div = self.macd_hist / self.pre.pre.macd_hist
#print(self.start_time, self.end_time, self.macd_hist, self.pre.pre.macd_hist, self.macd_div)
def cal_macdhist(self):
self.macd_hist = 0
for klc in self.klc_list:
for klu in klc.klu_list:
if self.dir == Chan_BI_DIR.UP and klu.macdhist > 0:
self.macd_hist += klu.macdhist
if self.dir == Chan_BI_DIR.DOWN and klu.macdhist < 0:
self.macd_hist -= klu.macdhist
def check_bi_zs_overlap(self):
if self.next and self.next.next:
if self.dir == Chan_BI_DIR.UP:
return self.low < self.next.next.high
else:
return self.high > self.next.next.low
else:
return False
def check_overlap(self):
if self.next and self.next.next and self.next.next.is_sure:
if self.dir == Chan_BI_DIR.UP:
return self.high > self.next.low and self.high < self.next.next.high
else:
return self.high > self.next.high and self.low > self.next.next.low
else:
return False
def set_end_klc(self, klc, sure_klc):
if self.dir == Chan_BI_DIR.UP and klc.high > self.high:
self.high = klc.high
if self.dir == Chan_BI_DIR.DOWN and klc.low < self.low:
self.low = klc.low
self.end_klc = klc
self.set_is_sure(True, sure_klc.end_time)
self.end_time = klc.end_time
self.cal_properties()
#print(self.start_time, klc.fx, "This bi is ended", len(self.klc_list), klc.index - self.start_klc.index)
def cal_properties(self):
if self.is_sure:
self.height = float(format(self.high - self.low, ".2f"))
self.width = self.end_klc.index - self.start_klc.index
self.slop = float(format(self.height / self.width, ".2f"))
fib_list = [0.0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0]
for fib in fib_list:
self.fib_list.append(float(format(self.height * fib + self.low, ".2f")))
#print(self.end_time, self.height, self.width, self.slop, self.fib_list)
def set_is_sure(self, is_sure, time):
self.is_sure = is_sure
self.sure_time = time
def set_start_klc(self, klc, ddir):
self.start_klc = klc
self.klc_list = []
self.klc_list.append(klc)
self.high = klc.high
self.low = klc.low
self.dir = ddir
def set_pre(self, bi):
self.pre = bi
def set_next(self, bi):
self.next = bi
def add_klc(self, klc):
added = False
if len(self.klc_list) > 0:
for index in range(0, len(self.klc_list)):
if self.klc_list[index].index == klc.index:
added = True
break
if not added:
self.klc_list.append(klc)
#print(self.start_time, klc.start_time)
#print(klc.end_time, klc.index)
self.end_klc = klc
self.end_time = klc.klu_list[-1].time
self.cal_macdhist()
self.cal_macd_div()
def append_klc_list(self, klc_list):
self.klc_list.append(klc_list)
def get_decimal(self, value):
return Decimal("{:.2f}".format(value))
def update_bi(self, klc):
self.end_klc = None
if self.dir == Chan_BI_DIR.UP and klc.high > self.high:
self.high = klc.high
if self.dir == Chan_BI_DIR.DOWN and klc.low < self.low:
self.low = klc.low
self.is_sure = False
self.sure_time = None
#print(self.start_time, klc.start_time, klc.fx, "This bi is extended")
+161
View File
@@ -0,0 +1,161 @@
from chanlun.core.ChanEnum import Chan_ZS_DIR, Chan_ZS_TYPE, Chan_BI_DIR
import chanlun.core.ChanBI as ChanBI
# 中枢
class ChanBIZS():
def __init__(self, start_bi: ChanBI, index, ddir: Chan_ZS_DIR):
self.start_klc = start_bi.start_klc
self.start_time = self.start_klc.start_time
self.end_time = None
self.index = index
self.start_bi = start_bi
self.bi_list = []
self.bi_list.append(start_bi)
self.end_bi = None
self.bi_out = None
self.is_sure = False
self.zg = 0
self.zd = 0
self.gg = 0
self.dd = 0
self.dir = ddir
self.sure_time = None
self.end_klc = None
self.zs_type = Chan_ZS_TYPE.NORMAL
start_bi.set_bi_zs(self)
def set_end_bi(self, end_bi, sure_time):
self.end_bi = end_bi
self.set_end_time(end_bi.end_klc.end_time)
self.is_sure = True
self.sure_time = sure_time
end_bi.set_bi_zs(self)
#print(self.start_time, self.is_sure, len(self.bi_list), self.dir, self.zs_type)
def set_end_time(self, end_time):
self.end_time = end_time
def set_zg(self, zg):
self.zg = zg
def set_zd(self, zd):
self.zd = zd
def set_gg(self, gg):
self.gg = gg
def set_dd(self, dd):
self.dd = dd
def add_bi(self, bi: ChanBI):
if bi:
self.bi_list.append(bi)
if bi.high > self.gg:
self.gg = bi.high
if bi.low < self.dd:
self.dd = bi.low
bi.set_bi_zs(self)
self.classify_zs()
def set_pre(self, pre):
self.pre = pre
def set_next(self, next):
self.next = next
def classify_zs(self):
"""
根据中枢内笔的高低点变化趋势,对中枢进行分类
分类逻辑:
- 取中枢内向上笔的高点(peaks)和向下笔的低点(valleys
- 比较前半段和后半段的均值,判断高点和低点的整体趋势
分类结果:
- RISING 上升中枢:高点抬高 + 低点抬高 → 多方占优,可能向上突破
- FALLING 下行中枢:高点降低 + 低点降低 → 空方占优,可能向下突破
- CONVERGING 收敛中枢:高点降低 + 低点抬高 → 区间收窄,即将选择方向
- DIVERGING 扩散中枢:高点抬高 + 低点降低 → 波动加剧,市场不稳定
- NORMAL 常规中枢:无明显趋势 → 多空均衡,区间震荡
"""
if len(self.bi_list) < 3:
self.zs_type = Chan_ZS_TYPE.NORMAL
return
# 提取向上笔的高点(peaks)和向下笔的低点(valleys
peaks = [bi.high for bi in self.bi_list if bi.dir == Chan_BI_DIR.UP]
valleys = [bi.low for bi in self.bi_list if bi.dir == Chan_BI_DIR.DOWN]
high_trend = self._calc_trend(peaks)
low_trend = self._calc_trend(valleys)
if high_trend > 0 and low_trend > 0:
self.zs_type = Chan_ZS_TYPE.RISING
elif high_trend < 0 and low_trend < 0:
self.zs_type = Chan_ZS_TYPE.FALLING
elif high_trend < 0 and low_trend > 0:
self.zs_type = Chan_ZS_TYPE.CONVERGING
elif high_trend > 0 and low_trend < 0:
self.zs_type = Chan_ZS_TYPE.DIVERGING
else:
self.zs_type = Chan_ZS_TYPE.NORMAL
def _calc_trend(self, values):
"""
计算序列的趋势方向
将序列分为前后两半,比较均值:
- 后半均值 > 前半均值 → 返回 1(上升趋势)
- 后半均值 < 前半均值 → 返回 -1(下降趋势)
- 相等或数据不足 → 返回 0(无趋势)
使用均值比较而非首尾比较,可以过滤单笔异常波动带来的误判
"""
if len(values) < 2:
return 0
mid = len(values) // 2
first_half = values[:mid] if mid > 0 else values[:1]
second_half = values[mid:]
avg_first = sum(first_half) / len(first_half)
avg_second = sum(second_half) / len(second_half)
# 使用中枢区间的一定比例作为阈值,避免微小波动误判
threshold = abs(avg_first) * 0.005 if avg_first != 0 else 0
if avg_second - avg_first > threshold:
return 1
elif avg_first - avg_second > threshold:
return -1
else:
return 0
def is_weakening(self):
"""
判断中枢是否在衰弱(即将反向突破的信号)
衰弱条件:
1. 中枢内笔数 >= 5(有足够的数据判断)
2. 最后一笔的MACD面积相比同方向前一笔出现背驰(macd_div < 1
3. 中枢类型为收敛型或常规型
返回: True表示中枢力量衰弱,可能反向
"""
if len(self.bi_list) < 5:
return False
last_bi = self.bi_list[-1]
# 最后一笔与同方向前一笔比较MACD面积是否背驰
if last_bi.macd_div > 0 and last_bi.macd_div < 1.0:
return True
return False
def get_zs_strength(self):
"""
计算中枢强度,用于辅助判断中枢延续还是反向
返回字典包含:
- type: 中枢类型 (Chan_ZS_TYPE)
- bi_count: 中枢内笔数
- range_ratio: 中枢区间占比 = (zg - zd) / (gg - dd),越小说明中枢越紧密
- last_bi_div: 最后一笔的MACD背驰比率
- is_weakening: 是否衰弱
- is_extending: 是否在延伸(笔数 >= 9 可能升级)
"""
total_range = self.gg - self.dd if self.gg != self.dd else 1
zs_range = self.zg - self.zd if self.zg != self.zd else 0
range_ratio = zs_range / total_range if total_range > 0 else 0
last_bi_div = self.bi_list[-1].macd_div if len(self.bi_list) > 0 else 0
return {
'type': self.zs_type,
'bi_count': len(self.bi_list),
'range_ratio': round(range_ratio, 4),
'last_bi_div': round(last_bi_div, 4),
'is_weakening': self.is_weakening(),
'is_extending': len(self.bi_list) >= 9, # 9段可能升级
}
+24
View File
@@ -0,0 +1,24 @@
import chanlun.core.ChanBI as ChanBI
from chanlun.core.ChanEnum import Chan_BSP_TYPE, Chan_BSP_DIR
class ChanBSP():
def __init__(self, bi: ChanBI, index, type: Chan_BSP_TYPE, ddir: Chan_BSP_DIR, sure_time, zs_count, zs, seg):
self.bi = bi
self.klc = bi.end_klc
self.index = index
self.type = type
self.start_time = self.klc.start_time
self.end_time = self.klc.end_time
if sure_time:
self.is_sure = True
self.sure_time = sure_time
else:
self.is_sure = False
self.sure_time = None
self.dir = ddir
self.zs_count = zs_count
self.zs = zs
self.seg = bi.seg
def set_sure_time(self, sure_time):
self.is_sure = True
self.sure_time = sure_time
+44
View File
@@ -0,0 +1,44 @@
from datetime import datetime
class ChanCTime:
def __init__(self, year, month, day, hour, minute, second=0, auto=True):
self.year = year
self.month = month
self.day = day
self.hour = hour
self.minute = minute
self.second = second
self.auto = auto # 自适应对天的理解
self.set_timestamp() # set self.ts
def __str__(self):
if self.hour == 0 and self.minute == 0:
return f"{self.year:04}/{self.month:02}/{self.day:02}"
else:
return f"{self.year:04}/{self.month:02}/{self.day:02} {self.hour:02}:{self.minute:02}"
def to_str(self):
if self.hour == 0 and self.minute == 0:
return f"{self.year:04}/{self.month:02}/{self.day:02}"
else:
return f"{self.year:04}/{self.month:02}/{self.day:02} {self.hour:02}:{self.minute:02}"
def toDateStr(self, splt=''):
return f"{self.year:04}{splt}{self.month:02}{splt}{self.day:02}"
def toDate(self):
return ChanCTime(self.year, self.month, self.day, 0, 0, auto=False)
def set_timestamp(self):
if self.hour == 0 and self.minute == 0 and self.auto:
date = datetime(self.year, self.month, self.day, 23, 59, self.second)
else:
date = datetime(self.year, self.month, self.day, self.hour, self.minute, self.second)
self.ts = date.timestamp()
def __gt__(self, t2):
return self.ts > t2.ts
def __ge__(self, t2):
return self.ts >= t2.ts
+368
View File
@@ -0,0 +1,368 @@
from enum import Enum, auto
from typing import Literal
class Chan_DATA_SRC(Enum):
BAO_STOCK = auto()
CCXT = auto()
CSV = auto()
class Chan_ZS_DIR(Enum):
UP = auto()
DOWN = auto()
class Chan_ZS_TYPE(Enum):
"""中枢类型分类"""
NORMAL = auto() # 常规中枢:高低点无明显趋势,区间震荡
RISING = auto() # 上升中枢:高点抬高,低点也抬高,重心上移
FALLING = auto() # 下行中枢:高点降低,低点也降低,重心下移
CONVERGING = auto() # 收敛中枢:高点降低,低点抬高,区间收窄(三角收敛)
DIVERGING = auto() # 扩散中枢:高点抬高,低点降低,区间扩大(喇叭口)
class Chan_K_DIR(Enum):
BULL = auto()
BEAR = auto()
CROSS = auto()
class Chan_EMA_POS(Enum):
"""K线与任意EMA的位置关系(与趋势方向无关的客观分类,支持threshold容差)"""
ABOVE = auto() # 完全在EMA上方(远离):low > ema + threshold
NEAR_ABOVE = auto() # 在EMA上方但接近:ema < low <= ema + threshold
CROSS_CLOSE_ABOVE = auto() # 跨越EMA,收盘在上方:close > ema, low <= ema(含threshold范围内触碰)
ON_EMA = auto() # 收盘价在EMA附近:abs(close - ema) <= threshold
CROSS_CLOSE_BELOW = auto() # 跨越EMA,收盘在下方:close < ema, high >= ema(含threshold范围内触碰)
NEAR_BELOW = auto() # 在EMA下方但接近:ema - threshold <= high < ema
BELOW = auto() # 完全在EMA下方(远离):high < ema - threshold
UNKNOWN = auto() # 未知(EMA值无效)
class Chan_EMA_SEMANTIC(Enum):
"""K线与EMA结合趋势方向的语义状态(用于交易判断)"""
STRONG_TREND = auto() # 7: 顺势K线完全在EMA趋势侧(强势,远未及EMA)
TREND_SIDE = auto() # 6: 完全在EMA趋势侧(正常趋势运行)
RECOVER = auto() # 5: 逆势后穿越EMA回到趋势侧(收复EMA,趋势恢复)
TOUCH_FAIL = auto() # 4: 逆势触碰EMA但未穿越(反弹/反抽力度不足)
DEEP_COUNTER = auto() # 3: 完全在EMA逆势侧(深度回调/反抽)
BREAK = auto() # 2: 穿越EMA,收盘在逆势侧(支撑/压力失败)
TOUCH_HOLD = auto() # 1: 触碰EMA,收盘守住趋势侧(支撑/压力有效)
WEAK_COUNTER = auto() # 8: 逆势K线完全在EMA逆势侧(弱势,远未到EMA)
APPROACHING = auto() # 9: K线接近EMA但未触碰(即将测试支撑/压力)
NEUTRAL = auto() # 0: 盘整/无法判断
class Chan_KL_TYPE(Enum):
K_1S = auto()
K_1M = auto()
K_DAY = auto()
K_WEEK = auto()
K_MON = auto()
K_YEAR = auto()
K_5M = auto()
K_15M = auto()
K_30M = auto()
K_60M = auto()
K_1H = auto()
K_2H = auto()
K_4H = auto()
K_6H = auto()
K_8H = auto()
K_12H = auto()
K_1D = auto()
K_3D = auto()
K_3M = auto()
K_QUARTER = auto()
class Chan_KLINE_DIR(Enum):
UP = auto()
DOWN = auto()
COMBINE = auto()
INCLUDED = auto()
class Chan_KLU_TYPE(Enum):
BigBull = auto()
MiddleBull = auto()
SmallBull = auto()
BigBear = auto()
MiddleBear = auto()
SmallBear = auto()
Cross = auto()
class Chan_KLU_PATTERN(Enum):
# 单根K线形态
HAMMER = auto() # 锤子线
INVERTED_HAMMER = auto() # 倒锤子线
SHOOTING_STAR = auto() # 射击之星
HANGING_MAN = auto() # 上吊线
DOJI = auto() # 十字星
LONG_LEGGED_DOJI = auto() # 长腿十字星
GRAVESTONE_DOJI = auto() # 墓碑十字星
DRAGONFLY_DOJI = auto() # 蜻蜓十字星
MARUBOZU = auto() # 光头光脚
SPINNING_TOP = auto() # 纺锤线
# 双根K线形态
BULLISH_ENGULFING = auto() # 看涨吞没
BEARISH_ENGULFING = auto() # 看跌吞没
PIERCING_LINE = auto() # 刺透形态
DARK_CLOUD_COVER = auto() # 乌云盖顶
TWEEZER_TOP = auto() # 镊子顶
TWEEZER_BOTTOM = auto() # 镊子底
HARAMI = auto() # 孕线
BULLISH_HARAMI = auto() # 看涨孕线
BEARISH_HARAMI = auto() # 看跌孕线
# 三根K线形态
MORNING_STAR = auto() # 早晨之星
EVENING_STAR = auto() # 黄昏之星
THREE_WHITE_SOLDIERS = auto() # 红三兵
THREE_BLACK_CROWS = auto() # 三只乌鸦
THREE_INNER_UP = auto() # 上升三法
THREE_INNER_DOWN = auto() # 下降三法
ABANDONED_BABY = auto() # 弃婴形态
# 多根K线形态
DOUBLE_TOP = auto() # 双顶
DOUBLE_BOTTOM = auto() # 双底
TRIPLE_TOP = auto() # 三顶
TRIPLE_BOTTOM = auto() # 三底
HEAD_AND_SHOULDERS = auto() # 头肩顶
INVERSE_HEAD_SHOULDERS = auto() # 头肩底
ROUNDING_BOTTOM = auto() # 圆弧底
ROUNDING_TOP = auto() # 圆弧顶
# 缺口形态
BREAKAWAY_GAP = auto() # 突破缺口
RUNAWAY_GAP = auto() # 持续缺口
EXHAUSTION_GAP = auto() # 衰竭缺口
# 特殊形态
ISLAND_REVERSAL = auto() # 岛形反转
KEY_REVERSAL = auto() # 关键反转
INSIDE_BAR = auto() # 内包线
OUTSIDE_BAR = auto() # 外包线
# 趋势形态
HIGHER_HIGH = auto() # 更高高点
HIGHER_LOW = auto() # 更高低点
LOWER_HIGH = auto() # 更低高点
LOWER_LOW = auto() # 更低低点
# 支撑阻力形态
SUPPORT_BOUNCE = auto() # 支撑反弹
RESISTANCE_REJECTION = auto() # 阻力拒绝
BREAKOUT = auto() # 突破
BREAKDOWN = auto() # 跌破
# 成交量相关形态
VOLUME_SPIKE = auto() # 成交量激增
VOLUME_DECLINE = auto() # 成交量萎缩
# 未知/无形态
UNKNOWN = auto() # 未知形态
class Chan_FX_TYPE(Enum):
BOTTOM = auto()
TOP = auto()
UNKNOWN = auto()
UP = auto()
DOWN = auto()
TT = auto()
BB = auto()
PTOP = auto()
PBOTTOM = auto()
class Chan_FX(Enum):
CONTINUATION = auto()
REVERSAL = auto()
UNKNOWN = auto()
class Chan_PRICE_TREND(Enum):
UP = auto()
DOWN = auto()
FLAT = auto()
UNKNOWN = auto()
class Chan_KLC_FX(Enum):
TOP0 = auto()
TOP1 = auto()
TOP2 = auto()
TOP3 = auto()
TOP4 = auto()
TOP5 = auto()
TOP6 = auto()
TOP7 = auto()
TOP8 = auto()
BOTTOM0 = auto()
BOTTOM1 = auto()
BOTTOM2 = auto()
BOTTOM3 = auto()
BOTTOM4 = auto()
BOTTOM5 = auto()
BOTTOM6 = auto()
BOTTOM7 = auto()
BOTTOM8 = auto()
UNKNOWN = auto()
# 统一的MACD状态枚举,包含所有可能的状态
class Chan_MACD_STATE(Enum):
"""MACD状态枚举 - 包含所有可能的状态"""
# 穿越状态
CROSS0_UP = auto() # 穿零轴后快速向上,能量柱呈现一根比一根长的排列方式
CROSS0_DOWN = auto() # 穿零轴后快速向下,能量柱呈现一根比一根短的排列方式
CROSS_OS = auto() # 穿零轴后缠绕/粘合,黄白线沿着能量柱运行,黄白线在运行的过程中没有释放出反向能量柱
CROSS_REV = auto() # 穿零轴后倒挂,MACD黄白线在穿零轴的时候与零轴的距离比较近,同时黄白线沿着能量柱运行,在运行的过程中,能量柱衰减导致它跟黄白线之间形成夹角空位,同时黄白线产生交叉并释放反向能量柱。
# 趋势状态
NEAR0 = auto()
NEAR0_52 = auto() # 价格在EMA52附近/价格接触EMA52并马上离开,需要观察离开强度
NEAR0_DIFF = auto() # MACD白线接近零轴,价格未到EMA52
NEAR0_PERFECT = auto() # MACD白线接近零轴和价格接触或短暂击穿EMA52,而MACD黄线不穿零轴,完美形态
NEAR0_24 = auto() # MACD黄白线接近零轴和价格在EMA24附近
# 位置状态
HIGH = auto() # 高位:MACD黄白线离开能量柱到高点,能量柱最大开始减弱
HIGH_EMPTY = auto() # 高位空:MACD黄白线处于高位,能量柱衰减,与黄白线形成空间夹角
RETURN_ZERO = auto() # 归零轴:能量柱呈现一根比一根短的排列方式
RZ_UP = auto() # 归零轴后的零轴上涨
RZ_DOWN = auto() # 归零轴后的零轴下跌
UP = auto() # 穿零轴后向上
DOWN = auto() # 穿零轴后向下
PEAK = auto() # 峰值:MACD白线处于高位
# 基础状态
UNKNOWN = auto() # 未知
START = auto() # 开始
class Chan_MACDSEG_DIR(Enum):
ABOVE = auto()
UNDER = auto()
class Chan_MACDUNITTF_TYPE(Enum):
START = auto()
CROSS0 = auto()
NEAR0 = auto()
class Chan_MACDUNITTF_JUMP(Enum):
CONTUNE = auto()
DISCRETE = auto()
class Chan_MACDUNITTF_DIV(Enum):
CONTUNE = auto()
DISCRETE = auto()
UNDIV = auto()
class Chan_MACDHISTSET_DIR(Enum):
ABOVE = auto()
UNDER = auto()
class Chan_MACDUNITTF_DIR(Enum):
ABOVE = auto()
UNDER = auto()
class Chan_MACDHIST_STATE(Enum):
UP = auto()
DOWN = auto()
PEAK = auto()
UNKNOWN = auto()
class Chan_BI_DIR(Enum):
UP = auto()
DOWN = auto()
class Chan_SEG_DIR(Enum):
UP = auto()
DOWN = auto()
class Chan_BI_TYPE(Enum):
UNKNOWN = auto()
STRICT = auto()
SUB_VALUE = auto() # 次高低点成笔
TIAOKONG_THRED = auto()
DAHENG = auto()
TUIBI = auto()
UNSTRICT = auto()
TIAOKONG_VALUE = auto()
Chan_BSP_MAIN_TYPE = Literal['1', '2', '3']
class Chan_BSP_DIR(Enum):
BUY = auto()
SELL = auto()
class Chan_BSP_TYPE(Enum):
B1 = auto()
B2 = auto()
B3 = auto()
S1 = auto()
S2 = auto()
S3 = auto()
NONE = auto()
"""
class Chan_BSP_TYPE(Enum):
T1 = '1'
T1P = '1p'
T2 = '2'
T2S = '2s'
T3A = '3a' # 中枢在1类后面
T3B = '3b' # 中枢在1类前面
T3 = '3'
T3E ='3e' # T3退出点
QJT = 'qjt' # 区间套突破
QJT1 = 'qjt1' # 区间套一类买点
QJT2 = 'qjt2' # 区间套一类卖点
QJT3 = 'qjt3' # 区间套三类买点
def main_type(self) -> Chan_BSP_MAIN_TYPE:
return self.value[0] # type: ignore
"""
class Chan_AUTYPE(Enum):
QFQ = auto()
HFQ = auto()
NONE = auto()
class Chan_TREND_TYPE(Enum):
MEAN = "mean"
MAX = "max"
MIN = "min"
class Chan_TREND_LINE_SIDE(Enum):
INSIDE = auto()
OUTSIDE = auto()
class Chan_LEFT_SEG_METHOD(Enum):
ALL = auto()
PEAK = auto()
class Chan_FX_CHECK_METHOD(Enum):
STRICT = auto()
LOSS = auto()
HALF = auto()
TOTALLY = auto()
class Chan_SEG_TYPE(Enum):
BI = auto()
SEG = auto()
class Chan_MACD_ALGO(Enum):
AREA = auto()
PEAK = auto()
FULL_AREA = auto()
DIFF = auto()
SLOPE = auto()
AMP = auto()
VOLUMN = auto()
AMOUNT = auto()
VOLUMN_AVG = auto()
AMOUNT_AVG = auto()
TURNRATE_AVG = auto()
RSI = auto()
class Chan_DATA_FIELD:
FIELD_TIME = "time_key"
FIELD_OPEN = "open"
FIELD_HIGH = "high"
FIELD_LOW = "low"
FIELD_CLOSE = "close"
FIELD_VOLUME = "volume" # 成交量
FIELD_TURNOVER = "turnover" # 成交额
FIELD_TURNRATE = "turnover_rate" # 换手率
class Chan_KLC_STATE:
"""笔当下状态(缠论笔定理)。任意时刻必属其一。"""
S10 = "(1, 0)" # 顶分型构造中 (1,0)
S_10 = "(-1, 0)" # 底分型构造中 (-1,0)
S11 = "(1,1)" # 向上笔延续中
S_11 = "(-1,1)" # 向下笔延续中
UNKNOWN = "Unknown" # 初始状态
+620
View File
@@ -0,0 +1,620 @@
import copy
from typing import Dict, Optional
from chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_KLC_FX
from chanlun.core.ChanEnum import Chan_K_DIR, Chan_MACD_STATE, Chan_PRICE_TREND, Chan_EMA_POS
from chanlun.core.ChanEnum import Chan_EMA_SEMANTIC, Chan_BSP_TYPE, Chan_KLC_STATE, Chan_FX
import chanlun.core.ChanKLU as ChanKLU
import chanlun.core.ChanCTime as ChanCTime
import chanlun.core.Chan_FX_Box as Chan_FX_Box
# 根据结合律合并K线后的K线
class ChanKLC():
def __init__(self, klu: ChanKLU, index, ddir=Chan_KLINE_DIR.UP):
self.start_time = klu.time
self.end_time = None
self.high = klu.high
self.low = klu.low
self.dir = ddir
self.index = index
self.klu_list = []
self.add_klu(klu)
self.fx = Chan_FX_TYPE.UNKNOWN
self.next = None
self.pre = None
self.start_klu = klu
self.end_klu = None
self.state = "00"
self.klc_state = Chan_KLC_STATE.UNKNOWN
self.open = klu.open
self.close = klu.close
self.volume = klu.volume
self.bi = None
self.distance = 0
self.klc_fx_type = Chan_KLC_FX.UNKNOWN
self.rsi = klu.rsi
self.volume_ratio = klu.volume_ratio
self.macdhist = klu.macdhist
self.body = klu.body
self.upper_shadow = klu.upper_shadow
self.lower_shadow = klu.lower_shadow
self.body_ratio = klu.body_ratio
self.upper_shadow_ratio = klu.upper_shadow_ratio
self.lower_shadow_ratio = klu.lower_shadow_ratio
self.candle_dir = klu.candle_dir
self.range = klu.range
self.bb_out = True
self.macd = klu.macd
self.signal = klu.signal
self.state = Chan_MACD_STATE.UNKNOWN
self.continue_div = False
self.separate_div = False
self.ema24 = klu.ema24
self.ema26 = klu.ema26
self.ema52 = klu.ema52
self.ema104 = klu.ema104
self.ema156 = klu.ema156
self.ema208 = klu.ema208
self.ema13 = klu.ema13
self.ema7 = klu.ema7
self.trend = Chan_PRICE_TREND.UNKNOWN
self.exception = klu.exception
self.klc_dir = Chan_KLINE_DIR.UP if klu.close > klu.open else Chan_KLINE_DIR.DOWN
self.ema_dir = klu.ema_dir
self.bsp = False
self.bsp_type = Chan_BSP_TYPE.NONE
# EMA状态字典:key为EMA名称,value为 {'pos': Chan_EMA_POS, 'semantic': Chan_EMA_SEMANTIC}
self.ema_status = {}
# 向后兼容:保留 ema52_status 和 ema52_pos
self.ema52_status = 0
self.ema52_pos = Chan_EMA_POS.UNKNOWN
self.bb2633upper = klu.bb2633upper
self.bb2633lower = klu.bb2633lower
self.bb2633middle = klu.bb2633middle
self.ema5 = klu.ema5
self.ma5 = klu.ma5
self.fx_box = None
self.in_fx = False
self.fx_confirmed = False
self.ema52_dis = klu.high - klu.ema52 if klu.close > klu.ema52 else klu.ema52 - klu.low
self.ema26_dis = klu.high - klu.ema26 if klu.close > klu.ema26 else klu.ema26 - klu.low
self.macd_signal_dis = abs(klu.macd - klu.signal)
self.ema52_ema26_dis = abs(klu.ema52 - klu.ema26)
self.fx_type = Chan_FX.UNKNOWN
self.bi_zs = None
self.seg_zs = None
self.last_bi_zs = None
# ==================== EMA 通用计算方法 ====================
@staticmethod
def cal_ema_pos(high, low, close, ema_value, threshold=0):
"""
计算K线与任意EMA的客观位置关系(与趋势方向无关,支持threshold容差)
参数:
high, low, close: K线的高低收盘价
ema_value: EMA的值
threshold: 容差值(绝对值),在此范围内视为"接近/触碰"
例如 BTC 价格 $100,000 时 threshold=100 表示差100点视为触碰
返回:
Chan_EMA_POS 枚举值
判断逻辑(以threshold=100, ema=97000为例):
ema_zone = [96900, 97100] EMA上下各扩展threshold
ABOVE: low > 97100 K线完全在zone上方(远离EMA)
NEAR_ABOVE: 97000 < low <= 97100 K线在上方但下影线进入zone(接近EMA)
CROSS_CLOSE_ABOVE: close > 97000, low <= 97000 K线穿越EMA,收盘在上方
ON_EMA: abs(close - 97000) <= 100 收盘价在zone内
CROSS_CLOSE_BELOW: close < 97000, high >= 97000 K线穿越EMA,收盘在下方
NEAR_BELOW: 96900 <= high < 97000 K线在下方但上影线进入zone(接近EMA)
BELOW: high < 96900 K线完全在zone下方(远离EMA)
"""
if ema_value is None or ema_value == 0:
return Chan_EMA_POS.UNKNOWN
ema_upper = ema_value + threshold # EMA zone 上界
ema_lower = ema_value - threshold # EMA zone 下界
# 1. 收盘价在EMA附近(zone内)
if threshold > 0 and abs(close - ema_value) <= threshold:
# 收盘价在zone内,但还需要看是否有实际穿越
if low <= ema_value and close >= ema_value:
return Chan_EMA_POS.CROSS_CLOSE_ABOVE # 实际穿越了精确EMA线
elif high >= ema_value and close <= ema_value:
return Chan_EMA_POS.CROSS_CLOSE_BELOW
return Chan_EMA_POS.ON_EMA
# 2. K线实际穿越了精确的EMA线
if close > ema_value and low <= ema_value:
return Chan_EMA_POS.CROSS_CLOSE_ABOVE
if close < ema_value and high >= ema_value:
return Chan_EMA_POS.CROSS_CLOSE_BELOW
if close == ema_value:
return Chan_EMA_POS.ON_EMA
# 3. 没有实际穿越,检查是否"接近"(在threshold zone内)
if close > ema_value:
# K线在EMA上方
if threshold > 0 and low <= ema_upper:
return Chan_EMA_POS.NEAR_ABOVE # 下影线进入zone,接近但未触碰
return Chan_EMA_POS.ABOVE # 远离EMA
else:
# K线在EMA下方
if threshold > 0 and high >= ema_lower:
return Chan_EMA_POS.NEAR_BELOW # 上影线进入zone,接近但未触碰
return Chan_EMA_POS.BELOW # 远离EMA
@staticmethod
def cal_ema_semantic(ema_pos, kline_dir, ema_dir):
"""
根据客观位置 + K线方向 + 趋势方向,计算语义状态
参数:
ema_pos: Chan_EMA_POS 客观位置
kline_dir: Chan_KLINE_DIR K线方向 (UP/DOWN/COMBINE/INCLUDED)
ema_dir: int 趋势方向 (1=多头, -1=空头, 0=盘整)
返回:
Chan_EMA_SEMANTIC 枚举值
语义含义(以多头为例,空头完全对称):
TOUCH_HOLD: 触碰EMA,收盘守住趋势侧(支撑/压力有效)
BREAK: 穿越EMA,收盘在逆势侧(支撑/压力失败)
DEEP_COUNTER: 完全在EMA逆势侧(深度回调/反抽)
TOUCH_FAIL: 逆势触碰EMA但未穿越(反弹/反抽力度不足)
RECOVER: 逆势后穿越EMA回到趋势侧(收复EMA)
TREND_SIDE: 完全在EMA趋势侧(正常运行)
STRONG_TREND: 顺势K线完全在EMA趋势侧(强势,远未及EMA)
WEAK_COUNTER: 逆势K线完全在EMA逆势侧(弱势,远未到EMA)
"""
if ema_pos == Chan_EMA_POS.UNKNOWN:
return Chan_EMA_SEMANTIC.NEUTRAL
# 统一处理:将多头/盘整和空头映射到同一套逻辑
# is_bull=True 时,"趋势侧"=上方,"逆势侧"=下方
# is_bull=False时,"趋势侧"=下方,"逆势侧"=上方
is_bull = ema_dir >= 0 # 多头和盘整都按多头逻辑处理
# K线是否是顺势方向(多头下UP为顺势,空头下DOWN为顺势)
is_trend_kline = (kline_dir == Chan_KLINE_DIR.UP) if is_bull else (kline_dir == Chan_KLINE_DIR.DOWN)
is_counter_kline = (kline_dir == Chan_KLINE_DIR.DOWN) if is_bull else (kline_dir == Chan_KLINE_DIR.UP)
# 位置映射:多头下 ABOVE=趋势侧, BELOW=逆势侧; 空头反过来
trend_side = Chan_EMA_POS.ABOVE if is_bull else Chan_EMA_POS.BELOW
counter_side = Chan_EMA_POS.BELOW if is_bull else Chan_EMA_POS.ABOVE
near_trend = Chan_EMA_POS.NEAR_ABOVE if is_bull else Chan_EMA_POS.NEAR_BELOW
near_counter = Chan_EMA_POS.NEAR_BELOW if is_bull else Chan_EMA_POS.NEAR_ABOVE
cross_to_trend = Chan_EMA_POS.CROSS_CLOSE_ABOVE if is_bull else Chan_EMA_POS.CROSS_CLOSE_BELOW
cross_to_counter = Chan_EMA_POS.CROSS_CLOSE_BELOW if is_bull else Chan_EMA_POS.CROSS_CLOSE_ABOVE
# COMBINE / INCLUDED 方向:只看位置,不区分强弱
if not is_trend_kline and not is_counter_kline:
if ema_pos == trend_side:
return Chan_EMA_SEMANTIC.TREND_SIDE
elif ema_pos in (near_trend, cross_to_trend, Chan_EMA_POS.ON_EMA):
return Chan_EMA_SEMANTIC.APPROACHING
elif ema_pos in (near_counter, cross_to_counter):
return Chan_EMA_SEMANTIC.APPROACHING
elif ema_pos == counter_side:
return Chan_EMA_SEMANTIC.DEEP_COUNTER
return Chan_EMA_SEMANTIC.NEUTRAL
# 逆势K线(多头下的下跌K线 / 空头下的上涨K线)
if is_counter_kline:
if ema_pos == trend_side:
return Chan_EMA_SEMANTIC.STRONG_TREND # 逆势K线仍在趋势侧(回调很浅)
elif ema_pos == near_trend:
return Chan_EMA_SEMANTIC.APPROACHING # 接近EMA,即将测试支撑/压力
elif ema_pos == cross_to_trend:
return Chan_EMA_SEMANTIC.TOUCH_HOLD # 触碰EMA后守住趋势侧
elif ema_pos == Chan_EMA_POS.ON_EMA:
return Chan_EMA_SEMANTIC.TOUCH_HOLD # 收盘在EMA附近,视为守住
elif ema_pos == cross_to_counter:
return Chan_EMA_SEMANTIC.BREAK # 穿越EMA到逆势侧
elif ema_pos == near_counter:
return Chan_EMA_SEMANTIC.BREAK # 接近EMA但收盘在逆势侧,也视为击穿
elif ema_pos == counter_side:
return Chan_EMA_SEMANTIC.DEEP_COUNTER # 完全在逆势侧
# 顺势K线(多头下的上涨K线 / 空头下的下跌K线)
if is_trend_kline:
if ema_pos == counter_side:
return Chan_EMA_SEMANTIC.WEAK_COUNTER # 顺势K线却在逆势侧(弱势)
elif ema_pos == near_counter:
return Chan_EMA_SEMANTIC.APPROACHING # 从逆势侧接近EMA
elif ema_pos == cross_to_counter:
return Chan_EMA_SEMANTIC.TOUCH_FAIL # 触碰EMA但未穿越回趋势侧
elif ema_pos == Chan_EMA_POS.ON_EMA:
return Chan_EMA_SEMANTIC.TOUCH_FAIL # 收盘在EMA附近,未确认突破
elif ema_pos == cross_to_trend:
return Chan_EMA_SEMANTIC.RECOVER # 从逆势侧穿越回趋势侧
elif ema_pos == near_trend:
return Chan_EMA_SEMANTIC.RECOVER # 接近趋势侧(刚收复EMA附近)
elif ema_pos == trend_side:
return Chan_EMA_SEMANTIC.TREND_SIDE # 完全在趋势侧(正常)
return Chan_EMA_SEMANTIC.NEUTRAL
@staticmethod
def semantic_to_int(semantic):
"""将 Chan_EMA_SEMANTIC 枚举转换为整数,兼容旧的 ema52_status 数值"""
mapping = {
Chan_EMA_SEMANTIC.TOUCH_HOLD: 1,
Chan_EMA_SEMANTIC.BREAK: 2,
Chan_EMA_SEMANTIC.DEEP_COUNTER: 3,
Chan_EMA_SEMANTIC.TOUCH_FAIL: 4,
Chan_EMA_SEMANTIC.RECOVER: 5,
Chan_EMA_SEMANTIC.TREND_SIDE: 6,
Chan_EMA_SEMANTIC.STRONG_TREND: 7,
Chan_EMA_SEMANTIC.WEAK_COUNTER: 8,
Chan_EMA_SEMANTIC.APPROACHING: 9,
Chan_EMA_SEMANTIC.NEUTRAL: 0,
}
return mapping.get(semantic, 0)
# threshold_pct: 阈值百分比,用于自动计算绝对阈值
# 例如 0.001 表示 EMA 值的 0.1%BTC $100,000 时 threshold = $100
threshold_pct = 0.001
def set_bsp_type(self, bsp_type):
if bsp_type and bsp_type != Chan_BSP_TYPE.NONE:
self.bsp_type = bsp_type
self.bsp = True
def cal_all_ema_status(self):
"""
统一计算所有EMA与K线的位置关系和语义状态
threshold 自动按 EMA 值的百分比计算(cls.threshold_pct,默认0.1%
- BTC $100,000 时:threshold ≈ $100
- ETH $3,000 时:threshold ≈ $3
- SOL $200 时:threshold ≈ $0.2
结果存储在 self.ema_status 字典中,格式:
{
'ema24': {'pos': Chan_EMA_POS, 'semantic': Chan_EMA_SEMANTIC, 'value': float, 'threshold': float},
'ema52': {...},
...
}
同时保持向后兼容:self.ema52_pos 和 self.ema52_status
"""
ema_configs = {
'ema24': self.ema24,
'ema52': self.ema52,
'ema104': self.ema104,
'ema156': self.ema156,
'ema208': self.ema208,
}
self.ema_status = {}
for name, value in ema_configs.items():
# 按 EMA 值的百分比自动计算阈值
threshold = abs(value) * self.threshold_pct if value and self.threshold_pct > 0 else 0
pos = ChanKLC.cal_ema_pos(self.high, self.low, self.close, value, threshold)
semantic = ChanKLC.cal_ema_semantic(pos, self.dir, self.ema_dir)
self.ema_status[name] = {
'pos': pos,
'semantic': semantic,
'value': value,
'threshold': threshold,
}
# 向后兼容
self.ema52_pos = self.ema_status['ema52']['pos']
self.ema52_status = ChanKLC.semantic_to_int(self.ema_status['ema52']['semantic'])
def get_ema_pos(self, ema_name):
"""获取指定EMA的客观位置,如 klc.get_ema_pos('ema24')"""
if ema_name in self.ema_status:
return self.ema_status[ema_name]['pos']
return Chan_EMA_POS.UNKNOWN
def check_ema_pos(self):
if len(self.ema_status) > 0:
for ema_name, pos in self.ema_status.items():
#print(self.end_time, ema_name, pos['pos'])
if ((self.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc_fx_type == Chan_KLC_FX.TOP2) and pos['pos'] == Chan_EMA_POS.CROSS_CLOSE_BELOW) or ((self.klc_fx_type == Chan_KLC_FX.BOTTOM1 or self.klc_fx_type == Chan_KLC_FX.BOTTOM2) and pos['pos'] == Chan_EMA_POS.CROSS_CLOSE_ABOVE):
#print("---------------------")
return ema_name
return None
def get_ema_semantic(self, ema_name):
"""获取指定EMA的语义状态,如 klc.get_ema_semantic('ema52')"""
if ema_name in self.ema_status:
return self.ema_status[ema_name]['semantic']
return Chan_EMA_SEMANTIC.NEUTRAL
def set_trend(self, trend):
self.trend = trend
def to_string(self):
out = ""
start = self.start_time if self.start_time is not None else ""
end = self.end_time if self.end_time is not None else ""
price_diff = getattr(self, 'price_diff', None)
out += str(start) + " " + str(end) + " " + str(self.close) + " " + str(self.ema24) + " " + str(self.ema52) + " " + str(self.trend) + " " + str(self.close - self.ema52)
return out
def set_bi_zs(self, bi_zs):
if bi_zs:
self.bi_zs = bi_zs
def set_klc_fx_type(self, klc_fx_type):
#print(self.start_time, klc_fx_type, self.get_feature_data()['klu_macd'], self.get_feature_data()['klu_macdhist'], self.get_feature_data()['klu_rsi'])
self.klc_fx_type = klc_fx_type
#self.cal_fx()
ema_name = self.check_ema_pos()
hist_div = abs(self.macdhist - self.next.macdhist)
#print(self.end_time, self.dir, abs(self.macdhist), hist_div)
#if ema_name:
#print(self.end_time, ema_name, self.ema_status[ema_name]['semantic'], hist_div)
#self.cal_bb_out()
#print(self.pre.start_time, self.next.end_time, self.klc_fx_type)
if klc_fx_type == Chan_KLC_FX.TOP1 or klc_fx_type == Chan_KLC_FX.TOP2 or klc_fx_type == Chan_KLC_FX.BOTTOM1 or klc_fx_type == Chan_KLC_FX.BOTTOM2:
self.cal_fx_box()
self.cal_fx_type()
def cal_fx_type(self):
if self.fx == Chan_FX_TYPE.TOP and self.next:
if self.ema52_dis > self.ema26_dis:
if self.pre.macd < self.macd and self.macd < self.next.macd:
self.fx_type = Chan_FX.CONTINUATION
else:
self.fx_type = Chan_FX.REVERSAL
elif self.fx == Chan_FX_TYPE.BOTTOM and self.next:
if self.ema52_dis < self.ema26_dis:
if self.pre.macd > self.macd and self.macd > self.next.macd:
self.fx_type = Chan_FX.CONTINUATION
else:
self.fx_type = Chan_FX.REVERSAL
#if self.fx_type != Chan_FX.UNKNOWN and self.fx_type != Chan_FX.CONTINUATION:
#print(self.end_time, self.fx_type)
def cal_fx_box(self):
# 每次重算前先清空,避免旧box残留
self.fx_box = None
start_time = None
end_time = None
high = 0
low = 0
display = False
if self.pre and self.next and self.next.end_time:
self.next.in_fx = True
if self.fx == Chan_FX_TYPE.TOP:
start_time = self.pre.end_time
end_time = self.next.end_time
high = self.high
low = self.pre.low if self.pre.low < self.next.low else self.next.low
if self.next.close < self.pre.low or True:
display = True
elif self.fx == Chan_FX_TYPE.BOTTOM:
start_time = self.pre.end_time
end_time = self.next.end_time
high = self.pre.high if self.pre.high > self.next.high else self.next.high
low = self.low
if self.next.close > self.pre.high or True:
display = True
if high > 0 and self.next.end_time and display:
#print(start_time, end_time, high, low)
# Chan_FX_BOX 这里导入的是模块,类名在模块内部为 Chan_FX_Box
self.fx_confirmed = True
self.fx_box = Chan_FX_Box.Chan_FX_Box(start_time, end_time, high, low)
def check_fx_confirmed(self, last_top, last_bottom):
if last_top and last_bottom and False:
if last_top.index > last_bottom.index:
if self.in_fx == False and last_top.fx_confirmed == False:
pre = last_top.pre
if pre.low > self.close:
last_top.fx_confirmed = True
if last_top.fx_box:
last_top.fx_box.end_time = self.end_time
#print(self.end_time, "fx_confirmed top")
else:
high = last_top.high
low = self.low
last_top.fx_box = Chan_FX_Box.Chan_FX_Box(last_top.pre.start_time, self.end_time, high, low)
#print(self.end_time, "fx_confirmed new box top")
elif self.in_fx == False and last_bottom.fx_confirmed == False:
pre = last_bottom.pre
if pre.high < self.close:
last_bottom.fx_confirmed = True
if last_bottom.fx_box:
last_bottom.fx_box.end_time = self.end_time
#print(self.end_time, "fx_confirmed bottom")
else:
high = self.high
low = last_bottom.low
last_bottom.fx_box = Chan_FX_Box.Chan_FX_Box(last_bottom.pre.start_time, self.end_time, high, low)
#print(self.end_time, "fx_confirmed new box bottom")
def add_klu(self, klu):
self.klu_list.append(klu)
def check_klc_state(self, last_fx_klc):
if last_fx_klc and last_fx_klc.fx == Chan_FX_TYPE.TOP:
if self.high > last_fx_klc.high:
self.klc_state = Chan_KLC_STATE.S11
else:
self.klc_state = Chan_KLC_STATE.S_11
elif last_fx_klc and last_fx_klc.fx == Chan_FX_TYPE.BOTTOM:
if self.low < last_fx_klc.low:
self.klc_state = Chan_KLC_STATE.S_11
else:
self.klc_state = Chan_KLC_STATE.S11
if self.pre and self.pre.fx == Chan_FX_TYPE.TOP:
self.klc_state = Chan_KLC_STATE.S10
elif self.pre and self.pre.fx == Chan_FX_TYPE.BOTTOM:
self.klc_state = Chan_KLC_STATE.S_10
#print(self.end_time, self.klc_state)
def set_end_klu(self, klu):
self.end_klu = klu
self.end_time = klu.time
self.close = klu.close
for klu in self.klu_list:
if klu.exception:
self.exception = True
print(klu.time, "exception")
if klu.separate_div > 0:
self.separate_div = True
if klu.continue_div:
self.continue_div = klu.continue_div
if klu.macd_state != Chan_MACD_STATE.UNKNOWN:
self.state = klu.macd_state
klu.set_klc(self)
self.klc_dir = Chan_KLINE_DIR.UP if self.close > self.open else Chan_KLINE_DIR.DOWN
self.cal_indicators()
self.cal_all_ema_status()
if self.open > self.high:
self.open = self.high
if self.close > self.high:
self.close = self.high
if self.close < self.low:
self.close = self.low
if self.open < self.low:
self.open = self.low
#print(self.end_time, self.open, self.close, self.high, self.low)
#print(klu.time, klu.open, klu.close, klu.high, klu.low)
def cal_fx(self):
if self.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc_fx_type == Chan_KLC_FX.TOP2:
#print(self.end_time, self.fx, self.macd, self.macdhist, len(self.klu_list))
if self.state == Chan_MACD_STATE.HIGH_EMPTY and self.macd > 0:
#print(self.end_time, self.state, self.macd, self.klc_fx_type)
self.klc_fx_type = Chan_KLC_FX.TOP6
if self.separate_div or self.continue_div:
self.klc_fx_type = Chan_KLC_FX.TOP7
if self.signal > 0 and self.macd > self.signal:
self.klc_fx_type = Chan_KLC_FX.TOP8
else:
if self.klc_fx_type == Chan_KLC_FX.BOTTOM1 or self.klc_fx_type == Chan_KLC_FX.BOTTOM2:
if self.macdhist > 0 and self.macd < 0:
self.klc_fx_type = Chan_KLC_FX.BOTTOM5
return
if self.state == Chan_MACD_STATE.HIGH_EMPTY and self.macd < 0:
self.klc_fx_type = Chan_KLC_FX.BOTTOM6
#print(self.end_time, self.state, self.macd, self.klc_fx_type)
if self.separate_div or self.continue_div:
self.klc_fx_type = Chan_KLC_FX.BOTTOM7
if self.signal < 0 and self.macd < self.signal:
self.klc_fx_type = Chan_KLC_FX.BOTTOM8
def cal_bb_out(self):
for klu in self.klu_list:
if self.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc_fx_type == Chan_KLC_FX.TOP2:
#print(self.start_time, self.klc_fx_type, klu.high, klu.bb52upper, self.macd, self.next.macd, klu.time)
if self.high >= klu.bb52upper and klu.bb52upper > 0 and self.next and self.high > self.next.high:
self.klc_fx_type = Chan_KLC_FX.TOP4
print(self.end_time, self.klc_fx_type)
if self.klc_fx_type == Chan_KLC_FX.BOTTOM1 or self.klc_fx_type == Chan_KLC_FX.BOTTOM2:
#print(self.start_time, self.klc_fx_type, klu.low, klu.bb52lower, self.macd, self.next.macd, klu.time)
if self.low <= klu.bb52lower and klu.bb52lower > 0 and self.next and self.low < self.next.low:
self.klc_fx_type = Chan_KLC_FX.BOTTOM4
print(self.end_time, self.klc_fx_type)
def cal_indicators(self):
for index in range(1, len(self.klu_list)):
self.volume += self.klu_list[index].volume
self.rsi += self.klu_list[index].rsi
self.volume_ratio += self.klu_list[index].volume_ratio
self.macdhist += self.klu_list[index].macdhist
self.ema26 += self.klu_list[index].ema26
self.ema24 += self.klu_list[index].ema24
self.ema52 += self.klu_list[index].ema52
self.ema104 += self.klu_list[index].ema104
self.ema156 += self.klu_list[index].ema156
self.ema208 += self.klu_list[index].ema208
self.ema13 += self.klu_list[index].ema13
self.ema7 += self.klu_list[index].ema7
self.bb2633upper += self.klu_list[index].bb2633upper
self.bb2633lower += self.klu_list[index].bb2633lower
self.bb2633middle += self.klu_list[index].bb2633middle
self.ma5 += self.klu_list[index].ma5
self.ema5 += self.klu_list[index].ema5
if self.ema_dir != self.klu_list[index].ema_dir:
self.ema_dir = 0
n = len(self.klu_list)
self.rsi = self.rsi / n
self.volume_ratio = self.volume_ratio / n
self.volume = self.volume / n
self.macdhist = self.macdhist / n
self.ema26 = self.ema26 / n
self.ema24 = self.ema24 / n
self.ema52 = self.ema52 / n
self.ema104 = self.ema104 / n
self.ema156 = self.ema156 / n
self.ema208 = self.ema208 / n
self.ema13 = self.ema13 / n
self.ema7 = self.ema7 / n
self.ma5 = self.ma5 / n
self.ema5 = self.ema5 / n
self.bb2633upper = self.bb2633upper / n
self.bb2633lower = self.bb2633lower / n
self.bb2633middle = self.bb2633middle / n
if len(self.klu_list) > 0:
self.macd = self.klu_list[-1].macd
self.signal = self.klu_list[-1].signal
self.body = abs(self.close - self.open)
self.upper_shadow = self.high - max(self.close, self.open)
self.lower_shadow = min(self.close, self.open) - self.low
self.body_ratio = self.body / self.open
self.upper_shadow_ratio = self.upper_shadow / self.open
self.lower_shadow_ratio = self.lower_shadow / self.open
self.candle_dir = Chan_K_DIR.CROSS if self.close == self.open else Chan_K_DIR.BULL if self.close > self.open else Chan_K_DIR.BEAR
self.range = self.high - self.low
def set_next(self, klc):
self.next = klc
def set_pre(self, klc):
self.pre = klc
def set_state(self, state):
self.state = state
def check_klu_included(self, klu):
if self.high >= klu.high:
# high大于,low小于,左包含
if self.low <= klu.low:
self.add_klu(klu=klu)
# gn>gn-1
if self.dir == Chan_KLINE_DIR.UP:
# UP -> max(dn)
self.low = klu.low
else:
# DOWN -> min(gn)
self.high = klu.high
#self.print(klu, "Z")
return True
# high大于,low大于,不包含
else:
# if self.low > klu.low
# high相等,右包含
if self.high == klu.high:
self.add_klu(klu=klu)
# UP -> max(gn)
if self.dir == Chan_KLINE_DIR.UP:
self.high = klu.high
else:
# DOWN -> min(dn)
self.low = klu.low
return True
else:
return False
else:
# high小于,low大于,右包含
if self.low >= klu.low:
self.add_klu(klu=klu)
# gn>gn-1
if self.dir == Chan_KLINE_DIR.UP:
# UP -> max(gn)
self.high = klu.high
else:
# DOWN -> min(dn)
self.low = klu.low
#self.print(klu, "Y")
return True
else:
# high小于,low小于,不包含
return False
def set_fx(self, fx: Chan_FX_TYPE):
self.fx = fx
def cal_invisible(self):
if self.fx == Chan_FX_TYPE.TOP:
if self.macdhist < 0 and self.macd > 0:
self.klc_fx_type = Chan_KLC_FX.TOP5
else:
if self.fx == Chan_FX_TYPE.BOTTOM:
if self.macdhist > 0 and self.macd < 0:
self.klc_fx_type = Chan_KLC_FX.BOTTOM5
def set_pre_fx(self):
if self.pre and self.pre.pre:
self.pre.fx = self.check_fx(self.pre.pre, self.pre)
def check_fx(self, k1, k2):
if k2.high > k1.high and k2.high > self.high:
return Chan_FX_TYPE.TOP
elif k2.low < k1.low and k2.low < self.low:
return Chan_FX_TYPE.BOTTOM
else:
return Chan_FX_TYPE.UNKNOWN
def set_bi(self, bi):
self.bi = bi
self.distance = self.index - bi.start_klc.index
#print(self.start_time, self.distance, bi.index, bi.dir)
+388
View File
@@ -0,0 +1,388 @@
from chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLU_TYPE, Chan_K_DIR, Chan_MACD_STATE, Chan_MACDHIST_STATE, Chan_PRICE_TREND, Chan_KLU_PATTERN, Chan_KLC_FX
class ChanKLU:
def __init__(self, time, open, high, low, close, volume):
# _time, _close, _open, _high, _low, _extra_info={}
self.kl_type = None
self.time = time
self.close = close
self.open = open
self.high = high
self.low = low
self.volume = volume
self.idx = 0
self.index = 0
self.macd = 0
self.signal = 0
self.macdhist = 0
self.klc = None
self.rsi = 0
self.volume_ratio = 0
self.bb52upper = 0
self.bb52lower = 0
# === 新增:K线类型 ===
self.kline_type = None # K线类型:大阳线、大阴线、小阳线、小阴线
self.pattern = Chan_KLU_PATTERN.UNKNOWN
# === 新增:实时分型相关属性 ===
self.pre = None # 前一根K线
self.next = None # 后一根K线
self.fx_type = Chan_FX_TYPE.UNKNOWN # 分型类型:0=无分型,1=顶分型,-1=底分型
self.fx_strength = 0 # 分型强度:0-100
self.fx_confirmed = False # 分型是否确认
self.klu_type = None
self.range = self.high - self.low
self.body = abs(self.close - self.open)
self.upper_shadow = self.high - max(self.close, self.open)
self.lower_shadow = min(self.close, self.open) - self.low
self.body_ratio = self.body / self.range if self.range != 0 else 0
self.upper_shadow_ratio = self.upper_shadow / self.body if self.body != 0 else float('inf')
self.lower_shadow_ratio = self.lower_shadow / self.body if self.body != 0 else float('inf')
self.exception = False
#self.cal_exception()
self.candle_dir = Chan_K_DIR.CROSS if self.close == self.open else Chan_K_DIR.BULL if self.close > self.open else Chan_K_DIR.BEAR
self.continue_div = 0
self.separate_div = 0
self.near0_return = 0
self.ema52 = 0
self.ema24 = 0
self.ema26 = 0
self.ema104 = 0
self.ema156 = 0
self.ema208 = 0
self.macd_slop = 0
self.signal_slop = 0
self.hist_slop = 0
self.hist_state = Chan_MACDHIST_STATE.UNKNOWN
self.macd_state = Chan_MACD_STATE.UNKNOWN
self.macd_hist_gap = 0
self.trend = Chan_PRICE_TREND.UNKNOWN
self.seg_histset_index = 0
# === 归零轴细化与模式/背离 ===
self.zero_axis = False # 是否归零轴(穿越或接近)
self.zero_axis_state = "none" # {none,crossing,near}
self.zero_axis_side = 0 # 1:above, -1:under, 0:none
self.zero_axis_score = 0 # 0-100 综合评分
self.mode1_touch_ema52 = False # 单边后触碰EMA52
self.mode2_fast_to_zero = False # 快线向零收敛
self.mode3_double_tf = False # 双周期归零(近似占位,由上层填充高周期确认)
self.mode3_dir = "none" # {long_strong_rebound, short_strong_rebound, none}
self.mode4_touch52_no_zero = False # 先触碰EMA52但黄白线未归零
self.div_type = "none" # {bearish, bullish, hidden_bearish, hidden_bullish, none}
self.div_score = 0.0 # 背离强度(0-100)
self.ema_dir = 0
self.get_ema_dir()
self.bb2633upper = 0
self.bb2633lower = 0
self.bb2633middle = 0
self.ma5 = 0
self.ema5 = 0
#print(self.open, self.close, self.high, self.low, self.candle_dir, self.strength)
def set_macd_state(self, state):
self.macd_state = state
def set_pattern(self, pattern):
self.pattern = pattern
def set_seg_histset_index(self, seg_histset_index):
self.seg_histset_index = seg_histset_index
#print(self.time, self.seg_histset_index)
def to_string(self):
return f"{self.time} {self.candle_dir} {self.pattern}"
def cal_exception(self):
if self.upper_shadow_ratio > 5 or self.lower_shadow_ratio > 5:
self.exception = True
#print(self.time, self.upper_shadow_ratio, self.lower_shadow_ratio, self.body, self.lower_shadow, self.upper_shadow, self.high, self.low, self.close, self.open)
#self.exception = False
def set_trend(self, trend):
self.trend = trend
def set_separate_div(self, separate_div):
self.separate_div = separate_div
if self.klc and self.klc.pre and self.klc.next:
fx = self.check_fx_dir(self.klc.pre, self.klc.next)
if fx == Chan_FX_TYPE.TOP:
if self.macdhist > 0:
self.separate_div = separate_div
else:
self.separate_div = 0
elif fx == Chan_FX_TYPE.BOTTOM:
if self.macdhist < 0:
self.separate_div = separate_div
else:
self.separate_div = 0
def check_fx_dir(self, pre, next):
fx = Chan_FX_TYPE.UNKNOWN
if pre.klc_fx_type == Chan_KLC_FX.TOP1 or pre.klc_fx_type == Chan_KLC_FX.TOP2 or next.klc_fx_type == Chan_KLC_FX.TOP1 or next.klc_fx_type == Chan_KLC_FX.TOP2 or self.klc.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc.klc_fx_type == Chan_KLC_FX.TOP2:
fx = Chan_FX_TYPE.TOP
elif pre.klc_fx_type == Chan_KLC_FX.BOTTOM1 or pre.klc_fx_type == Chan_KLC_FX.BOTTOM2 or next.klc_fx_type == Chan_KLC_FX.BOTTOM1 or next.klc_fx_type == Chan_KLC_FX.BOTTOM2 or self.klc.klc_fx_type == Chan_KLC_FX.BOTTOM1 or self.klc.klc_fx_type == Chan_KLC_FX.BOTTOM2:
fx = Chan_FX_TYPE.BOTTOM
return fx
def set_next(self, next):
self.next = next
#if self.fx_type != Chan_FX_TYPE.UNKNOWN and self.fx_strength > 1:
#print(self.index, self.time, self.fx_type, self.fx_confirmed, self.fx_strength)
def set_pre(self, pre):
self.pre = pre
def set_klc(self, klc):
self.klc = klc
def set_histset(self, histset):
"""设置HistSet关联"""
self.histset = histset
def set_seg(self, seg):
"""设置Seg关联"""
self.seg = seg
def set_unittf(self, unittf):
"""设置UnitTF关联"""
self.unittf = unittf
def set_idx(self, idx):
self.idx = idx
self.index = idx
def check_price_ema156(self):
if self.check_indicators():
if self.close > self.ema156:
return 1
elif self.close < self.ema156:
return -1
else:
return 0
else:
return 0
def get_ema_dir(self):
if self.check_indicators():
if self.ema24 > self.ema52 and self.ema52 > self.ema104 and self.ema104 > self.ema156:
self.ema_dir = 1
elif self.ema24 < self.ema52 and self.ema52 < self.ema104 and self.ema104 < self.ema156:
self.ema_dir = -1
else:
self.ema_dir = 0
def check_indicators(self):
if self.ema156 == 0:
return False
else:
return True
def set_indicators(self, item):
self.macd = float(item['macd']) if 'macd' in item and item['macd'] else 0
self.signal = float(item['macdsignal']) if 'macdsignal' in item and item['macdsignal'] else 0
self.macdhist = float(item['macdhist']) if 'macdhist' in item and item['macdhist'] else 0
self.ema26 = float(item['ema26']) if 'ema26' in item and item['ema26'] else 0
self.ema52 = float(item['ema52']) if 'ema52' in item and item['ema52'] else 0
self.ema24 = float(item['ema24']) if 'ema24' in item and item['ema24'] else 0
self.ema104 = float(item['ema104']) if 'ema104' in item and item['ema104'] else 0
self.ema156 = float(item['ema156']) if 'ema156' in item and item['ema156'] else 0
self.ema208 = float(item['ema208']) if 'ema208' in item and item['ema208'] else 0
self.ema13 = float(item['ema13']) if 'ema13' in item and item['ema13'] else 0
self.ema7 = float(item['ema7']) if 'ema7' in item and item['ema7'] else 0
self.rsi = float(item['rsi']) if 'rsi' in item and item['rsi'] else 0
self.volume_ratio = float(item['volume_ratio']) if 'volume_ratio' in item and item['volume_ratio'] else 0
self.bb52upper = float(item['bb52upper']) if 'bb52upper' in item and item['bb52upper'] else 0
self.bb52lower = float(item['bb52lower']) if 'bb52lower' in item and item['bb52lower'] else 0
self.bb2633upper = float(item['bb2633upper']) if 'bb2633upper' in item and item['bb2633upper'] else 0
self.bb2633lower = float(item['bb2633lower']) if 'bb2633lower' in item and item['bb2633lower'] else 0
self.bb2633middle = float(item['bb2633middle']) if 'bb2633middle' in item and item['bb2633middle'] else 0
self.ma5 = float(item['ma5']) if 'ma5' in item and item['ma5'] else 0
self.ema5 = float(item['ema5']) if 'ema5' in item and item['ema5'] else 0
def cal_macd_state(self):
# 按定义精简实现:优先级 CROSS0 > 位置(HIGH/HE/RETURN_ZERO) > NEAR0 > UNKNOWN
# 首条或缺前一根
if not hasattr(self, 'pre') or self.pre is None:
self.macd_state = Chan_MACD_STATE.START
return self.macd_state
# 基本校验
if (self.macd == 0 and self.signal == 0 and self.macdhist == 0) or self.ema52 == 0:
self.macd_state = Chan_MACD_STATE.UNKNOWN
return self.macd_state
# 归零轴判断
if self.signal > 0:
if self.macd < self.signal:
if 0 < self.low - self.ema52 < 100:
self.near0_return = 0
elif self.close > self.ema52 and self.low < self.ema52 and self.open > self.ema52:
self.near0_return = 0
elif self.close < self.ema52 and self.open > self.ema52 and self.high > self.ema52 and self.low < self.ema52:
self.near0_return = 0
elif self.close < self.ema52 and self.open < self.ema52 and self.high > self.ema52 and self.low < self.ema52:
self.near0_return = 0
elif self.close > self.ema52 and self.open > self.ema52 and self.high > self.ema52 and self.low < self.ema52:
self.near0_return = 0
elif self.close > self.ema52 and self.high > self.ema52 and self.low < self.ema52:
self.near0_return = 0
else:
if self.macd > self.signal:
if 0 < self.ema52 - self.high < 100:
self.near0_return = 0
elif self.close < self.ema52 and self.high > self.ema52 and self.open < self.ema52:
self.near0_return = 0
elif self.close < self.ema52 and self.open > self.ema52 and self.high > self.ema52 and self.low < self.ema52:
self.near0_return = 0
elif self.close > self.ema52 and self.open < self.ema52 and self.high > self.ema52 and self.low < self.ema52:
self.near0_return = 0
elif self.close > self.ema52 and self.high > self.ema52 and self.open >= self.ema52 and self.low < self.ema52:
self.near0_return = 0
elif self.close > self.ema52 and self.high > self.ema52 and self.low < self.ema52:
self.near0_return = 0
# 向上穿越EMA52 7
if self.close > self.ema52 and self.open < self.ema52:
self.near0_return = 0
# 向下穿越EMA52 8
elif self.close < self.ema52 and self.open > self.ema52:
self.near0_return = 0
if self.pre.near0_return == 7:
# 向上穿越后的一根价格再EMA52上方 9
if self.low > self.ema52 and self.close > self.open:
self.near0_return = 9
if self.pre.near0_return == 8:
# 向下穿越后的一根价格再EMA52下方 10
if self.high < self.ema52 and self.close < self.open:
self.near0_return = 10
# CROSS0 仅以 Signal 穿越零轴判定
if self.pre.signal >= 0 and self.signal < 0:
self.macd_state = Chan_MACD_STATE.CROSS0_DOWN
return self.macd_state
if self.pre.signal <= 0 and self.signal > 0:
self.macd_state = Chan_MACD_STATE.CROSS0_UP
return self.macd_state
# 穿零轴后的形态:缠绕/倒挂(基于前一状态为CROSS0_*)
if self.pre.macd_state == Chan_MACD_STATE.CROSS0_UP or self.pre.macd_state == Chan_MACD_STATE.CROSS0_DOWN:
direction = 1 if self.pre.macd_state == Chan_MACD_STATE.CROSS0_UP else -1
hist_same_dir = (self.macdhist * direction) > 0
hist_decreasing = abs(self.macdhist) < abs(self.pre.macdhist)
lines_tight = abs(self.macd - self.signal) <= 12
# 倒挂:能量柱衰减且黄白线相对方向不利/出现反向能量释放
if hist_decreasing and (((self.macd - self.signal) * direction) < 0 or not hist_same_dir):
self.macd_state = Chan_MACD_STATE.CROSS_REV
return self.macd_state
# 缠绕/粘合:紧贴能量柱运行,无反向能量释放
if lines_tight and hist_same_dir:
self.macd_state = Chan_MACD_STATE.CROSS_OS
return self.macd_state
# 趋近零轴:细化 NEAR0_* 判定
NEAR0_EPS = 15
lines_near_zero = abs(self.macd) <= NEAR0_EPS or abs(self.signal) <= NEAR0_EPS
touch_52 = (self.ema52 != 0) and ((abs(self.close - self.ema52) <= NEAR0_EPS) or (self.low <= self.ema52 <= self.high))
touch_24 = (self.ema24 != 0) and ((abs(self.close - self.ema24) <= NEAR0_EPS) or (self.low <= self.ema24 <= self.high))
# 完美形态:白线接近零轴 + 价格触碰/轻破EMA52 + 黄线不穿零轴
if abs(self.macd) <= NEAR0_EPS and touch_52 and (not (self.pre.signal >= 0 and self.signal < 0)) and (not (self.pre.signal <= 0 and self.signal > 0)):
self.macd_state = Chan_MACD_STATE.NEAR0_PERFECT
#self.near0_return = 1
return self.macd_state
# EMA24 附近
if lines_near_zero and touch_24:
self.macd_state = Chan_MACD_STATE.NEAR0_24
#self.near0_return = 2
return self.macd_state
# EMA52 附近
if lines_near_zero and touch_52:
self.macd_state = Chan_MACD_STATE.NEAR0_52
#self.near0_return = 3
return self.macd_state
# 白线接近零轴但价格未至EMA52
if abs(self.macd) <= NEAR0_EPS and not touch_52:
self.macd_state = Chan_MACD_STATE.NEAR0_DIFF
#self.near0_return = 4
return self.macd_state
# 一般近零轴
if lines_near_zero or touch_52:
self.macd_state = Chan_MACD_STATE.NEAR0
#self.near0_return = 5
return self.macd_state
# 穿零轴后离开零轴
if self.pre.macd_state == Chan_MACD_STATE.CROSS0_UP and ((self.macd >= self.pre.macd and self.signal >= self.pre.signal) or (abs(self.macdhist) >= abs(self.pre.macdhist))):
self.macd_state = Chan_MACD_STATE.UP
return self.macd_state
if self.pre.macd_state == Chan_MACD_STATE.CROSS0_DOWN and ((self.macd <= self.pre.macd and self.signal <= self.pre.signal) or (abs(self.macdhist) >= abs(self.pre.macdhist))):
self.macd_state = Chan_MACD_STATE.DOWN
return self.macd_state
if self.pre.macd_state == Chan_MACD_STATE.UP and self.macd > self.pre.macd and self.signal > self.pre.signal:
self.macd_state = Chan_MACD_STATE.UP
return self.macd_state
if self.pre.macd_state == Chan_MACD_STATE.DOWN and self.macd < self.pre.macd and self.signal < self.pre.signal:
self.macd_state = Chan_MACD_STATE.DOWN
return self.macd_state
# 趋势兜底:强势同步上行/下行直接进入 UP/DOWN
if self.macd > 0 and self.signal > 0 and (self.macd >= self.pre.macd and self.signal >= self.pre.signal):
self.macd_state = Chan_MACD_STATE.UP
return self.macd_state
if self.macd < 0 and self.signal < 0 and (self.macd <= self.pre.macd and self.signal <= self.pre.signal):
self.macd_state = Chan_MACD_STATE.DOWN
return self.macd_state
# 峰值:白线高位出现局部顶
if hasattr(self.pre, 'pre') and self.pre and self.pre.pre and self.macd > 0:
if self.pre.macd > self.pre.pre.macd and self.pre.macd > self.macd:
self.macd_state = Chan_MACD_STATE.PEAK
return self.macd_state
# 高位状态的位置状态, 高位,高位空,归零轴
if (self.pre.macd_state == Chan_MACD_STATE.UP or self.pre.macd_state == Chan_MACD_STATE.HIGH or self.pre.macd_state == Chan_MACD_STATE.RZ_UP or self.pre.macd_state == Chan_MACD_STATE.PEAK or self.pre.macd_state == Chan_MACD_STATE.HIGH_EMPTY) and self.macd > 0:
# 高位空(正区间):能量柱衰减且黄白线间距较大
if abs(self.pre.macdhist) > 0 and abs(self.macdhist) < abs(self.pre.macdhist) and abs(self.macd - self.signal) > 5:
self.macd_state = Chan_MACD_STATE.HIGH_EMPTY
return self.macd_state
if abs(self.pre.macd - self.macd) < 10:
self.macd_state = Chan_MACD_STATE.HIGH
return self.macd_state
else:
if self.macd > self.pre.macd and self.signal > self.pre.signal:
self.macd_state = Chan_MACD_STATE.UP
return self.macd_state
elif self.macd < self.pre.macd and self.signal < self.pre.signal:
self.macd_state = Chan_MACD_STATE.RETURN_ZERO
return self.macd_state
if (self.pre.macd_state == Chan_MACD_STATE.DOWN or self.pre.macd_state == Chan_MACD_STATE.HIGH or self.pre.macd_state == Chan_MACD_STATE.RZ_DOWN or self.pre.macd_state == Chan_MACD_STATE.PEAK or self.pre.macd_state == Chan_MACD_STATE.HIGH_EMPTY) and self.macd < 0:
# 高位空(负区间):能量柱衰减且黄白线间距较大
if abs(self.pre.macdhist) > 0 and abs(self.macdhist) < abs(self.pre.macdhist) and abs(self.macd - self.signal) > 5:
self.macd_state = Chan_MACD_STATE.HIGH_EMPTY
return self.macd_state
if abs(self.pre.macd - self.macd) < 10:
self.macd_state = Chan_MACD_STATE.HIGH
return self.macd_state
else:
if self.macd > self.pre.macd and self.signal > self.pre.signal:
self.macd_state = Chan_MACD_STATE.RETURN_ZERO
return self.macd_state
elif self.macd < self.pre.macd and self.signal < self.pre.signal:
self.macd_state = Chan_MACD_STATE.DOWN
return self.macd_state
# 离开0轴开始上涨或者下跌阶段,高位之前的
if self.macd > 0 and self.pre:
if (self.pre.macd_state == Chan_MACD_STATE.NEAR0 or self.pre.macd_state == Chan_MACD_STATE.RZ_UP or self.pre.macd_state == Chan_MACD_STATE.CROSS0_UP) and (self.signal > self.pre.signal or self.close > self.ema52):
self.macd_state = Chan_MACD_STATE.RZ_UP
return self.macd_state
elif self.macd < 0 and self.pre:
if (self.pre.macd_state == Chan_MACD_STATE.NEAR0 or self.pre.macd_state == Chan_MACD_STATE.RZ_DOWN or self.pre.macd_state == Chan_MACD_STATE.CROSS0_DOWN) and (self.signal < self.pre.signal or self.close < self.ema52):
self.macd_state = Chan_MACD_STATE.RZ_DOWN
return self.macd_state
# 归零轴走势
if self.pre.macd_state == Chan_MACD_STATE.RETURN_ZERO:
if self.macd > 0:
if self.pre.macd > self.macd or abs(self.macdhist) <= abs(self.pre.macdhist) or self.signal <= self.pre.signal:
self.macd_state = Chan_MACD_STATE.RETURN_ZERO
return self.macd_state
else:
if self.pre.macd < self.macd or abs(self.macdhist) <= abs(self.pre.macdhist) or self.signal >= self.pre.signal:
self.macd_state = Chan_MACD_STATE.RETURN_ZERO
return self.macd_state
# 从 NEAR0 收敛到零轴的归零轴承接(正负两侧)
if self.pre.macd_state == Chan_MACD_STATE.NEAR0:
# 正区间朝零轴收敛
if self.macd > 0 and self.pre.macd > 0 and self.macd <= self.pre.macd and self.signal <= self.pre.signal:
self.macd_state = Chan_MACD_STATE.RETURN_ZERO
return self.macd_state
# 负区间朝零轴收敛
if self.macd < 0 and self.pre.macd < 0 and self.macd >= self.pre.macd and self.signal >= self.pre.signal:
self.macd_state = Chan_MACD_STATE.RETURN_ZERO
return self.macd_state
# 其余情况
if self.pre.macd_state == Chan_MACD_STATE.UNKNOWN:
if self.macd > 0 and self.close > self.ema52 and self.pre.pre and (self.pre.pre.macd_state == Chan_MACD_STATE.UP or self.pre.pre.macd_state == Chan_MACD_STATE.RZ_UP):
self.macd_state = self.pre.pre.macd_state
return self.macd_state
elif self.macd < 0 and self.close < self.ema52 and self.pre.pre and (self.pre.pre.macd_state == Chan_MACD_STATE.DOWN or self.pre.pre.macd_state == Chan_MACD_STATE.RZ_DOWN):
self.macd_state = self.pre.pre.macd_state
return self.macd_state
else:
self.macd_state = Chan_MACD_STATE.UNKNOWN
return self.macd_state
return self.macd_state
+91
View File
@@ -0,0 +1,91 @@
import copy
from typing import Dict, Optional
from chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR
import chanlun.core.ChanKLU as ChanKLU
from chanlun.core.ChanBI import ChanBI
class ChanSBI():
def __init__(self, start_bi: ChanBI, index, dir=Chan_BI_DIR.UP):
self.start_bi = start_bi
self.end_bi = None
self.index = index
self.dir = dir
self.high = start_bi.high
self.low = start_bi.low
self.pre = None
self.next = None
self.fx = Chan_FX_TYPE.UNKNOWN
self.bi_list = []
self.bi_list.append(start_bi)
self.has_fx_gap = False
def set_fx(self, fx):
self.fx = fx
def set_end_bi(self, bi):
self.end_bi = bi
def set_pre(self, sbi):
self.pre = sbi
def set_next(self, sbi):
self.next = sbi
def add_bi(self, bi):
self.bi_list.append(bi)
def check_fx(self):
if self.pre and self.next:
#print(self.pre.start_bi.start_time, self.start_bi.start_time, self.end_bi.end_time, self.next.start_bi.start_time, self.pre.high, self.high, self.next.high, self.pre.low, self.low, self.next.low, self.dir)
if self.high > self.pre.high and self.high > self.next.high:
self.fx = Chan_FX_TYPE.TOP
#print(self.start_bi.start_time, self.pre.start_bi.start_time, self.next.start_bi.start_time, self.fx)
if self.low > self.pre.high:
self.has_fx_gap = True
#print(self.start_bi.start_time, self.end_bi.end_time, self.pre.start_bi.start_time, self.next.start_bi.start_time, self.dir, self.has_fx_gap, self.fx)
return Chan_FX_TYPE.TOP
else:
if self.low < self.pre.low and self.low < self.next.low:
self.fx = Chan_FX_TYPE.BOTTOM
#print(self.start_bi.start_time, self.pre.start_bi.start_time, self.next.start_bi.start_time, self.fx)
if self.high < self.pre.low:
self.has_fx_gap = True
#print(self.start_bi.start_time, self.end_bi.end_time, self.pre.start_bi.start_time, self.next.start_bi.start_time, self.dir, self.has_fx_gap, self.fx)
return Chan_FX_TYPE.BOTTOM
return Chan_FX_TYPE.UNKNOWN
def check_seg_bi_broken(self):
broken = False
if self.fx == Chan_FX_TYPE.TOP:
if self.next.low < self.pre.high:
broken = True
elif self.fx == Chan_FX_TYPE.BOTTOM:
if self.next.high > self.pre.low:
broken = True
return broken
def check_bi_included(self, bi):
included = False
if self.high > bi.high:
# high大于,low小于,左包含
if self.low < bi.low:
included = True
# high大于,low大于,不包含
else:
# if self.low > bi.low
# high相等,右包含
included = False
else:
included = False
# high小于,low大于,右包含
#if self.low > bi.low:
#included = True
if included:
if self.pre:
if self.high > self.pre.high and self.low < self.pre.low:
included = True
if included:
self.add_bi(bi)
# gn>gn-1
if self.dir == Chan_BI_DIR.DOWN:
# UP -> max(dn)
self.low = bi.low
else:
# DOWN -> min(gn)
self.high = bi.high
#self.print(bi, "Z")
#print(self.start_bi.start_time, bi.start_time, included)
return included
+197
View File
@@ -0,0 +1,197 @@
import copy
from typing import Dict, Optional
from chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_SEG_DIR, Chan_BI_DIR, Chan_ZS_DIR
import chanlun.core.ChanCTime as ChanCTime
from chanlun.core.ChanBI import ChanBI
from chanlun.core.ChanBIZS import ChanBIZS
class ChanSEG():
def __init__(self, start_bi: ChanBI, index, ddir=Chan_SEG_DIR.UP, pre_end_bi: ChanBI = None):
self.start_bi = start_bi
self.start_time = start_bi.start_time
self.end_time = None
self.end_bi = None
self.dir = ddir
self.low = 0
self.high = 0
if self.dir == Chan_SEG_DIR.UP and start_bi:
self.low = start_bi.low
else:
if start_bi:
self.high = start_bi.high
self.index = index
self.pre = None
self.next = None
self.bi_list = []
self.bi_list.append(start_bi)
self.is_sure = False
self.sure_time = None
self.macd_hist = 0
self.macd_div = 0
self.start_bi.set_seg(self)
self.pre_end_bi = pre_end_bi
if self.pre_end_bi:
self.ini_seg()
def ini_seg(self):
next_bi = self.start_bi.next
for index in range(self.start_bi.index+1, self.pre_end_bi.index):
if next_bi:
self.bi_list.append(next_bi)
next_bi.set_seg(self)
next_bi = next_bi.next
def set_macdhist(self, macd_hist):
self.macd_hist = macd_hist
def set_macd_div(self, macd_div):
self.macd_div = macd_div
def set_end_bi(self, bi: ChanBI, sure_bi: ChanBI):
self.end_bi = bi
if bi and bi.is_sure:
if self.dir == Chan_SEG_DIR.UP:
self.high = bi.high
else:
self.low = bi.low
self.is_sure = True
self.end_time = bi.end_klc.end_time
if sure_bi.is_sure:
self.sure_time = sure_bi.sure_time
self.format_bi_list()
def pre_set_end_bi(self, bi: ChanBI):
self.end_bi = bi
if bi and bi.is_sure:
if self.dir == Chan_SEG_DIR.UP:
self.high = bi.high
else:
self.low = bi.low
self.end_time = bi.end_klc.end_time
self.format_bi_list()
def set_pre(self, seg):
self.pre = seg
def set_next(self, seg):
self.next = seg
def set_sure(self, sure_bi):
if sure_bi.is_sure:
self.sure_time = sure_bi.sure_time
self.is_sure = True
self.format_bi_list()
def format_bi_list(self):
self.bi_list = []
self.bi_list.append(self.start_bi)
if self.end_bi:
next_bi = self.start_bi.next
for i in range(self.start_bi.index, self.end_bi.index):
if next_bi:
self.bi_list.append(next_bi)
next_bi.set_seg(self)
next_bi = next_bi.next
def add_bi(self, bi: ChanBI):
if len(self.bi_list) > 0:
self.bi_list.append(bi)
bi.set_seg(self)
self.end_time = bi.end_time
self.end_bi = bi
def cal_bi_zs(self):
zs_list = []
if len(self.bi_list) > 3:
last_zs = None
if self.dir == Chan_SEG_DIR.UP:
for index in range(1, len(self.bi_list)):
bi = self.bi_list[index]
#print(bi.end_time, bi.next,"UP SEG BI ZS Index")
if bi.next == None or bi.next.next == None:
if last_zs and (bi.low > last_zs.zg or bi.high < last_zs.zd):
last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time)
continue
bi2 = bi.next
bi3 = bi.next.next
if len(zs_list) == 0 or (last_zs and last_zs.is_sure):
if bi3.is_sure and bi3.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.DOWN:
zg = min(bi.high, bi2.high, bi3.high)
zd = max(bi.low, bi2.low, bi3.low)
gg = max(bi.high, bi2.high, bi3.high)
dd = min(bi.low, bi2.low, bi3.low)
zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.UP)
zs.set_zg(zg)
zs.set_zd(zd)
zs.set_gg(gg)
zs.set_dd(dd)
zs.add_bi(bi2)
zs.add_bi(bi3)
zs_list.append(zs)
last_zs = zs
else:
if bi.index > last_zs.bi_list[-1].index and bi.dir == Chan_BI_DIR.DOWN and bi.is_sure:
if bi.low > last_zs.zg or bi.high < last_zs.zd:
#print(bi.end_time, "UP SEG BI ZS End")
last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time)
if bi3.is_sure and bi3.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.DOWN:
zg = min(bi.high, bi2.high, bi3.high)
zd = max(bi.low, bi2.low, bi3.low)
gg = max(bi.high, bi2.high, bi3.high)
dd = min(bi.low, bi2.low, bi3.low)
zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.UP)
zs.set_zg(zg)
zs.set_zd(zd)
zs.set_gg(gg)
zs.set_dd(dd)
zs.add_bi(bi2)
zs.add_bi(bi3)
zs_list.append(zs)
last_zs = zs
else:
last_zs.add_bi(bi.pre)
last_zs.add_bi(bi)
if index == len(self.bi_list) - 1 and last_zs and not last_zs.is_sure:
#print(bi.start_time, "BI", last_zs.is_sure)
last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time)
else:
for index in range(1, len(self.bi_list)):
bi = self.bi_list[index]
if bi.next == None or bi.next.next == None:
if last_zs and (bi.low > last_zs.zg or bi.high < last_zs.zd):
last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time)
continue
bi2 = bi.next
bi3 = bi.next.next
if len(zs_list) == 0 or (last_zs and last_zs.is_sure):
if bi3.is_sure and bi3.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.UP:
zg = min(bi.high, bi2.high, bi3.high)
zd = max(bi.low, bi2.low, bi3.low)
gg = max(bi.high, bi2.high, bi3.high)
dd = min(bi.low, bi2.low, bi3.low)
zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.DOWN)
zs.set_zg(zg)
zs.set_zd(zd)
zs.set_gg(gg)
zs.set_dd(dd)
zs.add_bi(bi2)
zs.add_bi(bi3)
zs_list.append(zs)
last_zs = zs
else:
if bi.index > last_zs.bi_list[-1].index and bi.dir == Chan_BI_DIR.UP and bi.is_sure:
if bi.low > last_zs.zg or bi.high < last_zs.zd:
last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time)
if bi3.is_sure and bi3.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.UP:
zg = min(bi.high, bi2.high, bi3.high)
zd = max(bi.low, bi2.low, bi3.low)
gg = max(bi.high, bi2.high, bi3.high)
dd = min(bi.low, bi2.low, bi3.low)
zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.DOWN)
zs.set_zg(zg)
zs.set_zd(zd)
zs.set_gg(gg)
zs.set_dd(dd)
zs.add_bi(bi2)
zs.add_bi(bi3)
zs_list.append(zs)
last_zs = zs
else:
last_zs.add_bi(bi.pre)
last_zs.add_bi(bi)
if index == len(self.bi_list) - 1 and last_zs and not last_zs.is_sure:
#print(bi.start_time, "BI", last_zs.is_sure)
last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time)
#print(self.start_time, len(zs_list))
#print(self.bi_list[-1].end_time, "end_bi")
return zs_list
+109
View File
@@ -0,0 +1,109 @@
from typing import Dict, Optional
import chanlun.core.ChanKLC as ChanKLC
import chanlun.core.ChanSEG as ChanSEG
import chanlun.core.ChanCTime as ChanCTime
from chanlun.core.ChanEnum import Chan_ZS_DIR
# 中枢
class ChanZS():
def __init__(self, start_seg: ChanSEG, index, ddir: Chan_ZS_DIR):
self.start_klc = start_seg.start_bi.start_klc
self.start_time = self.start_klc.start_time
self.end_time = None
self.index = index
self.next = None
self.pre = None
self.start_seg = start_seg
self.seg_list = []
self.seg_list.append(start_seg)
self.end_seg = None
self.last_bi_in = None
self.bi_out = None
self.is_sure = False
self.zg = 0
self.zd = 0
self.gg = 0
self.dd = 0
self.dir = ddir
self.sure_time = None
self.end_klc = None
self.bi_out_count = 0
self.bi_out_list = []
self.bi_out_seg_list = []
self.bi_out_seg = None
self.is_extended = False
def set_last_bi_in(self, last_bi_in):
self.last_bi_in = last_bi_in
def set_bi_out(self, bi_out, bi_out_seg):
if bi_out:
#print(bi_out.start_klc.start_time, bi_out.sure_time, bi_out.dir, bi_out_seg.dir, len(self.bi_out_list))
if len(self.bi_out_list) > 0:
last_bi = self.bi_out_list[-1]
if last_bi.index != bi_out.index:
self.bi_out_list.append(bi_out)
self.bi_out_seg_list.append(bi_out_seg)
else:
self.bi_out_list.append(bi_out)
self.bi_out_seg_list.append(bi_out_seg)
self.bi_out = bi_out
self.bi_out_seg = bi_out_seg
def set_end_klc(self, end_klc, sure_time, bi_out_count, seg):
self.end_klc = end_klc
self.set_end_time(end_klc.end_time)
self.is_sure = True
self.sure_time = sure_time
self.bi_out_count = bi_out_count
self.end_seg = seg
def set_end_seg(self, end_seg):
self.end_seg = end_seg
def set_pre(self, pre):
self.pre = pre
def set_next(self, next):
self.next = next
def set_end_time(self, end_time):
self.end_time = end_time
def add_klc(self, klc):
self.klc_list.append(klc)
def add_seg(self, seg):
self.seg_list.append(seg)
self.end_time = seg.end_time
self.end_seg = seg
def set_zg(self, zg):
self.zg = zg
def set_zd(self, zd):
self.zd = zd
def set_gg(self, gg):
self.gg = gg
def set_dd(self, dd):
self.dd = dd
def extend_zs(self, seg_list):
self.is_sure = False
self.end_seg = None
self.end_klc = None
self.sure_time = None
for seg in seg_list:
if seg.end_bi.high > self.gg:
self.set_gg(seg.end_bi.high)
if seg.end_bi.low < self.dd:
self.set_dd(seg.end_bi.low)
self.seg_list.append(seg)
self.is_extended = True
#print(self.start_time, "extend zs", seg_list[-1].end_time)
# 大级别中枢:由多个区间重叠(扩张)的笔/线段中枢合并而成,用于显示更大级别的震荡区间
class ChanZS_Big():
def __init__(self, zs_list):
assert len(zs_list) >= 1
self.zs_list = list(zs_list)
first = self.zs_list[0]
last = self.zs_list[-1]
self.start_time = first.start_time
self.end_time = last.end_time if last.end_time else None
self.start_klc = first.start_klc
self.end_klc = last.end_klc
# 大级别区间取并集:包住所有子中枢
self.zd = min(zs.zd for zs in self.zs_list)
self.zg = max(zs.zg for zs in self.zs_list)
self.dd = min(zs.dd for zs in self.zs_list)
self.gg = max(zs.gg for zs in self.zs_list)
self.index = 0 # 由外部设置
+7
View File
@@ -0,0 +1,7 @@
class Chan_FX_Box():
def __init__(self, start_time, end_time, high, low):
self.start_time = start_time
self.end_time = end_time
self.high = high
self.low = low
View File