From 4f3629ba765ebb53e7e66d4d96532ea747891c0b Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Thu, 8 May 2025 19:30:58 +0800 Subject: [PATCH] A new strategy is added haha --- ChanKLC.py | 677 +++++++++++++++++++++++++++- ChanLun.py | 45 +- ChanLun_Classifier.py | 22 +- __pycache__/ChanKLC.cpython-312.pyc | Bin 19292 -> 47250 bytes __pycache__/ChanLun.cpython-312.pyc | Bin 115063 -> 116797 bytes strategies/ChanLun_SOL_5.py | 157 +++++-- 6 files changed, 838 insertions(+), 63 deletions(-) diff --git a/ChanKLC.py b/ChanKLC.py index a5f625e..4a9d948 100644 --- a/ChanKLC.py +++ b/ChanKLC.py @@ -30,6 +30,7 @@ class ChanKLC(): self.klc_fx_type = Chan_KLC_FX.UNKNOWN self.rsi = klu.rsi self.volume_ratio = klu.volume_ratio + self.macdhist = 0 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 @@ -43,8 +44,11 @@ class ChanKLC(): self.volume += self.klus[index].volume self.rsi += self.klus[index].rsi self.volume_ratio += self.klus[index].volume_ratio + self.macdhist += self.klus[index].macdhist self.rsi = self.rsi / len(self.klus) self.volume_ratio = self.volume_ratio / len(self.klus) + self.volume = self.volume / len(self.klus) + self.macdhist = self.macdhist / len(self.klus) def set_next(self, klc): self.next = klc def set_pre(self, klc): @@ -122,6 +126,7 @@ 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() @@ -189,7 +194,7 @@ class ChanKLC(): # K线波动范围 if self.close != 0: # 避免除以零 - features['klc_range'] = (self.high - self.low) / self.close + features['klc_range'] = 0 #(self.high - self.low) / self.close else: features['klc_range'] = 0 @@ -301,9 +306,9 @@ class ChanKLC(): features['klc_macd_signal'] = 0 if 'klu_macdhist' in klu_features: - features['klc_macd_hist'] = klu_features['klu_macdhist'] + features['klc_macdhist'] = klu_features['klu_macdhist'] else: - features['klc_macd_hist'] = 0 + features['klc_macdhist'] = 0 # 成交量变化 if self.pre: @@ -459,4 +464,670 @@ class ChanKLC(): # 从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 + return features \ No newline at end of file diff --git a/ChanLun.py b/ChanLun.py index 0ecc6f2..a42f307 100644 --- a/ChanLun.py +++ b/ChanLun.py @@ -88,21 +88,38 @@ class ChanLun(): df['macdhist'] = macd['macdhist'] return df def plot_dataframe(self, dataframe): - return self.get_klu_list(dataframe) klc_list = self.get_klc_list(dataframe) bi_list= self.cal_bi_list(klc_list) - seg_list = self.get_seg_list(bi_list) - zs_list = self.calculate_zs(bi_list, seg_list) - #bi_macd_div_list = self.get_bi_macd_div_list(bi_list, dataframe) - #seg_macd_div_list = self.get_seg_macd_div_list(seg_list, dataframe) - #buy_sell_points = self.identify_buy_sell_points(bi_list, seg_list, zs_list, dataframe) - #divergence_points = self.identify_macd_divergence(dataframe, bi_list) - #self.print_bi(bi_list) - #self.print_seg(seg_list) - #self.print_zs(zs_list) - #self.print_bsp_list(bsp_list) - #self.plot(dataframe, bi_list, seg_list, zs_list, buy_sell_points, divergence_points) - #return plt.gcf() + 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 + features = klc.get_feature_data() + if (klc.klc_fx_type == Chan_KLC_FX.TOP1 or klc.klc_fx_type == Chan_KLC_FX.TOP2) and klc.bi.dir == Chan_BI_DIR.UP: + state_list.append("10") + #print(klc.start_time, klc.klc_fx_type) + elif (klc.klc_fx_type == Chan_KLC_FX.BOTTOM1 or klc.klc_fx_type == Chan_KLC_FX.BOTTOM2) and klc.bi.dir == Chan_BI_DIR.DOWN: + state_list.append("-10") + #print(klc.end_time, klc.klc_fx_type, klc.bi.start_time, klc.bi.dir) + 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 print_data(self, dataframe): klc_list = self.get_klc_list(dataframe) bi_list = self.cal_bi_list(klc_list) @@ -658,7 +675,7 @@ class ChanLun(): #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Bottom Change 1") klc.set_klc_fx_type(Chan_KLC_FX.TOP2) #print(klc.start_time, last_bi.start_klc.start_time, "New TOP Found reset last bi") - klc.set_state("10") + #klc.set_state("10") #print(klc.start_time, klc.fx, "笔卖点Sell 1") klc.set_klc_fx_type(Chan_KLC_FX.TOP2) bi_list[-1].add_klc(klc) diff --git a/ChanLun_Classifier.py b/ChanLun_Classifier.py index c72c794..3933036 100644 --- a/ChanLun_Classifier.py +++ b/ChanLun_Classifier.py @@ -62,15 +62,15 @@ class ChanLunClassifier: # 默认XGBoost参数 default_params = { 'objective': 'binary:logistic', - 'max_depth': 4, - 'eta': 0.03, + 'max_depth': 8, + 'eta': 0.01, 'subsample': 0.8, 'colsample_bytree': 0.8, 'eval_metric': 'auc', - 'gamma': 0.1, - 'min_child_weight': 3, - 'alpha': 1, # L1正则化 - 'lambda': 3, # L2正则化 + 'gamma': 0.0, + 'min_child_weight': 1, + 'alpha': 0, # L1正则化 + 'lambda': 0.5, # L2正则化 'scale_pos_weight': 1 } @@ -208,7 +208,7 @@ class ChanLunClassifier: else: self.model = xgb.Booster() self.model.load_model(model_file_path) - def find_best_params(self, dataframe=None, save_csv=False, csv_path_prefix='param_'): + def find_best_params(self, dataframe=None, save_csv=False, csv_path_prefix='param_', model_name=None): """ 寻找最佳参数组合 :param dataframe: 输入的DataFrame,如果为None则使用初始化时的dataframe @@ -240,7 +240,7 @@ class ChanLunClassifier: param_info = f"eta{params['eta']}_depth{params['max_depth']}" train_csv_path = f"{csv_path_prefix}train_{param_info}.csv" if save_csv else None - model = self.train_model(dataframe=dataframe, data_file_path=train_csv_path, custom_params=params) + model = self.train_model(dataframe=dataframe, data_file_path=train_csv_path, custom_params=params, model_name=model_name) # 分割数据集,后20%用于测试 if dataframe is None: @@ -298,6 +298,8 @@ class ChanLunClassifier: for klc in klc_list: if klc.klc_fx_type != Chan_KLC_FX.UNKNOWN: sample_list.append(klc) + klc_count = 0 + print('Processing data...') for klc in sample_list: if bi_index >= len(bi_list): bi_index = len(bi_list) - 1 @@ -333,6 +335,10 @@ class ChanLunClassifier: feature_data.append(feature_vec) labels.append(label) + klc_count += 1 + percent = klc_count/len(sample_list)*100 + if percent % 10 == 0: + print('Data processed:', percent, '%') for index, key in enumerate(feature_keys): print(index, key, feature_data[0][index]) # 如果需要保存到CSV diff --git a/__pycache__/ChanKLC.cpython-312.pyc b/__pycache__/ChanKLC.cpython-312.pyc index f1bcb6212499dd89bf0e1b85ab2864db0c180ed4..5ff9b25794e8a0d6ecb219e79d2bee648b429b8b 100644 GIT binary patch literal 47250 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 delta 1483 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 0.37 and (klc.klc_fx_type == Chan_KLC_FX.BOTTOM1 or klc.klc_fx_type == Chan_KLC_FX.BOTTOM2): - features = klc.get_feature_data() - print(klc.end_time, klc.fx, self.classifier.predict(klc), features['klc_volume_ratio'], features['klc_macd_hist'], features['klc_rsi']) + features = klc.get_feature_data() + if self.classifier.predict(klc) > 0.5 and (klc.klc_fx_type == Chan_KLC_FX.BOTTOM1 or klc.klc_fx_type == Chan_KLC_FX.BOTTOM2) and features['klc_rsi'] < 40: + print(klc.end_time, klc.fx, self.classifier.predict(klc), features['klc_volume_ratio'], features['klc_macdhist'], features['klc_rsi']) bottom_avg += self.classifier.predict(klc) bottom_count += 1 - if self.classifier.predict(klc) > 0.37 and (klc.klc_fx_type == Chan_KLC_FX.TOP1 or klc.klc_fx_type == Chan_KLC_FX.TOP2): - features = klc.get_feature_data() - print(klc.end_time, klc.fx, self.classifier.predict(klc), features['klc_volume_ratio'], features['klc_macd_hist'], features['klc_rsi']) + if self.classifier.predict(klc) > 0.5 and (klc.klc_fx_type == Chan_KLC_FX.TOP1 or klc.klc_fx_type == Chan_KLC_FX.TOP2) and features['klc_rsi'] > 50: + print(klc.end_time, klc.fx, self.classifier.predict(klc), features['klc_volume_ratio'], features['klc_macdhist'], features['klc_rsi']) top_avg += self.classifier.predict(klc) top_count += 1 if bottom_count > 0: bottom_avg /= bottom_count if top_count > 0: top_avg /= top_count - print(bottom_avg, top_avg) - print("-------------------------------------------------------------------------------") + print('Bottom:', bottom_avg, 'Top:', top_avg) + print("-------------------------------------------------------------------------------") """ self.print_xgb(dataframe, "1m_model") @@ -155,7 +162,7 @@ class ChanLun_SOL_5(IStrategy): print("-------------------------------------------------------------------------------") """ - #classifier.find_best_params(dataframe) + #classifier.train_model(use_cv=False) #classifier.validate_model(dataframe) #self.chan.get_bsp_list(dataframe) @@ -164,7 +171,7 @@ class ChanLun_SOL_5(IStrategy): #dataframe_30['state'] = self.chan.cal_klu_state(dataframe_30) #dataframe_60['state'] = self.chan.cal_klu_state(dataframe_60) #dataframe_4h['state'] = self.chan.resample_klc_list(dataframe_4h) - + #self.print_bi_klc_fx(dataframe_60, "60m_model") #self.chan.plot_dual(dataframe_5, dataframe_30) dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) #self.print_macd_div_list(dataframe) @@ -176,20 +183,94 @@ class ChanLun_SOL_5(IStrategy): #self.print_klc(dataframe_5, "5m: ") #self.print_klc(dataframe_30, "30m:") #self.log_macd_div_list(dataframe) - #self.print_xgb(dataframe, "1m_model") - #self.print_xgb(dataframe_5, "5m_model") - #self.print_xgb(dataframe_30, "30m_model") - #self.print_xgb(dataframe_60, "1h_model") - #self.print_xgb(dataframe_4h, "4h_model") + self.print_xgb(dataframe, "1m_model") + self.print_xgb(dataframe_5, "5m_model") + self.print_xgb(dataframe_30, "30m_model") + self.print_xgb(dataframe_60, "60m_model") + self.print_xgb(dataframe_4h, "4h_model") + #self.print_xgb(dataframe_1d, "1d_model") print("-------------------------------------------------------------------------------") self.last_time = datetime.now() #dataframe = resampled_merge(dataframe, dataframe_5) #dataframe = resampled_merge(dataframe, dataframe_15) #dataframe = resampled_merge(dataframe, dataframe_30) - #dataframe = resampled_merge(dataframe, dataframe_60) + dataframe = resampled_merge(dataframe, dataframe_60) #dataframe = resampled_merge(dataframe, dataframe_4h) return dataframe - # This is called when placing the initial order (opening trade) + def print_bi_klc_fx(self, dataframe, model_name): + klc_list = self.chan.get_full_klc_list(dataframe) + bi_list = self.chan.cal_bi_list(klc_list) + fx_count_list = [] + fx_count_list_up = [] + fx_count_list_down = [] + self.classifier.load_model(model_name) + for bi in bi_list[1:-1]: + fx_count = 0 + if bi.end_klc: + for index in range(bi.start_klc.index, bi.end_klc.index+1): + klc = klc_list[index] + features = klc.get_feature_data() + if bi.dir == Chan_BI_DIR.UP and (klc.klc_fx_type == Chan_KLC_FX.TOP1 or klc.klc_fx_type == Chan_KLC_FX.TOP2): + fx_count += 1 + print(klc.start_time, klc.klc_fx_type, self.classifier.predict(klc), bi.dir, features['klc_volume_ratio'], features['klc_macdhist'], features['klc_rsi']) + else: + if bi.dir == Chan_BI_DIR.DOWN and (klc.klc_fx_type == Chan_KLC_FX.BOTTOM1 or klc.klc_fx_type == Chan_KLC_FX.BOTTOM2): + fx_count += 1 + print(klc.start_time, klc.klc_fx_type, self.classifier.predict(klc), bi.dir, features['klc_volume_ratio'], features['klc_macdhist'], features['klc_rsi']) + fx_count_list.append(fx_count) + if fx_count == 0: + print("Not a bi: ", bi.start_time, bi.end_time, bi.dir) + if bi.dir == Chan_BI_DIR.UP: + fx_count_list_up.append(fx_count) + else: + fx_count_list_down.append(fx_count) + #print(bi.start_time, bi.end_time, bi.dir, fx_count) + avg_count = sum(fx_count_list) / len(fx_count_list) + max_count = max(fx_count_list) + min_count = min(fx_count_list) + median_count = median(fx_count_list) + print("Total bi:", len(bi_list), "AVG:", avg_count, "MAX:", max_count, "MIN:", min_count, "MEDIAN:", median_count) + for index in range(0, max_count+1): + index_count = fx_count_list.count(index) + print("Total:", index, "COUNT:", index_count, "RATIO:", index_count/len(fx_count_list)) + avg_count_up = sum(fx_count_list_up) / len(fx_count_list_up) + avg_count_down = sum(fx_count_list_down) / len(fx_count_list_down) + median_count_up = median(fx_count_list_up) + median_count_down = median(fx_count_list_down) + max_count_up = max(fx_count_list_up) + min_count_up = min(fx_count_list_up) + max_count_down = max(fx_count_list_down) + min_count_down = min(fx_count_list_down) + print("Total UP bi:", len(fx_count_list_up), "AVG:", avg_count_up, "MAX:", max_count_up, "MIN:", min_count_up, "MEDIAN:", median_count_up) + for index in range(0, max_count_up+1): + index_count = fx_count_list_up.count(index) + print("UP:", index, "COUNT:", index_count, "RATIO:", index_count/len(fx_count_list_up)) + print("Total DOWN bi:", len(fx_count_list_down), "AVG:", avg_count_down, "MAX:", max_count_down, "MIN:", min_count_down, "MEDIAN:", median_count_down) + for index in range(0, max_count_down+1): + index_count = fx_count_list_down.count(index) + print("DOWN:", index, "COUNT:", index_count, "RATIO:", index_count/len(fx_count_list_down)) + + def print_klc_list(self, klc_list, bi_list): + bi_index = 0 + for klc in klc_list: + if bi_index == len(bi_list): + bi_index = len(bi_list) - 1 + bi = bi_list[bi_index] + if self.check_klc_in_bi(klc, bi): + print("KLC in Bi: ", klc.start_time, klc.klc_fx_type, klc.bi.dir, klc.distance, bi.start_time, bi.dir) + else: + if bi.start_klc.index < klc.index: + bi_index += 1 + print("KLC not in Bi: ", klc.start_time, klc.klc_fx_type, klc.bi.dir, klc.distance, bi.start_time, bi.dir) + else: + if klc.bi: + print("KLC not in Bi: ", klc.start_time, klc.klc_fx_type, klc.bi.dir, klc.distance, bi.start_time, bi.dir) + else: + print("KLC not in Bi: ", klc.start_time, klc.klc_fx_type, klc.distance, bi.start_time, bi.dir) + def check_klc_in_bi(self, klc, bi): + if klc.bi and klc.bi.index == bi.index: + return True + return False def print_xgb(self, dataframe, model_name): self.classifier.load_model(model_name) klc_list = self.chan.get_klc_list(dataframe) @@ -210,7 +291,7 @@ class ChanLun_SOL_5(IStrategy): # We need to leave most of the funds for possible further DCA orders # This also applies to fixed stakes return proposed_stake / self.max_dca_multiplier - def adjust_trade_position1(self, trade: Trade, current_time: datetime, + def adjust_trade_position(self, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, min_stake: float | None, max_stake: float, current_entry_rate: float, current_exit_rate: float, @@ -282,12 +363,12 @@ class ChanLun_SOL_5(IStrategy): stake_amount = stake_amount * (1 + (count_of_entries * 0.5)) dataframe_date = dataframe.iloc[-1]['date'] #print(stake_amount, "---------------------------------------------------") - if last_entry.order_filled_utc + timedelta(minutes=self.time5) < dataframe_date: + if last_entry.order_filled_utc + timedelta(minutes=self.time5*6) < dataframe_date: if dataframe.iloc[-self.time5]['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)] == "-10" and last_entry.side == "buy": #print(dataframe.iloc[-self.time5]) #print(stake_amount) return stake_amount, "1/3rd_increase" - if last_entry.order_filled_utc + timedelta(minutes=self.time5) < dataframe_date: + if last_entry.order_filled_utc + timedelta(minutes=self.time5*6) < dataframe_date: if dataframe.iloc[-self.time5]['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)] == "10" and last_entry.side == "sell": #print(dataframe.iloc[-self.time5]) #print(stake_amount) @@ -392,8 +473,8 @@ class ChanLun_SOL_5(IStrategy): dataframe.loc[ ( - (dataframe['state'] == "-30") - #((dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)] == "-11") | + #(dataframe['state'] == "-30") + (dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") & #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "-10") & #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") @@ -402,8 +483,8 @@ class ChanLun_SOL_5(IStrategy): ['enter_long', 'enter_tag']] = (1, 'long_signal_chan') dataframe.loc[ ( - (dataframe['state'] == "30") - #((dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)] == "11") | + #(dataframe['state'] == "30") + (dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "10") #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "10") & #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "10") & #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "10") @@ -414,16 +495,16 @@ class ChanLun_SOL_5(IStrategy): def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[ ( - (dataframe['state']== "30") - #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "10") + #(dataframe['state']== "30") + (dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "10") #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "10") & #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time60)] == "10") ), ['exit_long', 'exit_tag']] = (1, 'long_close_signal_chan') dataframe.loc[ ( - (dataframe['state'] == "-30") - #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") + #(dataframe['state'] == "-30") + (dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "-10") & #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time60)] == "-10") ),