diff --git a/ChanEnum.py b/ChanEnum.py index 7abeb6a..22f0c2a 100644 --- a/ChanEnum.py +++ b/ChanEnum.py @@ -61,7 +61,11 @@ class Chan_FX_TYPE(Enum): BB = auto() PTOP = auto() PBOTTOM = auto() - +class Chan_PRICE_TREND(Enum): + UP = auto() + DOWN = auto() + FLAT = auto() + UNKNOWN = auto() class Chan_KLC_FX(Enum): TOP1 = auto() TOP2 = auto() diff --git a/ChanKLC.py b/ChanKLC.py index 898c8a8..2a5b8f0 100644 --- a/ChanKLC.py +++ b/ChanKLC.py @@ -1,7 +1,7 @@ import copy from typing import Dict, Optional -from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_KLC_FX, Chan_K_DIR, Chan_MACD_STATE +from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_KLC_FX, Chan_K_DIR, Chan_MACD_STATE, Chan_PRICE_TREND import ChanKLU import ChanCTime @@ -39,9 +39,6 @@ class ChanKLC(): self.lower_shadow_ratio = klu.lower_shadow_ratio self.candle_dir = klu.candle_dir self.range = klu.range - self.strength = klu.strength - self.last_top_klc = None - self.last_bottom_klc = None self.bb_out = True self.macd = 0 self.signal = 0 @@ -50,14 +47,13 @@ class ChanKLC(): self.separate_div = False self.ema52 = klu.ema52 self.ema24 = klu.ema24 - def set_last_top_klu(self, last_top_klc): - self.last_top_klc = last_top_klc - def set_last_bottom_klc(self, last_bottom_klc): - self.last_bottom_klc = last_bottom_klc + self.trend = Chan_PRICE_TREND.UNKNOWN + def set_trend(self, trend): + self.trend = trend 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_invisible() + #self.cal_invisible() self.cal_bb_out() def add_klu(self, klu): self.klus.append(klu) @@ -109,9 +105,6 @@ class ChanKLC(): if self.low <= klu.bblow30 and klu.bblow30 > 0 and self.next and (self.macd - self.next.macd) < 0: self.klc_fx_type = Chan_KLC_FX.BOTTOM4 #self.bb_out = True - def cal_macd_state(self, dir): - macd_state = 0 - return macd_state def cal_indicators(self): for index in range(1, len(self.klus)): self.volume += self.klus[index].volume @@ -129,23 +122,7 @@ class ChanKLC(): if len(self.klus) > 0: self.macd = self.klus[-1].macd self.signal = self.klus[-1].signal - def cal_shape_1(self): - for index in range(1, len(self.klus)): - self.body += self.klus[index].body - self.upper_shadow += self.klus[index].upper_shadow - self.lower_shadow += self.klus[index].lower_shadow - self.body_ratio += self.klus[index].body_ratio - self.upper_shadow_ratio += self.klus[index].upper_shadow_ratio - self.lower_shadow_ratio += self.klus[index].lower_shadow_ratio - self.range += self.klus[index].range - self.body = self.body / len(self.klus) - self.upper_shadow = self.upper_shadow / len(self.klus) - self.lower_shadow = self.lower_shadow / len(self.klus) - self.body_ratio = self.body_ratio / len(self.klus) - self.upper_shadow_ratio = self.upper_shadow_ratio / len(self.klus) - self.lower_shadow_ratio = self.lower_shadow_ratio / len(self.klus) - self.range = self.range / len(self.klus) - def cal_shape_2(self): + 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 @@ -207,17 +184,6 @@ class ChanKLC(): return False def set_fx(self, fx: Chan_FX_TYPE): self.fx = fx - def print(self): - print(self.time, self.high, self.low, self.start_time, self.end_time, self.fx, self.index) - def copy(self): - """创建KLC对象的浅拷贝, 避免循环引用""" - new_klc = ChanKLC(self.start_klu, self.index, self.dir) - new_klc.high = self.high - new_klc.low = self.low - new_klc.state = self.state - new_klc.fx = self.fx - # 不复制 next 和 pre 引用,避免循环引用 - return new_klc def set_pre_fx(self): if self.pre and self.pre.pre: self.pre.fx = self.check_fx(self.pre.pre, self.pre) @@ -231,1517 +197,4 @@ class ChanKLC(): 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) - def cal_klu_features(self): - features = dict() - feature_sums = dict() - feature_counts = dict() - - # 遍历所有klu,累计每个特征的总和和计数 - for klu in self.klus: - for key, value in klu.get_feature_data().items(): - if key not in feature_sums: - feature_sums[key] = 0 - feature_counts[key] = 0 - - feature_sums[key] += value - feature_counts[key] += 1 - - # 计算每个特征的平均值 - for key in feature_sums: - features[key] = feature_sums[key] / feature_counts[key] - - return features - def cal_fx_shape(self): - if self.klc_fx_type != Chan_KLC_FX.UNKNOWN: - if self.pre and self.next and self.next.end_klu: - klc1 = self.pre - klc2 = self - klc3 = self.next - klu_list = [] - klu_list.append(klc1.klus) - klu_list.append(klc2.klus) - klu_list.append(klc3.klus) - gap = klc3.end_klu.index - klc1.start_klu.index + 1 - if gap < 4: - pass - return gap - def get_feature_data(self): - features = dict() - # 原有基础特征 - features['klc_close'] = self.close #0 - features['klc_open'] = self.open #1 - features['klc_high'] = self.high #2 - features['klc_low'] = self.low #3 - features['klc_index'] = self.index #4 - features['klc_dir'] = 0 if self.dir == Chan_KLINE_DIR.UP else 1 #5 - features['klc_state'] = self.state #6 - features['klc_fx'] = 0 if self.fx == Chan_FX_TYPE.UNKNOWN else 1 if self.fx == Chan_FX_TYPE.TOP else 2 #7 - features['klc_klus'] = len(self.klus) #8 - features['klc_volume'] = self.volume #9 - features['klc_pre_fx'] = (0 if self.pre.fx == Chan_FX_TYPE.UNKNOWN else 1 if self.pre.fx == Chan_FX_TYPE.TOP else 2) if self.pre else 0 #10 - features['klc_pre_pre_fx'] = (0 if self.pre.pre.fx == Chan_FX_TYPE.UNKNOWN else 1 if self.pre.pre.fx == Chan_FX_TYPE.TOP else 2) if self.pre and self.pre.pre else 0 #11 - features['klc_pre_pre_pre_fx'] = (0 if self.pre.pre.pre.fx == Chan_FX_TYPE.UNKNOWN else 1 if self.pre.pre.pre.fx == Chan_FX_TYPE.TOP else 2) if self.pre and self.pre.pre and self.pre.pre.pre else 0 #12 - features['klc_distance'] = self.distance #13 - features['klc_volume_ratio'] = self.volume_ratio #14 - features['klc_rsi'] = self.rsi #15 - # New Add 20250422 - #features['klc_macdhist'] = self.get_macdhist() #11 - #features['klc_bi_macdhist'] = self.bi_macdhist #12 - #features['klc_bi_macd_div'] = self.bi_macd_div #13 - #features['klc_bi_dir'] = 1 if self.bi.dir == Chan_BI_DIR.UP else -1 #14 - # ===== 2.1 K线形态因子 ===== - - # K线实体大小 - if self.open != 0: # 避免除以零 - features['klc_body_size_rel'] = abs(self.close - self.open) / self.open # 相对实体大小 - else: - features['klc_body_size_rel'] = 0 - features['klc_body_size_abs'] = abs(self.close - self.open) # 绝对实体大小 - - # 上下影线长度 - max_oc = max(self.open, self.close) - min_oc = min(self.open, self.close) - high_low_range = self.high - self.low - - if high_low_range != 0: # 避免除以零 - features['klc_upper_shadow'] = (self.high - max_oc) / high_low_range # 上影线相对长度 - features['klc_lower_shadow'] = (min_oc - self.low) / high_low_range # 下影线相对长度 - else: - features['klc_upper_shadow'] = 0 - features['klc_lower_shadow'] = 0 - - # K线波动范围 - if self.close != 0: # 避免除以零 - features['klc_range'] = 0 #(self.high - self.low) / self.close - else: - features['klc_range'] = 0 - - # 与前K线的价格关系 - if self.pre: - # 当前K线最高价与前一根K线最高价的比较 - if self.pre.high != 0: # 避免除以零 - features['klc_high_ratio'] = self.high / self.pre.high - else: - features['klc_high_ratio'] = 1 - - # 当前K线最低价与前一根K线最低价的比较 - if self.pre.low != 0: # 避免除以零 - features['klc_low_ratio'] = self.low / self.pre.low - else: - features['klc_low_ratio'] = 1 - - # 当前K线收盘价与前一根K线收盘价的相对位置 - if self.pre.close != 0: # 避免除以零 - features['klc_close_change_1'] = (self.close - self.pre.close) / self.pre.close - else: - features['klc_close_change_1'] = 0 - - # 如果有前两根K线 - if self.pre.pre: - if self.pre.pre.close != 0: # 避免除以零 - features['klc_close_change_2'] = (self.close - self.pre.pre.close) / self.pre.pre.close - else: - features['klc_close_change_2'] = 0 - else: - features['klc_close_change_2'] = 0 - else: - # 如果没有前K线,设置默认值 - features['klc_high_ratio'] = 1 - features['klc_low_ratio'] = 1 - features['klc_close_change_1'] = 0 - features['klc_close_change_2'] = 0 - - # 分型特征编码 - # 这里直接使用现有的fx字段,不重复计算 - - # ===== 2.2 价格关系因子 ===== - - # 价格与均线的关系 (从KLU中获取) - klu_features = self.cal_klu_features() - - # MA5与收盘价的关系 - if 'klu_ma5' in klu_features and klu_features['klu_ma5'] != 0: - features['klc_close_to_ma5'] = (self.close - klu_features['klu_ma5']) / klu_features['klu_ma5'] - else: - features['klc_close_to_ma5'] = 0 - - # MA10与收盘价的关系 - if 'klu_ma10' in klu_features and klu_features['klu_ma10'] != 0: - features['klc_close_to_ma10'] = (self.close - klu_features['klu_ma10']) / klu_features['klu_ma10'] - else: - features['klc_close_to_ma10'] = 0 - - # MA30与收盘价的关系 - if 'klu_ma30' in klu_features and klu_features['klu_ma30'] != 0: - features['klc_close_to_ma30'] = (self.close - klu_features['klu_ma30']) / klu_features['klu_ma30'] - else: - features['klc_close_to_ma30'] = 0 - - # 短期均线与长期均线的差异 - if 'klu_ma5' in klu_features and 'klu_ma30' in klu_features and klu_features['klu_ma30'] != 0: - features['klc_ma_diff'] = (klu_features['klu_ma5'] - klu_features['klu_ma30']) / klu_features['klu_ma30'] - else: - features['klc_ma_diff'] = 0 - - # 价格突破特征 - # 检查当前K线是否突破前3根K线的最高/最低价 - if self.pre: - max_high = self.pre.high - min_low = self.pre.low - - temp = self.pre - count = 1 - while temp.pre and count < 3: - temp = temp.pre - max_high = max(max_high, temp.high) - min_low = min(min_low, temp.low) - count += 1 - - features['klc_break_high'] = 1 if self.high > max_high else 0 - features['klc_break_low'] = 1 if self.low < min_low else 0 - else: - features['klc_break_high'] = 0 - features['klc_break_low'] = 0 - - # ===== 2.3 技术指标因子 ===== - - # 获取技术指标 - # RSI (从KLU中获取) - if 'klu_rsi' in klu_features: - features['klc_rsi'] = klu_features['klu_rsi'] - else: - features['klc_rsi'] = 50 # 默认中性值 - - # MACD (从KLU中获取) - if 'klu_macd' in klu_features: - features['klc_macd'] = klu_features['klu_macd'] - else: - features['klc_macd'] = 0 - - if 'klu_signal' in klu_features: - features['klc_macd_signal'] = klu_features['klu_signal'] - else: - features['klc_macd_signal'] = 0 - - if 'klu_macdhist' in klu_features: - features['klc_macdhist'] = klu_features['klu_macdhist'] - else: - features['klc_macdhist'] = 0 - - # 成交量变化 - if self.pre: - vol_sum = 0 - count = 0 - temp = self.pre - - # 计算前5根K线的平均成交量 - while temp and count < 5: - vol_sum += temp.volume - count += 1 - temp = temp.pre - - avg_vol = vol_sum / count if count > 0 else self.volume - - if avg_vol != 0: # 避免除以零 - features['klc_vol_ratio'] = self.volume / avg_vol - else: - features['klc_vol_ratio'] = 1 - else: - features['klc_vol_ratio'] = 1 - - # ===== 2.4 市场环境因子 ===== - - # 价格波动率 (前5根K线收盘价的标准差) - if self.pre: - close_vals = [self.close] - temp = self.pre - count = 0 - - while temp and count < 5: - close_vals.append(temp.close) - count += 1 - temp = temp.pre - - if len(close_vals) > 1: - import numpy as np - std_dev = np.std(close_vals) - avg_close = np.mean(close_vals) - - if avg_close != 0: # 避免除以零 - features['klc_volatility'] = std_dev / avg_close - else: - features['klc_volatility'] = 0 - else: - features['klc_volatility'] = 0 - else: - features['klc_volatility'] = 0 - - # 前5根K线的价格趋势 (简单线性回归斜率) - if self.pre: - price_vals = [self.close] - temp = self.pre - count = 0 - - while temp and count < 5: - price_vals.append(temp.close) - count += 1 - temp = temp.pre - - if len(price_vals) > 2: - import numpy as np - y = np.array(price_vals) - x = np.arange(len(y)) - - # 简单线性回归 - slope = np.polyfit(x, y, 1)[0] - - # 归一化斜率 - if abs(np.mean(y)) > 0: # 避免除以零 - features['klc_trend_slope'] = slope / abs(np.mean(y)) - else: - features['klc_trend_slope'] = 0 - else: - features['klc_trend_slope'] = 0 - else: - features['klc_trend_slope'] = 0 - - # ===== 2.5 其他衍生因子 ===== - - # K线组合形态 - # 十字星 (实体非常小) - body_pct = abs(self.close - self.open) / (self.high - self.low) if (self.high - self.low) > 0 else 0 - features['klc_is_doji'] = 1 if body_pct < 0.1 else 0 # 实体小于10%算十字星 - - # 锤子线/上吊线 (下影线长,上影线短,实体小) - if high_low_range > 0: - lower_shadow_pct = (min_oc - self.low) / high_low_range - upper_shadow_pct = (self.high - max_oc) / high_low_range - features['klc_is_hammer'] = 1 if (lower_shadow_pct > 0.6 and upper_shadow_pct < 0.1) else 0 - else: - features['klc_is_hammer'] = 0 - - # 吞没形态 - if self.pre: - prev_body_size = abs(self.pre.close - self.pre.open) - curr_body_size = abs(self.close - self.open) - - # 看涨吞没 - if (self.pre.close < self.pre.open # 前一根是阴线 - and self.close > self.open # 当前是阳线 - and self.open <= self.pre.close # 当前开盘低于前收盘 - and self.close >= self.pre.open # 当前收盘高于前开盘 - and curr_body_size > prev_body_size): # 当前实体大于前实体 - features['klc_is_bullish_engulfing'] = 1 - else: - features['klc_is_bullish_engulfing'] = 0 - - # 看跌吞没 - if (self.pre.close > self.pre.open # 前一根是阳线 - and self.close < self.open # 当前是阴线 - and self.open >= self.pre.close # 当前开盘高于前收盘 - and self.close <= self.pre.open # 当前收盘低于前开盘 - and curr_body_size > prev_body_size): # 当前实体大于前实体 - features['klc_is_bearish_engulfing'] = 1 - else: - features['klc_is_bearish_engulfing'] = 0 - else: - features['klc_is_bullish_engulfing'] = 0 - features['klc_is_bearish_engulfing'] = 0 - - # 包含关系 - if self.pre: - # 向上包含 - if (self.high >= self.pre.high and self.low >= self.pre.low): - features['klc_is_up_inclusive'] = 1 - else: - features['klc_is_up_inclusive'] = 0 - - # 向下包含 - if (self.high <= self.pre.high and self.low <= self.pre.low): - features['klc_is_down_inclusive'] = 1 - else: - features['klc_is_down_inclusive'] = 0 - - # 完全包含 - if (self.high >= self.pre.high and self.low <= self.pre.low): - features['klc_is_full_inclusive'] = 1 - else: - features['klc_is_full_inclusive'] = 0 - - # 被完全包含 - if (self.high <= self.pre.high and self.low >= self.pre.low): - features['klc_is_inner_inclusive'] = 1 - else: - features['klc_is_inner_inclusive'] = 0 - else: - features['klc_is_up_inclusive'] = 0 - features['klc_is_down_inclusive'] = 0 - features['klc_is_full_inclusive'] = 0 - features['klc_is_inner_inclusive'] = 0 - - # 从KLU获取其他特征 - #features.update(self.cal_klu_features()) - - # ===== 3.1 价格形态扩展因子 ===== - - # 区间突破强度 - if self.pre and self.pre.pre: - prev_range = self.pre.high - self.pre.low - if prev_range > 0: - features['klc_breakout_strength'] = (self.close - self.pre.high) / prev_range if self.close > self.pre.high else (self.pre.low - self.close) / prev_range if self.close < self.pre.low else 0 - else: - features['klc_breakout_strength'] = 0 - else: - features['klc_breakout_strength'] = 0 - - # 价格动量 - if self.pre: - features['klc_momentum_1'] = self.close - self.pre.close - if self.pre.pre: - features['klc_momentum_2'] = self.close - self.pre.pre.close - else: - features['klc_momentum_2'] = 0 - else: - features['klc_momentum_1'] = 0 - features['klc_momentum_2'] = 0 - - # 价格加速度 - if self.pre and self.pre.pre: - prev_change = self.pre.close - self.pre.pre.close - curr_change = self.close - self.pre.close - features['klc_price_acceleration'] = curr_change - prev_change - else: - features['klc_price_acceleration'] = 0 - - # 相对位置 - if self.high != self.low: - features['klc_relative_position'] = (self.close - self.low) / (self.high - self.low) - else: - features['klc_relative_position'] = 0.5 - - # 价格区间位置 (前N根K线) - prev_klcs = [] - temp = self.pre - for _ in range(10): # 前10根K线 - if temp: - prev_klcs.append(temp) - temp = temp.pre - else: - break - - if prev_klcs: - max_high = max([klc.high for klc in prev_klcs]) if prev_klcs else self.high - min_low = min([klc.low for klc in prev_klcs]) if prev_klcs else self.low - price_range = max_high - min_low - - if price_range > 0: - features['klc_range_position'] = (self.close - min_low) / price_range - else: - features['klc_range_position'] = 0.5 - else: - features['klc_range_position'] = 0.5 - - # ===== 3.2 更多技术指标因子 ===== - - # MACD趋势 - if self.pre and 'klc_macdhist' in features: - features['klc_macdhist_change'] = features['klc_macdhist'] - self.pre.macdhist - else: - features['klc_macdhist_change'] = 0 - - # RSI趋势 - if self.pre and 'klc_rsi' in features: - features['klc_rsi_change'] = features['klc_rsi'] - self.pre.rsi - else: - features['klc_rsi_change'] = 0 - - # RSI超买超卖 - if 'klc_rsi' in features: - features['klc_rsi_overbought'] = 1 if features['klc_rsi'] > 70 else 0 - features['klc_rsi_oversold'] = 1 if features['klc_rsi'] < 30 else 0 - else: - features['klc_rsi_overbought'] = 0 - features['klc_rsi_oversold'] = 0 - - # 布林带位置 (如果可从KLU获取) - if 'klu_upper_band' in klu_features and 'klu_lower_band' in klu_features: - upper_band = klu_features['klu_upper_band'] - lower_band = klu_features['klu_lower_band'] - middle_band = klu_features['klu_middle_band'] if 'klu_middle_band' in klu_features else (upper_band + lower_band) / 2 - - band_width = upper_band - lower_band - - if band_width > 0: - features['klc_bollinger_position'] = (self.close - lower_band) / band_width - else: - features['klc_bollinger_position'] = 0.5 - - features['klc_bollinger_width'] = band_width / middle_band if middle_band > 0 else 0 - features['klc_upper_band_touch'] = 1 if self.high >= upper_band else 0 - features['klc_lower_band_touch'] = 1 if self.low <= lower_band else 0 - else: - features['klc_bollinger_position'] = 0.5 - features['klc_bollinger_width'] = 0 - features['klc_upper_band_touch'] = 0 - features['klc_lower_band_touch'] = 0 - - # 量价关系 - if self.pre: - price_change = self.close - self.pre.close - if price_change != 0: - features['klc_volume_price_ratio'] = self.volume / abs(price_change) - else: - features['klc_volume_price_ratio'] = 0 - else: - features['klc_volume_price_ratio'] = 0 - - # ===== 3.3 波动性因子 ===== - - # 真实波动幅度 (True Range) - if self.pre: - tr1 = self.high - self.low - tr2 = abs(self.high - self.pre.close) - tr3 = abs(self.low - self.pre.close) - features['klc_true_range'] = max(tr1, tr2, tr3) - else: - features['klc_true_range'] = self.high - self.low - - # 归一化真实波动幅度 - if self.pre and self.pre.close > 0: - features['klc_normalized_tr'] = features['klc_true_range'] / self.pre.close - else: - features['klc_normalized_tr'] = 0 - - # 滑动窗口波动率 - if prev_klcs: - tr_values = [] - - for i in range(len(prev_klcs)): - if i == 0: - tr = max(prev_klcs[i].high - prev_klcs[i].low, - abs(prev_klcs[i].high - self.close), - abs(prev_klcs[i].low - self.close)) - else: - tr = max(prev_klcs[i].high - prev_klcs[i].low, - abs(prev_klcs[i].high - prev_klcs[i-1].close), - abs(prev_klcs[i].low - prev_klcs[i-1].close)) - tr_values.append(tr) - - if tr_values: - import numpy as np - # ATR (Average True Range) - features['klc_atr'] = np.mean(tr_values) - if self.close > 0: - features['klc_atr_percent'] = features['klc_atr'] / self.close - else: - features['klc_atr_percent'] = 0 - - # 高低点波动 - if len(prev_klcs) >= 5: - highs = [klc.high for klc in prev_klcs[:5]] - lows = [klc.low for klc in prev_klcs[:5]] - - max_high = max(highs) - min_low = min(lows) - - features['klc_high_volatility'] = np.std(highs) / np.mean(highs) if np.mean(highs) > 0 else 0 - features['klc_low_volatility'] = np.std(lows) / np.mean(lows) if np.mean(lows) > 0 else 0 - features['klc_price_range'] = (max_high - min_low) / min_low if min_low > 0 else 0 - else: - features['klc_high_volatility'] = 0 - features['klc_low_volatility'] = 0 - features['klc_price_range'] = 0 - else: - features['klc_atr'] = 0 - features['klc_atr_percent'] = 0 - features['klc_high_volatility'] = 0 - features['klc_low_volatility'] = 0 - features['klc_price_range'] = 0 - else: - features['klc_atr'] = 0 - features['klc_atr_percent'] = 0 - features['klc_high_volatility'] = 0 - features['klc_low_volatility'] = 0 - features['klc_price_range'] = 0 - - # ===== 3.4 趋势强度因子 ===== - - # 方向移动指标 - if self.pre: - # 上升动量和下降动量 - up_move = self.high - self.pre.high - down_move = self.pre.low - self.low - - features['klc_plus_dm'] = up_move if up_move > down_move and up_move > 0 else 0 - features['klc_minus_dm'] = down_move if down_move > up_move and down_move > 0 else 0 - - # 方向指数 - if features['klc_atr'] > 0: - features['klc_plus_di'] = 100 * features['klc_plus_dm'] / features['klc_atr'] - features['klc_minus_di'] = 100 * features['klc_minus_dm'] / features['klc_atr'] - else: - features['klc_plus_di'] = 0 - features['klc_minus_di'] = 0 - - # 方向指数差 - features['klc_dx'] = 100 * abs(features['klc_plus_di'] - features['klc_minus_di']) / (features['klc_plus_di'] + features['klc_minus_di']) if (features['klc_plus_di'] + features['klc_minus_di']) > 0 else 0 - else: - features['klc_plus_dm'] = 0 - features['klc_minus_dm'] = 0 - features['klc_plus_di'] = 0 - features['klc_minus_di'] = 0 - features['klc_dx'] = 0 - - # 价格趋势强度 - if prev_klcs and len(prev_klcs) >= 5: - import numpy as np - prices = [self.close] + [klc.close for klc in prev_klcs[:5]] - x = np.arange(len(prices)) - - # 线性回归 - slope, intercept = np.polyfit(x, prices, 1) - - # 趋势线拟合度 (R^2) - y_pred = slope * x + intercept - ss_total = np.sum((prices - np.mean(prices)) ** 2) - ss_residual = np.sum((prices - y_pred) ** 2) - - if ss_total > 0: - features['klc_trend_r2'] = 1 - (ss_residual / ss_total) - else: - features['klc_trend_r2'] = 0 - - # 趋势线斜率 - features['klc_trend_slope_norm'] = slope / np.mean(prices) if np.mean(prices) > 0 else 0 - - # 价格与趋势线的距离 - current_trend_value = slope * 0 + intercept # x=0 表示当前K线在预测线上的值 - if current_trend_value > 0: - features['klc_trend_distance'] = (self.close - current_trend_value) / current_trend_value - else: - features['klc_trend_distance'] = 0 - else: - features['klc_trend_r2'] = 0 - features['klc_trend_slope_norm'] = 0 - features['klc_trend_distance'] = 0 - - # ===== 3.5 支撑与阻力因子 ===== - - # 前N根K线的支撑和阻力 - if prev_klcs and len(prev_klcs) >= 5: - highs = [klc.high for klc in prev_klcs[:5]] - lows = [klc.low for klc in prev_klcs[:5]] - - # 简单支撑位 (前5根K线最低点) - support = min(lows) - # 简单阻力位 (前5根K线最高点) - resistance = max(highs) - - # 与支撑阻力的距离 - if support > 0: - features['klc_distance_to_support'] = (self.close - support) / support - else: - features['klc_distance_to_support'] = 0 - - if resistance > 0: - features['klc_distance_to_resistance'] = (resistance - self.close) / resistance - else: - features['klc_distance_to_resistance'] = 0 - - # 支撑阻力突破 - features['klc_breaks_support'] = 1 if self.low < support else 0 - features['klc_breaks_resistance'] = 1 if self.high > resistance else 0 - - # 支撑阻力区间位置 - if resistance > support: - features['klc_sr_position'] = (self.close - support) / (resistance - support) - else: - features['klc_sr_position'] = 0.5 - else: - features['klc_distance_to_support'] = 0 - features['klc_distance_to_resistance'] = 0 - features['klc_breaks_support'] = 0 - features['klc_breaks_resistance'] = 0 - features['klc_sr_position'] = 0.5 - - # ===== 3.6 量价关系扩展因子 ===== - - # 价格与成交量的相关性 - if prev_klcs and len(prev_klcs) >= 5: - import numpy as np - prices = [self.close] + [klc.close for klc in prev_klcs[:5]] - volumes = [self.volume] + [klc.volume for klc in prev_klcs[:5]] - - # 计算相关系数 - if len(prices) > 1 and np.std(prices) > 0 and np.std(volumes) > 0: - price_mean = np.mean(prices) - volume_mean = np.mean(volumes) - - numerator = np.sum((prices - price_mean) * (volumes - volume_mean)) - denominator = np.sqrt(np.sum((prices - price_mean) ** 2) * np.sum((volumes - volume_mean) ** 2)) - - if denominator > 0: - features['klc_price_volume_corr'] = numerator / denominator - else: - features['klc_price_volume_corr'] = 0 - else: - features['klc_price_volume_corr'] = 0 - - # 价格上涨时的平均成交量 - up_prices = [] - up_volumes = [] - - # 价格下跌时的平均成交量 - down_prices = [] - down_volumes = [] - - for i in range(len(prev_klcs)): - if i < len(prev_klcs) - 1: - if prev_klcs[i].close > prev_klcs[i+1].close: - up_prices.append(prev_klcs[i].close) - up_volumes.append(prev_klcs[i].volume) - else: - down_prices.append(prev_klcs[i].close) - down_volumes.append(prev_klcs[i].volume) - - features['klc_up_volume_avg'] = np.mean(up_volumes) if up_volumes else 0 - features['klc_down_volume_avg'] = np.mean(down_volumes) if down_volumes else 0 - - if features['klc_down_volume_avg'] > 0: - features['klc_volume_ratio_up_down'] = features['klc_up_volume_avg'] / features['klc_down_volume_avg'] - else: - features['klc_volume_ratio_up_down'] = 1 - else: - features['klc_price_volume_corr'] = 0 - features['klc_up_volume_avg'] = 0 - features['klc_down_volume_avg'] = 0 - features['klc_volume_ratio_up_down'] = 1 - - # 成交量变化率 - if self.pre: - if self.pre.volume > 0: - features['klc_volume_change'] = (self.volume - self.pre.volume) / self.pre.volume - else: - features['klc_volume_change'] = 0 - else: - features['klc_volume_change'] = 0 - - # 量能扩散 - if prev_klcs and len(prev_klcs) >= 5: - avg_volume = np.mean([klc.volume for klc in prev_klcs[:5]]) - if avg_volume > 0: - features['klc_volume_expansion'] = self.volume / avg_volume - else: - features['klc_volume_expansion'] = 1 - else: - features['klc_volume_expansion'] = 1 - - # ===== 3.7 K线时序模式因子 ===== - - # 连续上涨/下跌计数 - up_count = 0 - down_count = 0 - - if prev_klcs: - temp = self - last_close = temp.close - - for klc in prev_klcs: - if klc.close < last_close: - up_count += 1 - down_count = 0 - elif klc.close > last_close: - down_count += 1 - up_count = 0 - last_close = klc.close - - features['klc_consecutive_up'] = up_count - features['klc_consecutive_down'] = down_count - else: - features['klc_consecutive_up'] = 0 - features['klc_consecutive_down'] = 0 - - # 跳空缺口 - if self.pre: - features['klc_gap_up'] = self.low - self.pre.high if self.low > self.pre.high else 0 - features['klc_gap_down'] = self.pre.low - self.high if self.high < self.pre.low else 0 - - # 归一化缺口大小 - if self.pre.close > 0: - features['klc_gap_up_pct'] = features['klc_gap_up'] / self.pre.close - features['klc_gap_down_pct'] = features['klc_gap_down'] / self.pre.close - else: - features['klc_gap_up_pct'] = 0 - features['klc_gap_down_pct'] = 0 - else: - features['klc_gap_up'] = 0 - features['klc_gap_down'] = 0 - features['klc_gap_up_pct'] = 0 - features['klc_gap_down_pct'] = 0 - - # 价格回撤 - if prev_klcs: - max_price = self.close - min_price = self.close - - for klc in prev_klcs[:5]: - max_price = max(max_price, klc.close) - min_price = min(min_price, klc.close) - - if max_price > 0: - features['klc_drawdown'] = (max_price - self.close) / max_price - else: - features['klc_drawdown'] = 0 - - if min_price > 0: - features['klc_pullback'] = (self.close - min_price) / min_price - else: - features['klc_pullback'] = 0 - else: - features['klc_drawdown'] = 0 - features['klc_pullback'] = 0 - - # ===== 3.8 复杂形态识别因子 ===== - - # 双顶/双底形态 - if self.pre and self.pre.pre and self.pre.pre.pre and self.pre.pre.pre.pre: - p5 = self.pre.pre.pre.pre - p4 = self.pre.pre.pre - p3 = self.pre.pre - p2 = self.pre - p1 = self - - # 双顶检测 (M形) - double_top = (p5.high < p4.high and p4.high > p3.high and - p3.high < p2.high and p2.high > p1.high and - abs(p4.high - p2.high) / p4.high < 0.03) # 两个顶的高度接近 - - # 双底检测 (W形) - double_bottom = (p5.low > p4.low and p4.low < p3.low and - p3.low > p2.low and p2.low < p1.low and - abs(p4.low - p2.low) / p4.low < 0.03) # 两个底的低点接近 - - features['klc_double_top'] = 1 if double_top else 0 - features['klc_double_bottom'] = 1 if double_bottom else 0 - else: - features['klc_double_top'] = 0 - features['klc_double_bottom'] = 0 - - # 头肩顶/底形态 - if self.pre and self.pre.pre and self.pre.pre.pre and self.pre.pre.pre.pre and self.pre.pre.pre.pre.pre: - p7 = self.pre.pre.pre.pre.pre - p6 = self.pre.pre.pre.pre - p5 = self.pre.pre.pre - p4 = self.pre.pre - p3 = self.pre - p2 = self - - # 头肩顶 (左肩-头-右肩) - head_shoulders_top = (p7.high < p6.high and p6.high > p5.high and - p5.high < p4.high and p4.high > p3.high and - p3.high < p2.high and - abs(p6.high - p2.high) / p6.high < 0.05 and # 左肩和右肩高度接近 - p4.high > p6.high and p4.high > p2.high) # 头部高于肩部 - - # 头肩底 (左肩-头-右肩) - head_shoulders_bottom = (p7.low > p6.low and p6.low < p5.low and - p5.low > p4.low and p4.low < p3.low and - p3.low > p2.low and - abs(p6.low - p2.low) / p6.low < 0.05 and # 左肩和右肩低点接近 - p4.low < p6.low and p4.low < p2.low) # 头部低于肩部 - - features['klc_head_shoulders_top'] = 1 if head_shoulders_top else 0 - features['klc_head_shoulders_bottom'] = 1 if head_shoulders_bottom else 0 - else: - features['klc_head_shoulders_top'] = 0 - features['klc_head_shoulders_bottom'] = 0 - - # 旗形/三角形 - if prev_klcs and len(prev_klcs) >= 5: - import numpy as np - - highs = [self.high] + [klc.high for klc in prev_klcs[:5]] - lows = [self.low] + [klc.low for klc in prev_klcs[:5]] - - # 计算高点趋势线斜率 - x = np.arange(len(highs)) - high_slope, _ = np.polyfit(x, highs, 1) - - # 计算低点趋势线斜率 - low_slope, _ = np.polyfit(x, lows, 1) - - # 旗形: 高点和低点趋势线平行且方向相同 - if abs(high_slope - low_slope) / (abs(high_slope) + 1e-10) < 0.2: - features['klc_flag_pattern'] = 1 - else: - features['klc_flag_pattern'] = 0 - - # 上升三角形: 高点趋势线水平,低点趋势线向上 - if abs(high_slope) < 0.01 and low_slope > 0.01: - features['klc_ascending_triangle'] = 1 - else: - features['klc_ascending_triangle'] = 0 - - # 下降三角形: 高点趋势线向下,低点趋势线水平 - if high_slope < -0.01 and abs(low_slope) < 0.01: - features['klc_descending_triangle'] = 1 - else: - features['klc_descending_triangle'] = 0 - - # 对称三角形: 高点趋势线向下,低点趋势线向上 - if high_slope < -0.01 and low_slope > 0.01: - features['klc_symmetric_triangle'] = 1 - else: - features['klc_symmetric_triangle'] = 0 - else: - features['klc_flag_pattern'] = 0 - features['klc_ascending_triangle'] = 0 - features['klc_descending_triangle'] = 0 - features['klc_symmetric_triangle'] = 0 - - # ===== 3.9 微观结构因子 ===== - - # 价格动量加速度 - if self.pre and self.pre.pre and self.pre.pre.pre: - mom1 = self.close - self.pre.close - mom2 = self.pre.close - self.pre.pre.close - mom3 = self.pre.pre.close - self.pre.pre.pre.close - - # 一阶动量变化 - features['klc_mom_change_1'] = mom1 - mom2 - - # 二阶动量变化 - features['klc_mom_change_2'] = (mom1 - mom2) - (mom2 - mom3) - - # 动量方向变化 - features['klc_mom_direction_change'] = 1 if (mom1 > 0 and mom2 < 0) or (mom1 < 0 and mom2 > 0) else 0 - else: - features['klc_mom_change_1'] = 0 - features['klc_mom_change_2'] = 0 - features['klc_mom_direction_change'] = 0 - - # 微观价格结构分析 - if self.pre: - # K线重叠程度 - overlap_range = min(self.high, self.pre.high) - max(self.low, self.pre.low) - total_range = max(self.high, self.pre.high) - min(self.low, self.pre.low) - - if total_range > 0: - features['klc_overlap_ratio'] = max(0, overlap_range) / total_range - else: - features['klc_overlap_ratio'] = 0 - - # 收盘价在当前K线的相对位置 - if self.high > self.low: - features['klc_close_position_inbar'] = (self.close - self.low) / (self.high - self.low) - else: - features['klc_close_position_inbar'] = 0.5 - - # 当前K线相对于前一根K线的位置 - if self.pre.high > self.pre.low: - features['klc_rel_position_to_prev'] = (self.close - self.pre.low) / (self.pre.high - self.pre.low) - else: - features['klc_rel_position_to_prev'] = 0.5 - else: - features['klc_overlap_ratio'] = 0 - features['klc_close_position_inbar'] = 0.5 - features['klc_rel_position_to_prev'] = 0.5 - - # 价格变化率序列 - if prev_klcs and len(prev_klcs) >= 3: - ret1 = self.close / prev_klcs[0].close - 1 if prev_klcs[0].close > 0 else 0 - ret2 = prev_klcs[0].close / prev_klcs[1].close - 1 if prev_klcs[1].close > 0 else 0 - ret3 = prev_klcs[1].close / prev_klcs[2].close - 1 if prev_klcs[2].close > 0 else 0 - - features['klc_return_1'] = ret1 - features['klc_return_2'] = ret2 - features['klc_return_3'] = ret3 - - # 收益率加速度 - features['klc_return_accel_1'] = ret1 - ret2 - features['klc_return_accel_2'] = (ret1 - ret2) - (ret2 - ret3) - else: - features['klc_return_1'] = 0 - features['klc_return_2'] = 0 - features['klc_return_3'] = 0 - features['klc_return_accel_1'] = 0 - features['klc_return_accel_2'] = 0 - - # ===== 3.10 综合形态因子 ===== - - # 能量比率 (K线实体与影线比例) - body_size = abs(self.close - self.open) - if self.high > self.low: - upper_shadow = self.high - max(self.open, self.close) - lower_shadow = min(self.open, self.close) - self.low - - features['klc_upper_shadow_ratio'] = upper_shadow / (self.high - self.low) - features['klc_lower_shadow_ratio'] = lower_shadow / (self.high - self.low) - features['klc_body_to_range_ratio'] = body_size / (self.high - self.low) - else: - features['klc_upper_shadow_ratio'] = 0 - features['klc_lower_shadow_ratio'] = 0 - features['klc_body_to_range_ratio'] = 1 - - # K线平衡点 - features['klc_balance_point'] = (self.high + self.low + self.close) / 3 - - # 与平衡点的距离 - if features['klc_balance_point'] > 0: - features['klc_distance_to_balance'] = (self.close - features['klc_balance_point']) / features['klc_balance_point'] - else: - features['klc_distance_to_balance'] = 0 - - # 波动性和趋势组合因子 - if 'klc_volatility' in features and 'klc_trend_slope_norm' in features: - features['klc_volatility_trend_ratio'] = features['klc_volatility'] / (abs(features['klc_trend_slope_norm']) + 1e-10) - else: - features['klc_volatility_trend_ratio'] = 0 - - # K线逆转形态 - if self.pre: - # 看涨逆转 (前一根阴线,当前阳线,且当前收盘高于前一根中点) - bullish_reversal = (self.pre.close < self.pre.open and # 前一根阴线 - self.close > self.open and # 当前阳线 - self.close > (self.pre.high + self.pre.low) / 2) # 收盘价高于前一根中点 - - # 看跌逆转 (前一根阳线,当前阴线,且当前收盘低于前一根中点) - bearish_reversal = (self.pre.close > self.pre.open and # 前一根阳线 - self.close < self.open and # 当前阴线 - self.close < (self.pre.high + self.pre.low) / 2) # 收盘价低于前一根中点 - - features['klc_bullish_reversal'] = 1 if bullish_reversal else 0 - features['klc_bearish_reversal'] = 1 if bearish_reversal else 0 - else: - features['klc_bullish_reversal'] = 0 - features['klc_bearish_reversal'] = 0 - - # 特殊K线形态 - # 大阳线/大阴线 - avg_body = 0 - if prev_klcs and len(prev_klcs) >= 5: - bodies = [abs(klc.close - klc.open) for klc in prev_klcs[:5]] - avg_body = sum(bodies) / len(bodies) if bodies else 0 - - if avg_body > 0: - features['klc_large_candle'] = body_size / avg_body - else: - features['klc_large_candle'] = 1 - - # 长上影线/长下影线 - if self.high > self.low: - upper_shadow_ratio = (self.high - max(self.open, self.close)) / (self.high - self.low) - lower_shadow_ratio = (min(self.open, self.close) - self.low) / (self.high - self.low) - - features['klc_long_upper_shadow'] = 1 if upper_shadow_ratio > 0.6 else 0 - features['klc_long_lower_shadow'] = 1 if lower_shadow_ratio > 0.6 else 0 - else: - features['klc_long_upper_shadow'] = 0 - features['klc_long_lower_shadow'] = 0 - - # 星线形态 (当前K线实体小,且与前一根K线有缺口) - if self.pre and (self.high - self.low) > 0: - small_body = body_size / (self.high - self.low) < 0.3 - gap_with_prev = (min(self.open, self.close) > self.pre.close) if self.pre.close > self.pre.open else (max(self.open, self.close) < self.pre.close) - - features['klc_star_pattern'] = 1 if small_body and gap_with_prev else 0 - else: - features['klc_star_pattern'] = 0 - - # ===== 分型强度特征 ===== - # 添加分型强度相关特征 - features['klc_fx_strength'] = self.cal_fx_strength() - features['klc_fx_strength_level'] = self.get_fx_strength_level() - features['klc_is_strong_fx'] = 1 if self.is_strong_fx() else 0 - - # 分型强度分类特征 - fx_strength = features['klc_fx_strength'] - features['klc_fx_strength_extreme'] = 1 if fx_strength >= 80 else 0 # 极强分型 - features['klc_fx_strength_strong'] = 1 if 60 <= fx_strength < 80 else 0 # 强分型 - features['klc_fx_strength_medium'] = 1 if 40 <= fx_strength < 60 else 0 # 中等分型 - features['klc_fx_strength_weak'] = 1 if 20 <= fx_strength < 40 else 0 # 弱分型 - features['klc_fx_strength_very_weak'] = 1 if fx_strength < 20 else 0 # 极弱分型 - - return features - - def cal_klc_strength(self): - strength = 0 - if not self.end_klu: - return strength - if len(self.klus) > 0: - for klu in self.klus: - strength += klu.strength - return strength - def cal_fx_strength(self, klc_offset=2): - strength = 0 - if not self.end_klu: - return 0 - if self.fx == Chan_FX_TYPE.UNKNOWN or not self.pre or not self.next: - return strength - else: - if self.pre and self.next: - klc1 = self.pre - klc2 = self - klc3 = self.next - if self.bi: - if self.bi.dir == Chan_BI_DIR.UP and self.fx == Chan_FX_TYPE.BOTTOM: - return strength - if self.bi.dir == Chan_BI_DIR.DOWN and self.fx == Chan_FX_TYPE.TOP: - return strength - if self.bi.dir == Chan_BI_DIR.UP: - if self.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc_fx_type == Chan_KLC_FX.TOP2 or self.klc_fx_type == Chan_KLC_FX.TOP3: - strength += self.check_bi_end(self.bi) - else: - if self.klc_fx_type == Chan_KLC_FX.BOTTOM1 or self.klc_fx_type == Chan_KLC_FX.BOTTOM2 or self.klc_fx_type == Chan_KLC_FX.BOTTOM3: - strength += self.check_bi_end(self.bi) - else: - return strength - return strength - def check_bi_end(self, bi): - if bi.dir == Chan_BI_DIR.UP: - return 1 - else: - return 1 - def calculate_fx_strength(self): - """ - 基于专业缠论理论的分型强度评估体系 - 返回值:0-100的强度分数,数值越大表示分型越强 - - 评分卡系统(总分29分,转换为100分制): - - 振幅比例:25%权重,最高5分 - - 量能配合:20%权重,最高5分 - - 均线位置:15%权重,最高5分 - - 形成速度:10%权重,最高4分 - - 次级别确认:30%权重,最高10分 - """ - if self.fx == Chan_FX_TYPE.UNKNOWN or not self.pre or not self.next: - return 0 - - # ===== 一、基础要素确认(先决条件) ===== - if not self._verify_basic_fx_structure(): - return 0 - - total_score = 0 - max_score = 29 # 5+5+5+4+10 - - # ===== 二、振幅比例评估 (25%权重,最高5分) ===== - amplitude_score = self._calculate_amplitude_score() - total_score += amplitude_score - - # ===== 三、量能配合评估 (20%权重,最高5分) ===== - volume_score = self._calculate_volume_score() - total_score += volume_score - - # ===== 四、均线位置评估 (15%权重,最高5分) ===== - ma_score = self._calculate_ma_position_score() - total_score += ma_score - - # ===== 五、形成速度评估 (10%权重,最高4分) ===== - speed_score = self._calculate_formation_speed_score() - total_score += speed_score - - # ===== 六、次级别确认评估 (30%权重,最高10分) ===== - confirmation_score = self._calculate_confirmation_score() - total_score += confirmation_score - - # 转换为100分制 - final_score = (total_score / max_score) * 100 - - return round(final_score, 2) - - def _verify_basic_fx_structure(self): - """ - 验证基础分型要素(先决条件) - 只验证最核心的分型定义,避免过度严格 - """ - if not self.pre or not self.next: - return False - - if self.fx == Chan_FX_TYPE.TOP: - # 顶分型核心要素:中间K线高点必须严格高于两侧 - if not (self.high > self.pre.high and self.high > self.next.high): - return False - - elif self.fx == Chan_FX_TYPE.BOTTOM: - # 底分型核心要素:中间K线低点必须严格低于两侧 - if not (self.low < self.pre.low and self.low < self.next.low): - return False - - return True - - def _calculate_amplitude_score(self): - """ - 计算振幅比例得分 (最高5分) - 强势分型:分型区间振幅>近期平均振幅的150% = 5分 - 标准分型:介于80%-150%之间 = 3分 - 弱势分型:<80% = 1分 - """ - score = 0 - - # 计算分型区间振幅 - if self.fx == Chan_FX_TYPE.TOP: - fx_amplitude = self.high - min(self.pre.low, self.next.low) - # 加分项:右侧K线低点低于左侧K线低点(经典缠论强势特征) - if self.next.low < self.pre.low: - score += 1 - else: # BOTTOM - fx_amplitude = max(self.pre.high, self.next.high) - self.low - # 加分项:右侧K线高点高于左侧K线高点(经典缠论强势特征) - if self.next.high > self.pre.high: - score += 1 - - # 计算近期平均振幅(前10根K线的ATR) - avg_amplitude = self._calculate_recent_atr(lookback=10) - - if avg_amplitude <= 0: - return max(1, score) # 确保至少有基础分 - - amplitude_ratio = fx_amplitude / avg_amplitude - - if amplitude_ratio >= 1.5: # >150% - score += 4 # 基础4分 + 可能的经典形态1分 = 最高5分 - elif amplitude_ratio >= 1.0: # 100%-150% - score += 2 + int((amplitude_ratio - 1.0) * 4) # 2-4分线性插值 - elif amplitude_ratio >= 0.8: # 80%-100% - score += 1 + int((amplitude_ratio - 0.8) * 5) # 1-2分线性插值 - else: # <80% - score += 1 - - return min(5, score) - - def _calculate_volume_score(self): - """ - 计算量能配合得分 (最高5分) - 顶分型:第二根K线放量滞涨为强烈信号 - 底分型:第三根K线放量回升为有效确认 - """ - # 计算前5根K线平均成交量 - avg_volume = self._calculate_average_volume(lookback=5) - - if avg_volume <= 0: - return 1 - - if self.fx == Chan_FX_TYPE.TOP: - # 顶分型:检查第二根K线(当前)是否放量滞涨 - volume_ratio = self.volume / avg_volume - - # 判断是否滞涨:收盘价位于K线下半部分 - price_position = (self.close - self.low) / (self.high - self.low) if self.high > self.low else 0.5 - - if volume_ratio >= 2.0 and price_position <= 0.4: # 放量+滞涨 - return 5 - elif volume_ratio >= 1.5 and price_position <= 0.5: - return 4 - elif volume_ratio >= 1.2: - return 3 - else: - return 1 - - else: # BOTTOM - # 底分型:检查第三根K线是否放量回升 - next_volume_ratio = self.next.volume / avg_volume if hasattr(self.next, 'volume') else 1 - - # 判断是否回升:第三根K线收盘价相对位置较高 - if self.next.high > self.next.low: - next_price_position = (self.next.close - self.next.low) / (self.next.high - self.next.low) - else: - next_price_position = 0.5 - - if next_volume_ratio >= 2.0 and next_price_position >= 0.6: # 放量+回升 - return 5 - elif next_volume_ratio >= 1.5 and next_price_position >= 0.5: - return 4 - elif next_volume_ratio >= 1.2: - return 3 - else: - return 1 - - def _calculate_ma_position_score(self): - """ - 计算均线位置得分 (最高5分) - 强势顶分型需在5/10均线乖离率>5%时出现 - 有效底分型常伴随MACD底背离 - """ - score = 0 - - # 获取均线数据 - klu_features = self.cal_klu_features() - - if self.fx == Chan_FX_TYPE.TOP: - # 顶分型:检查与5日和10日均线的乖离率 - ma5_bias = 0 - ma10_bias = 0 - - if 'klu_ma5' in klu_features and klu_features['klu_ma5'] > 0: - ma5_bias = (self.close - klu_features['klu_ma5']) / klu_features['klu_ma5'] - - if 'klu_ma10' in klu_features and klu_features['klu_ma10'] > 0: - ma10_bias = (self.close - klu_features['klu_ma10']) / klu_features['klu_ma10'] - - # 乖离率>5%为强势信号 - if ma5_bias > 0.05 or ma10_bias > 0.05: - score += 3 - elif ma5_bias > 0.03 or ma10_bias > 0.03: - score += 2 - elif ma5_bias > 0 or ma10_bias > 0: - score += 1 - - else: # BOTTOM - # 底分型:检查MACD背离和均线支撑 - # 简化处理:检查价格是否在均线附近或下方 - ma5_support = False - ma10_support = False - - if 'klu_ma5' in klu_features and klu_features['klu_ma5'] > 0: - ma5_bias = (self.close - klu_features['klu_ma5']) / klu_features['klu_ma5'] - if ma5_bias >= -0.05: # 在5日均线附近或上方 - ma5_support = True - - if 'klu_ma10' in klu_features and klu_features['klu_ma10'] > 0: - ma10_bias = (self.close - klu_features['klu_ma10']) / klu_features['klu_ma10'] - if ma10_bias >= -0.05: # 在10日均线附近或上方 - ma10_support = True - - if ma5_support and ma10_support: - score += 3 - elif ma5_support or ma10_support: - score += 2 - else: - score += 1 - - # 检查MACD状态 - if hasattr(self, 'macdhist'): - if self.fx == Chan_FX_TYPE.BOTTOM and self.macdhist > 0: - score += 2 # MACD金叉附近的底分型加分 - elif self.fx == Chan_FX_TYPE.TOP and self.macdhist < 0: - score += 2 # MACD死叉附近的顶分型加分 - - return min(5, score) - - def _calculate_formation_speed_score(self): - """ - 计算形成速度得分 (最高4分) - 强势特征:分型形成时间小于对应级别平均周期的1/3 - 弱势特征:形成时间超过平均周期2倍 - """ - # 简化处理:基于分型K线的收敛程度 - # 分型区间内的价格收敛速度越快,形成速度越快 - - if self.fx == Chan_FX_TYPE.TOP: - # 顶分型:检查左右两根K线相对于中间K线的收敛程度 - left_convergence = (self.high - self.pre.high) / self.high if self.high > 0 else 0 - right_convergence = (self.high - self.next.high) / self.high if self.high > 0 else 0 - else: # BOTTOM - left_convergence = (self.pre.low - self.low) / self.low if self.low > 0 else 0 - right_convergence = (self.next.low - self.low) / self.low if self.low > 0 else 0 - - avg_convergence = (left_convergence + right_convergence) / 2 - - if avg_convergence >= 0.03: # 快速形成 - return 4 - elif avg_convergence >= 0.02: - return 3 - elif avg_convergence >= 0.01: - return 2 - else: - return 1 - - def _calculate_confirmation_score(self): - """ - 计算次级别确认得分 (最高10分) - - 笔破坏检测:真实强势分型会破坏前一笔的趋势 - - 观察分型后3根K线能否站稳分型区间1/2以上 - - 结合技术指标确认 - """ - score = 0 - - # 1. 检查分型后确认(如果有next的next数据) - if hasattr(self.next, 'next'): - next2 = self.next.next - if next2: - if self.fx == Chan_FX_TYPE.TOP: - # 顶分型:检查后续2根K线是否持续走弱 - fx_mid_level = (self.high + min(self.pre.low, self.next.low)) / 2 - if self.next.close < fx_mid_level and next2.close < fx_mid_level: - score += 5 # 强确认 - elif self.next.close < fx_mid_level: - score += 3 # 中等确认 - else: # BOTTOM - # 底分型:检查后续2根K线是否持续走强 - fx_mid_level = (max(self.pre.high, self.next.high) + self.low) / 2 - if self.next.close > fx_mid_level and next2.close > fx_mid_level: - score += 5 # 强确认 - elif self.next.close > fx_mid_level: - score += 3 # 中等确认 - - # 2. 技术指标确认 - if hasattr(self, 'rsi'): - if self.fx == Chan_FX_TYPE.TOP and self.rsi > 70: - score += 2 # 超买区顶分型 - elif self.fx == Chan_FX_TYPE.BOTTOM and self.rsi < 30: - score += 2 # 超卖区底分型 - - # 3. 分型强度自身确认(K线形态) - if self.fx == Chan_FX_TYPE.TOP: - # 长上影线确认 - upper_shadow = self.high - max(self.open, self.close) - candle_range = self.high - self.low - if candle_range > 0 and upper_shadow / candle_range > 0.5: - score += 2 - else: # BOTTOM - # 长下影线确认 - lower_shadow = min(self.open, self.close) - self.low - candle_range = self.high - self.low - if candle_range > 0 and lower_shadow / candle_range > 0.5: - score += 2 - - # 4. 与前一个分型的关系 - if self.pre and hasattr(self.pre, 'fx') and self.pre.fx != Chan_FX_TYPE.UNKNOWN: - # 检查是否形成有效的笔结构 - if self.fx != self.pre.fx: # 分型类型相反 - score += 1 - - return min(10, score) - - def _calculate_recent_atr(self, lookback=10): - """ - 计算近期ATR(平均真实波动范围) - """ - tr_values = [] - temp = self - - for i in range(lookback): - if temp and temp.pre: - tr = max( - temp.high - temp.low, - abs(temp.high - temp.pre.close), - abs(temp.low - temp.pre.close) - ) - tr_values.append(tr) - temp = temp.pre - else: - break - - return sum(tr_values) / len(tr_values) if tr_values else 0 - - def _calculate_average_volume(self, lookback=5): - """ - 计算平均成交量 - """ - volumes = [] - temp = self.pre # 从前一根K线开始计算 - - for i in range(lookback): - if temp: - volumes.append(temp.volume) - temp = temp.pre - else: - break - - return sum(volumes) / len(volumes) if volumes else 0 - - def get_fx_strength_level(self): - """ - 获取分型强度等级 - 根据专业评分标准:≥80分为有效强势分型,≤40分建议忽略 - """ - strength = self.calculate_fx_strength() - return "" - if strength >= 80: - return "极强" - elif strength >= 65: - return "强" - elif strength >= 50: - return "中等" - elif strength >= 40: - return "弱" - else: - return "极弱" - - def is_strong_fx(self, threshold=65): - """ - 判断是否为强分型 - 根据专业标准调整阈值为65分 - """ - return self.calculate_fx_strength() >= threshold - - def _default_top_strength_judgment(self, first_info, middle_info, last_info, first_kline, middle_kline, last_kline): - """ - 顶分型默认强弱判断 - 当不满足特定强弱条件时的保底判断 - """ - # 严格的强分型判断条件 - strong_signals = 0 - - # 判断条件1:成交量显著放大(提高标准) - avg_volume = self._calculate_average_volume(lookback=5) - volume_significantly_amplified = middle_kline.volume > avg_volume * 2.0 if avg_volume > 0 else False - if volume_significantly_amplified: - strong_signals += 1 - - # 判断条件2:中间K线有长上影线(提高标准) - has_long_upper_shadow = middle_info['upper_shadow_ratio'] > 0.6 # 从0.3提高到0.6 - if has_long_upper_shadow: - strong_signals += 1 - - # 判断条件3:后续K线收盘明显偏低(更严格) - middle_range = middle_kline.high - middle_kline.low - last_close_position = (last_kline.close - middle_kline.low) / middle_range if middle_range > 0 else 0.5 - close_significantly_low = last_close_position < 0.3 # 从0.6提高到0.3 - if close_significantly_low: - strong_signals += 1 - - # 判断条件4:最后一根K线是明显的阴线且跌幅较大 - is_significant_bearish = (last_info['is_bearish'] and - last_info['body_size'] > last_info['total_range'] * 0.5) - if is_significant_bearish: - strong_signals += 1 - - # 判断条件5:跌破前一根K线重要价位 - breaks_important_level = last_kline.low < first_kline.low - if breaks_important_level: - strong_signals += 1 - - # 需要至少4个强信号才判断为强分型,否则为弱分型 - return 1 if strong_signals >= 4 else -1 - - def _default_bottom_strength_judgment(self, first_info, middle_info, last_info, first_kline, middle_kline, last_kline): - """ - 底分型默认强弱判断 - 当不满足特定强弱条件时的保底判断 - """ - # 严格的强分型判断条件 - strong_signals = 0 - - # 判断条件1:成交量显著放大(提高标准) - avg_volume = self._calculate_average_volume(lookback=5) - volume_significantly_amplified = middle_kline.volume > avg_volume * 2.0 if avg_volume > 0 else False - if volume_significantly_amplified: - strong_signals += 1 - - # 判断条件2:中间K线有长下影线(提高标准) - has_long_lower_shadow = middle_info['lower_shadow_ratio'] > 0.6 # 从0.3提高到0.6 - if has_long_lower_shadow: - strong_signals += 1 - - # 判断条件3:后续K线收盘明显偏高(更严格) - middle_range = middle_kline.high - middle_kline.low - last_close_position = (last_kline.close - middle_kline.low) / middle_range if middle_range > 0 else 0.5 - close_significantly_high = last_close_position > 0.7 # 从0.4降低到0.7 - if close_significantly_high: - strong_signals += 1 - - # 判断条件4:最后一根K线是明显的阳线且涨幅较大 - is_significant_bullish = (last_info['is_bullish'] and - last_info['body_size'] > last_info['total_range'] * 0.5) - if is_significant_bullish: - strong_signals += 1 - - # 判断条件5:突破前一根K线重要价位 - breaks_important_level = last_kline.high > first_kline.high - if breaks_important_level: - strong_signals += 1 - - # 需要至少4个强信号才判断为强分型,否则为弱分型 - return 1 if strong_signals >= 4 else -1 \ No newline at end of file + #print(self.start_time, self.distance, bi.index, bi.dir) \ No newline at end of file diff --git a/ChanKLU.py b/ChanKLU.py index aced1ef..75e4cd4 100644 --- a/ChanKLU.py +++ b/ChanKLU.py @@ -1,4 +1,4 @@ -from ChanEnum import Chan_FX_TYPE, Chan_KLU_TYPE, Chan_K_DIR, Chan_MACD_STATE, Chan_MACDHIST_STATE +from ChanEnum import Chan_FX_TYPE, Chan_KLU_TYPE, Chan_K_DIR, Chan_MACD_STATE, Chan_MACDHIST_STATE, Chan_PRICE_TREND class ChanKLU: def __init__(self, time, open, high, low, close, volume): # _time, _close, _open, _high, _low, _extra_info={} @@ -42,7 +42,6 @@ class ChanKLU: self.fx_strength = 0 # 分型强度:0-100 self.fx_confirmed = False # 分型是否确认 self.klu_type = None - self.cal_klu_min_max() self.range = self.high - self.low self.body = abs(self.close - self.open) self.upper_shadow = self.high - max(self.close, self.open) @@ -51,7 +50,6 @@ class ChanKLU: self.upper_shadow_ratio = self.upper_shadow / self.body self.lower_shadow_ratio = self.lower_shadow / self.body 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.strength = 0 if self.candle_dir == Chan_K_DIR.CROSS else self.cal_klu_strength() self.continue_div = 0 self.separate_div = 0 @@ -64,6 +62,7 @@ class ChanKLU: 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.zero_axis = False # 是否归零轴(穿越或接近) self.zero_axis_state = "none" # {none,crossing,near} @@ -79,49 +78,10 @@ class ChanKLU: #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 cal_klu_strength(self): - strength = 0 - if range != 0: - if self.candle_dir == Chan_K_DIR.BULL and (self.upper_shadow + self.lower_shadow) != 0: - strength += self.body / self.range - strength += self.body / (self.upper_shadow + self.lower_shadow) - elif self.candle_dir == Chan_K_DIR.BEAR and (self.upper_shadow + self.lower_shadow) != 0: - strength -=self.body / self.range - strength -= self.body / (self.upper_shadow + self.lower_shadow) - #print(self.time, self.body, self.range, self.upper_shadow, self.lower_shadow, self.candle_dir, strength) - return strength - return strength - def cal_klu_min_max(self): - """ - 计算K线类型:大阳线、大阴线、小阳线、小阴线 - """ - if self.open <= 0: # 避免除零错误 - self.kline_type = None - return - - # 计算涨跌幅 - price_change_ratio = (self.close - self.open) / self.open - strength = 0 - # 判断K线类型 - if price_change_ratio > 0.005: # 涨幅超过2% - self.kline_type = Chan_KLU_TYPE.BigBull - strength += abs(price_change_ratio) - elif price_change_ratio > 0: # 涨幅0-2% - self.kline_type = Chan_KLU_TYPE.SmallBull - strength += abs(price_change_ratio) - elif price_change_ratio < -0.005: # 跌幅超过2% - self.kline_type = Chan_KLU_TYPE.BigBear - strength += abs(price_change_ratio) - elif price_change_ratio < 0: # 跌幅0-2% - self.kline_type = Chan_KLU_TYPE.SmallBear - strength += abs(price_change_ratio) - else: # 开盘价等于收盘价 - self.kline_type = Chan_KLU_TYPE.Cross - strength += abs(price_change_ratio) - return strength + def set_trend(self, trend): + self.trend = trend def set_next(self, next): self.next = next - self.update_realtime_analysis() #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): @@ -139,471 +99,6 @@ class ChanKLU: def set_unittf(self, unittf): """设置UnitTF关联""" self.unittf = unittf - def detect_realtime_fx(self): - """ - 实时检测K线分型(不等待KLC确认) - 基于原始K线的即时分型识别 - """ - if not self.pre or not self.next: - self.fx_type = Chan_FX_TYPE.UNKNOWN - return False - - # 顶分型检测 - if (self.high > self.pre.high and - self.high > self.next.high): - self.fx_type = Chan_FX_TYPE.TOP - self.fx_confirmed = True - return True - - # 底分型检测 - elif (self.low < self.pre.low and - self.low < self.next.low): - self.fx_type = Chan_FX_TYPE.BOTTOM - self.fx_confirmed = True - return True - - self.fx_type = Chan_FX_TYPE.UNKNOWN - self.fx_confirmed = False - return False - def cal_fx(self): - """ - 根据缠论经典规则计算分型强弱 - 返回分型强度:3=极强,2=强,1=中等,0=弱,-1=极弱 - """ - if self.fx_type == Chan_FX_TYPE.UNKNOWN or not self.pre or not self.next: - return 0 - - if self.fx_type == Chan_FX_TYPE.TOP: - return self._cal_top_fx_strength() - else: # BOTTOM - return self._cal_bottom_fx_strength() - - def _check_contain_relation(self, k1, k2): - """检查两根K线是否存在包含关系""" - return (k1.high >= k2.high and k1.low <= k2.low) or (k2.high >= k1.high and k2.low <= k1.low) - - def _is_big_yang_line(self, klu): - """判断是否为大阳线""" - return klu.close > klu.open and (klu.close - klu.open) / klu.open > 0.02 - - def _is_big_yin_line(self, klu): - """判断是否为大阴线""" - return klu.close < klu.open and (klu.open - klu.close) / klu.open > 0.02 - - def _is_small_line(self, klu): - """判断是否为小K线""" - return abs(klu.close - klu.open) / klu.open < 0.01 - - def _has_long_upper_shadow(self, klu): - """判断是否有长上影线""" - body_size = abs(klu.close - klu.open) - upper_shadow = klu.high - max(klu.close, klu.open) - return upper_shadow > body_size * 1.5 - - def _cal_top_fx_strength(self): - """计算顶分型强度""" - strength = 0 - k1, k2, k3 = self.pre, self, self.next - - # (1) 检查包含关系 - 没有包含关系加分 - has_contain_12 = self._check_contain_relation(k1, k2) - has_contain_23 = self._check_contain_relation(k2, k3) - - if not has_contain_12 and not has_contain_23: - strength += 1 # 完全没有包含关系,加1分 - - # (2) 检查第1条K线是大阳线,第2、3条是小K线的情况 - if self._is_big_yang_line(k1) and self._is_small_line(k2) and self._is_small_line(k3): - strength -= 2 # 中继顶分型特征,减2分 - - # (3) 检查第2条K线有长上影线或大阴线,且第3条K线条件 - k2_mid = (k2.high + k2.low) / 2 - k3_is_yang = k3.close > k3.open - k3_close_above_mid = k3.close > k2_mid - - if (self._has_long_upper_shadow(k2) or self._is_big_yin_line(k2)) and not (k3_is_yang and k3_close_above_mid): - strength += 2 # 力度大的顶分型,加2分 - - # (4) 检查第2、3条K线包含关系,第3条为大阴线"吃掉"第2条 - if has_contain_23 and self._is_big_yin_line(k3) and k3.low < k2.low and k3.high < k2.high: - strength += 1 # 最坏包含关系,但对顶分型有利,加1分 - - # (5) 第3条K线跌破第1条K线底部且不能高于第1条K线区间一半之上 - k1_mid = (k1.high + k1.low) / 2 - if k3.low < k1.low and k3.high < k1_mid: - strength -= 1 # 较弱的顶分型,减1分 - - # 额外检查:第3条K线收盘价相对第1条K线的位置 - if k3.close < k1.low: - strength += 1 # 强烈下跌确认,加1分 - - return max(-1, min(3, strength)) # 限制在-1到3范围内 - - def _cal_bottom_fx_strength(self): - """计算底分型强度""" - strength = 0 - k1, k2, k3 = self.pre, self, self.next - - # 底分型上边沿 - fx_top = max(k1.high, k2.high) - - # (1) 第3条K线高点远高于第1条K线高点 - if k3.high > k1.high * 1.02: # 高出2%以上认为是"远高于" - strength += 2 # 较强走势,加2分 - - # (2) 第3条K线高点正好是第1根K线高点,或略微高于底分型上边沿 - elif k1.high * 0.99 <= k3.high <= fx_top * 1.01: # 在合理范围内 - strength += 0 # 一般走势,不加分也不减分 - - # (3) 第3条K线高点低于第1条K线高点 - elif k3.high < k1.high: - strength -= 1 # 较弱走势,减1分 - - # 检查包含关系 - has_contain_12 = self._check_contain_relation(k1, k2) - has_contain_23 = self._check_contain_relation(k2, k3) - - if not has_contain_12 and not has_contain_23: - strength += 1 # 完全没有包含关系,加1分 - - # 检查第3条K线是否为强阳线 - if self._is_big_yang_line(k3): - strength += 1 # 强阳线确认,加1分 - - # (4) 检查后续第1条K线(如果存在) - if hasattr(k3, 'next') and k3.next: - next_k = k3.next - if next_k.low > fx_top: - strength += 2 # 后续K线低点高于底分型上边沿,强烈确认,加2分 - elif next_k.low <= k2.low: - strength -= 1 # 后续K线跌破分型低点,减1分 - - return max(-1, min(3, strength)) # 限制在-1到3范围内 - - def calculate_realtime_fx_strength(self): - """ - 用self.pre和self.next实现分型强弱判断(与KLC中cal_fx_strength一致) - - 核心缠论原理: - - 强分型:出现在笔的末端,能够终结当前笔,标志着趋势转折 - - 弱分型:出现在笔的中间,是中继性质,笔还会继续延伸 - - 返回值: - 3: 极强分型(笔终结+强确认) - 2: 强分型(笔终结) - 1: 偏强分型(可能终结笔) - 0: 中性分型 - -1: 偏弱分型(中继特征明显) - -2: 弱分型(明显中继) - -3: 极弱分型(无效分型) - """ - # 检查是否为分型,且有前后K线数据 - if self.fx_type == Chan_FX_TYPE.UNKNOWN: - return 0 - if not self.pre or not self.next: - return 100 - # === 核心判断:分型在笔中的位置 === - - # 1. 检查这个分型是否能够终结当前笔 - is_bi_end = self._check_if_bi_ending_fx() - - # 2. 检查分型的后续走势确认 - post_fx_confirmation = self._check_post_fx_confirmation() - - # 3. 检查分型的标准性和强度 - fx_quality = self._check_fx_quality() - - # === 综合评分 === - base_score = 0 - - # 笔位置是最重要的判断标准 - if is_bi_end == 2: # 强烈确认笔终结 - base_score = 2 - elif is_bi_end == 1: # 可能笔终结 - base_score = 1 - elif is_bi_end == -1: # 明显中继 - base_score = -2 - elif is_bi_end == -2: # 强烈中继特征 - base_score = -3 - else: # 不确定 - base_score = 0 - - # 后续确认调整 - base_score += post_fx_confirmation - - # 分型质量调整 - base_score += fx_quality - - # 限制在-3到3范围内 - final_score = max(-3, min(3, base_score)) - self.fx_strength = final_score - # 转换为0-100分制以保持接口一致性 - #self.fx_strength = int((final_score + 3) * 100 / 6) # -3到3映射到0-100 - #if final_score > 1.8: - #print(self.time, final_score, is_bi_end, post_fx_confirmation, fx_quality) - #print(self.time, final_score, is_bi_end, post_fx_confirmation, fx_quality) - #self.fx_strength = self.cal_fx() - return self.fx_strength - - def _check_if_bi_ending_fx(self): - """ - 检查分型是否为笔终结分型 - 返回值: - 2: 强烈确认笔终结 - 1: 可能笔终结 - 0: 不确定 - -1: 明显中继 - -2: 强烈中继特征 - """ - # 检查是否有足够的后续数据来判断 - if not self.next or not hasattr(self.next, 'next'): - return 0 - - # 获取分型后的几根K线数据 - subsequent_klus = [] - temp = self.next - for i in range(2): # 检查后续2根K线 - if temp: - subsequent_klus.append(temp) - temp = temp.next if hasattr(temp, 'next') else None - else: - break - - if len(subsequent_klus) < 2: - return 0 - - if self.fx_type == Chan_FX_TYPE.TOP: - return self._check_top_bi_ending(subsequent_klus) - else: # BOTTOM - return self._check_bottom_bi_ending(subsequent_klus) - - def _check_top_bi_ending(self, subsequent_klus): - """检查顶分型是否为笔终结""" - # 强烈笔终结特征: - # 1. 后续K线持续下跌,且跌破关键位置 - # 2. 没有新的更高的高点出现 - - broken_key_levels = 0 - new_highs = 0 - downward_trend = 0 - - # 检查关键价位突破 - first_low = self.pre.low - middle_low = self.low - key_support = min(first_low, middle_low) - - for i, klu in enumerate(subsequent_klus): - # 检查是否跌破关键支撑 - if klu.low < key_support: - broken_key_levels += 1 - - # 检查是否出现新高 - if klu.high > self.high: - new_highs += 1 - - # 检查下跌趋势 - if i > 0 and klu.close < subsequent_klus[i-1].close: - downward_trend += 1 - - # 强烈笔终结:跌破关键位且无新高 - if broken_key_levels >= 1 and new_highs == 0 and downward_trend >= 2: - return 2 - - # 可能笔终结:部分条件满足 - if (broken_key_levels >= 1 and new_highs <= 1) or (new_highs == 0 and downward_trend >= 3): - return 1 - - # 明显中继:出现新高且未跌破关键位 - if new_highs >= 2 and broken_key_levels == 0: - return -2 - - # 中继倾向:出现新高 - if new_highs >= 1: - return -1 - - return 0 - - def _check_bottom_bi_ending(self, subsequent_klus): - """检查底分型是否为笔终结""" - # 强烈笔终结特征: - # 1. 后续K线持续上涨,且突破关键位置 - # 2. 没有新的更低的低点出现 - - broken_key_levels = 0 - new_lows = 0 - upward_trend = 0 - - # 检查关键价位突破 - first_high = self.pre.high - middle_high = self.high - key_resistance = max(first_high, middle_high) - - for i, klu in enumerate(subsequent_klus): - # 检查是否突破关键阻力 - if klu.high > key_resistance: - broken_key_levels += 1 - - # 检查是否出现新低 - if klu.low < self.low: - new_lows += 1 - - # 检查上涨趋势 - if i > 0 and klu.close > subsequent_klus[i-1].close: - upward_trend += 1 - - # 强烈笔终结:突破关键位且无新低 - if broken_key_levels >= 1 and new_lows == 0 and upward_trend >= 2: - return 2 - - # 可能笔终结:部分条件满足 - if (broken_key_levels >= 1 and new_lows <= 1) or (new_lows == 0 and upward_trend >= 3): - return 1 - - # 明显中继:出现新低且未突破关键位 - if new_lows >= 2 and broken_key_levels == 0: - return -2 - - # 中继倾向:出现新低 - if new_lows >= 1: - return -1 - - return 0 - - def _check_post_fx_confirmation(self): - """ - 检查分型后的走势确认 - 返回值:-1到1的调整分数 - """ - if not self.next: - return 0 - - score = 0 - - # 检查第三根K线的确认 - third_klu = self.next - - if self.fx_type == Chan_FX_TYPE.TOP: - # 顶分型:第三根K线应该走弱 - middle_price = (self.high + self.low) / 2 - - if third_klu.close < middle_price: - score += 0.5 - if third_klu.low < self.pre.low: # 跌破第一根K线低点 - score += 0.5 - if third_klu.close < third_klu.open and abs(third_klu.close - third_klu.open) > abs(self.close - self.open) * 0.5: - score += 0.3 # 明显阴线 - - else: # BOTTOM - # 底分型:第三根K线应该走强 - middle_price = (self.high + self.low) / 2 - - if third_klu.close > middle_price: - score += 0.5 - if third_klu.high > self.pre.high: # 突破第一根K线高点 - score += 0.5 - if third_klu.close > third_klu.open and abs(third_klu.close - third_klu.open) > abs(self.close - self.open) * 0.5: - score += 0.3 # 明显阳线 - - return min(1, max(-1, score)) - - def _check_fx_quality(self): - """ - 检查分型本身的质量 - 返回值:-1到1的调整分数 - """ - score = 0 - - # 检查分型的标准性 - if self.fx_type == Chan_FX_TYPE.TOP: - # 高点突出程度 - high_diff1 = (self.high - self.pre.high) / self.high if self.high > 0 else 0 - high_diff2 = (self.high - self.next.high) / self.high if self.high > 0 else 0 - min_diff = min(high_diff1, high_diff2) - - if min_diff > 0.03: # 非常突出 - score += 0.5 - elif min_diff > 0.01: # 比较突出 - score += 0.2 - elif min_diff < 0.003: # 不够突出 - score -= 0.5 - - else: # BOTTOM - # 低点突出程度 - low_diff1 = (self.pre.low - self.low) / self.pre.low if self.pre.low > 0 else 0 - low_diff2 = (self.next.low - self.low) / self.next.low if self.next.low > 0 else 0 - min_diff = min(low_diff1, low_diff2) - - if min_diff > 0.03: # 非常突出 - score += 0.5 - elif min_diff > 0.01: # 比较突出 - score += 0.2 - elif min_diff < 0.003: # 不够突出 - score -= 0.5 - - # 检查量价配合 - avg_volume = self._get_avg_volume(lookback=5) - if avg_volume > 0: - volume_ratio = self.volume / avg_volume - if volume_ratio > 1.5: - score += 0.3 - elif volume_ratio < 0.7: - score -= 0.2 - - return min(1, max(-1, score)) - - def _get_avg_volume(self, lookback=5): - """获取前N根K线平均成交量""" - volumes = [] - temp = self.pre - - for i in range(lookback): - if temp: - volumes.append(temp.volume) - temp = temp.pre if hasattr(temp, 'pre') else None - else: - break - - return sum(volumes) / len(volumes) if volumes else self.volume - - def get_fx_signal(self): - """ - 获取分型交易信号 - 返回: (信号类型, 强度, 建议) - """ - if not self.fx_confirmed: - return ("无信号", 0, "等待分型确认") - - strength_level = "弱" - if self.fx_strength >= 80: - strength_level = "极强" - elif self.fx_strength >= 65: - strength_level = "强" - elif self.fx_strength >= 50: - strength_level = "中等" - - if self.fx_type == Chan_FX_TYPE.TOP: - signal_type = f"{strength_level}顶分型" - if self.fx_strength >= 65: - suggestion = "考虑减仓或止盈" - else: - suggestion = "谨慎观望" - else: - signal_type = f"{strength_level}底分型" - if self.fx_strength >= 65: - suggestion = "考虑建仓或加仓" - else: - suggestion = "谨慎观望" - - return (signal_type, self.fx_strength, suggestion) - - def update_realtime_analysis(self): - """ - 更新实时分析(在每根K线完成时调用) - """ - self.detect_realtime_fx() - if self.fx_confirmed: - self.calculate_realtime_fx_strength() - def set_idx(self, idx): self.idx = idx self.index = idx @@ -639,9 +134,6 @@ class ChanKLU: self.bblow120 = float(item['bblow120']) if 'bblow120' in item and item['bblow120'] else 0 self.bbup365 = float(item['bbup365']) if 'bbup365' in item and item['bbup365'] else 0 self.bblow365 = float(item['bblow365']) if 'bblow365' in item and item['bblow365'] else 0 - # 设置指标后更新实时分析 - self.update_realtime_analysis() - #self.cal_macd_state() def cal_macd_state(self): # 按定义精简实现:优先级 CROSS0 > 位置(HIGH/HE/RETURN_ZERO) > NEAR0 > UNKNOWN # 首条或缺前一根 @@ -828,38 +320,4 @@ class ChanKLU: self.macd_state = Chan_MACD_STATE.UNKNOWN return self.macd_state return self.macd_state - - def get_feature_data(self): - features = dict() - features['klu_close'] = self.close - features['klu_open'] = self.open - features['klu_high'] = self.high - features['klu_low'] = self.low - features['klu_volume'] = self.volume - features['klu_index'] = self.index - features['klu_macd'] = self.macd - features['klu_signal'] = self.signal - features['klu_macdhist'] = self.macdhist - features['klu_ma5'] = self.ma5 - features['klu_ma10'] = self.ma10 - features['klu_ma30'] = self.ma30 - features['klu_ma50'] = self.ma50 - features['klu_ma200'] = self.ma200 - features['klu_ma250'] = self.ma250 - features['klu_rsi'] = self.rsi - features['klu_volume_ratio'] = self.volume_ratio - - # === 新增:实时分型特征 === - # 将枚举转换为数值:UNKNOWN=0, TOP=1, BOTTOM=-1 - if self.fx_type == Chan_FX_TYPE.TOP: - fx_type_value = 1 - elif self.fx_type == Chan_FX_TYPE.BOTTOM: - fx_type_value = -1 - else: - fx_type_value = 0 - - features['klu_fx_type'] = fx_type_value - features['klu_fx_strength'] = self.fx_strength - features['klu_fx_confirmed'] = 1 if self.fx_confirmed else 0 - - return features \ No newline at end of file + \ No newline at end of file diff --git a/ChanLun.py b/ChanLun.py index 073b814..6ac7686 100644 --- a/ChanLun.py +++ b/ChanLun.py @@ -1,6 +1,6 @@ from datetime import timedelta from pandas import DataFrame -from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_MACD_STATE +from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_MACD_STATE, Chan_PRICE_TREND from ChanKLU import ChanKLU from ChanKLC import ChanKLC from ChanBI import ChanBI @@ -18,6 +18,7 @@ from decimal import Decimal import xgboost as xgb import numpy as np from ChanMACD import ChanMACD +from TF_DF import TF_DF class ChanLun(): time1 = 1 @@ -38,26 +39,18 @@ class ChanLun(): def init_data(self, dataframe, ticker_indicator): for timeframe in self.timeframes: self.tf_df_dict[timeframe] = TF_DF(timeframe, dataframe, ticker_indicator) - def calculate_bsp(self, dataframe, ticker_indicator): - - return dataframe - def get_list_by_time(self, dataframe, ticker_indicator, time): - df = None - if time > 1: - df = resample_to_interval(dataframe, ticker_indicator) - else: - df = dataframe - klu_list = self.get_klu_list(df) - klc_list = self.get_klc_list(klc_list) - bi_list = self.cal_bi_list(klc_list) - return klu_list, klc_list, bi_list + def cal_bsp(self, dataframe, ticker_indicator): + # 初始化多周期数据 + self.init_data(dataframe, ticker_indicator) + + def check_fx(self, klc): if klc.pre and klc.next: - if klc.high > klc.pre.high and klc.high > klc.next.high and klc.ema24 > klc.ema52: + if klc.high > klc.pre.high and klc.high > klc.next.high: klc.set_fx(Chan_FX_TYPE.TOP) #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time,klc.fx, "TOP") return Chan_FX_TYPE.TOP - elif klc.low < klc.pre.low and klc.low < klc.next.low and klc.ema24 < klc.ema52: + elif klc.low < klc.pre.low and klc.low < klc.next.low: klc.set_fx(Chan_FX_TYPE.BOTTOM) #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time,klc.fx, "BOTTOM") return Chan_FX_TYPE.BOTTOM @@ -106,174 +99,6 @@ class ChanLun(): df['rsi'] = ta.RSI(df, timeperiod=14) df['volume_ratio'] = self.cal_volume_ratio(df) return df - def get_klc_state_list(self, dataframe): - klc_list = self.get_klc_list(dataframe) - bi_list= self.cal_bi_list(klc_list) - state_list = [] - if len(klc_list) > 0: - klc_index = 0 - for index in range(0, len(dataframe)): - if klc_index == len(klc_list): - klc_index = len(klc_list) - 1 - klc = klc_list[klc_index] - if klc.end_klu: - if klc.end_klu.idx == index: - klc_index += 1 - if klc.klc_fx_type == Chan_KLC_FX.TOP4: - state_list.append("10") - elif klc.klc_fx_type == Chan_KLC_FX.TOP5: - state_list.append("20") - #print(klc.start_time, klc.end_time, klc.klc_fx_type) - elif klc.klc_fx_type == Chan_KLC_FX.BOTTOM4: - state_list.append("-10") - #print(klc.start_time, klc.end_time, klc.klc_fx_type) - elif klc.klc_fx_type == Chan_KLC_FX.BOTTOM5: - state_list.append("-20") - else: - state_list.append("00") - else: - state_list.append("00") - else: - state_list.append("00") - else: - for index in range(0, len(dataframe)): - state_list.append("00") - return state_list - def get_klc_strength_list(self, dataframe): - klc_list = self.get_klc_list(dataframe) - bi_list = self.cal_bi_list(klc_list) - klc_strength_list = [] - klc_index = 0 - fx_list = [] - for index in range(0, len(dataframe)): - if klc_index == len(klc_list): - klc_index = len(klc_list) - 1 - klc = klc_list[klc_index] - if klc.end_klu and klc.end_klu.idx == index: - klc_index += 1 - if klc.klc_fx_type == Chan_KLC_FX.TOP1 or klc.klc_fx_type == Chan_KLC_FX.TOP2: - fx_list.append(1) - elif klc.klc_fx_type == Chan_KLC_FX.BOTTOM1 or klc.klc_fx_type == Chan_KLC_FX.BOTTOM2: - fx_list.append(-1) - else: - fx_list.append(0) - klc_strength_list.append(klc.cal_fx_strength(2)) - #if klc.klc_fx_type != Chan_KLC_FX.UNKNOWN and klc.cal_fx_strength() > 1: - #print(klc.start_time, klc.end_time, klc.cal_fx_strength(), klc.klc_fx_type, fx_list[-1], klc_strength_list[-1]) - else: - klc_strength_list.append(0) - fx_list.append(0) - return klc_strength_list, fx_list - def get_klc_bsp_list(self, dataframe): - klc_list = self.get_klc_list(dataframe) - bi_list = self.cal_bi_list(klc_list) - bsp_list = [] - klc_index = 0 - last_top = None - last_bottom = None - for index in range(0, len(dataframe)): - if klc_index == len(klc_list): - klc_index = len(klc_list) - 1 - klc = klc_list[klc_index] - if klc.end_klu and klc.end_klu.idx == index: - klc_index += 1 - if klc.klc_fx_type == Chan_KLC_FX.TOP1 or klc.klc_fx_type == Chan_KLC_FX.TOP2: - if klc.cal_fx_strength() > 1.0 and klc.cal_fx_shape() < 4: - bsp_list.append(1) - last_top = klc - last_bottom = None - - else: - bsp_list.append(0) - elif klc.klc_fx_type == Chan_KLC_FX.BOTTOM1 or klc.klc_fx_type == Chan_KLC_FX.BOTTOM2: - if klc.cal_fx_strength() > 1.0 and klc.cal_fx_shape() < 4: - bsp_list.append(-1) - last_bottom = klc - last_top = None - - else: - bsp_list.append(0) - else: - if last_top: - klc_offset = klc.index - last_top.index if klc.index - last_top.index > 2 else 2 - last_top_strength = last_top.cal_fx_strength(klc_offset) - if klc.high > last_top.high or (klc_offset > 2 and last_top_strength < 2): - bsp_list.append(-1) - last_top = None - else: - bsp_list.append(0) - elif last_bottom: - klc_offset = klc.index - last_bottom.index if klc.index - last_bottom.index > 2 else 2 - last_bottom_strength = last_bottom.cal_fx_strength(klc_offset) - if klc.low < last_bottom.low or (klc_offset > 2 and last_bottom_strength < 2): - bsp_list.append(1) - last_bottom = None - else: - bsp_list.append(0) - else: - bsp_list.append(0) - else: - bsp_list.append(0) - return bsp_list - def get_all_state(self, df_list): - state_list = [] - for df in df_list: - state_list.append(self.get_klc_state_list(df)) - return state_list - def resample_bsp_list(self, bsp_list, dataframe): - bsp_index = 0 - resampled_bsp_list = [] - if len(bsp_list) > 0: - for index in range(0, len(dataframe)): - if bsp_index == len(bsp_list): - bsp_index = len(bsp_list) - 1 - bsp = bsp_list[bsp_index] - if dataframe['date'][index].strftime('%Y-%m-%d %H:%M:%S') == bsp.klc.end_time: - if bsp.type == Chan_BSP_TYPE.T3E or bsp.type == Chan_BSP_TYPE.T3: - if bsp.dir == Chan_BSP_DIR.BUY: - resampled_bsp_list.append("-30") - #print(bsp.klc.end_time, bsp.dir, bsp.seg.dir, "BUY") - else: - if bsp.dir == Chan_BSP_DIR.SELL: - resampled_bsp_list.append("30") - #print(bsp.klc.end_time, bsp.dir, bsp.seg.dir, "SELL") - else: - resampled_bsp_list.append("00") - #print(bsp.klc.end_time, bsp.dir, bsp.seg.dir, "00") - bsp_index += 1 - else: - resampled_bsp_list.append("00") - else: - for index in range(0, len(dataframe)): - resampled_bsp_list.append("00") - return resampled_bsp_list - def cal_klu_state(self, dataframe): - klc_list = self.get_klc_list(dataframe) - bi_list = self.cal_bi_list(klc_list) - klc_index = 0 - state_list = [] - bi_dir_list = [] - for index in range(0, len(dataframe)): - klc = klc_list[klc_index] - if klc.bi and klc.bi.dir == Chan_BI_DIR.UP: - bi_dir_list.append(1) - else: - bi_dir_list.append(-1) - if klc.end_klu and klc.end_klu.idx == index: - klc.set_state("00") - if klc.klc_fx_type == Chan_KLC_FX.BOTTOM1: - klc.set_state("10") - elif klc.klc_fx_type == Chan_KLC_FX.TOP1: - klc.set_state("-10") - elif klc.klc_fx_type == Chan_KLC_FX.BOTTOM2: - klc.set_state("20") - elif klc.klc_fx_type == Chan_KLC_FX.TOP2: - klc.set_state("-20") - state_list.append(klc.state) - klc_index += 1 - else: - state_list.append("00") - return state_list, bi_dir_list def get_bi_list(self, dataframe): bi_list = self.cal_bi_list(self.get_klc_list(dataframe)) return bi_list @@ -601,7 +426,126 @@ class ChanLun(): break """ return seg_list - + def cal_trend(self, klc_list): + """ + 基于价格与EMA24/EMA52的位置关系、以及MACD/Signal/Hist的方向, + 为每个KLC打上趋势标签:'UP' / 'DOWN' / 'FLAT'。 + 仅设置 klc.trend,不影响其它字段。 + """ + if not klc_list: + return klc_list + last_trend = Chan_PRICE_TREND.UNKNOWN + for klc in klc_list: + price = getattr(klc, 'close', None) + ema24 = getattr(klc, 'ema24', None) + ema52 = getattr(klc, 'ema52', None) + macd = getattr(klc, 'macd', 0) if getattr(klc, 'macd', None) is not None else 0 + signal = getattr(klc, 'signal', 0) if getattr(klc, 'signal', None) is not None else 0 + hist = getattr(klc, 'macdhist', 0) if getattr(klc, 'macdhist', None) is not None else 0 + rsi = getattr(klc, 'rsi', None) + trend = Chan_PRICE_TREND.UNKNOWN + try: + # 有效性 + price_valid = price is not None and price != 0 + ema24_valid = ema24 is not None and ema24 != 0 + ema52_valid = ema52 is not None and ema52 != 0 + # 多因子投票 + score = 0 + # 1) 均线结构 + 价位 + if ema24_valid and ema52_valid: + score += 1 if ema24 > ema52 else -1 + if price_valid and ema24_valid: + score += 1 if price > ema24 else -1 + if price_valid and ema52_valid: + score += 1 if price > ema52 else -1 + # 2) MACD结构 + score += 1 if macd >= signal else -1 + if hist != 0: + score += 1 if hist > 0 else -1 + # 3) 动量与均线差分斜率 + pre = getattr(klc, 'pre', None) + if pre: + pre_close = getattr(pre, 'close', None) + if price_valid and pre_close is not None: + score += 1 if price >= pre_close else -1 + pre_ema24 = getattr(pre, 'ema24', None) + pre_ema52 = getattr(pre, 'ema52', None) + if ema24_valid and ema52_valid and pre_ema24 not in (None, 0) and pre_ema52 not in (None, 0): + spread_now = ema24 - ema52 + spread_pre = pre_ema24 - pre_ema52 + score += 1 if spread_now >= spread_pre else -1 + # 4) RSI 辅助 + if rsi is not None: + if rsi >= 55: + score += 1 + elif rsi <= 45: + score -= 1 + # 5) 指标未就绪回退(EMA/MACD缺失时,用动量与RSI辅助,延续趋势) + has_full_ind = ema24_valid and ema52_valid and not (macd == 0 and signal == 0 and hist == 0) + if not has_full_ind: + # 仅根据价动量/RSI做轻量判断,默认延续 last_trend,除非出现强反向 + strong_up = False + strong_down = False + pre = getattr(klc, 'pre', None) + if pre: + pre_close = getattr(pre, 'close', None) + if price_valid and pre_close is not None: + strong_up = (price >= pre_close) + strong_down = (price < pre_close) + if rsi is not None: + if rsi >= 60: + strong_up = True + elif rsi <= 40: + strong_down = True + if last_trend == Chan_PRICE_TREND.UP and not strong_down: + trend = Chan_PRICE_TREND.UP + elif last_trend == Chan_PRICE_TREND.DOWN and not strong_up: + trend = Chan_PRICE_TREND.DOWN + else: + trend = Chan_PRICE_TREND.UP if strong_up and not strong_down else (Chan_PRICE_TREND.DOWN if strong_down and not strong_up else Chan_PRICE_TREND.FLAT) + else: + # 6) 震荡过滤(仅当极近EMA52且MACD贴合时判作震荡) + near_flat = False + if price_valid and ema52_valid: + near_ema52 = abs(price - ema52) / abs(ema52) <= 0.0005 # 0.05% + near_macd = abs(macd - signal) <= (abs(price) * 0.00005 if price_valid else 0) + near_flat = near_ema52 and near_macd + # 7) 动态阈值 + 趋势记忆(更强粘滞:趋势中容忍小幅反分) + if near_flat: + trend = Chan_PRICE_TREND.FLAT + else: + if last_trend == Chan_PRICE_TREND.UP: + # 仅当出现明显反向才翻转,否则维持UP + if score <= -2: + trend = Chan_PRICE_TREND.DOWN + else: + trend = Chan_PRICE_TREND.UP + elif last_trend == Chan_PRICE_TREND.DOWN: + if score >= 2: + trend = Chan_PRICE_TREND.UP + else: + trend = Chan_PRICE_TREND.DOWN + else: + # 初始无记忆时,降低进入门槛 + if score >= 1: + trend = Chan_PRICE_TREND.UP + elif score <= -1: + trend = Chan_PRICE_TREND.DOWN + else: + trend = Chan_PRICE_TREND.FLAT + except Exception: + trend = Chan_PRICE_TREND.UNKNOWN + # 写回趋势 + if hasattr(klc, 'set_trend'): + klc.set_trend(trend) + else: + setattr(klc, 'trend', trend) + last_trend = trend + price_diff = klc.close - klc.pre.close if klc.pre else 0 + if klc.index > len(klc_list) - 10: + print(klc.start_time, klc.end_time, klc.close, klc.ema24, klc.ema52, klc.macd, klc.signal, klc.macdhist, klc.trend, price_diff) + #print(klc.start_time, klc.end_time, klc.trend, price_diff) + return klc_list def cal_bi_list(self, klc_list): bi_list = [] last_top = None @@ -657,7 +601,6 @@ class ChanLun(): bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) #klc.set_klc_fx_type(Chan_KLC_FX.TOP3) - klc.set_last_top_klu(last_top) #print(klc.start_time, klc.fx, "二类卖点Sell 1") else: # A new top found @@ -782,7 +725,6 @@ class ChanLun(): bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM3) - klc.set_last_bottom_klc(last_bottom) #print(last_bottom.start_time, last_bottom.end_time, "--------------------------------1") #print(klc.start_time, klc.fx, "二类买点Buy 1") else: @@ -1087,113 +1029,8 @@ class ChanLun(): #self.print_zs(zs_list) return zs_list - def get_bi_macdhist_list(self, bi_list, dataframe): - bi_macdhist_list = [] - for bi in bi_list: - start_index = bi.start_klc.start_klu.index - if bi.end_klc: - end_index = bi.end_klc.end_klu.index - else: - end_index = len(dataframe) - 1 - total_macd_hist = 0 - for index in range(start_index, end_index+1): - macd_hist = dataframe['macdhist'][index] - if bi.dir == Chan_BI_DIR.UP and macd_hist > 0: - total_macd_hist += macd_hist - if bi.dir == Chan_BI_DIR.DOWN and macd_hist < 0: - total_macd_hist -= macd_hist - bi_macdhist_list.append(abs(total_macd_hist)) - bi.set_macdhist(total_macd_hist) - return bi_macdhist_list, bi_list - - def get_seg_macdhist_list(self, seg_list, dataframe): - seg_macdhist_list = [] - for seg in seg_list: - start_index = seg.start_bi.start_klc.start_klu.index - if seg.end_bi: - end_index = seg.end_bi.end_klc.end_klu.index - else: - end_index = len(dataframe) - 1 - total_macd_hist = 0 - for index in range(start_index, end_index+1): - macd_hist = dataframe['macdhist'][index] - if seg.dir == Chan_SEG_DIR.UP and macd_hist > 0: - total_macd_hist += macd_hist - if seg.dir == Chan_SEG_DIR.DOWN and macd_hist < 0: - total_macd_hist -= macd_hist - seg_macdhist_list.append(abs(total_macd_hist)) - seg.set_macdhist(total_macd_hist) - return seg_macdhist_list, seg_list - - def get_bi_macd_div_list(self, bi_list, dataframe): - bi_macd_div_list = [] - bi_macdhist_list, bi_list = self.get_bi_macdhist_list(bi_list, dataframe) - for index in range(2, len(bi_list)): - if bi_macdhist_list[index-2] == 0: - bi_macd_div = 0.0 - if index > 3 and bi_macdhist_list[index-4] > 0.0: - bi_macd_div = bi_macdhist_list[index]/bi_macdhist_list[index-4] - else: - bi_macd_div = bi_macdhist_list[index]/bi_macdhist_list[index-2] - if bi_macd_div < 0.01: - if index > 3 and bi_macdhist_list[index-4] > 0.0: - bi_macd_div = bi_macdhist_list[index]/bi_macdhist_list[index-4] - bi_macd_div = self.get_decimal(bi_macd_div) - bi_macd_div_list.append(bi_macd_div) - bi_list[index].set_macd_div(bi_macd_div) - #print(bi_list[index].start_klc.start_time, self.get_decimal(bi_macdhist_list[index]), self.get_decimal(bi_macdhist_list[index - 1]), self.get_decimal(bi_macd_div)) - return bi_macd_div_list, bi_list - - def get_seg_macd_div_list(self, seg_list, dataframe): - seg_macd_div_list = [] - seg_macdhist_list, seg_list = self.get_seg_macdhist_list(seg_list, dataframe) - for index in range(2, len(seg_list)): - if seg_macdhist_list[index-2] == 0: - seg_macd_div = 0.0 - if index > 3 and seg_macdhist_list[index-4] > 0.0: - seg_macd_div = seg_macdhist_list[index]/seg_macdhist_list[index - 4] - else: - seg_macd_div = seg_macdhist_list[index]/seg_macdhist_list[index - 2] - if seg_macd_div < 0.01: - if index > 3 and seg_macdhist_list[index-4] > 0.0: - seg_macd_div = seg_macdhist_list[index]/seg_macdhist_list[index - 4] - seg_macd_div = self.get_decimal(seg_macd_div) - seg_macd_div_list.append(seg_macd_div) - seg_list[index].set_macd_div(seg_macd_div) - #print(seg_list[index].start_bi.start_klc.start_time, self.get_decimal(seg_macdhist_list[index]), self.get_decimal(seg_macdhist_list[index - 1]), self.get_decimal(seg_macd_div)) - return seg_macd_div_list, seg_list - - def get_macd_div_list(self, dataframe): - bi_list = self.get_bi_list(dataframe) - seg_list = self.get_seg_list(bi_list) - bi_macd_div_list, bi_list = self.get_bi_macd_div_list(bi_list, dataframe) - seg_macd_div_list, seg_list = self.get_seg_macd_div_list(seg_list, dataframe) - return bi_macd_div_list, bi_list, seg_macd_div_list, seg_list - def get_decimal(self, value): return Decimal("{:.2f}".format(value)) - def add_indicators(self, df): - fast = 12 - slow = 26 - period = 9 - macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period) - bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0) - bbp365 = ta.BBP(df, timeperiod=365) - bb120 = ta.BBANDS(df, timeperiod=120, nbdevup=3.0, nbdevdn=3.0, matype=0) - bbp120 = ta.BBP(df, timeperiod=120) - df['bb365'] = bb365['upperband'] - df['bbp365'] = bbp365 - df['bb120'] = bb120['upperband'] - df['bbp120'] = bbp120 - df['macd'] = macd['macd'] - df['macdsignal'] = macd['macdsignal'] - df['macdhist'] = macd['macdhist'] - df['ma5'] = ta.MA(df, timeperiod=5) - df['ma10'] = ta.MA(df, timeperiod=10) - df['ma30'] = ta.EMA(df, timeperiod=30) - df['ma250'] = ta.MA(df, timeperiod=250) - df['rsi'] = ta.RSI(df, timeperiod=14) - return df def get_klc_list(self, dataframe): klu_list = self.get_klu_list(dataframe) klc_list = [] @@ -1223,1196 +1060,12 @@ class ChanLun(): klc = ChanKLC(klu, 0, ddir) klc_list.append(klc) last_klu = klu + klc_list = self.cal_trend(klc_list) return klc_list def get_klu_list(self, dataframe): return self.get_kl_data(dataframe) - - def copy_klu_to_klc(self, klu_list): - klc_list = [] - for klu in klu_list: - if len(klc_list) > 0: - last_klc = klc_list[-1] - ddir = Chan_KLINE_DIR.DOWN - if last_klc.high < klu.high: - ddir = Chan_KLINE_DIR.UP - klc = ChanKLC(klu, index=len(klc_list), ddir=ddir) - klc.set_end_klu(klu) - klc_list.append(klc) - last_klc.set_next(klc) - klc.set_pre(last_klc) - else: - klc = ChanKLC(klu, 0) - klc_list.append(klc) - klc.set_end_klu(klu) - return klc_list - - # ================================================ - # 计算BSP列表 - # ================================================ - def get_bsp_list(self, big_df): - big_bi_list = self.get_bi_list(big_df) - big_seg_list = self.get_seg_list(big_bi_list) - big_zs_list = self.calculate_zs(big_bi_list, big_seg_list) - big_bsp_list = self.find_third_bsp(big_zs_list) - big_bi_macd_div_list, big_bi_list = self.get_bi_macd_div_list(big_bi_list, big_df) - for index in range(0, len(big_bsp_list)-1): - bsp = big_bsp_list[index] - last_zs = bsp.zs - bsp_next = big_bsp_list[index + 1] - if bsp.zs.index != bsp_next.zs.index: - end_index = bsp.seg.end_bi.index - else: - end_index = bsp_next.bi.index - # Down trend - if bsp.bi.dir == Chan_BI_DIR.UP: - last_up_bi = bsp.bi - last_down_bi = bsp.bi.pre - for bi_index in range(bsp.bi.index + 1, end_index + 1): - bi = big_bi_list[bi_index] - if bi.is_sure: - if bi.dir == Chan_BI_DIR.DOWN: - if bi.low < last_down_bi.low: - print("背驰点1,第一类买点", bi.start_klc.end_time, bi.macd_div) - else: - if bi.macd_div > 1.5: - print("快速下跌,等待背驰:", bi.start_klc.end_time, bi.macd_div) - last_down_bi = bi - else: - if last_up_bi: - if (bi.high > last_up_bi.high and bi.macd_div > 1.2) or bi.high > last_zs.zd: - print("回中枢或者快速拉升,止损点:", bi.start_klc.end_time, bi.macd_div) - last_up_bi = bi - # Up trend - else: - last_down_bi = bsp.bi - last_up_bi = bsp.bi.pre - for bi_index in range(bsp.bi.index + 1, end_index + 1): - bi = big_bi_list[bi_index] - if bi.is_sure: - if bi.dir == Chan_BI_DIR.UP: - if last_up_bi: - if bi.high < last_up_bi.high: - if bi.macd_div < 0.8 and bi.macd_div > 0.1: - print("背驰点2,第一类卖点", bi.start_klc.end_time, bi.macd_div) - else: - if bi.macd_div > 1.5: - print("快速上涨,等待背驰:", bi.start_klc.end_time, bi.macd_div) - last_up_bi = bi - else: - if last_down_bi: - if (bi.low < last_down_bi.low and bi.macd_div > 1.2) or bi.low < last_zs.zg: - print("回中枢或者快速下跌,止损点:", bi.start_klc.end_time, bi.macd_div) - last_down_bi = bi - bsp_bi = big_bi_list[-1] - last_zs = big_zs_list[-1] - # Down trend - if bsp_bi.dir == Chan_BI_DIR.UP: - last_down_bi = bsp_bi.pre - last_up_bi = bsp_bi - for bi_index in range(bsp_bi.index + 1, bsp.seg.end_bi.index + 1): - bi = big_bi_list[bi_index] - if bi.is_sure: - if bi.dir == Chan_BI_DIR.DOWN: - if last_down_bi: - if bi.low < last_down_bi.low: - if bi.macd_div < 0.8 and bi.macd_div > 0.1: - print("背驰点3,第一类买点", bi.start_klc.end_time, bi.macd_div) - else: - if bi.macd_div > 1.5: - print("快速下跌,等待背驰:", bi.start_klc.end_time, bi.macd_div) - last_down_bi = bi - else: - if last_up_bi: - if (bi.high > last_up_bi.high and bi.macd_div > 1.2) or bi.high > last_zs.zd: - print("回中枢或者快速拉升,止损点:", bi.start_klc.end_time, bi.macd_div) - last_up_bi = bi - # Up trend - else: - last_down_bi = bsp_bi.pre - last_up_bi = bsp_bi - for bi_index in range(bsp_bi.index + 1, bsp.seg.end_bi.index + 1): - bi = big_bi_list[bi_index] - if bi.is_sure: - if bi.dir == Chan_BI_DIR.UP: - if last_up_bi: - if bi.high < last_up_bi.high: - if bi.macd_div < 0.8 and bi.macd_div > 0.1: - print("背驰点4,第一类卖点", bi.start_klc.end_time, bi.macd_div) - else: - if bi.macd_div > 1.5: - print("快速上涨,等待背驰:", bi.start_klc.end_time, bi.macd_div) - last_up_bi = bi - else: - if last_down_bi: - if (bi.low < last_down_bi.low and bi.macd_div > 1.2) or bi.low < last_zs.zg: - print("回中枢或者快速下跌,止损点:", bi.start_klc.end_time, bi.macd_div) - last_down_bi = bi - return big_bsp_list - def cal_qjt(self, small_df, big_df): - big_bi_list = self.get_bi_list(big_df) - big_seg_list = self.get_seg_list(big_bi_list) - big_zs_list = self.calculate_zs(big_bi_list, big_seg_list) - - small_bi_list = self.get_bi_list(small_df) - small_seg_list = self.get_seg_list(small_bi_list) - small_zs_list = self.calculate_zs(small_bi_list, small_seg_list) - - big_bsp_list = self.find_third_bsp(big_zs_list) - small_bsp_list = self.find_third_bsp(small_zs_list) - - #self.print_bsp_list(big_bsp_list) - - self.print_bsp_list(small_bsp_list) - - def get_seg_bsp_list(self, big_df): - big_bi_list = self.get_bi_list(big_df) - big_seg_list = self.get_seg_list(big_bi_list) - big_bsp_list = [] - seg = big_seg_list[-1] - bi = big_bi_list[-1] - if seg.dir == Chan_SEG_DIR.UP: - if bi.dir == Chan_BI_DIR.UP: - if bi.high > seg.high: - if bi.macd_div < 0.8 and bi.macd_div > 0.1: - bi_bsp = ChanBSP(bi, len(big_bsp_list), Chan_BSP_TYPE.T1, Chan_BSP_DIR.BUY, bi.sure_time, 0, None, seg) - big_bsp_list.append(bi_bsp) - else: - if bi.dir == Chan_BI_DIR.DOWN: - if bi.low < seg.low: - if bi.macd_div < 0.8 and bi.macd_div > 0.1: - bi_bsp = ChanBSP(bi, len(big_bsp_list), Chan_BSP_TYPE.T1, Chan_BSP_DIR.SELL, bi.sure_time, 0, None, seg) - big_bsp_list.append(bi_bsp) - print("Last SEG: ", seg.start_bi.start_klc.start_time) - for bsp in big_bsp_list: - if bsp.bi.end_klc: - print(bsp.bi.end_klc.end_time, bsp.sure_time, bsp.dir, bsp.bi.macd_div) - return big_bsp_list - - def get_bi_bsp_list(self, big_df): - big_bi_list = self.get_bi_list(big_df) - big_seg_list = self.get_seg_list(big_bi_list) - big_bi_macd_div_list, big_bi_list = self.get_bi_macd_div_list(big_bi_list, big_df) - big_seg_macd_div_list, big_seg_list = self.get_seg_macd_div_list(big_seg_list, big_df) - bi_bsp_list = [] - for index in range(0, len(big_seg_list)): - big_seg = big_seg_list[index] - if big_seg.end_bi: - if big_seg.dir == Chan_SEG_DIR.UP: - max_high = big_seg.high - for bi_index in range(big_seg.start_bi.index, big_seg.end_bi.index+1): - bi = big_bi_list[bi_index] - if bi.dir == Chan_BI_DIR.DOWN and bi.macd_div < 0.8 and bi.macd_div > 0.1 and bi.high > max_high: - max_high = bi.high - bi_bsp = ChanBSP(bi, len(bi_bsp_list), Chan_BSP_TYPE.T1, Chan_BSP_DIR.BUY, bi.sure_time, 0, None, big_seg) - bi_bsp_list.append(bi_bsp) - else: - max_low = big_seg.low - for bi_index in range(big_seg.start_bi.index, big_seg.end_bi.index+1): - bi = big_bi_list[bi_index] - if bi.dir == Chan_BI_DIR.UP and bi.macd_div < 0.8 and bi.macd_div > 0.1 and bi.low < max_low: - max_low = bi.low - bi_bsp = ChanBSP(bi, len(bi_bsp_list), Chan_BSP_TYPE.T1, Chan_BSP_DIR.SELL, bi.sure_time, 0, None, big_seg) - bi_bsp_list.append(bi_bsp) - else: - print("Not completed segment.", len(big_bi_list) - big_seg.start_bi.index, big_seg.dir) - if big_seg.dir == Chan_SEG_DIR.UP: - max_high = big_seg.high - for bi_index in range(big_seg.start_bi.index, len(big_bi_list)): - bi = big_bi_list[bi_index] - if bi.end_klc and bi.dir == Chan_BI_DIR.DOWN and bi.macd_div < 0.8 and bi.macd_div > 0.1 and bi.high > max_high: - max_high = bi.high - bi_bsp = ChanBSP(bi, len(bi_bsp_list), Chan_BSP_TYPE.T1, Chan_BSP_DIR.SELL, bi.sure_time, 0, None, big_seg) - bi_bsp_list.append(bi_bsp) - else: - max_low = big_seg.low - for bi_index in range(big_seg.start_bi.index, len(big_bi_list)): - bi = big_bi_list[bi_index] - if bi.end_klc and bi.dir == Chan_BI_DIR.UP and bi.macd_div < 0.8 and bi.macd_div > 0.1 and bi.low < max_low: - max_low = bi.low - bi_bsp = ChanBSP(bi, len(bi_bsp_list), Chan_BSP_TYPE.T1, Chan_BSP_DIR.BUY, bi.sure_time, 0, None, big_seg) - bi_bsp_list.append(bi_bsp) - for bsp in bi_bsp_list: - if bsp.bi.end_klc: - print(bsp.bi.end_klc.end_time, bsp.sure_time, bsp.dir, bsp.seg.dir, bsp.bi.macd_div) - return bi_bsp_list - - # ================================================ - # 第三类买卖点 - def find_third_bsp(self, zs_list): - bsp_list = [] - zs_count = 0 - last_zs = None - for zs in zs_list: - if last_zs and last_zs.dir == zs.dir: - zs_count += 1 - else: - zs_count = 1 - if zs.is_sure and zs.end_klc: - for index in range(0, len(zs.bi_out_list)): - bi_out = zs.bi_out_list[index] - bi_out_seg = zs.bi_out_seg_list[index] - bsp = ChanBSP(bi_out, len(bsp_list), Chan_BSP_TYPE.T3, Chan_BSP_DIR.BUY if bi_out.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, bi_out.sure_time, zs_count, zs, bi_out_seg) - bsp_list.append(bsp) - - elif len(zs.bi_out_list) > 0: - for index in range(0, len(zs.bi_out_list)): - bi_out = zs.bi_out_list[index] - bi_out_seg = zs.bi_out_seg_list[index] - bsp = ChanBSP(bi_out, len(bsp_list), Chan_BSP_TYPE.T3, Chan_BSP_DIR.BUY if bi_out.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, bi_out.sure_time, zs_count, zs, bi_out_seg) - bsp_list.append(bsp) - last_zs = zs - return bsp_list - - def find_first_bsp(self, bi_list, seg_list, zs_list, dataframe): - bsp_list = [] - zs_count = 0 - for index in range(1, len(zs_list)): - zs = zs_list[index] - pre_zs = zs_list[index - 1] - if zs.is_sure: - if pre_zs.dir == zs.dir: - zs_count += 1 - continue - else: - zs_count = 1 - else: - current_bi = bi_list[-1] - current_seg = seg_list[-1] - if zs.dir == pre_zs.dir and ((current_bi.dir == Chan_BI_DIR.UP and current_seg.dir == Chan_SEG_DIR.UP) or (current_bi.dir == Chan_BI_DIR.DOWN and current_seg.dir == Chan_SEG_DIR.DOWN)): - if zs.bi_out and zs.bi_out.is_sure and bi_list[-1].is_sure: - pre_start_index = pre_zs.end_seg.start_klc.end_klu.index - pre_end_index = zs.start_klc.end_klu.index - start_index = zs.bi_out_seg.start_bi.start_klc.start_klu.index - end_index = current_bi.end_klc.end_klu.index - pre_macd_area = self.cal_macd_area(dataframe, pre_start_index, pre_end_index, pre_zs.dir) - macd_area = self.cal_macd_area(dataframe, start_index, end_index, zs.dir) - print(zs.bi_out.start_klc.start_time, pre_macd_area, macd_area, zs_count) - if pre_macd_area > macd_area: - bsp = ChanBSP(current_bi, len(bsp_list), Chan_BSP_TYPE.T1, Chan_BSP_DIR.BUY if current_bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, current_bi.sure_time, zs.zs_count, zs, current_seg) - bsp_list.append(bsp) - return bsp_list - - def cal_macd_area(self, dataframe, start_idx, end_idx, zs_dir): - """ - 计算指定区间内的MACD面积 - - :param dataframe: K线数据 - :param start_idx: 开始索引 - :param end_idx: 结束索引 - :param seg_dir: 线段方向(Chan_SEG_DIR.UP或Chan_SEG_DIR.DOWN) - :return: MACD面积的绝对值 - """ - # 计算MACD指标 - exp1 = dataframe['close'].ewm(span=12, adjust=False).mean() - exp2 = dataframe['close'].ewm(span=26, adjust=False).mean() - macd = exp1 - exp2 - signal = macd.ewm(span=9, adjust=False).mean() - histogram = macd - signal - - # 根据线段方向选择计算正面积还是负面积 - if zs_dir == Chan_ZS_DIR.UP: - # 上升线段计算正面积 - area = histogram[start_idx:end_idx+1][histogram[start_idx:end_idx+1] > 0].sum() - else: - # 下降线段计算负面积 - area = histogram[start_idx:end_idx+1][histogram[start_idx:end_idx+1] < 0].sum() - - return abs(area) - # -------------------------------------------------------------------- - def plot_dual(self, small_df, big_df): - """ - 绘制双周期K线图表,包括两个周期的笔、线段、中枢和买卖点 - - :param small_df: 小周期K线数据 - :param big_df: 大周期K线数据 - """ - plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'Microsoft YaHei', 'WenQuanYi Micro Hei'] - plt.rcParams['axes.unicode_minus'] = False - - # 创建图表和子图 - fig = plt.figure(figsize=(15, 12)) - - # 大周期图表(上方60%) - ax1 = plt.subplot2grid((10, 1), (0, 0), rowspan=4) - # 小周期图表(中间40%) - ax2 = plt.subplot2grid((10, 1), (4, 0), rowspan=4, sharex=ax1) - # MACD图表(下方20%) - ax3 = plt.subplot2grid((10, 1), (8, 0), rowspan=2, sharex=ax1) - - # 计算两个周期的缠论结构 - big_klc = self.get_klc_list(big_df) - big_bi = self.cal_bi_list(big_klc) - big_seg = self.get_seg_list(big_bi) - big_zs = self.calculate_zs(big_bi, big_seg) - big_buy_sell_points = self.check_top_bottom(big_df, big_bi, big_seg, big_zs) - big_bi_macd_div, big_bi = self.get_bi_macd_div_list(big_bi, big_df) - big_seg_macd_div, big_seg = self.get_seg_macd_div_list(big_seg, big_df) - - small_klc = self.get_klc_list(small_df) - small_bi = self.cal_bi_list(small_klc) - small_seg = self.get_seg_list(small_bi) - small_zs = self.calculate_zs(small_bi, small_seg) - small_buy_sell_points = self.check_top_bottom(small_df, small_bi, small_seg, small_zs) - small_bi_macd_div, small_bi = self.get_bi_macd_div_list(small_bi, small_df) - small_seg_macd_div, small_seg = self.get_seg_macd_div_list(small_seg, small_df) - - # 绘制大周期K线 - big_dates = pd.to_datetime(big_df['date']).dt.tz_localize(None) - big_dates_num = [date2num(date) for date in big_dates] - - # 绘制大周期K线 - for i in range(len(big_df)): - color = 'red' if big_df['close'][i] > big_df['open'][i] else 'green' - ax1.bar(big_dates_num[i], - big_df['close'][i] - big_df['open'][i], - bottom=big_df['open'][i], - color=color, - width=0.0005) - ax1.plot([big_dates_num[i], big_dates_num[i]], - [big_df['low'][i], big_df['high'][i]], - color=color, - linewidth=1.2) - - # 绘制大周期笔 - for bi in big_bi: - if bi.end_klc: - start_time = pd.to_datetime(bi.start_klc.end_time) - end_time = pd.to_datetime(bi.end_klc.end_time) - color = 'blue' if bi.dir == Chan_BI_DIR.UP else 'purple' - start_price = bi.start_klc.low if bi.dir == Chan_BI_DIR.UP else bi.start_klc.high - end_price = bi.end_klc.high if bi.dir == Chan_BI_DIR.UP else bi.end_klc.low - ax1.plot([date2num(start_time), date2num(end_time)], - [start_price, end_price], - color=color, - linewidth=1.5) - else: - start_time = pd.to_datetime(bi.start_klc.end_time) - end_time = pd.to_datetime(big_klc[-1].start_time) - color = 'blue' if bi.dir == Chan_BI_DIR.UP else 'purple' - start_price = bi.start_klc.low if bi.dir == Chan_BI_DIR.UP else bi.start_klc.high - end_price = big_klc[-1].high if bi.dir == Chan_BI_DIR.UP else big_klc[-1].low - ax1.plot([date2num(start_time), date2num(end_time)], - [start_price, end_price], - color=color, - linewidth=0.5) - # 绘制大周期线段 - for seg in big_seg: - if seg.end_bi: - start_time = pd.to_datetime(seg.start_bi.start_klc.end_time) - end_time = pd.to_datetime(seg.end_bi.end_klc.end_time) - color = 'red' if seg.dir == Chan_SEG_DIR.UP else 'green' - start_price = seg.start_bi.start_klc.low if seg.dir == Chan_SEG_DIR.UP else seg.start_bi.start_klc.high - end_price = seg.end_bi.end_klc.high if seg.dir == Chan_SEG_DIR.UP else seg.end_bi.end_klc.low - ax1.plot([date2num(start_time), date2num(end_time)], - [start_price, end_price], - color=color, - linewidth=2.5) - else: - start_time = pd.to_datetime(seg.start_bi.start_klc.end_time) - end_time = pd.to_datetime(big_klc[-1].start_time) - color = 'red' if seg.dir == Chan_SEG_DIR.UP else 'green' - start_price = seg.start_bi.start_klc.low if seg.dir == Chan_SEG_DIR.UP else seg.start_bi.start_klc.high - end_price = big_klc[-1].high if seg.dir == Chan_SEG_DIR.UP else big_klc[-1].low - ax1.plot([date2num(start_time), date2num(end_time)], - [start_price, end_price], - color=color, - linewidth=1) - # 绘制大周期中枢 - for idx, zs in enumerate(big_zs): - start_time = pd.to_datetime(zs.start_klc.end_time).tz_localize(None) - color = ['orange', 'cyan', 'magenta', 'yellow', 'lime'][idx % 5] - - if zs.end_klc: - end_time = pd.to_datetime(zs.end_klc.end_time).tz_localize(None) - width = date2num(end_time) - date2num(start_time) - rect = patches.Rectangle( - (date2num(start_time), zs.zd), - width, - zs.zg - zs.zd, - linewidth=1, - edgecolor=color, - facecolor=color, - alpha=0.2 - ) - ax1.add_patch(rect) - label_text = f"大中枢{idx+1}" - else: - end_time = pd.to_datetime(big_dates.iloc[-1]).tz_localize(None) - width = date2num(end_time) - date2num(start_time) - rect = patches.Rectangle( - (date2num(start_time), zs.zd), - width, - zs.zg - zs.zd, - linewidth=1.5, - edgecolor=color, - facecolor=color, - alpha=0.1, - linestyle='--' - ) - ax1.add_patch(rect) - label_text = f"大中枢{idx+1}(未完成)" - - ax1.text( - date2num(start_time) + width/2, - zs.zd + (zs.zg - zs.zd)/2, - label_text, - ha='center', - va='center', - fontsize=9, - color='black', - bbox=dict(boxstyle="round,pad=0.2", fc=color, alpha=0.6) - ) - for index in range(0, len(big_bi_macd_div)): - bi_macd_div = big_bi_macd_div[index] - bi = big_bi[index + 2] - if bi.end_klc and False: - text_index = bi.end_klc.end_klu.index - if bi.dir == Chan_BI_DIR.UP: - ax1.text(big_dates_num[text_index], bi.end_klc.high+1, bi_macd_div, color='red', fontsize=10, alpha=0.6) - else: - ax1.text(big_dates_num[text_index], bi.end_klc.low-1, bi_macd_div, color='green', fontsize=10, alpha=0.6) - for index in range(0, len(big_seg_macd_div)): - seg_macd_div = big_seg_macd_div[index] - seg = big_seg[index + 2] - if seg.end_bi and False: - text_index = seg.end_bi.end_klc.end_klu.index - if seg.dir == Chan_SEG_DIR.UP: - ax1.text(big_dates_num[text_index], seg.end_bi.end_klc.high+1, seg_macd_div, color='red', fontsize=14, alpha=0.6) - else: - ax1.text(big_dates_num[text_index], seg.end_bi.end_klc.low-1, seg_macd_div, color='green', fontsize=14, alpha=0.6) - big_klc = self.get_klc_list(big_df) - self.cal_bi_list(big_klc) - for klc in big_klc: - if klc.klc_fx_type != Chan_KLC_FX.UNKNOWN: - text_index = klc.end_klu.index - if klc.fx == Chan_FX_TYPE.BOTTOM: - ax1.text(big_dates_num[text_index], klc.low, str(klc.klc_fx_type).replace("Chan_KLC_FX.", ""), color='green', fontsize=6, alpha=1) - else: - ax1.text(big_dates_num[text_index], klc.high, str(klc.klc_fx_type).replace("Chan_KLC_FX.", ""), color='red', fontsize=6, alpha=1) - """ - model = xgb.Booster() - model.load_model("30m_modelchan_xgb_model.json") - for klc in big_klc: - predict = self.predict(klc, model) - if predict > 0.35 and klc.fx == Chan_FX_TYPE.BOTTOM: - text_index = klc.end_klu.index - ax1.text(big_dates_num[text_index], klc.high+1, predict, color='red', fontsize=14, alpha=0.6) - if predict > 0.35 and klc.fx == Chan_FX_TYPE.TOP: - text_index = klc.end_klu.index - ax1.text(big_dates_num[text_index], klc.low-1, predict, color='green', fontsize=14, alpha=0.6) - """ - - """ - # 绘制大周期买卖点 - marker_styles = { - '第一类买点': {'marker': '^', 'color': 'red', 'size': 10}, - '第一类卖点': {'marker': 'v', 'color': 'green', 'size': 10}, - '2类买点': {'marker': '^', 'color': 'orange', 'size': 10}, - '2类卖点': {'marker': 'v', 'color': 'cyan', 'size': 10}, - '3类买点': {'marker': '^', 'color': 'purple', 'size': 10}, - '3类卖点': {'marker': 'v', 'color': 'magenta', 'size': 10} - } - - for idx, point in big_buy_sell_points.items(): - if idx < 0 or idx >= len(big_df): - continue - style = marker_styles.get(point['type'], {'marker': 'o', 'color': 'black', 'size': 8}) - ax1.plot(big_dates_num[idx], point['price'], style['marker'], - color=style['color'], - markersize=style['size']) - ax1.annotate(point['type'], - (big_dates_num[idx], point['price']), - textcoords="offset points", - xytext=(0, 10), - ha='center', - fontsize=8, - bbox=dict(boxstyle="round,pad=0.2", fc=style['color'], alpha=0.5)) - """ - # 绘制小周期K线 - small_dates = pd.to_datetime(small_df['date']).dt.tz_localize(None) - small_dates_num = [date2num(date) for date in small_dates] - - for i in range(len(small_df)): - color = 'red' if small_df['close'][i] > small_df['open'][i] else 'green' - ax2.bar(small_dates_num[i], - small_df['close'][i] - small_df['open'][i], - bottom=small_df['open'][i], - color=color, - width=0.0002) - ax2.plot([small_dates_num[i], small_dates_num[i]], - [small_df['low'][i], small_df['high'][i]], - color=color, - linewidth=0.8) - - # 绘制小周期笔 - for bi in small_bi: - if bi.end_klc: - start_time = pd.to_datetime(bi.start_klc.end_time) - end_time = pd.to_datetime(bi.end_klc.end_time) - color = 'blue' if bi.dir == Chan_BI_DIR.UP else 'purple' - start_price = bi.start_klc.low if bi.dir == Chan_BI_DIR.UP else bi.start_klc.high - end_price = bi.end_klc.high if bi.dir == Chan_BI_DIR.UP else bi.end_klc.low - ax2.plot([date2num(start_time), date2num(end_time)], - [start_price, end_price], - color=color, - linewidth=1.2) - else: - start_time = pd.to_datetime(bi.start_klc.end_time) - end_time = pd.to_datetime(small_klc[-1].start_time) - color = 'blue' if bi.dir == Chan_BI_DIR.UP else 'purple' - start_price = bi.start_klc.low if bi.dir == Chan_BI_DIR.UP else bi.start_klc.high - end_price = small_klc[-1].high if bi.dir == Chan_BI_DIR.UP else small_klc[-1].low - ax2.plot([date2num(start_time), date2num(end_time)], - [start_price, end_price], - color=color, - linewidth=0.6) - - - # 绘制小周期线段 - for seg in small_seg: - if seg.end_bi: - start_time = pd.to_datetime(seg.start_bi.start_klc.end_time) - end_time = pd.to_datetime(seg.end_bi.end_klc.end_time) - color = 'red' if seg.dir == Chan_SEG_DIR.UP else 'green' - start_price = seg.start_bi.start_klc.low if seg.dir == Chan_SEG_DIR.UP else seg.start_bi.start_klc.high - end_price = seg.end_bi.end_klc.high if seg.dir == Chan_SEG_DIR.UP else seg.end_bi.end_klc.low - ax2.plot([date2num(start_time), date2num(end_time)], - [start_price, end_price], - color=color, - linewidth=1.8) - else: - start_time = pd.to_datetime(seg.start_bi.start_klc.end_time) - end_time = pd.to_datetime(small_klc[-1].start_time) - color = 'red' if seg.dir == Chan_SEG_DIR.UP else 'green' - start_price = seg.start_bi.start_klc.low if seg.dir == Chan_SEG_DIR.UP else seg.start_bi.start_klc.high - end_price = small_klc[-1].high if seg.dir == Chan_SEG_DIR.UP else small_klc[-1].low - ax2.plot([date2num(start_time), date2num(end_time)], - [start_price, end_price], - color=color, - linewidth=0.9) - - # 绘制小周期中枢 - for idx, zs in enumerate(small_zs): - start_time = pd.to_datetime(zs.start_klc.end_time).tz_localize(None) - color = ['orange', 'cyan', 'magenta', 'yellow', 'lime'][idx % 5] - - if zs.end_klc: - end_time = pd.to_datetime(zs.end_klc.end_time).tz_localize(None) - width = date2num(end_time) - date2num(start_time) - rect = patches.Rectangle( - (date2num(start_time), zs.zd), - width, - zs.zg - zs.zd, - linewidth=0.8, - edgecolor=color, - facecolor=color, - alpha=0.2 - ) - ax2.add_patch(rect) - label_text = f"小中枢{idx+1}" - else: - end_time = pd.to_datetime(small_dates.iloc[-1]).tz_localize(None) - width = date2num(end_time) - date2num(start_time) - rect = patches.Rectangle( - (date2num(start_time), zs.zd), - width, - zs.zg - zs.zd, - linewidth=1, - edgecolor=color, - facecolor=color, - alpha=0.1, - linestyle='--' - ) - ax2.add_patch(rect) - label_text = f"小中枢{idx+1}(未完成)" - - ax2.text( - date2num(start_time) + width/2, - zs.zd + (zs.zg - zs.zd)/2, - label_text, - ha='center', - va='center', - fontsize=8, - color='black', - bbox=dict(boxstyle="round,pad=0.2", fc=color, alpha=0.6) - ) - for index in range(0, len(small_bi_macd_div)): - bi_macd_div = small_bi_macd_div[index] - bi = small_bi[index + 2] - if bi.end_klc and False: - text_index = bi.end_klc.end_klu.index - if bi.dir == Chan_BI_DIR.UP: - ax2.text(small_dates_num[text_index], bi.end_klc.high+1, bi_macd_div, color='red', fontsize=10, alpha=0.6) - else: - ax2.text(small_dates_num[text_index], bi.end_klc.low-1, bi_macd_div, color='green', fontsize=10, alpha=0.6) - for index in range(0, len(small_seg_macd_div)): - seg_macd_div = small_seg_macd_div[index] - seg = small_seg[index + 2] - if seg.end_bi and False: - text_index = seg.end_bi.end_klc.end_klu.index - if seg.dir == Chan_SEG_DIR.UP: - ax2.text(small_dates_num[text_index], seg.end_bi.end_klc.high+1, seg_macd_div, color='red', fontsize=14, alpha=0.8) - else: - ax2.text(small_dates_num[text_index], seg.end_bi.end_klc.low-1, seg_macd_div, color='green', fontsize=14, alpha=0.8) - small_klc = self.get_klc_list(small_df) - self.cal_bi_list(small_klc) - for klc in small_klc: - if klc.klc_fx_type != Chan_KLC_FX.UNKNOWN: - text_index = klc.end_klu.index - if klc.fx == Chan_FX_TYPE.BOTTOM: - ax2.text(small_dates_num[text_index], klc.low, str(klc.klc_fx_type).replace("Chan_KLC_FX.", ""), color='green', fontsize=6, alpha=1) - else: - ax2.text(small_dates_num[text_index], klc.high, str(klc.klc_fx_type).replace("Chan_KLC_FX.", ""), color='red', fontsize=6, alpha=1) - """ - model = xgb.Booster() - model.load_model("5m_modelchan_xgb_model.json") - for klc in small_klc: - predict = self.predict(klc, model) - if predict > 0.35 and klc.fx == Chan_FX_TYPE.BOTTOM: - text_index = klc.end_klu.index - ax2.text(small_dates_num[text_index], klc.high+1, predict, color='red', fontsize=14, alpha=0.6) - if predict > 0.35 and klc.fx == Chan_FX_TYPE.TOP: - text_index = klc.end_klu.index - ax2.text(small_dates_num[text_index], klc.low-1, predict, color='green', fontsize=14, alpha=0.6) - """ - - """ - # 绘制小周期买卖点 - for idx, point in small_buy_sell_points.items(): - if idx < 0 or idx >= len(small_df): - continue - style = marker_styles.get(point['type'], {'marker': 'o', 'color': 'black', 'size': 6}) - ax2.plot(small_dates_num[idx], point['price'], style['marker'], - color=style['color'], - markersize=style['size']) - ax2.annotate(point['type'], - (small_dates_num[idx], point['price']), - textcoords="offset points", - xytext=(0, 8), - ha='center', - fontsize=7, - bbox=dict(boxstyle="round,pad=0.2", fc=style['color'], alpha=0.5)) - """ - # 绘制MACD(使用小周期数据) - exp1 = small_df['close'].ewm(span=12, adjust=False).mean() - exp2 = small_df['close'].ewm(span=26, adjust=False).mean() - macd = exp1 - exp2 - signal = macd.ewm(span=9, adjust=False).mean() - histogram = macd - signal - - ax3.bar(small_dates_num, histogram, width=0.0002, color=['red' if h > 0 else 'green' for h in histogram]) - ax3.plot(small_dates_num, macd, color='blue', linewidth=0.8, label='MACD') - ax3.plot(small_dates_num, signal, color='orange', linewidth=0.8, label='Signal') - ax3.axhline(y=0, color='black', linestyle='-', linewidth=0.5) - ax3.legend(loc='upper left') - - # 设置图表标题和标签 - ax1.set_title('大周期图表', fontsize=12) - ax2.set_title('小周期图表', fontsize=12) - ax3.set_title('MACD指标(小周期)', fontsize=10) - - ax1.grid(True, linestyle='--', alpha=0.3) - ax2.grid(True, linestyle='--', alpha=0.3) - ax3.grid(True, linestyle='--', alpha=0.3) - - ax1.xaxis.set_major_formatter(DateFormatter('%Y-%m-%d')) - plt.xticks(rotation=45) - plt.tight_layout() - plt.show() - - - def predict(self, klc, model): - """ - 使用训练好的模型预测单个KLC - :param klc: 需要预测的ChanKLC对象 - :return: 预测结果(概率值) - """ - # 提取特征 - features = klc.get_feature_data() - feature_vec = [] - # 与get_feature_data保持一致,只使用相同的特征集 - for key, value in features.items(): - if isinstance(value, (int, float)): - feature_vec.append(value) - else: - feature_vec.append(0) - - # 转换为模型输入格式 - dtest = xgb.DMatrix(np.array([feature_vec])) - - # 预测 - return self.get_decimal(model.predict(dtest)[0]) - def get_decimal(self, value): return Decimal("{:.2f}".format(value)) - - def plot(self, dataframe, bi_list, seg_list, zs_list=None, buy_sell_points=None, divergence_points=None): - """ - 绘制缠论分析图表,包括K线、笔、线段、中枢、买卖点和MACD背驰 - - :param dataframe: K线数据 - :param bi_list: 笔的列表 - :param seg_list: 线段的列表 - :param zs_list: 中枢的列表 - :param buy_sell_points: 买卖点字典 - :param divergence_points: 背驰点字典 - """ - plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'Microsoft YaHei', 'WenQuanYi Micro Hei'] - plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题 - bar_line_width = 0.003 - show_sure_time = False - # 创建具有两个子图的图表 - fig = plt.figure(figsize=(15, 10)) - - # 主图占据上方70%空间 - ax1 = plt.subplot2grid((5, 1), (0, 0), rowspan=3) - # MACD子图占据下方30%空间 - ax2 = plt.subplot2grid((5, 1), (3, 0), rowspan=2, sharex=ax1) - - # 转换日期格式 - 确保都是无时区的 - dates = pd.to_datetime(dataframe['date']).dt.tz_localize(None) - dates_num = [date2num(date) for date in dates] - - # 绘制K线图 - for i in range(len(dataframe)): - # 红涨绿跌 - if dataframe['close'][i] > dataframe['open'][i]: - body_color = 'red' - else: - body_color = 'green' - - # 绘制实体 - ax1.bar(dates_num[i], - dataframe['close'][i] - dataframe['open'][i], - bottom=dataframe['open'][i], - color=body_color, - width=bar_line_width/len(dataframe)) - - # 绘制上下影线 - ax1.plot([dates_num[i], dates_num[i]], - [dataframe['low'][i], dataframe['high'][i]], - color=body_color, - linewidth=1.2) - - # 绘制笔 - for bi in bi_list: - if bi.end_klc: # 确保笔已完成 - start_time = pd.to_datetime(bi.start_klc.start_time) - end_time = pd.to_datetime(bi.end_klc.end_time) - - # 上升笔蓝色,下降笔紫色 - color = 'blue' if bi.dir == Chan_BI_DIR.UP else 'purple' - start_price = bi.start_klc.low if bi.dir == Chan_BI_DIR.UP else bi.start_klc.high - end_price = bi.end_klc.high if bi.dir == Chan_BI_DIR.UP else bi.end_klc.low - - # 绘制笔 - ax1.plot([date2num(start_time), date2num(end_time)], - [start_price, end_price], - color=color, - linewidth=1.5) - - # 绘制线段 - for seg in seg_list: - if seg.end_bi: # 确保线段已完成 - start_time = pd.to_datetime(seg.start_bi.start_klc.start_time) - end_time = pd.to_datetime(seg.end_bi.end_klc.end_time) - - # 上升线段红色,下降线段绿色 - color = 'red' if seg.dir == Chan_SEG_DIR.UP else 'green' - start_price = seg.start_bi.start_klc.low if seg.dir == Chan_SEG_DIR.UP else seg.start_bi.start_klc.high - end_price = seg.end_bi.end_klc.high if seg.dir == Chan_SEG_DIR.UP else seg.end_bi.end_klc.low - - # 绘制线段(粗线) - ax1.plot([date2num(start_time), date2num(end_time)], - [start_price, end_price], - color=color, - linewidth=2.5) - - # 在线段确认点绘制标记 - if hasattr(seg, 'sure_time') and seg.sure_time and show_sure_time: - try: - # 确保sure_time无时区 - sure_time = pd.to_datetime(seg.sure_time).tz_localize(None) - - # 找到最接近的K线 - closest_idx = (dates - sure_time).abs().argmin() - - # 获取确认点的价格 - confirm_price = dataframe['close'][closest_idx] - - # 绘制标记和标签 - ax1.plot(date2num(sure_time), confirm_price, 'D', - color='black', markersize=6) - ax1.annotate(sure_time.strftime('%m-%d %H:%M'), - (date2num(sure_time), confirm_price), - textcoords="offset points", - xytext=(0, 10), - ha='center', - fontsize=8, - bbox=dict(boxstyle="round,pad=0.3", fc="yellow", alpha=0.7)) - except Exception as e: - print(f"处理线段确认时间时出错: {e}") - continue - - # 绘制中枢区域 - if zs_list: - # 定义中枢的颜色和透明度 - zs_colors = ['orange', 'cyan', 'magenta', 'yellow', 'lime'] - - for idx, zs in enumerate(zs_list): - # 无论中枢是否完成都绘制 - start_time = pd.to_datetime(zs.start_klc.start_time).tz_localize(None) - - # 选择颜色,循环使用预定义的颜色 - color = zs_colors[idx % len(zs_colors)] - - if zs.end_klc: # 已完成的中枢 - # 转换结束时间格式 - end_time = pd.to_datetime(zs.end_klc.end_time).tz_localize(None) - - # 矩形的宽度和高度 - width = date2num(end_time) - date2num(start_time) - height = zs.zg - zs.zd - - # 创建实线矩形补丁表示已完成中枢 - rect = patches.Rectangle( - (date2num(start_time), zs.zd), # 左下角坐标 - width, # 宽度 - height, # 高度 - linewidth=1, - edgecolor=color, - facecolor=color, - alpha=0.2 # 透明度 - ) - ax1.add_patch(rect) - - # 添加中枢编号标签 - label_text = f"中枢{idx+1}" - else: # 未完成的中枢 - # 使用最后一根K线的时间作为临时结束时间 - end_time = pd.to_datetime(dates.iloc[-1]).tz_localize(None) - - # 矩形的宽度和高度 - width = date2num(end_time) - date2num(start_time) - height = zs.zg - zs.zd - - # 创建虚线矩形补丁表示未完成中枢 - rect = patches.Rectangle( - (date2num(start_time), zs.zd), # 左下角坐标 - width, # 宽度 - height, # 高度 - linewidth=1.5, - edgecolor=color, - facecolor=color, - alpha=0.1, # 较低的透明度 - linestyle='--' # 虚线边框 - ) - ax1.add_patch(rect) - - # 添加中枢编号标签,标明未完成 - label_text = f"中枢{idx+1}(未完成)" - - # 添加中枢标签 - ax1.text( - date2num(start_time) + width/2, # x位置(中枢中间) - zs.zd + height/2, # y位置(中枢中间) - label_text, - ha='center', - va='center', - fontsize=9, - color='black', - bbox=dict(boxstyle="round,pad=0.2", fc=color, alpha=0.6) - ) - - # 绘制买卖点 - if buy_sell_points: - marker_styles = { - '1类买点': {'marker': '^', 'color': 'red', 'size': 10, 'label': '1类买点'}, - '1类卖点': {'marker': 'v', 'color': 'green', 'size': 10, 'label': '1类卖点'}, - '2类买点': {'marker': '^', 'color': 'orange', 'size': 10, 'label': '2类买点'}, - '2类卖点': {'marker': 'v', 'color': 'cyan', 'size': 10, 'label': '2类卖点'}, - '3类买点': {'marker': '^', 'color': 'purple', 'size': 10, 'label': '3类买点'}, - '3类卖点': {'marker': 'v', 'color': 'magenta', 'size': 10, 'label': '3类卖点'} - } - - for idx, point in buy_sell_points.items(): - if idx < 0 or idx >= len(dataframe): - continue - print("Plot buy sell point: ", point['type']) - style = marker_styles.get(point['type'], {'marker': 'o', 'color': 'black', 'size': 8, 'label': '其他'}) - - # 绘制买卖点标记 - ax1.plot(dates_num[idx], point['price'], style['marker'], - color=style['color'], - markersize=style['size'], - label=style['label']) - - # 添加买卖点标签 - ax1.annotate(point['type'], - (dates_num[idx], point['price']), - textcoords="offset points", - xytext=(0, 10), - ha='center', - fontsize=8, - bbox=dict(boxstyle="round,pad=0.2", fc=style['color'], alpha=0.5)) - - # 绘制背驰点 - if divergence_points: - for idx, point in divergence_points.items(): - if idx < 0 or idx >= len(dataframe) or True: - continue - print("Plot divergence point") - color = 'red' if point['type'] == '底背驰' else 'green' - marker = '*' - - # 绘制背驰点标记 - ax1.plot(dates_num[idx], point['price'], marker, - color=color, - markersize=12, - label=point['type']) - - # 添加背驰点标签 - ax1.annotate(point['type'], - (dates_num[idx], point['price']), - textcoords="offset points", - xytext=(0, -15), - ha='center', - fontsize=8, - bbox=dict(boxstyle="round,pad=0.2", fc=color, alpha=0.5)) - - # 计算MACD指标 - exp1 = dataframe['close'].ewm(span=12, adjust=False).mean() - exp2 = dataframe['close'].ewm(span=26, adjust=False).mean() - macd = exp1 - exp2 - signal = macd.ewm(span=9, adjust=False).mean() - histogram = macd - signal - - # 绘制MACD - ax2.bar(dates_num, histogram, width=bar_line_width, color=['red' if h > 0 else 'green' for h in histogram]) - ax2.plot(dates_num, macd, color='blue', linewidth=1.2, label='MACD') - ax2.plot(dates_num, signal, color='orange', linewidth=1.2, label='Signal') - ax2.axhline(y=0, color='black', linestyle='-', linewidth=0.5) - ax2.legend(loc='upper left') - - # 在MACD图上标记背驰点 - if divergence_points: - for idx, point in divergence_points.items(): - if idx < 0 or idx >= len(dataframe): - continue - - color = 'red' if point['type'] == '底背驰' else 'green' - - # 在MACD图上标记背驰点 - ax2.plot(dates_num[idx], histogram[idx], '*', - color=color, - markersize=12) - - # 添加简单网格 - ax1.grid(True, linestyle='--', alpha=0.3) - ax2.grid(True, linestyle='--', alpha=0.3) - - # 设置坐标轴格式 - ax1.xaxis.set_major_formatter(DateFormatter('%Y-%m-%d')) - - # 添加简单图例 - from matplotlib.lines import Line2D - legend_elements = [ - Line2D([0], [0], color='blue', lw=2, label='上升笔'), - Line2D([0], [0], color='purple', lw=2, label='下降笔'), - Line2D([0], [0], color='red', lw=2.5, label='上升线段'), - Line2D([0], [0], color='green', lw=2.5, label='下降线段'), - patches.Patch(facecolor='orange', alpha=0.2, label='已完成中枢'), - patches.Patch(facecolor='orange', alpha=0.1, edgecolor='orange', linestyle='--', label='未完成中枢'), - Line2D([0], [0], marker='^', color='red', label='买点', markersize=10, linestyle='None'), - Line2D([0], [0], marker='v', color='green', label='卖点', markersize=10, linestyle='None'), - Line2D([0], [0], marker='*', color='red', label='底背驰', markersize=12, linestyle='None'), - Line2D([0], [0], marker='*', color='green', label='顶背驰', markersize=12, linestyle='None') - ] - ax1.legend(handles=legend_elements, loc='upper left') - - # 设置标题和标签 - ax1.set_title('缠论分析图', fontsize=14) - ax1.set_ylabel('价格', fontsize=12) - ax2.set_xlabel('时间', fontsize=12) - ax2.set_ylabel('MACD', fontsize=12) - plt.xticks(rotation=45) - plt.tight_layout() - - # 显示图表 - plt.show() - - """ - for index in range(seg.next.next.start_bi.index, seg.next.next.end_bi.index): - bi = bi_list[index] - if (bi.high >= last_zs.zd and bi.high <= last_zs.zg) or (bi.low >= last_zs.zd and bi.low <= last_zs.zg) or (bi.high >= last_zs.zg and bi.low <= last_zs.zd): - in_again = True - last_zs.set_bi_out(None) - last_zs.set_last_bi_in(None) - last_zs.set_end_seg(None) - first_bi_out = None - #print("Bi in again 1", bi.start_klc.start_time) - if in_again and (bi.low > last_zs.zg or bi.high < last_zs.zd): - last_zs.set_bi_out(bi) - last_zs.set_last_bi_in(bi_list[index - 1]) - last_zs.set_end_seg(seg.next.next) - bi_out_count += 1 - first_bi_out = bi - print("First bi out 1", first_bi_out.start_klc.start_time) - in_again = False - """ - - def check_top_bottom(self, dataframe, bi_list, seg_list, zs_list): - """ - 检测新高/新低时的第一类买卖点,结合MACD背驰判断 - - :param dataframe: K线数据 - :param bi_list: 笔的列表 - :param seg_list: 线段的列表 - :param zs_list: 中枢的列表 - :return: 第一类买卖点列表,格式为{index: {'type': 类型, 'price': 价格, 'time': 时间}} - """ - buy_sell_points = {} - - # 计算MACD指标 - exp1 = dataframe['close'].ewm(span=12, adjust=False).mean() - exp2 = dataframe['close'].ewm(span=26, adjust=False).mean() - macd = exp1 - exp2 - signal = macd.ewm(span=9, adjust=False).mean() - histogram = macd - signal - - # MACD柱状图的面积 - positive_hist = histogram.copy() - negative_hist = histogram.copy() - positive_hist[positive_hist < 0] = 0 - negative_hist[negative_hist > 0] = 0 - - # 找到所有底分型和顶分型的笔 - bottom_bi_indices = [] # 底分型的笔索引 - top_bi_indices = [] # 顶分型的笔索引 - - for i, bi in enumerate(bi_list): - if not bi.end_klc: - continue - - if bi.dir == Chan_BI_DIR.UP and i > 0: - bottom_bi_indices.append(i-1) # 上升笔的前一笔是底分型 - elif bi.dir == Chan_BI_DIR.DOWN and i > 0: - top_bi_indices.append(i-1) # 下降笔的前一笔是顶分型 - - # 查找创新高的顶分型(第一类卖点) - for i in range(1, len(top_bi_indices)): - curr_idx = top_bi_indices[i] - prev_idx = top_bi_indices[i-1] - - if curr_idx >= len(bi_list) or prev_idx >= len(bi_list): - continue - - curr_bi = bi_list[curr_idx] - prev_bi = bi_list[prev_idx] - - if not curr_bi.end_klc or not prev_bi.end_klc: - continue - - # 确保是新高:当前高点比前一高点更高 - if curr_bi.high > prev_bi.high: - # 找到对应的MACD值 - curr_time = curr_bi.end_klc.end_time - prev_time = prev_bi.end_klc.end_time - - # 获取对应的dataframe索引 - curr_date_idx = dataframe[dataframe['date'].astype(str).str.contains(curr_time)].index[0] if any(dataframe['date'].astype(str).str.contains(curr_time)) else -1 - prev_date_idx = dataframe[dataframe['date'].astype(str).str.contains(prev_time)].index[0] if any(dataframe['date'].astype(str).str.contains(prev_time)) else -1 - - if curr_date_idx >= 0 and prev_date_idx >= 0: - # 计算两段走势的MACD柱状图面积(顶分型关注正面积) - curr_area = positive_hist[prev_date_idx:curr_date_idx+1].sum() - prev_area = positive_hist[max(0, prev_date_idx-abs(curr_date_idx-prev_date_idx)):prev_date_idx+1].sum() - - # 检查是否有MACD背驰 - # 新高但MACD力度减弱,形成顶背驰 - if curr_area < prev_area and curr_area > 0: - # 检查是否在中枢中 - in_zs = False - for zs in zs_list: - if zs.zd <= curr_bi.high <= zs.zg: - in_zs = True - break - - if not in_zs: # 不在中枢中的第一类卖点更可靠 - # 检查线段方向,确保是上升趋势 - is_uptrend = False - for seg in seg_list: - if seg.end_bi and seg.dir == Chan_SEG_DIR.UP and seg.end_bi.index >= curr_bi.index: - is_uptrend = True - break - - if is_uptrend: - buy_sell_points[curr_date_idx] = { - 'type': '第一类卖点', - 'price': dataframe.loc[curr_date_idx, 'high'], - 'time': curr_time, - 'reason': f'新高+顶背驰(MACD: {curr_area:.2f}<{prev_area:.2f})', - 'bi_idx': curr_idx, - 'is_sure': curr_bi.is_sure - } - - # 查找创新低的底分型(第一类买点) - for i in range(1, len(bottom_bi_indices)): - curr_idx = bottom_bi_indices[i] - prev_idx = bottom_bi_indices[i-1] - - if curr_idx >= len(bi_list) or prev_idx >= len(bi_list): - continue - - curr_bi = bi_list[curr_idx] - prev_bi = bi_list[prev_idx] - - if not curr_bi.end_klc or not prev_bi.end_klc: - continue - - # 确保是新低:当前低点比前一低点更低 - if curr_bi.low < prev_bi.low: - # 找到对应的MACD值 - curr_time = curr_bi.end_klc.end_time - prev_time = prev_bi.end_klc.end_time - - # 获取对应的dataframe索引 - curr_date_idx = dataframe[dataframe['date'].astype(str).str.contains(curr_time)].index[0] if any(dataframe['date'].astype(str).str.contains(curr_time)) else -1 - prev_date_idx = dataframe[dataframe['date'].astype(str).str.contains(prev_time)].index[0] if any(dataframe['date'].astype(str).str.contains(prev_time)) else -1 - - if curr_date_idx >= 0 and prev_date_idx >= 0: - # 计算两段走势的MACD柱状图面积(底分型关注负面积) - curr_area = abs(negative_hist[prev_date_idx:curr_date_idx+1].sum()) - prev_area = abs(negative_hist[max(0, prev_date_idx-abs(curr_date_idx-prev_date_idx)):prev_date_idx+1].sum()) - - # 检查是否有MACD背驰 - # 新低但MACD力度减弱,形成底背驰 - if curr_area < prev_area and curr_area > 0: - # 检查是否在中枢中 - in_zs = False - for zs in zs_list: - if zs.zd <= curr_bi.low <= zs.zg: - in_zs = True - break - - if not in_zs: # 不在中枢中的第一类买点更可靠 - # 检查线段方向,确保是下降趋势 - is_downtrend = False - for seg in seg_list: - if seg.end_bi and seg.dir == Chan_SEG_DIR.DOWN and seg.end_bi.index >= curr_bi.index: - is_downtrend = True - break - - if is_downtrend: - buy_sell_points[curr_date_idx] = { - 'type': '第一类买点', - 'price': dataframe.loc[curr_date_idx, 'low'], - 'time': curr_time, - 'reason': f'新低+底背驰(MACD: {curr_area:.2f}<{prev_area:.2f})', - 'bi_idx': curr_idx, - 'is_sure': curr_bi.is_sure - } - - return buy_sell_points \ No newline at end of file + \ No newline at end of file diff --git a/TF_DF.py b/TF_DF.py index 942bf14..97229d6 100644 --- a/TF_DF.py +++ b/TF_DF.py @@ -1,6 +1,6 @@ from datetime import timedelta from pandas import DataFrame -from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX +from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_PRICE_TREND from ChanKLU import ChanKLU from ChanKLC import ChanKLC from ChanBI import ChanBI @@ -20,40 +20,213 @@ import numpy as np from ChanMACD import ChanMACD class TF_DF(): - def __init__(self, timeframe, df, ticker_indicator): - self.timeframe = timeframe - self.dataframe = resample_to_interval(df, ticker_indicator*timeframe) - self.ticker_indicator = ticker_indicator - self.klu_list = [] - self.klc_list = [] - self.bi_list = [] - self.zs_list = [] - self.bsp_list = [] - self.seg_list = [] - self.init_TF_DF() - def init_TF_DF(self): - self.klu_list = self.cal_kl_data(self.dataframe) - self.klc_list = self.cal_klc_list(self.klu_list) - self.bi_list = self.cal_bi_list(self.klc_list) - self.seg_list = self.cal_seg_list(self.bi_list) - self.zs_list = self.cal_zs_list(self.bi_list, self.seg_list) - self.chanmacd = ChanMACD(self.klu_list) - self.klu_list = self.chanmacd.cal_macd_state() - def check_fx(self, klc): + def __init__(self, timeframe, df, ticker_indicator): + self.timeframe = timeframe + self.dataframe = resample_to_interval(df, ticker_indicator*timeframe) + self.dataframe = self.add_indicators(self.dataframe) + self.ticker_indicator = ticker_indicator + self.klu_list = [] + self.klc_list = [] + self.bi_list = [] + self.zs_list = [] + self.bsp_list = [] + self.seg_list = [] + self.init_TF_DF() + def init_TF_DF(self): + self.klu_list = self.cal_kl_data(self.dataframe) + self.klc_list = self.cal_klc_list(self.klu_list) + self.bi_list = self.cal_bi_list(self.klc_list) + self.seg_list = self.cal_seg_list(self.bi_list) + self.zs_list = self.cal_zs_list(self.bi_list, self.seg_list) + self.chanmacd = ChanMACD(self.klu_list) + self.klu_list = self.chanmacd.cal_macd_state() + def add_indicators(self, df): + fast = 12 + slow = 26 + period = 9 + macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period) + bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0) + bb120 = ta.BBANDS(df, timeperiod=120, nbdevup=3.0, nbdevdn=3.0, matype=0) + bb30 = ta.BBANDS(df, timeperiod=41, nbdevup=2.3, nbdevdn=2.3, matype=0) + bb302 = ta.BBANDS(df, timeperiod=41, nbdevup=2.0, nbdevdn=2.0, matype=0) + bb30 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0) + bb302 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0) + # 计算布林带中轨(移动平均线) + bb30_middle = ta.SMA(df, timeperiod=90) + + # 手动计算布林带 %B 指标 (BBP) + # %B = (Price - Lower Band) / (Upper Band - Lower Band) + bbp365 = (df['close'] - bb365['lowerband']) / (bb365['upperband'] - bb365['lowerband']) + bbp120 = (df['close'] - bb120['lowerband']) / (bb120['upperband'] - bb120['lowerband']) + bbp30 = (df['close'] - bb30['lowerband']) / (bb30['upperband'] - bb30['lowerband']) + bbp302 = (df['close'] - bb302['lowerband']) / (bb302['upperband'] - bb302['lowerband']) + df['atr'] = ta.ATR(df, timeperiod=14) + df['bbup365'] = bb365['upperband'] + df['bblow365'] = bb365['lowerband'] + df['bbp365'] = bbp365 + df['bbup120'] = bb120['upperband'] + df['bblow120'] = bb120['lowerband'] + df['bbp120'] = bbp120 + df['bbup30'] = bb30['upperband'] + df['bblow30'] = bb30['lowerband'] + df['bbmiddle30'] = bb30_middle # 添加bb30中轨 + df['bbp30'] = bbp30 + df['bbup302'] = bb302['upperband'] + df['bblow302'] = bb302['lowerband'] + df['bbp302'] = bbp302 + df['macd'] = macd['macd'] + df['macdsignal'] = macd['macdsignal'] + df['macdhist'] = macd['macdhist'] + df['ema5'] = ta.EMA(df, timeperiod=5) + df['ema10'] = ta.EMA(df, timeperiod=10) + df['ema24'] = ta.EMA(df, timeperiod=24) + df['ema26'] = ta.EMA(df, timeperiod=26) + df['ema52'] = ta.EMA(df, timeperiod=52) + df['rsi'] = ta.RSI(df, timeperiod=14) + df['volume_ratio'] = self.cal_volume_ratio(df) + return df + def check_fx(self, klc): if klc.pre and klc.next: if klc.high > klc.pre.high and klc.high > klc.next.high: if klc.macd > 0 and klc.macd > klc.signal and klc.signal > klc.macdhist: klc.set_fx(Chan_FX_TYPE.TOP) - #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time,klc.fx, "TOP") + # print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time,klc.fx, "TOP") return Chan_FX_TYPE.TOP elif klc.low < klc.pre.low and klc.low < klc.next.low: if klc.macd < 0 and klc.macd < klc.signal and klc.signal < klc.macdhist: klc.set_fx(Chan_FX_TYPE.BOTTOM) - #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time,klc.fx, "BOTTOM") + # print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time,klc.fx, "BOTTOM") return Chan_FX_TYPE.BOTTOM return Chan_FX_TYPE.UNKNOWN - - def cal_kl_data(self, dataframe:DataFrame): + def cal_volume_ratio(self, dataframe, window=10): + df = dataframe.copy() + # 计算过去N根K线的平均成交量 + df['avg_volume'] = df['volume'].rolling(window=window).mean() + # 计算量比 + df['volume_ratio'] = df['volume'] / df['avg_volume'] + # 填充缺失值(前N根K线) + df['volume_ratio'] = df['volume_ratio'].fillna(1.0) + return df['volume_ratio'] + def cal_trend(self, klc_list): + """ + 基于价格与EMA24/EMA52的位置关系、以及MACD/Signal/Hist的方向, + 为每个KLC打上趋势标签:'UP' / 'DOWN' / 'FLAT'。 + 仅设置 klc.trend,不影响其它字段。 + """ + if not klc_list: + return klc_list + last_trend = Chan_PRICE_TREND.UNKNOWN + for klc in klc_list: + price = getattr(klc, 'close', None) + ema24 = getattr(klc, 'ema24', None) + ema52 = getattr(klc, 'ema52', None) + macd = getattr(klc, 'macd', 0) if getattr(klc, 'macd', None) is not None else 0 + signal = getattr(klc, 'signal', 0) if getattr(klc, 'signal', None) is not None else 0 + hist = getattr(klc, 'macdhist', 0) if getattr(klc, 'macdhist', None) is not None else 0 + rsi = getattr(klc, 'rsi', None) + trend = Chan_PRICE_TREND.UNKNOWN + try: + # 有效性 + price_valid = price is not None and price != 0 + ema24_valid = ema24 is not None and ema24 != 0 + ema52_valid = ema52 is not None and ema52 != 0 + # 多因子投票 + score = 0 + # 1) 均线结构 + 价位 + if ema24_valid and ema52_valid: + score += 1 if ema24 > ema52 else -1 + if price_valid and ema24_valid: + score += 1 if price > ema24 else -1 + if price_valid and ema52_valid: + score += 1 if price > ema52 else -1 + # 2) MACD结构 + score += 1 if macd >= signal else -1 + if hist != 0: + score += 1 if hist > 0 else -1 + # 3) 动量与均线差分斜率 + pre = getattr(klc, 'pre', None) + if pre: + pre_close = getattr(pre, 'close', None) + if price_valid and pre_close is not None: + score += 1 if price >= pre_close else -1 + pre_ema24 = getattr(pre, 'ema24', None) + pre_ema52 = getattr(pre, 'ema52', None) + if ema24_valid and ema52_valid and pre_ema24 not in (None, 0) and pre_ema52 not in (None, 0): + spread_now = ema24 - ema52 + spread_pre = pre_ema24 - pre_ema52 + score += 1 if spread_now >= spread_pre else -1 + # 4) RSI 辅助 + if rsi is not None: + if rsi >= 55: + score += 1 + elif rsi <= 45: + score -= 1 + # 5) 指标未就绪回退(EMA/MACD缺失时,用动量与RSI辅助,延续趋势) + has_full_ind = ema24_valid and ema52_valid and not (macd == 0 and signal == 0 and hist == 0) + if not has_full_ind: + # 仅根据价动量/RSI做轻量判断,默认延续 last_trend,除非出现强反向 + strong_up = False + strong_down = False + pre = getattr(klc, 'pre', None) + if pre: + pre_close = getattr(pre, 'close', None) + if price_valid and pre_close is not None: + strong_up = (price >= pre_close) + strong_down = (price < pre_close) + if rsi is not None: + if rsi >= 60: + strong_up = True + elif rsi <= 40: + strong_down = True + if last_trend == Chan_PRICE_TREND.UP and not strong_down: + trend = Chan_PRICE_TREND.UP + elif last_trend == Chan_PRICE_TREND.DOWN and not strong_up: + trend = Chan_PRICE_TREND.DOWN + else: + trend = Chan_PRICE_TREND.UP if strong_up and not strong_down else (Chan_PRICE_TREND.DOWN if strong_down and not strong_up else Chan_PRICE_TREND.FLAT) + else: + # 6) 震荡过滤(仅当极近EMA52且MACD贴合时判作震荡) + near_flat = False + if price_valid and ema52_valid: + near_ema52 = abs(price - ema52) / abs(ema52) <= 0.0005 # 0.05% + near_macd = abs(macd - signal) <= (abs(price) * 0.00005 if price_valid else 0) + near_flat = near_ema52 and near_macd + # 7) 动态阈值 + 趋势记忆(更强粘滞:趋势中容忍小幅反分) + if near_flat: + trend = Chan_PRICE_TREND.FLAT + else: + if last_trend == Chan_PRICE_TREND.UP: + # 仅当出现明显反向才翻转,否则维持UP + if score <= -2: + trend = Chan_PRICE_TREND.DOWN + else: + trend = Chan_PRICE_TREND.UP + elif last_trend == Chan_PRICE_TREND.DOWN: + if score >= 2: + trend = Chan_PRICE_TREND.UP + else: + trend = Chan_PRICE_TREND.DOWN + else: + # 初始无记忆时,降低进入门槛 + if score >= 1: + trend = Chan_PRICE_TREND.UP + elif score <= -1: + trend = Chan_PRICE_TREND.DOWN + else: + trend = Chan_PRICE_TREND.FLAT + except Exception: + trend = Chan_PRICE_TREND.UNKNOWN + # 写回趋势 + if hasattr(klc, 'set_trend'): + klc.set_trend(trend) + else: + setattr(klc, 'trend', trend) + last_trend = trend + price_diff = klc.close - klc.pre.close if klc.pre else 0 + print(klc.start_time, klc.end_time, klc.close, klc.ema24, klc.ema52, klc.macd, klc.signal, klc.macdhist, klc.trend, price_diff) + #print(klc.start_time, klc.end_time, klc.trend, price_diff) + return klc_list + def cal_kl_data(self, dataframe:DataFrame): fields = "time,open,high,low,close,volume" klu_list = [] last_klu = None @@ -65,8 +238,8 @@ class TF_DF(): l = item['low'] c = item['close'] v = item['volume'] - #time_obj = date.fromtimestamp(date) - #date = date + timedelta(hours=8) + # time_obj = date.fromtimestamp(date) + # date = date + timedelta(hours=8) time_str = date.strftime('%Y-%m-%d %H:%M:%S') item_data = [ time_str, @@ -76,9 +249,9 @@ class TF_DF(): c, v ] - #klu = KLU(self.create_item_dict(item_data, GetColumnNameFromFieldList(fields))) + # klu = KLU(self.create_item_dict(item_data, GetColumnNameFromFieldList(fields))) klu = ChanKLU(time_str, o, h, l, c, v) - #print(klu.time, klu.open, klu.high, klu.low, klu.close, klu.volume) + # print(klu.time, klu.open, klu.high, klu.low, klu.close, klu.volume) klu.set_idx(i) klu_list.append(klu) if last_klu: @@ -89,7 +262,7 @@ class TF_DF(): klu.set_indicators(item) return klu_list - def cal_klc_list(self, klu_list): + def cal_klc_list(self, klu_list): klc_list = [] last_klu = None macd = ChanMACD(klu_list) @@ -117,9 +290,10 @@ class TF_DF(): klc = ChanKLC(klu, 0, ddir) klc_list.append(klc) last_klu = klu + klc_list = self.cal_trend(klc_list) return klc_list - def cal_seg_list(self, bi_list): + def cal_seg_list(self, bi_list): seg_list = [] up_bi_list = [] down_bi_list = [] @@ -707,7 +881,7 @@ class TF_DF(): #print(bi_list[index].start_time, bi_list[index].start_klc.start_time, bi_list[index].dir) return bi_list - def get_decimal(self, value): + def get_decimal(self, value): return Decimal("{:.2f}".format(value)) def cal_zs_list(self, bi_list, seg_list): diff --git a/strategies/ChanLun_BTC.py b/strategies/ChanLun_BTC.py new file mode 100644 index 0000000..02ccea1 --- /dev/null +++ b/strategies/ChanLun_BTC.py @@ -0,0 +1,328 @@ +# --- Do not remove these libs --- +from statistics import median +from freqtrade.strategy import IStrategy, stoploss_from_absolute +import sys +import os +# 添加父目录到系统路径 +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from ChanLun import ChanLun +from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX +# -------------------------------- +from technical.util import resample_to_interval, resampled_merge +import talib.abstract as ta +from pandas import DataFrame +import pandas as pd +from datetime import datetime, timedelta +from freqtrade.persistence import Trade, Order +from typing import Optional +import logging +logger = logging.getLogger(__name__) +### Now you can use logger.info('asfd') to log +# freqtrade plot-dataframe --strategy ChanLun_BTC --datadir user_data/data/binance -c ./user_data/ChanLun_SOL_30.json --timerange=20250309- + +# freqtrade trade -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies +# freqtrade backtesting -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies --timerange=20250901- +# freqtrade download-data -c ./user_data/Chan/config/ChanLun_BTC_30.json -t 1m --pairs BTC/USDT:USDT --timerange=20250405- +# freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/ChanLun_BTC_30.json -e 200 --timerange=20250201-20250901 +# freqtrade edge -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies --timerange 20250721-20250901 +# freqtrade plot-dataframe -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies --timerange 20250721-20250901 + +# sudo docker compose run --rm chanlun_btc backtesting -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies --timerange=20250721- +# sudo docker compose run --rm chanlun_btc download-data -c ./user_data/Chan/config/ChanLun_BTC_30.json --pairs BTC/USDT:USDT -t 1m --timerange 20240101- +# sudo docker compose run --rm chanlun_btc trade -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies + +class ChanLun_BTC(IStrategy): + INTERFACE_VERSION: int = 3 + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi" + # 30m and 1h + + minimal_roi = { + "0": 0.15, + "360": 0.2, + "640": 0.1, + "1200": 0 + } + # 5m and 15m + minimal_roi_1 = { + "0": 0.1, + "60": 0.05, + "120": 0.02, + "240": 0 + } + # 15m and 30m + minimal_roi_1 = { + "0": 0.1, + "240": 0.05, + "480": 0.03, + "600": 0 + } + minimal_roi_1 = { + "0": 1.50, + "120": 0.05, + "240": 0.025, + "360": 0 + } + + can_short = True + lev = 1.0 + stoploss = -0.3 # 设置为很大的负值,让custom_stoploss来控制 + use_custom_stoploss = True # 启用自定义止损 + + trailing_stop = False + trailing_stop_positive = 0.03 + trailing_stop_positive_offset = 0.06 + trailing_only_offset_is_reached = False + + # 关闭分批止盈/仓位调整 + position_adjustment_enable = False + startup_candle_count = 2880 + time3 = 3 + time5 = 5 + time15 = 15 + time30 = 30 + time60 = 60 + time2h = 120 + time4h = 240 + time1d = 1440 + last_time = datetime.now() + chan = ChanLun() + last_order = None + last_trade = None + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + dataframe = self.add_indicators(dataframe) + # 仅保留15m(用于BSP)与60m(用于ATR过滤/止损)两个重采样 + dataframe_15 = resample_to_interval(dataframe, self.get_ticker_indicator() * 15) + dataframe_60 = resample_to_interval(dataframe, self.get_ticker_indicator() * 60) + # 计算多周期BSP(以15m为基准),并合并到15m数据上 + # 先给重采样帧补指标 + dataframe_15 = self.add_indicators(dataframe_15) + dataframe_60 = self.add_indicators(dataframe_60) + # 计算15m BSP + bsp_15 = self.chan.cal_bsp(dataframe, self.get_ticker_indicator()) + # 合并15m与60m到主DF,生成 resample_*_* 列 + dataframe = resampled_merge(dataframe, dataframe_15) + dataframe = resampled_merge(dataframe, dataframe_60) + return dataframe + def add_indicators(self, df): + fast = 12 + slow = 26 + period = 9 + macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period) + bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0) + bb120 = ta.BBANDS(df, timeperiod=120, nbdevup=3.0, nbdevdn=3.0, matype=0) + bb30 = ta.BBANDS(df, timeperiod=41, nbdevup=2.3, nbdevdn=2.3, matype=0) + bb302 = ta.BBANDS(df, timeperiod=41, nbdevup=2.0, nbdevdn=2.0, matype=0) + bb30 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0) + bb302 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0) + # 计算布林带中轨(移动平均线) + bb30_middle = ta.SMA(df, timeperiod=90) + + # 手动计算布林带 %B 指标 (BBP) + # %B = (Price - Lower Band) / (Upper Band - Lower Band) + bbp365 = (df['close'] - bb365['lowerband']) / (bb365['upperband'] - bb365['lowerband']) + bbp120 = (df['close'] - bb120['lowerband']) / (bb120['upperband'] - bb120['lowerband']) + bbp30 = (df['close'] - bb30['lowerband']) / (bb30['upperband'] - bb30['lowerband']) + bbp302 = (df['close'] - bb302['lowerband']) / (bb302['upperband'] - bb302['lowerband']) + df['atr'] = ta.ATR(df, timeperiod=14) + df['bbup365'] = bb365['upperband'] + df['bblow365'] = bb365['lowerband'] + df['bbp365'] = bbp365 + df['bbup120'] = bb120['upperband'] + df['bblow120'] = bb120['lowerband'] + df['bbp120'] = bbp120 + df['bbup30'] = bb30['upperband'] + df['bblow30'] = bb30['lowerband'] + df['bbmiddle30'] = bb30_middle # 添加bb30中轨 + df['bbp30'] = bbp30 + df['bbup302'] = bb302['upperband'] + df['bblow302'] = bb302['lowerband'] + df['bbp302'] = bbp302 + df['macd'] = macd['macd'] + df['macdsignal'] = macd['macdsignal'] + df['macdhist'] = macd['macdhist'] + df['ema5'] = ta.EMA(df, timeperiod=5) + df['ema10'] = ta.EMA(df, timeperiod=10) + df['ema24'] = ta.EMA(df, timeperiod=24) + df['ema26'] = ta.EMA(df, timeperiod=26) + df['ema52'] = ta.EMA(df, timeperiod=52) + df['rsi'] = ta.RSI(df, timeperiod=14) + df['volume_ratio'] = self.cal_volume_ratio(df) + return df + def cal_volume_ratio(self, dataframe, window=10): + df = dataframe.copy() + # 计算过去N根K线的平均成交量 + df['avg_volume'] = df['volume'].rolling(window=window).mean() + # 计算量比 + df['volume_ratio'] = df['volume'] / df['avg_volume'] + # 填充缺失值(前N根K线) + df['volume_ratio'] = df['volume_ratio'].fillna(1.0) + return df['volume_ratio'] + def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float, + entry_tag: str | None, side: str, **kwargs) -> float: + new_entryprice = proposed_rate + if trade: + if trade.is_short: + new_entryprice = proposed_rate - 50 + else: + new_entryprice = proposed_rate + 50 + return new_entryprice + + def custom_exit_price(self, pair: str, trade: Trade, + current_time: datetime, proposed_rate: float, + current_profit: float, exit_tag: str | None, **kwargs) -> float: + new_exitprice = proposed_rate + if trade: + if trade.is_short: + new_exitprice = proposed_rate + 50 + else: + new_exitprice = proposed_rate - 50 + return new_exitprice + + def adjust_trade_position(self, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, + min_stake: Optional[float], max_stake: float, + current_entry_rate: float, current_exit_rate: float, + current_entry_profit: float, current_exit_profit: float, + **kwargs) -> Optional[float]: + # 关闭分批止盈,始终不调整仓位 + return None + + def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, after_fill: bool, + **kwargs) -> float | None: + """ + 止损 = 开仓价 ± 1 * ATR(开仓时的ATR)。 + 多单: 开仓价 - ATR;空单: 开仓价 + ATR。 + """ + # 保本止损:当浮盈达到或超过 1% 时,将止损提至开仓价 + #if current_profit is not None and current_profit >= 0.14: + #return stoploss_from_absolute(trade.open_rate, current_rate, is_short=trade.is_short) + + entry_atr = trade.get_custom_data(key="entry_atr") + if entry_atr is None: + # 回退:取当前数据的 ATR 估算 + dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) + if dataframe is not None and len(dataframe) > 0 and 'atr' in dataframe.columns: + entry_atr = float(dataframe.iloc[-1]['atr']) + else: + # 最保守的回退:5% + return -0.05 + dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + ema52_str = 'resample_{}_ema52'.format(self.get_ticker_indicator()*self.time15) + ema52_val = float(last_candle.get(ema52_str, 0) or 0) + close_str = 'resample_{}_close'.format(self.get_ticker_indicator()*self.time15) + close_val = float(last_candle.get(close_str, 0) or 0) + if close_val < ema52_val: + return -0.01 + if trade.is_short: + stop_price = trade.open_rate + float(entry_atr) + else: + stop_price = trade.open_rate - float(entry_atr) + return stoploss_from_absolute(stop_price, current_rate, is_short=trade.is_short) + + def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs): + # 不做分批止盈/最终止盈处理,退出由策略信号/ROI/止损决定 + return None + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, current_time: datetime, entry_tag: str | None, + side: str, **kwargs) -> bool: + """ + ATR 过滤:atr < 100 不开单。 + """ + try: + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + if dataframe is None or len(dataframe) == 0: + return False + last = dataframe.iloc[-1] + atr_str = 'resample_{}_atr'.format(self.get_ticker_indicator()*self.time60) + atr_val = float(last.get(atr_str, 0) or 0) + if atr_val < 0.001: + #logger.info(f"ATR过滤:atr={atr_val:.2f} < 100, 拒绝进场 {pair}") + return False + return True + except Exception as e: + logger.warning(f"confirm_trade_entry 异常: {e}") + return True + + def order_filled(self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs) -> None: + """ + Called right after an order fills. + Will be called for all order types (entry, exit, stoploss, position adjustment). + :param pair: Pair for trade + :param trade: trade object. + :param order: Order object. + :param current_time: datetime object, containing the current datetime + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + """ + # Obtain pair dataframe (just to show how to access it) + dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + atr_str = 'resample_{}_atr'.format(self.get_ticker_indicator()*self.time15) + # 保存开仓时的ATR值用于止损计算 + if (trade.nr_of_successful_entries == 1) and (order.ft_order_side == trade.entry_side): + entry_atr = last_candle[atr_str] * 4 + trade.set_custom_data(key="entry_atr", value=entry_atr) + #logger.info(f"保存开仓时ATR值: {entry_atr}") + return None + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + shift15 = self.time15 + shift60 = self.time60 + bsp_col = 'resample_{}_bsp_mtf'.format(self.get_ticker_indicator()*shift15) + score_col = 'resample_{}_mtf_score'.format(self.get_ticker_indicator()*shift15) + macdh_col = 'resample_{}_macdhist'.format(self.get_ticker_indicator()*shift15) + c60_col = 'resample_{}_close'.format(self.get_ticker_indicator()*shift60) + e60_col = 'resample_{}_ema52'.format(self.get_ticker_indicator()*shift60) + # 强化过滤:15m BSP + 分数阈值 + 60m 趋势同向 + 15m MACD柱同向 + if all(col in dataframe.columns for col in [bsp_col, score_col, macdh_col, c60_col, e60_col]): + dataframe.loc[ + ( + (dataframe[bsp_col].shift(shift15) == 1) & + (dataframe[score_col].shift(shift15) >= 1.2) & + (dataframe[c60_col].shift(shift60) >= dataframe[e60_col].shift(shift60)) & + (dataframe[macdh_col].shift(shift15) > 0) + ), + ['enter_long', 'enter_tag']] = (1, 'long_bsp15_v2') + dataframe.loc[ + ( + (dataframe[bsp_col].shift(shift15) == -1) & + (dataframe[score_col].shift(shift15) <= -1.2) & + (dataframe[c60_col].shift(shift60) <= dataframe[e60_col].shift(shift60)) & + (dataframe[macdh_col].shift(shift15) < 0) + ), + ['enter_short', 'enter_tag']] = (1, 'short_bsp15_v2') + return dataframe + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + shift15 = self.time15 + shift60 = self.time60 + bsp_col = 'resample_{}_bsp_mtf'.format(self.get_ticker_indicator()*shift15) + score_col = 'resample_{}_mtf_score'.format(self.get_ticker_indicator()*shift15) + c60_col = 'resample_{}_close'.format(self.get_ticker_indicator()*shift60) + e60_col = 'resample_{}_ema52'.format(self.get_ticker_indicator()*shift60) + # 反向强信号或60m趋势反向时平仓 + if all(col in dataframe.columns for col in [bsp_col, score_col, c60_col, e60_col]): + dataframe.loc[ + ( + ((dataframe[bsp_col].shift(shift15) == -1) & (dataframe[score_col].shift(shift15) <= -0.8)) | + (dataframe[c60_col].shift(shift60) < dataframe[e60_col].shift(shift60)) + ), + ['exit_long', 'exit_tag']] = (1, 'long_close_bsp15') + dataframe.loc[ + ( + ((dataframe[bsp_col].shift(shift15) == 1) & (dataframe[score_col].shift(shift15) >= 0.8)) | + (dataframe[c60_col].shift(shift60) > dataframe[e60_col].shift(shift60)) + ), + ['exit_short', 'exit_tag']] = (1, 'short_close_bsp15') + return dataframe + def leverage(self, pair: str, current_time: datetime, current_rate: float, + proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str, + **kwargs) -> float: + return self.lev + + def get_ticker_indicator(self): + return int(self.timeframe[:-1]) \ No newline at end of file