From 626d3a6f264918863934138e5b650bc22341416d Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Tue, 22 Apr 2025 10:04:30 +0800 Subject: [PATCH] Initial commit --- .DS_Store | Bin 0 -> 6148 bytes .gitattributes | 2 + ChanBI.py | 67 + ChanBIZS.py | 55 + ChanBSP.py | 24 + ChanCTime.py | 44 + ChanEnum.py | 147 + ChanKLC.py | 440 ++ ChanKLU.py | 40 + ChanLun.py | 2236 ++++++++++ ChanLun_Classifier.py | 435 ++ ChanSBI.py | 78 + ChanSEG.py | 60 + ChanZS.py | 66 + Find_Trend.py | 448 ++ __init__.py | 0 __pycache__/BI.cpython-312.pyc | Bin 0 -> 8645 bytes __pycache__/CTime.cpython-312.pyc | Bin 0 -> 3367 bytes __pycache__/ChanBI.cpython-312.pyc | Bin 0 -> 4790 bytes __pycache__/ChanBIZS.cpython-312.pyc | Bin 0 -> 3483 bytes __pycache__/ChanBSP.cpython-312.pyc | Bin 0 -> 1359 bytes __pycache__/ChanCTime.cpython-312.pyc | Bin 0 -> 3396 bytes __pycache__/ChanEnum.cpython-312.pyc | Bin 0 -> 5877 bytes __pycache__/ChanKLC.cpython-312.pyc | Bin 0 -> 17033 bytes __pycache__/ChanKLU.cpython-312.pyc | Bin 0 -> 2125 bytes __pycache__/ChanLun.cpython-312.pyc | Bin 0 -> 108624 bytes .../ChanLun_Classifier.cpython-312.pyc | Bin 0 -> 15561 bytes __pycache__/ChanSBI.cpython-312.pyc | Bin 0 -> 3867 bytes __pycache__/ChanSEG.cpython-312.pyc | Bin 0 -> 3481 bytes __pycache__/ChanZS.cpython-312.pyc | Bin 0 -> 3796 bytes __pycache__/Enum.cpython-312.pyc | Bin 0 -> 5237 bytes __pycache__/KLC.cpython-312.pyc | Bin 0 -> 4285 bytes __pycache__/KLU.cpython-312.pyc | Bin 0 -> 955 bytes __pycache__/ZS.cpython-312.pyc | Bin 0 -> 2681 bytes __pycache__/__init__.cpython-312.pyc | Bin 0 -> 164 bytes chanlun.txt | 13 + chanlun_trading.log | 39 + web/.DS_Store | Bin 0 -> 6148 bytes web/README.txt | 2 + web/app.py | 539 +++ web/requirements.txt | 5 + web/static/css/style.css | 131 + web/templates/index.html | 3736 +++++++++++++++++ 43 files changed, 8607 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitattributes create mode 100644 ChanBI.py create mode 100644 ChanBIZS.py create mode 100644 ChanBSP.py create mode 100644 ChanCTime.py create mode 100644 ChanEnum.py create mode 100644 ChanKLC.py create mode 100644 ChanKLU.py create mode 100644 ChanLun.py create mode 100644 ChanLun_Classifier.py create mode 100644 ChanSBI.py create mode 100644 ChanSEG.py create mode 100644 ChanZS.py create mode 100644 Find_Trend.py create mode 100644 __init__.py create mode 100644 __pycache__/BI.cpython-312.pyc create mode 100644 __pycache__/CTime.cpython-312.pyc create mode 100644 __pycache__/ChanBI.cpython-312.pyc create mode 100644 __pycache__/ChanBIZS.cpython-312.pyc create mode 100644 __pycache__/ChanBSP.cpython-312.pyc create mode 100644 __pycache__/ChanCTime.cpython-312.pyc create mode 100644 __pycache__/ChanEnum.cpython-312.pyc create mode 100644 __pycache__/ChanKLC.cpython-312.pyc create mode 100644 __pycache__/ChanKLU.cpython-312.pyc create mode 100644 __pycache__/ChanLun.cpython-312.pyc create mode 100644 __pycache__/ChanLun_Classifier.cpython-312.pyc create mode 100644 __pycache__/ChanSBI.cpython-312.pyc create mode 100644 __pycache__/ChanSEG.cpython-312.pyc create mode 100644 __pycache__/ChanZS.cpython-312.pyc create mode 100644 __pycache__/Enum.cpython-312.pyc create mode 100644 __pycache__/KLC.cpython-312.pyc create mode 100644 __pycache__/KLU.cpython-312.pyc create mode 100644 __pycache__/ZS.cpython-312.pyc create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100644 chanlun.txt create mode 100644 chanlun_trading.log create mode 100644 web/.DS_Store create mode 100644 web/README.txt create mode 100644 web/app.py create mode 100644 web/requirements.txt create mode 100644 web/static/css/style.css create mode 100644 web/templates/index.html diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..462effb19475fd3a8683c87cb61bf1b860cc5e88 GIT binary patch literal 6148 zcmeHKPm2>V6o1){?$jdmprE&a2SEyU>uM1%W4C+rnV%Z z4=?tdsD3|6RfHby{eN33-5fqK8x=(83zt9$ApkYWs&o^r%|3J#jM@_ zDt2nlm8-k%uG?_m`D0c3vv4*qdg1g9^ z`zpy|Rrb_8iwhm=8-rVS>p}DOV$o@LyDi!6be1i-c-(2Xk}l-{E0zJvfMsBv0m=^&3!%qY8Pur*EBypO zbWp7Y+w_*87|G}{RtC|6B1|Zv2^IXr5GEY;lKni!%Ag4c!H*BYBMW|^2t7J}Uut&{ zo 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 0000000000000000000000000000000000000000..a7ad2cf32dec17105cb5a5d37d44acdd0fce61e5 GIT binary patch literal 8645 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 bzc7t|W4h*naDT literal 0 HcmV?d00001 diff --git a/__pycache__/CTime.cpython-312.pyc b/__pycache__/CTime.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb87c580a19f29e037fa884cce114adb3b3ce66d GIT binary patch literal 3367 zcmeHJUrZE77@yhOy<6@MuW~>QDpIY9S0kr}q+lCskSZoE6*w9>tINr(dcbiyyVOFi z(FfB&W1~C)F^1y_iA{R858{*97j62`rM8i5Y|^BC@vY?^n()-`+ub|P^U$R6v6Jj? z=KC`{-+c3Z-^`EYX~IxL2yr$~gTDYKZa>hXlyYfEi(R7v&#&=W)@QOZbEU|vQq)2Iu77!{cdP+~qN z0?*APpghbCC^HXW8Iu9M!(}1ADIJX95^}*BxP%M7>=)J&DVicmUYrtuqB%0<0xD+` zNr^z=Osl0NpmO^sDK}8y9Y^K?%AKc3Um!PL?Qc0lN`}cCZ41e!IIPCFDJ7DF`dQP( zV#B6*HaWyicOpJGq^qV|Q+tzx%oJlodeSV{R9!*;X?iSi4ySACz-f*<F2cV%!ozqqhgu67AYvI0FZ-4kGPxh(3diXR~&+9zK)bJ1p6*#F_SObQxo{6hk z_~6;tVAyhO{kdUNR+RW)TvwElbv7?YqdTpNQ(5KN>O99e+O)NA4u~@xT@kv zcY-v3hy_yNIz$=B@`qR;6CGJ$P&r}-vKvw-TJ(zl${^fI?h>tm;48UDw5F_+%z4^4 zy1*26H>}VwbbC!&w?g;LNd*D&Y~;*Q{u)ft(Cu}6J>s ze=75#5o|X6&A-1~abf*J!-J;#O%FR3>W!oAM#smGJ5DTioG?!I80|45c*^jf;z2Ox z|9JWD@xrUXfqWyvCV;{iaO+77f)T$R0ybr-*P^=R`NC|&{IN3i6Bw+Wsn^96y^dK^ zI2UGMV~~rEbd8+Cj0?Hicr|GA4FDlmQ5_5kRy0G6JU5_gxJaE7)Ma|I1?pNS>?x4j zlfysd?277%(0FLF`C4sSocuJ+X8UIPG8Jhh6UiK!Z@AZZw{f9)ewPtwHRRUk z;}28I@wb1Gq$<1S+pZCc4MF)8edm#(T5no7TD3Ol*GrLe(#U!-jnL1>3&z@-iJM z?Fy$%zhBxVyh-m3<%(cO(Mm`H zw?rBM_l1_FXdhPTqLzDQzY2C78z9gw0<3jmWE$=?ecSXTxN)*&`pDFgv_3mBGXl)U z+l})b3l$5I2gmOpe-vyVYx%RJPUK$q-R_6tPre^~MpyUauAarNo=3shXbYxs{doPO zimjvKSDxpq6}sKZ(*_uE6N#sEjcCB zrgp#ttr7ry5(r+sFm_?GV!CRoYV4vR*S=aqp%OcW@?EeXug9eeOClPmK4TzIJ5dV@ z{i=amr?*dSUkq$9EyP9E16XDa){L3UO$kOKJN>S>i%YMYHpGrsQ zvYSe+fReOrc^#FuWPz@;jJ5uP7F%&{709>YPqqvbX+?h5ws}8q1QJWOhSvV&qLltd PHvi+^LF<+YfHmh|((_Sg literal 0 HcmV?d00001 diff --git a/__pycache__/ChanBI.cpython-312.pyc b/__pycache__/ChanBI.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..52a365c0cafe3f7b6a1de40d094d698813ce5825 GIT binary patch literal 4790 zcmcInO>7fK6rNrGtrP!2LTm>x0ZJ1inuM0W6bOZ+Kw6X{64a8*%6K=#juXPH192K5 z4jiBYp-7dIRFzUr5ODxbT+0cmz2FEGYpY5~JypH5iKcQ z6cj=A42p_))idD51qXbJ8>pmsfch0L&>F=Dv{sRT1{6Qgpi%=gq|^cpD*>PpB?xp) zA(Ra3UR3Q!kM0`IrPJ14aXnNG|0~A@RBcL_A~N#Xrx0iX-_|mBPQWtIXKjp3xuC`8 zof4tt;p1>luhp6ovLUNpEw%+O~m)U5! zZq=rQd*f|vPqxvjmrGbBXtfmFA>RjUg{{5n6)mv^GzrlS(0UV1XaWX+E{J1G7pzz465y-Ckx$lDs{%|m#MC6#gf=lW;g=ky zgKpSB7}We?j9@-A;T?T?o%+5U;A?3Dr<8m zP?j9-CS*dO?+EA^UVo9^A?XQG_YcDNtL<5!|@H4df6i z#oqp;zYIwF7OcP!sk^&B`LvhelA_1bYtg%zA(CQ@Pr3}BjEd?K3zQHdH9TX8Xoh=c zgUq-)xz^?pP2Oe@)~*0Cu)FX2zArzYxjcP&v3A?tt@m3NS`1_^ZtpC`*3V0G(zWgz zJ&1B9xfE?KMw=IFo9}krKelkpfaKzi&Qi2-=F;>fkUb7nh(G}dR!dT>D8**$XD606 zwHG(FKa+OioPoJO*$>{8GITd41{H>( zfE`j{5Yw_r;A`j%z#!2CQyrtO6|(FKVx1{?7b?{XvaF0N2oT~I3xv%t%K>L`Ab+!E zWv_8<1Shu)PyU6C$e?H$50OQgd&7NlP_dZL^E&Pn5ucok z?+EP>5RCao_Ooi*;+*VYdUR9z$C*Mcti51lExLF%Q;{h6P*q4jCquRy0qyrtX)`^j zTlcS)XK|(-=fi`yuA_ttWA@9lMT@c9wsd_1EbV`F=NxeFgD?Qj(Pw5+s_L_Y%7g%z zhM&G^CYBsmsOw89EMw<(GTVD$ehpm^9ByS6`dr%l|LRTmz_L~bh&!lIIBdKO@Rh-y z3X=`y0ax)mR@=a5&!x9t-2it*AFE)S#*eHHiJ z(bL$paR_}ptn(LOStZcMfy)D_Lmz0vLnj1D+sl^Ojq49U+zRVJ^urtH6ar~8cAzsZ z`*q5&XTMYa@iBOqsq)#(WW+pe&e;!RwGklb&5g&G6}$f9C}%v|Y@wnzxfUOXqp1;9 zmh~E09yza!=dd1-lIzD1-iYqp*Ws@&K@ff*p&v=pAL1b)u}pxlVSfTC(>X){ literal 0 HcmV?d00001 diff --git a/__pycache__/ChanBIZS.cpython-312.pyc b/__pycache__/ChanBIZS.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c226f229cbe1293569a023129cb7c209158e574 GIT binary patch literal 3483 zcmb_f-EUMy6u)1)d%Ju4LBF=UEzk-Gmty-sRHCSnB1kaRh{j}JZkC-(-Ll=5xpxz~ z*@r&Vq<~ctO^Tlo6HOE(#29U?${*lU6G+#a7)=bX9wOMQJ*g4X^z^ z4dJ|q&|vQdA+Ph{E$gz7O7c}7Fb^PW|=Q3WDi*9zr_H;W>EGC7r zd19aSw7h-R_0%zHdy$h_E0=X#FPt7513NE_#&Gx1T-w1Pu(EkC3~aREdNI!Vgh&G87<^pQ}Z;(&Yfb2Df)z>dmX&dZfN%p^uO-d z)af5iXHNIOLPv+~jN5;T+GkvvCU$=T1}&0y)BPB*j9}FEjg5P{1z@wTWfixGVD1xL z7zWN&AQMu#qkBqwyS|+0SxEHGCwf6?C?~gF?)#u`A-R7(xgR=MZ(r)P;-Ef-)vT1!H1k`i;w`QrHOtcd!4v^3(O@k0+Kc zzOy(xu{itH-J2gRz4iIx?AObmezx-Ay4$(>W$bR+EnZ7>rgENDU57zAY_?vhNq{-!Vli4D{IR-K&V zGCMQw$IrZS?HWJu$`^A>Kf=kY53JHQFr@8B(0``NkB+@w)Q?fe9&!*ZL?ZV?6pzaX zi!C)i;GMX+vkM5Eza4Kl_G@fQ*=SlYy5^0p1!Kp&vE#~-+eYuIDjChpH=9>f3Tr&z z8fm!NjbMQd>a514YFbsZu=WJ=YU?GdHi28=SyBsl-Q0b?zvkXs{^T-*$?=sRZm!Ht zE?t~@ZO{zUF0k_qbcd)X6bl@UBk4qfv0!T4t7yI68>BaPYD}yG<)PSG<5jgcdqu(|9%1RzwTI;mck;i>n-17&Qz!Vjf@N3a z3d%LP!eWHi%?&ZaJNt2hpCcF})3Jwvh;hmxE}fg^Va_iMa~U5Hfe+;xd=YdvRQqC6 zjjN#tJuHy5v;#8zzad+ky|57(K6C;Z$~DMzM24?XRmm1zPgY^W`a50sFxWP~*yWk; z?#+CQj>8ZZ@KCOXNB2NSv8~2wBKDuILWd8^bp3jUd;%cDYF@HK zxApB0p5>pP9>iFkLW{HKm%q7*KE^u`K1R6~AKrIys0ttN7y!eHpES*_;NZyr1L}eg zDp3UruT)YyN97xIM>el7;;8MH;`FrfGKgWMy}(&wR1hM$uJvZ~6md`IfJ UBenk~b^jS2kma^D2`QWV7rM70O8@`> literal 0 HcmV?d00001 diff --git a/__pycache__/ChanBSP.cpython-312.pyc b/__pycache__/ChanBSP.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b0fd3e677ed44eb941ce53b45d2c935a0b934c2c GIT binary patch literal 1359 zcmZ`&J8u&~5Z-&)XA_4&oY*))m_$OlM2aH{Xd((oKuE}nAQV_gE92b+8`~j!X9-&; zL6O2mQc#6dsSyqQ1A3}dIhFz>D!PCRBr0b1e8)qu#ms#3-r3oAnM|6%c{BZWLo*2Z zj37GZP#7$NutykST9Z_e>rJhtSM-)qF*Kr+8-y9#gqg4OFrs31HOKmbpzP=}bM0ZZ zO|RZrrEB+ASL8?xmv7##g%Kf548h<62z!K+ipEGqXBsz{&P`@;YdyitZL^YK);6gm zy9p;Llb8>(OZ`DznqK!YmnLI%QA_=n8#{yhk-nplCXftD$BXSwr3!`4c08sOXCSiyHlBR5H@~mN?0s5tEA2n;zKw{O~j6V@rR3>?w zDN8zZ^6D)vZB!~LS$C=15gc*=4Rr>*m9%>fw6*SylY;9H)kGG6L{k+P~+E#dYk z0xPuexGQZ+>+QNnX>V+}SPPNH!ZJ^RJR`yBY~S2T2ICWZOV5`;w1bHPuyl~k14{*k z>4UlbxuZhquuuZU$sk`iNbjeQ^7DuJc~E5DpU4JN=laV#w}P`5`pdiJPgBJZod;G9 z#&h6h2l*m8$K@vb=B_<}7Dz2#P5|+G7XiRm{cFA9F=6e)1CObf*Bo7(0;~oJaxRix z%v3CjJTRGYxu>2>JydTZn#Uh%HTyAehoR)ApxKe&%nWonDhi-1J2p S$$TV>-_5Kx`;7omVLt&XNf}oF literal 0 HcmV?d00001 diff --git a/__pycache__/ChanCTime.cpython-312.pyc b/__pycache__/ChanCTime.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b9efe3f5ed8e57ce8f5400c2c88103387ea80110 GIT binary patch literal 3396 zcmeHJO>7fK6rNe{+UwZCU?%|r1Or9FMZ|z2HKrA`Vg6!R&xCa{xM-3|KSb47hdYf%DO!17X}ow(Nu3*x)OCWo(n8DWa6cDFG;2B2zY? zN@|gm2o$a~T1o<{WCN120|gc`We%Y1Ws1Z-rSWoq+Z<9dLS|`4K-R^PSd{BhA~~d; z*KI61qKoH~!(6u~`iF+Km~K~NeaRuFi_u{%sn@D8O+g!~S~PJ1m#eYCvm8&#oe1bT z?m}=Q)FP|`P;rXiY)heFj~e4@Fdpq22p;3fc&twgp5?KN8jrGAa2SLNcqjh5A0g6|zvbZcM!9T&Qn^u4cj40G(W7Hf#jSwP1GU zOxgWZZk(4Jf0Z{D;1{H?>=7XFT!#O)0(XL>NF1kBG)4z@z|B}nKhzJF$3ARQa5V!hX2YS)~cQowSma1dPda7*-d609UM)d3%gsE zXb`%+I`uEneY27!I$jK$S;|+#5*5wv=WB4aLs3+XgRj?EeQmHXxC>(d;A)rWdT2b9 zreE(*v#Ei}f$usWH#ILbG*0y2?9T`{PG|OH!ZVTSNLI*vn%$EP-#vck_}rQ7CwX66 z-re^5>nG;cu zdN3g0fUpt38V7bQiGeV>dN~Y=9AilnE-|cc%{t5@BV#{@!O9tXOH9#Qm_63DumT%{ zoHV5?B!H5AGVgPMw)_|4#UYRPuEk-FhrKI^$mCK^Hw z0ziO!8zurb1L?LKO&Kx$MTX79r{h^~M#+Y=AI-JgZ@t%=Yn$7Z_Z-a22VaguOf$n0 zy)c*{TDh)OKQ(ZxoEEs%DidXt3E$gpn=-J5Sj_y70@Nt{0g+=>uCPiq597IC^e}SJ zx#~86E99AHUAk^+{p9*g`^@3#!&&jJd`F(!GS`{w`NeZ2FCTfu(XYL}Y=^6x!z|k$ zRyPX|X%0}%XfUHnfd(#K9u*C^ctKV)CKy)$UYHfFWZ*C9(9t)!Bj{1F6OzEKk`BOq zrDiD_$Esb?N>2`WoZt(=Xd~q?dA=4H&))?I)L3o?+=pY#R>loJD0FA}1)>X@TFx*A z;bP4&j(5c9+U4AO;qkHsgz9C0l`fEM%iYiqp=Z7g>Gr8ZlZP_e%;@weFkA1m&UNLy zx$wi24^BSub&j|HS#?hM{)u}h9*IA@estw~PCV`Do$u*=;){;8V>&nAY<}Y1IwpSO zc)4<+>5Y7Cfx$|Z3yclY$~MqWNH^gal5m~Ajh$J41Z}U?Ho9F=&fzP{Qc{bY9k58P z0|2{teAh3HUrKwY>L=^RFX!c^H>)gEW>ZNSdzNV4HU9J>|e;&61ZDB)h1&o?3X z5wPs=?Fbl2d^f^gfZ5`8W4uv{)eTFW2%m~axJn9wENxn>6Qw@7=teBKsdR`g+NpFD zP?EMSuAX#S>rFLu@Un|!F&h)RFq*-Ey?c{b>7cgfy5H7qPBn8D5bxVO@G^W LQ2!zUFxLDFE_hp1 literal 0 HcmV?d00001 diff --git a/__pycache__/ChanEnum.cpython-312.pyc b/__pycache__/ChanEnum.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4a77e10815bd80fe00f4d1e1afde78810ca63665 GIT binary patch literal 5877 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%0AS8pbzTzL0g6f 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%0D9b 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 literal 0 HcmV?d00001 diff --git a/__pycache__/ChanSBI.cpython-312.pyc b/__pycache__/ChanSBI.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..143d5236730e0878382392f046422aa6324ec6a5 GIT binary patch literal 3867 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 On?J_&Y!Lu#mwy0vN0QS3 literal 0 HcmV?d00001 diff --git a/__pycache__/ChanZS.cpython-312.pyc b/__pycache__/ChanZS.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..de2cfbc95afc5da96fd3afb36dd1eeca15da55a6 GIT binary patch literal 3796 zcmcguO>7%Q6rNqLz3YEBcH-DcTciBuI*j~qF}X-g_Eix5a4q28+GP^lMqZ}unaHc~~A7-_$G@6FG9 zGxO$~{V^Vo5GXHx`LOw_CdC}_YY=J>vFT9=~k$^v;@5^5r<&jM4DAim(Mrk zM%xNOwYaQXVndU)WvW_yrw*TD1t7P;IhbrWY&1p@vq#9a#pSkcMIDWKN;-;bnZ*fX=0F-M(XHGGcY-nDjvEDSUCNmwtR6OZkD)%w=37l} z!Wu};yK11IjF)W_^A0aMBppb0Ss}ML;4F%nR^sUxR7}=zRG~okKxHK~RhPFCp)=@g zUcY%abk*>jU<>obtHNrM7D20d7abIjbUt!OhkmO+g-qPbs&EU_5S+X{Scy|RHX&?DNy|I(_sFcyHN2QEH z7=zsZ8%jyE0!_6|cfnfh3x~8s@klAs4lMwb1Uo-GgC#mi;oVF-Um}ADDGo8PF{IlaX zPk%G>^~~M$ezcQr^z%Uw;HoK;a~6*eIEP)L$Ree425zZLwSM2qMisGY=?1x!%Z0Ye~1I$ za+fJl?u$dj4TN{KlkjaG=)g3d|MU>}m!5ea@Xx~ak8+=Xc*%7cUw7|;a}6)~BEaWr zHT=vGV4}GZpzM!;Vv2OqzSX<=dCV8UZ~+2{KN)~`(=f;2rjc#`Nxgdmot)yQ<`rV6 z3~x!0vh2TxBAYoOTLG~8L$<`K!m`^tG)F%UgEUMv$o9TheD0n!_GIu+d1u``9^MLt zqTCls2#>2%@5Fr@*r~q?-8P-<9mv4eiO=jEUrG1uP~OJ7((he)9lFsI2t|O|QVM^g z+SQgS%T`2|TZ_ta6XlpJUtF#>ofZ*SFn8(&H!8?5BQ0Sj>;4SM1$6LC=B>e{?kD<_7_e<9;J!f067jB=svPJQR*`gBt{Z4gLp) CMA+H@ literal 0 HcmV?d00001 diff --git a/__pycache__/Enum.cpython-312.pyc b/__pycache__/Enum.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eeca940f655a48e644143a5ed6a807c3213884fd GIT binary patch literal 5237 zcmb_g&2JmW72oA9$t6YcOQz&+%WWK#bzntSYqUWcU_~y)HJ7_Ixs+}8uwliuEQOSu zA>~$X3akJFwILKW;Dh}a1Py%cAJAig06~1RG(b_HJ@w{7EDEHTzBjv+mMk5lwh7Fe zH@}&kd0#X0hF?xjMmhNX?LU8Bl_DJXcWexPL6153is!f|T#IY*ZLY&N_)ee^;ISQO z3mvf`@?3yh;#$Iet|i+1Gr|3?5oBl((2x%eF*FRQvM3u?%`|c)D&}%GOe*AR%U^H@xb8PUnyYth(w$pxu6#0AA)D*=N^kBKu|MsR zX3L)2f=;W|>^0|dx0@StsOY6T+awI1GJ^5lodI&6+fN>Q9CFnCiNx*|C%+$!@4mZ} zBZ$;+puvs>AY~EDlDuppWW@F~vYT

x&jq+ zEtF}bWL>MvRa2>wIL^!rm_#l;AQQlJ4Ip52&)Tu%ZoyIZCsVu2PGNtzH9W-Y^D3_o zUALpUYvD5Eot1~4H+L2@Wu&C3x?<(ks-Bg`8Z%I@P%&>@*J(Islow$%l~g^a)$>Yz z)H3HlFoFb6sqR@IV429|?zFQwFi@WGk{dyV z8(suO#ws0JG)&VdQ^7PUG+ft9Iv9Y=f;i+nl97nGfEbclC|zVgZ!ZRL|2esRPj83#qNeCWlT7l_`qkU8;q+0h5l=^a0~9i8Gn_U-6ySmH!kf%5Z|zfAds@s`IyN0x8# z4WSiih%KQJY>ACfE7%CPLJg@Ewj;L!E$P14h;B!+Q7SFgD$HEuColmG%-vc)F2_y34Q>nK=5v}y=4O< zcJ219E>1a1l0a#s(_GuIdfRvGd+C8tmkzN44C&%Hhucd`{r=v=dr!|l{LR?R+NDBAXij!kTQDejLnV6 zJP1QY&8XF=Sd_H_4V#7uD&8E`4kac?K87+#jJ$K%OZ~jc?;iXB%i-^J<4Qu$xZ8Jp zKn38g5n#}$+_zzAf@BrSz$;!3gU%W3030%2(mz7a(S{wmaliGQH8k?I@8+RdzV%@- zG&14l#$`E|w`6U}(6h<0r=BQR6`3?aMH(s8HO;~{WXo!yK!Y{Ss3=sB%M}`0Hne&f z9v^bqsOu(;x@}8dUZPPKV|>i4SM{oFDy$_`YwD;&+OQTuf~AYw8+P_^1Pwp`ybIpj zNnXf#RVwp%+PS*#RZ}yMuQ=M!dl45Ia_iwa{2ufix%Jf-e4eMXVEZaG<~EpLAS?4`0&f`0O4Z*+?~FQS z8;n7a{2EFZ(`2|PQm34&d#RZLNoE{%FMYx(?Mw7twZN8ii5Gf1}ZV?BG4iZ(p# zi2yC{poh$4ZDW-L5n+#g_V~mX6MJv4w4Ei(1XD4hAz@hryK&hKh6gG;S=j-^b~B40 zCNcRf_C7%J5D4U>RXML%s$MWGt!9pB`i=CV`v=EI~m1{<3T=+I8^;)0C=;{LDiR#2b}Q|e;RqH zUQTnV<4-4_C*O4@`=JPb2Iq&We_U|Cg{mJD_~fHwxNfo^3-FmoY@SR%3X{2fx*ru? z9IAdi;K~VAKN{mxC>vD&J}dDT{t2sIfK{i?yhvxCr?Y?f;KjxK^NaaEt-mN(&kNSq LsgHN!>2AkrF9Ou4&mpOe8H`xUH<#DB$X0sbXkDqHF6=;6U(B7AcZa zmK019EZ_nPAfr(&0x2k^DP#cw9Mu8b04>rykjGbB^sB0kGUosl&?rv6s+u1K^3|C; z-jTBDj|%8Vm*DKo?Cj3$?Ci|mpWE6T1X_IjPgj5FBIF-P7>%nVwI2ggAu^Gf6cMqv zq?oitw4_;)Wsqi5T$&g8v{kgGZK5qLh(g*f+O;tzT0L_DVo7Z!4f`AjSoI%MlaEdxk}C`4prB3cwSDKv}e_zWX6 z3O{3!Ela#;mDwdC+R$!Mka>W1*$U7h+W;gOQGc zGw4>?iSUrh&ZQRPhF4J$+JygFoQt~(<#LI% zqH?o|tFtPbn!g5&!h)i*a)PRSA|oq#m4hS9s*RKtFx;3inFm#Tp7b$yHaNTs> zaIH?=6M8=F?5TvxAz<6gDL!pVPakycRbMZ3u4-2gbg)1}ZI{0?U7lX%Rva}jiNwtq z1;V@=-vt8h25EISl4cy^e7!YCWPxlkkXC3H45`-G0(f2Cvd{pKn!B^8F%Y1~kkJjG z=tOVTkGB^XSv(MM7VOu4!~QqH{};i(=D9B%p~qohpT)Qq+Ec^e$J-2X2AJLoggygU zkfD9h(PsfPD_5OaB`4LRr07N?nixk=+718=@xd{yxDKqn2Ue+j0P|6NpAiD;!~oWV z#-NTisrSOkuGkN({xB>>pTqTbH`*i}(#Mm^3;EHfyt>{09f&g?>^JTMg zJL=P}WwFihyGG&x8wOzTuQ;AG0MA=H^N(Q3r$s4bArO1K%|ThEpFmurF^@Hv(Skb% z-lJ16!_3f)X(i^(rc4YnX5MbdZvr2pps~r@ccxq8rkYqj%Ri}zRH>Mnb!&%rS;Ob=eeDu{TtWT zudNQhGPdU+{=Pk)96NDa*c5;>zT@i!rq^E?EDzpdk-P0V2?eSWE{AVTt%mP;gCJ7L zm-DX=-afl|cH0-Ko_O~5*yh+8_nLiAAbo*7mH=m9a{xF(wfE>wZ(z?#I=ugliGkyW zL57KeW~wwe4S%G9e#rx)^?P0MSM zI3_Zq;kcIAUIzq=8I9!36!4TQKw3UMpYkAWm7w4fwMbj7uU5%&$EF+0kOKVl^2(|F z0!?Ic6d#F=2oAE1ny*@lg)D9j-d1|yKLbP&J{#cSgma@nX1@dAI{C!e{;BKG>WNCQ z99)ZTOs`MB!QK_Ngx{Qa`{Y|Ee?Ro$`FGBLH1&roA7A++z3-ZYcyYP)B4PRp*1>eG z`SLSBD&&BEY?;#U)X(fE@YrbeAyZs-Utd5zX>S5yBZJS#wlvGTe~Lpa93>^;~>@uvDeF4CA@>yrss$9vGGq>d1$<3sGihu3=-m(qesFgywuxx{OYWLyFY|LzYbcSqN-^Xp2SDXUGK*=7;X^S(CQK1%P`CX;`}=~_K-~6Cle2d;{kE~+xjDh@jW64 HwMG5|iU9tP literal 0 HcmV?d00001 diff --git a/__pycache__/KLU.cpython-312.pyc b/__pycache__/KLU.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f99936db88c64bbc3407b1aafa39e0213bb3a941 GIT binary patch literal 955 zcmX|9y>HV{5Wi`s2Pwtac3tv$-lebyN`Fid*7Fr>wxlJd^?;lfL}!B!e_>` zi^eqs5U?0r(ptz1U`|-rd^U5-(rx>~{Ssr9$kgl<69(YY4$*0k~SgF2L0T4UrFZn=Adz z?!I4}1(TwSb^N9U0<}K^fmS6WhHPFMMh3E3qW~if8D*9-(vi(a0V9sgSoln2=9~po zbJ4*NY^^7$sBNj*Ni2$CKUBPkl293boaIQeewZqL6dfKZJxcf79^Vl%LI1_8U%L{a=LG`lG~@wejdu)IrjV$=bg-sL%(niWO!OgF9@9hIz`|W zp0j=Ar4G*PNq?v;A)+)Ygg9%`$(&hK{29VI{OoL8^3Tn2YYo-Xcy%3Bea>cwD{VZq z#_flw8snALCBL$!m<|47tVk9sd4S{^$|X=xDL63Jvzfey%+}<6RHdC$*5;<9997H1 zLf~}H8r1BJkSAY4IEOJca%IUDvYu_w-=$s>3Zd#kBv~+s$!-d9I`HDkBUdn1wh_KS znOEKvpNhG9jtt!EPK@VJ7sa37v&;tj=t%?swJ@MYGXJaQppseKIyf<%le(!5$ z{aY*+Ch+|7>%+C5RYLwiMLeMMr~M5Kd&DA^R3TN#kgBpFOQ_2grK%cgRWr0|zz9@> zMzE?II=3m6kP(74N+oQB`4}-GGWncX>JG8A2eP-h5p5{xz+b4OWv0%TOD@x|ZMo&m zTCtMWSa^Q6Fl#O>W|!w#Utwi-K5H)K7Ot7g`2q_URjv$d+U)tFk5UKpd8`Lktc5GXj-7sS9nJY`h}m<8fo)SQe5y zD5M9SIIsy|uZWoETC_)t`5OArC$t81LT(}5&s4WuwOQCpE0@*v^4dC6Dx2Ritz5J0 zTTHFlw_K)dQJdoCOo#NFm9pcqKyho!u31ch85RTyE5o#J%VxR80x)dWT^1}mrcmfXHJEM!N!c~cW&|dL62fg=fbU6bZ1QmCVCJ^^Lw{>~sPdwHWKjxq46RnX8V2HMc;xL3; z!?-Nc8cEz%ccX3KB_+RwKtY)|CWecR2}Jx_1 z=_qW&qUasA0d^9_Zlf0fL~yvKC%A}M4{g)z>qOUSqc|>30YG>o7kSS0=rcY3SdZ^d z9bP`T{6wGP#n_FWj{86IPAAYSiQuDX9y{LLLc#J1c5K?+(0SRJE~+G4c;|!*^b$-o`@3p_HO0J%o*PYq2eI>d zK=IZS1II_c;X8}WbQnNXj$c9;p}4PtMT>t0 zcoI8u8te|9FL1j1Lfy|l)hD>mxpW_1=(S?iHcb{b&FZFAub>5?{bq)J*GVZxXA++-W(yoB@J;15c`ZETfmYZUWMRRE@DYxOF{e+3l(3{dfq literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..823233e10c5ccb12ad65b2c66d61da04ef28838c GIT binary patch literal 6148 zcmeHK!A`?440XnYI^Zyg3n!El_x(XP$_Jn!afP-MAhk-R!UZn8Q|}Og)*jsT|ocoz~EZ|;1tVVFy~%^W2|5**ad_IVnQe|gc@lvObExi z6~9!l3mC%5Nb_OjWFsAl@uy>d>)pwu0d-Ucl!2ImgYY=#`hWF({~rhGlQN(T{3`~` zG+SpYTuEx{;Bs7RE%Xk`!hT)AZ3rg56vLNG@fp+$)-6wfsbCinD-iu7;Azl78TeBM FJ^`C=T8jVx literal 0 HcmV?d00001 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