commit 626d3a6f264918863934138e5b650bc22341416d Author: jackyu66git Date: Tue Apr 22 10:04:30 2025 +0800 Initial commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..462effb Binary files /dev/null and b/.DS_Store differ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/ChanBI.py b/ChanBI.py new file mode 100644 index 0000000..7f2b6bd --- /dev/null +++ b/ChanBI.py @@ -0,0 +1,67 @@ +import ChanKLC +from ChanEnum import Chan_BI_DIR +class ChanBI(): + def __init__(self, klc: ChanKLC, index, ddir=Chan_BI_DIR.UP): + self.start_klc = klc + self.end_klc = None + self.next = None + self.pre = None + self.dir = ddir + self.index = index + self.is_sure = False + self.high = klc.high + self.low = klc.low + self.sure_time = None + self.klc_list = [] + self.klc_list.append(klc) + self.end_time = None + self.start_time = klc.start_time + self.macd_hist = 0 + self.macd_div = 0 + def set_macd_hist(self, macd_hist): + self.macd_hist = macd_hist + def set_macd_div(self, macd_div): + self.macd_div = macd_div + def check_overlap(self): + if self.next and self.next.next: + if self.dir == Chan_BI_DIR.UP: + return self.high > self.next.low and self.high < self.next.next.high + else: + return self.high > self.next.high and self.low > self.next.next.low + else: + return False + def set_end_klc(self, klc, sure_klc): + if self.dir == Chan_BI_DIR.UP and klc.high > self.high: + self.high = klc.high + if self.dir == Chan_BI_DIR.DOWN and klc.low < self.low: + self.low = klc.low + self.end_klc = klc + self.set_is_sure(True, sure_klc.end_time) + self.end_time = klc.end_time + def set_is_sure(self, is_sure, time): + self.is_sure = is_sure + self.sure_time = time + def set_start_klc(self, klc, ddir): + self.start_klc = klc + self.klc_list = [] + self.klc_list.append(klc) + self.high = klc.high + self.low = klc.low + self.dir = ddir + def set_pre(self, bi): + self.pre = bi + def set_next(self, bi): + self.next = bi + def add_klc(self, klc): + self.klc_list.append(klc) + def append_klc_list(self, klc_list): + self.klc_list.append(klc_list) + def update_bi(self, klc): + self.end_klc = None + if self.dir == Chan_BI_DIR.UP and klc.high > self.high: + self.high = klc.high + if self.dir == Chan_BI_DIR.DOWN and klc.low < self.low: + self.low = klc.low + self.is_sure = False + self.sure_time = None + #print(self.start_klc.start_time, klc.start_time, klc.fx, "This bi is extended") \ No newline at end of file diff --git a/ChanBIZS.py b/ChanBIZS.py new file mode 100644 index 0000000..4cd496a --- /dev/null +++ b/ChanBIZS.py @@ -0,0 +1,55 @@ +from ChanEnum import Chan_ZS_DIR +import ChanBI +# 中枢 +class ChanBIZS(): + def __init__(self, start_bi: ChanBI, index, ddir: Chan_ZS_DIR): + self.start_klc = start_bi.start_klc + self.start_time = self.start_klc.start_time + self.end_time = None + self.index = index + self.next = None + self.pre = None + self.start_bi = start_bi + self.bi_list = [] + self.bi_list.append(start_bi) + self.end_bi = None + self.last_bi_in = None + self.bi_out = None + self.is_sure = False + self.zg = 0 + self.zd = 0 + self.dir = ddir + self.sure_time = None + self.end_klc = None + self.bi_out_count = 0 + self.bi_out_list = [] + def set_last_bi_in(self, last_bi_in): + self.last_bi_in = last_bi_in + def set_bi_out(self, bi_out): + if bi_out: + #print(bi_out.start_klc.start_time, bi_out.sure_time, bi_out.dir, bi_out_seg.dir, len(self.bi_out_list)) + if len(self.bi_out_list) > 0: + last_bi = self.bi_out_list[-1] + if last_bi.index != bi_out.index: + self.bi_out_list.append(bi_out) + else: + self.bi_out_list.append(bi_out) + self.bi_out = bi_out + def set_end_klc(self, end_klc, sure_time, bi_out_count): + self.end_klc = end_klc + self.set_end_time(end_klc.end_time) + self.is_sure = True + self.sure_time = sure_time + self.bi_out_count = bi_out_count + def set_pre(self, pre): + self.pre = pre + def set_next(self, next): + self.next = next + def set_end_time(self, end_time): + self.end_time = end_time + def add_klc(self, klc): + self.klc_list.append(klc) + def set_zg(self, zg): + self.zg = zg + def set_zd(self, zd): + self.zd = zd \ No newline at end of file diff --git a/ChanBSP.py b/ChanBSP.py new file mode 100644 index 0000000..756195a --- /dev/null +++ b/ChanBSP.py @@ -0,0 +1,24 @@ +import ChanBI +from ChanEnum import Chan_BSP_TYPE, Chan_BSP_DIR + +class ChanBSP(): + def __init__(self, bi: ChanBI, index, type: Chan_BSP_TYPE, ddir: Chan_BSP_DIR, sure_time, zs_count, zs, seg): + self.bi = bi + self.klc = bi.start_klc + self.index = index + self.type = type + self.start_time = bi.end_klc.start_time + self.end_time = bi.end_klc.end_time + if sure_time: + self.is_sure = True + self.sure_time = sure_time + else: + self.is_sure = False + self.sure_time = None + self.dir = ddir + self.zs_count = zs_count + self.zs = zs + self.seg = seg + def set_sure_time(self, sure_time): + self.is_sure = True + self.sure_time = sure_time \ No newline at end of file diff --git a/ChanCTime.py b/ChanCTime.py new file mode 100644 index 0000000..04e1bf6 --- /dev/null +++ b/ChanCTime.py @@ -0,0 +1,44 @@ +from datetime import datetime + + +class ChanCTime: + def __init__(self, year, month, day, hour, minute, second=0, auto=True): + self.year = year + self.month = month + self.day = day + self.hour = hour + self.minute = minute + self.second = second + self.auto = auto # 自适应对天的理解 + self.set_timestamp() # set self.ts + + def __str__(self): + if self.hour == 0 and self.minute == 0: + return f"{self.year:04}/{self.month:02}/{self.day:02}" + else: + return f"{self.year:04}/{self.month:02}/{self.day:02} {self.hour:02}:{self.minute:02}" + + def to_str(self): + if self.hour == 0 and self.minute == 0: + return f"{self.year:04}/{self.month:02}/{self.day:02}" + else: + return f"{self.year:04}/{self.month:02}/{self.day:02} {self.hour:02}:{self.minute:02}" + + def toDateStr(self, splt=''): + return f"{self.year:04}{splt}{self.month:02}{splt}{self.day:02}" + + def toDate(self): + return ChanCTime(self.year, self.month, self.day, 0, 0, auto=False) + + def set_timestamp(self): + if self.hour == 0 and self.minute == 0 and self.auto: + date = datetime(self.year, self.month, self.day, 23, 59, self.second) + else: + date = datetime(self.year, self.month, self.day, self.hour, self.minute, self.second) + self.ts = date.timestamp() + + def __gt__(self, t2): + return self.ts > t2.ts + + def __ge__(self, t2): + return self.ts >= t2.ts diff --git a/ChanEnum.py b/ChanEnum.py new file mode 100644 index 0000000..93c2d78 --- /dev/null +++ b/ChanEnum.py @@ -0,0 +1,147 @@ +from enum import Enum, auto +from typing import Literal + + +class Chan_DATA_SRC(Enum): + BAO_STOCK = auto() + CCXT = auto() + CSV = auto() + +class Chan_ZS_DIR(Enum): + UP = auto() + DOWN = auto() + +class Chan_KL_TYPE(Enum): + K_1M = auto() + K_DAY = auto() + K_WEEK = auto() + K_MON = auto() + K_YEAR = auto() + K_5M = auto() + K_15M = auto() + K_30M = auto() + K_60M = auto() + K_3M = auto() + K_QUARTER = auto() + + +class Chan_KLINE_DIR(Enum): + UP = auto() + DOWN = auto() + COMBINE = auto() + INCLUDED = auto() + + +class Chan_FX_TYPE(Enum): + BOTTOM = auto() + TOP = auto() + UNKNOWN = auto() + UP = auto() + DOWN = auto() + TT = auto() + BB = auto() + + +class Chan_BI_DIR(Enum): + UP = auto() + DOWN = auto() + +class Chan_SEG_DIR(Enum): + UP = auto() + DOWN = auto() + +class Chan_BI_TYPE(Enum): + UNKNOWN = auto() + STRICT = auto() + SUB_VALUE = auto() # 次高低点成笔 + TIAOKONG_THRED = auto() + DAHENG = auto() + TUIBI = auto() + UNSTRICT = auto() + TIAOKONG_VALUE = auto() + + +Chan_BSP_MAIN_TYPE = Literal['1', '2', '3'] + +class Chan_BSP_DIR(Enum): + BUY = auto() + SELL = auto() + +class Chan_BSP_TYPE(Enum): + T1 = '1' + T1P = '1p' + T2 = '2' + T2S = '2s' + T3A = '3a' # 中枢在1类后面 + T3B = '3b' # 中枢在1类前面 + T3 = '3' + T3E ='3e' # T3退出点 + QJT = 'qjt' # 区间套突破 + QJT1 = 'qjt1' # 区间套一类买点 + QJT2 = 'qjt2' # 区间套一类卖点 + QJT3 = 'qjt3' # 区间套三类买点 + def main_type(self) -> Chan_BSP_MAIN_TYPE: + return self.value[0] # type: ignore + + +class Chan_AUTYPE(Enum): + QFQ = auto() + HFQ = auto() + NONE = auto() + + +class Chan_TREND_TYPE(Enum): + MEAN = "mean" + MAX = "max" + MIN = "min" + + +class Chan_TREND_LINE_SIDE(Enum): + INSIDE = auto() + OUTSIDE = auto() + + +class Chan_LEFT_SEG_METHOD(Enum): + ALL = auto() + PEAK = auto() + + +class Chan_FX_CHECK_METHOD(Enum): + STRICT = auto() + LOSS = auto() + HALF = auto() + TOTALLY = auto() + + +class Chan_SEG_TYPE(Enum): + BI = auto() + SEG = auto() + + +class Chan_MACD_ALGO(Enum): + AREA = auto() + PEAK = auto() + FULL_AREA = auto() + DIFF = auto() + SLOPE = auto() + AMP = auto() + VOLUMN = auto() + AMOUNT = auto() + VOLUMN_AVG = auto() + AMOUNT_AVG = auto() + TURNRATE_AVG = auto() + RSI = auto() + + +class Chan_DATA_FIELD: + FIELD_TIME = "time_key" + FIELD_OPEN = "open" + FIELD_HIGH = "high" + FIELD_LOW = "low" + FIELD_CLOSE = "close" + FIELD_VOLUME = "volume" # 成交量 + FIELD_TURNOVER = "turnover" # 成交额 + FIELD_TURNRATE = "turnover_rate" # 换手率 + + +Chan_TRADE_INFO_LST = [Chan_DATA_FIELD.FIELD_VOLUME, Chan_DATA_FIELD.FIELD_TURNOVER, Chan_DATA_FIELD.FIELD_TURNRATE] diff --git a/ChanKLC.py b/ChanKLC.py new file mode 100644 index 0000000..47806d2 --- /dev/null +++ b/ChanKLC.py @@ -0,0 +1,440 @@ +import copy +from typing import Dict, Optional + +from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR +import ChanKLU +import ChanCTime + +# 根据结合律合并K线后的K线 +class ChanKLC(): + def __init__(self, klu: ChanKLU, index, ddir=Chan_KLINE_DIR.UP): + self.start_time = klu.time + self.end_time = None + self.high = klu.high + self.low = klu.low + self.dir = ddir + self.index = index + self.klus = [] + self.add_klu(klu) + self.fx = Chan_FX_TYPE.UNKNOWN + self.next = None + self.pre = None + self.start_klu = klu + self.end_klu = None + self.state = "00" + self.open = klu.open + self.close = klu.close + self.volume = klu.volume + def add_klu(self, klu): + self.klus.append(klu) + def set_end_klu(self, klu): + self.end_klu = klu + self.end_time = klu.time + self.close = klu.close + for index in range(1, len(self.klus)): + self.volume += self.klus[index].volume + def set_next(self, klc): + self.next = klc + def set_pre(self, klc): + self.pre = klc + def set_state(self, state): + self.state = state + def check_klu_included(self, klu): + if self.high >= klu.high: + # high大于,low小于,左包含 + if self.low <= klu.low: + self.add_klu(klu=klu) + # gn>gn-1 + if self.dir == Chan_KLINE_DIR.UP: + # UP -> max(dn) + self.low = klu.low + else: + # DOWN -> min(gn) + self.high = klu.high + #self.print(klu, "Z") + return True + # high大于,low大于,不包含 + else: + # if self.low > klu.low + # high相等,右包含 + if self.high == klu.high: + self.add_klu(klu=klu) + # UP -> max(gn) + if self.dir == Chan_KLINE_DIR.UP: + self.high = klu.high + else: + # DOWN -> min(dn) + self.low = klu.low + return True + else: + return False + else: + # high小于,low大于,右包含 + if self.low >= klu.low: + self.add_klu(klu=klu) + # gn>gn-1 + if self.dir == Chan_KLINE_DIR.UP: + # UP -> max(gn) + self.high = klu.high + else: + # DOWN -> min(dn) + self.low = klu.low + #self.print(klu, "Y") + return True + else: + # high小于,low小于,不包含 + return False + def set_fx(self, fx: Chan_FX_TYPE): + self.fx = fx + def 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) + def check_fx(self, k1, k2): + if k2.high > k1.high and k2.high > self.high: + return Chan_FX_TYPE.TOP + elif k2.low < k1.low and k2.low < self.low: + return Chan_FX_TYPE.BOTTOM + else: + return Chan_FX_TYPE.UNKNOWN + def set_bi_data(self, bi): + self.bi = bi + 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 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 + + # ===== 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'] = (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_macd_hist'] = klu_features['klu_macdhist'] + else: + features['klc_macd_hist'] = 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()) + + return features \ No newline at end of file diff --git a/ChanKLU.py b/ChanKLU.py new file mode 100644 index 0000000..237dae5 --- /dev/null +++ b/ChanKLU.py @@ -0,0 +1,40 @@ +class ChanKLU: + def __init__(self, time, open, high, low, close, volume): + # _time, _close, _open, _high, _low, _extra_info={} + self.kl_type = None + self.time = time + self.close = close + self.open = open + self.high = high + self.low = low + self.volume = volume + self.idx = 0 + self.index = 0 + self.macd = 0 + self.signal = 0 + self.macdhist = 0 + self.ma5 = 0 + self.ma10 = 0 + self.ma30 = 0 + self.ma250 = 0 + self.rsi = 0 + def set_idx(self, idx): + self.idx = idx + self.index = idx + 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_ma250'] = self.ma250 + features['klu_rsi'] = self.rsi + return features \ No newline at end of file diff --git a/ChanLun.py b/ChanLun.py new file mode 100644 index 0000000..8584705 --- /dev/null +++ b/ChanLun.py @@ -0,0 +1,2236 @@ +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 +from ChanKLU import ChanKLU +from ChanKLC import ChanKLC +from ChanBI import ChanBI +from ChanSBI import ChanSBI +from ChanSEG import ChanSEG +from ChanZS import ChanZS +from ChanBSP import ChanBSP +import talib.abstract as ta +import pandas as pd +import matplotlib.pyplot as plt +from matplotlib.dates import DateFormatter, date2num +import matplotlib.patches as patches +from technical.util import resample_to_interval +from decimal import Decimal +class ChanLun(): + timeframes = ["5m", "15m", "30m", "60m", "4h"] + times = { + "5m": 5, + "15m": 15, + "30m": 30, + "60m": 60, + "4h": 240 + } + time5 = 5 + time15 = 15 + time30 = 30 + time60 = 60 + time4h = 240 + + def create_all_data(self, dataframe, ticker_indicator): + all_data = dict() + all_data['1m'] = dataframe + for timeframe in self.timeframes: + df = resample_to_interval(dataframe, ticker_indicator*self.times[timeframe]) + all_data[timeframe] = df + return all_data + def print_zs(self, zs_list): + for zs in zs_list: + if zs.end_klc: + print(zs.start_klc.start_time, zs.end_klc.end_time, zs.sure_time, zs.zg, zs.zd, zs.bi_out_count) + else: + print(zs.start_klc.start_time, zs.zg, zs.zd, zs.bi_out_count) + def print_seg(self, seg_list): + for seg in seg_list: + if seg.is_sure: + print(seg.start_bi.start_time, seg.end_bi.end_time, seg.dir, seg.sure_time, "SEG") + else: + print(seg.start_bi.start_time, seg.dir, "SEG") + def print_bsp_list(self, bsp_list): + for bsp in bsp_list: + if bsp.is_sure: + print(bsp.klc.start_time, bsp.type, bsp.dir, bsp.sure_time, bsp.zs_count, len(bsp.zs.bi_out_list), bsp.dir, bsp.seg.dir, bsp.bi.dir) + else: + print(bsp.klc.start_time, bsp.type, bsp.dir, bsp.zs_count, len(bsp.zs.bi_out_list), bsp.dir, bsp.seg.dir, bsp.bi.dir) + def print_bi(self, bi_list): + for bi in bi_list: + if bi.end_klc: + if bi.sure_time: + print(bi.start_klc.end_time, bi.end_klc.end_time, bi.dir) + else: + print(bi.start_klc.end_time, bi.end_klc.end_time, bi.dir) + else: + print(bi.start_klc.end_time, bi.dir, bi.is_sure) + def check_fx(self, klc): + if klc.pre and klc.next: + 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 + if klc.pre and klc.next: + if 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 + return Chan_FX_TYPE.UNKNOWN + def get_macd(self, df): + fast = 8 + slow = 16 + period = 6 + macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period) + df['macd'] = macd['macd'] + df['macdsignal'] = macd['macdsignal'] + df['macdhist'] = macd['macdhist'] + return df + def plot_dataframe(self, 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 print_data(self, dataframe): + klc_list = self.get_klc_list(dataframe) + bi_list = self.cal_bi_list(klc_list) + seg_list = self.get_seg_list(bi_list) + bsp_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) + 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 = [] + for index in range(0, len(dataframe)): + klc = klc_list[klc_index] + if klc.end_klu and klc.end_klu.idx == index: + state_list.append(klc.state) + klc_index += 1 + else: + state_list.append("00") + return state_list + def get_bi_list(self, dataframe): + bi_list, klc_list = self.cal_bi_list(self.get_klc_list(dataframe)) + return bi_list + def calculate_zs(self, bi_list, seg_list): + return self.get_zs_list(bi_list, seg_list) + + def get_seg_list(self, bi_list): + seg_list = [] + up_bi_list = [] + down_bi_list = [] + last_up_bi = None + last_down_bi = None + last_up_sbi = None + last_down_sbi = None + last_seg = None + up_sbi_list = [] + down_sbi_list = [] + look_for_bottom = False + look_for_top = False + for bi in bi_list: + #print(len(up_sbi_list), len(down_sbi_list)) + if len(seg_list) > 0: + # Last seg is up + if last_seg.dir == Chan_SEG_DIR.UP: + if bi.dir == Chan_BI_DIR.DOWN: + if len(down_sbi_list) > 1: + # Check down sbi inclusion + included = last_down_sbi.check_bi_included(bi) + if not included: + down_sbi = ChanSBI(bi, len(down_sbi_list), bi.dir) + last_down_sbi.set_next(down_sbi) + last_down_sbi.set_end_bi(last_down_bi) + down_sbi.set_pre(last_down_sbi) + down_sbi_list.append(down_sbi) + fx = last_down_sbi.check_fx() + # Found top + if fx == Chan_FX_TYPE.TOP: + if look_for_top: + seg_list[-2].set_sure(bi) + look_for_top = False + #print(bi.start_time, look_for_top, "UP 1") + # Has gap and search for bottom fx + if last_down_sbi.has_fx_gap: + look_for_bottom = True + last_seg.pre_set_end_bi(bi_list[last_down_sbi.start_bi.index - 1]) + seg = ChanSEG(last_down_sbi.start_bi, len(seg_list), Chan_SEG_DIR.DOWN) + seg_list.append(seg) + last_seg.set_next(seg) + seg.set_pre(last_seg) + last_seg = seg + up_sbi_list = [] + last_up_sbi = ChanSBI(last_up_bi, len(up_sbi_list), last_up_bi.dir) + up_sbi_list.append(last_up_sbi) + #up_sbi_list.append(last_up_sbi) + #print(last_up_bi.start_time, last_up_sbi.start_bi.start_time, "Reset up sbi list 1") + #print(bi.start_time, look_for_top, "UP 2") + # No gap end SEG + else: + if look_for_bottom: + look_for_bottom = False + last_seg.set_start_bi(last_down_sbi.start_bi) + seg_list[-2].set_end_bi(bi_list[last_down_sbi.start_bi.index - 1], bi) + up_sbi_list = [] + last_up_sbi = ChanSBI(last_up_bi, len(up_sbi_list), last_up_bi.dir) + up_sbi_list.append(last_up_sbi) + #up_sbi_list.append(last_up_sbi) + #print(last_up_bi.start_time, last_up_sbi.start_bi.start_time, "Reset up sbi list 2") + #print(bi.start_time, look_for_top, "UP 3") + else: + last_seg.set_end_bi(bi_list[last_down_sbi.start_bi.index - 1], bi) + seg = ChanSEG(last_down_sbi.start_bi, len(seg_list), Chan_SEG_DIR.DOWN) + seg_list.append(seg) + last_seg.set_next(seg) + seg.set_pre(last_seg) + last_seg = seg + #print(last_down_sbi.end_bi.start_time, "Normal UP SEG", last_up_sbi.start_bi.start_time, bi.start_time) + #l_up_sbi = up_sbi_list[-1] + up_sbi_list = [] + last_up_sbi = ChanSBI(last_up_bi, len(up_sbi_list), last_up_bi.dir) + up_sbi_list.append(last_up_sbi) + #up_sbi_list.append(last_up_sbi) + #print(last_up_bi.start_time, last_up_sbi.start_bi.start_time, "Reset up sbi list 3") + last_down_sbi = down_sbi + else: + if len(down_sbi_list) == 1: + included = last_down_sbi.check_bi_included(bi) + if not included: + down_sbi = ChanSBI(bi, len(down_sbi_list), bi.dir) + last_down_sbi.set_next(down_sbi) + last_down_sbi.set_end_bi(last_down_bi) + down_sbi.set_pre(last_down_sbi) + down_sbi_list.append(down_sbi) + last_down_sbi = down_sbi + #print(bi.start_time, look_for_top, "UP 4") + else: + last_down_sbi = ChanSBI(bi, len(down_sbi_list), bi.dir) + down_sbi_list.append(last_down_sbi) + #print(bi.start_time, look_for_top, "UP 5") + else: + if last_up_sbi: + included = last_up_sbi.check_bi_included(bi) + if not included: + up_sbi = ChanSBI(bi, len(up_sbi_list), bi.dir) + last_up_sbi.set_next(up_sbi) + last_up_sbi.set_end_bi(last_up_bi) + up_sbi.set_pre(last_up_sbi) + up_sbi_list.append(up_sbi) + last_up_sbi = up_sbi + #print(bi.start_time, look_for_top, "UP 6") + # Last seg is down + else: + if bi.dir == Chan_BI_DIR.UP: + if len(up_sbi_list) > 1: + # Check down sbi inclusion + included = last_up_sbi.check_bi_included(bi) + if not included: + up_sbi = ChanSBI(bi, len(up_sbi_list), bi.dir) + last_up_sbi.set_next(up_sbi) + last_up_sbi.set_end_bi(last_up_bi) + up_sbi.set_pre(last_up_sbi) + up_sbi_list.append(up_sbi) + fx = last_up_sbi.check_fx() + # Found bottom + if fx == Chan_FX_TYPE.BOTTOM: + if look_for_bottom: + seg_list[-2].set_sure(bi) + look_for_bottom = False + #print(bi.start_time, look_for_top, "DOWN 1") + # Has gap and search for bottom fx + if last_up_sbi.has_fx_gap: + look_for_top = True + last_seg.pre_set_end_bi(bi_list[last_up_sbi.start_bi.index - 1]) + seg = ChanSEG(last_up_sbi.start_bi, len(seg_list), Chan_SEG_DIR.UP) + seg_list.append(seg) + last_seg.set_next(seg) + seg.set_pre(last_seg) + last_seg = seg + down_sbi_list = [] + last_down_sbi = ChanSBI(last_down_bi, len(down_sbi_list), last_down_bi.dir) + down_sbi_list.append(last_down_sbi) + #down_sbi_list.append(last_down_sbi) + #print(last_down_bi.start_time, last_down_sbi.start_bi.start_time, "Reset down sbi list 1") + #print(bi.start_time, look_for_top, "DOWN 2") + # No gap end SEG + else: + if look_for_top: + look_for_top = False + last_seg.set_start_bi(last_up_sbi.start_bi) + seg_list[-2].set_end_bi(bi_list[last_up_sbi.start_bi.index - 1], bi) + down_sbi_list = [] + last_down_sbi = ChanSBI(last_down_bi, len(down_sbi_list), last_down_bi.dir) + down_sbi_list.append(last_down_sbi) + #down_sbi_list.append(last_down_sbi) + #print(last_down_bi.start_time, last_down_sbi.start_bi.start_time, "Reset down sbi list 2") + #print(bi.start_time, look_for_top, "DOWN 3") + else: + last_seg.set_end_bi(bi_list[last_up_sbi.start_bi.index - 1], bi) + seg = ChanSEG(last_up_sbi.start_bi, len(seg_list), Chan_SEG_DIR.UP) + #print(last_up_sbi.start_bi.start_time) + last_seg.set_next(seg) + seg.set_pre(last_seg) + seg_list.append(seg) + last_seg = seg + #print(last_up_sbi.end_bi.start_time, "Normal DOWN SEG", last_down_sbi.start_bi.start_time, bi.start_time) + down_sbi_list = [] + last_down_sbi = ChanSBI(last_down_bi, len(down_sbi_list), last_down_bi.dir) + down_sbi_list.append(last_down_sbi) + #down_sbi_list.append(last_down_sbi) + #print(last_down_bi.start_time, last_down_sbi.start_bi.start_time, "Reset down sbi list 3") + last_up_sbi = up_sbi + else: + if len(up_sbi_list) == 1: + #last_up_sbi = up_sbi_list[-1] + included = last_up_sbi.check_bi_included(bi) + if not included: + up_sbi = ChanSBI(bi, len(up_sbi_list), bi.dir) + last_up_sbi.set_next(up_sbi) + last_up_sbi.set_end_bi(last_up_bi) + up_sbi.set_pre(last_up_sbi) + up_sbi_list.append(up_sbi) + last_up_sbi = up_sbi + #print(bi.start_time, look_for_top, "DOWN 4") + else: + last_up_sbi = ChanSBI(bi, len(up_sbi_list), bi.dir) + up_sbi_list.append(last_up_sbi) + #print(bi.start_time, look_for_top, "DOWN 5") + else: + if last_down_sbi: + included = last_down_sbi.check_bi_included(bi) + if not included: + down_sbi = ChanSBI(bi, len(down_sbi_list), bi.dir) + last_down_sbi.set_next(down_sbi) + last_down_sbi.set_end_bi(last_down_bi) + down_sbi.set_pre(last_down_sbi) + down_sbi_list.append(down_sbi) + last_down_sbi = down_sbi + #print(bi.start_time, look_for_top, look_for_bottom, "DOWN 6") + # len(seg_list) = 0 + else: + if bi.check_overlap(): + if bi.dir == Chan_BI_DIR.UP: + seg = ChanSEG(bi, len(seg_list), Chan_SEG_DIR.UP) + last_up_bi = bi + last_up_sbi = ChanSBI(bi, len(up_sbi_list), bi.dir) + seg_list.append(seg) + last_seg = seg + #print(bi.start_time, 'Create first UP SEG') + else: + seg = ChanSEG(bi, len(seg_list), Chan_SEG_DIR.DOWN) + last_down_bi = bi + last_down_sbi = ChanSBI(bi, len(down_sbi_list), bi.dir) + seg_list.append(seg) + last_seg = seg + #print(bi.start_time, 'Create first DOWN SEG') + if bi.dir == Chan_BI_DIR.UP: + last_up_bi = bi + up_bi_list.append(bi) + else: + last_down_bi = bi + down_bi_list.append(bi) + return seg_list + + def get_bi_zs_list(self, bi_list): + """识别笔中枢列表 + + 与线段中枢不同,笔中枢是由连续的同向笔构成,是更细粒度的中枢结构 + + Args: + bi_list: 笔列表 + + Returns: + bi_zs_list: 笔中枢列表 + """ + bi_zs_list = [] + if len(bi_list) < 3: # 至少需要3个笔才能形成中枢 + print("笔数量不足,无法形成中枢") + return bi_zs_list + + last_zs = None + first_bi_out = None + in_again = False + bi_out_count = 0 + + # 遍历所有笔,识别中枢 + for i in range(2, len(bi_list)): + # 确保当前笔和前两个笔都是完成的 + if not bi_list[i].end_klc or not bi_list[i-1].end_klc or not bi_list[i-2].end_klc: + continue + + current_bi = bi_list[i] + prev_bi = bi_list[i-1] + prev_prev_bi = bi_list[i-2] + + # 如果没有中枢或上一个中枢已完成 + if len(bi_zs_list) == 0 or (last_zs and last_zs.is_sure): + # 检查是否是三个连续同向笔 + if (current_bi.dir == prev_bi.dir == prev_prev_bi.dir): + # 创建潜在中枢 + if current_bi.dir == Chan_BI_DIR.UP: + # 向上的三笔区间定义中枢 + # 中枢的上沿:取三个笔的终点的最小值 + # 中枢的下沿:取三个笔的起点的最大值 + zd = max(prev_prev_bi.start_klc.low, prev_bi.start_klc.low, current_bi.start_klc.low) + zg = min(prev_prev_bi.end_klc.high, prev_bi.end_klc.high, current_bi.end_klc.high) + + # 确保中枢有效(上沿大于下沿) + if zg > zd: + print(f"发现向上笔中枢: 起始时间={prev_prev_bi.start_klc.start_time}, ZG={zg}, ZD={zd}") + zs = ChanZS(prev_prev_bi.start_klc, zg, zd) + zs.start_bi = prev_prev_bi + zs.start_idx = i-2 + zs.end_bi = current_bi + zs.end_idx = i + zs.end_klc = current_bi.end_klc + zs.type = "BI_ZS" + zs.direction = Chan_ZS_DIR.UP + zs.sure_time = None # 中枢尚未确认完成 + bi_zs_list.append(zs) + last_zs = zs + else: + # 向下的三笔区间定义中枢 + # 中枢的上沿:取三个笔的起点的最小值 + # 中枢的下沿:取三个笔的终点的最大值 + zg = min(prev_prev_bi.start_klc.high, prev_bi.start_klc.high, current_bi.start_klc.high) + zd = max(prev_prev_bi.end_klc.low, prev_bi.end_klc.low, current_bi.end_klc.low) + + # 确保中枢有效(上沿大于下沿) + if zg > zd: + print(f"发现向下笔中枢: 起始时间={prev_prev_bi.start_klc.start_time}, ZG={zg}, ZD={zd}") + zs = ChanZS(prev_prev_bi.start_klc, zg, zd) + zs.start_bi = prev_prev_bi + zs.start_idx = i-2 + zs.end_bi = current_bi + zs.end_idx = i + zs.end_klc = current_bi.end_klc + zs.type = "BI_ZS" + zs.direction = Chan_ZS_DIR.DOWN + zs.sure_time = None # 中枢尚未确认完成 + bi_zs_list.append(zs) + last_zs = zs + # 处理已有的未完成中枢 + elif last_zs and not last_zs.is_sure: + # 当前笔与中枢最后一笔方向相同,可能延伸中枢 + if current_bi.dir == prev_bi.dir: + if last_zs.direction == Chan_ZS_DIR.UP and current_bi.dir == Chan_BI_DIR.UP: + # 检查是否仍在中枢内:向上时终点高价在中枢区间内 + if current_bi.end_klc.high >= last_zs.zd and current_bi.end_klc.high <= last_zs.zg: + print(f"延伸向上笔中枢: 终点时间={current_bi.end_klc.end_time}") + # 延伸中枢 + last_zs.end_klc = current_bi.end_klc + last_zs.end_bi = current_bi + last_zs.end_idx = i + else: + # 笔离开中枢,记录第一个离开的笔 + if not first_bi_out: + first_bi_out = current_bi + bi_out_count += 1 + print(f"笔离开向上中枢: 时间={current_bi.end_klc.end_time}, 价格={current_bi.end_klc.high}, 中枢上沿={last_zs.zg}") + else: + if not in_again: + # 第二次离开,确认中枢完成 + print(f"确认向上笔中枢完成: 时间={current_bi.end_klc.end_time}") + last_zs.is_sure = True + last_zs.sure_bi = current_bi + last_zs.sure_time = current_bi.end_klc.end_time + elif last_zs.direction == Chan_ZS_DIR.DOWN and current_bi.dir == Chan_BI_DIR.DOWN: + # 检查是否仍在中枢内:向下时终点低价在中枢区间内 + if current_bi.end_klc.low <= last_zs.zg and current_bi.end_klc.low >= last_zs.zd: + print(f"延伸向下笔中枢: 终点时间={current_bi.end_klc.end_time}") + # 延伸中枢 + last_zs.end_klc = current_bi.end_klc + last_zs.end_bi = current_bi + last_zs.end_idx = i + else: + # 笔离开中枢,记录第一个离开的笔 + if not first_bi_out: + first_bi_out = current_bi + bi_out_count += 1 + print(f"笔离开向下中枢: 时间={current_bi.end_klc.end_time}, 价格={current_bi.end_klc.low}, 中枢下沿={last_zs.zd}") + else: + if not in_again: + # 第二次离开,确认中枢完成 + print(f"确认向下笔中枢完成: 时间={current_bi.end_klc.end_time}") + last_zs.is_sure = True + last_zs.sure_bi = current_bi + last_zs.sure_time = current_bi.end_klc.end_time + # 方向改变,判断是否破坏中枢 + else: + # 方向改变可能导致重新进入中枢或破坏中枢 + # 向上中枢被向下笔破坏:低点低于中枢下沿 + # 向下中枢被向上笔破坏:高点高于中枢上沿 + if (last_zs.direction == Chan_ZS_DIR.UP and current_bi.end_klc.low < last_zs.zd) or \ + (last_zs.direction == Chan_ZS_DIR.DOWN and current_bi.end_klc.high > last_zs.zg): + # 破坏中枢 + print(f"笔中枢被破坏: 方向={current_bi.dir}, 时间={current_bi.end_klc.end_time}") + last_zs.is_sure = True + last_zs.sure_bi = current_bi + last_zs.sure_time = current_bi.end_klc.end_time + elif first_bi_out: + # 重新进入中枢 + print(f"笔重新进入中枢: 时间={current_bi.end_klc.end_time}") + in_again = True + first_bi_out = None + # 延伸中枢 + last_zs.end_klc = current_bi.end_klc + last_zs.end_bi = current_bi + last_zs.end_idx = i + + # 打印识别结果 + print(f"笔中枢识别完成,共找到 {len(bi_zs_list)} 个笔中枢") + return bi_zs_list + + def cal_bi_list(self, klc_list): + bi_list = [] + last_top = None + last_bottom = None + for klc in klc_list: + fx = self.check_fx(klc) + # Do nothing + if fx == Chan_FX_TYPE.UNKNOWN: + klc.set_fx(Chan_FX_TYPE.UNKNOWN) + else: + if fx == Chan_FX_TYPE.TOP: + if last_top: + if last_bottom: + #print(klc.start_time, last_bottom.start_time, last_top.start_time) + if last_bottom.index < last_top.index: + # Second top lower to be second sell point + if last_top.high > klc.high: + klc.set_fx(Chan_FX_TYPE.TT) + klc.set_state("20") + #print(klc.start_time, klc.fx, "二类买卖点Sell 1") + else: + # A new top found + #last_top.set_fx(Chan_FX_TYPE.UNKNOWN) + last_top = klc + #print(klc.start_time, klc.fx, "一类买卖点Sell 1") + klc.set_state("10") + else: + # 不满足结合律的分型 + if last_bottom.index + 4 > klc.index: + if last_top.high > klc.high: + #print(klc.start_time, last_bottom.start_time, klc.fx, "中枢买卖点Sell 1") + klc.set_fx(Chan_FX_TYPE.UNKNOWN) + # New TOP Found replace last top + else: + if last_top.index + 4 < klc.index and len(bi_list) > 1: + pre_last_bi = bi_list[-2] + last_bi = bi_list[-1] + if pre_last_bi.is_sure and not last_bi.is_sure and pre_last_bi.dir == Chan_BI_DIR.UP: + pre_last_bi.update_bi(klc) + bi_list.remove(last_bi) + pre_last_bi.set_next(None) + last_top.set_fx(Chan_FX_TYPE.UNKNOWN) + last_top = klc + last_bottom = pre_last_bi.start_klc + #print(klc.start_time, last_bi.start_klc.start_time, "New TOP Found reset last bi") + klc.set_state("10") + else: + klc.set_fx(Chan_FX_TYPE.UNKNOWN) + #print(klc.start_time, last_bottom.start_time, klc.fx, "中枢买卖点Sell 2") + # 满足结合律 + else: + # New Temp TOP and last bottom confirmed ***** confirm last down bi(last bottom and last top) + last_bi = bi_list[-1] + if not last_bi.is_sure: + last_bi.set_end_klc(last_bottom, klc) + bi = ChanBI(last_bottom, len(bi_list), Chan_BI_DIR.UP) + last_bi.set_next(bi) + bi.set_pre(last_bi) + bi.add_klc(klc) + bi_list.append(bi) + last_top = klc + klc.set_state('30') + #print(klc.start_time, last_bottom.start_time, "Normal TOP Found, Confirm down bi 4") + # last bottom = None + else: + if last_top.high < klc.high: + last_bi = bi_list[-1] + last_bi.set_start_klc(klc, Chan_BI_DIR.DOWN) + #last_top.set_fx(Chan_FX_TYPE.UNKNOWN) + last_top = klc + else: + klc.set_fx(Chan_FX_TYPE.TT) + klc.set_state('20') + #print(klc.start_time, klc.fx, "二类买卖点Sell 2") + else: + if last_bottom: + # 不满足结合律的分型 + if last_bottom.index + 4 > klc.index: + klc.set_fx(Chan_FX_TYPE.UNKNOWN) + else: + # First temp top and last bottom confirmed + last_top = klc + # Last top = None, last bottom = None, create first down bi + else: + # First temp top + last_top = klc + bi = ChanBI(klc, len(bi_list), Chan_BI_DIR.DOWN) + bi_list.append(bi) + #print(klc.start_time, 'Create first top') + #klc.fx = Bottom ======================== + else: + if last_bottom: + if last_top: + # Bottom after top and find a new bottom + if last_top.index < last_bottom.index: + # Second bottom uppper to be second buy point and confirm last bi + if last_bottom.low < klc.low: + klc.set_fx(Chan_FX_TYPE.BB) + klc.set_state("-20") + #print(klc.start_time, klc.fx, "二类买卖点Buy 1") + else: + # A new bottom found + #last_bottom.set_fx(Chan_FX_TYPE.UNKNOWN) + last_bottom = klc + #print(klc.start_time, klc.fx, "一类买卖点Buy 1") + klc.set_state("-10") + else: + # 不满足结合律的分型 + if last_top.index + 4 > klc.index: + if last_bottom.low < klc.low: + klc.set_fx(Chan_FX_TYPE.UNKNOWN) + #klc.set_fx(Chan_FX_TYPE.BB) + #klc.set_state("-100") + #print(klc.start_time, klc.fx, "中枢买卖点Buy 1") + # Found new bottom + else: + if last_bottom.index + 4 < klc.index and len(bi_list) > 1: + pre_last_bi = bi_list[-2] + last_bi = bi_list[-1] + if pre_last_bi.is_sure and not last_bi.is_sure and pre_last_bi.dir == Chan_BI_DIR.DOWN: + pre_last_bi.update_bi(klc) + bi_list.remove(last_bi) + pre_last_bi.set_next(None) + last_bottom.set_fx(Chan_FX_TYPE.UNKNOWN) + last_bottom = klc + last_top = pre_last_bi.start_klc + #print(klc.start_time, last_bi.start_klc.start_time, "New BOTTOM Found reset last bi") + klc.set_state("-10") + else: + klc.set_fx(Chan_FX_TYPE.UNKNOWN) + #print(klc.start_time, klc.fx, "中枢买卖点Buy 2") + # 满足结合律的分型 + else: + # New Temp Bottom and last top confirmed ***** confirm last up bi(last bottom and last top) + last_bi = bi_list[-1] + if not last_bi.is_sure: + last_bi.set_end_klc(last_top, klc) + bi = ChanBI(last_top, len(bi_list), Chan_BI_DIR.DOWN) + last_bi.set_next(bi) + bi.set_pre(last_bi) + bi.add_klc(klc) + bi_list.append(bi) + last_bottom = klc + klc.set_state('-30') + #print(klc.start_time, last_top.start_time, "Normal Bottom Found, Confirm up bi 6") + # last_top = None + else: + if last_bottom.low > klc.low: + last_bi = bi_list[-1] + last_bi.set_start_klc(klc, Chan_BI_DIR.UP) + #last_bottom.set_fx(Chan_FX_TYPE.UNKNOWN) + last_bottom = klc + else: + klc.set_fx(Chan_FX_TYPE.BB) + klc.set_state('-20') + #print(klc.start_time, klc.fx, "二类买卖点Buy 2") + # last_bottom = None + else: + if last_top: + # 不满足结合律的分型 + if last_top.index + 4 > klc.index: + klc.set_fx(Chan_FX_TYPE.UNKNOWN) + else: + # First temp bottom and last top confirmed + last_bottom = klc + # Last top = None, last bottom = None, create first up bi + else: + # First temp bottom and no top yet + last_bottom = klc + bi = ChanBI(klc, len(bi_list), Chan_BI_DIR.UP) + bi_list.append(bi) + #print(klc.start_time, 'Create first bottom') + #print(klc.time, klc.fx, klc.state) + for klc in klc_list: + if klc.fx == Chan_FX_TYPE.TOP: + klc.state = "10" + #print(klc.time, klc.state) + if klc.fx == Chan_FX_TYPE.BOTTOM: + klc.state = "-10" + #print(klc.time, klc.state) + return bi_list + + def get_zs_list(self, bi_list, seg_list): + zs_list = [] + bsp_list = [] + if len(seg_list) > 3: + last_zs = None + first_bi_out = None + in_again = False + bi_out_count = 0 + zs_count = 0 + for seg in seg_list: + # No zs or Last ZS is completed + if len(zs_list) == 0 or (last_zs and last_zs.is_sure): + # Has three completed segments + if seg.next and seg.next.next: + if seg.next.next.is_sure: + zg = min(seg.high, seg.next.high, seg.next.next.high) + zd = max(seg.low, seg.next.low, seg.next.next.low) + ddir = Chan_ZS_DIR.UP + ddir = None + if last_zs: + if zg < last_zs.zd: + ddir = Chan_ZS_DIR.DOWN + else: + if zd > last_zs.zg: + ddir = Chan_ZS_DIR.UP + else: + ddir = None + else: + if seg.dir == Chan_SEG_DIR.UP: + ddir = Chan_ZS_DIR.DOWN + else: + ddir = Chan_ZS_DIR.UP + if (seg.dir == Chan_SEG_DIR.DOWN and ddir == Chan_ZS_DIR.DOWN) or (seg.dir == Chan_SEG_DIR.UP and ddir == Chan_ZS_DIR.UP): + ddir = None + if ddir and zg > zd: + # New ZS + zs = ChanZS(seg, len(zs_list), ddir) + zs.set_zg(zg) + zs.set_zd(zd) + if last_zs: + last_zs.set_next(zs) + zs.set_pre(last_zs) + zs_list.append(zs) + if last_zs and last_zs.dir == zs.dir: + zs_count += 1 + else: + zs_count = 1 + last_zs = zs + # Last ZS is not completed + else: + # Last ZS is not completed + if last_zs and not last_zs.is_sure: + if first_bi_out: + # SEG is not in ZS + if seg.is_sure: + if ((seg.low > last_zs.zg and seg.high > last_zs.zg) or (seg.high < last_zs.zd and seg.low < last_zs.zd)): + last_zs.set_end_klc(last_zs.last_bi_in.end_klc, seg.sure_time, bi_out_count, seg) + bi_out_count = 0 + #print(seg.start_bi.start_klc.start_time) + first_bi_out = None + # Last ZS is completed and look for new ZS + if seg.next and seg.next.next: + if seg.next.next.is_sure: + zg = min(seg.high, seg.next.high, seg.next.next.high) + zd = max(seg.low, seg.next.low, seg.next.next.low) + ddir = None + if last_zs: + if zg < last_zs.zd: + ddir = Chan_ZS_DIR.DOWN + else: + if zd > last_zs.zg: + ddir = Chan_ZS_DIR.UP + else: + ddir = None + else: + if seg.dir == Chan_SEG_DIR.UP: + ddir = Chan_ZS_DIR.DOWN + else: + ddir = Chan_ZS_DIR.UP + if (seg.dir == Chan_SEG_DIR.DOWN and ddir == Chan_ZS_DIR.DOWN) or (seg.dir == Chan_SEG_DIR.UP and ddir == Chan_ZS_DIR.UP): + ddir = None + if ddir and zg > zd: + # New ZS + zs = ChanZS(seg, len(zs_list), ddir) + zs.set_zg(zg) + zs.set_zd(zd) + last_zs.set_next(zs) + zs.set_pre(last_zs) + zs_list.append(zs) + if last_zs and last_zs.dir == zs.dir: + zs_count += 1 + else: + zs_count = 1 + last_zs = zs + # Last SEG is in ZS + else: + # SEG is inside ZS + if seg.end_bi: + for index in range(seg.start_bi.index, seg.end_bi.index+1): + 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, None) + last_zs.set_last_bi_in(None) + last_zs.set_end_seg(None) + first_bi_out = None + #print("Bi in again 3", 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, seg) + 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 + if (bi.dir == Chan_BI_DIR.UP and seg.dir == Chan_SEG_DIR.DOWN) or (bi.dir == Chan_BI_DIR.DOWN and seg.dir == Chan_SEG_DIR.UP): + bsp = ChanBSP(first_bi_out, len(bsp_list), Chan_BSP_TYPE.T3, Chan_BSP_DIR.BUY if first_bi_out.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, first_bi_out.sure_time, zs_count, zs, seg) + bsp_list.append(bsp) + #print("First bi out 3", first_bi_out.start_klc.start_time) + in_again = False + """" + if first_bi_out: + if seg.dir == Chan_SEG_DIR.UP and bi.dir == Chan_BI_DIR.UP: + #print(bi.start_klc.start_time, bi.high, seg.high) + if bi.high == seg.high: + bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.T3E, Chan_BSP_DIR.SELL if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.BUY, bi.sure_time, zs_count, zs, seg) + bsp_list.append(bsp) + else: + if seg.dir == Chan_SEG_DIR.DOWN and bi.dir == Chan_BI_DIR.DOWN: + if bi.low == seg.low: + bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.T3E, Chan_BSP_DIR.BUY if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, bi.sure_time, zs_count, zs, seg) + bsp_list.append(bsp) + """ + else: + # SEG in ZS and not out and find first bi out + if seg.end_bi: + for index in range(seg.start_bi.index, seg.end_bi.index+1): + 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, None) + last_zs.set_last_bi_in(None) + last_zs.set_end_seg(None) + first_bi_out = None + #print("Bi in again 4", 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, seg) + 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 + if (bi.dir == Chan_BI_DIR.UP and seg.dir == Chan_SEG_DIR.DOWN) or (bi.dir == Chan_BI_DIR.DOWN and seg.dir == Chan_SEG_DIR.UP): + bsp = ChanBSP(first_bi_out, len(bsp_list), Chan_BSP_TYPE.T3, Chan_BSP_DIR.BUY if first_bi_out.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, first_bi_out.sure_time, zs_count, zs, seg) + bsp_list.append(bsp) + #print("First bi out 4", first_bi_out.start_klc.start_time) + in_again = False + if first_bi_out: + if seg.dir == Chan_SEG_DIR.UP and bi.dir == Chan_BI_DIR.UP: + #print(bi.start_klc.start_time, bi.high, seg.high) + if bi.high == seg.high: + bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.T3E, Chan_BSP_DIR.SELL if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.BUY, bi.sure_time, zs_count, zs, seg) + bsp_list.append(bsp) + else: + if seg.dir == Chan_SEG_DIR.DOWN and bi.dir == Chan_BI_DIR.DOWN: + if bi.low == seg.low: + bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.T3E, Chan_BSP_DIR.BUY if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, bi.sure_time, zs_count, zs, seg) + bsp_list.append(bsp) + #self.print_zs(zs_list) + return zs_list + + def get_bi_macd_hist_list(self, bi_list, dataframe): + bi_macd_hist_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_macd_hist_list.append(abs(total_macd_hist)) + bi.set_macd_hist(total_macd_hist) + return bi_macd_hist_list, bi_list + + def get_seg_macd_hist_list(self, seg_list, dataframe): + seg_macd_hist_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_macd_hist_list.append(abs(total_macd_hist)) + seg.set_macd_hist(total_macd_hist) + return seg_macd_hist_list, seg_list + + def get_bi_macd_div_list(self, bi_list, dataframe): + bi_macd_div_list = [] + bi_macd_hist_list, bi_list = self.get_bi_macd_hist_list(bi_list, dataframe) + for index in range(2, len(bi_list)): + if bi_macd_hist_list[index-2] == 0: + bi_macd_div = 0.0 + if index > 3 and bi_macd_hist_list[index-4] > 0.0: + bi_macd_div = bi_macd_hist_list[index]/bi_macd_hist_list[index-4] + else: + bi_macd_div = bi_macd_hist_list[index]/bi_macd_hist_list[index-2] + if bi_macd_div < 0.01: + if index > 3 and bi_macd_hist_list[index-4] > 0.0: + bi_macd_div = bi_macd_hist_list[index]/bi_macd_hist_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_macd_hist_list[index]), self.get_decimal(bi_macd_hist_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_macd_hist_list, seg_list = self.get_seg_macd_hist_list(seg_list, dataframe) + for index in range(2, len(seg_list)): + if seg_macd_hist_list[index-2] == 0: + seg_macd_div = 0.0 + if index > 3 and seg_macd_hist_list[index-4] > 0.0: + seg_macd_div = seg_macd_hist_list[index]/seg_macd_hist_list[index - 4] + else: + seg_macd_div = seg_macd_hist_list[index]/seg_macd_hist_list[index - 2] + if seg_macd_div < 0.01: + if index > 3 and seg_macd_hist_list[index-4] > 0.0: + seg_macd_div = seg_macd_hist_list[index]/seg_macd_hist_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_macd_hist_list[index]), self.get_decimal(seg_macd_hist_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 get_klc_list(self, dataframe): + klu_list = self.get_klu_list(dataframe) + klc_list = [] + last_klu = None + for klu in klu_list: + if len(klc_list) > 0: + last_klc = klc_list[-1] + included = last_klc.check_klu_included(klu) + if not included: + 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_list.append(klc) + last_klc.set_next(klc) + klc.set_pre(last_klc) + last_klc.set_end_klu(last_klu) + klc.set_pre_fx() + else: + last_klc.add_klu(klu) + else: + ddir = Chan_KLINE_DIR.UP + if klu.open > klu.close: + ddir = Chan_KLINE_DIR.DOWN + klc = ChanKLC(klu, 0, ddir) + klc_list.append(klc) + last_klu = klu + 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 + + def get_kl_data(self, dataframe:DataFrame): + fields = "time,open,high,low,close,volume" + klu_list = [] + for i in range(0, len(dataframe)): + item = dataframe.iloc[i] + date = item['date'] + o = item['open'] + h = item['high'] + l = item['low'] + c = item['close'] + v = item['volume'] + #time_obj = date.fromtimestamp(date) + #date = date + timedelta(hours=8) + time_str = date.strftime('%Y-%m-%d %H:%M:%S') + item_data = [ + time_str, + o, + h, + l, + c, + v + ] + #klu = KLU(self.create_item_dict(item_data, GetColumnNameFromFieldList(fields))) + klu = ChanKLU(time_str, o, h, l, c, v) + klu.set_idx(i) + klu_list.append(klu) + klu.macd = item['macd'] + klu.signal = item['macdsignal'] + klu.macdhist = item['macdhist'] + klu.ma5 = item['ma5'] + klu.ma10 = item['ma10'] + klu.ma30 = item['ma30'] + klu.ma250 = item['ma250'] + klu.rsi = item['rsi'] + return klu_list + + def get_bsp_list1(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.get_zs_list(big_bi_list, big_seg_list) + big_bi_macd_div_list = self.get_bi_macd_div_list(big_bi_list, big_df) + big_seg_macd_div_list = self.get_seg_macd_div_list(big_seg_list, big_df) + big_bi_macd_hist_list = self.get_bi_macd_hist_list(big_bi_list, big_df) + big_seg_macd_hist_list = self.get_seg_macd_hist_list(big_seg_list, big_df) + 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: + if big_seg.end_bi.index - big_seg.start_bi.index > 1: + max_high = big_seg.start_bi.high + for bi_index in range(big_seg.start_bi.index + 2, big_seg.end_bi.index + 1): + bi = big_bi_list[bi_index] + #print("MACD DIV: ", big_bi_macd_hist_list[index]/big_bi_macd_hist_list[index - 2]) + if bi.is_sure and bi.dir == Chan_BI_DIR.UP: + if bi.high > max_high: + max_high = bi.high + if big_bi_macd_hist_list[bi_index - 2] > 0.0: + bi_macd_div = big_bi_macd_hist_list[bi_index]/big_bi_macd_hist_list[bi_index - 2] + if bi_macd_div < 0.01 and len(big_bi_list) - big_seg.start_bi.index > 4 and big_bi_macd_hist_list[bi_index - 4] > 0.0: + bi_macd_div = big_bi_macd_hist_list[bi_index]/big_bi_macd_hist_list[bi_index - 4] + else: + if len(big_bi_list) - big_seg.start_bi.index > 4 and big_bi_macd_hist_list[bi_index - 4] > 0.0: + bi_macd_div = big_bi_macd_hist_list[bi_index]/big_bi_macd_hist_list[bi_index - 4] + else: + bi_macd_div = 0.0 + macd_index = bi.end_klc.end_klu.index + if bi_macd_div < 0.8 and bi_macd_div > 0.01 and big_df['macd'][macd_index] > 0 and big_df['macdsignal'][macd_index] > 0: + print("UP SEG Possible BSP:", bi.start_klc.end_time, bi_macd_div) + else: + if big_seg.end_bi.index - big_seg.start_bi.index > 1: + max_low = big_seg.start_bi.low + for bi_index in range(big_seg.start_bi.index + 2, big_seg.end_bi.index + 1): + bi = big_bi_list[bi_index] + if bi.is_sure and bi.dir == Chan_BI_DIR.DOWN: + #print("DOWN: ", max_low, bi.low) + if bi.low < max_low: + max_low = bi.low + if big_bi_macd_hist_list[bi_index - 2] > 0.0: + bi_macd_div = big_bi_macd_hist_list[bi_index]/big_bi_macd_hist_list[bi_index - 2] + if bi_macd_div < 0.01 and len(big_bi_list) - big_seg.start_bi.index > 4 and big_bi_macd_hist_list[bi_index - 4] > 0.0: + bi_macd_div = big_bi_macd_hist_list[bi_index]/big_bi_macd_hist_list[bi_index - 4] + else: + if len(big_bi_list) - big_seg.start_bi.index > 4 and big_bi_macd_hist_list[bi_index - 4] > 0.0: + bi_macd_div = big_bi_macd_hist_list[bi_index]/big_bi_macd_hist_list[bi_index - 4] + else: + bi_macd_div = 0.0 + macd_index = bi.end_klc.end_klu.index + if bi_macd_div < 0.8 and bi_macd_div > 0.01 and big_df['macd'][macd_index] < 0 and big_df['macdsignal'][macd_index] < 0: + print("DOWN SEG Possible BSP:", bi.start_klc.end_time, bi_macd_div) + 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: + if len(big_bi_list) - big_seg.start_bi.index > 1: + max_high = big_seg.start_bi.high + for bi_index in range(big_seg.start_bi.index + 2, len(big_bi_list)): + bi = big_bi_list[bi_index] + #print("MACD DIV: ", big_bi_macd_hist_list[index]/big_bi_macd_hist_list[index - 2]) + if bi.is_sure and bi.dir == Chan_BI_DIR.UP: + if bi.high > max_high: + max_high = bi.high + if big_bi_macd_hist_list[bi_index - 2] > 0.0: + bi_macd_div = big_bi_macd_hist_list[bi_index]/big_bi_macd_hist_list[bi_index - 2] + if bi_macd_div < 0.01 and len(big_bi_list) - big_seg.start_bi.index > 4 and big_bi_macd_hist_list[bi_index - 4] > 0.0: + bi_macd_div = big_bi_macd_hist_list[bi_index]/big_bi_macd_hist_list[bi_index - 4] + else: + if len(big_bi_list) - big_seg.start_bi.index > 4 and big_bi_macd_hist_list[bi_index - 4] > 0.0: + bi_macd_div = big_bi_macd_hist_list[bi_index]/big_bi_macd_hist_list[bi_index - 4] + else: + bi_macd_div = 0.0 + macd_index = len(big_df) - 1 + if bi_macd_div < 0.8 and bi_macd_div > 0.01 and big_df['macd'][macd_index] > 0 and big_df['macdsignal'][macd_index] > 0: + print("UP SEG Possible BSP:", bi.start_klc.end_time, bi_macd_div) + else: + if len(big_bi_list) - big_seg.start_bi.index > 1: + max_low = big_seg.start_bi.low + for bi_index in range(big_seg.start_bi.index + 2, len(big_bi_list)): + bi = big_bi_list[bi_index] + #print("MACD DIV: ", big_bi_macd_hist_list[index]/big_bi_macd_hist_list[index - 2]) + if bi.is_sure and bi.dir == Chan_BI_DIR.DOWN: + if bi.low < max_low: + max_low = bi.low + if big_bi_macd_hist_list[bi_index - 2] > 0.0: + bi_macd_div = big_bi_macd_hist_list[bi_index]/big_bi_macd_hist_list[bi_index - 2] + if bi_macd_div < 0.01 and len(big_bi_list) - big_seg.start_bi.index > 4 and big_bi_macd_hist_list[bi_index - 4] > 0.0: + bi_macd_div = big_bi_macd_hist_list[bi_index]/big_bi_macd_hist_list[bi_index - 4] + else: + if len(big_bi_list) - big_seg.start_bi.index > 4 and big_bi_macd_hist_list[bi_index - 4] > 0.0: + bi_macd_div = big_bi_macd_hist_list[bi_index]/big_bi_macd_hist_list[bi_index - 4] + else: + bi_macd_div = 0.0 + macd_index = len(big_df) - 1 + if bi_macd_div < 0.8 and bi_macd_div > 0.01 and big_df['macd'][macd_index] < 0 and big_df['macdsignal'][macd_index] < 0: + print("DOWN SEG Possible BSP:", bi.start_klc.end_time, bi_macd_div) + + 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: + 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: + 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) + """ + # 绘制大周期买卖点 + 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: + 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: + 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) + """ + # 绘制小周期买卖点 + 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=8, adjust=False).mean() + exp2 = small_df['close'].ewm(span=16, adjust=False).mean() + macd = exp1 - exp2 + signal = macd.ewm(span=6, 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 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 diff --git a/ChanLun_Classifier.py b/ChanLun_Classifier.py new file mode 100644 index 0000000..89938da --- /dev/null +++ b/ChanLun_Classifier.py @@ -0,0 +1,435 @@ +import sys +import os +#sys.setrecursionlimit(1000000) #例如这里设置为一百万 +#sys.path.append(os.path.abspath("/freqtrade/user_data/Chan")) +sys.path.append(os.path.abspath("/Users/jack/Project/freqtrade/user_data/Chan")) +import numpy as np +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 +from ChanKLU import ChanKLU +from ChanKLC import ChanKLC +from ChanBI import ChanBI +from ChanSBI import ChanSBI +from ChanSEG import ChanSEG +from ChanZS import ChanZS +from ChanBSP import ChanBSP +import talib.abstract as ta +import pandas as pd +import matplotlib.pyplot as plt +from matplotlib.dates import DateFormatter, date2num +import matplotlib.patches as patches +from technical.util import resample_to_interval +from decimal import Decimal +from ChanLun import ChanLun +import xgboost as xgb +from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, classification_report + +class ChanLunClassifier: + def __init__(self, dataframe: DataFrame): + self.dataframe = dataframe + self.model = None + chan = ChanLun() + + def train_model(self, dataframe=None, data_file_path=None, model_file_path='chan_xgb_model.json', use_cv=False, custom_params=None, model_name=None): + """ + 使用dataframe前80%的数据训练XGBoost模型 + :param dataframe: 输入的DataFrame,如果为None则使用初始化时的dataframe + :param data_file_path: 特征数据保存路径,可选 + :param model_file_path: 模型保存路径 + :param use_cv: 是否使用交叉验证寻找最佳参数 + :param custom_params: 自定义模型参数 + :return: 训练好的模型 + """ + if dataframe is None: + dataframe = self.dataframe + + # 分割数据集,前80%用于训练 + train_size = int(len(dataframe) * 0.8) + train_df = dataframe.iloc[:train_size].copy() + + # 获取训练集特征和标签 + X_train, y_train = self.get_feature_data(train_df) + + if len(X_train) == 0: + print("没有提取到足够的特征数据进行训练") + return None + + # 保存特征数据(可选) + if data_file_path: + feature_df = pd.DataFrame(X_train) + feature_df['label'] = y_train + feature_df.to_csv(data_file_path, index=False) + #{'eta': 0.03, 'max_depth': 4, 'subsample': 0.8, 'colsample_bytree': 0.8, 'gamma': 0.1, 'min_child_weight': 3, 'alpha': 1, 'lambda': 3}, + # 默认XGBoost参数 + default_params = { + 'objective': 'binary:logistic', + 'max_depth': 4, + 'eta': 0.03, + 'subsample': 0.8, + 'colsample_bytree': 0.8, + 'eval_metric': 'auc', + 'gamma': 0.1, + 'min_child_weight': 3, + 'alpha': 1, # L1正则化 + 'lambda': 3, # L2正则化 + 'scale_pos_weight': 1 + } + + # 使用自定义参数覆盖默认参数 + if custom_params: + for key, value in custom_params.items(): + default_params[key] = value + + params = default_params + dtrain = xgb.DMatrix(X_train, label=y_train) + + # 如果使用交叉验证寻找最佳参数 + if use_cv: + from sklearn.model_selection import GridSearchCV, RandomizedSearchCV + from sklearn.metrics import make_scorer, accuracy_score, f1_score + import numpy as np + + # 转换为sklearn兼容格式 + xgb_model = xgb.XGBClassifier( + objective=params['objective'], + max_depth=params['max_depth'], + learning_rate=params['eta'], + subsample=params['subsample'], + colsample_bytree=params['colsample_bytree'], + gamma=params['gamma'], + min_child_weight=params['min_child_weight'], + reg_alpha=params['alpha'], + reg_lambda=params['lambda'], + scale_pos_weight=params['scale_pos_weight'], + use_label_encoder=False, + eval_metric='auc' + ) + + # 参数网格 + param_grid = { + 'max_depth': [3, 5, 7, 9], + 'learning_rate': [0.01, 0.05, 0.1, 0.2], + 'subsample': [0.6, 0.8, 1.0], + 'colsample_bytree': [0.6, 0.8, 1.0], + 'min_child_weight': [1, 3, 5], + 'gamma': [0, 0.1, 0.2], + 'n_estimators': [50, 100, 200] + } + + # 使用随机搜索寻找最佳参数(比网格搜索快) + random_search = RandomizedSearchCV( + estimator=xgb_model, + param_distributions=param_grid, + n_iter=10, # 随机尝试的参数组合数 + scoring=make_scorer(f1_score), + cv=5, + verbose=1, + n_jobs=-1, + random_state=42 + ) + + print("进行交叉验证参数搜索...") + random_search.fit(X_train, y_train) + + # 获取最佳参数 + best_params = random_search.best_params_ + print(f"最佳参数: {best_params}") + + # 使用最佳参数更新模型参数 + params['max_depth'] = best_params['max_depth'] + params['eta'] = best_params['learning_rate'] + params['subsample'] = best_params['subsample'] + params['colsample_bytree'] = best_params['colsample_bytree'] + params['min_child_weight'] = best_params['min_child_weight'] + params['gamma'] = best_params['gamma'] + num_round = best_params['n_estimators'] + + # 使用最佳参数训练最终模型 + self.model = xgb.train(params, dtrain, num_round) + else: + # 标准训练(不使用交叉验证) + # 使用早停机制避免过拟合 + # 分割训练集为训练和验证 + eval_size = int(len(X_train) * 0.2) + X_eval = X_train[-eval_size:] + y_eval = y_train[-eval_size:] + X_train_part = X_train[:-eval_size] + y_train_part = y_train[:-eval_size] + + dtrain_part = xgb.DMatrix(X_train_part, label=y_train_part) + deval = xgb.DMatrix(X_eval, label=y_eval) + + # 评估列表 + evallist = [(dtrain_part, 'train'), (deval, 'eval')] + + # 训练模型,使用早停 + num_round = 1000 # 设置较大的轮数,让早停机制决定何时停止 + self.model = xgb.train( + params, + dtrain_part, + num_round, + evallist, + early_stopping_rounds=50, # 50轮内评估指标无改善则停止 + verbose_eval=True + ) + + # 使用全部训练数据重新训练最终模型,使用最佳轮数 + # best_rounds = self.model.best_ntree_limit + # 兼容新版本的XGBoost + if hasattr(self.model, 'best_ntree_limit'): + best_rounds = self.model.best_ntree_limit + elif hasattr(self.model, 'best_iteration'): + best_rounds = self.model.best_iteration + elif hasattr(self.model, 'best_ntree_idx'): + best_rounds = self.model.best_ntree_idx + else: + # 如果都不存在,使用默认值 + best_rounds = num_round + print(f"最佳轮数: {best_rounds}") + + # 使用全部训练数据和最佳轮数训练最终模型 + self.model = xgb.train(params, dtrain, best_rounds) + + # 保存模型 + if model_file_path: + self.model.save_model(model_name + model_file_path) + + # 特征重要性分析 + if hasattr(self.model, 'get_score'): + importance = self.model.get_score(importance_type='gain') + print("\n特征重要性 (gain):") + for key, value in sorted(importance.items(), key=lambda x: x[1], reverse=True): + print(f"{key}: {value}") + + return self.model + def load_model(self, model_name=None, model_file_path='chan_xgb_model.json'): + if model_name: + self.model = xgb.Booster() + self.model.load_model(model_name + model_file_path) + else: + self.model = xgb.Booster() + self.model.load_model(model_file_path) + def find_best_params(self, dataframe=None): + """ + 寻找最佳参数组合 + :param dataframe: 输入的DataFrame,如果为None则使用初始化时的dataframe + :return: 最佳参数 + """ + # 不同参数组合 + param_combinations = [ + # 低学习率,深树 + {'eta': 0.01, 'max_depth': 8, 'subsample': 0.8, 'colsample_bytree': 0.8, 'gamma': 0, 'min_child_weight': 1}, + # 中等学习率,中等树深度 + {'eta': 0.05, 'max_depth': 5, 'subsample': 0.7, 'colsample_bytree': 0.7, 'gamma': 0.1, 'min_child_weight': 3}, + # 高学习率,浅树 + {'eta': 0.1, 'max_depth': 3, 'subsample': 0.6, 'colsample_bytree': 0.6, 'gamma': 0.2, 'min_child_weight': 5}, + # 正则化较强 best here + {'eta': 0.03, 'max_depth': 4, 'subsample': 0.8, 'colsample_bytree': 0.8, 'gamma': 0.1, 'min_child_weight': 3, 'alpha': 1, 'lambda': 3}, + # 正则化较弱 + {'eta': 0.08, 'max_depth': 6, 'subsample': 0.9, 'colsample_bytree': 0.9, 'gamma': 0, 'min_child_weight': 1, 'alpha': 0, 'lambda': 0.5}, + ] + + best_score = 0 + best_params = None + best_model = None + + for params in param_combinations: + print(f"\n尝试参数组合: {params}") + model = self.train_model(dataframe=dataframe, custom_params=params) + + # 分割数据集,后20%用于测试 + if dataframe is None: + dataframe = self.dataframe + + train_size = int(len(dataframe) * 0.8) + test_df = dataframe.iloc[train_size:].copy() + + # 获取测试集特征和标签 + X_test, y_test = self.get_validate_feature_data(test_df) + + if len(X_test) == 0: + print("没有提取到足够的测试特征数据") + continue + + # 预测 + dtest = xgb.DMatrix(X_test) + y_pred_prob = model.predict(dtest) + y_pred = [1 if p > 0.5 else 0 for p in y_pred_prob] + + # 计算F1分数 + f1 = f1_score(y_test, y_pred, zero_division=0) + print(f"F1分数: {f1:.4f}") + + if f1 > best_score: + best_score = f1 + best_params = params + best_model = model + + print(f"\n最佳参数组合 (F1={best_score:.4f}):") + print(best_params) + self.model = best_model + + return best_params + + def get_feature_data(self, dataframe): + """ + 从dataframe提取特征数据 + :param dataframe: 输入的DataFrame + :return: 特征矩阵X和标签y + """ + # 使用ChanLun获取bi_list + bi_list = self.chan.cal_bi_list(self.chan.get_klc_list(dataframe)) + klc_list = self.chan.get_klc_list(dataframe) + # 筛选方向为UP的bi的起始klc + feature_data = [] + labels = [] + + 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] + # 提取特征 + features = klc.get_feature_data() + + # 将特征转换为模型可用的格式 + feature_vec = [] + for key, value in features.items(): + if isinstance(value, (int, float)): + feature_vec.append(value) + else: + feature_vec.append(0) + + # 判断这个bi是否赚钱(这里简单定义为:如果bi的结束价格高于起始价格,则标记为1,否则为0) + # 这个标签定义可以根据实际需求修改 + if bi.start_klc.index == klc.index: + label = 1 + bi_index += 1 + else: + label = 0 + + feature_data.append(feature_vec) + labels.append(label) + print("Trainning data: ", klc_list[-1].start_time, klc_list[-1].fx) + return np.array(feature_data), np.array(labels) + def get_validate_feature_data(self, dataframe): + """ + 从dataframe提取特征数据 + :param dataframe: 输入的DataFrame + :return: 特征矩阵X和标签y + """ + # 使用ChanLun获取bi_list + bi_list = self.chan.cal_bi_list(self.chan.get_klc_list(dataframe)) + klc_list = self.chan.get_klc_list(dataframe) + # 筛选方向为UP的bi的起始klc + feature_data = [] + labels = [] + 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] + # 提取特征 + 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) + + if bi.start_klc.index == klc.index: + label = 1 + bi_index += 1 + else: + label = 0 + + feature_data.append(feature_vec) + labels.append(label) + + return np.array(feature_data), np.array(labels) + def validate_model(self, dataframe=None): + """ + 使用dataframe后20%的数据验证模型 + :param dataframe: 输入的DataFrame,如果为None则使用初始化时的dataframe + :return: 验证结果 + """ + if self.model is None: + print("模型尚未训练,请先调用train_model方法") + return None + + if dataframe is None: + dataframe = self.dataframe + + # 分割数据集,后20%用于测试 + train_size = int(len(dataframe) * 0.8) + test_df = dataframe.iloc[train_size:].copy() + + # 获取测试集特征和标签 + X_test, y_test = self.get_validate_feature_data(test_df) + + if len(X_test) == 0: + print("没有提取到足够的测试特征数据") + return None + + # 预测 + dtest = xgb.DMatrix(X_test) + y_pred_prob = self.model.predict(dtest) + y_pred = [1 if p > 0.5 else 0 for p in y_pred_prob] + + # 计算评估指标 + accuracy = accuracy_score(y_test, y_pred) + precision = precision_score(y_test, y_pred, zero_division=0) + recall = recall_score(y_test, y_pred, zero_division=0) + f1 = f1_score(y_test, y_pred, zero_division=0) + + # 打印评估报告 + print("模型评估结果:") + print(f"准确率: {accuracy:.4f}") + print(f"精确率: {precision:.4f}") + print(f"召回率: {recall:.4f}") + print(f"F1分数: {f1:.4f}") + print("\n分类报告:") + print(classification_report(y_test, y_pred, zero_division=0)) + + return { + 'accuracy': accuracy, + 'precision': precision, + 'recall': recall, + 'f1': f1, + 'y_test': y_test, + 'y_pred': y_pred, + 'y_pred_prob': y_pred_prob + } + + def predict(self, klc): + """ + 使用训练好的模型预测单个KLC + :param klc: 需要预测的ChanKLC对象 + :return: 预测结果(概率值) + """ + if self.model is None: + print("模型尚未训练,请先调用train_model方法") + return None + + # 提取特征 + 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.model.predict(dtest)[0] + + diff --git a/ChanSBI.py b/ChanSBI.py new file mode 100644 index 0000000..0f693c7 --- /dev/null +++ b/ChanSBI.py @@ -0,0 +1,78 @@ +import copy +from typing import Dict, Optional + +from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR +import ChanKLU +from ChanBI import ChanBI + +class ChanSBI(): + def __init__(self, start_bi: ChanBI, index, dir=Chan_BI_DIR.UP): + self.start_bi = start_bi + self.end_bi = None + self.index = index + self.dir = dir + self.high = start_bi.high + self.low = start_bi.low + self.pre = None + self.next = None + self.fx = Chan_FX_TYPE.UNKNOWN + self.bi_list = [] + self.bi_list.append(start_bi) + self.has_fx_gap = False + def set_fx(self, fx): + self.fx = fx + def set_end_bi(self, bi): + self.end_bi = bi + def set_pre(self, sbi): + self.pre = sbi + def set_next(self, sbi): + self.next = sbi + def add_bi(self, bi): + self.bi_list.append(bi) + def check_fx(self): + if self.pre and self.next: + #print(self.pre.start_bi.start_time, self.start_bi.start_time, self.end_bi.end_time, self.next.start_bi.start_time, self.pre.high, self.high, self.next.high, self.pre.low, self.low, self.next.low, self.dir) + if self.high > self.pre.high and self.high > self.next.high: + self.fx = Chan_FX_TYPE.TOP + #print(self.start_bi.start_time, self.pre.start_bi.start_time, self.next.start_bi.start_time, self.fx) + if self.low > self.pre.high: + self.has_fx_gap = True + #print(self.start_bi.start_time, self.end_bi.end_time, self.pre.start_bi.start_time, self.next.start_bi.start_time, self.dir, self.has_fx_gap, self.fx) + return Chan_FX_TYPE.TOP + else: + if self.low < self.pre.low and self.low < self.next.low: + self.fx = Chan_FX_TYPE.BOTTOM + #print(self.start_bi.start_time, self.pre.start_bi.start_time, self.next.start_bi.start_time, self.fx) + if self.high < self.pre.low: + self.has_fx_gap = True + #print(self.start_bi.start_time, self.end_bi.end_time, self.pre.start_bi.start_time, self.next.start_bi.start_time, self.dir, self.has_fx_gap, self.fx) + return Chan_FX_TYPE.BOTTOM + return Chan_FX_TYPE.UNKNOWN + def check_bi_included(self, bi): + included = False + if self.high > bi.high: + # high大于,low小于,左包含 + if self.low < bi.low: + included = True + # high大于,low大于,不包含 + else: + # if self.low > bi.low + # high相等,右包含 + included = False + else: + included = False + if included: + if self.pre: + if self.high > self.pre.high and self.low < self.pre.low: + included = True + if included: + self.add_bi(bi) + # gn>gn-1 + if self.dir == Chan_BI_DIR.DOWN: + # UP -> max(dn) + self.low = bi.low + else: + # DOWN -> min(gn) + self.high = bi.high + #self.print(bi, "Z") + return included \ No newline at end of file diff --git a/ChanSEG.py b/ChanSEG.py new file mode 100644 index 0000000..7cbd1cd --- /dev/null +++ b/ChanSEG.py @@ -0,0 +1,60 @@ +import copy +from typing import Dict, Optional + +from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_SEG_DIR +import ChanKLU +import ChanCTime +from ChanBI import ChanBI +class ChanSEG(): + def __init__(self, start_bi: ChanBI, index, ddir=Chan_SEG_DIR.UP): + self.start_bi = start_bi + self.end_bi = None + self.dir = ddir + self.low = 0 + self.high = 0 + if self.dir == Chan_SEG_DIR.UP and start_bi: + self.low = start_bi.low + else: + if start_bi: + self.high = start_bi.high + self.index = index + self.pre = None + self.next = None + self.bi_list = [] + self.bi_list.append(start_bi) + self.is_sure = False + self.sure_time = None + self.macd_hist = 0 + self.macd_div = 0 + def set_macd_hist(self, macd_hist): + self.macd_hist = macd_hist + def set_macd_div(self, macd_div): + self.macd_div = macd_div + def set_end_bi(self, bi: ChanBI, sure_bi: ChanBI): + self.end_bi = bi + if bi: + if self.dir == Chan_SEG_DIR.UP: + self.high = bi.high + else: + self.low = bi.low + self.is_sure = True + if sure_bi.is_sure: + self.sure_time = sure_bi.end_klc.end_time + def pre_set_end_bi(self, bi: ChanBI): + self.end_bi = bi + if bi: + if self.dir == Chan_SEG_DIR.UP: + self.high = bi.high + else: + self.low = bi.low + def set_pre(self, seg): + self.pre = seg + def set_next(self, seg): + self.next = seg + def set_sure(self, sure_bi): + if sure_bi.is_sure: + self.sure_time = sure_bi.end_klc.end_time + self.is_sure = True + def add_bi(self, bi: ChanBI): + if len(self.bi_list) > 1: + self.bi_list.append(bi) \ No newline at end of file diff --git a/ChanZS.py b/ChanZS.py new file mode 100644 index 0000000..ca577f8 --- /dev/null +++ b/ChanZS.py @@ -0,0 +1,66 @@ +from typing import Dict, Optional + +import ChanKLC, ChanSEG +import ChanCTime +from ChanEnum import Chan_ZS_DIR +# 中枢 +class ChanZS(): + def __init__(self, start_seg: ChanSEG, index, ddir: Chan_ZS_DIR): + self.start_klc = start_seg.start_bi.start_klc + self.start_time = self.start_klc.start_time + self.end_time = None + self.index = index + self.next = None + self.pre = None + self.start_seg = start_seg + self.seg_list = [] + self.seg_list.append(start_seg) + self.end_seg = None + self.last_bi_in = None + self.bi_out = None + self.is_sure = False + self.zg = 0 + self.zd = 0 + self.dir = ddir + self.sure_time = None + self.end_klc = None + self.bi_out_count = 0 + self.bi_out_list = [] + self.bi_out_seg_list = [] + self.bi_out_seg = None + def set_last_bi_in(self, last_bi_in): + self.last_bi_in = last_bi_in + def set_bi_out(self, bi_out, bi_out_seg): + if bi_out: + #print(bi_out.start_klc.start_time, bi_out.sure_time, bi_out.dir, bi_out_seg.dir, len(self.bi_out_list)) + if len(self.bi_out_list) > 0: + last_bi = self.bi_out_list[-1] + if last_bi.index != bi_out.index: + self.bi_out_list.append(bi_out) + self.bi_out_seg_list.append(bi_out_seg) + else: + self.bi_out_list.append(bi_out) + self.bi_out_seg_list.append(bi_out_seg) + self.bi_out = bi_out + self.bi_out_seg = bi_out_seg + def set_end_klc(self, end_klc, sure_time, bi_out_count, seg): + self.end_klc = end_klc + self.set_end_time(end_klc.end_time) + self.is_sure = True + self.sure_time = sure_time + self.bi_out_count = bi_out_count + self.end_seg = seg + def set_end_seg(self, end_seg): + self.end_seg = end_seg + def set_pre(self, pre): + self.pre = pre + def set_next(self, next): + self.next = next + def set_end_time(self, end_time): + self.end_time = end_time + def add_klc(self, klc): + self.klc_list.append(klc) + def set_zg(self, zg): + self.zg = zg + def set_zd(self, zd): + self.zd = zd \ No newline at end of file diff --git a/Find_Trend.py b/Find_Trend.py new file mode 100644 index 0000000..8d392a3 --- /dev/null +++ b/Find_Trend.py @@ -0,0 +1,448 @@ +import ccxt +import pandas as pd +import numpy as np +import mplfinance as mpf +from talib import MACD, SMA +from datetime import datetime, timedelta +import logging +import datetime as dt + +# Configure logging +logging.basicConfig( + filename='chanlun_trading.log', + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) + +# Configuration (user to modify) +BINANCE_API_KEY = 'your_api_key' # Replace with your Binance API key +BINANCE_API_SECRET = 'your_api_secret' # Replace with your Binance API secret +SIMULATION_MODE = True # Set to False for live trading + +# 1. Fetch K-line data from Binance (multi-timeframe support) +def fetch_binance_data(symbol='BTC/USDT', timeframe='5m', limit=500): + try: + exchange = ccxt.binance({ + 'apiKey': BINANCE_API_KEY if not SIMULATION_MODE else '', + 'secret': BINANCE_API_SECRET if not SIMULATION_MODE else '', + 'enableRateLimit': True, + 'options': {'defaultType': 'spot'} + }) + since = exchange.parse8601((datetime.now(dt.UTC) - timedelta(days=7)).isoformat()) + ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since, limit) + df = pd.DataFrame(ohlcv, columns=['Date', 'Open', 'High', 'Low', 'Close', 'Volume']) + df['Date'] = pd.to_datetime(df['Date'], unit='ms') + df.set_index('Date', inplace=True) + logging.info(f"Fetched {len(df)} K-lines for {symbol} ({timeframe})") + return df + except Exception as e: + logging.error(f"Failed to fetch data: {e}") + raise + +# 2. K-line merging (vectorized) +def merge_kline(df): + try: + df = df.copy() + merged_data = [] + trend = np.sign(df['Close'].diff().shift(-1)) # 1: up, -1: down, 0: neutral + + # Detect inclusion + is_included = ((df['High'].shift(-1) <= df['High']) & (df['Low'].shift(-1) >= df['Low'])) | \ + ((df['High'].shift(-1) >= df['High']) & (df['Low'].shift(-1) <= df['Low'])) + + i = 0 + while i < len(df) - 1: + if is_included.iloc[i]: + current_k = df.iloc[i] + next_k = df.iloc[i + 1] + high = max(current_k['High'], next_k['High']) + low = min(current_k['Low'], next_k['Low']) + open_price = current_k['Open'] + close_price = next_k['Close'] if trend.iloc[i] >= 0 else next_k['Close'] + volume = current_k['Volume'] + next_k['Volume'] + + merged_data.append({ + 'Date': next_k.name, + 'Open': open_price, + 'High': high, + 'Low': low, + 'Close': close_price, + 'Volume': volume + }) + i += 2 + else: + current_k = df.iloc[i] + merged_data.append({ + 'Date': current_k.name, + 'Open': current_k['Open'], + 'High': current_k['High'], + 'Low': current_k['Low'], + 'Close': current_k['Close'], + 'Volume': current_k['Volume'] + }) + i += 1 + + if i == len(df) - 1: + last_k = df.iloc[i] + merged_data.append({ + 'Date': last_k.name, + 'Open': last_k['Open'], + 'High': last_k['High'], + 'Low': last_k['Low'], + 'Close': last_k['Close'], + 'Volume': last_k['Volume'] + }) + + merged_df = pd.DataFrame(merged_data) + merged_df['Date'] = pd.to_datetime(merged_df['Date']) + merged_df.set_index('Date', inplace=True) + logging.info(f"Merged K-lines: {len(df)} -> {len(merged_df)}") + return merged_df + except Exception as e: + logging.error(f"K-line merging failed: {e}") + raise + +# 3. Detect fractals (vectorized) +def detect_fractals(df): + try: + df = df.copy() + df['is_top'] = (df['High'] > df['High'].shift(1)) & (df['High'] > df['High'].shift(-1)) & \ + (df['High'] > df['High'].shift(2)) & (df['High'] > df['High'].shift(-2)) + df['is_bottom'] = (df['Low'] < df['Low'].shift(1)) & (df['Low'] < df['Low'].shift(-1)) & \ + (df['Low'] < df['Low'].shift(2)) & (df['Low'] < df['Low'].shift(-2)) + df['is_top'] = df['is_top'].fillna(False) + df['is_bottom'] = df['is_bottom'].fillna(False) + logging.info(f"Detected {df['is_top'].sum()} top fractals and {df['is_bottom'].sum()} bottom fractals") + return df + except Exception as e: + logging.error(f"Fractal detection failed: {e}") + raise + +# 4. Detect strokes +def detect_strokes(df): + try: + strokes = [] + last_fractal = None + last_price = None + last_index = None + + for i in range(len(df)): + if df['is_top'].iloc[i] or df['is_bottom'].iloc[i]: + current_fractal = 'top' if df['is_top'].iloc[i] else 'bottom' + current_price = df['High'].iloc[i] if current_fractal == 'top' else df['Low'].iloc[i] + + if last_fractal is None: + last_fractal = current_fractal + last_price = current_price + last_index = df.index[i] + continue + + if (last_fractal == 'top' and current_fractal == 'bottom' and current_price < last_price) or \ + (last_fractal == 'bottom' and current_fractal == 'top' and current_price > last_price): + strokes.append({ + 'start_time': last_index, + 'end_time': df.index[i], + 'start_price': last_price, + 'end_price': current_price, + 'type': 'down' if current_fractal == 'bottom' else 'up', + 'volume': df['Volume'].loc[last_index:df.index[i]].sum() + }) + + last_fractal = current_fractal + last_price = current_price + last_index = df.index[i] + + logging.info(f"Detected {len(strokes)} strokes") + return strokes + except Exception as e: + logging.error(f"Stroke detection failed: {e}") + raise + +# 5. Detect segments +def detect_segments(strokes): + try: + segments = [] + if len(strokes) < 3: + return segments + + i = 0 + while i < len(strokes) - 2: + stroke1, stroke2, stroke3 = strokes[i], strokes[i+1], strokes[i+2] + + if stroke1['type'] == 'up' and stroke2['type'] == 'down' and stroke3['type'] == 'up': + if stroke3['end_price'] > stroke1['end_price']: + segments.append({ + 'start_time': stroke1['start_time'], + 'end_time': stroke3['end_time'], + 'start_price': stroke1['start_price'], + 'end_price': stroke3['end_price'], + 'type': 'up' + }) + i += 3 + else: + i += 1 + elif stroke1['type'] == 'down' and stroke2['type'] == 'up' and stroke3['type'] == 'down': + if stroke3['end_price'] < stroke1['end_price']: + segments.append({ + 'start_time': stroke1['start_time'], + 'end_time': stroke3['end_time'], + 'start_price': stroke1['start_price'], + 'end_price': stroke3['end_price'], + 'type': 'down' + }) + i += 3 + else: + i += 1 + else: + i += 1 + + logging.info(f"Detected {len(segments)} segments") + return segments + except Exception as e: + logging.error(f"Segment detection failed: {e}") + raise + +# 6. Detect pivots (midlines) +def detect_pivots(strokes): + try: + pivots = [] + if len(strokes) < 3: + return pivots + + for i in range(len(strokes) - 2): + s1, s2, s3 = strokes[i:i+3] + high = min(s1['start_price'], s1['end_price'], s2['start_price'], s2['end_price'], + s3['start_price'], s3['end_price']) + low = max(s1['start_price'], s1['end_price'], s2['start_price'], s2['end_price'], + s3['start_price'], s3['end_price']) + + if high > low: + pivots.append({ + 'start_time': s1['start_time'], + 'end_time': s3['end_time'], + 'high': high, + 'low': low + }) + + logging.info(f"Detected {len(pivots)} pivots") + return pivots + except Exception as e: + logging.error(f"Pivot detection failed: {e}") + raise + +# 7. Analyze higher timeframe (30m) +def analyze_higher_timeframe(df_30m): + try: + df_30m = detect_fractals(df_30m) + strokes_30m = detect_strokes(df_30m) + + if not strokes_30m: + return 'neutral' + + last_stroke = strokes_30m[-1] + logging.info(f"30m trend: {last_stroke['type']}") + return last_stroke['type'] + except Exception as e: + logging.error(f"Higher timeframe analysis failed: {e}") + raise + +# 8. Back-divergence detection (enhanced) +def detect_back_divergence(df, strokes, higher_trend): + try: + macd, signal, hist = MACD(df['Close'], fastperiod=12, slowperiod=26, signalperiod=9) + sma20 = SMA(df['Close'], timeperiod=20) + df['macd'] = macd + df['hist'] = hist + df['sma20'] = sma20 + df['buy_signal'] = False + df['sell_signal'] = False + + stroke_metrics = [] + for stroke in strokes: + start_idx = df.index.get_loc(stroke['start_time']) + end_idx = df.index.get_loc(stroke['end_time']) + hist_segment = df['hist'].iloc[start_idx:end_idx+1] + price_change = abs(stroke['end_price'] - stroke['start_price']) + hist_area = sum(abs(h) for h in hist_segment if not np.isnan(h)) + volume = stroke['volume'] + stroke_metrics.append({ + 'start_time': stroke['start_time'], + 'end_time': stroke['end_time'], + 'type': stroke['type'], + 'price_change': price_change, + 'hist_area': hist_area, + 'volume': volume + }) + + for i in range(2, len(stroke_metrics)): + current_stroke = stroke_metrics[i] + prev_stroke = stroke_metrics[i-2] + + if current_stroke['type'] != prev_stroke['type']: + continue + + current_end_idx = df.index.get_loc(current_stroke['end_time']) + + # Uptrend back-divergence (sell signal) + if current_stroke['type'] == 'up': + price_increase = df['High'].loc[current_stroke['end_time']] > df['High'].loc[prev_stroke['end_time']] + hist_decrease = current_stroke['hist_area'] < prev_stroke['hist_area'] + volume_decrease = current_stroke['volume'] < prev_stroke['volume'] + is_top_fractal = df['is_top'].loc[current_stroke['end_time']] + hist_positive = df['hist'].iloc[current_end_idx] > 0 or \ + (df['hist'].iloc[current_end_idx] < 0 and df['hist'].iloc[current_end_idx-1] > 0) + sma_trend = df['Close'].iloc[current_end_idx] > df['sma20'].iloc[current_end_idx] + trend_match = higher_trend in ['up', 'neutral'] + + if price_increase and hist_decrease and volume_decrease and is_top_fractal and \ + hist_positive and sma_trend and trend_match: + df.loc[df.index[current_end_idx], 'sell_signal'] = True + + # Downtrend back-divergence (buy signal) + elif current_stroke['type'] == 'down': + price_decrease = df['Low'].loc[current_stroke['end_time']] < df['Low'].loc[prev_stroke['end_time']] + hist_decrease = current_stroke['hist_area'] < prev_stroke['hist_area'] + volume_decrease = current_stroke['volume'] < prev_stroke['volume'] + is_bottom_fractal = df['is_bottom'].loc[current_stroke['end_time']] + hist_negative = df['hist'].iloc[current_end_idx] < 0 or \ + (df['hist'].iloc[current_end_idx] > 0 and df['hist'].iloc[current_end_idx-1] < 0) + sma_trend = df['Close'].iloc[current_end_idx] < df['sma20'].iloc[current_end_idx] + trend_match = higher_trend in ['down', 'neutral'] + + if price_decrease and hist_decrease and volume_decrease and is_bottom_fractal and \ + hist_negative and sma_trend and trend_match: + df.loc[df.index[current_end_idx], 'buy_signal'] = True + + logging.info(f"Detected {df['buy_signal'].sum()} buy signals and {df['sell_signal'].sum()} sell signals") + return df + except Exception as e: + logging.error(f"Back-divergence detection failed: {e}") + raise + +# 9. Execute trade +def execute_trade(exchange, symbol, signal, amount=0.001): + try: + if SIMULATION_MODE: + msg = f"[SIMULATION] {'Buy' if signal == 'buy' else 'Sell'} {amount} {symbol} at {datetime.now(dt.UTC)}" + print(msg) + logging.info(msg) + return + + if signal == 'buy': + order = exchange.create_market_buy_order(symbol, amount) + msg = f"Buy order executed: {order}" + print(msg) + logging.info(msg) + elif signal == 'sell': + order = exchange.create_market_sell_order(symbol, amount) + msg = f"Sell order executed: {order}" + print(msg) + logging.info(msg) + except Exception as e: + msg = f"Trade execution failed: {e}" + print(msg) + logging.error(msg) + +# 10. Plot chart +def plot_chart(df, strokes, segments, pivots): + try: + # Initialize additional plots + apds = [] + alines = [] # For line segments + + # Plot strokes as line segments + for stroke in strokes: + alines.append([(stroke['start_time'], stroke['start_price']), + (stroke['end_time'], stroke['end_price'])]) + + # Plot segments as line segments + for segment in segments: + alines.append([(segment['start_time'], segment['start_price']), + (segment['end_time'], segment['end_price'])]) + + # Plot pivots as horizontal lines + for pivot in pivots: + alines.append([(pivot['start_time'], pivot['high']), + (pivot['end_time'], pivot['high'])]) + alines.append([(pivot['start_time'], pivot['low']), + (pivot['end_time'], pivot['low'])]) + + # Add alines to plot (single color for simplicity, can customize) + if alines: + apds.append(mpf.make_addplot( + None, # No y-data needed for alines + alines=alines, + type='line', + color=['blue' if i < len(strokes) else 'purple' if i < len(strokes) + len(segments) else 'orange' + for i in range(len(alines))], + linestyle=['--' if i < len(strokes) else '-' if i < len(strokes) + len(segments) else ':' + for i in range(len(alines))] + )) + + # Plot buy/sell signals + buy_signals = df[df['buy_signal']]['Close'] + sell_signals = df[df['sell_signal']]['Close'] + apds.append(mpf.make_addplot(buy_signals, type='scatter', markersize=100, marker='^', color='green')) + apds.append(mpf.make_addplot(sell_signals, type='scatter', markersize=100, marker='v', color='red')) + + # Plot K-line chart + mpf.plot(df, type='candle', addplot=apds, title='Chanlun Advanced Analysis', style='yahoo') + logging.info("Chart plotted successfully") + except Exception as e: + logging.error(f"Chart plotting failed: {e}") + raise + +# 11. Main function +def main(): + try: + # Initialize exchange + exchange = ccxt.binance({ + 'apiKey': BINANCE_API_KEY if not SIMULATION_MODE else '', + 'secret': BINANCE_API_SECRET if not SIMULATION_MODE else '', + 'enableRateLimit': True, + 'options': {'defaultType': 'spot'} + }) + + # Fetch data + df_5m = fetch_binance_data(symbol='BTC/USDT', timeframe='5m', limit=500) + df_30m = fetch_binance_data(symbol='BTC/USDT', timeframe='30m', limit=200) + + # Merge 5m K-lines + df_5m = merge_kline(df_5m) + + # Detect fractals, strokes, segments, pivots + df_5m = detect_fractals(df_5m) + strokes = detect_strokes(df_5m) + segments = detect_segments(strokes) + pivots = detect_pivots(strokes) + + # Analyze 30m trend + higher_trend = analyze_higher_timeframe(df_30m) + print(f"30m Trend: {higher_trend}") + + # Detect back-divergence + df_5m = detect_back_divergence(df_5m, strokes, higher_trend) + + # Plot chart + plot_chart(df_5m, strokes, segments, pivots) + + # Output and execute trades + print("Buy Signals:") + buy_signals = df_5m[df_5m['buy_signal']][['Close']] + print(buy_signals) + for idx, row in buy_signals.iterrows(): + execute_trade(exchange, 'BTC/USDT', 'buy', amount=0.001) + + print("Sell Signals:") + sell_signals = df_5m[df_5m['sell_signal']][['Close']] + print(sell_signals) + for idx, row in sell_signals.iterrows(): + execute_trade(exchange, 'BTC/USDT', 'sell', amount=0.001) + + logging.info("Main function completed successfully") + except Exception as e: + logging.error(f"Main function failed: {e}") + raise + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/__pycache__/BI.cpython-312.pyc b/__pycache__/BI.cpython-312.pyc new file mode 100644 index 0000000..a7ad2cf Binary files /dev/null and b/__pycache__/BI.cpython-312.pyc differ diff --git a/__pycache__/CTime.cpython-312.pyc b/__pycache__/CTime.cpython-312.pyc new file mode 100644 index 0000000..bb87c58 Binary files /dev/null and b/__pycache__/CTime.cpython-312.pyc differ diff --git a/__pycache__/ChanBI.cpython-312.pyc b/__pycache__/ChanBI.cpython-312.pyc new file mode 100644 index 0000000..52a365c Binary files /dev/null and b/__pycache__/ChanBI.cpython-312.pyc differ diff --git a/__pycache__/ChanBIZS.cpython-312.pyc b/__pycache__/ChanBIZS.cpython-312.pyc new file mode 100644 index 0000000..4c226f2 Binary files /dev/null and b/__pycache__/ChanBIZS.cpython-312.pyc differ diff --git a/__pycache__/ChanBSP.cpython-312.pyc b/__pycache__/ChanBSP.cpython-312.pyc new file mode 100644 index 0000000..b0fd3e6 Binary files /dev/null and b/__pycache__/ChanBSP.cpython-312.pyc differ diff --git a/__pycache__/ChanCTime.cpython-312.pyc b/__pycache__/ChanCTime.cpython-312.pyc new file mode 100644 index 0000000..b9efe3f Binary files /dev/null and b/__pycache__/ChanCTime.cpython-312.pyc differ diff --git a/__pycache__/ChanEnum.cpython-312.pyc b/__pycache__/ChanEnum.cpython-312.pyc new file mode 100644 index 0000000..4a77e10 Binary files /dev/null and b/__pycache__/ChanEnum.cpython-312.pyc differ diff --git a/__pycache__/ChanKLC.cpython-312.pyc b/__pycache__/ChanKLC.cpython-312.pyc new file mode 100644 index 0000000..2a7b85d Binary files /dev/null and b/__pycache__/ChanKLC.cpython-312.pyc differ diff --git a/__pycache__/ChanKLU.cpython-312.pyc b/__pycache__/ChanKLU.cpython-312.pyc new file mode 100644 index 0000000..9037ccf Binary files /dev/null and b/__pycache__/ChanKLU.cpython-312.pyc differ diff --git a/__pycache__/ChanLun.cpython-312.pyc b/__pycache__/ChanLun.cpython-312.pyc new file mode 100644 index 0000000..5844d14 Binary files /dev/null and b/__pycache__/ChanLun.cpython-312.pyc differ diff --git a/__pycache__/ChanLun_Classifier.cpython-312.pyc b/__pycache__/ChanLun_Classifier.cpython-312.pyc new file mode 100644 index 0000000..5755a5f Binary files /dev/null and b/__pycache__/ChanLun_Classifier.cpython-312.pyc differ diff --git a/__pycache__/ChanSBI.cpython-312.pyc b/__pycache__/ChanSBI.cpython-312.pyc new file mode 100644 index 0000000..143d523 Binary files /dev/null and b/__pycache__/ChanSBI.cpython-312.pyc differ diff --git a/__pycache__/ChanSEG.cpython-312.pyc b/__pycache__/ChanSEG.cpython-312.pyc new file mode 100644 index 0000000..f284a91 Binary files /dev/null and b/__pycache__/ChanSEG.cpython-312.pyc differ diff --git a/__pycache__/ChanZS.cpython-312.pyc b/__pycache__/ChanZS.cpython-312.pyc new file mode 100644 index 0000000..de2cfbc Binary files /dev/null and b/__pycache__/ChanZS.cpython-312.pyc differ diff --git a/__pycache__/Enum.cpython-312.pyc b/__pycache__/Enum.cpython-312.pyc new file mode 100644 index 0000000..eeca940 Binary files /dev/null and b/__pycache__/Enum.cpython-312.pyc differ diff --git a/__pycache__/KLC.cpython-312.pyc b/__pycache__/KLC.cpython-312.pyc new file mode 100644 index 0000000..445115e Binary files /dev/null and b/__pycache__/KLC.cpython-312.pyc differ diff --git a/__pycache__/KLU.cpython-312.pyc b/__pycache__/KLU.cpython-312.pyc new file mode 100644 index 0000000..f99936d Binary files /dev/null and b/__pycache__/KLU.cpython-312.pyc differ diff --git a/__pycache__/ZS.cpython-312.pyc b/__pycache__/ZS.cpython-312.pyc new file mode 100644 index 0000000..b1c00b6 Binary files /dev/null and b/__pycache__/ZS.cpython-312.pyc differ diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..1b26654 Binary files /dev/null and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/chanlun.txt b/chanlun.txt new file mode 100644 index 0000000..dc190b6 --- /dev/null +++ b/chanlun.txt @@ -0,0 +1,13 @@ +1. **第一类买卖点**: + - 定义:趋势反转的起始点,即在下跌趋势结束时形成的买点(第一类买点),或在上涨趋势结束时形成的卖点(第一类卖点)。这是市场多空力量发生根本性转变的位置。 + - 与MACD背驰的关系:Macd背驰是指出中枢后形成的Macd的红绿柱面积比进入中枢时的面积绝对值小,背驰比较的黄白线和柱子面积都在0轴的一个方向上。第一类买点都是在0轴之下背驰形成的,第一类卖点都是在0轴之上的背驰形成的。 + +2. **第二类买卖点**: + - 定义:趋势确认后的回调点。在第一类买卖点之后,价格会回调或反弹,形成第二类买点(回调不破前低)或第二类卖点(反弹不破前高),是对第一类买卖点的确认。第二类买点都是第一次上0轴后回抽确认形成的。第二类卖点都是第一次0轴之下上涨确认形成的。第二类买卖点只会在趋势确认后,第一类买卖点出现之后出现一次,不会重复出现,除非趋势反转之后。 + +3. **第三类买卖点**: + - 定义:趋势延续的确认点。价格突破回调或反弹的中枢区间后,回踩不破关键位置(如中枢上沿或下沿),形成第三类买点(上升趋势延续)或第三类卖点(下降趋势延续)。第三类买卖点只会在中枢确认之后出现。 + + + +我们交易的是币安的比特币合约, 数据格式是json, 数据包括现有的持仓, 仓位历史, 账户余额, 你用缠论分析之后, 给出以下分析, 最近的一个中枢在哪里,现在的趋势是什么,现在是否是买卖点,如果是,是那一类买卖点,应该进行何种操作。 \ No newline at end of file diff --git a/chanlun_trading.log b/chanlun_trading.log new file mode 100644 index 0000000..250079b --- /dev/null +++ b/chanlun_trading.log @@ -0,0 +1,39 @@ +2025-04-18 20:25:03,767 - INFO - Fetched 500 K-lines for BTC/USDT (5m) +2025-04-18 20:25:06,074 - INFO - Fetched 200 K-lines for BTC/USDT (30m) +2025-04-18 20:25:06,104 - INFO - Merged K-lines: 500 -> 404 +2025-04-18 20:25:06,105 - INFO - Detected 49 top fractals and 48 bottom fractals +2025-04-18 20:25:06,111 - INFO - Detected 80 strokes +2025-04-18 20:25:06,111 - INFO - Detected 22 segments +2025-04-18 20:25:06,111 - INFO - Detected 0 pivots +2025-04-18 20:25:06,112 - INFO - Detected 27 top fractals and 29 bottom fractals +2025-04-18 20:25:06,115 - INFO - Detected 45 strokes +2025-04-18 20:25:06,115 - INFO - 30m trend: down +2025-04-18 20:25:06,119 - INFO - Detected 4 buy signals and 0 sell signals +2025-04-18 20:25:06,233 - ERROR - Chart plotting failed: x and y must have same first dimension, but have shapes (404,) and (2,) +2025-04-18 20:25:06,233 - ERROR - Main function failed: x and y must have same first dimension, but have shapes (404,) and (2,) +2025-04-18 20:27:05,455 - INFO - Fetched 500 K-lines for BTC/USDT (5m) +2025-04-18 20:27:08,491 - INFO - Fetched 200 K-lines for BTC/USDT (30m) +2025-04-18 20:27:08,521 - INFO - Merged K-lines: 500 -> 404 +2025-04-18 20:27:08,522 - INFO - Detected 49 top fractals and 48 bottom fractals +2025-04-18 20:27:08,528 - INFO - Detected 80 strokes +2025-04-18 20:27:08,528 - INFO - Detected 22 segments +2025-04-18 20:27:08,528 - INFO - Detected 0 pivots +2025-04-18 20:27:08,529 - INFO - Detected 27 top fractals and 29 bottom fractals +2025-04-18 20:27:08,532 - INFO - Detected 45 strokes +2025-04-18 20:27:08,532 - INFO - 30m trend: down +2025-04-18 20:27:08,537 - INFO - Detected 4 buy signals and 0 sell signals +2025-04-18 20:27:08,647 - ERROR - Chart plotting failed: x and y must have same first dimension, but have shapes (404,) and (2,) +2025-04-18 20:27:08,647 - ERROR - Main function failed: x and y must have same first dimension, but have shapes (404,) and (2,) +2025-04-18 20:28:51,970 - INFO - Fetched 500 K-lines for BTC/USDT (5m) +2025-04-18 20:28:53,861 - INFO - Fetched 200 K-lines for BTC/USDT (30m) +2025-04-18 20:28:53,897 - INFO - Merged K-lines: 500 -> 404 +2025-04-18 20:28:53,898 - INFO - Detected 49 top fractals and 48 bottom fractals +2025-04-18 20:28:53,904 - INFO - Detected 80 strokes +2025-04-18 20:28:53,904 - INFO - Detected 22 segments +2025-04-18 20:28:53,904 - INFO - Detected 0 pivots +2025-04-18 20:28:53,905 - INFO - Detected 27 top fractals and 29 bottom fractals +2025-04-18 20:28:53,908 - INFO - Detected 45 strokes +2025-04-18 20:28:53,908 - INFO - 30m trend: down +2025-04-18 20:28:53,913 - INFO - Detected 4 buy signals and 0 sell signals +2025-04-18 20:28:53,913 - ERROR - Chart plotting failed: Wrong type for data, in make_addplot() +2025-04-18 20:28:53,913 - ERROR - Main function failed: Wrong type for data, in make_addplot() diff --git a/web/.DS_Store b/web/.DS_Store new file mode 100644 index 0000000..823233e Binary files /dev/null and b/web/.DS_Store differ diff --git a/web/README.txt b/web/README.txt new file mode 100644 index 0000000..d934441 --- /dev/null +++ b/web/README.txt @@ -0,0 +1,2 @@ +pip install -r requirements.txt +python app.py \ No newline at end of file diff --git a/web/app.py b/web/app.py new file mode 100644 index 0000000..af86b4a --- /dev/null +++ b/web/app.py @@ -0,0 +1,539 @@ +from flask import Flask, render_template, jsonify, request +import ccxt +import pandas as pd +from datetime import datetime, timedelta +import sys +import os +import matplotlib +matplotlib.use('Agg') # 设置使用非GUI后端,必须在导入pyplot之前设置 +import matplotlib.pyplot as plt +import io +import base64 +import time +import traceback +from pytz import timezone + +# 添加父目录到系统路径 +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from ChanLun import ChanLun +from ChanEnum import Chan_BI_DIR, Chan_SEG_DIR + +# 添加买卖点枚举类型 +class TRADE_POINT_TYPE: + BUY1 = 1 # 一类买点 + BUY2 = 2 # 二类买点 + BUY3 = 3 # 三类买点 + SELL1 = -1 # 一类卖点 + SELL2 = -2 # 二类卖点 + SELL3 = -3 # 三类卖点 + +app = Flask(__name__) + +# 初始化交易所 +exchange = ccxt.binance({ + 'enableRateLimit': True, +}) + +# 时间周期映射 +TIMEFRAMES = { + '1m': '1分钟', + '5m': '5分钟', + '15m': '15分钟', + '30m': '30分钟', + '1h': '1小时', + '4h': '4小时', + '1d': '日线', + '1w': '周线', + '1M': '月线', +} + +# 常见交易对 +SYMBOLS = [ + 'SOL/USDT:USDT', 'BTC/USDT:USDT', 'ETH/USDT:USDT', 'BNB/USDT:USDT', 'XRP/USDT:USDT', + 'ADA/USDT:USDT', 'DOGE/USDT:USDT', 'AVAX/USDT:USDT', 'DOT/USDT:USDT', 'MATIC/USDT:USDT' +] + +def get_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=None): + """获取K线数据,支持分页加载确保获取指定时间范围内的所有数据""" + try: + # 初始化参数 + since = None + if start_time: + try: + since = int(start_time) + except ValueError: + print(f"无效的起始时间: {start_time}") + + # 结束时间处理 + until = None + if end_time: + try: + until = int(end_time) + except ValueError: + print(f"无效的结束时间: {end_time}") + + # 初始化存储所有K线数据的列表 + all_ohlcv = [] + + # 初始化当前查询的开始时间 + current_since = since + + # 分页加载数据 + while True: + print(f"获取数据: {symbol}, {timeframe}, limit={limit}, since={current_since}") + + # 获取当前页的数据 + ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=current_since, limit=limit) + + # 如果没有获取到数据,结束循环 + if not ohlcv or len(ohlcv) == 0: + break + + # 将获取到的数据添加到总列表中 + all_ohlcv.extend(ohlcv) + + # 获取最后一条数据的时间戳 + last_timestamp = ohlcv[-1][0] + + # 如果已达到结束时间,结束循环 + if until and last_timestamp >= until: + break + + # 如果获取的数据条数小于限制数,说明已经获取完所有数据 + if len(ohlcv) < limit: + break + + # 更新下一页的开始时间(加1毫秒避免重复) + current_since = last_timestamp + 1 + + # 防止API请求过于频繁 + time.sleep(0.5) # 等待0.5秒 + + # 数据为空的情况 + if not all_ohlcv or len(all_ohlcv) == 0: + print(f"未获取到数据: {symbol}, {timeframe}") + return None + + # 转换为DataFrame + df = pd.DataFrame(all_ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']) + df['date'] = pd.to_datetime(df['timestamp'], unit='ms').dt.tz_localize('UTC').dt.tz_convert('Asia/Shanghai') + + # 在客户端进行结束时间过滤 + if until: + df = df[df['timestamp'] <= until] + + # 去除重复数据 + df = df.drop_duplicates(subset=['timestamp']) + + # 按时间排序 + df = df.sort_values('timestamp') + + # 如果过滤后没有数据,返回None + if len(df) == 0: + print("过滤后无数据") + return None + + print(f"获取到总共 {len(df)} 条数据") + return df + + except Exception as e: + print(f"获取数据错误: {e}") + traceback.print_exc() + return None + +def calculate_macd(df): + """计算MACD指标""" + exp1 = df['close'].ewm(span=12, adjust=False).mean() + exp2 = df['close'].ewm(span=26, adjust=False).mean() + macd = exp1 - exp2 + signal = macd.ewm(span=9, adjust=False).mean() + histogram = macd - signal + + return { + 'macd': macd.tolist(), + 'signal': signal.tolist(), + 'histogram': histogram.tolist() + } + +def analyze_chan(df): + """进行缠论分析""" + chan = ChanLun() + + # 获取分析结果 + klc_list = chan.get_klc_list(df) + bi_list = chan.cal_bi_list(klc_list) + seg_list = chan.get_seg_list(bi_list) + zs_list = chan.calculate_zs(bi_list, seg_list) + + # 获取笔中枢列表 + bi_zs_list = chan.get_bi_zs_list(bi_list) + + # 添加买卖点识别 + buy_sell_points = identify_trade_points(bi_list, seg_list, zs_list) + + return { + 'klc_list': klc_list, + 'bi_list': bi_list, + 'seg_list': seg_list, + 'zs_list': zs_list, + 'bi_zs_list': bi_zs_list, # 添加笔中枢数据 + 'trade_points': buy_sell_points + } + +def identify_trade_points(bi_list, seg_list, zs_list): + """识别缠论买卖点 - 只保留最重要的一类买卖点,减少标记干扰""" + trade_points = [] + + # 输出调试信息 + print(f"识别买卖点:总共 {len(bi_list)} 个笔, {len(seg_list)} 个线段, {len(zs_list)} 个中枢") + + # 只识别一类买卖点:线段向上或向下突破 + if len(seg_list) >= 3: + for i in range(2, len(seg_list)): + # 确保线段已完成 + if seg_list[i].end_bi and seg_list[i-1].end_bi and seg_list[i-2].end_bi: + # 一类买点:向下-向上-向下的底分型,第三段结束点为买点 + if (convert_direction(seg_list[i-2].dir) == -1 and + convert_direction(seg_list[i-1].dir) == 1 and + convert_direction(seg_list[i].dir) == -1): + print(f"发现一类买点:线段方向 {convert_direction(seg_list[i-2].dir)}-{convert_direction(seg_list[i-1].dir)}-{convert_direction(seg_list[i].dir)}") + trade_points.append({ + 'type': TRADE_POINT_TYPE.BUY1, + 'time': seg_list[i].end_bi.end_klc.end_time, + 'price': seg_list[i].end_bi.end_klc.low, + 'desc': '一类买点' + }) + + # 一类卖点:向上-向下-向上的顶分型,第三段结束点为卖点 + if (convert_direction(seg_list[i-2].dir) == 1 and + convert_direction(seg_list[i-1].dir) == -1 and + convert_direction(seg_list[i].dir) == 1): + print(f"发现一类卖点:线段方向 {convert_direction(seg_list[i-2].dir)}-{convert_direction(seg_list[i-1].dir)}-{convert_direction(seg_list[i].dir)}") + trade_points.append({ + 'type': TRADE_POINT_TYPE.SELL1, + 'time': seg_list[i].end_bi.end_klc.end_time, + 'price': seg_list[i].end_bi.end_klc.high, + 'desc': '一类卖点' + }) + + print(f"总共识别出 {len(trade_points)} 个买卖点") + return trade_points + +# 辅助函数,转换缠论方向枚举为整数 +def convert_direction(direction): + """将缠论方向枚举转换为整数""" + if direction == Chan_BI_DIR.UP: + return 1 + elif direction == Chan_BI_DIR.DOWN: + return -1 + elif direction == Chan_SEG_DIR.UP: + return 1 + elif direction == Chan_SEG_DIR.DOWN: + return -1 + else: + return 0 + +def format_time_safely(time_obj, client_tz): + """安全地格式化时间对象,处理字符串和datetime两种情况""" + if time_obj is None: + return None + + if isinstance(time_obj, str): + # 尝试将字符串解析为datetime + try: + from dateutil import parser + time_obj = parser.parse(time_obj) + return time_obj.astimezone(client_tz).isoformat() + except: + return time_obj + else: + # 已经是datetime对象 + return time_obj.astimezone(client_tz).isoformat() + +def is_smaller_timeframe(tf1, tf2): + """判断时间周期tf1是否小于tf2""" + # 定义时间周期的分钟数映射 + tf_values = { + '1m': 1, + '3m': 3, + '5m': 5, + '15m': 15, + '30m': 30, + '1h': 60, + '2h': 120, + '4h': 240, + '6h': 360, + '8h': 480, + '12h': 720, + '1d': 1440, + '3d': 4320, + '1w': 10080, + '1M': 43200 + } + + # 获取时间周期对应的分钟数 + tf1_value = tf_values.get(tf1) + tf2_value = tf_values.get(tf2) + + # 如果某个时间周期不在映射中,返回False + if tf1_value is None or tf2_value is None: + return False + + # 返回tf1是否小于tf2 + return tf1_value < tf2_value + +def is_smaller_or_equal_timeframe(tf1, tf2): + """判断时间周期tf1是否小于等于tf2""" + # 定义时间周期的分钟数映射 + tf_values = { + '1m': 1, + '3m': 3, + '5m': 5, + '15m': 15, + '30m': 30, + '1h': 60, + '2h': 120, + '4h': 240, + '6h': 360, + '8h': 480, + '12h': 720, + '1d': 1440, + '3d': 4320, + '1w': 10080, + '1M': 43200 + } + + # 获取时间周期对应的分钟数 + tf1_value = tf_values.get(tf1) + tf2_value = tf_values.get(tf2) + + # 如果某个时间周期不在映射中,返回False + if tf1_value is None or tf2_value is None: + return False + + # 返回tf1是否小于等于tf2 + return tf1_value <= tf2_value + +@app.route('/') +def index(): + """主页""" + return render_template('index.html', timeframes=TIMEFRAMES, symbols=SYMBOLS) + +@app.route('/api/analyze') +def analyze(): + """分析接口""" + symbol = request.args.get('symbol', 'SOL/USDT:USDT') + timeframe = request.args.get('timeframe', '5m') + + # 验证交易对不为空 + if not symbol or symbol.strip() == '': + print(f"错误: 空交易对") + return jsonify({'error': '交易对不能为空'}) + + # 获取时间范围参数 + start_time = request.args.get('start_time') + end_time = request.args.get('end_time') + + # 获取客户端请求的时区 + client_timezone = request.args.get('timezone', 'Asia/Shanghai') + + # 获取分形元素时间周期 + element_timeframe = request.args.get('element_timeframe') + + # 获取是否只需要分形元素数据的参数 + elements_only_param = request.args.get('elements_only') + elements_only = elements_only_param == 'true' + + print(f"API请求参数: symbol={symbol}, timeframe={timeframe}, element_timeframe={element_timeframe}") + print(f"时间范围: start_time={start_time}, end_time={end_time}") + print(f"elements_only参数: 原始值={elements_only_param}, 处理后={elements_only}") + + # 验证小周期是否小于主周期 + if element_timeframe and not is_smaller_or_equal_timeframe(element_timeframe, timeframe): + print(f"错误: 元素周期 {element_timeframe} 大于主周期 {timeframe}") + return jsonify({'error': '分形元素时间周期必须小于或等于主图表时间周期'}) + + # 获取数据 + df = get_kl_data(symbol, timeframe, start_time=start_time, end_time=end_time) + if df is None: + print(f"错误: 获取数据失败 - symbol={symbol}, timeframe={timeframe}") + return jsonify({'error': '获取数据失败'}) + + if len(df) == 0: + print(f"错误: 所选时间范围内没有数据 - symbol={symbol}, timeframe={timeframe}") + return jsonify({'error': '所选时间范围内没有数据'}) + + # 使用客户端指定的时区 + client_tz = timezone(client_timezone) + + # 如果只需要分形元素数据而不需要主周期数据,则初始化一个空结果 + result = { + 'timezone': client_timezone + } + + # 如果不是只需要分形元素数据,则添加主周期数据 + if not elements_only: + print(f"处理主周期数据 (elements_only={elements_only})") + # 进行缠论分析 + analysis_result = analyze_chan(df) + + # 计算MACD + macd_data = calculate_macd(df) + + # 添加主周期分析结果到返回数据 + result.update({ + 'kline_data': df.to_dict('records'), + 'bi_list': [{ + 'start_time': bi.start_klc.start_time if isinstance(bi.start_klc.start_time, str) else bi.start_klc.start_time.astimezone(client_tz).isoformat(), + 'end_time': (bi.end_klc.end_time if isinstance(bi.end_klc.end_time, str) else bi.end_klc.end_time.astimezone(client_tz).isoformat()) if bi.end_klc else None, + 'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high, + 'end_price': bi.end_klc.high if convert_direction(bi.dir) == 1 else bi.end_klc.low if bi.end_klc else None, + 'direction': convert_direction(bi.dir) + } for bi in analysis_result['bi_list'] if bi.end_klc], + 'seg_list': [{ + 'start_time': seg.start_bi.start_klc.start_time if isinstance(seg.start_bi.start_klc.start_time, str) else seg.start_bi.start_klc.start_time.astimezone(client_tz).isoformat(), + 'end_time': (seg.end_bi.end_klc.end_time if isinstance(seg.end_bi.end_klc.end_time, str) else seg.end_bi.end_klc.end_time.astimezone(client_tz).isoformat()) if seg.end_bi else None, + 'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_klc.high, + 'end_price': seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low if seg.end_bi else None, + 'direction': convert_direction(seg.dir) + } for seg in analysis_result['seg_list'] if seg.end_bi], + 'zs_list': [{ + 'start_time': zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat(), + 'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None, + 'zg': zs.zg, + 'zd': zs.zd, + 'is_sure': zs.is_sure, # 添加中枢是否完成的标志 + 'type': getattr(zs, 'type', 'SEG_ZS') # 中枢类型,默认为线段中枢 + } for zs in analysis_result['zs_list'] if zs.end_klc], + # 添加笔中枢列表 + 'bi_zs_list': [{ + 'start_time': zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat(), + 'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None, + 'zg': zs.zg, + 'zd': zs.zd, + 'is_sure': zs.is_sure, # 添加中枢是否完成的标志 + 'type': 'BI_ZS' # 标记为笔中枢 + } for zs in analysis_result['bi_zs_list'] if zs.end_klc], + # 添加未完成中枢列表 + 'uncompleted_zs_list': [{ + 'start_time': zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat(), + 'end_time': None, # 未完成中枢没有结束时间 + 'zg': zs.zg, + 'zd': zs.zd, + 'is_sure': zs.is_sure, # 未完成中枢的is_sure为False + 'type': getattr(zs, 'type', 'SEG_ZS') # 中枢类型,默认为线段中枢 + } for zs in analysis_result['zs_list'] if not zs.is_sure], + # 添加未完成笔中枢列表 + 'uncompleted_bi_zs_list': [{ + 'start_time': zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat(), + 'end_time': None, # 未完成中枢没有结束时间 + 'zg': zs.zg, + 'zd': zs.zd, + 'is_sure': zs.is_sure, # 未完成中枢的is_sure为False + 'type': 'BI_ZS' # 标记为笔中枢 + } for zs in analysis_result['bi_zs_list'] if not zs.is_sure], + 'trade_points': [{ + 'type': point['type'], + 'time': format_time_safely(point['time'], client_tz), + 'price': point['price'], + 'desc': point['desc'] + } for point in analysis_result['trade_points']], + 'macd': macd_data + }) + else: + print(f"只请求元素数据,跳过主周期数据处理 (elements_only={elements_only})") + + # 如果有指定分形元素时间周期,获取小周期数据 + if element_timeframe: + print(f"处理元素周期数据: {element_timeframe}") + # 获取小周期数据,使用与主周期相同的时间范围 + element_df = get_kl_data(symbol, element_timeframe, start_time=start_time, end_time=end_time) + + if element_df is not None and len(element_df) > 0: + # 对小周期数据进行缠论分析 + element_analysis = analyze_chan(element_df) + + # 添加小周期分析结果到返回数据 + result['element_timeframe'] = element_timeframe + result['element_bi_list'] = [{ + 'start_time': bi.start_klc.start_time if isinstance(bi.start_klc.start_time, str) else bi.start_klc.start_time.astimezone(client_tz).isoformat(), + 'end_time': (bi.end_klc.end_time if isinstance(bi.end_klc.end_time, str) else bi.end_klc.end_time.astimezone(client_tz).isoformat()) if bi.end_klc else None, + 'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high, + 'end_price': bi.end_klc.high if convert_direction(bi.dir) == 1 else bi.end_klc.low if bi.end_klc else None, + 'direction': convert_direction(bi.dir) + } for bi in element_analysis['bi_list'] if bi.end_klc] + + result['element_seg_list'] = [{ + 'start_time': seg.start_bi.start_klc.start_time if isinstance(seg.start_bi.start_klc.start_time, str) else seg.start_bi.start_klc.start_time.astimezone(client_tz).isoformat(), + 'end_time': (seg.end_bi.end_klc.end_time if isinstance(seg.end_bi.end_klc.end_time, str) else seg.end_bi.end_klc.end_time.astimezone(client_tz).isoformat()) if seg.end_bi else None, + 'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_klc.high, + 'end_price': seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low if seg.end_bi else None, + 'direction': convert_direction(seg.dir) + } for seg in element_analysis['seg_list'] if seg.end_bi] + + result['element_zs_list'] = [{ + 'start_time': zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat(), + 'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None, + 'zg': zs.zg, + 'zd': zs.zd, + 'is_sure': zs.is_sure, # 添加中枢是否完成的标志 + 'type': getattr(zs, 'type', 'SEG_ZS') # 中枢类型,默认为线段中枢 + } for zs in element_analysis['zs_list'] if zs.end_klc] + + # 添加小周期笔中枢 + result['element_bi_zs_list'] = [{ + 'start_time': zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat(), + 'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None, + 'zg': zs.zg, + 'zd': zs.zd, + 'is_sure': zs.is_sure, # 添加中枢是否完成的标志 + 'type': 'BI_ZS' # 标记为笔中枢 + } for zs in element_analysis['bi_zs_list'] if zs.end_klc] + + # 添加小周期未完成中枢列表 + result['element_uncompleted_zs_list'] = [{ + 'start_time': zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat(), + 'end_time': None, # 未完成中枢没有结束时间 + 'zg': zs.zg, + 'zd': zs.zd, + 'is_sure': zs.is_sure, # 未完成中枢的is_sure为False + 'type': getattr(zs, 'type', 'SEG_ZS') # 中枢类型,默认为线段中枢 + } for zs in element_analysis['zs_list'] if not zs.is_sure] + + # 添加小周期未完成笔中枢列表 + result['element_uncompleted_bi_zs_list'] = [{ + 'start_time': zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat(), + 'end_time': None, # 未完成中枢没有结束时间 + 'zg': zs.zg, + 'zd': zs.zd, + 'is_sure': zs.is_sure, # 未完成中枢的is_sure为False + 'type': 'BI_ZS' # 标记为笔中枢 + } for zs in element_analysis['bi_zs_list'] if not zs.is_sure] + + result['element_trade_points'] = [{ + 'type': point['type'], + 'time': format_time_safely(point['time'], client_tz), + 'price': point['price'], + 'desc': point['desc'] + } for point in element_analysis['trade_points']] + + print(f"小周期分析完成: {element_timeframe}, 笔数量: {len(result['element_bi_list'])}, {'仅元素数据' if elements_only else '包含主周期数据'}") + else: + print(f"无法获取小周期数据: {element_timeframe}") + + return jsonify(result) + +@app.route('/api/symbols') +def get_symbols(): + """获取可用交易对""" + try: + markets = exchange.load_markets() + # 合约交易对通常是以USDT结尾的永续合约 + symbols = [symbol for symbol in markets.keys() if '/USDT' in symbol and ':USDT' in symbol] + return jsonify(symbols) + except Exception as e: + return jsonify({'error': str(e)}) + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=8124) \ No newline at end of file diff --git a/web/requirements.txt b/web/requirements.txt new file mode 100644 index 0000000..05b50c2 --- /dev/null +++ b/web/requirements.txt @@ -0,0 +1,5 @@ +flask==2.0.1 +ccxt==4.4.70 +pandas==1.3.3 +numpy==1.21.2 +plotly==5.3.1 \ No newline at end of file diff --git a/web/static/css/style.css b/web/static/css/style.css new file mode 100644 index 0000000..596cf3f --- /dev/null +++ b/web/static/css/style.css @@ -0,0 +1,131 @@ +/* 缠论分析系统样式 */ +body { + font-family: "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; + margin: 0; + padding: 20px; + background-color: #f8f9fa; + color: #333; +} + +.container { + max-width: 1400px; + margin: 0 auto; + background-color: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); +} + +.header { + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 1px solid #eee; +} + +.controls { + margin-bottom: 20px; + padding: 15px; + background-color: #f1f3f5; + border-radius: 6px; +} + +.chart-container { + width: 100%; + height: 600px; + margin-top: 20px; + border: 1px solid #eee; + border-radius: 6px; + padding: 10px; + background-color: #fff; + box-shadow: 0 1px 3px rgba(0,0,0,0.05); +} + +.data-container { + margin-top: 30px; +} + +.nav-tabs { + margin-bottom: 15px; +} + +.table-container { + overflow-x: auto; +} + +.refresh-btn { + background-color: #0d6efd; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; +} + +.refresh-btn:hover { + background-color: #0b5ed7; +} + +#loadingIndicator { + display: none; + text-align: center; + padding: 20px; + font-size: 18px; + color: #666; +} + +/* 表格样式定制 */ +.dataTables_wrapper .dataTables_length, +.dataTables_wrapper .dataTables_filter { + margin-bottom: 15px; +} + +table.dataTable { + border-collapse: collapse; + width: 100%; +} + +table.dataTable thead th { + background-color: #f8f9fa; + border-bottom: 2px solid #dee2e6; + font-weight: 600; +} + +table.dataTable tbody tr:hover { + background-color: #f1f3f5; +} + +/* 方向列颜色 */ +.direction-up { + color: #dc3545; /* 红色 */ + font-weight: bold; +} + +.direction-down { + color: #28a745; /* 绿色 */ + font-weight: bold; +} + +/* MACD列颜色 */ +.positive { + color: #dc3545; +} + +.negative { + color: #28a745; +} + +/* 响应式调整 */ +@media (max-width: 768px) { + .container { + padding: 10px; + } + + .chart-container { + height: 400px; + } + + .controls .row { + flex-direction: column; + } +} \ No newline at end of file diff --git a/web/templates/index.html b/web/templates/index.html new file mode 100644 index 0000000..cb7eb05 --- /dev/null +++ b/web/templates/index.html @@ -0,0 +1,3736 @@ + + + + 缠论分析系统 + + + + + + + + + + + + + +
+
+

缠论分析系统

+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+
+ +
+ + +
+
+ + +
+ +
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + + 仅影响笔、线段和中枢分析 +
+ +
+ + +
+ + +
+ + +
+
+
+
+ + + +
+
+
+ +
+ + +
+
+
+ + + + + + + + + + + + +
时间开盘价最高价最低价收盘价成交量
+
+
+
+
+ + + + + + + + + + + +
起始时间结束时间起始价格结束价格方向
+
+
+
+
+ + + + + + + + + + + +
起始时间结束时间起始价格结束价格方向
+
+
+
+
+
已完成中枢
+ + + + + + + + + + +
起始时间结束时间中枢上沿(ZG)中枢下沿(ZD)
+ +
未完成中枢
+ + + + + + + + + +
起始时间中枢上沿(ZG)中枢下沿(ZD)
+
+
+
+
+ + + + + + + + + + + +
时间收盘价MACD信号线直方图
+
+
+
+
+ + + + + + + + + + +
时间价格类型描述
+
+
+
+
+
+ + + + + \ No newline at end of file