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:
@@ -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")
|
||||
@@ -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段可能升级
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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" # 初始状态
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 # 由外部设置
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user