diff --git a/CLAUDE.md b/CLAUDE.md index 0bdfec7..6c14e44 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,42 +8,58 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Core Architecture -### Chan Theory Engine (`Chan*.py`) +### Chan Theory Engine (`chan/` package) -Data processing pipeline (each step feeds the next): +引擎已分层解耦;根目录 `Chan*.py` / `TF_DF.py` 仅为兼容旧 import 的 shim。新代码优先: -1. **`ChanKLU.py`** — Raw K-line unit with TA indicators (EMA, MACD, RSI, Bollinger Bands) and candlestick pattern recognition (`Chan_KLU_PATTERN`) -2. **`ChanKLC.py`** — Combined K-line: inclusion processing (包含处理), fractal (分型) detection. Linked-list structure with `.next`/`.pre` pointers -3. **`ChanBI.py`** — Stroke (笔): basic trend unit connecting alternating fractals -4. **`ChanSBI.py`** — Special Stroke: aggregates multiple BI into higher-level units with fractal detection, feeds into SEG -5. **`ChanSEG.py`** — Segment (线段): built from SBI strokes -6. **`ChanZS.py`** / **`ChanBIZS.py`** — Center/pivot (中枢): consolidation zones (segment-level and stroke-level) -7. **`ChanBSP.py`** — Buy/Sell points (买卖点): Type 1/2/3 signals -8. **`ChanLun.py`** — Main orchestrator: ties all steps together, entry point -9. **`TF_DF.py`** — Timeframe-aware DataFrame processor: resamples data, runs the full pipeline per timeframe, handles multi-timeframe analysis +```python +from chan import ChanLun, TF_DF +from chan.indicators import IndicatorEngine, IndicatorStore +from chan.analysis.bsp_macd import confirm_bsp, bi_macd_area +``` + +| 层 | 路径 | 职责 | +|----|------|------| +| **core** | `chan/core/` | 纯结构:KLU→KLC→BI→SBI→SEG→ZS→BSP 几何;不依赖 talib/指标参数 | +| **indicators** | `chan/indicators/` | `IndicatorConfig` / `IndicatorEngine` / `IndicatorStore`;MACD/EMA/BB/RSI 外置计算 | +| **analysis** | `chan/analysis/` | 结构+指标接合:`ChanMACD`、`bsp_macd`(ConfirmedBSP)、Zone/Classifier 等 | +| **pipeline** | `chan/pipeline/` | `TF_DF` / `ChanLun` 编排:先指标再结构,可选 attach 兼容 | + +流水线: + +1. **`chan/core/ChanKLU.py`** — K 线单元(OHLCV + 结构链);指标字段仅兼容挂载 +2. **`chan/core/ChanKLC.py`** — 合并 K 线:包含处理、分型 +3. **`chan/core/ChanBI.py`** — 笔 +4. **`chan/core/ChanSBI.py`** — 特征笔 +5. **`chan/core/ChanSEG.py`** — 线段 +6. **`chan/core/ChanZS.py`** / **`ChanBIZS.py`** — 中枢 +7. **`chan/core/ChanBSP.py`** — 几何买卖点 +8. **`chan/analysis/bsp_macd.py`** — 买卖点 × MACD 背驰 → `ConfirmedBSP` +9. **`chan/pipeline/ChanLun.py`** / **`TF_DF.py`** — 多周期/单周期编排 ### Support modules -- **`ChanEnum.py`** — All enumerations: K-line types, fractal types, MACD states, buy/sell point types, EMA position/semantic states, K-line patterns -- **`ChanCTime.py`** — Chan theory time utility: auto-adaptive day understanding (e.g. crypto 24h vs stock market hours) -- **`ChanMACD.py`** / **`ChanMACDHistSet.py`** / **`ChanMACDSeg.py`** / **`ChanMACDUnitTF.py`** — MACD state analysis and divergence detection -- **`ChanPY.py`** — Consolidation (盘整) analysis -- **`ChanHeng.py`** — Sideways market analysis -- **`Chan_FX_Box.py`** — Fractal box (分型箱体) detection -- **`ChanLun_Classifier.py`** — Standalone classifier script: runs full pipeline and classifies market states +- **`chan/core/ChanEnum.py`** — 枚举 +- **`chan/core/ChanCTime.py`** — 时间工具 +- **`chan/analysis/ChanMACD*.py`** — MACD 状态/段分析(读 KLU 上兼容指标或 Store) +- **`chan/analysis/ChanZone.py`** / **`ChanHeng.py`** / **`ChanLun_Classifier.py`** 等 — 分析扩展 +- **`chan/analysis/ChanPY.py`** — 外部 chan.py 桥接(与本包名冲突已隔离) ### Services - **`data_provider/`** — FastAPI data service: fetches crypto data from Binance via CCXT, caches to CSV, serves REST API + WebSocket. Synthesizes derived timeframes (e.g. 5m/15m/4h from 1m/1h base). Port 9009. -- **`web/`** — Flask web UI for interactive chart visualization with Chan theory overlays. Port 8123. +- **`web/`** — Flask web UI for interactive chart visualization with Chan theory overlays. Port 8123.(本重构分支不改 web) - **`strategies/`** — Freqtrade trading strategies using the Chan theory engine (53 strategies) - **`config/`** — Freqtrade JSON config files per pair/timeframe ### Data Flow ``` -Exchange (CCXT) → data_provider (CSV cache) → Freqtrade → Strategy → ChanLun → TF_DF - → KLU → KLC → BI → SBI → SEG → ZS → BSP +Exchange (CCXT) → data_provider (CSV cache) → Freqtrade → Strategy + → ChanLun / TF_DF + → IndicatorEngine → IndicatorStore + → KLU → KLC → BI → SBI → SEG → ZS → 几何 BSP + → analysis.bsp_macd → ConfirmedBSP ``` ## Common Commands diff --git a/ChanBI.py b/ChanBI.py index 19aaee3..7c8ec4f 100644 --- a/ChanBI.py +++ b/ChanBI.py @@ -1,132 +1,5 @@ -from decimal import Decimal -import ChanKLC -from ChanEnum import Chan_BI_DIR -class ChanBI(): - def __init__(self, klc: ChanKLC, index, ddir=Chan_BI_DIR.UP): - self.start_klc = klc - self.end_klc = klc - self.next = None - self.pre = None - self.dir = ddir - self.index = index - self.is_sure = False - self.high = klc.high - self.low = klc.low - self.sure_time = None - self.klc_list = [] - self.klc_list.append(klc) - self.end_time = klc.end_time - self.start_time = klc.start_time - self.macd_hist = 0 - self.macd_div = 0 - self.seg = None - self.height = 0 - self.width = 0 - self.slop = 0 - self.fib_list = [] - self.seg_index = 0 - self.bi_zs = None - self.seg_zs = None - def set_bi_zs(self, bi_zs): - for klc in self.klc_list: - klc.set_bi_zs(bi_zs) - def set_seg(self, seg): - self.seg = seg - self.seg_index = len(seg.bi_list)-1 - def set_macdhist(self, macd_hist): - self.macd_hist = macd_hist - def set_macd_div(self, macd_div): - self.macd_div = macd_div - def cal_macd_div(self): - self.macd_div = 0.0 - if self.pre and self.pre.pre: - if self.pre.pre.macd_hist == 0: - self.macd_div = 0.0 - else: - self.macd_div = self.macd_hist / self.pre.pre.macd_hist - #print(self.start_time, self.end_time, self.macd_hist, self.pre.pre.macd_hist, self.macd_div) - def cal_macdhist(self): - self.macd_hist = 0 - for klc in self.klc_list: - for klu in klc.klu_list: - if self.dir == Chan_BI_DIR.UP and klu.macdhist > 0: - self.macd_hist += klu.macdhist - if self.dir == Chan_BI_DIR.DOWN and klu.macdhist < 0: - self.macd_hist -= klu.macdhist - def check_bi_zs_overlap(self): - if self.next and self.next.next: - if self.dir == Chan_BI_DIR.UP: - return self.low < self.next.next.high - else: - return self.high > self.next.next.low - else: - return False - def check_overlap(self): - if self.next and self.next.next and self.next.next.is_sure: - if self.dir == Chan_BI_DIR.UP: - return self.high > self.next.low and self.high < self.next.next.high - else: - return self.high > self.next.high and self.low > self.next.next.low - else: - return False - def set_end_klc(self, klc, sure_klc): - if self.dir == Chan_BI_DIR.UP and klc.high > self.high: - self.high = klc.high - if self.dir == Chan_BI_DIR.DOWN and klc.low < self.low: - self.low = klc.low - self.end_klc = klc - self.set_is_sure(True, sure_klc.end_time) - self.end_time = klc.end_time - self.cal_properties() - #print(self.start_time, klc.fx, "This bi is ended", len(self.klc_list), klc.index - self.start_klc.index) - def cal_properties(self): - if self.is_sure: - self.height = float(format(self.high - self.low, ".2f")) - self.width = self.end_klc.index - self.start_klc.index - self.slop = float(format(self.height / self.width, ".2f")) - fib_list = [0.0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0] - for fib in fib_list: - self.fib_list.append(float(format(self.height * fib + self.low, ".2f"))) - #print(self.end_time, self.height, self.width, self.slop, self.fib_list) - def set_is_sure(self, is_sure, time): - self.is_sure = is_sure - self.sure_time = time - def set_start_klc(self, klc, ddir): - self.start_klc = klc - self.klc_list = [] - self.klc_list.append(klc) - self.high = klc.high - self.low = klc.low - self.dir = ddir - def set_pre(self, bi): - self.pre = bi - def set_next(self, bi): - self.next = bi - def add_klc(self, klc): - added = False - if len(self.klc_list) > 0: - for index in range(0, len(self.klc_list)): - if self.klc_list[index].index == klc.index: - added = True - break - if not added: - self.klc_list.append(klc) - #print(self.start_time, klc.start_time) - #print(klc.end_time, klc.index) - self.end_klc = klc - self.end_time = klc.klu_list[-1].time - self.cal_macdhist() - self.cal_macd_div() - def append_klc_list(self, klc_list): - self.klc_list.append(klc_list) - def get_decimal(self, value): - return Decimal("{:.2f}".format(value)) - def update_bi(self, klc): - self.end_klc = None - if self.dir == Chan_BI_DIR.UP and klc.high > self.high: - self.high = klc.high - if self.dir == Chan_BI_DIR.DOWN and klc.low < self.low: - self.low = klc.low - self.is_sure = False - self.sure_time = None - #print(self.start_time, klc.start_time, klc.fx, "This bi is extended") \ No newline at end of file +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.core.ChanBI") +sys.modules[__name__] = _impl diff --git a/ChanBIZS.py b/ChanBIZS.py index 58b1d30..1c037c4 100644 --- a/ChanBIZS.py +++ b/ChanBIZS.py @@ -1,161 +1,5 @@ -from ChanEnum import Chan_ZS_DIR, Chan_ZS_TYPE, Chan_BI_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.start_bi = start_bi - self.bi_list = [] - self.bi_list.append(start_bi) - self.end_bi = None - self.bi_out = None - self.is_sure = False - self.zg = 0 - self.zd = 0 - self.gg = 0 - self.dd = 0 - self.dir = ddir - self.sure_time = None - self.end_klc = None - self.zs_type = Chan_ZS_TYPE.NORMAL - start_bi.set_bi_zs(self) - def set_end_bi(self, end_bi, sure_time): - self.end_bi = end_bi - self.set_end_time(end_bi.end_klc.end_time) - self.is_sure = True - self.sure_time = sure_time - end_bi.set_bi_zs(self) - #print(self.start_time, self.is_sure, len(self.bi_list), self.dir, self.zs_type) - def set_end_time(self, end_time): - self.end_time = end_time - def set_zg(self, zg): - self.zg = zg - def set_zd(self, zd): - self.zd = zd - def set_gg(self, gg): - self.gg = gg - def set_dd(self, dd): - self.dd = dd - def add_bi(self, bi: ChanBI): - if bi: - self.bi_list.append(bi) - if bi.high > self.gg: - self.gg = bi.high - if bi.low < self.dd: - self.dd = bi.low - bi.set_bi_zs(self) - self.classify_zs() - def set_pre(self, pre): - self.pre = pre - def set_next(self, next): - self.next = next - def classify_zs(self): - """ - 根据中枢内笔的高低点变化趋势,对中枢进行分类 - - 分类逻辑: - - 取中枢内向上笔的高点(peaks)和向下笔的低点(valleys) - - 比较前半段和后半段的均值,判断高点和低点的整体趋势 - - 分类结果: - - RISING 上升中枢:高点抬高 + 低点抬高 → 多方占优,可能向上突破 - - FALLING 下行中枢:高点降低 + 低点降低 → 空方占优,可能向下突破 - - CONVERGING 收敛中枢:高点降低 + 低点抬高 → 区间收窄,即将选择方向 - - DIVERGING 扩散中枢:高点抬高 + 低点降低 → 波动加剧,市场不稳定 - - NORMAL 常规中枢:无明显趋势 → 多空均衡,区间震荡 - """ - if len(self.bi_list) < 3: - self.zs_type = Chan_ZS_TYPE.NORMAL - return - - # 提取向上笔的高点(peaks)和向下笔的低点(valleys) - peaks = [bi.high for bi in self.bi_list if bi.dir == Chan_BI_DIR.UP] - valleys = [bi.low for bi in self.bi_list if bi.dir == Chan_BI_DIR.DOWN] - - high_trend = self._calc_trend(peaks) - low_trend = self._calc_trend(valleys) - - if high_trend > 0 and low_trend > 0: - self.zs_type = Chan_ZS_TYPE.RISING - elif high_trend < 0 and low_trend < 0: - self.zs_type = Chan_ZS_TYPE.FALLING - elif high_trend < 0 and low_trend > 0: - self.zs_type = Chan_ZS_TYPE.CONVERGING - elif high_trend > 0 and low_trend < 0: - self.zs_type = Chan_ZS_TYPE.DIVERGING - else: - self.zs_type = Chan_ZS_TYPE.NORMAL - - def _calc_trend(self, values): - """ - 计算序列的趋势方向 - 将序列分为前后两半,比较均值: - - 后半均值 > 前半均值 → 返回 1(上升趋势) - - 后半均值 < 前半均值 → 返回 -1(下降趋势) - - 相等或数据不足 → 返回 0(无趋势) - - 使用均值比较而非首尾比较,可以过滤单笔异常波动带来的误判 - """ - if len(values) < 2: - return 0 - mid = len(values) // 2 - first_half = values[:mid] if mid > 0 else values[:1] - second_half = values[mid:] - avg_first = sum(first_half) / len(first_half) - avg_second = sum(second_half) / len(second_half) - # 使用中枢区间的一定比例作为阈值,避免微小波动误判 - threshold = abs(avg_first) * 0.005 if avg_first != 0 else 0 - if avg_second - avg_first > threshold: - return 1 - elif avg_first - avg_second > threshold: - return -1 - else: - return 0 - - def is_weakening(self): - """ - 判断中枢是否在衰弱(即将反向突破的信号) - - 衰弱条件: - 1. 中枢内笔数 >= 5(有足够的数据判断) - 2. 最后一笔的MACD面积相比同方向前一笔出现背驰(macd_div < 1) - 3. 中枢类型为收敛型或常规型 - - 返回: True表示中枢力量衰弱,可能反向 - """ - if len(self.bi_list) < 5: - return False - last_bi = self.bi_list[-1] - # 最后一笔与同方向前一笔比较MACD面积是否背驰 - if last_bi.macd_div > 0 and last_bi.macd_div < 1.0: - return True - return False - - def get_zs_strength(self): - """ - 计算中枢强度,用于辅助判断中枢延续还是反向 - - 返回字典包含: - - type: 中枢类型 (Chan_ZS_TYPE) - - bi_count: 中枢内笔数 - - range_ratio: 中枢区间占比 = (zg - zd) / (gg - dd),越小说明中枢越紧密 - - last_bi_div: 最后一笔的MACD背驰比率 - - is_weakening: 是否衰弱 - - is_extending: 是否在延伸(笔数 >= 9 可能升级) - """ - total_range = self.gg - self.dd if self.gg != self.dd else 1 - zs_range = self.zg - self.zd if self.zg != self.zd else 0 - range_ratio = zs_range / total_range if total_range > 0 else 0 - last_bi_div = self.bi_list[-1].macd_div if len(self.bi_list) > 0 else 0 - - return { - 'type': self.zs_type, - 'bi_count': len(self.bi_list), - 'range_ratio': round(range_ratio, 4), - 'last_bi_div': round(last_bi_div, 4), - 'is_weakening': self.is_weakening(), - 'is_extending': len(self.bi_list) >= 9, # 9段可能升级 - } \ No newline at end of file +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.core.ChanBIZS") +sys.modules[__name__] = _impl diff --git a/ChanBSP.py b/ChanBSP.py index 568cd14..7a5b6eb 100644 --- a/ChanBSP.py +++ b/ChanBSP.py @@ -1,24 +1,5 @@ -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.end_klc - self.index = index - self.type = type - self.start_time = self.klc.start_time - self.end_time = self.klc.end_time - if sure_time: - self.is_sure = True - self.sure_time = sure_time - else: - self.is_sure = False - self.sure_time = None - self.dir = ddir - self.zs_count = zs_count - self.zs = zs - self.seg = bi.seg - def set_sure_time(self, sure_time): - self.is_sure = True - self.sure_time = sure_time \ No newline at end of file +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.core.ChanBSP") +sys.modules[__name__] = _impl diff --git a/ChanCTime.py b/ChanCTime.py index 04e1bf6..6519ae3 100644 --- a/ChanCTime.py +++ b/ChanCTime.py @@ -1,44 +1,5 @@ -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 +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.core.ChanCTime") +sys.modules[__name__] = _impl diff --git a/ChanEnum.py b/ChanEnum.py index 9164374..d13ce9b 100644 --- a/ChanEnum.py +++ b/ChanEnum.py @@ -1,368 +1,5 @@ -from enum import Enum, auto -from typing import Literal - - -class Chan_DATA_SRC(Enum): - BAO_STOCK = auto() - CCXT = auto() - CSV = auto() - -class Chan_ZS_DIR(Enum): - UP = auto() - DOWN = auto() - -class Chan_ZS_TYPE(Enum): - """中枢类型分类""" - NORMAL = auto() # 常规中枢:高低点无明显趋势,区间震荡 - RISING = auto() # 上升中枢:高点抬高,低点也抬高,重心上移 - FALLING = auto() # 下行中枢:高点降低,低点也降低,重心下移 - CONVERGING = auto() # 收敛中枢:高点降低,低点抬高,区间收窄(三角收敛) - DIVERGING = auto() # 扩散中枢:高点抬高,低点降低,区间扩大(喇叭口) -class Chan_K_DIR(Enum): - BULL = auto() - BEAR = auto() - CROSS = auto() - -class Chan_EMA_POS(Enum): - """K线与任意EMA的位置关系(与趋势方向无关的客观分类,支持threshold容差)""" - ABOVE = auto() # 完全在EMA上方(远离):low > ema + threshold - NEAR_ABOVE = auto() # 在EMA上方但接近:ema < low <= ema + threshold - CROSS_CLOSE_ABOVE = auto() # 跨越EMA,收盘在上方:close > ema, low <= ema(含threshold范围内触碰) - ON_EMA = auto() # 收盘价在EMA附近:abs(close - ema) <= threshold - CROSS_CLOSE_BELOW = auto() # 跨越EMA,收盘在下方:close < ema, high >= ema(含threshold范围内触碰) - NEAR_BELOW = auto() # 在EMA下方但接近:ema - threshold <= high < ema - BELOW = auto() # 完全在EMA下方(远离):high < ema - threshold - UNKNOWN = auto() # 未知(EMA值无效) - -class Chan_EMA_SEMANTIC(Enum): - """K线与EMA结合趋势方向的语义状态(用于交易判断)""" - STRONG_TREND = auto() # 7: 顺势K线完全在EMA趋势侧(强势,远未及EMA) - TREND_SIDE = auto() # 6: 完全在EMA趋势侧(正常趋势运行) - RECOVER = auto() # 5: 逆势后穿越EMA回到趋势侧(收复EMA,趋势恢复) - TOUCH_FAIL = auto() # 4: 逆势触碰EMA但未穿越(反弹/反抽力度不足) - DEEP_COUNTER = auto() # 3: 完全在EMA逆势侧(深度回调/反抽) - BREAK = auto() # 2: 穿越EMA,收盘在逆势侧(支撑/压力失败) - TOUCH_HOLD = auto() # 1: 触碰EMA,收盘守住趋势侧(支撑/压力有效) - WEAK_COUNTER = auto() # 8: 逆势K线完全在EMA逆势侧(弱势,远未到EMA) - APPROACHING = auto() # 9: K线接近EMA但未触碰(即将测试支撑/压力) - NEUTRAL = auto() # 0: 盘整/无法判断 -class Chan_KL_TYPE(Enum): - K_1S = auto() - K_1M = auto() - K_DAY = auto() - K_WEEK = auto() - K_MON = auto() - K_YEAR = auto() - K_5M = auto() - K_15M = auto() - K_30M = auto() - K_60M = auto() - K_1H = auto() - K_2H = auto() - K_4H = auto() - K_6H = auto() - K_8H = auto() - K_12H = auto() - K_1D = auto() - K_3D = auto() - K_3M = auto() - K_QUARTER = auto() - - -class Chan_KLINE_DIR(Enum): - UP = auto() - DOWN = auto() - COMBINE = auto() - INCLUDED = auto() -class Chan_KLU_TYPE(Enum): - BigBull = auto() - MiddleBull = auto() - SmallBull = auto() - BigBear = auto() - MiddleBear = auto() - SmallBear = auto() - Cross = auto() - -class Chan_KLU_PATTERN(Enum): - # 单根K线形态 - HAMMER = auto() # 锤子线 - INVERTED_HAMMER = auto() # 倒锤子线 - SHOOTING_STAR = auto() # 射击之星 - HANGING_MAN = auto() # 上吊线 - DOJI = auto() # 十字星 - LONG_LEGGED_DOJI = auto() # 长腿十字星 - GRAVESTONE_DOJI = auto() # 墓碑十字星 - DRAGONFLY_DOJI = auto() # 蜻蜓十字星 - MARUBOZU = auto() # 光头光脚 - SPINNING_TOP = auto() # 纺锤线 - - # 双根K线形态 - BULLISH_ENGULFING = auto() # 看涨吞没 - BEARISH_ENGULFING = auto() # 看跌吞没 - PIERCING_LINE = auto() # 刺透形态 - DARK_CLOUD_COVER = auto() # 乌云盖顶 - TWEEZER_TOP = auto() # 镊子顶 - TWEEZER_BOTTOM = auto() # 镊子底 - HARAMI = auto() # 孕线 - BULLISH_HARAMI = auto() # 看涨孕线 - BEARISH_HARAMI = auto() # 看跌孕线 - - # 三根K线形态 - MORNING_STAR = auto() # 早晨之星 - EVENING_STAR = auto() # 黄昏之星 - THREE_WHITE_SOLDIERS = auto() # 红三兵 - THREE_BLACK_CROWS = auto() # 三只乌鸦 - THREE_INNER_UP = auto() # 上升三法 - THREE_INNER_DOWN = auto() # 下降三法 - ABANDONED_BABY = auto() # 弃婴形态 - - # 多根K线形态 - DOUBLE_TOP = auto() # 双顶 - DOUBLE_BOTTOM = auto() # 双底 - TRIPLE_TOP = auto() # 三顶 - TRIPLE_BOTTOM = auto() # 三底 - HEAD_AND_SHOULDERS = auto() # 头肩顶 - INVERSE_HEAD_SHOULDERS = auto() # 头肩底 - ROUNDING_BOTTOM = auto() # 圆弧底 - ROUNDING_TOP = auto() # 圆弧顶 - - # 缺口形态 - BREAKAWAY_GAP = auto() # 突破缺口 - RUNAWAY_GAP = auto() # 持续缺口 - EXHAUSTION_GAP = auto() # 衰竭缺口 - - # 特殊形态 - ISLAND_REVERSAL = auto() # 岛形反转 - KEY_REVERSAL = auto() # 关键反转 - INSIDE_BAR = auto() # 内包线 - OUTSIDE_BAR = auto() # 外包线 - - # 趋势形态 - HIGHER_HIGH = auto() # 更高高点 - HIGHER_LOW = auto() # 更高低点 - LOWER_HIGH = auto() # 更低高点 - LOWER_LOW = auto() # 更低低点 - - # 支撑阻力形态 - SUPPORT_BOUNCE = auto() # 支撑反弹 - RESISTANCE_REJECTION = auto() # 阻力拒绝 - BREAKOUT = auto() # 突破 - BREAKDOWN = auto() # 跌破 - - # 成交量相关形态 - VOLUME_SPIKE = auto() # 成交量激增 - VOLUME_DECLINE = auto() # 成交量萎缩 - - # 未知/无形态 - UNKNOWN = auto() # 未知形态 - - -class Chan_FX_TYPE(Enum): - BOTTOM = auto() - TOP = auto() - UNKNOWN = auto() - UP = auto() - DOWN = auto() - TT = auto() - BB = auto() - PTOP = auto() - PBOTTOM = auto() -class Chan_FX(Enum): - CONTINUATION = auto() - REVERSAL = auto() - UNKNOWN = auto() -class Chan_PRICE_TREND(Enum): - UP = auto() - DOWN = auto() - FLAT = auto() - UNKNOWN = auto() -class Chan_KLC_FX(Enum): - TOP0 = auto() - TOP1 = auto() - TOP2 = auto() - TOP3 = auto() - TOP4 = auto() - TOP5 = auto() - TOP6 = auto() - TOP7 = auto() - TOP8 = auto() - BOTTOM0 = auto() - BOTTOM1 = auto() - BOTTOM2 = auto() - BOTTOM3 = auto() - BOTTOM4 = auto() - BOTTOM5 = auto() - BOTTOM6 = auto() - BOTTOM7 = auto() - BOTTOM8 = auto() - UNKNOWN = auto() -# 统一的MACD状态枚举,包含所有可能的状态 -class Chan_MACD_STATE(Enum): - """MACD状态枚举 - 包含所有可能的状态""" - # 穿越状态 - CROSS0_UP = auto() # 穿零轴后快速向上,能量柱呈现一根比一根长的排列方式 - CROSS0_DOWN = auto() # 穿零轴后快速向下,能量柱呈现一根比一根短的排列方式 - - CROSS_OS = auto() # 穿零轴后缠绕/粘合,黄白线沿着能量柱运行,黄白线在运行的过程中没有释放出反向能量柱 - CROSS_REV = auto() # 穿零轴后倒挂,MACD黄白线在穿零轴的时候与零轴的距离比较近,同时黄白线沿着能量柱运行,在运行的过程中,能量柱衰减导致它跟黄白线之间形成夹角空位,同时黄白线产生交叉并释放反向能量柱。 - - # 趋势状态 - NEAR0 = auto() - NEAR0_52 = auto() # 价格在EMA52附近/价格接触EMA52并马上离开,需要观察离开强度 - NEAR0_DIFF = auto() # MACD白线接近零轴,价格未到EMA52 - NEAR0_PERFECT = auto() # MACD白线接近零轴和价格接触或短暂击穿EMA52,而MACD黄线不穿零轴,完美形态 - NEAR0_24 = auto() # MACD黄白线接近零轴和价格在EMA24附近 - # 位置状态 - HIGH = auto() # 高位:MACD黄白线离开能量柱到高点,能量柱最大开始减弱 - HIGH_EMPTY = auto() # 高位空:MACD黄白线处于高位,能量柱衰减,与黄白线形成空间夹角 - RETURN_ZERO = auto() # 归零轴:能量柱呈现一根比一根短的排列方式 - RZ_UP = auto() # 归零轴后的零轴上涨 - RZ_DOWN = auto() # 归零轴后的零轴下跌 - UP = auto() # 穿零轴后向上 - DOWN = auto() # 穿零轴后向下 - PEAK = auto() # 峰值:MACD白线处于高位 - # 基础状态 - UNKNOWN = auto() # 未知 - START = auto() # 开始 -class Chan_MACDSEG_DIR(Enum): - ABOVE = auto() - UNDER = auto() -class Chan_MACDUNITTF_TYPE(Enum): - START = auto() - CROSS0 = auto() - NEAR0 = auto() -class Chan_MACDUNITTF_JUMP(Enum): - CONTUNE = auto() - DISCRETE = auto() -class Chan_MACDUNITTF_DIV(Enum): - CONTUNE = auto() - DISCRETE = auto() - UNDIV = auto() -class Chan_MACDHISTSET_DIR(Enum): - ABOVE = auto() - UNDER = auto() -class Chan_MACDUNITTF_DIR(Enum): - ABOVE = auto() - UNDER = auto() -class Chan_MACDHIST_STATE(Enum): - UP = auto() - DOWN = auto() - PEAK = auto() - UNKNOWN = auto() - -class Chan_BI_DIR(Enum): - UP = auto() - DOWN = auto() - -class Chan_SEG_DIR(Enum): - UP = auto() - DOWN = auto() - -class Chan_BI_TYPE(Enum): - UNKNOWN = auto() - STRICT = auto() - SUB_VALUE = auto() # 次高低点成笔 - TIAOKONG_THRED = auto() - DAHENG = auto() - TUIBI = auto() - UNSTRICT = auto() - TIAOKONG_VALUE = auto() - - -Chan_BSP_MAIN_TYPE = Literal['1', '2', '3'] - -class Chan_BSP_DIR(Enum): - BUY = auto() - SELL = auto() -class Chan_BSP_TYPE(Enum): - B1 = auto() - B2 = auto() - B3 = auto() - S1 = auto() - S2 = auto() - S3 = auto() - NONE = auto() -""" -class Chan_BSP_TYPE(Enum): - T1 = '1' - T1P = '1p' - T2 = '2' - T2S = '2s' - T3A = '3a' # 中枢在1类后面 - T3B = '3b' # 中枢在1类前面 - T3 = '3' - T3E ='3e' # T3退出点 - QJT = 'qjt' # 区间套突破 - QJT1 = 'qjt1' # 区间套一类买点 - QJT2 = 'qjt2' # 区间套一类卖点 - QJT3 = 'qjt3' # 区间套三类买点 - def main_type(self) -> Chan_BSP_MAIN_TYPE: - return self.value[0] # type: ignore - -""" -class Chan_AUTYPE(Enum): - QFQ = auto() - HFQ = auto() - NONE = auto() - - -class Chan_TREND_TYPE(Enum): - MEAN = "mean" - MAX = "max" - MIN = "min" - - -class Chan_TREND_LINE_SIDE(Enum): - INSIDE = auto() - OUTSIDE = auto() - - -class Chan_LEFT_SEG_METHOD(Enum): - ALL = auto() - PEAK = auto() - - -class Chan_FX_CHECK_METHOD(Enum): - STRICT = auto() - LOSS = auto() - HALF = auto() - TOTALLY = auto() - - -class Chan_SEG_TYPE(Enum): - BI = auto() - SEG = auto() - - -class Chan_MACD_ALGO(Enum): - AREA = auto() - PEAK = auto() - FULL_AREA = auto() - DIFF = auto() - SLOPE = auto() - AMP = auto() - VOLUMN = auto() - AMOUNT = auto() - VOLUMN_AVG = auto() - AMOUNT_AVG = auto() - TURNRATE_AVG = auto() - RSI = auto() - - -class Chan_DATA_FIELD: - FIELD_TIME = "time_key" - FIELD_OPEN = "open" - FIELD_HIGH = "high" - FIELD_LOW = "low" - FIELD_CLOSE = "close" - FIELD_VOLUME = "volume" # 成交量 - FIELD_TURNOVER = "turnover" # 成交额 - FIELD_TURNRATE = "turnover_rate" # 换手率 - -class Chan_KLC_STATE: - """笔当下状态(缠论笔定理)。任意时刻必属其一。""" - S10 = "(1, 0)" # 顶分型构造中 (1,0) - S_10 = "(-1, 0)" # 底分型构造中 (-1,0) - S11 = "(1,1)" # 向上笔延续中 - S_11 = "(-1,1)" # 向下笔延续中 - UNKNOWN = "Unknown" # 初始状态 +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.core.ChanEnum") +sys.modules[__name__] = _impl diff --git a/ChanHeng.py b/ChanHeng.py index 973837a..5093c3d 100644 --- a/ChanHeng.py +++ b/ChanHeng.py @@ -1,414 +1,5 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -""" -使用 ccxt 获取币安交易所所有 `*/USDT` 交易对最新 100 根 1 小时 K 线数据,并筛选出长期横盘的币种。 - -横盘判定基于以下三项指标(均可通过命令行参数调整): -1. 价格振幅占均价的比例(默认 ≤ 5%) -2. 收盘价线性回归斜率占均价的比例(默认 ≤ 0.05%) -3. 收盘价标准差占均价的比例(默认 ≤ 1.5%) - -满足以上全部条件的交易对会被视为长期横盘。 -""" - -import argparse -import csv -import logging -import math -import statistics +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib import sys -import time -from dataclasses import dataclass -from typing import Iterable, List, Optional, Sequence - -import ccxt - -# python ChanHeng.py --range-threshold 5 --slope-threshold 5 --std-threshold 0.015 - -DEFAULT_LIMIT = 100 -DEFAULT_TIMEFRAME = "1h" -STABLECOINS = { - "USDT", - "USDC", - "BUSD", - "TUSD", - "USDP", - "DAI", - "FDUSD", - "SUSD", - "UST", - "USTC", - "EUR", - "TRY", - "BFUSD", - "USDE", - "XUSD", - "USD1", - "XUSD" -} - - -@dataclass -class SidewaysMetrics: - symbol: str - price_range_pct: float - slope_pct: float - std_pct: float - mean_close: float - last_close: float - data_points: int - - -def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="筛选币安长期横盘币种(默认 500 根 1 小时 K 线)" - ) - parser.add_argument( - "--timeframe", - default=DEFAULT_TIMEFRAME, - help="K 线周期(默认:1h)", - ) - parser.add_argument( - "--limit", - type=int, - default=DEFAULT_LIMIT, - help="每个交易对获取的 K 线数量(默认:500)", - ) - parser.add_argument( - "--range-threshold", - type=float, - default=0.05, - help="最大价格振幅占均价比例阈值(默认:0.05,表示 5%%)", - ) - parser.add_argument( - "--slope-threshold", - type=float, - default=0.0005, - help="线性回归斜率占均价比例阈值(默认:0.0005,约 0.05%%)", - ) - parser.add_argument( - "--std-threshold", - type=float, - default=0.015, - help="标准差占均价比例阈值(默认:0.015,表示 1.5%%)", - ) - parser.add_argument( - "--quote", - action="append", - default=[], - help="只保留指定计价货币的交易对,可重复指定(示例:--quote USDT --quote FDUSD)", - ) - parser.add_argument( - "--symbol", - action="append", - default=[], - help="仅检测指定交易对,可重复(不指定则遍历所有符合条件的现货交易对)", - ) - parser.add_argument( - "--max-symbols", - type=int, - default=None, - help="限制最多检测的交易对数量(用于调试)", - ) - parser.add_argument( - "--sleep", - type=float, - default=0.35, - help="请求失败后的基础重试等待秒数(默认:0.35)", - ) - parser.add_argument( - "--retries", - type=int, - default=3, - help="单个交易对请求失败后的最大重试次数(默认:3)", - ) - parser.add_argument( - "--include-inactive", - action="store_true", - help="包含已下架/不可交易的交易对(默认不包含)", - ) - parser.add_argument( - "--export", - type=str, - default=None, - help="将筛选结果导出为 CSV 文件的路径", - ) - parser.add_argument( - "--verbose", - action="store_true", - help="输出更详细的日志信息", - ) - return parser.parse_args(argv) - - -def setup_logging(verbose: bool) -> None: - level = logging.DEBUG if verbose else logging.INFO - logging.basicConfig( - level=level, - format="%(asctime)s [%(levelname)s] %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - - -def create_exchange() -> ccxt.binance: - exchange = ccxt.binance({"enableRateLimit": True}) - exchange.options["defaultType"] = "spot" - return exchange - - -def iter_target_symbols( - exchange: ccxt.binance, - quotes: Sequence[str], - includes: Sequence[str], - include_inactive: bool, -) -> List[str]: - markets = exchange.load_markets() - filtered = [] - - quote_set = {quote.upper() for quote in quotes} - include_set = {sym.upper() for sym in includes} - - for symbol, meta in markets.items(): - if not meta.get("spot", False): - continue - if not include_inactive and meta.get("active") is False: - continue - - normalized_symbol = symbol.upper() - - if include_set and normalized_symbol not in include_set: - continue - - parts = symbol.split("/") - if len(parts) != 2: - continue - - base_asset, quote_asset = parts[0].upper(), parts[1].upper() - - target_quote = quote_set or {"USDT"} - if quote_asset not in target_quote: - continue - - if base_asset in STABLECOINS: - continue - - filtered.append(symbol) - - filtered.sort() - logging.info( - "已筛选 %s 个目标交易对(quote 过滤:%s,专门列表:%s)", - len(filtered), - ",".join(sorted(quote_set or {"USDT"})), - ",".join(sorted(include_set)) or "无", - ) - return filtered - - -def fetch_ohlcv_with_retry( - exchange: ccxt.binance, - symbol: str, - timeframe: str, - limit: int, - retries: int, - base_sleep: float, -) -> List[List[float]]: - attempt = 0 - while True: - try: - return exchange.fetch_ohlcv(symbol, timeframe=timeframe, limit=limit) - except ccxt.RateLimitExceeded as exc: - wait_time = max(exchange.rateLimit / 1000.0 if exchange.rateLimit else 0, base_sleep) - logging.debug("触发限频,等待 %.2f 秒后重试 %s:%s", wait_time, symbol, exc) - time.sleep(wait_time) - except (ccxt.NetworkError, ccxt.ExchangeError) as exc: - attempt += 1 - if attempt > retries: - logging.warning("多次获取失败,跳过 %s:%s", symbol, exc) - return [] - wait_time = base_sleep * attempt - logging.debug("请求失败,等待 %.2f 秒后重试 %s(第 %d 次):%s", wait_time, symbol, attempt, exc) - time.sleep(wait_time) - - -def linear_regression_slope(values: Sequence[float]) -> float: - n = len(values) - if n < 2: - return 0.0 - mean_x = (n - 1) / 2.0 - mean_y = sum(values) / n - numerator = 0.0 - denominator = 0.0 - for idx, value in enumerate(values): - dx = idx - mean_x - numerator += dx * (value - mean_y) - denominator += dx * dx - - if denominator == 0: - return 0.0 - return numerator / denominator - - -def compute_sideways_metrics(closes: Sequence[float], symbol: str) -> Optional[SidewaysMetrics]: - if not closes: - return None - - mean_close = sum(closes) / len(closes) - if math.isclose(mean_close, 0.0): - return None - - max_close = max(closes) - min_close = min(closes) - price_range_pct = (max_close - min_close) / mean_close - - slope = linear_regression_slope(closes) - slope_pct = slope / mean_close - - std_dev = statistics.pstdev(closes) if len(closes) > 1 else 0.0 - std_pct = std_dev / mean_close - - return SidewaysMetrics( - symbol=symbol, - price_range_pct=price_range_pct, - slope_pct=slope_pct, - std_pct=std_pct, - mean_close=mean_close, - last_close=closes[-1], - data_points=len(closes), - ) - - -def is_sideways(metrics: SidewaysMetrics, range_threshold: float, slope_threshold: float, std_threshold: float) -> bool: - return ( - metrics.price_range_pct <= range_threshold - and abs(metrics.slope_pct) <= slope_threshold - and metrics.std_pct <= std_threshold - ) - - -def export_results(path: str, results: Sequence[SidewaysMetrics]) -> None: - fieldnames = [ - "symbol", - "price_range_pct", - "slope_pct", - "std_pct", - "mean_close", - "last_close", - "data_points", - ] - with open(path, "w", newline="", encoding="utf-8") as fp: - writer = csv.DictWriter(fp, fieldnames=fieldnames) - writer.writeheader() - for item in results: - writer.writerow( - { - "symbol": item.symbol, - "price_range_pct": f"{item.price_range_pct:.6f}", - "slope_pct": f"{item.slope_pct:.6f}", - "std_pct": f"{item.std_pct:.6f}", - "mean_close": f"{item.mean_close:.8f}", - "last_close": f"{item.last_close:.8f}", - "data_points": item.data_points, - } - ) - logging.info("结果已导出至 %s", path) - - -def run(argv: Optional[Sequence[str]] = None) -> int: - args = parse_args(argv) - if not args.quote: - args.quote = ["USDT"] - setup_logging(args.verbose) - - exchange = create_exchange() - symbols = iter_target_symbols( - exchange=exchange, - quotes=args.quote, - includes=args.symbol, - include_inactive=args.include_inactive, - ) - - if args.max_symbols is not None: - symbols = symbols[: args.max_symbols] - logging.info("出于调试目的,仅检测前 %d 个交易对。", len(symbols)) - - if not symbols: - logging.error("未找到任何满足条件的交易对,请检查过滤条件。") - return 1 - - sideways_results: List[SidewaysMetrics] = [] - total = len(symbols) - - for idx, symbol in enumerate(symbols, start=1): - logging.info("(%d/%d) 正在获取 %s 的 %s K 线(limit=%d)", idx, total, symbol, args.timeframe, args.limit) - ohlcv = fetch_ohlcv_with_retry( - exchange=exchange, - symbol=symbol, - timeframe=args.timeframe, - limit=args.limit, - retries=args.retries, - base_sleep=args.sleep, - ) - - if len(ohlcv) < max(100, args.limit // 2): - logging.debug("交易对 %s 返回数据不足(%d 根),跳过。", symbol, len(ohlcv)) - continue - - closes = [entry[4] for entry in ohlcv if entry[4] is not None] - metrics = compute_sideways_metrics(closes, symbol) - if not metrics: - continue - - if is_sideways(metrics, args.range_threshold, args.slope_threshold, args.std_threshold): - sideways_results.append(metrics) - logging.info( - "识别为横盘:%s | 振幅 %.2f%% | 斜率 %.4f%% | 标准差 %.2f%%", - symbol, - metrics.price_range_pct * 100, - metrics.slope_pct * 100, - metrics.std_pct * 100, - ) - else: - logging.debug( - "未满足条件:%s | 振幅 %.2f%% | 斜率 %.4f%% | 标准差 %.2f%%", - symbol, - metrics.price_range_pct * 100, - metrics.slope_pct * 100, - metrics.std_pct * 100, - ) - - if not sideways_results: - logging.warning("未检测到满足定义的长期横盘交易对。") - return 0 - - sideways_results.sort(key=lambda item: (item.price_range_pct, abs(item.slope_pct), item.std_pct)) - print("=" * 88) - print( - f"共识别 {len(sideways_results)} 个长期横盘交易对(阈值:振幅≤{args.range_threshold:.2%}," - f"斜率≤{args.slope_threshold:.2%},标准差≤{args.std_threshold:.2%})" - ) - print("=" * 88) - header = f"{'Symbol':15s} {'Range%':>10s} {'Slope%':>10s} {'STD%':>10s} {'Mean':>14s} {'Last':>14s} {'Count':>6s}" - print(header) - print("-" * len(header)) - for item in sideways_results: - print( - f"{item.symbol:15s}" - f" {item.price_range_pct * 100:10.4f}" - f" {item.slope_pct * 100:10.4f}" - f" {item.std_pct * 100:10.4f}" - f" {item.mean_close:14.8f}" - f" {item.last_close:14.8f}" - f" {item.data_points:6d}" - ) - - if args.export: - export_results(args.export, sideways_results) - - logging.info("任务完成。") - return 0 - - -if __name__ == "__main__": - sys.exit(run()) - +_impl = importlib.import_module("chan.analysis.ChanHeng") +sys.modules[__name__] = _impl diff --git a/ChanKLC.py b/ChanKLC.py index 7dcafcb..fc19384 100644 --- a/ChanKLC.py +++ b/ChanKLC.py @@ -1,620 +1,5 @@ -import copy -from typing import Dict, Optional - -from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_KLC_FX -from ChanEnum import Chan_K_DIR, Chan_MACD_STATE, Chan_PRICE_TREND, Chan_EMA_POS -from ChanEnum import Chan_EMA_SEMANTIC, Chan_BSP_TYPE, Chan_KLC_STATE, Chan_FX -import ChanKLU -import ChanCTime -import Chan_FX_Box -# 根据结合律合并K线后的K线 -class ChanKLC(): - def __init__(self, klu: ChanKLU, index, ddir=Chan_KLINE_DIR.UP): - self.start_time = klu.time - self.end_time = None - self.high = klu.high - self.low = klu.low - self.dir = ddir - self.index = index - self.klu_list = [] - self.add_klu(klu) - self.fx = Chan_FX_TYPE.UNKNOWN - self.next = None - self.pre = None - self.start_klu = klu - self.end_klu = None - self.state = "00" - self.klc_state = Chan_KLC_STATE.UNKNOWN - self.open = klu.open - self.close = klu.close - self.volume = klu.volume - self.bi = None - self.distance = 0 - self.klc_fx_type = Chan_KLC_FX.UNKNOWN - self.rsi = klu.rsi - self.volume_ratio = klu.volume_ratio - self.macdhist = klu.macdhist - self.body = klu.body - self.upper_shadow = klu.upper_shadow - self.lower_shadow = klu.lower_shadow - self.body_ratio = klu.body_ratio - self.upper_shadow_ratio = klu.upper_shadow_ratio - self.lower_shadow_ratio = klu.lower_shadow_ratio - self.candle_dir = klu.candle_dir - self.range = klu.range - self.bb_out = True - self.macd = klu.macd - self.signal = klu.signal - self.state = Chan_MACD_STATE.UNKNOWN - self.continue_div = False - self.separate_div = False - self.ema24 = klu.ema24 - self.ema26 = klu.ema26 - self.ema52 = klu.ema52 - self.ema104 = klu.ema104 - self.ema156 = klu.ema156 - self.ema208 = klu.ema208 - self.ema13 = klu.ema13 - self.ema7 = klu.ema7 - self.trend = Chan_PRICE_TREND.UNKNOWN - self.exception = klu.exception - self.klc_dir = Chan_KLINE_DIR.UP if klu.close > klu.open else Chan_KLINE_DIR.DOWN - self.ema_dir = klu.ema_dir - self.bsp = False - self.bsp_type = Chan_BSP_TYPE.NONE - # EMA状态字典:key为EMA名称,value为 {'pos': Chan_EMA_POS, 'semantic': Chan_EMA_SEMANTIC} - self.ema_status = {} - # 向后兼容:保留 ema52_status 和 ema52_pos - self.ema52_status = 0 - self.ema52_pos = Chan_EMA_POS.UNKNOWN - self.bb2633upper = klu.bb2633upper - self.bb2633lower = klu.bb2633lower - self.bb2633middle = klu.bb2633middle - self.ema5 = klu.ema5 - self.ma5 = klu.ma5 - self.fx_box = None - self.in_fx = False - self.fx_confirmed = False - self.ema52_dis = klu.high - klu.ema52 if klu.close > klu.ema52 else klu.ema52 - klu.low - self.ema26_dis = klu.high - klu.ema26 if klu.close > klu.ema26 else klu.ema26 - klu.low - self.macd_signal_dis = abs(klu.macd - klu.signal) - self.ema52_ema26_dis = abs(klu.ema52 - klu.ema26) - self.fx_type = Chan_FX.UNKNOWN - self.bi_zs = None - self.seg_zs = None - self.last_bi_zs = None - # ==================== EMA 通用计算方法 ==================== - - @staticmethod - def cal_ema_pos(high, low, close, ema_value, threshold=0): - """ - 计算K线与任意EMA的客观位置关系(与趋势方向无关,支持threshold容差) - - 参数: - high, low, close: K线的高低收盘价 - ema_value: EMA的值 - threshold: 容差值(绝对值),在此范围内视为"接近/触碰" - 例如 BTC 价格 $100,000 时 threshold=100 表示差100点视为触碰 - 返回: - Chan_EMA_POS 枚举值 - - 判断逻辑(以threshold=100, ema=97000为例): - ema_zone = [96900, 97100] (EMA上下各扩展threshold) - - ABOVE: low > 97100 K线完全在zone上方(远离EMA) - NEAR_ABOVE: 97000 < low <= 97100 K线在上方但下影线进入zone(接近EMA) - CROSS_CLOSE_ABOVE: close > 97000, low <= 97000 K线穿越EMA,收盘在上方 - ON_EMA: abs(close - 97000) <= 100 收盘价在zone内 - CROSS_CLOSE_BELOW: close < 97000, high >= 97000 K线穿越EMA,收盘在下方 - NEAR_BELOW: 96900 <= high < 97000 K线在下方但上影线进入zone(接近EMA) - BELOW: high < 96900 K线完全在zone下方(远离EMA) - """ - if ema_value is None or ema_value == 0: - return Chan_EMA_POS.UNKNOWN - - ema_upper = ema_value + threshold # EMA zone 上界 - ema_lower = ema_value - threshold # EMA zone 下界 - - # 1. 收盘价在EMA附近(zone内) - if threshold > 0 and abs(close - ema_value) <= threshold: - # 收盘价在zone内,但还需要看是否有实际穿越 - if low <= ema_value and close >= ema_value: - return Chan_EMA_POS.CROSS_CLOSE_ABOVE # 实际穿越了精确EMA线 - elif high >= ema_value and close <= ema_value: - return Chan_EMA_POS.CROSS_CLOSE_BELOW - return Chan_EMA_POS.ON_EMA - - # 2. K线实际穿越了精确的EMA线 - if close > ema_value and low <= ema_value: - return Chan_EMA_POS.CROSS_CLOSE_ABOVE - if close < ema_value and high >= ema_value: - return Chan_EMA_POS.CROSS_CLOSE_BELOW - if close == ema_value: - return Chan_EMA_POS.ON_EMA - - # 3. 没有实际穿越,检查是否"接近"(在threshold zone内) - if close > ema_value: - # K线在EMA上方 - if threshold > 0 and low <= ema_upper: - return Chan_EMA_POS.NEAR_ABOVE # 下影线进入zone,接近但未触碰 - return Chan_EMA_POS.ABOVE # 远离EMA - else: - # K线在EMA下方 - if threshold > 0 and high >= ema_lower: - return Chan_EMA_POS.NEAR_BELOW # 上影线进入zone,接近但未触碰 - return Chan_EMA_POS.BELOW # 远离EMA - - @staticmethod - def cal_ema_semantic(ema_pos, kline_dir, ema_dir): - """ - 根据客观位置 + K线方向 + 趋势方向,计算语义状态 - - 参数: - ema_pos: Chan_EMA_POS 客观位置 - kline_dir: Chan_KLINE_DIR K线方向 (UP/DOWN/COMBINE/INCLUDED) - ema_dir: int 趋势方向 (1=多头, -1=空头, 0=盘整) - 返回: - Chan_EMA_SEMANTIC 枚举值 - - 语义含义(以多头为例,空头完全对称): - TOUCH_HOLD: 触碰EMA,收盘守住趋势侧(支撑/压力有效) - BREAK: 穿越EMA,收盘在逆势侧(支撑/压力失败) - DEEP_COUNTER: 完全在EMA逆势侧(深度回调/反抽) - TOUCH_FAIL: 逆势触碰EMA但未穿越(反弹/反抽力度不足) - RECOVER: 逆势后穿越EMA回到趋势侧(收复EMA) - TREND_SIDE: 完全在EMA趋势侧(正常运行) - STRONG_TREND: 顺势K线完全在EMA趋势侧(强势,远未及EMA) - WEAK_COUNTER: 逆势K线完全在EMA逆势侧(弱势,远未到EMA) - """ - if ema_pos == Chan_EMA_POS.UNKNOWN: - return Chan_EMA_SEMANTIC.NEUTRAL - - # 统一处理:将多头/盘整和空头映射到同一套逻辑 - # is_bull=True 时,"趋势侧"=上方,"逆势侧"=下方 - # is_bull=False时,"趋势侧"=下方,"逆势侧"=上方 - is_bull = ema_dir >= 0 # 多头和盘整都按多头逻辑处理 - - # K线是否是顺势方向(多头下UP为顺势,空头下DOWN为顺势) - is_trend_kline = (kline_dir == Chan_KLINE_DIR.UP) if is_bull else (kline_dir == Chan_KLINE_DIR.DOWN) - is_counter_kline = (kline_dir == Chan_KLINE_DIR.DOWN) if is_bull else (kline_dir == Chan_KLINE_DIR.UP) - - # 位置映射:多头下 ABOVE=趋势侧, BELOW=逆势侧; 空头反过来 - trend_side = Chan_EMA_POS.ABOVE if is_bull else Chan_EMA_POS.BELOW - counter_side = Chan_EMA_POS.BELOW if is_bull else Chan_EMA_POS.ABOVE - near_trend = Chan_EMA_POS.NEAR_ABOVE if is_bull else Chan_EMA_POS.NEAR_BELOW - near_counter = Chan_EMA_POS.NEAR_BELOW if is_bull else Chan_EMA_POS.NEAR_ABOVE - cross_to_trend = Chan_EMA_POS.CROSS_CLOSE_ABOVE if is_bull else Chan_EMA_POS.CROSS_CLOSE_BELOW - cross_to_counter = Chan_EMA_POS.CROSS_CLOSE_BELOW if is_bull else Chan_EMA_POS.CROSS_CLOSE_ABOVE - - # COMBINE / INCLUDED 方向:只看位置,不区分强弱 - if not is_trend_kline and not is_counter_kline: - if ema_pos == trend_side: - return Chan_EMA_SEMANTIC.TREND_SIDE - elif ema_pos in (near_trend, cross_to_trend, Chan_EMA_POS.ON_EMA): - return Chan_EMA_SEMANTIC.APPROACHING - elif ema_pos in (near_counter, cross_to_counter): - return Chan_EMA_SEMANTIC.APPROACHING - elif ema_pos == counter_side: - return Chan_EMA_SEMANTIC.DEEP_COUNTER - return Chan_EMA_SEMANTIC.NEUTRAL - - # 逆势K线(多头下的下跌K线 / 空头下的上涨K线) - if is_counter_kline: - if ema_pos == trend_side: - return Chan_EMA_SEMANTIC.STRONG_TREND # 逆势K线仍在趋势侧(回调很浅) - elif ema_pos == near_trend: - return Chan_EMA_SEMANTIC.APPROACHING # 接近EMA,即将测试支撑/压力 - elif ema_pos == cross_to_trend: - return Chan_EMA_SEMANTIC.TOUCH_HOLD # 触碰EMA后守住趋势侧 - elif ema_pos == Chan_EMA_POS.ON_EMA: - return Chan_EMA_SEMANTIC.TOUCH_HOLD # 收盘在EMA附近,视为守住 - elif ema_pos == cross_to_counter: - return Chan_EMA_SEMANTIC.BREAK # 穿越EMA到逆势侧 - elif ema_pos == near_counter: - return Chan_EMA_SEMANTIC.BREAK # 接近EMA但收盘在逆势侧,也视为击穿 - elif ema_pos == counter_side: - return Chan_EMA_SEMANTIC.DEEP_COUNTER # 完全在逆势侧 - - # 顺势K线(多头下的上涨K线 / 空头下的下跌K线) - if is_trend_kline: - if ema_pos == counter_side: - return Chan_EMA_SEMANTIC.WEAK_COUNTER # 顺势K线却在逆势侧(弱势) - elif ema_pos == near_counter: - return Chan_EMA_SEMANTIC.APPROACHING # 从逆势侧接近EMA - elif ema_pos == cross_to_counter: - return Chan_EMA_SEMANTIC.TOUCH_FAIL # 触碰EMA但未穿越回趋势侧 - elif ema_pos == Chan_EMA_POS.ON_EMA: - return Chan_EMA_SEMANTIC.TOUCH_FAIL # 收盘在EMA附近,未确认突破 - elif ema_pos == cross_to_trend: - return Chan_EMA_SEMANTIC.RECOVER # 从逆势侧穿越回趋势侧 - elif ema_pos == near_trend: - return Chan_EMA_SEMANTIC.RECOVER # 接近趋势侧(刚收复EMA附近) - elif ema_pos == trend_side: - return Chan_EMA_SEMANTIC.TREND_SIDE # 完全在趋势侧(正常) - - return Chan_EMA_SEMANTIC.NEUTRAL - - @staticmethod - def semantic_to_int(semantic): - """将 Chan_EMA_SEMANTIC 枚举转换为整数,兼容旧的 ema52_status 数值""" - mapping = { - Chan_EMA_SEMANTIC.TOUCH_HOLD: 1, - Chan_EMA_SEMANTIC.BREAK: 2, - Chan_EMA_SEMANTIC.DEEP_COUNTER: 3, - Chan_EMA_SEMANTIC.TOUCH_FAIL: 4, - Chan_EMA_SEMANTIC.RECOVER: 5, - Chan_EMA_SEMANTIC.TREND_SIDE: 6, - Chan_EMA_SEMANTIC.STRONG_TREND: 7, - Chan_EMA_SEMANTIC.WEAK_COUNTER: 8, - Chan_EMA_SEMANTIC.APPROACHING: 9, - Chan_EMA_SEMANTIC.NEUTRAL: 0, - } - return mapping.get(semantic, 0) - - # threshold_pct: 阈值百分比,用于自动计算绝对阈值 - # 例如 0.001 表示 EMA 值的 0.1%,BTC $100,000 时 threshold = $100 - threshold_pct = 0.001 - def set_bsp_type(self, bsp_type): - if bsp_type and bsp_type != Chan_BSP_TYPE.NONE: - self.bsp_type = bsp_type - self.bsp = True - def cal_all_ema_status(self): - """ - 统一计算所有EMA与K线的位置关系和语义状态 - - threshold 自动按 EMA 值的百分比计算(cls.threshold_pct,默认0.1%) - - BTC $100,000 时:threshold ≈ $100 - - ETH $3,000 时:threshold ≈ $3 - - SOL $200 时:threshold ≈ $0.2 - - 结果存储在 self.ema_status 字典中,格式: - { - 'ema24': {'pos': Chan_EMA_POS, 'semantic': Chan_EMA_SEMANTIC, 'value': float, 'threshold': float}, - 'ema52': {...}, - ... - } - - 同时保持向后兼容:self.ema52_pos 和 self.ema52_status - """ - ema_configs = { - 'ema24': self.ema24, - 'ema52': self.ema52, - 'ema104': self.ema104, - 'ema156': self.ema156, - 'ema208': self.ema208, - } - self.ema_status = {} - for name, value in ema_configs.items(): - # 按 EMA 值的百分比自动计算阈值 - threshold = abs(value) * self.threshold_pct if value and self.threshold_pct > 0 else 0 - pos = ChanKLC.cal_ema_pos(self.high, self.low, self.close, value, threshold) - semantic = ChanKLC.cal_ema_semantic(pos, self.dir, self.ema_dir) - self.ema_status[name] = { - 'pos': pos, - 'semantic': semantic, - 'value': value, - 'threshold': threshold, - } - # 向后兼容 - self.ema52_pos = self.ema_status['ema52']['pos'] - self.ema52_status = ChanKLC.semantic_to_int(self.ema_status['ema52']['semantic']) - def get_ema_pos(self, ema_name): - """获取指定EMA的客观位置,如 klc.get_ema_pos('ema24')""" - if ema_name in self.ema_status: - return self.ema_status[ema_name]['pos'] - return Chan_EMA_POS.UNKNOWN - def check_ema_pos(self): - if len(self.ema_status) > 0: - for ema_name, pos in self.ema_status.items(): - #print(self.end_time, ema_name, pos['pos']) - if ((self.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc_fx_type == Chan_KLC_FX.TOP2) and pos['pos'] == Chan_EMA_POS.CROSS_CLOSE_BELOW) or ((self.klc_fx_type == Chan_KLC_FX.BOTTOM1 or self.klc_fx_type == Chan_KLC_FX.BOTTOM2) and pos['pos'] == Chan_EMA_POS.CROSS_CLOSE_ABOVE): - #print("---------------------") - return ema_name - return None - def get_ema_semantic(self, ema_name): - """获取指定EMA的语义状态,如 klc.get_ema_semantic('ema52')""" - if ema_name in self.ema_status: - return self.ema_status[ema_name]['semantic'] - return Chan_EMA_SEMANTIC.NEUTRAL - def set_trend(self, trend): - self.trend = trend - def to_string(self): - out = "" - start = self.start_time if self.start_time is not None else "" - end = self.end_time if self.end_time is not None else "" - price_diff = getattr(self, 'price_diff', None) - out += str(start) + " " + str(end) + " " + str(self.close) + " " + str(self.ema24) + " " + str(self.ema52) + " " + str(self.trend) + " " + str(self.close - self.ema52) - return out - def set_bi_zs(self, bi_zs): - if bi_zs: - self.bi_zs = bi_zs - def set_klc_fx_type(self, klc_fx_type): - #print(self.start_time, klc_fx_type, self.get_feature_data()['klu_macd'], self.get_feature_data()['klu_macdhist'], self.get_feature_data()['klu_rsi']) - self.klc_fx_type = klc_fx_type - #self.cal_fx() - ema_name = self.check_ema_pos() - hist_div = abs(self.macdhist - self.next.macdhist) - #print(self.end_time, self.dir, abs(self.macdhist), hist_div) - #if ema_name: - #print(self.end_time, ema_name, self.ema_status[ema_name]['semantic'], hist_div) - #self.cal_bb_out() - #print(self.pre.start_time, self.next.end_time, self.klc_fx_type) - if klc_fx_type == Chan_KLC_FX.TOP1 or klc_fx_type == Chan_KLC_FX.TOP2 or klc_fx_type == Chan_KLC_FX.BOTTOM1 or klc_fx_type == Chan_KLC_FX.BOTTOM2: - self.cal_fx_box() - self.cal_fx_type() - def cal_fx_type(self): - if self.fx == Chan_FX_TYPE.TOP and self.next: - if self.ema52_dis > self.ema26_dis: - if self.pre.macd < self.macd and self.macd < self.next.macd: - self.fx_type = Chan_FX.CONTINUATION - else: - self.fx_type = Chan_FX.REVERSAL - elif self.fx == Chan_FX_TYPE.BOTTOM and self.next: - if self.ema52_dis < self.ema26_dis: - if self.pre.macd > self.macd and self.macd > self.next.macd: - self.fx_type = Chan_FX.CONTINUATION - else: - self.fx_type = Chan_FX.REVERSAL - #if self.fx_type != Chan_FX.UNKNOWN and self.fx_type != Chan_FX.CONTINUATION: - #print(self.end_time, self.fx_type) - def cal_fx_box(self): - # 每次重算前先清空,避免旧box残留 - self.fx_box = None - start_time = None - end_time = None - high = 0 - low = 0 - display = False - if self.pre and self.next and self.next.end_time: - self.next.in_fx = True - if self.fx == Chan_FX_TYPE.TOP: - start_time = self.pre.end_time - end_time = self.next.end_time - high = self.high - low = self.pre.low if self.pre.low < self.next.low else self.next.low - if self.next.close < self.pre.low or True: - display = True - elif self.fx == Chan_FX_TYPE.BOTTOM: - start_time = self.pre.end_time - end_time = self.next.end_time - high = self.pre.high if self.pre.high > self.next.high else self.next.high - low = self.low - if self.next.close > self.pre.high or True: - display = True - if high > 0 and self.next.end_time and display: - #print(start_time, end_time, high, low) - # Chan_FX_BOX 这里导入的是模块,类名在模块内部为 Chan_FX_Box - self.fx_confirmed = True - self.fx_box = Chan_FX_Box.Chan_FX_Box(start_time, end_time, high, low) - def check_fx_confirmed(self, last_top, last_bottom): - if last_top and last_bottom and False: - if last_top.index > last_bottom.index: - if self.in_fx == False and last_top.fx_confirmed == False: - pre = last_top.pre - if pre.low > self.close: - last_top.fx_confirmed = True - if last_top.fx_box: - last_top.fx_box.end_time = self.end_time - #print(self.end_time, "fx_confirmed top") - else: - high = last_top.high - low = self.low - last_top.fx_box = Chan_FX_Box.Chan_FX_Box(last_top.pre.start_time, self.end_time, high, low) - #print(self.end_time, "fx_confirmed new box top") - elif self.in_fx == False and last_bottom.fx_confirmed == False: - pre = last_bottom.pre - if pre.high < self.close: - last_bottom.fx_confirmed = True - if last_bottom.fx_box: - last_bottom.fx_box.end_time = self.end_time - #print(self.end_time, "fx_confirmed bottom") - else: - high = self.high - low = last_bottom.low - last_bottom.fx_box = Chan_FX_Box.Chan_FX_Box(last_bottom.pre.start_time, self.end_time, high, low) - #print(self.end_time, "fx_confirmed new box bottom") - def add_klu(self, klu): - self.klu_list.append(klu) - def check_klc_state(self, last_fx_klc): - if last_fx_klc and last_fx_klc.fx == Chan_FX_TYPE.TOP: - if self.high > last_fx_klc.high: - self.klc_state = Chan_KLC_STATE.S11 - else: - self.klc_state = Chan_KLC_STATE.S_11 - elif last_fx_klc and last_fx_klc.fx == Chan_FX_TYPE.BOTTOM: - if self.low < last_fx_klc.low: - self.klc_state = Chan_KLC_STATE.S_11 - else: - self.klc_state = Chan_KLC_STATE.S11 - if self.pre and self.pre.fx == Chan_FX_TYPE.TOP: - self.klc_state = Chan_KLC_STATE.S10 - elif self.pre and self.pre.fx == Chan_FX_TYPE.BOTTOM: - self.klc_state = Chan_KLC_STATE.S_10 - #print(self.end_time, self.klc_state) - def set_end_klu(self, klu): - self.end_klu = klu - self.end_time = klu.time - self.close = klu.close - for klu in self.klu_list: - if klu.exception: - self.exception = True - print(klu.time, "exception") - if klu.separate_div > 0: - self.separate_div = True - if klu.continue_div: - self.continue_div = klu.continue_div - if klu.macd_state != Chan_MACD_STATE.UNKNOWN: - self.state = klu.macd_state - klu.set_klc(self) - self.klc_dir = Chan_KLINE_DIR.UP if self.close > self.open else Chan_KLINE_DIR.DOWN - self.cal_indicators() - self.cal_all_ema_status() - if self.open > self.high: - self.open = self.high - if self.close > self.high: - self.close = self.high - if self.close < self.low: - self.close = self.low - if self.open < self.low: - self.open = self.low - #print(self.end_time, self.open, self.close, self.high, self.low) - #print(klu.time, klu.open, klu.close, klu.high, klu.low) - def cal_fx(self): - if self.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc_fx_type == Chan_KLC_FX.TOP2: - #print(self.end_time, self.fx, self.macd, self.macdhist, len(self.klu_list)) - if self.state == Chan_MACD_STATE.HIGH_EMPTY and self.macd > 0: - #print(self.end_time, self.state, self.macd, self.klc_fx_type) - self.klc_fx_type = Chan_KLC_FX.TOP6 - if self.separate_div or self.continue_div: - self.klc_fx_type = Chan_KLC_FX.TOP7 - if self.signal > 0 and self.macd > self.signal: - self.klc_fx_type = Chan_KLC_FX.TOP8 - else: - if self.klc_fx_type == Chan_KLC_FX.BOTTOM1 or self.klc_fx_type == Chan_KLC_FX.BOTTOM2: - if self.macdhist > 0 and self.macd < 0: - self.klc_fx_type = Chan_KLC_FX.BOTTOM5 - return - if self.state == Chan_MACD_STATE.HIGH_EMPTY and self.macd < 0: - self.klc_fx_type = Chan_KLC_FX.BOTTOM6 - #print(self.end_time, self.state, self.macd, self.klc_fx_type) - if self.separate_div or self.continue_div: - self.klc_fx_type = Chan_KLC_FX.BOTTOM7 - if self.signal < 0 and self.macd < self.signal: - self.klc_fx_type = Chan_KLC_FX.BOTTOM8 - def cal_bb_out(self): - for klu in self.klu_list: - if self.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc_fx_type == Chan_KLC_FX.TOP2: - #print(self.start_time, self.klc_fx_type, klu.high, klu.bb52upper, self.macd, self.next.macd, klu.time) - if self.high >= klu.bb52upper and klu.bb52upper > 0 and self.next and self.high > self.next.high: - self.klc_fx_type = Chan_KLC_FX.TOP4 - print(self.end_time, self.klc_fx_type) - if self.klc_fx_type == Chan_KLC_FX.BOTTOM1 or self.klc_fx_type == Chan_KLC_FX.BOTTOM2: - #print(self.start_time, self.klc_fx_type, klu.low, klu.bb52lower, self.macd, self.next.macd, klu.time) - if self.low <= klu.bb52lower and klu.bb52lower > 0 and self.next and self.low < self.next.low: - self.klc_fx_type = Chan_KLC_FX.BOTTOM4 - print(self.end_time, self.klc_fx_type) - def cal_indicators(self): - for index in range(1, len(self.klu_list)): - self.volume += self.klu_list[index].volume - self.rsi += self.klu_list[index].rsi - self.volume_ratio += self.klu_list[index].volume_ratio - self.macdhist += self.klu_list[index].macdhist - self.ema26 += self.klu_list[index].ema26 - self.ema24 += self.klu_list[index].ema24 - self.ema52 += self.klu_list[index].ema52 - self.ema104 += self.klu_list[index].ema104 - self.ema156 += self.klu_list[index].ema156 - self.ema208 += self.klu_list[index].ema208 - self.ema13 += self.klu_list[index].ema13 - self.ema7 += self.klu_list[index].ema7 - self.bb2633upper += self.klu_list[index].bb2633upper - self.bb2633lower += self.klu_list[index].bb2633lower - self.bb2633middle += self.klu_list[index].bb2633middle - self.ma5 += self.klu_list[index].ma5 - self.ema5 += self.klu_list[index].ema5 - if self.ema_dir != self.klu_list[index].ema_dir: - self.ema_dir = 0 - n = len(self.klu_list) - self.rsi = self.rsi / n - self.volume_ratio = self.volume_ratio / n - self.volume = self.volume / n - self.macdhist = self.macdhist / n - self.ema26 = self.ema26 / n - self.ema24 = self.ema24 / n - self.ema52 = self.ema52 / n - self.ema104 = self.ema104 / n - self.ema156 = self.ema156 / n - self.ema208 = self.ema208 / n - self.ema13 = self.ema13 / n - self.ema7 = self.ema7 / n - self.ma5 = self.ma5 / n - self.ema5 = self.ema5 / n - self.bb2633upper = self.bb2633upper / n - self.bb2633lower = self.bb2633lower / n - self.bb2633middle = self.bb2633middle / n - if len(self.klu_list) > 0: - self.macd = self.klu_list[-1].macd - self.signal = self.klu_list[-1].signal - self.body = abs(self.close - self.open) - self.upper_shadow = self.high - max(self.close, self.open) - self.lower_shadow = min(self.close, self.open) - self.low - self.body_ratio = self.body / self.open - self.upper_shadow_ratio = self.upper_shadow / self.open - self.lower_shadow_ratio = self.lower_shadow / self.open - self.candle_dir = Chan_K_DIR.CROSS if self.close == self.open else Chan_K_DIR.BULL if self.close > self.open else Chan_K_DIR.BEAR - self.range = self.high - self.low - def set_next(self, klc): - self.next = klc - def set_pre(self, klc): - self.pre = klc - def set_state(self, state): - self.state = state - def check_klu_included(self, klu): - if self.high >= klu.high: - # high大于,low小于,左包含 - if self.low <= klu.low: - self.add_klu(klu=klu) - # gn>gn-1 - if self.dir == Chan_KLINE_DIR.UP: - # UP -> max(dn) - self.low = klu.low - else: - # DOWN -> min(gn) - self.high = klu.high - #self.print(klu, "Z") - return True - # high大于,low大于,不包含 - else: - # if self.low > klu.low - # high相等,右包含 - if self.high == klu.high: - self.add_klu(klu=klu) - # UP -> max(gn) - if self.dir == Chan_KLINE_DIR.UP: - self.high = klu.high - else: - # DOWN -> min(dn) - self.low = klu.low - return True - else: - return False - else: - # high小于,low大于,右包含 - if self.low >= klu.low: - self.add_klu(klu=klu) - # gn>gn-1 - if self.dir == Chan_KLINE_DIR.UP: - # UP -> max(gn) - self.high = klu.high - else: - # DOWN -> min(dn) - self.low = klu.low - #self.print(klu, "Y") - return True - else: - # high小于,low小于,不包含 - return False - def set_fx(self, fx: Chan_FX_TYPE): - self.fx = fx - def cal_invisible(self): - if self.fx == Chan_FX_TYPE.TOP: - if self.macdhist < 0 and self.macd > 0: - self.klc_fx_type = Chan_KLC_FX.TOP5 - else: - if self.fx == Chan_FX_TYPE.BOTTOM: - if self.macdhist > 0 and self.macd < 0: - self.klc_fx_type = Chan_KLC_FX.BOTTOM5 - def set_pre_fx(self): - if self.pre and self.pre.pre: - self.pre.fx = self.check_fx(self.pre.pre, self.pre) - def check_fx(self, k1, k2): - if k2.high > k1.high and k2.high > self.high: - return Chan_FX_TYPE.TOP - elif k2.low < k1.low and k2.low < self.low: - return Chan_FX_TYPE.BOTTOM - else: - return Chan_FX_TYPE.UNKNOWN - def set_bi(self, bi): - self.bi = bi - self.distance = self.index - bi.start_klc.index - #print(self.start_time, self.distance, bi.index, bi.dir) \ No newline at end of file +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.core.ChanKLC") +sys.modules[__name__] = _impl diff --git a/ChanKLU.py b/ChanKLU.py index 3a4855b..e45ec9b 100644 --- a/ChanKLU.py +++ b/ChanKLU.py @@ -1,388 +1,5 @@ -from ChanEnum import Chan_FX_TYPE, Chan_KLU_TYPE, Chan_K_DIR, Chan_MACD_STATE, Chan_MACDHIST_STATE, Chan_PRICE_TREND, Chan_KLU_PATTERN, Chan_KLC_FX -class ChanKLU: - def __init__(self, time, open, high, low, close, volume): - # _time, _close, _open, _high, _low, _extra_info={} - self.kl_type = None - self.time = time - self.close = close - self.open = open - self.high = high - self.low = low - self.volume = volume - self.idx = 0 - self.index = 0 - self.macd = 0 - self.signal = 0 - self.macdhist = 0 - self.klc = None - self.rsi = 0 - self.volume_ratio = 0 - self.bb52upper = 0 - self.bb52lower = 0 - # === 新增:K线类型 === - self.kline_type = None # K线类型:大阳线、大阴线、小阳线、小阴线 - self.pattern = Chan_KLU_PATTERN.UNKNOWN - - # === 新增:实时分型相关属性 === - self.pre = None # 前一根K线 - self.next = None # 后一根K线 - self.fx_type = Chan_FX_TYPE.UNKNOWN # 分型类型:0=无分型,1=顶分型,-1=底分型 - self.fx_strength = 0 # 分型强度:0-100 - self.fx_confirmed = False # 分型是否确认 - self.klu_type = None - self.range = self.high - self.low - self.body = abs(self.close - self.open) - self.upper_shadow = self.high - max(self.close, self.open) - self.lower_shadow = min(self.close, self.open) - self.low - self.body_ratio = self.body / self.range if self.range != 0 else 0 - self.upper_shadow_ratio = self.upper_shadow / self.body if self.body != 0 else float('inf') - self.lower_shadow_ratio = self.lower_shadow / self.body if self.body != 0 else float('inf') - self.exception = False - #self.cal_exception() - self.candle_dir = Chan_K_DIR.CROSS if self.close == self.open else Chan_K_DIR.BULL if self.close > self.open else Chan_K_DIR.BEAR - - self.continue_div = 0 - self.separate_div = 0 - self.near0_return = 0 - self.ema52 = 0 - self.ema24 = 0 - self.ema26 = 0 - self.ema104 = 0 - self.ema156 = 0 - self.ema208 = 0 - self.macd_slop = 0 - self.signal_slop = 0 - self.hist_slop = 0 - self.hist_state = Chan_MACDHIST_STATE.UNKNOWN - self.macd_state = Chan_MACD_STATE.UNKNOWN - self.macd_hist_gap = 0 - self.trend = Chan_PRICE_TREND.UNKNOWN - self.seg_histset_index = 0 - # === 归零轴细化与模式/背离 === - self.zero_axis = False # 是否归零轴(穿越或接近) - self.zero_axis_state = "none" # {none,crossing,near} - self.zero_axis_side = 0 # 1:above, -1:under, 0:none - self.zero_axis_score = 0 # 0-100 综合评分 - self.mode1_touch_ema52 = False # 单边后触碰EMA52 - self.mode2_fast_to_zero = False # 快线向零收敛 - self.mode3_double_tf = False # 双周期归零(近似占位,由上层填充高周期确认) - self.mode3_dir = "none" # {long_strong_rebound, short_strong_rebound, none} - self.mode4_touch52_no_zero = False # 先触碰EMA52但黄白线未归零 - self.div_type = "none" # {bearish, bullish, hidden_bearish, hidden_bullish, none} - self.div_score = 0.0 # 背离强度(0-100) - self.ema_dir = 0 - self.get_ema_dir() - self.bb2633upper = 0 - self.bb2633lower = 0 - self.bb2633middle = 0 - self.ma5 = 0 - self.ema5 = 0 - #print(self.open, self.close, self.high, self.low, self.candle_dir, self.strength) - def set_macd_state(self, state): - self.macd_state = state - def set_pattern(self, pattern): - self.pattern = pattern - def set_seg_histset_index(self, seg_histset_index): - self.seg_histset_index = seg_histset_index - #print(self.time, self.seg_histset_index) - def to_string(self): - return f"{self.time} {self.candle_dir} {self.pattern}" - def cal_exception(self): - if self.upper_shadow_ratio > 5 or self.lower_shadow_ratio > 5: - self.exception = True - #print(self.time, self.upper_shadow_ratio, self.lower_shadow_ratio, self.body, self.lower_shadow, self.upper_shadow, self.high, self.low, self.close, self.open) - #self.exception = False - def set_trend(self, trend): - self.trend = trend - def set_separate_div(self, separate_div): - self.separate_div = separate_div - if self.klc and self.klc.pre and self.klc.next: - fx = self.check_fx_dir(self.klc.pre, self.klc.next) - if fx == Chan_FX_TYPE.TOP: - if self.macdhist > 0: - self.separate_div = separate_div - else: - self.separate_div = 0 - elif fx == Chan_FX_TYPE.BOTTOM: - if self.macdhist < 0: - self.separate_div = separate_div - else: - self.separate_div = 0 - def check_fx_dir(self, pre, next): - fx = Chan_FX_TYPE.UNKNOWN - if pre.klc_fx_type == Chan_KLC_FX.TOP1 or pre.klc_fx_type == Chan_KLC_FX.TOP2 or next.klc_fx_type == Chan_KLC_FX.TOP1 or next.klc_fx_type == Chan_KLC_FX.TOP2 or self.klc.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc.klc_fx_type == Chan_KLC_FX.TOP2: - fx = Chan_FX_TYPE.TOP - elif pre.klc_fx_type == Chan_KLC_FX.BOTTOM1 or pre.klc_fx_type == Chan_KLC_FX.BOTTOM2 or next.klc_fx_type == Chan_KLC_FX.BOTTOM1 or next.klc_fx_type == Chan_KLC_FX.BOTTOM2 or self.klc.klc_fx_type == Chan_KLC_FX.BOTTOM1 or self.klc.klc_fx_type == Chan_KLC_FX.BOTTOM2: - fx = Chan_FX_TYPE.BOTTOM - return fx - def set_next(self, next): - self.next = next - #if self.fx_type != Chan_FX_TYPE.UNKNOWN and self.fx_strength > 1: - #print(self.index, self.time, self.fx_type, self.fx_confirmed, self.fx_strength) - def set_pre(self, pre): - self.pre = pre - def set_klc(self, klc): - self.klc = klc - def set_histset(self, histset): - """设置HistSet关联""" - self.histset = histset - - def set_seg(self, seg): - """设置Seg关联""" - self.seg = seg - - def set_unittf(self, unittf): - """设置UnitTF关联""" - self.unittf = unittf - def set_idx(self, idx): - self.idx = idx - self.index = idx - def check_price_ema156(self): - if self.check_indicators(): - if self.close > self.ema156: - return 1 - elif self.close < self.ema156: - return -1 - else: - return 0 - else: - return 0 - def get_ema_dir(self): - if self.check_indicators(): - if self.ema24 > self.ema52 and self.ema52 > self.ema104 and self.ema104 > self.ema156: - self.ema_dir = 1 - elif self.ema24 < self.ema52 and self.ema52 < self.ema104 and self.ema104 < self.ema156: - self.ema_dir = -1 - else: - self.ema_dir = 0 - def check_indicators(self): - if self.ema156 == 0: - return False - else: - return True - def set_indicators(self, item): - self.macd = float(item['macd']) if 'macd' in item and item['macd'] else 0 - self.signal = float(item['macdsignal']) if 'macdsignal' in item and item['macdsignal'] else 0 - self.macdhist = float(item['macdhist']) if 'macdhist' in item and item['macdhist'] else 0 - self.ema26 = float(item['ema26']) if 'ema26' in item and item['ema26'] else 0 - self.ema52 = float(item['ema52']) if 'ema52' in item and item['ema52'] else 0 - self.ema24 = float(item['ema24']) if 'ema24' in item and item['ema24'] else 0 - self.ema104 = float(item['ema104']) if 'ema104' in item and item['ema104'] else 0 - self.ema156 = float(item['ema156']) if 'ema156' in item and item['ema156'] else 0 - self.ema208 = float(item['ema208']) if 'ema208' in item and item['ema208'] else 0 - self.ema13 = float(item['ema13']) if 'ema13' in item and item['ema13'] else 0 - self.ema7 = float(item['ema7']) if 'ema7' in item and item['ema7'] else 0 - self.rsi = float(item['rsi']) if 'rsi' in item and item['rsi'] else 0 - self.volume_ratio = float(item['volume_ratio']) if 'volume_ratio' in item and item['volume_ratio'] else 0 - self.bb52upper = float(item['bb52upper']) if 'bb52upper' in item and item['bb52upper'] else 0 - self.bb52lower = float(item['bb52lower']) if 'bb52lower' in item and item['bb52lower'] else 0 - self.bb2633upper = float(item['bb2633upper']) if 'bb2633upper' in item and item['bb2633upper'] else 0 - self.bb2633lower = float(item['bb2633lower']) if 'bb2633lower' in item and item['bb2633lower'] else 0 - self.bb2633middle = float(item['bb2633middle']) if 'bb2633middle' in item and item['bb2633middle'] else 0 - self.ma5 = float(item['ma5']) if 'ma5' in item and item['ma5'] else 0 - self.ema5 = float(item['ema5']) if 'ema5' in item and item['ema5'] else 0 - def cal_macd_state(self): - # 按定义精简实现:优先级 CROSS0 > 位置(HIGH/HE/RETURN_ZERO) > NEAR0 > UNKNOWN - # 首条或缺前一根 - if not hasattr(self, 'pre') or self.pre is None: - self.macd_state = Chan_MACD_STATE.START - return self.macd_state - - # 基本校验 - if (self.macd == 0 and self.signal == 0 and self.macdhist == 0) or self.ema52 == 0: - self.macd_state = Chan_MACD_STATE.UNKNOWN - return self.macd_state - # 归零轴判断 - if self.signal > 0: - if self.macd < self.signal: - if 0 < self.low - self.ema52 < 100: - self.near0_return = 0 - elif self.close > self.ema52 and self.low < self.ema52 and self.open > self.ema52: - self.near0_return = 0 - elif self.close < self.ema52 and self.open > self.ema52 and self.high > self.ema52 and self.low < self.ema52: - self.near0_return = 0 - elif self.close < self.ema52 and self.open < self.ema52 and self.high > self.ema52 and self.low < self.ema52: - self.near0_return = 0 - elif self.close > self.ema52 and self.open > self.ema52 and self.high > self.ema52 and self.low < self.ema52: - self.near0_return = 0 - elif self.close > self.ema52 and self.high > self.ema52 and self.low < self.ema52: - self.near0_return = 0 - else: - if self.macd > self.signal: - if 0 < self.ema52 - self.high < 100: - self.near0_return = 0 - elif self.close < self.ema52 and self.high > self.ema52 and self.open < self.ema52: - self.near0_return = 0 - elif self.close < self.ema52 and self.open > self.ema52 and self.high > self.ema52 and self.low < self.ema52: - self.near0_return = 0 - elif self.close > self.ema52 and self.open < self.ema52 and self.high > self.ema52 and self.low < self.ema52: - self.near0_return = 0 - elif self.close > self.ema52 and self.high > self.ema52 and self.open >= self.ema52 and self.low < self.ema52: - self.near0_return = 0 - elif self.close > self.ema52 and self.high > self.ema52 and self.low < self.ema52: - self.near0_return = 0 - # 向上穿越EMA52 7 - if self.close > self.ema52 and self.open < self.ema52: - self.near0_return = 0 - # 向下穿越EMA52 8 - elif self.close < self.ema52 and self.open > self.ema52: - self.near0_return = 0 - if self.pre.near0_return == 7: - # 向上穿越后的一根价格再EMA52上方 9 - if self.low > self.ema52 and self.close > self.open: - self.near0_return = 9 - if self.pre.near0_return == 8: - # 向下穿越后的一根价格再EMA52下方 10 - if self.high < self.ema52 and self.close < self.open: - self.near0_return = 10 - # CROSS0 仅以 Signal 穿越零轴判定 - if self.pre.signal >= 0 and self.signal < 0: - self.macd_state = Chan_MACD_STATE.CROSS0_DOWN - return self.macd_state - if self.pre.signal <= 0 and self.signal > 0: - self.macd_state = Chan_MACD_STATE.CROSS0_UP - return self.macd_state - # 穿零轴后的形态:缠绕/倒挂(基于前一状态为CROSS0_*) - if self.pre.macd_state == Chan_MACD_STATE.CROSS0_UP or self.pre.macd_state == Chan_MACD_STATE.CROSS0_DOWN: - direction = 1 if self.pre.macd_state == Chan_MACD_STATE.CROSS0_UP else -1 - hist_same_dir = (self.macdhist * direction) > 0 - hist_decreasing = abs(self.macdhist) < abs(self.pre.macdhist) - lines_tight = abs(self.macd - self.signal) <= 12 - # 倒挂:能量柱衰减且黄白线相对方向不利/出现反向能量释放 - if hist_decreasing and (((self.macd - self.signal) * direction) < 0 or not hist_same_dir): - self.macd_state = Chan_MACD_STATE.CROSS_REV - return self.macd_state - # 缠绕/粘合:紧贴能量柱运行,无反向能量释放 - if lines_tight and hist_same_dir: - self.macd_state = Chan_MACD_STATE.CROSS_OS - return self.macd_state - # 趋近零轴:细化 NEAR0_* 判定 - NEAR0_EPS = 15 - lines_near_zero = abs(self.macd) <= NEAR0_EPS or abs(self.signal) <= NEAR0_EPS - touch_52 = (self.ema52 != 0) and ((abs(self.close - self.ema52) <= NEAR0_EPS) or (self.low <= self.ema52 <= self.high)) - touch_24 = (self.ema24 != 0) and ((abs(self.close - self.ema24) <= NEAR0_EPS) or (self.low <= self.ema24 <= self.high)) - # 完美形态:白线接近零轴 + 价格触碰/轻破EMA52 + 黄线不穿零轴 - if abs(self.macd) <= NEAR0_EPS and touch_52 and (not (self.pre.signal >= 0 and self.signal < 0)) and (not (self.pre.signal <= 0 and self.signal > 0)): - self.macd_state = Chan_MACD_STATE.NEAR0_PERFECT - #self.near0_return = 1 - return self.macd_state - # EMA24 附近 - if lines_near_zero and touch_24: - self.macd_state = Chan_MACD_STATE.NEAR0_24 - #self.near0_return = 2 - return self.macd_state - # EMA52 附近 - if lines_near_zero and touch_52: - self.macd_state = Chan_MACD_STATE.NEAR0_52 - #self.near0_return = 3 - return self.macd_state - # 白线接近零轴但价格未至EMA52 - if abs(self.macd) <= NEAR0_EPS and not touch_52: - self.macd_state = Chan_MACD_STATE.NEAR0_DIFF - #self.near0_return = 4 - return self.macd_state - # 一般近零轴 - if lines_near_zero or touch_52: - self.macd_state = Chan_MACD_STATE.NEAR0 - #self.near0_return = 5 - return self.macd_state - - # 穿零轴后离开零轴 - if self.pre.macd_state == Chan_MACD_STATE.CROSS0_UP and ((self.macd >= self.pre.macd and self.signal >= self.pre.signal) or (abs(self.macdhist) >= abs(self.pre.macdhist))): - self.macd_state = Chan_MACD_STATE.UP - return self.macd_state - if self.pre.macd_state == Chan_MACD_STATE.CROSS0_DOWN and ((self.macd <= self.pre.macd and self.signal <= self.pre.signal) or (abs(self.macdhist) >= abs(self.pre.macdhist))): - self.macd_state = Chan_MACD_STATE.DOWN - return self.macd_state - if self.pre.macd_state == Chan_MACD_STATE.UP and self.macd > self.pre.macd and self.signal > self.pre.signal: - self.macd_state = Chan_MACD_STATE.UP - return self.macd_state - if self.pre.macd_state == Chan_MACD_STATE.DOWN and self.macd < self.pre.macd and self.signal < self.pre.signal: - self.macd_state = Chan_MACD_STATE.DOWN - return self.macd_state - # 趋势兜底:强势同步上行/下行直接进入 UP/DOWN - if self.macd > 0 and self.signal > 0 and (self.macd >= self.pre.macd and self.signal >= self.pre.signal): - self.macd_state = Chan_MACD_STATE.UP - return self.macd_state - if self.macd < 0 and self.signal < 0 and (self.macd <= self.pre.macd and self.signal <= self.pre.signal): - self.macd_state = Chan_MACD_STATE.DOWN - return self.macd_state - # 峰值:白线高位出现局部顶 - if hasattr(self.pre, 'pre') and self.pre and self.pre.pre and self.macd > 0: - if self.pre.macd > self.pre.pre.macd and self.pre.macd > self.macd: - self.macd_state = Chan_MACD_STATE.PEAK - return self.macd_state - # 高位状态的位置状态, 高位,高位空,归零轴 - if (self.pre.macd_state == Chan_MACD_STATE.UP or self.pre.macd_state == Chan_MACD_STATE.HIGH or self.pre.macd_state == Chan_MACD_STATE.RZ_UP or self.pre.macd_state == Chan_MACD_STATE.PEAK or self.pre.macd_state == Chan_MACD_STATE.HIGH_EMPTY) and self.macd > 0: - # 高位空(正区间):能量柱衰减且黄白线间距较大 - if abs(self.pre.macdhist) > 0 and abs(self.macdhist) < abs(self.pre.macdhist) and abs(self.macd - self.signal) > 5: - self.macd_state = Chan_MACD_STATE.HIGH_EMPTY - return self.macd_state - if abs(self.pre.macd - self.macd) < 10: - self.macd_state = Chan_MACD_STATE.HIGH - return self.macd_state - else: - if self.macd > self.pre.macd and self.signal > self.pre.signal: - self.macd_state = Chan_MACD_STATE.UP - return self.macd_state - elif self.macd < self.pre.macd and self.signal < self.pre.signal: - self.macd_state = Chan_MACD_STATE.RETURN_ZERO - return self.macd_state - if (self.pre.macd_state == Chan_MACD_STATE.DOWN or self.pre.macd_state == Chan_MACD_STATE.HIGH or self.pre.macd_state == Chan_MACD_STATE.RZ_DOWN or self.pre.macd_state == Chan_MACD_STATE.PEAK or self.pre.macd_state == Chan_MACD_STATE.HIGH_EMPTY) and self.macd < 0: - # 高位空(负区间):能量柱衰减且黄白线间距较大 - if abs(self.pre.macdhist) > 0 and abs(self.macdhist) < abs(self.pre.macdhist) and abs(self.macd - self.signal) > 5: - self.macd_state = Chan_MACD_STATE.HIGH_EMPTY - return self.macd_state - if abs(self.pre.macd - self.macd) < 10: - self.macd_state = Chan_MACD_STATE.HIGH - return self.macd_state - else: - if self.macd > self.pre.macd and self.signal > self.pre.signal: - self.macd_state = Chan_MACD_STATE.RETURN_ZERO - return self.macd_state - elif self.macd < self.pre.macd and self.signal < self.pre.signal: - self.macd_state = Chan_MACD_STATE.DOWN - return self.macd_state - - # 离开0轴开始上涨或者下跌阶段,高位之前的 - if self.macd > 0 and self.pre: - if (self.pre.macd_state == Chan_MACD_STATE.NEAR0 or self.pre.macd_state == Chan_MACD_STATE.RZ_UP or self.pre.macd_state == Chan_MACD_STATE.CROSS0_UP) and (self.signal > self.pre.signal or self.close > self.ema52): - self.macd_state = Chan_MACD_STATE.RZ_UP - return self.macd_state - elif self.macd < 0 and self.pre: - if (self.pre.macd_state == Chan_MACD_STATE.NEAR0 or self.pre.macd_state == Chan_MACD_STATE.RZ_DOWN or self.pre.macd_state == Chan_MACD_STATE.CROSS0_DOWN) and (self.signal < self.pre.signal or self.close < self.ema52): - self.macd_state = Chan_MACD_STATE.RZ_DOWN - return self.macd_state - # 归零轴走势 - if self.pre.macd_state == Chan_MACD_STATE.RETURN_ZERO: - if self.macd > 0: - if self.pre.macd > self.macd or abs(self.macdhist) <= abs(self.pre.macdhist) or self.signal <= self.pre.signal: - self.macd_state = Chan_MACD_STATE.RETURN_ZERO - return self.macd_state - else: - if self.pre.macd < self.macd or abs(self.macdhist) <= abs(self.pre.macdhist) or self.signal >= self.pre.signal: - self.macd_state = Chan_MACD_STATE.RETURN_ZERO - return self.macd_state - # 从 NEAR0 收敛到零轴的归零轴承接(正负两侧) - if self.pre.macd_state == Chan_MACD_STATE.NEAR0: - # 正区间朝零轴收敛 - if self.macd > 0 and self.pre.macd > 0 and self.macd <= self.pre.macd and self.signal <= self.pre.signal: - self.macd_state = Chan_MACD_STATE.RETURN_ZERO - return self.macd_state - # 负区间朝零轴收敛 - if self.macd < 0 and self.pre.macd < 0 and self.macd >= self.pre.macd and self.signal >= self.pre.signal: - self.macd_state = Chan_MACD_STATE.RETURN_ZERO - return self.macd_state - # 其余情况 - if self.pre.macd_state == Chan_MACD_STATE.UNKNOWN: - if self.macd > 0 and self.close > self.ema52 and self.pre.pre and (self.pre.pre.macd_state == Chan_MACD_STATE.UP or self.pre.pre.macd_state == Chan_MACD_STATE.RZ_UP): - self.macd_state = self.pre.pre.macd_state - return self.macd_state - elif self.macd < 0 and self.close < self.ema52 and self.pre.pre and (self.pre.pre.macd_state == Chan_MACD_STATE.DOWN or self.pre.pre.macd_state == Chan_MACD_STATE.RZ_DOWN): - self.macd_state = self.pre.pre.macd_state - return self.macd_state - else: - self.macd_state = Chan_MACD_STATE.UNKNOWN - return self.macd_state - return self.macd_state - \ No newline at end of file +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.core.ChanKLU") +sys.modules[__name__] = _impl diff --git a/ChanLun.py b/ChanLun.py index 35154b7..6343d10 100644 --- a/ChanLun.py +++ b/ChanLun.py @@ -1,188 +1,5 @@ -import warnings - -# 抑制 Docker 内 technical.util 的 fillna/ffill/bfill 的 pandas FutureWarning(pandas 2.x 弃用 object 静默 downcast) -warnings.filterwarnings( - "ignore", - category=FutureWarning, - message=".*Downcasting object dtype arrays on \\.fillna.*", -) - -from datetime import timedelta -from pandas import DataFrame -from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_MACD_STATE, Chan_PRICE_TREND, Chan_KLU_PATTERN -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 -from technical.util import resample_to_interval -from decimal import Decimal -import numpy as np -from ChanMACD import ChanMACD -from TF_DF import TF_DF -from ChanZone import StructureZone, StructureZoneConfig, analyze_structure_zones - -class ChanLun(): - def __init__(self): - self.time2m = 2 - self.time3m = 3 - self.time5m = 5 - self.time10m = 10 - self.time20m = 20 - self.time_m_intervals = [2, 3, 5, 10, 20] - self.time_m_symbols = ['2m', '3m', '5m', '10m', '20m'] - self.time30m = 30 - self.time45m = 45 - self.time_m15_intervals = [30, 45] - self.time_m15_symbols = ['30m', '45m'] - self.time2h = 2*60 - self.time4h = 4*60 - self.time6h = 6*60 - self.time8h = 8*60 - self.time12h = 12*60 - self.time16h = 16*60 - self.time_h_intervals = [2*60, 4*60, 6*60, 8*60, 12*60, 16*60] - self.time_h_symbols = ['2h', '4h', '6h', '8h', '12h', '16h'] - self.time2d = 2*24*60 - self.time3d = 3*24*60 - self.time_d_intervals = [2*24*60, 3*24*60] - self.time_d_symbols = ['2d', '3d'] - self.time1w = 7*24*60 - self.time2w = 14*24*60 - self.time_w_intervals = [14*24*60] - self.time_w_symbols = ['2w'] - self.time2M = 2*30*24*60 - self.time3M = 3*30*24*60 - self.time6M = 6*30*24*60 - self.time1y = 12*30*24*60 - self.time_M_intervals = [2*30*24*60, 3*30*24*60, 6*30*24*60, 12*30*24*60] - self.time_M_symbols = ['2M', '3M', '6M', '1y'] - self.time_symbols = ['1m', '2m', '3m', '5m', '10m', '15m', '20m', '30m', '45m','1h', '2h', '4h', '6h', '8h', '12h', '16h', '1d', '2d', '3d'] - self.tf_df_dict = {} - self.ema_symbols = ['5m', '15m', '30m', '45m', '1h', '2h', '4h', '8h', '12h', '1d', '2d', '3d'] - self.tf_df = TF_DF() - def init_data(self, dataframe, intervals, timeframes): - for index in range(0, len(intervals)): - timeframe = timeframes[index] - interval = intervals[index] - self.tf_df_dict[timeframe] = TF_DF(dataframe, interval, timeframe) - def init_dataframes(self, dataframe_m=None, dataframe_15m=None, dataframe_h=None, dataframe_d=None, dataframe_w=None, dataframe_M=None): - self.tf_df_dict = {} - if dataframe_m is not None: - self.tf_df_dict['1m'] = TF_DF(dataframe_m, 1, '1m') - self.init_data(dataframe_m, self.time_m_intervals, self.time_m_symbols) - if dataframe_15m is not None: - self.tf_df_dict['15m'] = TF_DF(dataframe_15m, 1, '15m') - self.init_data(dataframe_15m, self.time_m15_intervals, self.time_m15_symbols) - if dataframe_h is not None: - self.tf_df_dict['1h'] = TF_DF(dataframe_h, 1, '1h') - self.init_data(dataframe_h, self.time_h_intervals, self.time_h_symbols) - if dataframe_d is not None: - self.tf_df_dict['1d'] = TF_DF(dataframe_d, 1, '1d') - self.init_data(dataframe_d, self.time_d_intervals, self.time_d_symbols) - if dataframe_w is not None and False: - self.tf_df_dict['1w'] = TF_DF(dataframe_w, 1, '1w') - self.init_data(dataframe_w, self.time_w_intervals, self.time_w_symbols) - if dataframe_M is not None and False: - self.tf_df_dict['1M'] = TF_DF(dataframe_M, 1, '1M') - self.init_data(dataframe_M, self.time_M_intervals, self.time_M_symbols) - def get_ema52_dict(self): - if len(self.tf_df_dict) > 0: - return {key: self.tf_df_dict[key].get_ema52() for key in self.ema_symbols} - return None - def get_ema24_dict(self): - if len(self.tf_df_dict) > 0: - return {key: self.tf_df_dict[key].get_ema24() for key in self.ema_symbols} - return None - def get_current_klc_dict(self): - if len(self.tf_df_dict) > 0: - return {key: self.tf_df_dict[key].get_current_klc() for key in self.ema_symbols} - return None - def get_tf_df_by_timeframe(self, timeframe): - if timeframe in self.tf_df_dict: - return self.tf_df_dict[timeframe] - return None - def check_price_ema52(self, price): - key_list = [] - if len(self.tf_df_dict) > 0: - ema52_dict = self.get_ema52_dict() - for key in self.ema_symbols: - if ema52_dict[key] is not None: - if abs(price - ema52_dict[key]) < 100: - key_list.append(key) - return key_list - def get_ema_bsp(self, long_tf='1h', short_tf='15m'): - if long_tf in self.tf_df_dict and short_tf in self.tf_df_dict: - long_df = self.tf_df_dict[long_tf] - short_df = self.tf_df_dict[short_tf] - return long_df.get_ema_bsp(short_df) - return None - - - - - - def get_bsp_state(self, dataframe): - return self.tf_df.get_bsp_state(dataframe) - - def get_structure_zones(self, current_price=None, config=None): - if config is None: - config = StructureZoneConfig() - return analyze_structure_zones( - self.tf_df_dict, - self.ema_symbols, - current_price=current_price, - config=config, - ) - # TF_DF methods ------------------------------------------ - def get_ema_state(self, dataframe): - return self.tf_df.get_ema_state(dataframe) - def get_klu_state(self, dataframe): - return self.tf_df.get_klu_state(dataframe) - def check_fx(self, klc): - return self.tf_df.check_fx(klc) - def add_indicators1(self, df): - return self.tf_df.add_indicators(df) - def get_bi_list(self, dataframe): - return self.tf_df.get_bi_list(dataframe) - def get_kl_data(self, dataframe:DataFrame): - return self.tf_df.cal_kl_data(dataframe) - def cal_volume_ratio(self, dataframe, window=10): - return self.tf_df.cal_volume_ratio(dataframe, window) - def calculate_seg_zs(self, bi_list, seg_list): - return self.get_seg_zs_list(bi_list, seg_list) - def get_seg_list(self, bi_list): - return self.tf_df.get_seg_list(bi_list) - def cal_trend(self, klc_list): - return self.tf_df.cal_trend(klc_list) - def check_top_fx(self, last_bottom, klc): - return self.tf_df.check_top_fx(last_bottom, klc) - def check_bottom_fx(self, last_top, klc): - return self.tf_df.check_bottom_fx(last_top, klc) - def cal_bi_list(self, klc_list): - return self.tf_df.cal_bi_list(klc_list) - def find_first_bsp(self, bi_list, bi_zs_list): - return self.tf_df.find_first_bsp(bi_list, bi_zs_list) - def find_second_bsp(self, bi_list, first_bsp_list): - return self.tf_df.find_second_bsp(bi_list, first_bsp_list) - def find_all_bsp(self, bi_list, bi_zs_list): - return self.tf_df.find_all_bsp(bi_list, bi_zs_list) - def get_zs_list(self, bi_list, seg_list): - return self.tf_df.get_zs_list(bi_list, seg_list) - def cal_bi_zs(self, seg_list): - return self.tf_df.cal_bi_zs(seg_list) - def cal_bi_zs_list(self, bi_list): - #return self.tf_df.cal_bi_zs(bi_list) - return self.tf_df.cal_bi_zs_list(bi_list) - def get_bi_zs_list(self, bi_list): - return self.tf_df.get_bi_zs_list(bi_list) - def get_decimal(self, value): - return Decimal("{:.2f}".format(value)) - def get_klc_list(self, klu_list): - return self.tf_df.get_klc_list(klu_list) - def get_klu_list(self, dataframe): - return self.tf_df.cal_klu_pattern(self.get_kl_data(dataframe)) \ No newline at end of file +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.pipeline.ChanLun") +sys.modules[__name__] = _impl diff --git a/ChanLun_Classifier.py b/ChanLun_Classifier.py index 3933036..d56cf3e 100644 --- a/ChanLun_Classifier.py +++ b/ChanLun_Classifier.py @@ -1,529 +1,5 @@ -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, Chan_KLC_FX -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() - - # 获取训练集特征和标签 - save_csv = True if data_file_path else False - X_train, y_train = self.get_feature_data(train_df, save_csv=save_csv, csv_path=data_file_path if data_file_path else 'feature_data.csv') - - if len(X_train) == 0: - print("没有提取到足够的特征数据进行训练") - return None - - # 保存特征数据的步骤已经移到get_feature_data方法中处理 - # 以下是原有代码 - #{'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': 8, - 'eta': 0.01, - 'subsample': 0.8, - 'colsample_bytree': 0.8, - 'eval_metric': 'auc', - 'gamma': 0.0, - 'min_child_weight': 1, - 'alpha': 0, # L1正则化 - 'lambda': 0.5, # 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, save_csv=False, csv_path_prefix='param_', model_name=None): - """ - 寻找最佳参数组合 - :param dataframe: 输入的DataFrame,如果为None则使用初始化时的dataframe - :param save_csv: 是否保存特征数据到CSV文件 - :param csv_path_prefix: CSV文件保存路径前缀,会自动添加参数信息 - :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 i, params in enumerate(param_combinations): - print(f"\n尝试参数组合: {params}") - # 生成CSV文件名,包含一些参数信息 - param_info = f"eta{params['eta']}_depth{params['max_depth']}" - train_csv_path = f"{csv_path_prefix}train_{param_info}.csv" if save_csv else None - - model = self.train_model(dataframe=dataframe, data_file_path=train_csv_path, custom_params=params, model_name=model_name) - - # 分割数据集,后20%用于测试 - if dataframe is None: - dataframe = self.dataframe - - train_size = int(len(dataframe) * 0.8) - test_df = dataframe.iloc[train_size:].copy() - - # 获取测试集特征和标签 - test_csv_path = f"{csv_path_prefix}test_{param_info}.csv" if save_csv else None - X_test, y_test = self.get_validate_feature_data(test_df, save_csv=save_csv, csv_path=test_csv_path) - - 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, save_csv=False, csv_path='feature_data.csv'): - """ - 从dataframe提取特征数据 - :param dataframe: 输入的DataFrame - :param save_csv: 是否保存特征数据到CSV文件 - :param csv_path: CSV文件保存路径 - :return: 特征矩阵X和标签y - """ - # 使用ChanLun获取bi_list - klc_list = self.chan.get_klc_list(dataframe) - bi_list = self.chan.cal_bi_list(klc_list) - # 筛选方向为UP的bi的起始klc - feature_data = [] - labels = [] - feature_keys = [] # 用于保存特征名称 - - bi_index = 1 - sample_list = [] - for klc in klc_list: - if klc.klc_fx_type != Chan_KLC_FX.UNKNOWN: - sample_list.append(klc) - klc_count = 0 - print('Processing data...') - for klc in sample_list: - if bi_index >= len(bi_list): - bi_index = len(bi_list) - 1 - #bi = bi_list[bi_index] - #if klc.end_klu and bi.end_klc and klc.start_klu.index >= bi.start_klc.start_klu.index and klc.end_klu.index <= bi.end_klc.end_klu.index: - #klc.set_bi(bi) - - # 提取特征 - features = klc.get_feature_data() - - # 保存第一个样本的特征名称,用于CSV列名 - if len(feature_keys) == 0: - feature_keys = list(features.keys()) - # 将特征转换为模型可用的格式 - 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) - # 这个标签定义可以根据实际需求修改 - matched = False - for bi in bi_list: - if bi.end_klc and bi.end_klc.index == klc.index: - #print(bi.start_time, bi.start_klc.start_time, bi.dir) - label = 1 - matched = True - break - if not matched: - label = 0 - - feature_data.append(feature_vec) - labels.append(label) - klc_count += 1 - percent = klc_count/len(sample_list)*100 - if percent % 10 == 0: - print('Data processed:', percent, '%') - for index, key in enumerate(feature_keys): - print(index, key, feature_data[0][index]) - # 如果需要保存到CSV - if save_csv: - # 创建DataFrame保存特征数据 - # 只保留数值型特征 - numeric_feature_keys = [key for i, key in enumerate(feature_keys) - if i < len(feature_data[0]) if isinstance(feature_data[0][i], (int, float))] - - # 创建特征数据的DataFrame - df_features = pd.DataFrame(feature_data, columns=numeric_feature_keys) - # 添加标签列 - df_features['label'] = labels - # 添加时间信息便于分析 - if len(sample_list) > 0: - times = [klc.start_time for klc in sample_list] - df_features['time'] = times - - # 保存到CSV - df_features.to_csv(csv_path, index=False) - print(f"特征数据已保存到 {csv_path}") - - # 在return前添加 - positive_count = np.sum(labels) - print(f"正样本数量: {positive_count}, 负样本数量: {len(labels) - positive_count}") - print("Trainning data: ", len(feature_data), klc_list[-1].start_time, klc_list[-1].klc_fx_type , "---------------------") - return np.array(feature_data), np.array(labels) - def get_validate_feature_data(self, dataframe, save_csv=False, csv_path='validate_feature_data.csv'): - """ - 从dataframe提取特征数据 - :param dataframe: 输入的DataFrame - :param save_csv: 是否保存特征数据到CSV文件 - :param csv_path: CSV文件保存路径 - :return: 特征矩阵X和标签y - """ - # 使用ChanLun获取bi_list - klc_list = self.chan.get_klc_list(dataframe) - bi_list = self.chan.cal_bi_list(klc_list) - seg_list = self.chan.get_seg_list(bi_list) - # 筛选方向为UP的bi的起始klc - feature_data = [] - labels = [] - feature_keys = [] # 用于保存特征名称 - - bi_index = 1 - sample_list = [] - for klc in klc_list: - if klc.klc_fx_type != Chan_KLC_FX.UNKNOWN: - sample_list.append(klc) - for klc in sample_list: - if bi_index >= len(bi_list): - bi_index = len(bi_list) - 1 - bi = bi_list[bi_index] - # 提取特征 - features = klc.get_feature_data() - - # 保存第一个样本的特征名称,用于CSV列名 - if len(feature_keys) == 0: - feature_keys = list(features.keys()) - - # 将特征转换为模型可用的格式 - 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) - seg = seg_list[bi_index] - matched = False - for bi in bi_list: - if bi.end_klc and bi.end_klc.index == klc.index: - label = 1 - matched = True - break - if not matched: - label = 0 - - feature_data.append(feature_vec) - labels.append(label) - - # 如果需要保存到CSV - if save_csv: - # 创建DataFrame保存特征数据 - # 只保留数值型特征 - numeric_feature_keys = [key for i, key in enumerate(feature_keys) - if i < len(feature_data[0]) if isinstance(feature_data[0][i], (int, float))] - - # 创建特征数据的DataFrame - df_features = pd.DataFrame(feature_data, columns=numeric_feature_keys) - # 添加标签列 - df_features['label'] = labels - # 添加时间信息便于分析 - if len(sample_list) > 0: - times = [klc.start_time for klc in sample_list] - df_features['time'] = times - - # 保存到CSV - df_features.to_csv(csv_path, index=False) - print(f"验证特征数据已保存到 {csv_path}") - - print("Validating data: ", len(feature_data), klc_list[-1].start_time, klc_list[-1].klc_fx_type , "---------------------") - return np.array(feature_data), np.array(labels) - def validate_model(self, dataframe=None, save_csv=False, csv_path='validate_feature_data.csv'): - """ - 使用dataframe后20%的数据验证模型 - :param dataframe: 输入的DataFrame,如果为None则使用初始化时的dataframe - :param save_csv: 是否保存特征数据到CSV文件 - :param csv_path: CSV文件保存路径 - :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, save_csv=save_csv, csv_path=csv_path) - - 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.get_decimal(self.model.predict(dtest)[0]) - - def get_decimal(self, value): - return Decimal("{:.4f}".format(value)) - +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.analysis.ChanLun_Classifier") +sys.modules[__name__] = _impl diff --git a/ChanMACD.py b/ChanMACD.py index 86d5d3c..8785ea0 100644 --- a/ChanMACD.py +++ b/ChanMACD.py @@ -1,274 +1,5 @@ -from ChanKLU import ChanKLU -from ChanEnum import Chan_MACD_STATE, Chan_MACDSEG_DIR, Chan_MACDHISTSET_DIR, Chan_MACDUNITTF_DIR, Chan_MACDUNITTF_TYPE -from ChanMACDSeg import ChanMACDSeg -from ChanMACDUnitTF import ChanMACDUnitTF -from ChanMACDHistSet import ChanMACDHistSet - -class ChanMACD(): - def __init__(self, klu_list: list[ChanKLU]): - self.klu_list = klu_list - self.seg_list = [] - self.unittf_list = [] - self.histset_list = [] - # 状态标记列表 - self.high_position_list = [] # 高位列表 - self.high_empty_list = [] # 高位空列表 - self.return_zero_list = [] # 归零轴列表 - self.cross0_up_list = [] # 向上穿越零轴列表 - self.cross0_down_list = [] # 向下穿越零轴列表 - # 计算段 / UnitTF / HistSet 及状态标记 - self.cal_macd_state() - self.get_klu_sd_list() - def get_klu_sd(self): - if self.klu_list: - sd = self.klu_list[-1].separate_div - if sd > 1: - print(self.klu_list[-1].time, sd) - return True - return False - def get_klu_sd_list(self): - sd_list = [] - if self.klu_list: - for klu in self.klu_list: - hist = klu.macdhist - signal = False - if klu.pre and klu.next: - if klu.signal > 0: - signal = klu.pre.signal > klu.signal and klu.next.signal < klu.signal - else: - signal = klu.pre.signal < klu.signal and klu.next.signal > klu.signal - sd = klu.separate_div - if sd > 1 and ((hist > 0 and hist < 200) or (hist < 0 and hist > -200)): - sd_list.append(klu.time) - #print(klu.time, sd) - return sd_list - def cal_macd_state(self): - last_seg = None - last_unittf = None - last_histset = None - last_klu = None - for klu in self.klu_list: - # initialise first histset - if klu.macd == 0 and klu.signal == 0 and klu.macdhist == 0: - continue - if last_histset is None: - if klu.macdhist > 0: - last_histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, None, Chan_MACDHISTSET_DIR.ABOVE) - self.histset_list.append(last_histset) - else: - last_histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, None, Chan_MACDHISTSET_DIR.UNDER) - self.histset_list.append(last_histset) - else: - # initialise first seg and unittf - if last_seg is None: - # create histset afterwards - if last_histset.histset_dir == Chan_MACDHISTSET_DIR.ABOVE: - if klu.macdhist > 0: - last_histset.add_klu(klu) - else: - histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, last_histset, Chan_MACDHISTSET_DIR.UNDER) - self.histset_list.append(histset) - last_histset.set_next(histset) - histset.set_pre(last_histset) - last_histset.set_end_klu(last_klu) - last_histset = histset - else: - if klu.macdhist < 0: - last_histset.add_klu(klu) - else: - histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, last_histset, Chan_MACDHISTSET_DIR.ABOVE) - self.histset_list.append(histset) - last_histset.set_next(histset) - histset.set_pre(last_histset) - last_histset.set_end_klu(last_klu) - last_histset = histset - if last_klu.signal >= 0 and klu.signal < 0: - last_unittf = ChanMACDUnitTF(len(self.unittf_list), klu.time, klu, None, Chan_MACDUNITTF_DIR.UNDER, Chan_MACDUNITTF_TYPE.CROSS0, last_histset) - self.unittf_list.append(last_unittf) - last_seg = ChanMACDSeg(len(self.seg_list), klu.time, klu, None, Chan_MACDSEG_DIR.UNDER, last_unittf) - self.seg_list.append(last_seg) - elif last_klu.signal <= 0 and klu.signal > 0: - last_unittf = ChanMACDUnitTF(len(self.unittf_list), klu.time, klu, None, Chan_MACDUNITTF_DIR.ABOVE, Chan_MACDUNITTF_TYPE.CROSS0, last_histset) - self.unittf_list.append(last_unittf) - last_seg = ChanMACDSeg(len(self.seg_list), klu.time, klu, None, Chan_MACDSEG_DIR.ABOVE, last_unittf) - self.seg_list.append(last_seg) - # after the first seg and unittf - else: - # create histset afterwards - if last_histset.histset_dir == Chan_MACDHISTSET_DIR.ABOVE: - if klu.macdhist > 0: - last_histset.add_klu(klu) - else: - histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, last_histset, Chan_MACDHISTSET_DIR.UNDER) - self.histset_list.append(histset) - last_histset.set_next(histset) - histset.set_pre(last_histset) - last_histset.set_end_klu(last_klu) - last_histset = histset - if last_unittf: - last_unittf.add_histset(last_histset) - else: - if klu.macdhist < 0: - last_histset.add_klu(klu) - else: - histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, last_histset, Chan_MACDHISTSET_DIR.ABOVE) - self.histset_list.append(histset) - last_histset.set_next(histset) - histset.set_pre(last_histset) - last_histset.set_end_klu(last_klu) - last_histset = histset - if last_unittf: - last_unittf.add_histset(last_histset) - if last_klu.signal >= 0 and klu.signal < 0: - last_unittf.set_end_klu(last_klu, Chan_MACDUNITTF_TYPE.CROSS0) - unittf = ChanMACDUnitTF(len(self.unittf_list), klu.time, klu, last_unittf, Chan_MACDUNITTF_DIR.UNDER, Chan_MACDUNITTF_TYPE.CROSS0, last_histset) - self.unittf_list.append(unittf) - last_unittf.set_next(unittf) - last_seg.set_end_klu(last_klu) - seg = ChanMACDSeg(len(self.seg_list), klu.time, klu, last_seg, Chan_MACDSEG_DIR.UNDER, unittf) - self.seg_list.append(seg) - last_seg.set_next(seg) - last_seg = seg - last_unittf = unittf - elif last_klu.signal <= 0 and klu.signal > 0: - last_unittf.set_end_klu(last_klu, Chan_MACDUNITTF_TYPE.CROSS0) - unittf = ChanMACDUnitTF(len(self.unittf_list), klu.time, klu, last_unittf, Chan_MACDUNITTF_DIR.ABOVE, Chan_MACDUNITTF_TYPE.CROSS0, last_histset) - self.unittf_list.append(unittf) - last_unittf.set_next(unittf) - last_seg.set_end_klu(last_klu) - seg = ChanMACDSeg(len(self.seg_list), klu.time, klu, last_seg, Chan_MACDSEG_DIR.ABOVE, unittf) - self.seg_list.append(seg) - last_seg.set_next(seg) - last_seg = seg - last_unittf = unittf - elif last_unittf.is_end and last_klu.macd < klu.macd and klu.macd > klu.signal: - unittf = ChanMACDUnitTF(len(self.unittf_list), klu.time, klu, last_unittf, Chan_MACDUNITTF_DIR.ABOVE, Chan_MACDUNITTF_TYPE.NEAR0, last_histset) - self.unittf_list.append(unittf) - last_unittf.set_next(unittf) - last_seg.add_unittf(unittf) - last_unittf = unittf - last_seg.add_klu(klu) - else: - if not last_unittf.is_end: - last_unittf.add_klu(klu) - last_seg.add_klu(klu) - last_klu = klu - klu.cal_macd_state() - #print(klu.time, klu.macd_state, klu.continue_div, klu.separate_div, klu.macd, klu.signal, klu.macdhist, klu.ema24, klu.ema52, klu.close) - return self.klu_list - def cal_macd(self): - last_seg = None - last_unittf = None - last_histset = None - histset = None - last_klu = None - for klu in self.klu_list: - klu.cal_macd_state() - print(klu.time, klu.macd_state) - # 1) 只有当 MACD 已可用(非 UNKNOWN)时,才开始初始化段/单元 - if last_seg is None: - if klu.macd_state != Chan_MACD_STATE.UNKNOWN: - # 初始化首个直方图集合(根据当前柱体正负) - if klu.macdhist >= 0: - histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, None, Chan_MACDHISTSET_DIR.ABOVE) - else: - histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, None, Chan_MACDHISTSET_DIR.UNDER) - self.histset_list.append(histset) - last_histset = histset - - # 初始化首段 - seg_dir = Chan_MACDSEG_DIR.ABOVE if klu.signal >= 0 else Chan_MACDSEG_DIR.UNDER - seg = ChanMACDSeg(len(self.seg_list), klu.time, klu, None, seg_dir, last_unittf) - self.seg_list.append(seg) - last_seg = seg - - # 初始化首个UnitTF - unittf_dir = Chan_MACDUNITTF_DIR.ABOVE if klu.signal >= 0 else Chan_MACDUNITTF_DIR.UNDER - unittf = ChanMACDUnitTF(len(self.unittf_list), klu.time, klu, None, unittf_dir, Chan_MACDUNITTF_TYPE.START, histset) - self.unittf_list.append(unittf) - last_unittf = unittf - last_seg.add_unittf(unittf) - # 未就绪则继续等下一根;已就绪亦已完成首个结构初始化,继续下一根 - last_klu = klu - continue - # 3) 直方图集合(基于当前 unittf) - if klu.macdhist >= 0: - if last_histset and last_histset.histset_dir == Chan_MACDHISTSET_DIR.ABOVE: - last_histset.add_klu(klu) - else: - # 结束旧 histset(以前一根结束更合理) - if last_histset and last_klu: - last_histset.set_end_klu(last_klu) - histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, last_histset, Chan_MACDHISTSET_DIR.ABOVE if klu.macdhist >= 0 else Chan_MACDHISTSET_DIR.UNDER) - self.histset_list.append(histset) - if last_histset: - last_histset.set_next(histset) - last_histset = histset - if last_unittf: - last_unittf.add_histset(histset) - else: - if last_histset and last_histset.histset_dir == Chan_MACDHISTSET_DIR.UNDER: - last_histset.add_klu(klu) - else: - # 结束旧 histset(以前一根结束更合理) - if last_histset and last_klu: - last_histset.set_end_klu(last_klu) - histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, last_histset, Chan_MACDHISTSET_DIR.ABOVE if klu.macdhist >= 0 else Chan_MACDHISTSET_DIR.UNDER) - self.histset_list.append(histset) - if last_histset: - last_histset.set_next(histset) - last_histset = histset - if last_unittf: - last_unittf.add_histset(histset) - # 2) 过零切段(使用KLU中的穿越状态) - if (klu.macd_state == Chan_MACD_STATE.CROSS0_UP or - klu.macd_state == Chan_MACD_STATE.CROSS0_DOWN): - # 结束旧 unittf - last_unittf.set_end_klu(last_klu, Chan_MACDUNITTF_TYPE.CROSS0) - # 新的单位时间周期 - new_dir = Chan_MACDUNITTF_DIR.ABOVE if klu.signal >= 0 else Chan_MACDUNITTF_DIR.UNDER - unittf = ChanMACDUnitTF(len(self.unittf_list), klu.time, klu, last_unittf, new_dir, Chan_MACDUNITTF_TYPE.CROSS0, histset) - self.unittf_list.append(unittf) - last_unittf.set_next(unittf) - last_unittf = unittf - # 收尾旧段 - last_seg.set_end_klu(last_klu) - # 新段方向取反 - new_dir = Chan_MACDSEG_DIR.UNDER if last_seg.seg_dir == Chan_MACDSEG_DIR.ABOVE else Chan_MACDSEG_DIR.ABOVE - seg = ChanMACDSeg(len(self.seg_list), klu.time, klu, last_seg, new_dir, last_unittf) - self.seg_list.append(seg) - last_seg.set_next(seg) - last_seg = seg - last_seg.add_unittf(unittf) - else: - # 4) UnitTF 状态机:用黄线Signal的归零轴 - if last_klu.macd_state == Chan_MACD_STATE.NEAR0 and last_unittf.div_count > 1: - #print(klu.time, klu.macd_state) - if klu.macd_state == Chan_MACD_STATE.RZ_UP: - last_unittf.set_end_klu(last_klu, Chan_MACDUNITTF_TYPE.NEAR0) - new_dir = Chan_MACDUNITTF_DIR.ABOVE if klu.signal >= 0 else Chan_MACDUNITTF_DIR.UNDER - unittf = ChanMACDUnitTF(len(self.unittf_list), klu.time, klu, last_unittf, new_dir, Chan_MACDUNITTF_TYPE.NEAR0, histset) - self.unittf_list.append(unittf) - last_unittf.set_next(unittf) - last_unittf = unittf - last_seg.add_unittf(unittf) - elif klu.macd_state == Chan_MACD_STATE.RZ_DOWN: - last_unittf.set_end_klu(last_klu, Chan_MACDUNITTF_TYPE.NEAR0) - new_dir = Chan_MACDUNITTF_DIR.ABOVE if klu.signal >= 0 else Chan_MACDUNITTF_DIR.UNDER - unittf = ChanMACDUnitTF(len(self.unittf_list), klu.time, klu, last_unittf, new_dir, Chan_MACDUNITTF_TYPE.NEAR0, histset) - self.unittf_list.append(unittf) - last_unittf.set_next(unittf) - last_unittf = unittf - last_seg.add_unittf(unittf) - else: - last_unittf.add_klu(klu) - last_seg.add_klu(klu) - else: - last_unittf.add_klu(klu) - last_seg.add_klu(klu) - - last_klu = klu - last_histset.set_end_klu(last_klu) - last_unittf.set_end_klu(last_klu, None) - last_seg.set_end_klu(last_klu) - return self.klu_list \ No newline at end of file +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.analysis.ChanMACD") +sys.modules[__name__] = _impl diff --git a/ChanMACDHistSet.py b/ChanMACDHistSet.py index 484fc94..0fb65a2 100644 --- a/ChanMACDHistSet.py +++ b/ChanMACDHistSet.py @@ -1,117 +1,5 @@ -from ChanEnum import Chan_MACDHISTSET_DIR, Chan_MACDUNITTF_DIV, Chan_MACD_STATE - -class ChanMACDHistSet(): - def __init__(self, index, start_time, start_klu, pre_histset, dir): - self.index = index - self.start_time = start_time - self.end_time = None - self.klu_list = [] - self.klu_list.append(start_klu) - self.histset_dir = dir - self.next = None - self.pre = pre_histset - self.peak_klu = None - self.area = start_klu.macdhist - self.unittf_div = Chan_MACDUNITTF_DIV.UNDIV - self.middle_klu = None - self.div_count = 0 - self.last_klu = start_klu - self.start_klu = start_klu - self.peak_div_list = [] - self.middle_area = 0 - self.total_macdhist = 0 - def set_next(self, next_histset): - self.next = next_histset - def set_pre(self, pre_histset): - self.pre = pre_histset - def set_middle_klu(self, middle_klu): - self.middle_klu = middle_klu - #self.middle_area = abs(middle_klu.macdhist) - #self.middle_klu = None - def set_unittf_div(self, unittf_div): - self.unittf_div = unittf_div - def add_klu(self, klu): - klu.set_histset(self) - self.klu_list.append(klu) - self.area += abs(klu.macdhist) - if self.middle_klu: - self.middle_area += abs(klu.macdhist) - if self.middle_klu and self.middle_klu.index + 1 == klu.index: - self.low_klu = None - self.peak_klu = None - self.div_count = 0 - self.peak_div_list = [] - else: - if self.last_klu: - self.cal_macdhist_klu(klu) - self.last_klu = klu - def cal_macdhist_klu(self, klu): - if self.middle_klu: - if klu.index >= self.middle_klu.index + 2: - if klu.pre.pre: - if abs(klu.pre.macdhist) > abs(klu.pre.pre.macdhist) and abs(klu.pre.macdhist) > abs(klu.macdhist): - if self.peak_klu: - if abs(klu.pre.macdhist) > abs(self.peak_klu.macdhist): - self.peak_klu = klu.pre - #self.div_count = 0 - #self.peak_div_list = [] - else: - if klu.pre.macd * klu.pre.macdhist > 0: - self.peak_div_list.append(klu.pre) - self.div_count += 1 - klu.pre.continue_div = True - else: - self.peak_klu = klu.pre - else: - if len(self.klu_list) >= 3: - if klu.pre.pre: - if abs(klu.pre.macdhist) > abs(klu.pre.pre.macdhist) and abs(klu.pre.macdhist) > abs(klu.macdhist): - if self.peak_klu: - if abs(klu.pre.macdhist) > abs(self.peak_klu.macdhist): - self.peak_klu = klu.pre - #self.div_count = 0 - #self.peak_div_list = [] - else: - if klu.pre.macd * klu.pre.macdhist > 0 and klu.pre.signal * klu.pre.macdhist > 0: - self.peak_div_list.append(klu.pre) - self.div_count += 1 - klu.pre.continue_div = True - else: - self.peak_klu = klu.pre - def set_end_klu(self, end_klu): - self.end_klu = end_klu - self.end_time = end_klu.time - #if len(self.peak_div_list) > 0: - #klu = self.peak_div_list[-1] - #if klu.macd * klu.macdhist > 0: - #end_klu.continue_div = True - #print(end_klu.time, "Continue Div") - if self.start_klu.index == end_klu.index: - self.peak_klu = self.start_klu - if self.start_klu.index + 1 == end_klu.index: - if abs(self.start_klu.macdhist) > abs(end_klu.macdhist): - self.peak_klu = self.start_klu - else: - self.peak_klu = end_klu - if len(self.klu_list) >= 3 and self.peak_klu == None: - self.peak_klu = self.klu_list[0] - for klu in self.klu_list: - if abs(klu.macdhist) > abs(self.peak_klu.macdhist): - self.peak_klu = klu - peak_str = "" - state_str = "" - for peak_div in self.peak_div_list: - peak_str += f"{peak_div.time}, " - state_str += f"{peak_div.macd_state}, " - total_macdhist = 0 - first_klu = self.klu_list[0] - last_klu = self.klu_list[-1] - if (first_klu.macd > 0 and last_klu.macd > 0 and first_klu.macdhist > 0) or (first_klu.macd < 0 and last_klu.macd < 0 and first_klu.macdhist < 0): - for klu in self.klu_list: - self.total_macdhist += klu.macdhist - if abs(self.total_macdhist) < 150: - #print(self.end_time, "Total MACDHist: ", self.total_macdhist) - last_klu.separate_div = 99999 - #if self.peak_klu and len(self.peak_div_list) > 0: - #print("Continue Div: ",self.start_time, "Peak:", self.peak_klu.time, "Div: ", peak_str, state_str) - \ No newline at end of file +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.analysis.ChanMACDHistSet") +sys.modules[__name__] = _impl diff --git a/ChanMACDSeg.py b/ChanMACDSeg.py index b013b3f..89a7338 100644 --- a/ChanMACDSeg.py +++ b/ChanMACDSeg.py @@ -1,49 +1,5 @@ -from ChanEnum import Chan_MACDSEG_DIR - - -class ChanMACDSeg(): - def __init__(self, index, start_time, start_klu, pre_seg, seg_dir, start_unittf): - self.index = index - self.start_time = start_time - self.end_time = None - self.start_klu = start_klu - self.end_klu = None - self.klu_list = [] - self.klu_list.append(start_klu) - self.unittf_list = [] - self.seg_dir = seg_dir - self.pre = pre_seg - self.next = None - self.high_klu = start_klu - self.low_klu = start_klu - self.ref_klu = None - self.unittf_list.append(start_unittf) - def set_next(self, next_seg): - self.next = next_seg - def set_pre(self, pre_seg): - self.pre = pre_seg - def add_klu(self, klu): - if klu: - self.klu_list.append(klu) - klu.set_seg(self) - if self.seg_dir == Chan_MACDSEG_DIR.ABOVE: - if klu.macdhist > self.high_klu.macdhist: - self.high_klu = klu - else: - if klu.macdhist < self.low_klu.macdhist: - self.low_klu = klu - else: - if klu.macdhist < self.high_klu.macdhist: - self.high_klu = klu - else: - if klu.macdhist > self.low_klu.macdhist: - self.low_klu = klu - if self.high_klu.index != self.start_klu.index: - self.ref_klu = self.high_klu - def add_unittf(self, unittf): - self.unittf_list.append(unittf) - unittf.set_next(self) - def set_end_klu(self, end_klu): - self.add_klu(end_klu) - self.end_klu = end_klu - self.end_time = end_klu.time \ No newline at end of file +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.analysis.ChanMACDSeg") +sys.modules[__name__] = _impl diff --git a/ChanMACDUnitTF.py b/ChanMACDUnitTF.py index 4f13fff..66f43ee 100644 --- a/ChanMACDUnitTF.py +++ b/ChanMACDUnitTF.py @@ -1,141 +1,5 @@ -from ChanEnum import Chan_MACD_STATE, Chan_MACDUNITTF_DIR, Chan_MACDHISTSET_DIR, Chan_MACDUNITTF_DIV, Chan_MACDUNITTF_TYPE - - -class ChanMACDUnitTF(): - def __init__(self, index, start_time, start_klu, pre_unittf, unittf_dir, start_type, start_histset): - self.index = index - self.start_time = start_time - self.end_time = None - self.start_klu = start_klu - self.end_klu = None - self.klu_list = [] - self.klu_list.append(start_klu) - self.histset_list = [] - self.histset_list.append(start_histset) - self.div_count = 0 - start_histset.set_middle_klu(start_klu) - self.next = None - self.pre = pre_unittf - self.unittf_dir = unittf_dir - self.start_type = start_type - self.end_type = None - self.peak_klu = None - self.div_type = Chan_MACDUNITTF_DIV.UNDIV - self.div_peak_list = [] - self.is_end = False - def set_next(self, next_unittf): - self.next = next_unittf - def set_pre(self, pre_unittf): - self.pre = pre_unittf - def add_histset(self, histset): - self.histset_list.append(histset) - def add_klu(self, klu): - self.klu_list.append(klu) - self.cal_peak_div() - self.cal_macd_state() - def cal_peak_div(self): - self.div_count = 0 - self.div_peak_list = [] - self.peak_klu = None - if len(self.histset_list) == 0: - return - if len(self.histset_list) == 1: - self.div_type = self.histset_list[0].unittf_div - self.peak_klu = self.histset_list[0].peak_klu - else: - - for index in range(0, len(self.histset_list)): - histset = self.histset_list[index] - if self.same_dir(histset): - #if histset.peak_klu: - #print("Unittf: ", self.start_klu.time, len(self.histset_list), histset.peak_klu.time) - if self.peak_klu: - if histset.peak_klu: - if abs(histset.peak_klu.macdhist) >= abs(self.peak_klu.macdhist): - self.peak_klu = histset.peak_klu - self.div_type = Chan_MACDUNITTF_DIV.UNDIV - self.div_count = 0 - else: - self.div_type = Chan_MACDUNITTF_DIV.DISCRETE - self.div_count += 1 - self.div_peak_list.append(histset.peak_klu) - #print("Unittf: ", self.start_klu.time) - if histset.peak_klu.macd > 0 and histset.histset_dir == Chan_MACDHISTSET_DIR.ABOVE: - histset.peak_klu.set_separate_div(self.div_count) - elif histset.peak_klu.macd < 0 and histset.histset_dir == Chan_MACDHISTSET_DIR.UNDER: - histset.peak_klu.set_separate_div(self.div_count) - else: - if histset.peak_klu: - self.peak_klu = histset.peak_klu - def cal_macd_state(self): - if len(self.klu_list) > 0: - last_klu = self.klu_list[0] - macd_peak_klu = None - signal_peak_klu = None - for index in range(1, len(self.klu_list)): - klu = self.klu_list[index] - if klu.macd == 0 and klu.signal == 0 and klu.macdhist == 0: - continue - if self.start_type == Chan_MACDUNITTF_TYPE.CROSS0 or self.start_type == Chan_MACDUNITTF_TYPE.NEAR0: - if self.unittf_dir == Chan_MACDUNITTF_DIR.ABOVE: - if macd_peak_klu is None: - if last_klu.macd < klu.macd: - if last_klu.signal < last_klu.macdhist: - last_klu.set_macd_state(Chan_MACD_STATE.UP) - else: - last_klu.set_macd_state(Chan_MACD_STATE.HIGH) - else: - macd_peak_klu = last_klu - last_klu.set_macd_state(Chan_MACD_STATE.PEAK) - elif klu.macd > macd_peak_klu.macd: - macd_peak_klu = None - last_klu.set_macd_state(Chan_MACD_STATE.HIGH) - elif last_klu.signal < klu.signal: - last_klu.set_macd_state(Chan_MACD_STATE.HIGH_EMPTY) - elif signal_peak_klu is None: - signal_peak_klu = last_klu - last_klu.set_macd_state(Chan_MACD_STATE.HIGH_EMPTY) - elif klu.signal > signal_peak_klu.signal: - signal_peak_klu = None - last_klu.set_macd_state(Chan_MACD_STATE.HIGH) - elif last_klu.macd < last_klu.signal: - last_klu.set_macd_state(Chan_MACD_STATE.RETURN_ZERO) - if self.return_zero(last_klu, klu): - self.end_type = Chan_MACDUNITTF_TYPE.NEAR0 - self.is_end = True - self.end_klu = klu - self.end_time = klu.time - klu.set_macd_state(Chan_MACD_STATE.NEAR0) - #print(self.index, last_klu.time, last_klu.macd, last_klu.signal, last_klu.macd_state, klu.macd_state) - break - #print(self.index, last_klu.time, last_klu.macd, last_klu.signal, last_klu.macd_state) - last_klu = klu - - def return_zero(self, last_klu, klu): - return_zero = False - if last_klu.close < last_klu.ema52 and klu.close > klu.ema52: - return_zero = True - return_zero = False - return return_zero - def same_dir(self, histset): - if self.unittf_dir == Chan_MACDUNITTF_DIR.ABOVE: - return histset.histset_dir == Chan_MACDHISTSET_DIR.ABOVE - else: - return histset.histset_dir == Chan_MACDHISTSET_DIR.UNDER - def set_end_klu(self, end_klu, end_type): - self.end_type = end_type - self.end_klu = end_klu - self.end_time = end_klu.time - self.is_end = True - self.cal_macd_state() - div_time = "" - for div in self.div_peak_list: - div_time += f"{div.time}, " - histset_time = "" - for histset in self.histset_list: - histset_time += f"{histset.start_time}, " - - #if self.peak_klu and self.div_type == Chan_MACDUNITTF_DIV.DISCRETE: - #print("Cross Div: ", self.start_klu.time, self.peak_klu.time, self.div_count, self.div_type, self.unittf_dir, div_time, len(self.histset_list)) - - \ No newline at end of file +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.analysis.ChanMACDUnitTF") +sys.modules[__name__] = _impl diff --git a/ChanPY.py b/ChanPY.py index 0096f3c..f435515 100644 --- a/ChanPY.py +++ b/ChanPY.py @@ -1,402 +1,5 @@ -import sys -import os -#sys.path.append(os.path.abspath("/Users/jack/Documents/GitHub/chan.py")) -sys.path.append(os.path.abspath("/Users/jack/Project/chan.py")) -from Chan import CChan -from BuySellPoint.BS_Point import CBS_Point -from ChanConfig import CChanConfig -from Common.CEnum import AUTYPE, DATA_SRC, KL_TYPE, DATA_FIELD, BSP_TYPE, FX_TYPE, BI_DIR, KLINE_DIR, SEG_DIR -from KLine.KLine_Unit import CKLine_Unit -from Common.CTime import CTime -from Common.func_util import kltype_lt_day, str2float -from Bi.Bi import CBi -from typing import Dict, List -from functools import reduce -from pandas import DataFrame -from datetime import datetime, timedelta, timezone - - -def GetColumnNameFromFieldList(fileds: str): - _dict = { - "time": DATA_FIELD.FIELD_TIME, - "open": DATA_FIELD.FIELD_OPEN, - "high": DATA_FIELD.FIELD_HIGH, - "low": DATA_FIELD.FIELD_LOW, - "close": DATA_FIELD.FIELD_CLOSE, - "volume": DATA_FIELD.FIELD_VOLUME - } - return [_dict[x] for x in fileds.split(",")] -class ChanPY(): - k_type = KL_TYPE.K_5M - config = CChanConfig({ - "bi_strict": True, - "bi_algo": "normal", - "trigger_step": True, - "skip_step": 0, - "divergence_rate": float("inf"), - "bsp2_follow_1": False, - "bsp3_follow_1": False, - "min_zs_cnt": 1, - "bs1_peak": False, - "macd_algo": "peak", - "bs_type": '1,2,3a,1p,2s,3b', - "print_warning": True, - "zs_algo": "normal", - }) - chan = CChan( - code="BTC/USDT:USDT", - data_src=DATA_SRC.CCXT, - lv_list=[k_type], - config=config, - autype=AUTYPE.QFQ, - ) - klu_list = [] - bsps = [] - chanIn = True - #def __init__(self, dataframe): - #self.klu_list = self.get_kl_data(dataframe) - #for klu in self.klu_list: - #self.chan.trigger_load({self.k_type: [klu]}) - def add_klu(self, klu): - if klu: - self.chan.trigger_load({self.k_type: [klu]}) - self.klu_list.append(klu) - def add_klu_from_dataframe(self, dataframe): - if len(dataframe) > len(self.klu_list) and len(dataframe) - len(self.klu_list) == 1: - klu = self.get_last_klu(dataframe) - self.chan.trigger_load({self.k_type: [klu]}) - self.klu_list.append(klu) - def parse_time_column(self, inp): - if len(inp) == 10: - year = int(inp[:4]) - month = int(inp[5:7]) - day = int(inp[8:10]) - hour = minute = 0 - elif len(inp) == 17: - year = int(inp[:4]) - month = int(inp[4:6]) - day = int(inp[6:8]) - hour = int(inp[8:10]) - minute = int(inp[10:12]) - elif len(inp) == 19: - year = int(inp[:4]) - month = int(inp[5:7]) - day = int(inp[8:10]) - hour = int(inp[11:13]) - minute = int(inp[14:16]) - else: - raise Exception(f"unknown time column from TradingView:{inp}") - return CTime(year, month, day, hour, minute, auto=not kltype_lt_day(self.k_type)) - - def create_item_dict(self, data, column_name): - for i in range(len(data)): - data[i] = self.parse_time_column(data[i]) if i == 0 else str2float(data[i]) - return dict(zip(column_name, data)) - def get_last_klu(self, dataframe:DataFrame): - fields = "time,open,high,low,close,volume" - item = dataframe.iloc[-1] - date = item['date'] - o = item['open'] - h = item['high'] - l = item['low'] - c = item['close'] - v = item['volume'] - #time_obj = date.fromtimestamp(date) - time_str = date.strftime('%Y-%m-%d %H:%M:%S') - item_data = [ - time_str, - o, - h, - l, - c, - v - ] - klu = CKLine_Unit(self.create_item_dict(item_data, GetColumnNameFromFieldList(fields)), autofix=True) - klu.set_idx(len(dataframe)-1) - return klu - 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) - time_str = date.strftime('%Y-%m-%d %H:%M:%S') - item_data = [ - time_str, - o, - h, - l, - c, - v - ] - klu = CKLine_Unit(self.create_item_dict(item_data, GetColumnNameFromFieldList(fields)), autofix=True) - klu.set_idx(i) - klu_list.append(klu) - return klu_list - def get_bsp_type(self, bsp_type, is_buy): - if is_buy: - if bsp_type == BSP_TYPE.T1: - return 1 - if bsp_type == BSP_TYPE.T1P: - return 2 - if bsp_type == BSP_TYPE.T2: - return 3 - if bsp_type == BSP_TYPE.T2S: - return 4 - if bsp_type == BSP_TYPE.T3A: - return 5 - if bsp_type == BSP_TYPE.T3B: - return 6 - else: - if bsp_type == BSP_TYPE.T1: - return -1 - if bsp_type == BSP_TYPE.T1P: - return -2 - if bsp_type == BSP_TYPE.T2: - return -3 - if bsp_type == BSP_TYPE.T2S: - return -4 - if bsp_type == BSP_TYPE.T3A: - return -5 - if bsp_type == BSP_TYPE.T3B: - return -6 - def get_bsps(self, dataframe:DataFrame): - fields = "time,open,high,low,close,volume" - bsps = [] - updown = [] - bi_sure = [] - if self.chanIn: - kl_data = self.get_kl_data(dataframe) - bsp_list = [] - bsp_list_pre_len = 0 - last_bsp_value = 0 - last_updown = -1 - bi_list_pre_len = 0 - pre_bi = None - zs_list_pre_len = 0 - pre_zs = None - for klu in kl_data: # 获取单根K线 - self.chan.trigger_load({self.k_type: [klu]}) # 喂给CChan新增k线 - self.last_kline = klu - bsp_list = self.chan.get_bsp() - kl_datas = self.chan.kl_datas[self.k_type] - bi_list = kl_datas.bi_list - lst = kl_datas.lst - if len(bsp_list) > 0: - last_bsp = bsp_list[-1] - #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, lst[-2].fx, bi_list[-1].dir, bi_list[-1].is_sure,klu.close) - if bsp_list_pre_len > len(bsp_list): - if abs(last_bsp_value) == 1 or abs(last_bsp_value) == 2: - bsps.append(1) - #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, 98) - else: - bsps.append(99) - else: - if bsp_list_pre_len == len(bsp_list): - if klu.idx == last_bsp.klu.idx: - last_bsp_value = self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy) - bsps.append(last_bsp_value) - #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value) - else: - bsps.append(0) - else: - last_bsp_value = self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy) - bsps.append(last_bsp_value) - #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value) - else: - bsps.append(0) - bsp_list_pre_len = len(bsp_list) - - #Check zs ----------------------------------- - zs_list = kl_datas.zs_list - if len(zs_list) > 0: - zs = zs_list[-1] - #if zs_list_pre_len > len(zs_list): - #print("No zs", zs.begin.time) - #if len(zs_list) > zs_list_pre_len: - #print(zs.begin.time, zs.end.time, zs.end.idx, zs.high, zs.low, zs.peak_high, zs.peak_low) - zs_list_pre_len = len(zs_list) - pre_zs = zs - - #Check Bi ----------------------------------- - if len(bi_list) > 0: - last_bi = bi_list[-1] - if len(bi_list) == 1: - if last_bi.dir == BI_DIR.UP: - updown.append(1) - last_updown = 1 - else: - updown.append(-1) - last_updown = -1 - else: - if last_updown == 1: - if last_bi.dir == BI_DIR.UP: - updown.append(0) - else: - updown.append(-1) - last_updown = -1 - else: - if last_bi.dir == BI_DIR.DOWN: - updown.append(0) - else: - updown.append(1) - last_updown = 1 - else: - updown.append(0) - bi_list = kl_datas.bi_list - if len(bi_list) > 0: - last_bi = bi_list[-1] - #if bi_list_pre_len > len(bi_list): - #print("Bi ", klu.time, pre_bi.idx, pre_bi.is_sure, bi_list[-1].idx, bi_list[-1].is_sure) - if last_bi.is_sure: - bi_sure.append(1) - #print(klu.time, last_bi.is_sure) - else: - bi_sure.append(0) - pre_bi = bi_list[-1] - bi_list_pre_len = len(bi_list) - else: - bi_sure.append(0) - #if bsps[-1] != 0 or updown[-1] != 0: - #print(klu.time, bsps[-1], updown[-1], bi_list[-1].is_sure) - self.chanIn = False - else: - klu = self.get_last_klu(dataframe) - if self.last_kline.time < klu.time: - self.chan.trigger_load({self.k_type: [klu]}) # 喂给CChan新增k线 - self.last_kline = klu - for index in range(0, len(bsps)): - if not (abs(bsps[index]) == 1 or abs(bsps[index]) == 2): - bsps[index] = 0 - else: - if bsps[index] == 2: - bsps[index] = 1 - else: - if bsps[index] == -2: - bsps[index] = -1 - else: - bsps[index] = 0 - #print(bsps) - #print(updown) - kl_datas = self.chan.kl_datas[self.k_type] - #for zs in kl_datas.zs_list: - #print(zs.begin.time, zs.end.time) - return bsps, updown, bi_sure - - def get_bsp_state1(self, dataframe:DataFrame): - fields = "time,open,high,low,close,volume" - bsps = [] - if self.chanIn: - kl_data = self.get_kl_data(dataframe) - self.chan.trigger_load({self.k_type: kl_data}) - bsp_list = self.chan.get_bsp() - bsp_index = 0 - for klu in kl_data: - if bsp_index >= len(bsp_list): - bsp_index = len(bsp_list) - 1 - bsp = bsp_list[bsp_index] - if klu.idx == bsp.klu.idx: - bsp_type = self.get_bsp_type(bsp.type[0], bsp.is_buy) - if abs(bsp_type) == 1 or abs(bsp_type) == 10: - bsps.append(1) - else: - bsps.append(0) - bsp_index = bsp_index + 1 - else: - bsps.append(0) - self.chanIn = False - else: - klu = CKLine_Unit(self.create_item_dict(self.get_last_item_data(dataframe), GetColumnNameFromFieldList(fields)), autofix=True) - if self.last_kline.time < klu.time: - self.chan.trigger_load({self.k_type: [klu]}) # 喂给CChan新增k线 - self.last_kline = klu - return bsps - def get_bsp_state(self, dataframe:DataFrame): - fields = "time,open,high,low,close,volume" - if self.chanIn: - kl_data = self.get_kl_data(dataframe) - bsp_list = [] - bsp_list_pre_len = 0 - last_bsp_value = 0 - last_bsp_index = 0 - for klu in kl_data: # 获取单根K线 - self.chan.trigger_load({self.k_type: [klu]}) # 喂给CChan新增k线 - self.last_kline = klu - bsp_list = self.chan.get_bsp() - kl_datas = self.chan.kl_datas[self.k_type] - bi_list = kl_datas.bi_list - lst = kl_datas.lst - if len(bsp_list) > 0: - last_bsp = bsp_list[-1] - #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, lst[-2].fx, bi_list[-1].dir, bi_list[-1].is_sure,klu.close) - if bsp_list_pre_len > len(bsp_list): - if abs(last_bsp_value) == 1: - self.bsps.append(1) - #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, 98) - else: - self.bsps.append(99) - else: - if bsp_list_pre_len == len(bsp_list): - if klu.idx == last_bsp.klu.idx: - if last_bsp.klu.idx - last_bsp_index > 3: - last_bsp_value = self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy) - self.bsps.append(last_bsp_value) - else: - self.bsps.append(0) - last_bsp_index = last_bsp.klu.idx - #if abs(last_bsp_value) == 1 or abs(last_bsp_value) == 2: - #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, "Knonw") - else: - self.bsps.append(0) - else: - if klu.idx == last_bsp.klu.idx: - if last_bsp.klu.idx - last_bsp_index > 3: - last_bsp_value = self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy) - self.bsps.append(last_bsp_value) - else: - self.bsps.append(0) - last_bsp_index = last_bsp.klu.idx - #if abs(last_bsp_value) == 1 or abs(last_bsp_value) == 2: - #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, "Knonw") - else: - self.bsps.append(0) - else: - self.bsps.append(0) - bsp_list_pre_len = len(bsp_list) - self.chanIn = False - else: - klu = self.get_last_klu(dataframe) - if self.last_kline.time < klu.time: - self.chan.trigger_load({self.k_type: [klu]}) # 喂给CChan新增k线 - self.last_kline = klu - bsp_list = self.chan.get_bsp() - last_bsp = bsp_list[-1] - if last_bsp.klu.idx == klu.idx: - self.bsps.append(self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy)) - else: - self.bsps.append(0) - for index in range(0, len(self.bsps)): - if not (abs(self.bsps[index]) == 1 or abs(self.bsps[index]) == 2): - self.bsps[index] = 0 - else: - if self.bsps[index] == 2: - self.bsps[index] = 10 - else: - if self.bsps[index] == -2: - self.bsps[index] = -10 - else: - if self.bsps[index] == 1: - self.bsps[index] = 1 - else: - if self.bsps[index] == -1: - self.bsps[index] = -1 - else: - self.bsps[index] = 0 - return self.bsps - +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.analysis.ChanPY") +sys.modules[__name__] = _impl diff --git a/ChanPivotClassifier.py b/ChanPivotClassifier.py index 52b1af0..ca73fcd 100644 --- a/ChanPivotClassifier.py +++ b/ChanPivotClassifier.py @@ -1,292 +1,5 @@ -""" -中枢结构特征提取 + 标签化 -Market Structure Dataset Builder — Phase 1 - -定位: 训练数据集构建工具,不是交易信号生成器。 -Feature 描述中枢内部结构,Label 记录中枢后实际演化。 -""" - -import math -import json -from typing import Optional -from ChanEnum import Chan_BI_DIR - - -class ChanPivotClassifier: - """ - 中枢结构特征提取 + 标签化 - 输入: bi_zs_list (list[ChanBIZS]) - 输出: 结构化数据集 (list[dict]) - """ - - DATASET_VERSION = "pivot_v1" - FEATURE_SCHEMA = ["duration_norm", "contraction", "shift_norm"] - LABEL_SCHEMA = {"name": "break_direction", "values": ["up", "down", "none"]} - - def __init__(self, bi_zs_list: list, symbol: str = "", timeframe: str = ""): - self.bi_zs_list = bi_zs_list - self.symbol = symbol - self.timeframe = timeframe - - # ------------------------------------------------------------------ - # Feature extraction - # ------------------------------------------------------------------ - - @staticmethod - def calc_duration(zs) -> int: - """持续时间: 第一笔首K → 最后一笔末K 的 index 差""" - bi_list = zs.bi_list - start_idx = bi_list[0].start_klc.index - end_idx = bi_list[-1].end_klc.index - return end_idx - start_idx - - @staticmethod - def calc_contraction(zs) -> float: - """收敛率: 后窗口振幅均值 / 前窗口振幅均值""" - bi_list = zs.bi_list - if len(bi_list) < 4: - return 1.0 - - n = min(3, len(bi_list) // 2) - first_ranges = [bi.high - bi.low for bi in bi_list[:n]] - last_ranges = [bi.high - bi.low for bi in bi_list[-n:]] - - first_mean = sum(first_ranges) / len(first_ranges) - last_mean = sum(last_ranges) / len(last_ranges) - - if first_mean == 0: - return 1.0 - return last_mean / first_mean - - @staticmethod - def calc_shift(zs) -> tuple[float, float]: - """重心漂移: 前后半段重心均值差 (原始值, 归一化值)""" - bi_list = zs.bi_list - mid = len(bi_list) // 2 - - first_centers = [(bi.high + bi.low) / 2 for bi in bi_list[:mid]] - last_centers = [(bi.high + bi.low) / 2 for bi in bi_list[mid:]] - - shift_raw = ( - sum(last_centers) / len(last_centers) - - sum(first_centers) / len(first_centers) - ) - - zs_height = zs.zg - zs.zd - if zs_height == 0: - shift_norm = 0.0 - else: - shift_norm = shift_raw / zs_height - - return shift_raw, shift_norm - - @staticmethod - def compute_duration_norm(duration_raw: int, historical_durations: list) -> float: - """用历史窗口均值归一化 duration""" - if not historical_durations: - return 1.0 - avg = sum(historical_durations) / len(historical_durations) - if avg == 0: - return 1.0 - return duration_raw / avg - - @staticmethod - def compute_features(zs, historical_durations: Optional[list] = None): - """计算单个中枢的全部结构特征(实时友好)""" - duration_raw = ChanPivotClassifier.calc_duration(zs) - contraction = ChanPivotClassifier.calc_contraction(zs) - shift_raw, shift_norm = ChanPivotClassifier.calc_shift(zs) - - if historical_durations is not None and len(historical_durations) > 0: - duration_norm = ChanPivotClassifier.compute_duration_norm( - duration_raw, historical_durations - ) - else: - duration_norm = 1.0 - - return { - "duration_raw": duration_raw, - "duration_norm": round(duration_norm, 4), - "contraction": round(contraction, 4), - "shift_raw": round(shift_raw, 6), - "shift_norm": round(shift_norm, 4), - "zs_height": round(zs.zg - zs.zd, 6), - } - - # ------------------------------------------------------------------ - # Label computation - # ------------------------------------------------------------------ - - @staticmethod - def _clamp(x: float, lo: float = 0.0, hi: float = 1.0) -> float: - return max(lo, min(hi, x)) - - def _compute_label(self, zs, contraction: float, shift_norm: float) -> dict: - """计算标签: up / down / none + 连续置信度""" - bi_out = zs.bi_out - - if bi_out is None: - return { - "label": "none", - "label_confidence": 0.0, - "label_detail": { - "bi_out_dir": "none", - "score_breakout": 0.0, - "score_shift": 0.0, - "score_contraction": 0.0, - }, - } - - zs_height = zs.zg - zs.zd - if zs_height == 0: - zs_height = 1e-8 - - # ---- 向上突破分数 ---- - if bi_out.dir == Chan_BI_DIR.UP: - raw_breakout = (bi_out.high - zs.gg) / zs_height - score_breakout_up = self._clamp(raw_breakout) - score_shift_up = math.tanh(self._clamp(shift_norm, -3.0, 3.0)) - score_contraction_up = max(0.0, 1.0 - contraction) - else: - score_breakout_up = 0.0 - score_shift_up = 0.0 - score_contraction_up = 0.0 - - up_score = ( - score_breakout_up * 0.5 - + score_shift_up * 0.3 - + score_contraction_up * 0.2 - ) - - # ---- 向下突破分数 ---- - if bi_out.dir == Chan_BI_DIR.DOWN: - raw_breakout = (zs.dd - bi_out.low) / zs_height - score_breakout_down = self._clamp(raw_breakout) - score_shift_down = math.tanh(self._clamp(-shift_norm, -3.0, 3.0)) - score_contraction_down = max(0.0, 1.0 - contraction) - else: - score_breakout_down = 0.0 - score_shift_down = 0.0 - score_contraction_down = 0.0 - - down_score = ( - score_breakout_down * 0.5 - + score_shift_down * 0.3 - + score_contraction_down * 0.2 - ) - - # ---- 判定 ---- - threshold = 0.15 - - if up_score > down_score and up_score > threshold: - label = "up" - confidence = up_score - detail = { - "bi_out_dir": "up", - "score_breakout": round(score_breakout_up, 4), - "score_shift": round(score_shift_up, 4), - "score_contraction": round(score_contraction_up, 4), - } - elif down_score > up_score and down_score > threshold: - label = "down" - confidence = down_score - detail = { - "bi_out_dir": "down", - "score_breakout": round(score_breakout_down, 4), - "score_shift": round(score_shift_down, 4), - "score_contraction": round(score_contraction_down, 4), - } - else: - label = "none" - confidence = max(up_score, down_score) - bi_dir = "up" if bi_out.dir == Chan_BI_DIR.UP else "down" - detail = { - "bi_out_dir": bi_dir, - "score_breakout": round(max(score_breakout_up, score_breakout_down), 4), - "score_shift": round(max(score_shift_up, score_shift_down), 4), - "score_contraction": round(max(score_contraction_up, score_contraction_down), 4), - } - - return { - "label": label, - "label_confidence": round(confidence, 4), - "label_detail": detail, - } - - # ------------------------------------------------------------------ - # Public API - # ------------------------------------------------------------------ - - def extract(self) -> list[dict]: - """主入口:对每个中枢提取 3 特征 + 1 标签""" - - # 第一遍:计算原始值 - raw = [] - for i, zs in enumerate(self.bi_zs_list): - if not zs.is_sure or len(zs.bi_list) < 3: - continue - - duration_raw = ChanPivotClassifier.calc_duration(zs) - contraction = ChanPivotClassifier.calc_contraction(zs) - shift_raw, shift_norm = ChanPivotClassifier.calc_shift(zs) - - raw.append({ - "zs": zs, - "zs_index": i, - "duration_raw": duration_raw, - "contraction": contraction, - "shift_raw": shift_raw, - "shift_norm": shift_norm, - "zs_height": zs.zg - zs.zd, - }) - - # 第二遍:组装输出 + 计算 label - result = [] - for r in raw: - zs = r["zs"] - historical = [x["duration_raw"] for x in raw] - duration_norm = ChanPivotClassifier.compute_duration_norm( - r["duration_raw"], historical - ) - label_info = self._compute_label(zs, r["contraction"], r["shift_norm"]) - - # 时间处理 - start_time = None - end_time = None - if hasattr(zs, "start_time") and zs.start_time is not None: - start_time = str(zs.start_time) - if hasattr(zs, "end_time") and zs.end_time is not None: - end_time = str(zs.end_time) - - result.append({ - "dataset_version": self.DATASET_VERSION, - "feature_schema": self.FEATURE_SCHEMA, - "label_schema": self.LABEL_SCHEMA, - - "symbol": self.symbol, - "timeframe": self.timeframe, - "zs_index": r["zs_index"], - "zs_start_time": start_time, - "zs_end_time": end_time, - - "duration_norm": round(duration_norm, 4), - "contraction": round(r["contraction"], 4), - "shift_norm": round(r["shift_norm"], 4), - - "label": label_info["label"], - "label_confidence": label_info["label_confidence"], - "label_detail": label_info["label_detail"], - - "duration_raw": r["duration_raw"], - "shift_raw": round(r["shift_raw"], 6), - "zs_height": round(r["zs_height"], 6), - }) - - return result - - def export_json(self, path: str): - """导出为 JSON 文件""" - data = self.extract() - with open(path, "w", encoding="utf-8") as f: - json.dump(data, f, indent=2, ensure_ascii=False, default=str) - return len(data) +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.analysis.ChanPivotClassifier") +sys.modules[__name__] = _impl diff --git a/ChanPivotMonitor.py b/ChanPivotMonitor.py index 778ae2a..5dc93c6 100644 --- a/ChanPivotMonitor.py +++ b/ChanPivotMonitor.py @@ -1,145 +1,5 @@ -""" -实时中枢特征跟踪器 -Real-time Pivot Feature Tracker - -定位: 观察者 — 不修改管线,只观察 bi_zs_list 中当前中枢的特征变化。 -每次管线重算后调用 update(),检测 bi_count 是否增长,若增长则重新计算 -shift / contraction / duration。 -""" - -from collections import deque -from typing import Optional -from ChanPivotClassifier import ChanPivotClassifier - - -class ChanPivotMonitor: - """ - 实时追踪当前中枢的结构特征。 - - update() 每次管线重算后调用,对比 bi_count 判断是否有新笔加入中枢。 - 若 bi_count 增长则重新计算 3 个结构特征并返回最新值。 - """ - - def __init__(self, window_size: int = 10): - self._window_size = window_size - self._duration_history: deque[int] = deque(maxlen=window_size) - self._current_zs_id: Optional[tuple] = None - self._current_bi_count: int = 0 - self._current_is_sure: bool = False - self._current_state: Optional[dict] = None - self._duration_added_for_zs: set = set() # 已加入窗口的中枢 ID(上限 200) - - # ------------------------------------------------------------------ - # Public API - # ------------------------------------------------------------------ - - def update(self, bi_zs_list: list) -> Optional[dict]: - """ - 主入口:检测当前中枢特征变化。 - - 参数: - bi_zs_list: 当前管线产出的笔中枢列表 - - 返回: - 特征 dict(有变化时),无变化返回 None - """ - if not bi_zs_list: - self._current_zs_id = None - self._current_bi_count = 0 - self._current_is_sure = False - self._current_state = None - return None - - zs = self._find_current_zs(bi_zs_list) - if zs is None: - return None - - zs_id = self._make_zs_id(zs) - bi_count = len(zs.bi_list) - is_sure = zs.is_sure - - # 无变化 → 跳过 - if (zs_id == self._current_zs_id - and bi_count == self._current_bi_count - and is_sure == self._current_is_sure): - return None - - # 中枢切换 → 将旧中枢 duration 加入窗口 - if zs_id != self._current_zs_id: - self._maybe_add_to_history() - - self._current_zs_id = zs_id - self._current_bi_count = bi_count - self._current_is_sure = is_sure - - features = ChanPivotClassifier.compute_features( - zs, list(self._duration_history) - ) - - self._current_state = { - "zs_id": zs_id, - "zs_index": zs.index, - "zs_dir": str(zs.dir), - "bi_count": bi_count, - "is_sure": zs.is_sure, - "zg": round(zs.zg, 6), - "zd": round(zs.zd, 6), - "gg": round(zs.gg, 6), - "dd": round(zs.dd, 6), - **features, - "start_time": str(t) if (t := getattr(zs, "start_time", None)) else None, - } - - # 中枢刚变为已确认时,将其 duration 加入滚动窗口 - if is_sure and zs_id not in self._duration_added_for_zs: - self._add_duration(features["duration_raw"]) - self._duration_added_for_zs.add(zs_id) - - return self._current_state - - def get_current(self) -> Optional[dict]: - """返回当前中枢的最新特征""" - return self._current_state - - def get_duration_history(self) -> list[int]: - """返回用于归一化的 duration 滚动窗口""" - return list(self._duration_history) - - # ------------------------------------------------------------------ - # Internal - # ------------------------------------------------------------------ - - @staticmethod - def _make_zs_id(zs) -> tuple: - """生成中枢的稳定标识(基于首笔首K线时间戳,不随 DataFrame 窗口偏移而变化)""" - bi0 = zs.bi_list[0] - return (bi0.start_klc.start_time,) - - @staticmethod - def _find_current_zs(bi_zs_list: list): - """ - 找到当前活跃中枢: - 优先取最后一个 is_sure=False(形成中)的中枢, - 没有则取最后一个 is_sure=True 的中枢。 - """ - forming = None - last_sure = None - for zs in bi_zs_list: - if len(zs.bi_list) < 3: - continue - if not zs.is_sure: - forming = zs - else: - last_sure = zs - return forming if forming is not None else last_sure - - def _add_duration(self, duration_raw: int): - """将已确认中枢的 duration 加入滚动窗口""" - self._duration_history.append(duration_raw) - - def _maybe_add_to_history(self): - """旧中枢切换前,若已确认且未记录过,则将其 duration 加入窗口""" - if (self._current_state and self._current_state["is_sure"] - and self._current_zs_id not in self._duration_added_for_zs): - self._add_duration(self._current_state["duration_raw"]) - self._duration_added_for_zs.add(self._current_zs_id) +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.analysis.ChanPivotMonitor") +sys.modules[__name__] = _impl diff --git a/ChanSBI.py b/ChanSBI.py index 855b621..474a594 100644 --- a/ChanSBI.py +++ b/ChanSBI.py @@ -1,91 +1,5 @@ -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_seg_bi_broken(self): - broken = False - if self.fx == Chan_FX_TYPE.TOP: - if self.next.low < self.pre.high: - broken = True - elif self.fx == Chan_FX_TYPE.BOTTOM: - if self.next.high > self.pre.low: - broken = True - return broken - def check_bi_included(self, bi): - included = False - if self.high > bi.high: - # high大于,low小于,左包含 - if self.low < bi.low: - included = True - # high大于,low大于,不包含 - else: - # if self.low > bi.low - # high相等,右包含 - included = False - else: - included = False - # high小于,low大于,右包含 - #if self.low > bi.low: - #included = True - if included: - if self.pre: - if self.high > self.pre.high and self.low < self.pre.low: - included = True - if included: - self.add_bi(bi) - # gn>gn-1 - if self.dir == Chan_BI_DIR.DOWN: - # UP -> max(dn) - self.low = bi.low - else: - # DOWN -> min(gn) - self.high = bi.high - #self.print(bi, "Z") - #print(self.start_bi.start_time, bi.start_time, included) - return included \ No newline at end of file +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.core.ChanSBI") +sys.modules[__name__] = _impl diff --git a/ChanSEG.py b/ChanSEG.py index 21f52cd..e337e5e 100644 --- a/ChanSEG.py +++ b/ChanSEG.py @@ -1,197 +1,5 @@ -import copy -from typing import Dict, Optional - -from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_SEG_DIR, Chan_BI_DIR, Chan_ZS_DIR -import ChanCTime -from ChanBI import ChanBI -from ChanBIZS import ChanBIZS -class ChanSEG(): - def __init__(self, start_bi: ChanBI, index, ddir=Chan_SEG_DIR.UP, pre_end_bi: ChanBI = None): - self.start_bi = start_bi - self.start_time = start_bi.start_time - self.end_time = None - self.end_bi = None - self.dir = ddir - self.low = 0 - self.high = 0 - if self.dir == Chan_SEG_DIR.UP and start_bi: - self.low = start_bi.low - else: - if start_bi: - self.high = start_bi.high - self.index = index - self.pre = None - self.next = None - self.bi_list = [] - self.bi_list.append(start_bi) - self.is_sure = False - self.sure_time = None - self.macd_hist = 0 - self.macd_div = 0 - self.start_bi.set_seg(self) - self.pre_end_bi = pre_end_bi - if self.pre_end_bi: - self.ini_seg() - def ini_seg(self): - next_bi = self.start_bi.next - for index in range(self.start_bi.index+1, self.pre_end_bi.index): - if next_bi: - self.bi_list.append(next_bi) - next_bi.set_seg(self) - next_bi = next_bi.next - def set_macdhist(self, macd_hist): - self.macd_hist = macd_hist - def set_macd_div(self, macd_div): - self.macd_div = macd_div - def set_end_bi(self, bi: ChanBI, sure_bi: ChanBI): - self.end_bi = bi - if bi and bi.is_sure: - if self.dir == Chan_SEG_DIR.UP: - self.high = bi.high - else: - self.low = bi.low - self.is_sure = True - self.end_time = bi.end_klc.end_time - if sure_bi.is_sure: - self.sure_time = sure_bi.sure_time - self.format_bi_list() - def pre_set_end_bi(self, bi: ChanBI): - self.end_bi = bi - if bi and bi.is_sure: - if self.dir == Chan_SEG_DIR.UP: - self.high = bi.high - else: - self.low = bi.low - self.end_time = bi.end_klc.end_time - self.format_bi_list() - def set_pre(self, seg): - self.pre = seg - def set_next(self, seg): - self.next = seg - def set_sure(self, sure_bi): - if sure_bi.is_sure: - self.sure_time = sure_bi.sure_time - self.is_sure = True - self.format_bi_list() - def format_bi_list(self): - self.bi_list = [] - self.bi_list.append(self.start_bi) - if self.end_bi: - next_bi = self.start_bi.next - for i in range(self.start_bi.index, self.end_bi.index): - if next_bi: - self.bi_list.append(next_bi) - next_bi.set_seg(self) - next_bi = next_bi.next - def add_bi(self, bi: ChanBI): - if len(self.bi_list) > 0: - self.bi_list.append(bi) - bi.set_seg(self) - self.end_time = bi.end_time - self.end_bi = bi - def cal_bi_zs(self): - zs_list = [] - if len(self.bi_list) > 3: - last_zs = None - if self.dir == Chan_SEG_DIR.UP: - for index in range(1, len(self.bi_list)): - bi = self.bi_list[index] - #print(bi.end_time, bi.next,"UP SEG BI ZS Index") - if bi.next == None or bi.next.next == None: - if last_zs and (bi.low > last_zs.zg or bi.high < last_zs.zd): - last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time) - continue - bi2 = bi.next - bi3 = bi.next.next - if len(zs_list) == 0 or (last_zs and last_zs.is_sure): - if bi3.is_sure and bi3.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.DOWN: - zg = min(bi.high, bi2.high, bi3.high) - zd = max(bi.low, bi2.low, bi3.low) - gg = max(bi.high, bi2.high, bi3.high) - dd = min(bi.low, bi2.low, bi3.low) - zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.UP) - zs.set_zg(zg) - zs.set_zd(zd) - zs.set_gg(gg) - zs.set_dd(dd) - zs.add_bi(bi2) - zs.add_bi(bi3) - zs_list.append(zs) - last_zs = zs - else: - if bi.index > last_zs.bi_list[-1].index and bi.dir == Chan_BI_DIR.DOWN and bi.is_sure: - if bi.low > last_zs.zg or bi.high < last_zs.zd: - #print(bi.end_time, "UP SEG BI ZS End") - last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time) - if bi3.is_sure and bi3.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.DOWN: - zg = min(bi.high, bi2.high, bi3.high) - zd = max(bi.low, bi2.low, bi3.low) - gg = max(bi.high, bi2.high, bi3.high) - dd = min(bi.low, bi2.low, bi3.low) - zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.UP) - zs.set_zg(zg) - zs.set_zd(zd) - zs.set_gg(gg) - zs.set_dd(dd) - zs.add_bi(bi2) - zs.add_bi(bi3) - zs_list.append(zs) - last_zs = zs - else: - last_zs.add_bi(bi.pre) - last_zs.add_bi(bi) - if index == len(self.bi_list) - 1 and last_zs and not last_zs.is_sure: - #print(bi.start_time, "BI", last_zs.is_sure) - last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time) - else: - for index in range(1, len(self.bi_list)): - bi = self.bi_list[index] - if bi.next == None or bi.next.next == None: - if last_zs and (bi.low > last_zs.zg or bi.high < last_zs.zd): - last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time) - continue - bi2 = bi.next - bi3 = bi.next.next - if len(zs_list) == 0 or (last_zs and last_zs.is_sure): - if bi3.is_sure and bi3.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.UP: - zg = min(bi.high, bi2.high, bi3.high) - zd = max(bi.low, bi2.low, bi3.low) - gg = max(bi.high, bi2.high, bi3.high) - dd = min(bi.low, bi2.low, bi3.low) - zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.DOWN) - zs.set_zg(zg) - zs.set_zd(zd) - zs.set_gg(gg) - zs.set_dd(dd) - zs.add_bi(bi2) - zs.add_bi(bi3) - zs_list.append(zs) - last_zs = zs - else: - if bi.index > last_zs.bi_list[-1].index and bi.dir == Chan_BI_DIR.UP and bi.is_sure: - if bi.low > last_zs.zg or bi.high < last_zs.zd: - last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time) - if bi3.is_sure and bi3.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.UP: - zg = min(bi.high, bi2.high, bi3.high) - zd = max(bi.low, bi2.low, bi3.low) - gg = max(bi.high, bi2.high, bi3.high) - dd = min(bi.low, bi2.low, bi3.low) - zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.DOWN) - zs.set_zg(zg) - zs.set_zd(zd) - zs.set_gg(gg) - zs.set_dd(dd) - zs.add_bi(bi2) - zs.add_bi(bi3) - zs_list.append(zs) - last_zs = zs - else: - last_zs.add_bi(bi.pre) - last_zs.add_bi(bi) - if index == len(self.bi_list) - 1 and last_zs and not last_zs.is_sure: - #print(bi.start_time, "BI", last_zs.is_sure) - last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time) - - #print(self.start_time, len(zs_list)) - #print(self.bi_list[-1].end_time, "end_bi") - return zs_list \ No newline at end of file +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.core.ChanSEG") +sys.modules[__name__] = _impl diff --git a/ChanZS.py b/ChanZS.py index 4a54bed..0a29008 100644 --- a/ChanZS.py +++ b/ChanZS.py @@ -1,108 +1,5 @@ -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.gg = 0 - self.dd = 0 - self.dir = ddir - self.sure_time = None - self.end_klc = None - self.bi_out_count = 0 - self.bi_out_list = [] - self.bi_out_seg_list = [] - self.bi_out_seg = None - self.is_extended = False - def set_last_bi_in(self, last_bi_in): - self.last_bi_in = last_bi_in - def set_bi_out(self, bi_out, bi_out_seg): - if bi_out: - #print(bi_out.start_klc.start_time, bi_out.sure_time, bi_out.dir, bi_out_seg.dir, len(self.bi_out_list)) - if len(self.bi_out_list) > 0: - last_bi = self.bi_out_list[-1] - if last_bi.index != bi_out.index: - self.bi_out_list.append(bi_out) - self.bi_out_seg_list.append(bi_out_seg) - else: - self.bi_out_list.append(bi_out) - self.bi_out_seg_list.append(bi_out_seg) - self.bi_out = bi_out - self.bi_out_seg = bi_out_seg - def set_end_klc(self, end_klc, sure_time, bi_out_count, seg): - self.end_klc = end_klc - self.set_end_time(end_klc.end_time) - self.is_sure = True - self.sure_time = sure_time - self.bi_out_count = bi_out_count - self.end_seg = seg - def set_end_seg(self, end_seg): - self.end_seg = end_seg - def set_pre(self, pre): - self.pre = pre - def set_next(self, next): - self.next = next - def set_end_time(self, end_time): - self.end_time = end_time - def add_klc(self, klc): - self.klc_list.append(klc) - def add_seg(self, seg): - self.seg_list.append(seg) - self.end_time = seg.end_time - self.end_seg = seg - def set_zg(self, zg): - self.zg = zg - def set_zd(self, zd): - self.zd = zd - def set_gg(self, gg): - self.gg = gg - def set_dd(self, dd): - self.dd = dd - def extend_zs(self, seg_list): - self.is_sure = False - self.end_seg = None - self.end_klc = None - self.sure_time = None - for seg in seg_list: - if seg.end_bi.high > self.gg: - self.set_gg(seg.end_bi.high) - if seg.end_bi.low < self.dd: - self.set_dd(seg.end_bi.low) - self.seg_list.append(seg) - self.is_extended = True - #print(self.start_time, "extend zs", seg_list[-1].end_time) - -# 大级别中枢:由多个区间重叠(扩张)的笔/线段中枢合并而成,用于显示更大级别的震荡区间 -class ChanZS_Big(): - def __init__(self, zs_list): - assert len(zs_list) >= 1 - self.zs_list = list(zs_list) - first = self.zs_list[0] - last = self.zs_list[-1] - self.start_time = first.start_time - self.end_time = last.end_time if last.end_time else None - self.start_klc = first.start_klc - self.end_klc = last.end_klc - # 大级别区间取并集:包住所有子中枢 - self.zd = min(zs.zd for zs in self.zs_list) - self.zg = max(zs.zg for zs in self.zs_list) - self.dd = min(zs.dd for zs in self.zs_list) - self.gg = max(zs.gg for zs in self.zs_list) - self.index = 0 # 由外部设置 \ No newline at end of file +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.core.ChanZS") +sys.modules[__name__] = _impl diff --git a/ChanZone.py b/ChanZone.py index 2245d76..95a4365 100644 --- a/ChanZone.py +++ b/ChanZone.py @@ -1,566 +1,5 @@ -""" -结构价值区 (Structure Zone) 系统 - -将多时间周期的 Chan 中枢边界 (ZD/ZG/GG/DD) 和 EMA52 统一表示为带强度评分的价值区对象。 -""" - -from dataclasses import dataclass, field -from typing import List, Dict, Optional, Any -from datetime import datetime - - -# ============================================================ -# Dataclasses -# ============================================================ - -@dataclass -class RawZonePoint: - """内部中间结构:从 Chan 中枢提取的单个价格点""" - price: float - timeframe: str # '5m', '1h', '4h' 等 - structure_type: str # 'bi_zhongshu' | 'xd_zhongshu' | 'ema52' - boundary_type: str # 'ZD' | 'ZG' | 'GG' | 'DD' | 'EMA52' - source_zs_id: int # 来源 ZS 在列表中的 index(调试用) - is_sure: bool # 来源 ZS 是否已完成 - candle_time: Optional[str] = None # 来源 ZS 的 end_time(用于 recency 计算) - - -@dataclass -class StructureZone: - """统一的价值区对象""" - id: int - lower: float - upper: float - center: float # (lower + upper) / 2 - width_pct: float # (upper - lower) / center * 100 - zone_type: str # 'support' | 'resistance' | 'neutral' - timeframes: List[str] # 参与形成此区间的时间周期 - structure_types: List[str] # 参与形成的结构类型 - boundary_types: List[str] # 参与形成的边界类型 - overlap_count: int # 聚类中的原始点数 - touch_count: int # MVP: 等于 overlap_count - recency_score: float # 0.0 - 1.0, 1.0 = 最近 - ema52_distance_pct: float # 到最近 EMA52 的距离百分比 - ema52_aligned: bool # 是否有 EMA52 落在区间内 - strength_score: float # 0-100 综合评分 - confidence: float # 0.0 - 1.0 - first_seen: Optional[str] # 最早的 candle_time - last_seen: Optional[str] # 最晚的 candle_time - metadata: Dict[str, Any] = field(default_factory=dict) - - -@dataclass -class StructureZoneConfig: - """StructureZone 提取与评分配置""" - cluster_radius_pct: float = 0.5 # 价格聚类半径(百分比) - min_overlap_for_zone: int = 2 # 最少重叠点数才能形成区间 - max_zones: int = 20 # 返回的最大区间数 - recency_halflife_bars: int = 50 # recency 衰减半衰期(K线数) - zone_timeframes: List[str] = field(default_factory=lambda: ['4h', '1h', '30m', '15m', '5m']) - kl_lines_per_tf: int = 500 # 每个时间周期使用最近多少根K线 - structure_weights: Dict[str, float] = field(default_factory=lambda: { - 'bi_zhongshu': 1.0, # 笔中枢 — 最直接的价格行为 - 'xd_zhongshu': 0.8, # 线段中枢 — 较高级别但粒度较粗 - 'ema52': 0.4, # EMA — 趋势参考,弱于结构 - }) - - -# ============================================================ -# Extraction -# ============================================================ - -def extract_raw_points_from_tf_df( - tf_df_dict: Dict[str, Any], - ema_symbols: List[str], - config: StructureZoneConfig, -) -> List[RawZonePoint]: - """ - 从 ChanLun.tf_df_dict 中提取所有原始价格点。 - 仅处理 config.zone_timeframes 中存在的时间周期。 - """ - points: List[RawZonePoint] = [] - - for tf_name in config.zone_timeframes: - if tf_name not in tf_df_dict: - continue - - tf_df = tf_df_dict[tf_name] - - # 1. 笔中枢 (ChanBIZS) - try: - if hasattr(tf_df, 'seg_list') and tf_df.seg_list: - bi_zs_result = tf_df.cal_bi_zs(tf_df.seg_list) - if bi_zs_result: - _extract_from_zs_objects( - points, tf_name, 'bi_zhongshu', bi_zs_result, config.kl_lines_per_tf - ) - except Exception: - pass - - # 2. 线段中枢 (ChanZS) - try: - zs_list = getattr(tf_df, 'zs_list', None) - if zs_list: - _extract_from_zs_objects( - points, tf_name, 'xd_zhongshu', zs_list, config.kl_lines_per_tf - ) - except Exception: - pass - - # 3. EMA52 值 - for tf_name in config.zone_timeframes: - if tf_name in tf_df_dict: - try: - ema_val = tf_df_dict[tf_name].get_ema52() - if ema_val is not None and ema_val > 0: - points.append(RawZonePoint( - price=float(ema_val), - timeframe=tf_name, - structure_type='ema52', - boundary_type='EMA52', - source_zs_id=-1, - is_sure=True, - candle_time=None, - )) - except Exception: - pass - - return points - - -def _extract_from_zs_objects( - points: List[RawZonePoint], - tf_name: str, - structure_type: str, - zs_list, - kl_limit: int, -): - """从 ZS 链表中提取 ZD/ZG/GG/DD 点""" - count = 0 - node = zs_list - while hasattr(node, 'next'): - node = node.next - # 从链表头开始遍历 - head = zs_list - # 收集所有节点 - all_nodes = [] - cur = head - while cur is not None and hasattr(cur, 'next'): - all_nodes.append(cur) - cur = cur.next - # 只取最近 kl_limit 根K线内的 ZS - all_nodes = all_nodes[-kl_limit:] if len(all_nodes) > kl_limit else all_nodes - - for idx, zs in enumerate(all_nodes): - if not getattr(zs, 'is_sure', False): - continue - try: - zg = float(zs.zg) - zd = float(zs.zd) - gg = float(zs.gg) if getattr(zs, 'gg', 0) else zg - dd = float(zs.dd) if getattr(zs, 'dd', 0) else zd - end_time = str(zs.end_time) if hasattr(zs, 'end_time') and zs.end_time else None - except (ValueError, TypeError, AttributeError): - continue - - if zg <= 0 or zd <= 0: - continue - - zs_id = getattr(zs, 'index', idx) - points.append(RawZonePoint(price=zg, timeframe=tf_name, structure_type=structure_type, - boundary_type='ZG', source_zs_id=zs_id, is_sure=True, - candle_time=end_time)) - points.append(RawZonePoint(price=zd, timeframe=tf_name, structure_type=structure_type, - boundary_type='ZD', source_zs_id=zs_id, is_sure=True, - candle_time=end_time)) - points.append(RawZonePoint(price=gg, timeframe=tf_name, structure_type=structure_type, - boundary_type='GG', source_zs_id=zs_id, is_sure=True, - candle_time=end_time)) - points.append(RawZonePoint(price=dd, timeframe=tf_name, structure_type=structure_type, - boundary_type='DD', source_zs_id=zs_id, is_sure=True, - candle_time=end_time)) - - -def extract_raw_points_from_serialized( - analyses: Dict[str, Dict], - ema52_dict: Dict[str, Optional[float]], - config: StructureZoneConfig, -) -> List[RawZonePoint]: - """ - 从已序列化的分析结果中提取价格点(用于 web API,避免重复计算)。 - analyses: {'5m': {'zs_list': [...], 'bi_zs_list': [...]}, '15m': {...}, ...} - ema52_dict: {'5m': 123.45, '15m': None, ...} - """ - points: List[RawZonePoint] = [] - - for tf_name in config.zone_timeframes: - if tf_name not in analyses: - continue - - analysis = analyses[tf_name] - - # 笔中枢 - bi_zs_items = analysis.get('bi_zs_list', []) - for idx, zs in enumerate(bi_zs_items): - if not zs.get('is_sure', False): - continue - try: - zg = float(zs['zg']); zd = float(zs['zd']) - gg = float(zs.get('gg', zg)); dd = float(zs.get('dd', zd)) - end_time = zs.get('end_time') - except (ValueError, KeyError): - continue - if zg <= 0 or zd <= 0: - continue - points.append(RawZonePoint(price=zg, timeframe=tf_name, structure_type='bi_zhongshu', - boundary_type='ZG', source_zs_id=idx, is_sure=True, - candle_time=str(end_time) if end_time else None)) - points.append(RawZonePoint(price=zd, timeframe=tf_name, structure_type='bi_zhongshu', - boundary_type='ZD', source_zs_id=idx, is_sure=True, - candle_time=str(end_time) if end_time else None)) - points.append(RawZonePoint(price=gg, timeframe=tf_name, structure_type='bi_zhongshu', - boundary_type='GG', source_zs_id=idx, is_sure=True, - candle_time=str(end_time) if end_time else None)) - points.append(RawZonePoint(price=dd, timeframe=tf_name, structure_type='bi_zhongshu', - boundary_type='DD', source_zs_id=idx, is_sure=True, - candle_time=str(end_time) if end_time else None)) - - # 线段中枢 - zs_items = analysis.get('zs_list', []) - for idx, zs in enumerate(zs_items): - if not zs.get('is_sure', False): - continue - try: - zg = float(zs['zg']); zd = float(zs['zd']) - gg = float(zs.get('gg', zg)); dd = float(zs.get('dd', zd)) - end_time = zs.get('end_time') - except (ValueError, KeyError): - continue - if zg <= 0 or zd <= 0: - continue - points.append(RawZonePoint(price=zg, timeframe=tf_name, structure_type='xd_zhongshu', - boundary_type='ZG', source_zs_id=idx, is_sure=True, - candle_time=str(end_time) if end_time else None)) - points.append(RawZonePoint(price=zd, timeframe=tf_name, structure_type='xd_zhongshu', - boundary_type='ZD', source_zs_id=idx, is_sure=True, - candle_time=str(end_time) if end_time else None)) - points.append(RawZonePoint(price=gg, timeframe=tf_name, structure_type='xd_zhongshu', - boundary_type='GG', source_zs_id=idx, is_sure=True, - candle_time=str(end_time) if end_time else None)) - points.append(RawZonePoint(price=dd, timeframe=tf_name, structure_type='xd_zhongshu', - boundary_type='DD', source_zs_id=idx, is_sure=True, - candle_time=str(end_time) if end_time else None)) - - # EMA52 - for tf_name in config.zone_timeframes: - ema_val = ema52_dict.get(tf_name) - if ema_val is not None and ema_val > 0: - points.append(RawZonePoint( - price=float(ema_val), - timeframe=tf_name, - structure_type='ema52', - boundary_type='EMA52', - source_zs_id=-1, - is_sure=True, - candle_time=None, - )) - - return points - - -# ============================================================ -# Clustering -# ============================================================ - -def cluster_raw_points( - points: List[RawZonePoint], - config: StructureZoneConfig, -) -> List[List[RawZonePoint]]: - """ - 贪心单通聚类:将价格相近的 RawZonePoint 归为一组。 - 仅在 1D 价格轴上操作,O(n log n)。 - """ - if not points: - return [] - - sorted_points = sorted(points, key=lambda p: p.price) - clusters: List[List[RawZonePoint]] = [] - - for p in sorted_points: - placed = False - for cluster in reversed(clusters): - # 检查是否可以放入当前聚类(与聚类均价比较) - avg_price = sum(pt.price for pt in cluster) / len(cluster) - if abs(p.price - avg_price) / avg_price * 100 <= config.cluster_radius_pct: - cluster.append(p) - placed = True - break - if not placed: - clusters.append([p]) - - # 过滤点数不足的聚类 - return [c for c in clusters if len(c) >= config.min_overlap_for_zone] - - -# ============================================================ -# Scoring & Building -# ============================================================ - -def build_structure_zones( - clusters: List[List[RawZonePoint]], - current_price: float, - ema52_values: Dict[str, Optional[float]], - latest_candle_time: Optional[str], - config: StructureZoneConfig, -) -> List[StructureZone]: - """ - 从聚类构建 StructureZone 列表,计算所有字段和评分。 - """ - zones: List[StructureZone] = [] - - # 收集所有 EMA52 值 - ema_prices = [v for v in ema52_values.values() if v is not None and v > 0] - - for zone_id, cluster in enumerate(clusters): - prices = [p.price for p in cluster] - lower = min(prices) - upper = max(prices) - center = (lower + upper) / 2 - width_pct = (upper - lower) / center * 100 if center > 0 else 0.0 - - # 区间类型 - if upper < current_price: - zone_type = 'support' # 区间在当前价格下方 → 支撑 - elif lower > current_price: - zone_type = 'resistance' # 区间在当前价格上方 → 阻力 - else: - zone_type = 'neutral' # 区间跨越当前价格 - - timeframes = sorted(set(p.timeframe for p in cluster)) - structure_types = sorted(set(p.structure_type for p in cluster)) - boundary_types = sorted(set(p.boundary_type for p in cluster)) - overlap_count = len(cluster) - - # Recency - times = [p.candle_time for p in cluster if p.candle_time] - first_seen = min(times) if times else None - last_seen = max(times) if times else None - recency_score = _calc_recency(last_seen, latest_candle_time, config.recency_halflife_bars) - - # EMA52 alignment - ema52_distance_pct = 999.0 - ema52_aligned = False - if ema_prices: - distances = [abs(center - ep) / ep * 100 for ep in ema_prices] - ema52_distance_pct = round(min(distances), 2) - ema52_aligned = any(lower <= ep <= upper for ep in ema_prices) - - # Strength score - strength_score = _calc_strength(cluster, config, recency_score, ema52_aligned, ema52_distance_pct, width_pct) - - # Confidence - confidence = _calc_confidence(overlap_count, len(timeframes), cluster) - - zones.append(StructureZone( - id=zone_id + 1, - lower=round(lower, 2), - upper=round(upper, 2), - center=round(center, 2), - width_pct=round(width_pct, 2), - zone_type=zone_type, - timeframes=timeframes, - structure_types=structure_types, - boundary_types=boundary_types, - overlap_count=overlap_count, - touch_count=overlap_count, # MVP: 等于 overlap_count - recency_score=round(recency_score, 3), - ema52_distance_pct=ema52_distance_pct, - ema52_aligned=ema52_aligned, - strength_score=round(strength_score, 1), - confidence=round(confidence, 2), - first_seen=first_seen, - last_seen=last_seen, - )) - - # 按强度降序排列 - zones.sort(key=lambda z: z.strength_score, reverse=True) - - # 截断 - if config.max_zones > 0 and len(zones) > config.max_zones: - zones = zones[:config.max_zones] - - return zones - - -def _calc_recency( - last_seen: Optional[str], - latest_time: Optional[str], - halflife_bars: int, -) -> float: - """计算 recency 分数:越近越高""" - if not last_seen or not latest_time: - return 0.5 - - try: - # 尝试解析 ISO 格式时间 - from dateutil import parser - t_last = parser.parse(last_seen) - t_latest = parser.parse(latest_time) - offset_seconds = (t_latest - t_last).total_seconds() - if offset_seconds < 0: - return 1.0 - # 假设每根K线平均 5 分钟 - bar_seconds = 300 - offset_bars = offset_seconds / bar_seconds - # 指数衰减: 2 ^ (-offset / halflife) - score = 2.0 ** (-offset_bars / halflife_bars) - return float(score) - except Exception: - return 0.5 - - -def _calc_strength( - cluster: List[RawZonePoint], - config: StructureZoneConfig, - recency_score: float, - ema52_aligned: bool, - ema52_distance_pct: float, - width_pct: float, -) -> float: - """计算综合强度评分 (0-100)""" - - # 组件 1: 结构类型多样性 (0-40) - structure_type_counts: Dict[str, int] = {} - for p in cluster: - structure_type_counts[p.structure_type] = structure_type_counts.get(p.structure_type, 0) + 1 - total = sum(structure_type_counts.values()) - structure_score = 0.0 - for st, count in structure_type_counts.items(): - weight = config.structure_weights.get(st, 0.5) - structure_score += weight * count - structure_score = min(structure_score / max(1, total), 1.0) - c1 = structure_score * 40 - - # 组件 2: 多周期确认 (0-25) - tf_set = set(p.timeframe for p in cluster) - tf_diversity = len(tf_set) - c2 = min(tf_diversity / 5, 1.0) * 25 - - # 组件 3: 区间紧密度 (0-15) — 越窄越强 - tightness = max(0.0, 1.0 - (width_pct / 3.0)) - c3 = tightness * 15 - - # 组件 4: Recency (0-10) - c4 = recency_score * 10 - - # 组件 5: EMA52 共振 (0-10) - if ema52_aligned: - ema_proximity = max(0.0, 1.0 - (ema52_distance_pct / 2.0)) - c5 = ema_proximity * 10 - else: - c5 = 0.0 - - return c1 + c2 + c3 + c4 + c5 - - -def _calc_confidence( - overlap_count: int, - tf_count: int, - cluster: List[RawZonePoint], -) -> float: - """计算置信度 (0-1)""" - base = min(overlap_count / 6.0, 0.85) - # 多周期加分 - tf_bonus = min(tf_count / 5.0, 0.1) - # 是否所有点都来自 sure 的 ZS - all_sure = all(p.is_sure for p in cluster) - sure_bonus = 0.05 if all_sure else 0.0 - return min(base + tf_bonus + sure_bonus, 1.0) - - -# ============================================================ -# Top-level pipeline -# ============================================================ - -def analyze_structure_zones( - tf_df_dict: Dict[str, Any], - ema_symbols: List[str], - current_price: Optional[float] = None, - config: Optional[StructureZoneConfig] = None, -) -> List[StructureZone]: - """ - 一站式分析:提取 → 聚类 → 评分 → 返回排序后的 StructureZone 列表。 - """ - if config is None: - config = StructureZoneConfig() - - # 提取 - raw_points = extract_raw_points_from_tf_df(tf_df_dict, ema_symbols, config) - - if not raw_points: - return [] - - # 获取当前价格 - if current_price is None: - for tf_name in config.zone_timeframes: - if tf_name in tf_df_dict: - try: - ema_val = tf_df_dict[tf_name].get_ema52() - if ema_val and ema_val > 0: - current_price = float(ema_val) - break - except Exception: - pass - if current_price is None: - current_price = 0.0 - - # EMA52 值 - ema52_values = {} - for tf_name in config.zone_timeframes: - if tf_name in tf_df_dict: - try: - ema52_values[tf_name] = tf_df_dict[tf_name].get_ema52() - except Exception: - ema52_values[tf_name] = None - - # 最晚时间 - latest_time = None - times = [p.candle_time for p in raw_points if p.candle_time] - if times: - latest_time = max(times) - - # 聚类 - clusters = cluster_raw_points(raw_points, config) - - # 构建 & 评分 - return build_structure_zones(clusters, current_price, ema52_values, latest_time, config) - - -def analyze_structure_zones_from_serialized( - analyses: Dict[str, Dict], - ema52_dict: Dict[str, Optional[float]], - current_price: float, - config: Optional[StructureZoneConfig] = None, -) -> List[StructureZone]: - """ - 从已序列化的分析结果构建 StructureZone(用于 web API)。 - """ - if config is None: - config = StructureZoneConfig() - - raw_points = extract_raw_points_from_serialized(analyses, ema52_dict, config) - - if not raw_points: - return [] - - # 最晚时间 - latest_time = None - times = [p.candle_time for p in raw_points if p.candle_time] - if times: - latest_time = max(times) - - # EMA52 值(用于 alignment 检测) - ema_values = {tf: v for tf, v in ema52_dict.items() if v is not None and v > 0} - - clusters = cluster_raw_points(raw_points, config) - return build_structure_zones(clusters, current_price, ema_values, latest_time, config) +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.analysis.ChanZone") +sys.modules[__name__] = _impl diff --git a/Chan_FX_Box.py b/Chan_FX_Box.py index 8a9d9ec..637976e 100644 --- a/Chan_FX_Box.py +++ b/Chan_FX_Box.py @@ -1,7 +1,5 @@ -class Chan_FX_Box(): - def __init__(self, start_time, end_time, high, low): - self.start_time = start_time - self.end_time = end_time - self.high = high - self.low = low - \ No newline at end of file +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.core.Chan_FX_Box") +sys.modules[__name__] = _impl diff --git a/Find_Trend.py b/Find_Trend.py index 8d392a3..3e5dee6 100644 --- a/Find_Trend.py +++ b/Find_Trend.py @@ -1,448 +1,5 @@ -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 +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.analysis.Find_Trend") +sys.modules[__name__] = _impl diff --git a/TF_DF.py b/TF_DF.py index 955a0b5..775f611 100644 --- a/TF_DF.py +++ b/TF_DF.py @@ -1,2583 +1,5 @@ -from datetime import timedelta -from pandas import DataFrame -from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_PRICE_TREND, Chan_KLU_PATTERN, Chan_K_DIR, Chan_KLC_STATE -from ChanKLU import ChanKLU -from ChanKLC import ChanKLC -from ChanBI import ChanBI -from ChanSBI import ChanSBI -from ChanSEG import ChanSEG -from ChanZS import ChanZS, ChanZS_Big -from ChanBIZS import ChanBIZS -from ChanBSP import ChanBSP -import talib.abstract as ta -import pandas as pd -from technical.util import resample_to_interval -from decimal import Decimal -import numpy as np -from ChanMACD import ChanMACD - -class TF_DF(): - def __init__(self, df=None, interval=0, timeframe=None): - if df is not None: - self.init_TF_DF(df, interval, timeframe) - def init_TF_DF(self, df, interval, timeframe): - self.timeframe = timeframe - self.interval = interval - # 检查 DataFrame 是否为空或没有 date 列 - if df is None or df.empty: - raise ValueError(f"DataFrame for {timeframe} is empty. Please download data first.") - if 'date' not in df.columns: - raise ValueError(f"DataFrame for {timeframe} missing 'date' column. Columns: {df.columns.tolist()}") - # interval=1 时不需要重采样 - if interval == 1: - self.dataframe = df.copy() - else: - self.dataframe = resample_to_interval(df, interval) - #print(self.timeframe, len(self.dataframe)) - self.dataframe = self.add_indicators(self.dataframe) - self.klu_list = [] - self.klc_list = [] - self.bi_list = [] - self.zs_list = [] - self.bsp_list = [] - self.seg_list = [] - self.klc_fx_list = [] - self.klu_list = self.cal_kl_data(self.dataframe) - self.klc_list = self.get_klc_list(self.klu_list) - self.bi_list = self.cal_bi_list(self.klc_list) - self.seg_list = self.get_seg_list(self.bi_list) - self.zs_list = self.get_zs_list(self.bi_list, self.seg_list) - self.big_zs_list = self.get_big_zs_list(self.zs_list) - self.chanmacd = ChanMACD(self.klu_list) - self.klu_list = self.chanmacd.cal_macd_state() - - - def get_ema52(self, index=-1): - if self.klu_list: - ema52_value = self.klu_list[index].ema52 - # 处理NaN值 - if pd.isna(ema52_value) or ema52_value is None: - return None - return float(ema52_value) - return None - def get_ema24(self, index=-1): - if self.klu_list: - ema24_value = self.klu_list[index].ema24 - # 处理NaN值 - if pd.isna(ema24_value) or ema24_value is None: - return None - return float(ema24_value) - return None - def get_current_klc(self): - if len(self.klc_list) > 0: - return self.klc_list[-2] - return None - def add_indicators(self, df): - fast = 26 - slow = 52 - period = 9 - macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period) - bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0) - bb120 = ta.BBANDS(df, timeperiod=120, nbdevup=3.0, nbdevdn=3.0, matype=0) - bb30 = ta.BBANDS(df, timeperiod=41, nbdevup=2.3, nbdevdn=2.3, matype=0) - bb302 = ta.BBANDS(df, timeperiod=41, nbdevup=2.0, nbdevdn=2.0, matype=0) - bb30 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0) - bb302 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0) - bb2633 = ta.BBANDS(df, timeperiod=26, nbdevup=3.0, nbdevdn=3.0, matype=0) - # 计算布林带中轨(移动平均线) - bb30_middle = ta.SMA(df, timeperiod=90) - - # 手动计算布林带 %B 指标 (BBP) - # %B = (Price - Lower Band) / (Upper Band - Lower Band) - bbp365 = (df['close'] - bb365['lowerband']) / (bb365['upperband'] - bb365['lowerband']) - bbp120 = (df['close'] - bb120['lowerband']) / (bb120['upperband'] - bb120['lowerband']) - bbp30 = (df['close'] - bb30['lowerband']) / (bb30['upperband'] - bb30['lowerband']) - bbp302 = (df['close'] - bb302['lowerband']) / (bb302['upperband'] - bb302['lowerband']) - bbp2633 = (df['close'] - bb2633['lowerband']) / (bb2633['upperband'] - bb2633['lowerband']) - df['bb2633upper'] = bb2633['upperband'] - df['bb2633lower'] = bb2633['lowerband'] - df['bbp2633'] = bbp2633 - df['bb2633middle'] = bb2633['middleband'] - df['atr'] = ta.ATR(df, timeperiod=14) - df['bbup365'] = bb365['upperband'] - df['bblow365'] = bb365['lowerband'] - df['bbp365'] = bbp365 - df['bbup120'] = bb120['upperband'] - df['bblow120'] = bb120['lowerband'] - df['bbp120'] = bbp120 - df['bbup30'] = bb30['upperband'] - df['bblow30'] = bb30['lowerband'] - df['bbmiddle30'] = bb30_middle # 添加bb30中轨 - df['bbp30'] = bbp30 - df['bbup302'] = bb302['upperband'] - df['bblow302'] = bb302['lowerband'] - df['bbp302'] = bbp302 - df['macd'] = macd['macd'] - df['macdsignal'] = macd['macdsignal'] - df['macdhist'] = macd['macdhist'] - df['ema5'] = ta.EMA(df, timeperiod=5) - df['ema10'] = ta.EMA(df, timeperiod=10) - df['ema24'] = ta.EMA(df, timeperiod=24) - df['ema52'] = ta.EMA(df, timeperiod=52) - df['ema104'] = ta.EMA(df, timeperiod=104) - df['ema156'] = ta.EMA(df, timeperiod=156) - df['ema208'] = ta.EMA(df, timeperiod=208) - df['ema26'] = ta.EMA(df, timeperiod=26) - df['ema13'] = ta.EMA(df, timeperiod=13) - df['ema7'] = ta.EMA(df, timeperiod=7) - df['rsi'] = ta.RSI(df, timeperiod=14) - df['volume_ratio'] = self.cal_volume_ratio(df) - return df - def get_klu_state(self, dataframe): - klc_list = self.get_klc_list(self.get_klu_list(dataframe)) - bi_list = self.cal_bi_list(klc_list) - klu_state_list = [] - klc_index = 0 - for index in range(0, len(dataframe)): - if klc_index == len(klc_list): - klc_index = len(klc_list) - 1 - klc = klc_list[klc_index] - if klc.end_klu and klc.end_klu.idx == index: - if klc.klc_state == Chan_KLC_STATE.S10: - klu_state_list.append("10") - #print(klc.end_time, klc.klc_fx_type) - elif klc.klc_state == Chan_KLC_STATE.S_10: - klu_state_list.append("-10") - #print(klc.end_time, klc.klc_fx_type) - elif klc.klc_state == Chan_KLC_STATE.S11: - klu_state_list.append("11") - #print(klc.end_time, klc.klc_fx_type) - elif klc.klc_state == Chan_KLC_STATE.S_11: - klu_state_list.append("-11") - #print(klc.end_time, klc.klc_fx_type) - else: - klu_state_list.append("00") - klc_index += 1 - else: - klu_state_list.append("00") - print(klu_state_list[:20]) - return klu_state_list - - def get_bsp_state(self, dataframe): - klu_list = self.get_klu_list(dataframe) - klc_list = self.get_klc_list(klu_list) - bi_list = self.cal_bi_list(klc_list) - seg_list = self.get_seg_list(bi_list) - bi_zs_list = self.cal_bi_zs(seg_list) - bsp_list = self.find_all_bsp(bi_list, bi_zs_list) - bsp_state_list = [0] * len(dataframe) - klc_index = 0 - for index in range(0, len(dataframe)): - if klc_index == len(klc_list): - klc_index = len(klc_list) - 1 - klc = klc_list[klc_index] - if klc.end_klu and klc.end_klu.idx == index: - if klc.klc_fx_type == Chan_KLC_FX.TOP2: - bi = klc.bi.pre - if bi and bi.is_sure and bi.end_klc.bsp_type == Chan_BSP_TYPE.B3: - # 第三类买点 - bsp_state_list[index] = -1 - #print(klc.end_time, "B3") - else: - bsp_state_list[index] = 0 - elif klc.klc_fx_type == Chan_KLC_FX.BOTTOM2: - bi = klc.bi.pre - if bi and bi.is_sure and bi.end_klc.bsp_type == Chan_BSP_TYPE.S3: - # 第三类卖点 - bsp_state_list[index] = 1 - #print(klc.end_time, "S3") - else: - bsp_state_list[index] = 0 - klc_index += 1 - else: - bsp_state_list[index] = 0 - return bsp_state_list - def get_ema_state(self, dataframe): - klu_list = self.get_klu_list(dataframe) - klc_list = self.get_klc_list(klu_list) - bi_list = self.cal_bi_list(klc_list) - klu_state_list = [] - for klu in klu_list: - if klu.near0_return == 1: - klu_state_list.append("1") - elif klu.near0_return == 9: - klu_state_list.append("-1") - elif klu.candle_dir == Chan_K_DIR.BULL: - klu_state_list.append("2") - elif klu.candle_dir == Chan_K_DIR.BEAR: - klu_state_list.append("-2") - else: - klu_state_list.append("0") - return klu_state_list - def check_fx1(self, klc): - if klc.pre and klc.next: - if klc.high > klc.pre.high and klc.high > klc.next.high and klc.low > klc.pre.low and klc.low > klc.next.low: - if klc.pre.pre and klc.next.next: - if klc.high > klc.pre.pre.high and klc.high > klc.next.next.high: - #if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0: - klc.set_fx(Chan_FX_TYPE.TOP) - #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "TOP") - return Chan_FX_TYPE.TOP - elif klc.low < klc.pre.low and klc.low < klc.next.low and klc.high < klc.pre.high and klc.high < klc.next.high: - #if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0: - if klc.pre.pre and klc.next.next: - if klc.low < klc.pre.pre.low and klc.low < klc.next.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.macd, klc.state, klc.fx, "BOTTOM") - return Chan_FX_TYPE.BOTTOM - return Chan_FX_TYPE.UNKNOWN - def check_fx(self, klc): - if klc.pre and klc.next: - if klc.high > klc.pre.high and klc.high > klc.next.high and klc.low > klc.pre.low and klc.low > klc.next.low: - #if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0: - klc.set_fx(Chan_FX_TYPE.TOP) - #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "TOP") - return Chan_FX_TYPE.TOP - elif klc.low < klc.pre.low and klc.low < klc.next.low and klc.high < klc.pre.high and klc.high < klc.next.high: - #if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0: - klc.set_fx(Chan_FX_TYPE.BOTTOM) - #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "BOTTOM") - return Chan_FX_TYPE.BOTTOM - return Chan_FX_TYPE.UNKNOWN - def check_fx2(self, klc): - if klc.pre and klc.next: - if klc.high > klc.pre.close and klc.close > klc.next.close and klc.close > klc.pre.close and klc.close > klc.next.close: - #if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0: - klc.set_fx(Chan_FX_TYPE.TOP) - #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "TOP") - return Chan_FX_TYPE.TOP - elif klc.low < klc.pre.close and klc.close < klc.next.close and klc.close < klc.pre.close and klc.close < klc.next.close: - #if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0: - klc.set_fx(Chan_FX_TYPE.BOTTOM) - #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "BOTTOM") - return Chan_FX_TYPE.BOTTOM - return Chan_FX_TYPE.UNKNOWN - def check_fx_pattern(self, klc): - klu_list = klc.pre.klu_list + klc.klu_list + klc.next.klu_list - - self.cal_klu_pattern(klu_list) - p = "" - for klu in klu_list: - p += klu.to_string() - #print(p) - def cal_volume_ratio(self, dataframe, window=10): - df = dataframe.copy() - # 计算过去N根K线的平均成交量 - df['avg_volume'] = df['volume'].rolling(window=window).mean() - # 计算量比 - df['volume_ratio'] = df['volume'] / df['avg_volume'] - # 填充缺失值(前N根K线) - df['volume_ratio'] = df['volume_ratio'].fillna(1.0) - return df['volume_ratio'] - def cal_trend(self, klc_list): - """ - 基于价格与EMA24/EMA52的位置关系、以及MACD/Signal/Hist的方向, - 为每个KLC打上趋势标签:'UP' / 'DOWN' / 'FLAT'。 - 仅设置 klc.trend,不影响其它字段。 - """ - if not klc_list: - return klc_list - last_trend = Chan_PRICE_TREND.UNKNOWN - # 趋势延续性:参考近 N 根已完成的KLC - lookback_n = 5 - prev_klcs = [] - for klc in klc_list: - price = getattr(klc, 'close', None) - ema24 = getattr(klc, 'ema24', None) - ema52 = getattr(klc, 'ema52', None) - macd_raw = getattr(klc, 'macd', None) - signal_raw = getattr(klc, 'signal', None) - hist_raw = getattr(klc, 'macdhist', None) - macd = macd_raw if macd_raw is not None else 0 - signal = signal_raw if signal_raw is not None else 0 - hist = hist_raw if hist_raw is not None else 0 - rsi = getattr(klc, 'rsi', None) - macd_ready = macd_raw is not None and signal_raw is not None - hist_ready = hist_raw is not None - trend = Chan_PRICE_TREND.UNKNOWN - score = 0 - try: - # 有效性 - price_valid = price is not None and price != 0 - ema24_valid = ema24 is not None and ema24 != 0 - ema52_valid = ema52 is not None and ema52 != 0 - # 多因子投票 - - # 1) 均线结构 + 价位 - if ema24_valid or ema52_valid: - ma_votes = 0 - if ema24_valid and ema52_valid: - ma_votes += 1 if ema24 > ema52 else -1 - if price_valid and ema24_valid: - ma_votes += 1 if price > ema24 else 0 - if price_valid and ema52_valid: - ma_votes += 1 if price > ema52 else -1 - # 限幅,避免相关因子重复计分 - score += max(-2, min(2, ma_votes)) - # 2) MACD结构 - if macd_ready: - score += 1 if macd >= signal else -1 - if hist_ready and hist != 0: - score += 1 if hist > 0 else -1 - # 3) 动量与均线差分斜率 - pre = getattr(klc, 'pre', None) - pre_hist = getattr(pre, 'macdhist', None) if pre else None - if pre: - pre_close = getattr(pre, 'close', None) - if price_valid and pre_close is not None: - score += 1 if price >= pre_close else -1 - pre_ema24 = getattr(pre, 'ema24', None) - pre_ema52 = getattr(pre, 'ema52', None) - if ema24_valid and ema52_valid and pre_ema24 not in (None, 0) and pre_ema52 not in (None, 0): - spread_now = ema24 - ema52 - spread_pre = pre_ema24 - pre_ema52 - score += 1 if spread_now >= spread_pre else -1 - # 3.1) MACD柱体动量趋势:考虑 macdhist 的斜率与过零 - if hist_ready and pre_hist is not None: - # 柱体斜率:上升加分,下降减分 - if hist > pre_hist: - score += 1 - elif hist < pre_hist: - score -= 1 - # 过零加权:负转正更偏多,正转负更偏空 - if pre_hist < 0 and hist > 0: - score += 1 - elif pre_hist > 0 and hist < 0: - score -= 1 - # 3.2) EMA52 突破/跌破加权 - if ema52_valid and price_valid and pre_close is not None and pre_ema52 not in (None, 0): - # 看多突破:从均线下方上破且动量配合 - if pre_close <= pre_ema52 and price > ema52 and (hist is None or pre_hist is None or hist >= pre_hist): - score += 1 - # 看空跌破:从均线上方下破且动量配合 - if pre_close >= pre_ema52 and price < ema52 and (hist is None or pre_hist is None or hist <= pre_hist): - score -= 1 - # 3.3) EMA52 支撑/阻力触碰(非强穿越) - if ema52_valid and price_valid: - low_v = getattr(klc, 'low', None) - high_v = getattr(klc, 'high', None) - if low_v is not None and high_v is not None and ema52 not in (None, 0): - # 触碰容差(相对EMA52的0.15%) - touch_tol = 0.0015 - # 作为支撑:收盘在上,最低靠近EMA52 - near_support_touch = (price > ema52) and (abs(low_v - ema52) / abs(ema52) <= touch_tol) - # 作为阻力:收盘在下,最高靠近EMA52 - near_resistance_touch = (price < ema52) and (abs(high_v - ema52) / abs(ema52) <= touch_tol) - if near_support_touch: - # 若动量不弱,则更偏多 - score += 1 if (hist is None or pre_hist is None or hist >= pre_hist) else 0 - if near_resistance_touch: - # 若动量不强,则更偏空 - score -= 1 if (hist is None or pre_hist is None or hist <= pre_hist) else 0 - # 3.4) 多次对 EMA52 的"拒绝"配合 MACD 逆向:易形成压/支并反向 - # 统计近窗口内的上/下拒绝次数: - # - 上拒绝:价格位于 EMA52 下方,最高触及/越过 EMA52 但收盘仍在下方 - # - 下拒绝:价格位于 EMA52 上方,最低触及/跌破 EMA52 但收盘仍在上方 - recent_up_rejects = 0 - recent_down_rejects = 0 - if ema52_valid: - window_rej = prev_klcs[-lookback_n:] if len(prev_klcs) > 0 else [] - rej_tol = 0.0015 - for wk in window_rej: - wk_close = getattr(wk, 'close', None) - wk_ema52 = getattr(wk, 'ema52', None) - wk_high = getattr(wk, 'high', None) - wk_low = getattr(wk, 'low', None) - if wk_close is None or wk_ema52 in (None, 0): - continue - # 上拒绝(阻力):下方多次试图上破但未站上 - if wk_close < wk_ema52 and wk_high is not None: - if wk_high >= wk_ema52 or abs(wk_high - wk_ema52) / abs(wk_ema52) <= rej_tol: - recent_up_rejects += 1 - # 下拒绝(支撑):上方多次试图下破但未跌破 - if wk_close > wk_ema52 and wk_low is not None: - if wk_low <= wk_ema52 or abs(wk_low - wk_ema52) / abs(wk_ema52) <= rej_tol: - recent_down_rejects += 1 - # 定义 MACD 的方向偏好 - macd_bias_up = macd_ready and (macd >= signal) and (not hist_ready or pre_hist is None or hist >= pre_hist) - macd_bias_down = macd_ready and (macd <= signal) and (not hist_ready or pre_hist is None or hist <= pre_hist) - # 若多次上拒绝且 MACD 偏空,则更偏向下行;若多次下拒绝且 MACD 偏多,则更偏向上行 - if recent_up_rejects >= 2 and macd_bias_down: - score -= 2 - if recent_down_rejects >= 2 and macd_bias_up: - score += 2 - # 4) RSI 辅助 - if rsi is not None: - if rsi >= 55: - score += 1 - elif rsi <= 45: - score -= 1 - # 5) 指标未就绪回退(EMA/MACD缺失时,用动量与RSI辅助,延续趋势) - has_full_ind = ema24_valid and ema52_valid and not (macd == 0 and signal == 0 and hist == 0) - if not has_full_ind: - # 仅根据价动量/RSI做轻量判断,默认延续 last_trend,除非出现强反向 - strong_up = False - strong_down = False - pre = getattr(klc, 'pre', None) - if pre: - pre_close = getattr(pre, 'close', None) - if price_valid and pre_close is not None: - strong_up = (price >= pre_close) - strong_down = (price < pre_close) - if rsi is not None: - if rsi >= 60: - strong_up = True - elif rsi <= 40: - strong_down = True - if last_trend == Chan_PRICE_TREND.UP and not strong_down: - trend = Chan_PRICE_TREND.UP - elif last_trend == Chan_PRICE_TREND.DOWN and not strong_up: - trend = Chan_PRICE_TREND.DOWN - else: - trend = Chan_PRICE_TREND.UP if strong_up and not strong_down else (Chan_PRICE_TREND.DOWN if strong_down and not strong_up else Chan_PRICE_TREND.FLAT) - else: - # 6) 震荡过滤(仅当极近EMA52且MACD贴合时判作震荡) - near_flat = False - if price_valid and ema52_valid: - near_ema52 = abs(price - ema52) / abs(ema52) <= 0.0005 # 0.05% - if macd_ready: - macd_scale = max(abs(macd), abs(signal), 1e-6) - near_macd = abs(macd - signal) / macd_scale <= 0.05 - else: - near_macd = False - near_flat = near_ema52 and near_macd - # 7) 动态阈值 + 趋势记忆(更强粘滞:趋势中容忍小幅反分) - # 引入过去 N 根KLC 的趋势延续性来动态调整翻转阈值,并结合 EMA52 支撑/阻力触碰强化门槛 - force_flip_down = False - force_flip_up = False - if near_flat: - trend = Chan_PRICE_TREND.FLAT - else: - # 计算过去窗口的趋势一致性 - window = prev_klcs[-lookback_n:] if len(prev_klcs) > 0 else [] - persist_up = 0 - persist_down = 0 - for wk in window: - if getattr(wk, 'trend', None) == Chan_PRICE_TREND.UP: - persist_up += 1 - elif getattr(wk, 'trend', None) == Chan_PRICE_TREND.DOWN: - persist_down += 1 - persist_ratio_up = (persist_up / len(window)) if len(window) > 0 else 0 - persist_ratio_down = (persist_down / len(window)) if len(window) > 0 else 0 - # 基准阈值 - down_flip_threshold = -2 - up_flip_threshold = 2 - # 若最近多为UP,则从UP翻转需更强反向信号;同理对DOWN - if last_trend == Chan_PRICE_TREND.UP and persist_ratio_up >= 0.6: - down_flip_threshold = -3 - elif last_trend == Chan_PRICE_TREND.DOWN and persist_ratio_down >= 0.6: - up_flip_threshold = 3 - # EMA52 触碰强化门槛:UP时若出现支撑触碰,下翻更难;DOWN时若出现阻力触碰,上翻更难 - if ema52_valid and price_valid: - low_v = getattr(klc, 'low', None) - high_v = getattr(klc, 'high', None) - if low_v is not None and high_v is not None and ema52 not in (None, 0): - touch_tol = 0.0015 - near_support_touch = (price > ema52) and (abs(low_v - ema52) / abs(ema52) <= touch_tol) - near_resistance_touch = (price < ema52) and (abs(high_v - ema52) / abs(ema52) <= touch_tol) - if last_trend == Chan_PRICE_TREND.UP and near_support_touch: - # 强化维持UP:进一步降低向下翻转阈值 - down_flip_threshold = min(down_flip_threshold - 1, -3) - if last_trend == Chan_PRICE_TREND.DOWN and near_resistance_touch: - # 强化维持DOWN:进一步提高向上翻转阈值 - up_flip_threshold = max(up_flip_threshold + 1, 3) - # 7.1) 复合拐头信号:MACD/Signal 同向拐头 + hist 连续减弱 + 多次未能越过 EMA52 - pre_macd = getattr(pre, 'macd', None) if pre else None - pre_signal = getattr(pre, 'signal', None) if pre else None - macd_slope = (macd - pre_macd) if (macd_ready and pre_macd is not None) else 0 - signal_slope = (signal - pre_signal) if (macd_ready and pre_signal is not None) else 0 - # hist 连续减弱(绝对值缩小) - hist_seq = [] - for wk in prev_klcs[-2:]: - val = getattr(wk, 'macdhist', None) - if val is not None: - hist_seq.append(val) - if hist is not None: - hist_seq.append(hist) - weaken_steps = 0 - for i in range(1, len(hist_seq)): - if abs(hist_seq[i]) < abs(hist_seq[i-1]): - weaken_steps += 1 - # 近窗口对 EMA52 的"未能站上/跌破"统计(放宽窗口与条件) - window_ema = prev_klcs[-4:] if len(prev_klcs) > 0 else [] - no_up_break = False - no_down_break = False - if ema52_valid: - # 未能有效上破:最近若干根收盘大多数不在 EMA52 上方,且高点多次触及/接近 - cnt_touch_up = 0 - cnt_close_above = 0 - for wk in window_ema: - wk_close = getattr(wk, 'close', None) - wk_high = getattr(wk, 'high', None) - wk_ema = getattr(wk, 'ema52', None) - if wk_close is not None and wk_ema not in (None, 0): - if wk_close > wk_ema: - cnt_close_above += 1 - if wk_high is not None and (wk_high >= wk_ema or abs(wk_high - wk_ema) / abs(wk_ema) <= 0.0015): - cnt_touch_up += 1 - no_up_break = (cnt_close_above <= 1 and cnt_touch_up >= 1 and price <= ema52) - # 未能有效下破:最近若干根收盘大多数不在 EMA52 下方,且低点多次触及/接近 - cnt_touch_down = 0 - cnt_close_below = 0 - for wk in window_ema: - wk_close = getattr(wk, 'close', None) - wk_low = getattr(wk, 'low', None) - wk_ema = getattr(wk, 'ema52', None) - if wk_close is not None and wk_ema not in (None, 0): - if wk_close < wk_ema: - cnt_close_below += 1 - if wk_low is not None and (wk_low <= wk_ema or abs(wk_low - wk_ema) / abs(wk_ema) <= 0.0015): - cnt_touch_down += 1 - no_down_break = (cnt_close_below <= 1 and cnt_touch_down >= 1 and price >= ema52) - # 若当前为UP趋势,出现明显拐头+hist减弱+未能上破EMA52,则加速看空 - if last_trend == Chan_PRICE_TREND.UP and macd_slope < 0 and signal_slope < 0 and weaken_steps >= 1 and no_up_break and macd_bias_down: - score -= 3 - down_flip_threshold = max(down_flip_threshold, 0) - force_flip_down = True - # 若当前为DOWN趋势,出现明显拐头+hist减弱+未能下破EMA52,则加速看多 - if last_trend == Chan_PRICE_TREND.DOWN and macd_slope > 0 and signal_slope > 0 and weaken_steps >= 1 and no_down_break and macd_bias_up: - score += 3 - up_flip_threshold = min(up_flip_threshold, 0) - force_flip_up = True - # 多次对 EMA52 的拒绝配合 MACD 逆向:加速反向翻转(降低相反方向阈值) - if recent_up_rejects >= 2 and macd_bias_down: - # 从 UP 向 DOWN 的翻转更容易 - down_flip_threshold = max(down_flip_threshold, -1) - if recent_down_rejects >= 2 and macd_bias_up: - # 从 DOWN 向 UP 的翻转更容易 - up_flip_threshold = min(up_flip_threshold, 1) - if force_flip_down: - trend = Chan_PRICE_TREND.DOWN - elif force_flip_up: - trend = Chan_PRICE_TREND.UP - elif last_trend == Chan_PRICE_TREND.UP: - if score <= down_flip_threshold: - trend = Chan_PRICE_TREND.DOWN - else: - trend = Chan_PRICE_TREND.UP - elif last_trend == Chan_PRICE_TREND.DOWN: - if score >= up_flip_threshold: - trend = Chan_PRICE_TREND.UP - else: - trend = Chan_PRICE_TREND.DOWN - else: - # 初始无记忆时,降低进入门槛 - if score >= 1: - trend = Chan_PRICE_TREND.UP - elif score <= -1: - trend = Chan_PRICE_TREND.DOWN - else: - trend = Chan_PRICE_TREND.FLAT - except Exception: - trend = Chan_PRICE_TREND.UNKNOWN - # 写回趋势 - if klc.end_time is None: - trend = Chan_PRICE_TREND.FLAT - if hasattr(klc, 'set_trend'): - klc.set_trend(trend) - else: - setattr(klc, 'trend', trend) - last_trend = trend - # 更新滑窗:仅向后看 - prev_klcs.append(klc) - price_diff = klc.close - klc.pre.close if klc.pre else 0 - #if klc.index > len(klc_list) - 10: - #print(klc.start_time, klc.end_time, klc.close, klc.ema24, klc.ema52, klc.macd, klc.signal, klc.macdhist, klc.trend, price_diff, score) - #print(klc.start_time, klc.end_time, klc.trend, price_diff, score) - return klc_list - def cal_kl_data(self, dataframe:DataFrame): - fields = "time,open,high,low,close,volume" - klu_list = [] - last_klu = None - 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) - # print(klu.time, klu.open, klu.high, klu.low, klu.close, klu.volume) - klu.set_idx(i) - klu_list.append(klu) - if last_klu: - last_klu.set_next(klu) - klu.set_pre(last_klu) - last_klu = klu - if 'macd' in item: - klu.set_indicators(item) - return klu_list - def get_bi_list(self, dataframe): - bi_list = self.cal_bi_list(self.get_klc_list(dataframe)) - #bi_list = self.cal_bi_list_chanlun(self.get_klc_list(dataframe)) - return bi_list - def get_kl_data(self, dataframe:DataFrame): - return self.cal_kl_data(dataframe) - def get_klc_list(self, klu_list): - klc_list = [] - last_klu = None - macd = ChanMACD(klu_list) - klu_list = macd.cal_macd_state() - ema_up_list = [] - ema_down_list = [] - ema_up_count = 0 - ema_down_count = 0 - last_klu = None - for klu in klu_list: - ema = klu.ema52 - last_ema = last_klu.ema52 if last_klu else 0 - if klu.close >= ema: - ema_up_count += 1 - elif klu.close < ema: - ema_down_count += 1 - if last_klu and last_klu.close >= last_ema and klu.close < ema: - ema_up_list.append(ema_up_count) - #print(last_klu.time, ema_up_count, "UP END") - ema_up_count = 0 - elif last_klu and last_klu.close < last_ema and klu.close >= ema: - ema_down_list.append(ema_down_count) - #print(last_klu.time, ema_down_count, "DOWN END") - ema_down_count = 0 - if len(klc_list) > 0: - last_klc = klc_list[-1] - if klu.exception: - 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.high = klu.close if klu.close > klu.open else klu.open - klc.low = klu.open if klu.close > klu.open else klu.close - 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() - #print(klu.time, klu.high, klu.low, klu.close, klu.open, klu.exception) - else: - 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 - klc_list = self.cal_trend(klc_list) - #print(ema52_up_list, ema52_down_list) - return klc_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, bi) - 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) - last_seg.add_bi(bi) - #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, bi) - 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 - last_seg.add_bi(bi) - 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") - last_seg.add_bi(bi) - - else: - last_down_sbi = ChanSBI(bi, len(down_sbi_list), bi.dir) - down_sbi_list.append(last_down_sbi) - last_seg.add_bi(bi) - #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.add_bi(bi) - - # 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, bi) - 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) - last_seg.add_bi(bi) - #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, bi) - #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 - last_seg.add_bi(bi) - 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 - last_seg.add_bi(bi) - #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) - last_seg.add_bi(bi) - #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 - last_seg.add_bi(bi) - #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, bi) - 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, bi) - 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) - """ - if len(seg_list) > 1: - seg = seg_list[-1] - last_seg = seg_list[-2] - last_seg_bi = last_seg.bi_list[-3] - bi_index = seg.start_bi.index - for i in range(bi_index, len(bi_list) - 1): - # last seg is down - if seg.dir == Chan_SEG_DIR.UP: - if bi_list[i].dir == Chan_BI_DIR.UP: - last_seg_peak = last_seg_bi.high - if bi_list[i].high > last_seg_peak: - # The confirmed - print("Last UP seg is broken, create a new seg. 1") - seg.pre_set_end_bi(bi_list[i]) - seg = ChanSEG(bi_list[i+1], len(seg_list), Chan_SEG_DIR.DOWN, bi) - seg_list.append(seg) - last_seg = seg_list[-2] - if len(last_seg.bi_list) > 3: - last_seg_bi = last_seg.bi_list[-3] - - else: - if bi_list[i].dir == Chan_BI_DIR.DOWN: - last_seg_peak = last_seg_bi.low - if bi_list[i].low < last_seg_peak: - print("Last DOWN seg is broken, create a new seg. 1") - seg.pre_set_end_bi(bi_list[i]) - seg = ChanSEG(bi_list[i+1], len(seg_list), Chan_SEG_DIR.UP, bi) - seg_list.append(seg) - last_seg = seg_list[-2] - if len(last_seg.bi_list) > 3: - last_seg_bi = last_seg.bi_list[-3] - else: - if len(seg_list) == 1: - last_seg = seg_list[-1] - bi_index = last_seg.bi_list[0].index - for i in range(bi_index, len(bi_list) - 1): - if i > bi_index + 2: - last_seg_peak = bi_list[i-2].high - # last seg is down - if last_seg.dir == Chan_SEG_DIR.DOWN: - if bi_list[i].dir == Chan_BI_DIR.UP: - if bi_list[i].high > last_seg_peak: - print("Last seg is broken, create a new seg. 2") - last_seg.pre_set_end_bi(bi_list[i-1]) - seg = ChanSEG(bi_list[i], len(seg_list), Chan_SEG_DIR.UP, bi) - seg_list.append(seg) - last_seg = seg - last_seg_bi = bi_list[i] - break - """ - #self.cal_bi_zs(seg_list) - return seg_list - def get_zs_state(self, df): - bi_list = self.cal_bi_list(self.get_klc_list(self.get_kl_data(df))) - seg_list = self.get_seg_list(bi_list) - zs_list = self.calculate_zs(seg_list) - for zs in zs_list: - last_zs = zs - return zs_list - def cal_bi_list(self, klc_list): - bi_list = [] - last_top = None - last_bottom = None - bi_klc_min = 4 - last_fx_klc = None - for klc in klc_list: - if last_fx_klc: - klc.check_klc_state(last_fx_klc) - klc.check_fx_confirmed(last_top, last_bottom) - fx = self.check_fx(klc) - if fx == Chan_FX_TYPE.TOP: - if last_bottom: - if self.check_top_fx(last_bottom, klc) == False: - fx = Chan_FX_TYPE.UNKNOWN - if fx == Chan_FX_TYPE.BOTTOM: - if last_top: - if self.check_bottom_fx(last_top, klc) == False: - #print(klc.end_time, last_top.end_time, "---") - fx = Chan_FX_TYPE.UNKNOWN - # Do nothing - if fx == Chan_FX_TYPE.UNKNOWN: - if len(bi_list) > 0: - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #continue - if len(bi_list) > 0 and klc.end_klu: - last_bi = bi_list[-1] - #print(klc.start_time, last_bi.start_time, last_bi.end_time, last_bi.dir, last_bi.high, last_bi.low, last_bottom.end_time, "last bi") - if last_top and last_bi.dir == Chan_BI_DIR.DOWN: - if last_bottom and klc.high > last_bi.high: - #print(klc.end_time, "Top 7, 1", last_bi.start_time, klc.high, last_bi.high) - #klc.klc_fx_type = Chan_KLC_FX.TOP7 - #klc.fx = Chan_FX_TYPE.TOP - """ - last_bi.set_end_klc(last_bottom, klc) - bi = ChanBI(last_bottom, len(bi_list), Chan_BI_DIR.UP) - #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM7) - #klc.bb_out = True - last_bi.set_next(bi) - bi.set_pre(last_bi) - for klc_index in range(last_bi.end_klc.index, len(klc_list)): - bi.add_klc(klc_list[klc_index]) - bi_list.append(bi) - last_top = klc - klc.set_bi(bi) - #print(klc.start_time, bi.start_time, bi.end_time, bi.dir, bi.high, bi.low, bi.is_sure) - """ - else: - if last_bottom and last_bi.dir == Chan_BI_DIR.UP: - if last_top and klc.low < last_bi.low: - #print(klc.end_time, "Bottom 8, 2", last_bi.start_time) - #klc.klc_fx_type = Chan_KLC_FX.BOTTOM8 - #klc.fx = Chan_FX_TYPE.BOTTOM - """ - last_bi.set_end_klc(last_top, klc) - bi = ChanBI(last_top, len(bi_list), Chan_BI_DIR.DOWN) - #klc.set_klc_fx_type(Chan_KLC_FX.TOP6) - #klc.bb_out = True - last_bi.set_next(bi) - bi.set_pre(last_bi) - for klc_index in range(last_bi.end_klc.index, len(klc_list)): - bi.add_klc(klc_list[klc_index]) - bi_list.append(bi) - last_bottom = klc - klc.set_bi(bi) - #print(klc.start_time, bi.start_time, bi.end_time, bi.dir, bi.high, bi.low, bi.is_sure) - """ - else: - last_fx_klc = klc - if fx == Chan_FX_TYPE.TOP: - #print(klc.end_time, fx, klc.pre.high, klc.high, klc.pre.start_time, klc.pre.end_time) - 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: - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #klc.set_klc_fx_type(Chan_KLC_FX.TOP3) - #print(klc.end_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, bi_list[-1].dir, "Last Top Change 1") - klc.set_klc_fx_type(Chan_KLC_FX.TOP1) - self.check_fx_pattern(klc) - #print(klc.end_time, klc.fx, "一类卖点Sell 1") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - # 不满足结合律的分型 - else: - #klc.set_klc_fx_type(Chan_KLC_FX.TOP0) - #print(klc.end_time, klc.klc_fx_type) - if last_bottom.index + bi_klc_min > klc.index: - if last_top.high > klc.high: - #print(klc.start_time, klc.fx, "二类卖点Sell 1") - #klc.set_klc_fx_type(Chan_KLC_FX.TOP8) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - # New TOP Found前面的UKNOWN可能出现TOP7,但是这里的也可能出现TOP8分型 - else: - # 顶分型在出现2之前超过前一个笔的顶 TOP8 - if last_top.index + bi_klc_min < 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 and False: - pre_last_bi.update_bi(klc) - bi_list.remove(last_bi) - pre_last_bi.set_next(None) - #last_top.set_fx(Chan_FX_TYPE.PTOP) - last_top = klc - last_bottom = pre_last_bi.start_klc - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Bottom Change 1") - klc.set_klc_fx_type(Chan_KLC_FX.TOP2) - #print(klc.start_time, last_bi.start_klc.start_time, "New TOP Found reset last bi") - #klc.set_state("10") - #print(klc.start_time, klc.fx, "笔卖点Sell 1") - ###klc.set_klc_fx_type(Chan_KLC_FX.TOP2) # when bi is down but the fx is top - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #klc.set_klc_fx_type(Chan_KLC_FX.TOP8) - #print(klc.start_time, last_bi.start_klc.start_time, "New TOP Found reset last bi") - else: - #klc.set_fx(Chan_FX_TYPE.PTOP) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.end_time, klc.fx, "无效顶分型") - # 满足结合律 - 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 - #print(klc.end_time, klc.fx, bi_list[-1].dir, "Last Top Change 2") - klc.set_klc_fx_type(Chan_KLC_FX.TOP2) - self.check_fx_pattern(klc) - #bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #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 = klc - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 3") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - # 初始化的时候用,其他时间不用 - else: - #klc.set_fx(Chan_FX_TYPE.TT) - #print(klc.start_time, klc.fx, "二类卖点Sell 2") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - # last_top == None 初始化的时候用,其他时间不用 - else: - if last_bottom: - # 不满足结合律的分型 - if last_bottom.index + bi_klc_min > klc.index: - #klc.set_fx(Chan_FX_TYPE.PTOP) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, "中枢卖点Sell 1") - else: - # First temp top and last bottom confirmed - last_top = klc - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 4") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - # 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) - bi.add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 5") - #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: - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM3) - #print(last_bottom.start_time, last_bottom.end_time, "--------------------------------1") - #print(klc.end_time, klc.fx, "二类买点Buy 1") - else: - # A new bottom found - last_bottom = klc - #print(klc.end_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 1") - klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM1) - self.check_fx_pattern(klc) - #print(klc.end_time, klc.fx, "一类买点Buy 1") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - # 不满足结合律的分型 - else: - #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM0) - #print(klc.end_time, klc.klc_fx_type) - if last_top.index + bi_klc_min > klc.index: - if last_bottom.low < klc.low: - #print(klc.end_time, klc.fx, "中枢买点Buy 1") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM8) - # Found new bottom没有意义,上面UNKNOWN的时候已经是笔破坏了 - else: - #print(klc.end_time, last_bottom.end_time, "Found a new bottom") - if last_bottom.index + bi_klc_min < 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 and False: - pre_last_bi.update_bi(klc) - bi_list.remove(last_bi) - pre_last_bi.set_next(None) - last_bottom = klc - last_top = pre_last_bi.start_klc - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Bottom Change 2") - klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM2) - #print(klc.start_time, last_bi.start_klc.start_time, "New BOTTOM Found reset last bi") - #print(klc.start_time, klc.fx, "笔买点Buy 1") - ###klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM2) # when bi is up but the fx is bottom - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM8) - else: - #klc.set_fx(Chan_FX_TYPE.UNKNOWN) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - print(klc.end_time, klc.fx, "无效底分型") - # 满足结合律的分型 - 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 - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 2") - klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM2) - self.check_fx_pattern(klc) - #bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #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 - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 3") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, "笔买点Buy 3") - else: - #klc.set_fx(Chan_FX_TYPE.BB) - #klc.set_state('-20') - #print(klc.start_time, klc.fx, "二类买点Buy 2") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - # last_bottom = None 初始化的时候用,其他时间不用 - else: - if last_top: - # 不满足结合律的分型 - if last_top.index + bi_klc_min > klc.index: - #klc.set_fx(Chan_FX_TYPE.PBOTTOM) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, "中枢买点Buy 1") - else: - # First temp bottom and last top confirmed - last_bottom = klc - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 4") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, "一类买点Buy 1") - # 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) - #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM7) - bi_list.append(bi) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 5") - #print(klc.start_time, klc.fx, "笔买点Buy 4") - self.get_above_zero_bsp(klc_list) - #print(bi_list[-1].start_time, bi_list[-1].end_time, len(bi_list[-1].klc_list)) - return bi_list - def get_above_zero_bsp(self, klc_list): - buy_bsp_list = [] - sell_bsp_list = [] - above_zero = False - buy_bsp = None - sell_bsp = None - for klc in klc_list: - if klc.pre and klc.pre.signal < 0 and klc.signal > 0: - above_zero = True - if klc.pre and klc.pre.signal > 0 and klc.signal < 0: - above_zero = False - if above_zero and klc.klc_fx_type == Chan_KLC_FX.BOTTOM2 and klc.macd > 0: - buy_bsp = klc - buy_bsp_list.append(klc) - #print(klc.end_time, "MACD 0轴上穿,回调笔底分型做多") - if buy_bsp and klc.pre and klc.pre.macdhist > 0 and klc.macdhist < 0: - sell_bsp = klc - sell_bsp_list.append(klc) - buy_bsp = None - #print(klc.end_time, "Sell BSP Found") - return buy_bsp_list - def check_top_fx(self, last_bottom, klc): - if (last_bottom.high > klc.pre.low or last_bottom.high > klc.next.low) and (klc.index - last_bottom.index < 100): - return False - return True - - def check_bottom_fx(self, last_top, klc): - if (last_top.low < klc.pre.high or last_top.low < klc.next.high) and (klc.index - last_top.index < 100): - return False - return True - # 线段内的中枢 - def cal_bi_zs(self, seg_list): - bi_zs_list = [] - for seg in seg_list: - zs_list = seg.cal_bi_zs() - if len(zs_list) > 0: - bi_zs_list = list(bi_zs_list) + list(zs_list) - return bi_zs_list - # 跨段不相连的中枢 - def cal_bi_zs_list(self, bi_list): - """ - 根据缠论笔中枢定义计算中枢(参照 get_zs_list 线段中枢判断规则) - 从第4根笔开始(索引3),每3根笔为一组检查 - 上涨中枢:后中枢 zd > 前中枢 zg(不重叠上移) - 下跌中枢:后中枢 zg < 前中枢 zd(不重叠下移) - 中枢可按两笔一组继续扩展到5根、7根... - """ - bi_zs_list = [] - if len(bi_list) < 3: - return bi_zs_list - - last_zs = None - start_idx = 3 - - while start_idx < len(bi_list): - if start_idx + 2 >= len(bi_list): - break - - bi1 = bi_list[start_idx] - bi2 = bi_list[start_idx + 1] - bi3 = bi_list[start_idx + 2] - - if not (bi1.is_sure and bi2.is_sure and bi3.is_sure): - start_idx += 1 - continue - - zg = min(bi1.high, bi2.high, bi3.high) - zd = max(bi1.low, bi2.low, bi3.low) - - if zg <= zd: - start_idx += 1 - continue - - valid = False - if last_zs is None: - if bi1.dir == Chan_BI_DIR.DOWN: - zs_dir = Chan_ZS_DIR.UP - valid = (bi2.dir == Chan_BI_DIR.UP and bi3.dir == Chan_BI_DIR.DOWN) - else: - zs_dir = Chan_ZS_DIR.DOWN - valid = (bi2.dir == Chan_BI_DIR.DOWN and bi3.dir == Chan_BI_DIR.UP) - else: - is_up_zs = zg > last_zs.zg - is_down_zs = zd < last_zs.zd - - if is_up_zs: - zs_dir = Chan_ZS_DIR.UP - valid = (bi1.dir == Chan_BI_DIR.DOWN and bi2.dir == Chan_BI_DIR.UP and bi3.dir == Chan_BI_DIR.DOWN) - elif is_down_zs: - zs_dir = Chan_ZS_DIR.DOWN - valid = (bi1.dir == Chan_BI_DIR.UP and bi2.dir == Chan_BI_DIR.DOWN and bi3.dir == Chan_BI_DIR.UP) - - if not valid: - start_idx += 1 - continue - gg = max(bi1.high, bi2.high, bi3.high) - dd = min(bi1.low, bi2.low, bi3.low) - zs = ChanBIZS(bi1, len(bi_zs_list), zs_dir) - zs.set_zg(zg) - zs.set_zd(zd) - zs.set_gg(gg) - zs.set_dd(dd) - zs.is_sure = False - zs.bi_list = [bi1, bi2, bi3] - - added_after_leave = [] - leave_index = start_idx + 4 - while leave_index < len(bi_list): - b = bi_list[leave_index] - if not b.is_sure: - break - if b.high >= zs.zd and b.low <= zs.zg: - added_after_leave.append(b.pre) - added_after_leave.append(b) - else: - break - leave_index += 2 - - if added_after_leave: - bis_for_zs = list(zs.bi_list) + list(added_after_leave) - bi_highs = [bi.high for bi in bis_for_zs] - bi_lows = [bi.low for bi in bis_for_zs] - zs.set_gg(max(bi_highs)) - zs.set_dd(min(bi_lows)) - zs.bi_list = bis_for_zs - bi = bis_for_zs[-1] - if bi.is_sure: - zs.set_end_bi(bi, bi.sure_time) - - start_idx = start_idx + len(added_after_leave) - else: - zs.set_end_bi(bi3, bi3.sure_time) - - if last_zs: - last_zs.set_next(zs) - zs.set_pre(last_zs) - - bi_zs_list.append(zs) - last_zs = zs - - start_idx += 4 - - if last_zs: - last_zs.is_sure = bi_list[-1].is_sure - - if last_zs and not last_zs.is_sure: - if last_zs.bi_list and len(last_zs.bi_list) > 0: - last_bi_of_zs = last_zs.bi_list[-1] - last_bi_idx = -1 - for i, bi in enumerate(bi_list): - if bi == last_bi_of_zs: - last_bi_idx = i - break - - has_leave = False - if last_bi_idx >= 0 and last_bi_idx + 1 < len(bi_list): - for i in range(last_bi_idx + 1, len(bi_list)): - bi = bi_list[i] - if bi.is_sure: - leave = (bi.low > last_zs.zg and bi.high > last_zs.zg) or \ - (bi.high < last_zs.zd and bi.low < last_zs.zd) - if leave: - has_leave = True - break - - if has_leave: - if last_bi_of_zs.is_sure: - last_zs.set_end_bi(last_bi_of_zs, last_bi_of_zs.sure_time) - return bi_zs_list - def get_bi_zs_list(self, bi_list): - """ - 根据缠论笔中枢定义计算中枢(完全参照 get_seg_zs_list 线段中枢判断规则) - 从第4根笔开始(索引3),每3根笔为一组检查 - 上涨中枢:后中枢 zd > 前中枢 zg(不重叠上移) - 下跌中枢:后中枢 zg < 前中枢 zd(不重叠下移) - 盘整/扩张:后中枢与前中枢整体区间有交集 → 合并扩展 - 中枢可按两笔一组继续扩展到5根、7根... - """ - bi_zs_list = [] - if len(bi_list) < 3: - return bi_zs_list - - last_zs = None - start_idx = 3 - - while start_idx < len(bi_list): - if start_idx + 2 >= len(bi_list): - break - - bi1 = bi_list[start_idx] - bi2 = bi_list[start_idx + 1] - bi3 = bi_list[start_idx + 2] - - if not (bi1.is_sure and bi2.is_sure and bi3.is_sure): - start_idx += 1 - continue - - zg = min(bi1.high, bi2.high, bi3.high) - zd = max(bi1.low, bi2.low, bi3.low) - - if zg <= zd: - start_idx += 1 - continue - - valid = False - if last_zs is None: - if bi1.dir == Chan_BI_DIR.DOWN: - zs_dir = Chan_ZS_DIR.UP - valid = (bi2.dir == Chan_BI_DIR.UP and bi3.dir == Chan_BI_DIR.DOWN) - else: - zs_dir = Chan_ZS_DIR.DOWN - valid = (bi2.dir == Chan_BI_DIR.DOWN and bi3.dir == Chan_BI_DIR.UP) - else: - is_up_zs = zd > last_zs.zg - is_down_zs = zg < last_zs.zd - - if is_up_zs: - zs_dir = Chan_ZS_DIR.UP - valid = (bi1.dir == Chan_BI_DIR.DOWN and bi2.dir == Chan_BI_DIR.UP and bi3.dir == Chan_BI_DIR.DOWN) - elif is_down_zs: - zs_dir = Chan_ZS_DIR.DOWN - valid = (bi1.dir == Chan_BI_DIR.UP and bi2.dir == Chan_BI_DIR.DOWN and bi3.dir == Chan_BI_DIR.UP) - - create_new_zs = False - if not valid: - # 如果新中枢和前一个中枢的中枢区间有重叠,不形成新中枢,合并扩展 - if last_zs is not None: - is_in_last_zs = (zd > last_zs.zd and zd < last_zs.zg) or \ - (zg < last_zs.zg and zg > last_zs.zd) or \ - (zg > last_zs.zg and zd < last_zs.zd) or \ - (zg < last_zs.zg and zd > last_zs.zd) - if is_in_last_zs: - # 扩展当前中枢:将 bi1-bi3 加入 last_zs - for bi in [bi1, bi2, bi3]: - if bi not in last_zs.bi_list: - last_zs.add_bi(bi) - create_new_zs = False - else: - start_idx += 1 - continue - else: - start_idx += 1 - continue - else: - create_new_zs = True - - # 新中枢形成时确认前一个中枢 - if last_zs and create_new_zs: - last_bi = last_zs.bi_list[-1] - if last_bi and last_bi.is_sure: - last_zs.is_sure = True - last_zs.set_end_bi(last_bi, last_bi.sure_time) - - zs = last_zs - if create_new_zs: - gg = max(bi1.high, bi2.high, bi3.high) - dd = min(bi1.low, bi2.low, bi3.low) - zs = ChanBIZS(bi1, len(bi_zs_list), zs_dir) - zs.set_zg(zg) - zs.set_zd(zd) - zs.set_gg(gg) - zs.set_dd(dd) - zs.is_sure = False - zs.bi_list = [bi1, bi2, bi3] - - # 离开后回抽扩展检查 - added_after_leave = [] - leave_index = start_idx + 4 - while leave_index < len(bi_list): - b = bi_list[leave_index] - if not b.is_sure: - break - if b.high >= zs.zd and b.low <= zs.zg: - added_after_leave.append(b.pre) - added_after_leave.append(b) - else: - break - leave_index += 2 - - if added_after_leave: - bis_for_zs = list(zs.bi_list) + list(added_after_leave) - bi_highs = [bi.high for bi in bis_for_zs] - bi_lows = [bi.low for bi in bis_for_zs] - zs.set_gg(max(bi_highs)) - zs.set_dd(min(bi_lows)) - zs.bi_list = bis_for_zs - bi = bis_for_zs[-1] - if bi.is_sure: - zs.set_end_bi(bi, bi.sure_time) - start_idx = start_idx + len(added_after_leave) - else: - if create_new_zs: - zs.set_end_bi(bi3, bi3.sure_time) - - if create_new_zs: - if last_zs: - last_zs.set_next(zs) - zs.set_pre(last_zs) - bi_zs_list.append(zs) - last_zs = zs - - start_idx += 4 - - # 最后一个中枢:根据 bi_list 最后一笔确认状态 - if last_zs: - last_zs.is_sure = bi_list[-1].is_sure - - if last_zs and not last_zs.is_sure: - if last_zs.bi_list and len(last_zs.bi_list) > 0: - last_bi_of_zs = last_zs.bi_list[-1] - last_bi_idx = -1 - for i, bi in enumerate(bi_list): - if bi == last_bi_of_zs: - last_bi_idx = i - break - - has_leave = False - if last_bi_idx >= 0 and last_bi_idx + 1 < len(bi_list): - for i in range(last_bi_idx + 1, len(bi_list)): - bi = bi_list[i] - if bi.is_sure: - leave = (bi.low > last_zs.zg and bi.high > last_zs.zg) or \ - (bi.high < last_zs.zd and bi.low < last_zs.zd) - if leave: - has_leave = True - break - - if has_leave: - if last_bi_of_zs.is_sure: - last_zs.set_end_bi(last_bi_of_zs, last_bi_of_zs.sure_time) - - return bi_zs_list - - def cal_bi_zs_list_pure(self, bi_list): - bi_zs_list = [] - if len(bi_list) < 3: - return bi_zs_list - - def get_zs_range(bis): - zg = min(bi.high for bi in bis) - zd = max(bi.low for bi in bis) - return zg, zd - - def is_bi_overlap_range(bi, zg, zd): - return bi.high >= zd and bi.low <= zg - - def check_zs_position_filter(last_zs, zg, zd, bis): - if last_zs is None: - return True - if zg <= last_zs.zd: - return bis[0].dir == Chan_BI_DIR.UP and bis[-1].dir == Chan_BI_DIR.UP - if zd >= last_zs.zg: - return bis[0].dir == Chan_BI_DIR.DOWN and bis[-1].dir == Chan_BI_DIR.DOWN - return True - - def set_zs_bi_list(zs, bis): - zs.bi_list = list(bis) - for bi in zs.bi_list: - bi.set_bi_zs(zs) - zs.set_gg(max(bi.high for bi in zs.bi_list)) - zs.set_dd(min(bi.low for bi in zs.bi_list)) - zs.classify_zs() - - last_zs = None - start_idx = 0 - while start_idx + 2 < len(bi_list): - bi1 = bi_list[start_idx] - bi2 = bi_list[start_idx + 1] - bi3 = bi_list[start_idx + 2] - - if not (bi1.is_sure and bi2.is_sure and bi3.is_sure): - start_idx += 1 - continue - - if not (bi1.dir != bi2.dir and bi1.dir == bi3.dir): - start_idx += 1 - continue - - zg, zd = get_zs_range([bi1, bi2, bi3]) - if zg <= zd: - start_idx += 1 - continue - - bis_for_zs = [bi1, bi2, bi3] - extend_idx = start_idx + 3 - while extend_idx + 1 < len(bi_list): - leave_bi = bi_list[extend_idx] - back_bi = bi_list[extend_idx + 1] - if not (leave_bi.is_sure and back_bi.is_sure): - break - if not is_bi_overlap_range(back_bi, zg, zd): - break - bis_for_zs.append(leave_bi) - bis_for_zs.append(back_bi) - extend_idx += 2 - - if not check_zs_position_filter(last_zs, zg, zd, bis_for_zs): - start_idx += 1 - continue - - zs_dir = Chan_ZS_DIR.UP if bi1.dir == Chan_BI_DIR.DOWN else Chan_ZS_DIR.DOWN - zs = ChanBIZS(bi1, len(bi_zs_list), zs_dir) - zs.set_zg(zg) - zs.set_zd(zd) - - set_zs_bi_list(zs, bis_for_zs) - zs.set_end_bi(bis_for_zs[-1], bis_for_zs[-1].sure_time) - - if last_zs: - last_zs.set_next(zs) - zs.set_pre(last_zs) - - bi_zs_list.append(zs) - last_zs = zs - start_idx = start_idx + len(bis_for_zs) - - # 与 cal_bi_zs_list 一致:最后一笔未确认时末中枢标为未完成;若其后已出现确认的离开笔,仍按离开前最后一笔确认中枢结束 - if last_zs: - last_zs.is_sure = bi_list[-1].is_sure - - if last_zs and not last_zs.is_sure: - if last_zs.bi_list and len(last_zs.bi_list) > 0: - last_bi_of_zs = last_zs.bi_list[-1] - last_bi_idx = -1 - for i, bi in enumerate(bi_list): - if bi == last_bi_of_zs: - last_bi_idx = i - break - - has_leave = False - if last_bi_idx >= 0 and last_bi_idx + 1 < len(bi_list): - for i in range(last_bi_idx + 1, len(bi_list)): - bi = bi_list[i] - if bi.is_sure: - leave = (bi.low > last_zs.zg and bi.high > last_zs.zg) or \ - (bi.high < last_zs.zd and bi.low < last_zs.zd) - if leave: - has_leave = True - break - - if has_leave: - if last_bi_of_zs.is_sure: - last_zs.set_end_bi(last_bi_of_zs, last_bi_of_zs.sure_time) - - return bi_zs_list - def find_all_bsp(self, bi_list, bi_zs_list): - """ - 笔中枢的三类买卖点识别 - - 三类买点:中枢形成后,一笔向上离开中枢(低点 > zg), - 随后回拉的一笔低点不跌回中枢(低点 >= zg),确认支撑有效。 - 三类卖点:中枢形成后,一笔向下离开中枢(高点 < zd), - 随后反弹的一笔高点不回到中枢(高点 <= zd),确认压力有效。 - - 参数: - bi_list: 笔列表 - bi_zs_list: 笔中枢列表(二维列表,每个seg内的中枢列表) - - 返回: - bsp_list: ChanBSP 列表,包含所有识别到的三类买卖点 - """ - bsp_list = [] - if len(bi_list) < 4 or len(bi_zs_list) == 0: - return bsp_list - - for zs in bi_zs_list: - if not zs.is_sure or len(zs.bi_list) < 3: - continue - #print(zs.start_time, zs.end_time, zs.dir, zs.is_sure, len(zs.bi_list)) - # 中枢结束后的第一笔(离开笔) - last_zs_bi = zs.bi_list[-1] - if last_zs_bi.dir == Chan_BI_DIR.UP: - if last_zs_bi.is_sure and last_zs_bi.end_klc.high <= zs.zg or (last_zs_bi.next and last_zs_bi.next.is_sure and last_zs_bi.next.end_klc.low < zs.zd): - leave_bi = last_zs_bi.next - else: - leave_bi = last_zs_bi - else: - if last_zs_bi.is_sure and last_zs_bi.end_klc.low >= zs.zd or (last_zs_bi.next and last_zs_bi.next.is_sure and last_zs_bi.next.end_klc.high > zs.zg): - leave_bi = last_zs_bi.next - else: - leave_bi = last_zs_bi - #print(zs.zg, zs.zd) - if leave_bi is None or not leave_bi.is_sure: - continue - if (zs.dir == Chan_ZS_DIR.UP and leave_bi.dir == Chan_BI_DIR.UP and leave_bi.end_klc.high < zs.zg and leave_bi.end_klc.high > zs.zd) or (zs.dir == Chan_ZS_DIR.DOWN and leave_bi.dir == Chan_BI_DIR.DOWN and leave_bi.end_klc.low < zs.zg and leave_bi.end_klc.low > zs.zd): - #print("--------------------", leave_bi.dir, leave_bi.end_klc.high, leave_bi.end_klc.low, zs.zg, zs.zd) - leave_bi = leave_bi.next - # 三类买点:向上离开中枢后回拉不破 zg - #print("Leave bi:", leave_bi.start_time, leave_bi.end_time, leave_bi.dir, leave_bi.is_sure, leave_bi.low, leave_bi.high) - if leave_bi.dir == Chan_BI_DIR.UP: - first_bsp_bi_div = self.check_bi_div(zs, leave_bi) - # 确认一类卖点:离开断能量小于进入段能量 - if first_bsp_bi_div: - bsp = ChanBSP( - leave_bi, len(bsp_list), - Chan_BSP_TYPE.S1, - Chan_BSP_DIR.SELL, - leave_bi.sure_time, - zs.index+1, zs, None - ) - leave_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.S1) - bsp_list.append(bsp) - # 回拉笔 - pullback_bi = leave_bi.next - #print(pullback_bi.start_klc.start_time, pullback_bi.dir, pullback_bi.is_sure, pullback_bi.low, pullback_bi.high) - if pullback_bi and pullback_bi.is_sure and pullback_bi.dir == Chan_BI_DIR.DOWN: - if pullback_bi.low >= zs.zg: - # 确认三类买点:回拉笔的低点不跌回中枢 - bsp = ChanBSP( - pullback_bi, len(bsp_list), - Chan_BSP_TYPE.B3, - Chan_BSP_DIR.BUY, - pullback_bi.sure_time, - zs.index+1, zs, None - ) - pullback_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B3) - bsp_list.append(bsp) - # 二类卖点 - if first_bsp_bi_div: - second_bsp_bi = pullback_bi.next - if second_bsp_bi and second_bsp_bi.is_sure and second_bsp_bi.end_klc.high < leave_bi.end_klc.high: - # 确认二类卖点:一类卖点后回拉不超过一类卖点高点 - bsp = ChanBSP( - second_bsp_bi, len(bsp_list), - Chan_BSP_TYPE.S2, - Chan_BSP_DIR.SELL, - second_bsp_bi.sure_time, - zs.index+1, zs, None - ) - second_bsp_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B2) - bsp_list.append(bsp) - # 三类卖点:向下离开中枢后反弹不破 zd - elif leave_bi.dir == Chan_BI_DIR.DOWN: - first_bsp_bi_div = self.check_bi_div(zs, leave_bi) - # 确认一类买点:离开段能量小于进入段 - if first_bsp_bi_div: - bsp = ChanBSP( - leave_bi, len(bsp_list), - Chan_BSP_TYPE.B1, - Chan_BSP_DIR.BUY, - leave_bi.sure_time, - zs.index+1, zs, None - ) - leave_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B1) - bsp_list.append(bsp) - # 反弹笔 - bounce_bi = leave_bi.next - #print(bounce_bi.start_klc.start_time, bounce_bi.dir, bounce_bi.is_sure, bounce_bi.low, bounce_bi.high) - if bounce_bi and bounce_bi.is_sure and bounce_bi.dir == Chan_BI_DIR.UP: - if bounce_bi.high <= zs.zd: - # 确认三类卖点:反弹笔的高点不回到中枢 - bsp = ChanBSP( - bounce_bi, len(bsp_list), - Chan_BSP_TYPE.S3, - Chan_BSP_DIR.SELL, - bounce_bi.sure_time, - zs.index+1, zs, None - ) - bounce_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.S3) - bsp_list.append(bsp) - # 二类卖点 - if first_bsp_bi_div: - second_bsp_bi = bounce_bi.next - if second_bsp_bi and second_bsp_bi.is_sure and second_bsp_bi.end_klc.low > leave_bi.end_klc.low: - # 确认二类买点:一类买点后回拉不超过一类卖点高点 - bsp = ChanBSP( - second_bsp_bi, len(bsp_list), - Chan_BSP_TYPE.B2, - Chan_BSP_DIR.BUY, - second_bsp_bi.sure_time, - zs.index+1, zs, None - ) - second_bsp_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B2) - bsp_list.append(bsp) - return bsp_list - def check_bi_div(self, zs, leave_bi): - enter_bi = zs.bi_list[0].pre - macdhist_div = 0 - if enter_bi and enter_bi.dir == leave_bi.dir: - macdhist_div = abs(leave_bi.macd_hist) - abs(enter_bi.macd_hist) - #print(enter_bi.end_time, leave_bi.end_time, macdhist_div < 0) - return macdhist_div < 0 - def find_first_bsp(self, bi_list, bi_zs_list): - """ - 笔中枢的一类买卖点识别 - - 一类买点:下跌趋势中,最后一个中枢完成后,向下离开中枢的笔创新低, - 但该笔与进入中枢前的最后一笔下跌形成底背驰(力度减弱), - 即趋势力竭的转折点。 - 一类卖点:上涨趋势中,最后一个中枢完成后,向上离开中枢的笔创新高, - 但该笔与进入中枢前的最后一笔上涨形成顶背驰(力度减弱), - 即趋势力竭的转折点。 - - 简化判断:中枢形成后,离开中枢的笔(突破笔)本身即为一类买卖点的触发笔。 - - 参数: - bi_list: 笔列表 - bi_zs_list: 笔中枢列表(扁平列表,每个元素是一个中枢对象) - - 返回: - bsp_list: ChanBSP 列表,包含所有识别到的一类买卖点 - """ - bsp_list = [] - if len(bi_list) < 4 or len(bi_zs_list) == 0: - return bsp_list - - for zs in bi_zs_list: - if not zs.is_sure or len(zs.bi_list) < 3: - continue - - # 找到中枢的最后一笔 - last_zs_bi = zs.bi_list[-1] - - # 确定离开笔:中枢最后一笔之后的第一笔 - if last_zs_bi.dir == Chan_BI_DIR.UP: - # 中枢最后一笔向上,如果没有真正离开中枢,取下一笔 - if last_zs_bi.is_sure and last_zs_bi.end_klc.high <= zs.zg: - leave_bi = last_zs_bi.next - else: - leave_bi = last_zs_bi - else: - # 中枢最后一笔向下,如果没有真正离开中枢,取下一笔 - if last_zs_bi.is_sure and last_zs_bi.end_klc.low >= zs.zd: - leave_bi = last_zs_bi.next - else: - leave_bi = last_zs_bi - - if leave_bi is None or not leave_bi.is_sure: - continue - - # 一类买点:向下离开中枢(leave_bi向下,低点 < zd),趋势力竭 - if leave_bi.dir == Chan_BI_DIR.DOWN and leave_bi.low < zs.zd: - # 背驰判断:比较离开笔与中枢内最后一笔同向笔的MACD柱状累积面积 - # 缠论原文:两段同向走势的MACD柱状面积比较,面积缩小即为背驰 - compare_bi = None - for bi in reversed(zs.bi_list): - if bi.dir == Chan_BI_DIR.DOWN and bi is not leave_bi: - compare_bi = bi - break - - is_divergence = False - if compare_bi: - # 笔的macd_hist是该笔内所有KLU的macdhist累积面积 - leave_macd_area = abs(leave_bi.macd_hist) - compare_macd_area = abs(compare_bi.macd_hist) - - # 价格创新低但MACD面积缩小 = 底背驰 - if leave_bi.low <= compare_bi.low and leave_macd_area < compare_macd_area: - is_divergence = True - # 即使没创新低,MACD面积明显缩小也算背驰 - elif leave_macd_area < compare_macd_area * 0.5: - is_divergence = True - else: - # 没有对比笔时,只要离开中枢就算一类买点 - is_divergence = True - - if is_divergence: - bsp = ChanBSP( - leave_bi, len(bsp_list), - Chan_BSP_TYPE.T1, - Chan_BSP_DIR.BUY, - leave_bi.sure_time, - 1, zs, None - ) - bsp_list.append(bsp) - - # 一类卖点:向上离开中枢(leave_bi向上,高点 > zg),趋势力竭 - elif leave_bi.dir == Chan_BI_DIR.UP and leave_bi.high > zs.zg: - # 背驰判断:比较离开笔与中枢内最后一笔同向笔的MACD柱状累积面积 - compare_bi = None - for bi in reversed(zs.bi_list): - if bi.dir == Chan_BI_DIR.UP and bi is not leave_bi: - compare_bi = bi - break - - is_divergence = False - if compare_bi: - leave_macd_area = abs(leave_bi.macd_hist) - compare_macd_area = abs(compare_bi.macd_hist) - - # 价格创新高但MACD面积缩小 = 顶背驰 - if leave_bi.high >= compare_bi.high and leave_macd_area < compare_macd_area: - is_divergence = True - # 即使没创新高,MACD面积明显缩小也算背驰 - elif leave_macd_area < compare_macd_area * 0.5: - is_divergence = True - else: - is_divergence = True - - if is_divergence: - bsp = ChanBSP( - leave_bi, len(bsp_list), - Chan_BSP_TYPE.T1, - Chan_BSP_DIR.SELL, - leave_bi.sure_time, - 1, zs, None - ) - bsp_list.append(bsp) - - return bsp_list - - def find_second_bsp(self, bi_list, first_bsp_list): - """ - 笔中枢的二类买卖点识别 - - 二类买点:一类买点出现后,价格向上反弹一笔,再回落一笔, - 回落笔的低点不跌破一类买点的低点,确认底部成立。 - 二类卖点:一类卖点出现后,价格向下回落一笔,再反弹一笔, - 反弹笔的高点不超过一类卖点的高点,确认顶部成立。 - - 参数: - bi_list: 笔列表 - first_bsp_list: 一类买卖点列表(find_first_bsp 的返回值) - - 返回: - bsp_list: ChanBSP 列表,包含所有识别到的二类买卖点 - """ - bsp_list = [] - if not first_bsp_list or len(bi_list) < 4: - return bsp_list - - for first_bsp in first_bsp_list: - trigger_bi = first_bsp.bi # 一类买卖点的触发笔 - - if first_bsp.dir == Chan_BSP_DIR.BUY: - # 一买之后:trigger_bi 向下 -> 反弹笔(向上) -> 回落笔(向下) - # 回落笔的低点 > trigger_bi 的低点 => 二类买点 - bounce_bi = trigger_bi.next # 反弹笔(向上) - if bounce_bi and bounce_bi.is_sure and bounce_bi.dir == Chan_BI_DIR.UP: - pullback_bi = bounce_bi.next # 回落笔(向下) - if pullback_bi and pullback_bi.is_sure and pullback_bi.dir == Chan_BI_DIR.DOWN: - if pullback_bi.low > trigger_bi.low: - bsp = ChanBSP( - pullback_bi, len(bsp_list), - Chan_BSP_TYPE.T2, - Chan_BSP_DIR.BUY, - pullback_bi.sure_time, - 1, first_bsp.zs, None - ) - bsp_list.append(bsp) - - elif first_bsp.dir == Chan_BSP_DIR.SELL: - # 一卖之后:trigger_bi 向上 -> 回落笔(向下) -> 反弹笔(向上) - # 反弹笔的高点 < trigger_bi 的高点 => 二类卖点 - drop_bi = trigger_bi.next # 回落笔(向下) - if drop_bi and drop_bi.is_sure and drop_bi.dir == Chan_BI_DIR.DOWN: - bounce_bi = drop_bi.next # 反弹笔(向上) - if bounce_bi and bounce_bi.is_sure and bounce_bi.dir == Chan_BI_DIR.UP: - if bounce_bi.high < trigger_bi.high: - bsp = ChanBSP( - bounce_bi, len(bsp_list), - Chan_BSP_TYPE.T2, - Chan_BSP_DIR.SELL, - bounce_bi.sure_time, - 1, first_bsp.zs, None - ) - bsp_list.append(bsp) - - return bsp_list - def calculate_seg_zs(self, seg_list): - return self.get_seg_zs_list(seg_list) - def get_seg_zs_list(self, seg_list): - """ - 根据缠论线段中枢定义计算中枢 - 从第4根线段开始(索引3),每3根线段为一组检查 - 上涨中枢:后中枢 zd > 前中枢 zg(不重叠上移) - 下跌中枢:后中枢 zg < 前中枢 zd(不重叠下移) - 盘整/扩张:后中枢与前中枢整体区间(GG/DD)有交集 - 中枢可按两段一组继续扩展到5根、7根... - """ - zs_list = [] - if len(seg_list) < 3: - return zs_list - - last_zs = None - - # 从第4根线段开始(索引3),每3根为一组 - start_idx = 3 - - while start_idx < len(seg_list): - # 取连续3个线段 - if start_idx + 2 >= len(seg_list): - break - - seg1 = seg_list[start_idx] - seg2 = seg_list[start_idx + 1] - seg3 = seg_list[start_idx + 2] - - # 三个线段都必须是已确认的 - if not (seg1.is_sure and seg2.is_sure and seg3.is_sure): - start_idx += 1 - continue - - # 计算这3个线段的中枢区间 - zg = min(seg1.high, seg2.high, seg3.high) - zd = max(seg1.low, seg2.low, seg3.low) - - if zg <= zd: - start_idx += 1 - #print(seg1.start_bi.start_klc.end_time, "not valid", zg, zd) - continue - - # 判断中枢类型(按注释定义) - # 上涨中枢:后中枢 zd > 前中枢 zg(不重叠上移) - # 下跌中枢:后中枢 zg < 前中枢 zd(不重叠下移) - # 盘整/扩张:后中枢与前中枢区间有交集 - if last_zs is None: - # 第一个中枢仅按线段形态判定方向 - if seg1.dir == Chan_SEG_DIR.DOWN: - # 下跌+上涨+下跌,对应上涨中枢 - zs_dir = Chan_ZS_DIR.UP - valid = (seg2.dir == Chan_SEG_DIR.UP and seg3.dir == Chan_SEG_DIR.DOWN) - else: - # 上涨+下跌+上涨,对应下跌中枢 - zs_dir = Chan_ZS_DIR.DOWN - valid = (seg2.dir == Chan_SEG_DIR.DOWN and seg3.dir == Chan_SEG_DIR.UP) - else: - is_up_zs = zd > last_zs.zg - is_down_zs = zg < last_zs.zd - - if is_up_zs: - # 不重叠上移 - zs_dir = Chan_ZS_DIR.UP - valid = (seg1.dir == Chan_SEG_DIR.DOWN and seg2.dir == Chan_SEG_DIR.UP and seg3.dir == Chan_SEG_DIR.DOWN) - elif is_down_zs: - # 不重叠下移 - zs_dir = Chan_ZS_DIR.DOWN - valid = (seg1.dir == Chan_SEG_DIR.UP and seg2.dir == Chan_SEG_DIR.DOWN and seg3.dir == Chan_SEG_DIR.UP) - create_new_zs = False - # 验证是否有效 - if not valid: - # 如果新中枢和前一个中枢的中枢区间有重叠,不行成新中枢需要合并两个中枢 - is_in_last_zs = (zd > last_zs.zd and zd < last_zs.zg) or (zg < last_zs.zg and zg > last_zs.zd) or (zg > last_zs.zg and zd < last_zs.zd) or (zg < last_zs.zg and zd > last_zs.zd) - if is_in_last_zs: - #print(seg1.start_time, "New zs is in last zs, not valid") - last_zs.extend_zs(seg_list[last_zs.seg_list[-1].index:(seg3.index + 1)]) - create_new_zs = False - else: - start_idx += 1 - continue - else: - create_new_zs = True - if last_zs and create_new_zs: - last_seg = last_zs.seg_list[-1] - last_bi = last_seg.end_bi - if last_bi: - last_zs.is_sure = True - last_zs.set_end_klc(last_bi.end_klc, last_bi.sure_time, 0, last_seg) - last_zs.set_end_seg(last_seg) - zs = last_zs - if create_new_zs: - # 创建新中枢 - gg = max(seg1.high, seg2.high, seg3.high) - dd = min(seg1.low, seg2.low, seg3.low) - - zs = ChanZS(seg1, len(zs_list), zs_dir) - zs.set_zg(zg) - zs.set_zd(zd) - zs.set_gg(gg) - zs.set_dd(dd) - zs.is_sure = False - zs.seg_list = [seg1, seg2, seg3] - # 若第二线段与 [zd,zg] 重叠(如离开后回抽回到前中枢)则并入扩展 - added_after_leave = [] - leave_index = start_idx + 4 - is_break = False - while leave_index < len(seg_list): - s = seg_list[leave_index] - if not s.is_sure: - break - sh = max(s.start_bi.high, s.end_bi.high) if s.end_bi else s.start_bi.high - sl = min(s.start_bi.low, s.end_bi.low) if s.end_bi else s.start_bi.low - if sh >= zs.zd and sl <= zs.zg: - added_after_leave.append(s.pre) - added_after_leave.append(s) - leave_index += 2 - else: - next_seg = s.next - if next_seg and next_seg.is_sure: - if next_seg.dir == Chan_SEG_DIR.UP: - if next_seg.high <= zs.zg and next_seg.low >= zs.zd: - leave_index += 2 - continue - else: - is_break = True - else: - if next_seg.low >= zs.zd and next_seg.low <= zs.zg: - leave_index += 2 - continue - else: - is_break = True - else: - break - if is_break: - break - if added_after_leave: - #print(len(added_after_leave)) - segs_for_zs = list(zs.seg_list) + list(added_after_leave) - seg_highs = [s.high for s in segs_for_zs] - seg_lows = [s.low for s in segs_for_zs] - zs.set_gg(max(seg_highs)) - zs.set_dd(min(seg_lows)) - zs.seg_list = segs_for_zs - seg = segs_for_zs[-1] - #if seg.end_bi: - #zs.set_end_klc(seg.end_bi.end_klc, seg.sure_time, 0, seg) - #zs.set_end_seg(seg) - #zs.is_sure = True - start_idx = start_idx + len(added_after_leave) - if last_zs and last_zs.index != zs.index: - last_zs.set_next(zs) - zs.set_pre(last_zs) - - zs_list.append(zs) - last_zs = zs - - # 移动到下一组 - start_idx += 4 - if last_zs: - last_zs.is_sure = seg_list[-1].is_sure - """ - # 处理最后一个未确认的中枢 - 不自动扩展,保持未完成状态 - if last_zs and not last_zs.is_sure: - # 获取中枢最后一个线段的索引 - if last_zs.seg_list and len(last_zs.seg_list) > 0: - last_seg_of_zs = last_zs.seg_list[-1] - # 找到这个线段在seg_list中的索引 - last_seg_idx = -1 - for i, seg in enumerate(seg_list): - if seg == last_seg_of_zs: - last_seg_idx = i - break - - # 从中枢最后一个线段之后检查是否有离开 - has_leave = False - if last_seg_idx >= 0 and last_seg_idx + 1 < len(seg_list): - for i in range(last_seg_idx + 1, len(seg_list)): - seg = seg_list[i] - if seg.is_sure: - # 检查是否离开中枢 - leave = (seg.low > last_zs.zg and seg.high > last_zs.zg) or \ - (seg.high < last_zs.zd and seg.low < last_zs.zd) - if leave: - has_leave = True - break - - if not has_leave: - # 没有离开,保持未完成状态 - pass - else: - # 有离开,确认中枢 - if last_seg_of_zs.end_bi: - #print(last_seg_of_zs.start_time, "last_seg_of_zs.end_time", last_seg_of_zs.end_time) - last_zs.set_end_klc(last_seg_of_zs.end_bi.end_klc, last_seg_of_zs.sure_time, 0, last_seg_of_zs) - last_zs.set_end_seg(last_seg_of_zs) - last_zs.is_sure = True - """ - return zs_list - - def get_big_zs_list(self, zs_list): - """ - 中枢扩张:将区间重叠的连续中枢合并为大级别中枢,便于显示更大级别的震荡区间。 - 重叠定义:两中枢 [zd,zg] 有交集,即 (zs_i.zg >= zs_j.zd and zs_i.zd <= zs_j.zg)。 - """ - big_list = [] - if len(zs_list) < 2: - return big_list - i = 0 - while i < len(zs_list): - group = [zs_list[i]] - j = i + 1 - while j < len(zs_list): - cur = zs_list[j] - # 与当前组内任一中枢有重叠即算扩张(通常只需与组内最后一个比) - last_in_group = group[-1] - overlap = (last_in_group.zg >= cur.zd and last_in_group.zd <= cur.zg) - if overlap: - group.append(cur) - j += 1 - else: - break - if len(group) >= 2: - big = ChanZS_Big(group) - big.index = len(big_list) - big_list.append(big) - i = j if len(group) >= 2 else i + 1 - return big_list - - def get_klu_list(self, dataframe): - klu_list = self.get_kl_data(dataframe) - #klu_list = self.cal_klu_pattern(klu_list) - return klu_list - def cal_klu_pattern(self, klu_list): - """ - 计算裸K的pattern - 识别反转形态 - """ - if not klu_list or len(klu_list) < 3: - return klu_list - - for i, klu in enumerate(klu_list): - # 单根K线反转模式识别 - self._detect_single_reversal_pattern(klu) - - # 双根K线形态识别 - if i >= 1: - self._detect_double_pattern(klu_list[i-1], klu) - - # 三根K线形态识别 - if i >= 2: - self._detect_triple_pattern(klu_list[i-2], klu_list[i-1], klu) - - #if klu.pattern != Chan_KLU_PATTERN.UNKNOWN: - #print(klu.time, klu.pattern, klu.lower_shadow_ratio, klu.upper_shadow_ratio, klu.body_ratio, klu.lower_shadow_ratio/klu.body_ratio, klu.upper_shadow_ratio/klu.body_ratio) - return klu_list - - def _detect_single_reversal_pattern(self, klu): - """检测单根K线反转模式""" - body = abs(klu.close - klu.open) - upper_shadow = klu.high - max(klu.close, klu.open) - lower_shadow = min(klu.close, klu.open) - klu.low - total_range = klu.high - klu.low - - # 避免除零 - if total_range == 0: - return - - body_ratio = body / total_range - upper_ratio = upper_shadow / total_range - lower_ratio = lower_shadow / total_range - #print(klu.time, upper_ratio, lower_ratio, body_ratio, upper_ratio/body_ratio, lower_ratio/body_ratio) - # 避免body_ratio为0时的除零错误 - if body_ratio == 0: - return - # 锤子线/上吊线 - 反转信号 - if lower_ratio / body_ratio >= 2: - # 锤子线:底部反转,需要前面一段 - if klu.close > klu.open and klu.pre: - klu.set_pattern(Chan_KLU_PATTERN.HAMMER) # 底部反转 - # 上吊线:顶部反转,需要前一根是上涨趋势 - elif klu.close < klu.open and klu.pre: - klu.set_pattern(Chan_KLU_PATTERN.HANGING_MAN) # 顶部反转 - - # 倒锤子线/射击之星 - 反转信号 - elif upper_ratio / body_ratio >= 2: - # 倒锤子线:底部反转,需要前一根是下跌趋势 - if klu.close > klu.open and klu.pre: - klu.set_pattern(Chan_KLU_PATTERN.INVERTED_HAMMER) # 底部反转 - # 射击之星:顶部反转,需要前一根是上涨趋势 - elif klu.close < klu.open and klu.pre: - klu.set_pattern(Chan_KLU_PATTERN.SHOOTING_STAR) # 顶部反转 - - # 十字星 - 反转信号 - elif body_ratio <= 0.1: - if upper_ratio > 0.4 and lower_ratio > 0.4: - klu.set_pattern(Chan_KLU_PATTERN.LONG_LEGGED_DOJI) # 强烈反转信号 - elif upper_ratio > 0.4 and lower_ratio <= 0.1: - # 墓碑十字星:顶部反转,需要前一根是上涨趋势 - if klu.pre and klu.pre.close > klu.pre.open: - klu.set_pattern(Chan_KLU_PATTERN.GRAVESTONE_DOJI) # 顶部反转 - elif lower_ratio > 0.4 and upper_ratio <= 0.1: - # 蜻蜓十字星:底部反转,需要前一根是下跌趋势 - if klu.pre and klu.pre.close < klu.pre.open: - klu.set_pattern(Chan_KLU_PATTERN.DRAGONFLY_DOJI) # 底部反转 - else: - klu.set_pattern(Chan_KLU_PATTERN.DOJI) # 一般反转信号 - - def _detect_double_pattern(self, prev_klu, curr_klu): - """检测两根K线形成的形态 - 包括:吞没形态(看涨/看跌)、乌云盖顶、曙光初现 - """ - # 如果前一根K线已经有形态,不再识别双K线形态 - if prev_klu.pattern != Chan_KLU_PATTERN.UNKNOWN: - return - - # 计算K线实体 - prev_body = abs(prev_klu.close - prev_klu.open) - curr_body = abs(curr_klu.close - curr_klu.open) - - # 判断K线颜色(阴阳) - prev_bullish = prev_klu.close > prev_klu.open - curr_bullish = curr_klu.close > curr_klu.open - - # 检查是否存在长期趋势(至少需要5根K线的趋势) - def check_long_trend(klu, bullish_trend=True, min_bars=5): - """检查是否存在长期趋势 - bullish_trend=True: 检查上涨趋势 - bullish_trend=False: 检查下跌趋势 - min_bars: 最少需要多少根K线形成趋势 - """ - if not klu or not klu.pre: - return False - return True - - # 使用EMA指标判断长期趋势 - if klu.ema52 > 0: - if bullish_trend and klu.close < klu.ema52: - return False - if not bullish_trend and klu.close > klu.ema52: - return False - - # 检查连续的K线方向 - count = 0 - current = klu.pre - - while current and count < min_bars: - if not current.pre: - break - - if bullish_trend: - # 上涨趋势:当前收盘价高于前一根收盘价 - if current.close <= current.pre.close: - break - else: - # 下跌趋势:当前收盘价低于前一根收盘价 - if current.close >= current.pre.close: - break - - count += 1 - current = current.pre - - return count >= min_bars - - # 1. 看涨吞没形态:前阴后阳,后者完全吞没前者 - # 要求前面有明显的下跌趋势 - if not prev_bullish and curr_bullish and \ - abs(curr_klu.open - prev_klu.close) < 10 and \ - curr_klu.close > prev_klu.open and \ - check_long_trend(prev_klu, bullish_trend=False, min_bars=5): - curr_klu.set_pattern(Chan_KLU_PATTERN.BULLISH_ENGULFING) - return - - # 2. 看跌吞没形态:前阳后阴,后者完全吞没前者 - # 要求前面有明显的上涨趋势 - if prev_bullish and not curr_bullish and \ - abs(curr_klu.open - prev_klu.close) < 10 and \ - curr_klu.close < prev_klu.open and \ - check_long_trend(prev_klu, bullish_trend=True, min_bars=5): - curr_klu.set_pattern(Chan_KLU_PATTERN.BEARISH_ENGULFING) - return - - # 3. 乌云盖顶:前阳后阴,后者开盘价高于前者最高价,收盘价在前者实体中部以下 - # 要求前面有明显的上涨趋势 - if prev_bullish and not curr_bullish and \ - curr_klu.open > prev_klu.high and \ - curr_klu.close < (prev_klu.open + prev_klu.close) / 2 and \ - curr_klu.close > prev_klu.open and \ - check_long_trend(prev_klu, bullish_trend=True, min_bars=5): - curr_klu.set_pattern(Chan_KLU_PATTERN.DARK_CLOUD_COVER) - return - - # 4. 曙光初现:前阴后阳,后者开盘价低于前者最低价,收盘价在前者实体中部以上 - # 要求前面有明显的下跌趋势 - if not prev_bullish and curr_bullish and \ - curr_klu.open < prev_klu.low and \ - curr_klu.close > (prev_klu.open + prev_klu.close) / 2 and \ - curr_klu.close < prev_klu.open and \ - check_long_trend(prev_klu, bullish_trend=False, min_bars=5): - curr_klu.set_pattern(Chan_KLU_PATTERN.PIERCING_LINE) - return - - # 平顶和平底移至三根K线形态中判断 - - def _detect_triple_pattern(self, first_klu, second_klu, third_klu): - """检测三根K线形成的形态 - 包括:早晨之星、黄昏之星、平顶、平底 - """ - # 如果前两根K线已经有形态,不再识别三K线形态 - if first_klu.pattern != Chan_KLU_PATTERN.UNKNOWN or \ - second_klu.pattern != Chan_KLU_PATTERN.UNKNOWN: - return - - # 判断K线颜色(阴阳) - first_bullish = first_klu.close > first_klu.open - second_bullish = second_klu.close > second_klu.open - third_bullish = third_klu.close > third_klu.open - - # 计算实体大小 - first_body = abs(first_klu.close - first_klu.open) - second_body = abs(second_klu.close - second_klu.open) - third_body = abs(third_klu.close - third_klu.open) - - # 检查是否存在长期趋势(至少需要5根K线的趋势) - def check_long_trend(klu, bullish_trend=True, min_bars=5): - """检查是否存在长期趋势 - bullish_trend=True: 检查上涨趋势 - bullish_trend=False: 检查下跌趋势 - min_bars: 最少需要多少根K线形成趋势 - """ - if not klu or not klu.pre: - return False - - # 使用EMA指标判断长期趋势 - if klu.ema52 > 0: - if bullish_trend and klu.close < klu.ema52: - return False - if not bullish_trend and klu.close > klu.ema52: - return False - - # 检查连续的K线方向 - count = 0 - current = klu.pre - - while current and count < min_bars: - if not current.pre: - break - - if bullish_trend: - # 上涨趋势:当前收盘价高于前一根收盘价 - if current.close <= current.pre.close: - break - else: - # 下跌趋势:当前收盘价低于前一根收盘价 - if current.close >= current.pre.close: - break - - count += 1 - current = current.pre - - return count >= min_bars - - # 1. 早晨之星:第一根阴线,第二根十字星或小实体,第三根阳线 - # 要求前面有明显的下跌趋势 - if not first_bullish and third_bullish and \ - second_body < first_body * 0.3 and \ - third_body > first_body * 0.5 and \ - max(second_klu.open, second_klu.close) < first_klu.close and \ - min(second_klu.open, second_klu.close) < third_klu.open and \ - third_klu.close > (first_klu.open + first_klu.close) / 2 and \ - check_long_trend(first_klu, bullish_trend=False, min_bars=7): - third_klu.set_pattern(Chan_KLU_PATTERN.MORNING_STAR) - return - - # 2. 黄昏之星:第一根阳线,第二根十字星或小实体,第三根阴线 - # 要求前面有明显的上涨趋势 - if first_bullish and not third_bullish and \ - second_body < first_body * 0.3 and \ - third_body > first_body * 0.5 and \ - min(second_klu.open, second_klu.close) > first_klu.close and \ - max(second_klu.open, second_klu.close) > third_klu.open and \ - third_klu.close < (first_klu.open + first_klu.close) / 2 and \ - check_long_trend(first_klu, bullish_trend=True, min_bars=7): - third_klu.set_pattern(Chan_KLU_PATTERN.EVENING_STAR) - return - - # 3. 平顶:三根K线的最高点几乎相同(上升趋势中更有意义) - # 要求前面有明显的上涨趋势 - if (abs(first_klu.high - second_klu.high) / first_klu.high < 0.0002 and - abs(second_klu.high - third_klu.high) / second_klu.high < 0.0002 and - check_long_trend(first_klu, bullish_trend=True, min_bars=7)): - # 额外确认:价格接近阻力位或关键技术指标 - is_near_resistance = False - - # 检查是否接近EMA52阻力位 - if first_klu.ema52 > 0: - resistance_level = first_klu.ema52 - if abs(first_klu.high - resistance_level) / resistance_level < 0.01: - is_near_resistance = True - - # 检查是否有成交量确认(成交量减少表示上涨动能减弱) - volume_confirmation = False - if (first_klu.volume > 0 and second_klu.volume > 0 and third_klu.volume > 0 and - third_klu.volume < second_klu.volume and second_klu.volume < first_klu.volume): - volume_confirmation = True - - if is_near_resistance or volume_confirmation: - third_klu.set_pattern(Chan_KLU_PATTERN.TWEEZER_TOP) - return - - # 4. 平底:三根K线的最低点几乎相同(下降趋势中更有意义) - # 要求前面有明显的下跌趋势 - if (abs(first_klu.low - second_klu.low) / first_klu.low < 0.0002 and - abs(second_klu.low - third_klu.low) / second_klu.low < 0.0002 and - check_long_trend(first_klu, bullish_trend=False, min_bars=7)): - # 额外确认:价格接近支撑位或关键技术指标 - is_near_support = False - - # 检查是否接近EMA52支撑位 - if first_klu.ema52 > 0: - support_level = first_klu.ema52 - if abs(first_klu.low - support_level) / support_level < 0.01: - is_near_support = True - - # 检查是否有成交量确认(成交量减少表示下跌动能减弱) - volume_confirmation = False - if (first_klu.volume > 0 and second_klu.volume > 0 and third_klu.volume > 0 and - third_klu.volume < second_klu.volume and second_klu.volume < first_klu.volume): - volume_confirmation = True - - if is_near_support or volume_confirmation: - third_klu.set_pattern(Chan_KLU_PATTERN.TWEEZER_BOTTOM) - return - - def get_decimal(self, value): - return Decimal("{:.2f}".format(value)) \ No newline at end of file +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.pipeline.TF_DF") +sys.modules[__name__] = _impl diff --git a/chan/__init__.py b/chan/__init__.py new file mode 100644 index 0000000..405870c --- /dev/null +++ b/chan/__init__.py @@ -0,0 +1,6 @@ +"""缠论引擎包:core(结构)/ indicators(指标)/ analysis(接合)/ pipeline(编排)。""" + +from .pipeline.ChanLun import ChanLun +from .pipeline.TF_DF import TF_DF + +__all__ = ["ChanLun", "TF_DF"] diff --git a/chan/analysis/ChanHeng.py b/chan/analysis/ChanHeng.py new file mode 100644 index 0000000..973837a --- /dev/null +++ b/chan/analysis/ChanHeng.py @@ -0,0 +1,414 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +""" +使用 ccxt 获取币安交易所所有 `*/USDT` 交易对最新 100 根 1 小时 K 线数据,并筛选出长期横盘的币种。 + +横盘判定基于以下三项指标(均可通过命令行参数调整): +1. 价格振幅占均价的比例(默认 ≤ 5%) +2. 收盘价线性回归斜率占均价的比例(默认 ≤ 0.05%) +3. 收盘价标准差占均价的比例(默认 ≤ 1.5%) + +满足以上全部条件的交易对会被视为长期横盘。 +""" + +import argparse +import csv +import logging +import math +import statistics +import sys +import time +from dataclasses import dataclass +from typing import Iterable, List, Optional, Sequence + +import ccxt + +# python ChanHeng.py --range-threshold 5 --slope-threshold 5 --std-threshold 0.015 + +DEFAULT_LIMIT = 100 +DEFAULT_TIMEFRAME = "1h" +STABLECOINS = { + "USDT", + "USDC", + "BUSD", + "TUSD", + "USDP", + "DAI", + "FDUSD", + "SUSD", + "UST", + "USTC", + "EUR", + "TRY", + "BFUSD", + "USDE", + "XUSD", + "USD1", + "XUSD" +} + + +@dataclass +class SidewaysMetrics: + symbol: str + price_range_pct: float + slope_pct: float + std_pct: float + mean_close: float + last_close: float + data_points: int + + +def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="筛选币安长期横盘币种(默认 500 根 1 小时 K 线)" + ) + parser.add_argument( + "--timeframe", + default=DEFAULT_TIMEFRAME, + help="K 线周期(默认:1h)", + ) + parser.add_argument( + "--limit", + type=int, + default=DEFAULT_LIMIT, + help="每个交易对获取的 K 线数量(默认:500)", + ) + parser.add_argument( + "--range-threshold", + type=float, + default=0.05, + help="最大价格振幅占均价比例阈值(默认:0.05,表示 5%%)", + ) + parser.add_argument( + "--slope-threshold", + type=float, + default=0.0005, + help="线性回归斜率占均价比例阈值(默认:0.0005,约 0.05%%)", + ) + parser.add_argument( + "--std-threshold", + type=float, + default=0.015, + help="标准差占均价比例阈值(默认:0.015,表示 1.5%%)", + ) + parser.add_argument( + "--quote", + action="append", + default=[], + help="只保留指定计价货币的交易对,可重复指定(示例:--quote USDT --quote FDUSD)", + ) + parser.add_argument( + "--symbol", + action="append", + default=[], + help="仅检测指定交易对,可重复(不指定则遍历所有符合条件的现货交易对)", + ) + parser.add_argument( + "--max-symbols", + type=int, + default=None, + help="限制最多检测的交易对数量(用于调试)", + ) + parser.add_argument( + "--sleep", + type=float, + default=0.35, + help="请求失败后的基础重试等待秒数(默认:0.35)", + ) + parser.add_argument( + "--retries", + type=int, + default=3, + help="单个交易对请求失败后的最大重试次数(默认:3)", + ) + parser.add_argument( + "--include-inactive", + action="store_true", + help="包含已下架/不可交易的交易对(默认不包含)", + ) + parser.add_argument( + "--export", + type=str, + default=None, + help="将筛选结果导出为 CSV 文件的路径", + ) + parser.add_argument( + "--verbose", + action="store_true", + help="输出更详细的日志信息", + ) + return parser.parse_args(argv) + + +def setup_logging(verbose: bool) -> None: + level = logging.DEBUG if verbose else logging.INFO + logging.basicConfig( + level=level, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + +def create_exchange() -> ccxt.binance: + exchange = ccxt.binance({"enableRateLimit": True}) + exchange.options["defaultType"] = "spot" + return exchange + + +def iter_target_symbols( + exchange: ccxt.binance, + quotes: Sequence[str], + includes: Sequence[str], + include_inactive: bool, +) -> List[str]: + markets = exchange.load_markets() + filtered = [] + + quote_set = {quote.upper() for quote in quotes} + include_set = {sym.upper() for sym in includes} + + for symbol, meta in markets.items(): + if not meta.get("spot", False): + continue + if not include_inactive and meta.get("active") is False: + continue + + normalized_symbol = symbol.upper() + + if include_set and normalized_symbol not in include_set: + continue + + parts = symbol.split("/") + if len(parts) != 2: + continue + + base_asset, quote_asset = parts[0].upper(), parts[1].upper() + + target_quote = quote_set or {"USDT"} + if quote_asset not in target_quote: + continue + + if base_asset in STABLECOINS: + continue + + filtered.append(symbol) + + filtered.sort() + logging.info( + "已筛选 %s 个目标交易对(quote 过滤:%s,专门列表:%s)", + len(filtered), + ",".join(sorted(quote_set or {"USDT"})), + ",".join(sorted(include_set)) or "无", + ) + return filtered + + +def fetch_ohlcv_with_retry( + exchange: ccxt.binance, + symbol: str, + timeframe: str, + limit: int, + retries: int, + base_sleep: float, +) -> List[List[float]]: + attempt = 0 + while True: + try: + return exchange.fetch_ohlcv(symbol, timeframe=timeframe, limit=limit) + except ccxt.RateLimitExceeded as exc: + wait_time = max(exchange.rateLimit / 1000.0 if exchange.rateLimit else 0, base_sleep) + logging.debug("触发限频,等待 %.2f 秒后重试 %s:%s", wait_time, symbol, exc) + time.sleep(wait_time) + except (ccxt.NetworkError, ccxt.ExchangeError) as exc: + attempt += 1 + if attempt > retries: + logging.warning("多次获取失败,跳过 %s:%s", symbol, exc) + return [] + wait_time = base_sleep * attempt + logging.debug("请求失败,等待 %.2f 秒后重试 %s(第 %d 次):%s", wait_time, symbol, attempt, exc) + time.sleep(wait_time) + + +def linear_regression_slope(values: Sequence[float]) -> float: + n = len(values) + if n < 2: + return 0.0 + mean_x = (n - 1) / 2.0 + mean_y = sum(values) / n + numerator = 0.0 + denominator = 0.0 + for idx, value in enumerate(values): + dx = idx - mean_x + numerator += dx * (value - mean_y) + denominator += dx * dx + + if denominator == 0: + return 0.0 + return numerator / denominator + + +def compute_sideways_metrics(closes: Sequence[float], symbol: str) -> Optional[SidewaysMetrics]: + if not closes: + return None + + mean_close = sum(closes) / len(closes) + if math.isclose(mean_close, 0.0): + return None + + max_close = max(closes) + min_close = min(closes) + price_range_pct = (max_close - min_close) / mean_close + + slope = linear_regression_slope(closes) + slope_pct = slope / mean_close + + std_dev = statistics.pstdev(closes) if len(closes) > 1 else 0.0 + std_pct = std_dev / mean_close + + return SidewaysMetrics( + symbol=symbol, + price_range_pct=price_range_pct, + slope_pct=slope_pct, + std_pct=std_pct, + mean_close=mean_close, + last_close=closes[-1], + data_points=len(closes), + ) + + +def is_sideways(metrics: SidewaysMetrics, range_threshold: float, slope_threshold: float, std_threshold: float) -> bool: + return ( + metrics.price_range_pct <= range_threshold + and abs(metrics.slope_pct) <= slope_threshold + and metrics.std_pct <= std_threshold + ) + + +def export_results(path: str, results: Sequence[SidewaysMetrics]) -> None: + fieldnames = [ + "symbol", + "price_range_pct", + "slope_pct", + "std_pct", + "mean_close", + "last_close", + "data_points", + ] + with open(path, "w", newline="", encoding="utf-8") as fp: + writer = csv.DictWriter(fp, fieldnames=fieldnames) + writer.writeheader() + for item in results: + writer.writerow( + { + "symbol": item.symbol, + "price_range_pct": f"{item.price_range_pct:.6f}", + "slope_pct": f"{item.slope_pct:.6f}", + "std_pct": f"{item.std_pct:.6f}", + "mean_close": f"{item.mean_close:.8f}", + "last_close": f"{item.last_close:.8f}", + "data_points": item.data_points, + } + ) + logging.info("结果已导出至 %s", path) + + +def run(argv: Optional[Sequence[str]] = None) -> int: + args = parse_args(argv) + if not args.quote: + args.quote = ["USDT"] + setup_logging(args.verbose) + + exchange = create_exchange() + symbols = iter_target_symbols( + exchange=exchange, + quotes=args.quote, + includes=args.symbol, + include_inactive=args.include_inactive, + ) + + if args.max_symbols is not None: + symbols = symbols[: args.max_symbols] + logging.info("出于调试目的,仅检测前 %d 个交易对。", len(symbols)) + + if not symbols: + logging.error("未找到任何满足条件的交易对,请检查过滤条件。") + return 1 + + sideways_results: List[SidewaysMetrics] = [] + total = len(symbols) + + for idx, symbol in enumerate(symbols, start=1): + logging.info("(%d/%d) 正在获取 %s 的 %s K 线(limit=%d)", idx, total, symbol, args.timeframe, args.limit) + ohlcv = fetch_ohlcv_with_retry( + exchange=exchange, + symbol=symbol, + timeframe=args.timeframe, + limit=args.limit, + retries=args.retries, + base_sleep=args.sleep, + ) + + if len(ohlcv) < max(100, args.limit // 2): + logging.debug("交易对 %s 返回数据不足(%d 根),跳过。", symbol, len(ohlcv)) + continue + + closes = [entry[4] for entry in ohlcv if entry[4] is not None] + metrics = compute_sideways_metrics(closes, symbol) + if not metrics: + continue + + if is_sideways(metrics, args.range_threshold, args.slope_threshold, args.std_threshold): + sideways_results.append(metrics) + logging.info( + "识别为横盘:%s | 振幅 %.2f%% | 斜率 %.4f%% | 标准差 %.2f%%", + symbol, + metrics.price_range_pct * 100, + metrics.slope_pct * 100, + metrics.std_pct * 100, + ) + else: + logging.debug( + "未满足条件:%s | 振幅 %.2f%% | 斜率 %.4f%% | 标准差 %.2f%%", + symbol, + metrics.price_range_pct * 100, + metrics.slope_pct * 100, + metrics.std_pct * 100, + ) + + if not sideways_results: + logging.warning("未检测到满足定义的长期横盘交易对。") + return 0 + + sideways_results.sort(key=lambda item: (item.price_range_pct, abs(item.slope_pct), item.std_pct)) + print("=" * 88) + print( + f"共识别 {len(sideways_results)} 个长期横盘交易对(阈值:振幅≤{args.range_threshold:.2%}," + f"斜率≤{args.slope_threshold:.2%},标准差≤{args.std_threshold:.2%})" + ) + print("=" * 88) + header = f"{'Symbol':15s} {'Range%':>10s} {'Slope%':>10s} {'STD%':>10s} {'Mean':>14s} {'Last':>14s} {'Count':>6s}" + print(header) + print("-" * len(header)) + for item in sideways_results: + print( + f"{item.symbol:15s}" + f" {item.price_range_pct * 100:10.4f}" + f" {item.slope_pct * 100:10.4f}" + f" {item.std_pct * 100:10.4f}" + f" {item.mean_close:14.8f}" + f" {item.last_close:14.8f}" + f" {item.data_points:6d}" + ) + + if args.export: + export_results(args.export, sideways_results) + + logging.info("任务完成。") + return 0 + + +if __name__ == "__main__": + sys.exit(run()) + diff --git a/chan/analysis/ChanLun_Classifier.py b/chan/analysis/ChanLun_Classifier.py new file mode 100644 index 0000000..192c125 --- /dev/null +++ b/chan/analysis/ChanLun_Classifier.py @@ -0,0 +1,529 @@ +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 ..core.ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX +from ..core.ChanKLU import ChanKLU +from ..core.ChanKLC import ChanKLC +from ..core.ChanBI import ChanBI +from ..core.ChanSBI import ChanSBI +from ..core.ChanSEG import ChanSEG +from ..core.ChanZS import ChanZS +from ..core.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 ..pipeline.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() + + # 获取训练集特征和标签 + save_csv = True if data_file_path else False + X_train, y_train = self.get_feature_data(train_df, save_csv=save_csv, csv_path=data_file_path if data_file_path else 'feature_data.csv') + + if len(X_train) == 0: + print("没有提取到足够的特征数据进行训练") + return None + + # 保存特征数据的步骤已经移到get_feature_data方法中处理 + # 以下是原有代码 + #{'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': 8, + 'eta': 0.01, + 'subsample': 0.8, + 'colsample_bytree': 0.8, + 'eval_metric': 'auc', + 'gamma': 0.0, + 'min_child_weight': 1, + 'alpha': 0, # L1正则化 + 'lambda': 0.5, # 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, save_csv=False, csv_path_prefix='param_', model_name=None): + """ + 寻找最佳参数组合 + :param dataframe: 输入的DataFrame,如果为None则使用初始化时的dataframe + :param save_csv: 是否保存特征数据到CSV文件 + :param csv_path_prefix: CSV文件保存路径前缀,会自动添加参数信息 + :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 i, params in enumerate(param_combinations): + print(f"\n尝试参数组合: {params}") + # 生成CSV文件名,包含一些参数信息 + param_info = f"eta{params['eta']}_depth{params['max_depth']}" + train_csv_path = f"{csv_path_prefix}train_{param_info}.csv" if save_csv else None + + model = self.train_model(dataframe=dataframe, data_file_path=train_csv_path, custom_params=params, model_name=model_name) + + # 分割数据集,后20%用于测试 + if dataframe is None: + dataframe = self.dataframe + + train_size = int(len(dataframe) * 0.8) + test_df = dataframe.iloc[train_size:].copy() + + # 获取测试集特征和标签 + test_csv_path = f"{csv_path_prefix}test_{param_info}.csv" if save_csv else None + X_test, y_test = self.get_validate_feature_data(test_df, save_csv=save_csv, csv_path=test_csv_path) + + 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, save_csv=False, csv_path='feature_data.csv'): + """ + 从dataframe提取特征数据 + :param dataframe: 输入的DataFrame + :param save_csv: 是否保存特征数据到CSV文件 + :param csv_path: CSV文件保存路径 + :return: 特征矩阵X和标签y + """ + # 使用ChanLun获取bi_list + klc_list = self.chan.get_klc_list(dataframe) + bi_list = self.chan.cal_bi_list(klc_list) + # 筛选方向为UP的bi的起始klc + feature_data = [] + labels = [] + feature_keys = [] # 用于保存特征名称 + + bi_index = 1 + sample_list = [] + for klc in klc_list: + if klc.klc_fx_type != Chan_KLC_FX.UNKNOWN: + sample_list.append(klc) + klc_count = 0 + print('Processing data...') + for klc in sample_list: + if bi_index >= len(bi_list): + bi_index = len(bi_list) - 1 + #bi = bi_list[bi_index] + #if klc.end_klu and bi.end_klc and klc.start_klu.index >= bi.start_klc.start_klu.index and klc.end_klu.index <= bi.end_klc.end_klu.index: + #klc.set_bi(bi) + + # 提取特征 + features = klc.get_feature_data() + + # 保存第一个样本的特征名称,用于CSV列名 + if len(feature_keys) == 0: + feature_keys = list(features.keys()) + # 将特征转换为模型可用的格式 + 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) + # 这个标签定义可以根据实际需求修改 + matched = False + for bi in bi_list: + if bi.end_klc and bi.end_klc.index == klc.index: + #print(bi.start_time, bi.start_klc.start_time, bi.dir) + label = 1 + matched = True + break + if not matched: + label = 0 + + feature_data.append(feature_vec) + labels.append(label) + klc_count += 1 + percent = klc_count/len(sample_list)*100 + if percent % 10 == 0: + print('Data processed:', percent, '%') + for index, key in enumerate(feature_keys): + print(index, key, feature_data[0][index]) + # 如果需要保存到CSV + if save_csv: + # 创建DataFrame保存特征数据 + # 只保留数值型特征 + numeric_feature_keys = [key for i, key in enumerate(feature_keys) + if i < len(feature_data[0]) if isinstance(feature_data[0][i], (int, float))] + + # 创建特征数据的DataFrame + df_features = pd.DataFrame(feature_data, columns=numeric_feature_keys) + # 添加标签列 + df_features['label'] = labels + # 添加时间信息便于分析 + if len(sample_list) > 0: + times = [klc.start_time for klc in sample_list] + df_features['time'] = times + + # 保存到CSV + df_features.to_csv(csv_path, index=False) + print(f"特征数据已保存到 {csv_path}") + + # 在return前添加 + positive_count = np.sum(labels) + print(f"正样本数量: {positive_count}, 负样本数量: {len(labels) - positive_count}") + print("Trainning data: ", len(feature_data), klc_list[-1].start_time, klc_list[-1].klc_fx_type , "---------------------") + return np.array(feature_data), np.array(labels) + def get_validate_feature_data(self, dataframe, save_csv=False, csv_path='validate_feature_data.csv'): + """ + 从dataframe提取特征数据 + :param dataframe: 输入的DataFrame + :param save_csv: 是否保存特征数据到CSV文件 + :param csv_path: CSV文件保存路径 + :return: 特征矩阵X和标签y + """ + # 使用ChanLun获取bi_list + klc_list = self.chan.get_klc_list(dataframe) + bi_list = self.chan.cal_bi_list(klc_list) + seg_list = self.chan.get_seg_list(bi_list) + # 筛选方向为UP的bi的起始klc + feature_data = [] + labels = [] + feature_keys = [] # 用于保存特征名称 + + bi_index = 1 + sample_list = [] + for klc in klc_list: + if klc.klc_fx_type != Chan_KLC_FX.UNKNOWN: + sample_list.append(klc) + for klc in sample_list: + if bi_index >= len(bi_list): + bi_index = len(bi_list) - 1 + bi = bi_list[bi_index] + # 提取特征 + features = klc.get_feature_data() + + # 保存第一个样本的特征名称,用于CSV列名 + if len(feature_keys) == 0: + feature_keys = list(features.keys()) + + # 将特征转换为模型可用的格式 + 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) + seg = seg_list[bi_index] + matched = False + for bi in bi_list: + if bi.end_klc and bi.end_klc.index == klc.index: + label = 1 + matched = True + break + if not matched: + label = 0 + + feature_data.append(feature_vec) + labels.append(label) + + # 如果需要保存到CSV + if save_csv: + # 创建DataFrame保存特征数据 + # 只保留数值型特征 + numeric_feature_keys = [key for i, key in enumerate(feature_keys) + if i < len(feature_data[0]) if isinstance(feature_data[0][i], (int, float))] + + # 创建特征数据的DataFrame + df_features = pd.DataFrame(feature_data, columns=numeric_feature_keys) + # 添加标签列 + df_features['label'] = labels + # 添加时间信息便于分析 + if len(sample_list) > 0: + times = [klc.start_time for klc in sample_list] + df_features['time'] = times + + # 保存到CSV + df_features.to_csv(csv_path, index=False) + print(f"验证特征数据已保存到 {csv_path}") + + print("Validating data: ", len(feature_data), klc_list[-1].start_time, klc_list[-1].klc_fx_type , "---------------------") + return np.array(feature_data), np.array(labels) + def validate_model(self, dataframe=None, save_csv=False, csv_path='validate_feature_data.csv'): + """ + 使用dataframe后20%的数据验证模型 + :param dataframe: 输入的DataFrame,如果为None则使用初始化时的dataframe + :param save_csv: 是否保存特征数据到CSV文件 + :param csv_path: CSV文件保存路径 + :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, save_csv=save_csv, csv_path=csv_path) + + 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.get_decimal(self.model.predict(dtest)[0]) + + def get_decimal(self, value): + return Decimal("{:.4f}".format(value)) + diff --git a/chan/analysis/ChanMACD.py b/chan/analysis/ChanMACD.py new file mode 100644 index 0000000..c3b59f1 --- /dev/null +++ b/chan/analysis/ChanMACD.py @@ -0,0 +1,274 @@ +from ..core.ChanKLU import ChanKLU +from ..core.ChanEnum import Chan_MACD_STATE, Chan_MACDSEG_DIR, Chan_MACDHISTSET_DIR, Chan_MACDUNITTF_DIR, Chan_MACDUNITTF_TYPE +from .ChanMACDSeg import ChanMACDSeg +from .ChanMACDUnitTF import ChanMACDUnitTF +from .ChanMACDHistSet import ChanMACDHistSet + +class ChanMACD(): + def __init__(self, klu_list: list[ChanKLU]): + self.klu_list = klu_list + self.seg_list = [] + self.unittf_list = [] + self.histset_list = [] + # 状态标记列表 + self.high_position_list = [] # 高位列表 + self.high_empty_list = [] # 高位空列表 + self.return_zero_list = [] # 归零轴列表 + self.cross0_up_list = [] # 向上穿越零轴列表 + self.cross0_down_list = [] # 向下穿越零轴列表 + # 计算段 / UnitTF / HistSet 及状态标记 + self.cal_macd_state() + self.get_klu_sd_list() + def get_klu_sd(self): + if self.klu_list: + sd = self.klu_list[-1].separate_div + if sd > 1: + print(self.klu_list[-1].time, sd) + return True + return False + def get_klu_sd_list(self): + sd_list = [] + if self.klu_list: + for klu in self.klu_list: + hist = klu.macdhist + signal = False + if klu.pre and klu.next: + if klu.signal > 0: + signal = klu.pre.signal > klu.signal and klu.next.signal < klu.signal + else: + signal = klu.pre.signal < klu.signal and klu.next.signal > klu.signal + sd = klu.separate_div + if sd > 1 and ((hist > 0 and hist < 200) or (hist < 0 and hist > -200)): + sd_list.append(klu.time) + #print(klu.time, sd) + return sd_list + def cal_macd_state(self): + last_seg = None + last_unittf = None + last_histset = None + last_klu = None + for klu in self.klu_list: + # initialise first histset + if klu.macd == 0 and klu.signal == 0 and klu.macdhist == 0: + continue + if last_histset is None: + if klu.macdhist > 0: + last_histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, None, Chan_MACDHISTSET_DIR.ABOVE) + self.histset_list.append(last_histset) + else: + last_histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, None, Chan_MACDHISTSET_DIR.UNDER) + self.histset_list.append(last_histset) + else: + # initialise first seg and unittf + if last_seg is None: + # create histset afterwards + if last_histset.histset_dir == Chan_MACDHISTSET_DIR.ABOVE: + if klu.macdhist > 0: + last_histset.add_klu(klu) + else: + histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, last_histset, Chan_MACDHISTSET_DIR.UNDER) + self.histset_list.append(histset) + last_histset.set_next(histset) + histset.set_pre(last_histset) + last_histset.set_end_klu(last_klu) + last_histset = histset + else: + if klu.macdhist < 0: + last_histset.add_klu(klu) + else: + histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, last_histset, Chan_MACDHISTSET_DIR.ABOVE) + self.histset_list.append(histset) + last_histset.set_next(histset) + histset.set_pre(last_histset) + last_histset.set_end_klu(last_klu) + last_histset = histset + if last_klu.signal >= 0 and klu.signal < 0: + last_unittf = ChanMACDUnitTF(len(self.unittf_list), klu.time, klu, None, Chan_MACDUNITTF_DIR.UNDER, Chan_MACDUNITTF_TYPE.CROSS0, last_histset) + self.unittf_list.append(last_unittf) + last_seg = ChanMACDSeg(len(self.seg_list), klu.time, klu, None, Chan_MACDSEG_DIR.UNDER, last_unittf) + self.seg_list.append(last_seg) + elif last_klu.signal <= 0 and klu.signal > 0: + last_unittf = ChanMACDUnitTF(len(self.unittf_list), klu.time, klu, None, Chan_MACDUNITTF_DIR.ABOVE, Chan_MACDUNITTF_TYPE.CROSS0, last_histset) + self.unittf_list.append(last_unittf) + last_seg = ChanMACDSeg(len(self.seg_list), klu.time, klu, None, Chan_MACDSEG_DIR.ABOVE, last_unittf) + self.seg_list.append(last_seg) + # after the first seg and unittf + else: + # create histset afterwards + if last_histset.histset_dir == Chan_MACDHISTSET_DIR.ABOVE: + if klu.macdhist > 0: + last_histset.add_klu(klu) + else: + histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, last_histset, Chan_MACDHISTSET_DIR.UNDER) + self.histset_list.append(histset) + last_histset.set_next(histset) + histset.set_pre(last_histset) + last_histset.set_end_klu(last_klu) + last_histset = histset + if last_unittf: + last_unittf.add_histset(last_histset) + else: + if klu.macdhist < 0: + last_histset.add_klu(klu) + else: + histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, last_histset, Chan_MACDHISTSET_DIR.ABOVE) + self.histset_list.append(histset) + last_histset.set_next(histset) + histset.set_pre(last_histset) + last_histset.set_end_klu(last_klu) + last_histset = histset + if last_unittf: + last_unittf.add_histset(last_histset) + if last_klu.signal >= 0 and klu.signal < 0: + last_unittf.set_end_klu(last_klu, Chan_MACDUNITTF_TYPE.CROSS0) + unittf = ChanMACDUnitTF(len(self.unittf_list), klu.time, klu, last_unittf, Chan_MACDUNITTF_DIR.UNDER, Chan_MACDUNITTF_TYPE.CROSS0, last_histset) + self.unittf_list.append(unittf) + last_unittf.set_next(unittf) + last_seg.set_end_klu(last_klu) + seg = ChanMACDSeg(len(self.seg_list), klu.time, klu, last_seg, Chan_MACDSEG_DIR.UNDER, unittf) + self.seg_list.append(seg) + last_seg.set_next(seg) + last_seg = seg + last_unittf = unittf + elif last_klu.signal <= 0 and klu.signal > 0: + last_unittf.set_end_klu(last_klu, Chan_MACDUNITTF_TYPE.CROSS0) + unittf = ChanMACDUnitTF(len(self.unittf_list), klu.time, klu, last_unittf, Chan_MACDUNITTF_DIR.ABOVE, Chan_MACDUNITTF_TYPE.CROSS0, last_histset) + self.unittf_list.append(unittf) + last_unittf.set_next(unittf) + last_seg.set_end_klu(last_klu) + seg = ChanMACDSeg(len(self.seg_list), klu.time, klu, last_seg, Chan_MACDSEG_DIR.ABOVE, unittf) + self.seg_list.append(seg) + last_seg.set_next(seg) + last_seg = seg + last_unittf = unittf + elif last_unittf.is_end and last_klu.macd < klu.macd and klu.macd > klu.signal: + unittf = ChanMACDUnitTF(len(self.unittf_list), klu.time, klu, last_unittf, Chan_MACDUNITTF_DIR.ABOVE, Chan_MACDUNITTF_TYPE.NEAR0, last_histset) + self.unittf_list.append(unittf) + last_unittf.set_next(unittf) + last_seg.add_unittf(unittf) + last_unittf = unittf + last_seg.add_klu(klu) + else: + if not last_unittf.is_end: + last_unittf.add_klu(klu) + last_seg.add_klu(klu) + last_klu = klu + klu.cal_macd_state() + #print(klu.time, klu.macd_state, klu.continue_div, klu.separate_div, klu.macd, klu.signal, klu.macdhist, klu.ema24, klu.ema52, klu.close) + return self.klu_list + def cal_macd(self): + last_seg = None + last_unittf = None + last_histset = None + histset = None + last_klu = None + for klu in self.klu_list: + klu.cal_macd_state() + print(klu.time, klu.macd_state) + # 1) 只有当 MACD 已可用(非 UNKNOWN)时,才开始初始化段/单元 + if last_seg is None: + if klu.macd_state != Chan_MACD_STATE.UNKNOWN: + # 初始化首个直方图集合(根据当前柱体正负) + if klu.macdhist >= 0: + histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, None, Chan_MACDHISTSET_DIR.ABOVE) + else: + histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, None, Chan_MACDHISTSET_DIR.UNDER) + self.histset_list.append(histset) + last_histset = histset + + # 初始化首段 + seg_dir = Chan_MACDSEG_DIR.ABOVE if klu.signal >= 0 else Chan_MACDSEG_DIR.UNDER + seg = ChanMACDSeg(len(self.seg_list), klu.time, klu, None, seg_dir, last_unittf) + self.seg_list.append(seg) + last_seg = seg + + # 初始化首个UnitTF + unittf_dir = Chan_MACDUNITTF_DIR.ABOVE if klu.signal >= 0 else Chan_MACDUNITTF_DIR.UNDER + unittf = ChanMACDUnitTF(len(self.unittf_list), klu.time, klu, None, unittf_dir, Chan_MACDUNITTF_TYPE.START, histset) + self.unittf_list.append(unittf) + last_unittf = unittf + last_seg.add_unittf(unittf) + # 未就绪则继续等下一根;已就绪亦已完成首个结构初始化,继续下一根 + last_klu = klu + continue + # 3) 直方图集合(基于当前 unittf) + if klu.macdhist >= 0: + if last_histset and last_histset.histset_dir == Chan_MACDHISTSET_DIR.ABOVE: + last_histset.add_klu(klu) + else: + # 结束旧 histset(以前一根结束更合理) + if last_histset and last_klu: + last_histset.set_end_klu(last_klu) + histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, last_histset, Chan_MACDHISTSET_DIR.ABOVE if klu.macdhist >= 0 else Chan_MACDHISTSET_DIR.UNDER) + self.histset_list.append(histset) + if last_histset: + last_histset.set_next(histset) + last_histset = histset + if last_unittf: + last_unittf.add_histset(histset) + else: + if last_histset and last_histset.histset_dir == Chan_MACDHISTSET_DIR.UNDER: + last_histset.add_klu(klu) + else: + # 结束旧 histset(以前一根结束更合理) + if last_histset and last_klu: + last_histset.set_end_klu(last_klu) + histset = ChanMACDHistSet(len(self.histset_list), klu.time, klu, last_histset, Chan_MACDHISTSET_DIR.ABOVE if klu.macdhist >= 0 else Chan_MACDHISTSET_DIR.UNDER) + self.histset_list.append(histset) + if last_histset: + last_histset.set_next(histset) + last_histset = histset + if last_unittf: + last_unittf.add_histset(histset) + # 2) 过零切段(使用KLU中的穿越状态) + if (klu.macd_state == Chan_MACD_STATE.CROSS0_UP or + klu.macd_state == Chan_MACD_STATE.CROSS0_DOWN): + # 结束旧 unittf + last_unittf.set_end_klu(last_klu, Chan_MACDUNITTF_TYPE.CROSS0) + # 新的单位时间周期 + new_dir = Chan_MACDUNITTF_DIR.ABOVE if klu.signal >= 0 else Chan_MACDUNITTF_DIR.UNDER + unittf = ChanMACDUnitTF(len(self.unittf_list), klu.time, klu, last_unittf, new_dir, Chan_MACDUNITTF_TYPE.CROSS0, histset) + self.unittf_list.append(unittf) + last_unittf.set_next(unittf) + last_unittf = unittf + # 收尾旧段 + last_seg.set_end_klu(last_klu) + # 新段方向取反 + new_dir = Chan_MACDSEG_DIR.UNDER if last_seg.seg_dir == Chan_MACDSEG_DIR.ABOVE else Chan_MACDSEG_DIR.ABOVE + seg = ChanMACDSeg(len(self.seg_list), klu.time, klu, last_seg, new_dir, last_unittf) + self.seg_list.append(seg) + last_seg.set_next(seg) + last_seg = seg + last_seg.add_unittf(unittf) + else: + # 4) UnitTF 状态机:用黄线Signal的归零轴 + if last_klu.macd_state == Chan_MACD_STATE.NEAR0 and last_unittf.div_count > 1: + #print(klu.time, klu.macd_state) + if klu.macd_state == Chan_MACD_STATE.RZ_UP: + last_unittf.set_end_klu(last_klu, Chan_MACDUNITTF_TYPE.NEAR0) + new_dir = Chan_MACDUNITTF_DIR.ABOVE if klu.signal >= 0 else Chan_MACDUNITTF_DIR.UNDER + unittf = ChanMACDUnitTF(len(self.unittf_list), klu.time, klu, last_unittf, new_dir, Chan_MACDUNITTF_TYPE.NEAR0, histset) + self.unittf_list.append(unittf) + last_unittf.set_next(unittf) + last_unittf = unittf + last_seg.add_unittf(unittf) + elif klu.macd_state == Chan_MACD_STATE.RZ_DOWN: + last_unittf.set_end_klu(last_klu, Chan_MACDUNITTF_TYPE.NEAR0) + new_dir = Chan_MACDUNITTF_DIR.ABOVE if klu.signal >= 0 else Chan_MACDUNITTF_DIR.UNDER + unittf = ChanMACDUnitTF(len(self.unittf_list), klu.time, klu, last_unittf, new_dir, Chan_MACDUNITTF_TYPE.NEAR0, histset) + self.unittf_list.append(unittf) + last_unittf.set_next(unittf) + last_unittf = unittf + last_seg.add_unittf(unittf) + else: + last_unittf.add_klu(klu) + last_seg.add_klu(klu) + else: + last_unittf.add_klu(klu) + last_seg.add_klu(klu) + + last_klu = klu + last_histset.set_end_klu(last_klu) + last_unittf.set_end_klu(last_klu, None) + last_seg.set_end_klu(last_klu) + return self.klu_list \ No newline at end of file diff --git a/chan/analysis/ChanMACDHistSet.py b/chan/analysis/ChanMACDHistSet.py new file mode 100644 index 0000000..36f0cb4 --- /dev/null +++ b/chan/analysis/ChanMACDHistSet.py @@ -0,0 +1,117 @@ +from ..core.ChanEnum import Chan_MACDHISTSET_DIR, Chan_MACDUNITTF_DIV, Chan_MACD_STATE + +class ChanMACDHistSet(): + def __init__(self, index, start_time, start_klu, pre_histset, dir): + self.index = index + self.start_time = start_time + self.end_time = None + self.klu_list = [] + self.klu_list.append(start_klu) + self.histset_dir = dir + self.next = None + self.pre = pre_histset + self.peak_klu = None + self.area = start_klu.macdhist + self.unittf_div = Chan_MACDUNITTF_DIV.UNDIV + self.middle_klu = None + self.div_count = 0 + self.last_klu = start_klu + self.start_klu = start_klu + self.peak_div_list = [] + self.middle_area = 0 + self.total_macdhist = 0 + def set_next(self, next_histset): + self.next = next_histset + def set_pre(self, pre_histset): + self.pre = pre_histset + def set_middle_klu(self, middle_klu): + self.middle_klu = middle_klu + #self.middle_area = abs(middle_klu.macdhist) + #self.middle_klu = None + def set_unittf_div(self, unittf_div): + self.unittf_div = unittf_div + def add_klu(self, klu): + klu.set_histset(self) + self.klu_list.append(klu) + self.area += abs(klu.macdhist) + if self.middle_klu: + self.middle_area += abs(klu.macdhist) + if self.middle_klu and self.middle_klu.index + 1 == klu.index: + self.low_klu = None + self.peak_klu = None + self.div_count = 0 + self.peak_div_list = [] + else: + if self.last_klu: + self.cal_macdhist_klu(klu) + self.last_klu = klu + def cal_macdhist_klu(self, klu): + if self.middle_klu: + if klu.index >= self.middle_klu.index + 2: + if klu.pre.pre: + if abs(klu.pre.macdhist) > abs(klu.pre.pre.macdhist) and abs(klu.pre.macdhist) > abs(klu.macdhist): + if self.peak_klu: + if abs(klu.pre.macdhist) > abs(self.peak_klu.macdhist): + self.peak_klu = klu.pre + #self.div_count = 0 + #self.peak_div_list = [] + else: + if klu.pre.macd * klu.pre.macdhist > 0: + self.peak_div_list.append(klu.pre) + self.div_count += 1 + klu.pre.continue_div = True + else: + self.peak_klu = klu.pre + else: + if len(self.klu_list) >= 3: + if klu.pre.pre: + if abs(klu.pre.macdhist) > abs(klu.pre.pre.macdhist) and abs(klu.pre.macdhist) > abs(klu.macdhist): + if self.peak_klu: + if abs(klu.pre.macdhist) > abs(self.peak_klu.macdhist): + self.peak_klu = klu.pre + #self.div_count = 0 + #self.peak_div_list = [] + else: + if klu.pre.macd * klu.pre.macdhist > 0 and klu.pre.signal * klu.pre.macdhist > 0: + self.peak_div_list.append(klu.pre) + self.div_count += 1 + klu.pre.continue_div = True + else: + self.peak_klu = klu.pre + def set_end_klu(self, end_klu): + self.end_klu = end_klu + self.end_time = end_klu.time + #if len(self.peak_div_list) > 0: + #klu = self.peak_div_list[-1] + #if klu.macd * klu.macdhist > 0: + #end_klu.continue_div = True + #print(end_klu.time, "Continue Div") + if self.start_klu.index == end_klu.index: + self.peak_klu = self.start_klu + if self.start_klu.index + 1 == end_klu.index: + if abs(self.start_klu.macdhist) > abs(end_klu.macdhist): + self.peak_klu = self.start_klu + else: + self.peak_klu = end_klu + if len(self.klu_list) >= 3 and self.peak_klu == None: + self.peak_klu = self.klu_list[0] + for klu in self.klu_list: + if abs(klu.macdhist) > abs(self.peak_klu.macdhist): + self.peak_klu = klu + peak_str = "" + state_str = "" + for peak_div in self.peak_div_list: + peak_str += f"{peak_div.time}, " + state_str += f"{peak_div.macd_state}, " + total_macdhist = 0 + first_klu = self.klu_list[0] + last_klu = self.klu_list[-1] + if (first_klu.macd > 0 and last_klu.macd > 0 and first_klu.macdhist > 0) or (first_klu.macd < 0 and last_klu.macd < 0 and first_klu.macdhist < 0): + for klu in self.klu_list: + self.total_macdhist += klu.macdhist + if abs(self.total_macdhist) < 150: + #print(self.end_time, "Total MACDHist: ", self.total_macdhist) + last_klu.separate_div = 99999 + #if self.peak_klu and len(self.peak_div_list) > 0: + #print("Continue Div: ",self.start_time, "Peak:", self.peak_klu.time, "Div: ", peak_str, state_str) + \ No newline at end of file diff --git a/chan/analysis/ChanMACDSeg.py b/chan/analysis/ChanMACDSeg.py new file mode 100644 index 0000000..ed658cf --- /dev/null +++ b/chan/analysis/ChanMACDSeg.py @@ -0,0 +1,49 @@ +from ..core.ChanEnum import Chan_MACDSEG_DIR + + +class ChanMACDSeg(): + def __init__(self, index, start_time, start_klu, pre_seg, seg_dir, start_unittf): + self.index = index + self.start_time = start_time + self.end_time = None + self.start_klu = start_klu + self.end_klu = None + self.klu_list = [] + self.klu_list.append(start_klu) + self.unittf_list = [] + self.seg_dir = seg_dir + self.pre = pre_seg + self.next = None + self.high_klu = start_klu + self.low_klu = start_klu + self.ref_klu = None + self.unittf_list.append(start_unittf) + def set_next(self, next_seg): + self.next = next_seg + def set_pre(self, pre_seg): + self.pre = pre_seg + def add_klu(self, klu): + if klu: + self.klu_list.append(klu) + klu.set_seg(self) + if self.seg_dir == Chan_MACDSEG_DIR.ABOVE: + if klu.macdhist > self.high_klu.macdhist: + self.high_klu = klu + else: + if klu.macdhist < self.low_klu.macdhist: + self.low_klu = klu + else: + if klu.macdhist < self.high_klu.macdhist: + self.high_klu = klu + else: + if klu.macdhist > self.low_klu.macdhist: + self.low_klu = klu + if self.high_klu.index != self.start_klu.index: + self.ref_klu = self.high_klu + def add_unittf(self, unittf): + self.unittf_list.append(unittf) + unittf.set_next(self) + def set_end_klu(self, end_klu): + self.add_klu(end_klu) + self.end_klu = end_klu + self.end_time = end_klu.time \ No newline at end of file diff --git a/chan/analysis/ChanMACDUnitTF.py b/chan/analysis/ChanMACDUnitTF.py new file mode 100644 index 0000000..e1d74f4 --- /dev/null +++ b/chan/analysis/ChanMACDUnitTF.py @@ -0,0 +1,141 @@ +from ..core.ChanEnum import Chan_MACD_STATE, Chan_MACDUNITTF_DIR, Chan_MACDHISTSET_DIR, Chan_MACDUNITTF_DIV, Chan_MACDUNITTF_TYPE + + +class ChanMACDUnitTF(): + def __init__(self, index, start_time, start_klu, pre_unittf, unittf_dir, start_type, start_histset): + self.index = index + self.start_time = start_time + self.end_time = None + self.start_klu = start_klu + self.end_klu = None + self.klu_list = [] + self.klu_list.append(start_klu) + self.histset_list = [] + self.histset_list.append(start_histset) + self.div_count = 0 + start_histset.set_middle_klu(start_klu) + self.next = None + self.pre = pre_unittf + self.unittf_dir = unittf_dir + self.start_type = start_type + self.end_type = None + self.peak_klu = None + self.div_type = Chan_MACDUNITTF_DIV.UNDIV + self.div_peak_list = [] + self.is_end = False + def set_next(self, next_unittf): + self.next = next_unittf + def set_pre(self, pre_unittf): + self.pre = pre_unittf + def add_histset(self, histset): + self.histset_list.append(histset) + def add_klu(self, klu): + self.klu_list.append(klu) + self.cal_peak_div() + self.cal_macd_state() + def cal_peak_div(self): + self.div_count = 0 + self.div_peak_list = [] + self.peak_klu = None + if len(self.histset_list) == 0: + return + if len(self.histset_list) == 1: + self.div_type = self.histset_list[0].unittf_div + self.peak_klu = self.histset_list[0].peak_klu + else: + + for index in range(0, len(self.histset_list)): + histset = self.histset_list[index] + if self.same_dir(histset): + #if histset.peak_klu: + #print("Unittf: ", self.start_klu.time, len(self.histset_list), histset.peak_klu.time) + if self.peak_klu: + if histset.peak_klu: + if abs(histset.peak_klu.macdhist) >= abs(self.peak_klu.macdhist): + self.peak_klu = histset.peak_klu + self.div_type = Chan_MACDUNITTF_DIV.UNDIV + self.div_count = 0 + else: + self.div_type = Chan_MACDUNITTF_DIV.DISCRETE + self.div_count += 1 + self.div_peak_list.append(histset.peak_klu) + #print("Unittf: ", self.start_klu.time) + if histset.peak_klu.macd > 0 and histset.histset_dir == Chan_MACDHISTSET_DIR.ABOVE: + histset.peak_klu.set_separate_div(self.div_count) + elif histset.peak_klu.macd < 0 and histset.histset_dir == Chan_MACDHISTSET_DIR.UNDER: + histset.peak_klu.set_separate_div(self.div_count) + else: + if histset.peak_klu: + self.peak_klu = histset.peak_klu + def cal_macd_state(self): + if len(self.klu_list) > 0: + last_klu = self.klu_list[0] + macd_peak_klu = None + signal_peak_klu = None + for index in range(1, len(self.klu_list)): + klu = self.klu_list[index] + if klu.macd == 0 and klu.signal == 0 and klu.macdhist == 0: + continue + if self.start_type == Chan_MACDUNITTF_TYPE.CROSS0 or self.start_type == Chan_MACDUNITTF_TYPE.NEAR0: + if self.unittf_dir == Chan_MACDUNITTF_DIR.ABOVE: + if macd_peak_klu is None: + if last_klu.macd < klu.macd: + if last_klu.signal < last_klu.macdhist: + last_klu.set_macd_state(Chan_MACD_STATE.UP) + else: + last_klu.set_macd_state(Chan_MACD_STATE.HIGH) + else: + macd_peak_klu = last_klu + last_klu.set_macd_state(Chan_MACD_STATE.PEAK) + elif klu.macd > macd_peak_klu.macd: + macd_peak_klu = None + last_klu.set_macd_state(Chan_MACD_STATE.HIGH) + elif last_klu.signal < klu.signal: + last_klu.set_macd_state(Chan_MACD_STATE.HIGH_EMPTY) + elif signal_peak_klu is None: + signal_peak_klu = last_klu + last_klu.set_macd_state(Chan_MACD_STATE.HIGH_EMPTY) + elif klu.signal > signal_peak_klu.signal: + signal_peak_klu = None + last_klu.set_macd_state(Chan_MACD_STATE.HIGH) + elif last_klu.macd < last_klu.signal: + last_klu.set_macd_state(Chan_MACD_STATE.RETURN_ZERO) + if self.return_zero(last_klu, klu): + self.end_type = Chan_MACDUNITTF_TYPE.NEAR0 + self.is_end = True + self.end_klu = klu + self.end_time = klu.time + klu.set_macd_state(Chan_MACD_STATE.NEAR0) + #print(self.index, last_klu.time, last_klu.macd, last_klu.signal, last_klu.macd_state, klu.macd_state) + break + #print(self.index, last_klu.time, last_klu.macd, last_klu.signal, last_klu.macd_state) + last_klu = klu + + def return_zero(self, last_klu, klu): + return_zero = False + if last_klu.close < last_klu.ema52 and klu.close > klu.ema52: + return_zero = True + return_zero = False + return return_zero + def same_dir(self, histset): + if self.unittf_dir == Chan_MACDUNITTF_DIR.ABOVE: + return histset.histset_dir == Chan_MACDHISTSET_DIR.ABOVE + else: + return histset.histset_dir == Chan_MACDHISTSET_DIR.UNDER + def set_end_klu(self, end_klu, end_type): + self.end_type = end_type + self.end_klu = end_klu + self.end_time = end_klu.time + self.is_end = True + self.cal_macd_state() + div_time = "" + for div in self.div_peak_list: + div_time += f"{div.time}, " + histset_time = "" + for histset in self.histset_list: + histset_time += f"{histset.start_time}, " + + #if self.peak_klu and self.div_type == Chan_MACDUNITTF_DIV.DISCRETE: + #print("Cross Div: ", self.start_klu.time, self.peak_klu.time, self.div_count, self.div_type, self.unittf_dir, div_time, len(self.histset_list)) + + \ No newline at end of file diff --git a/chan/analysis/ChanPY.py b/chan/analysis/ChanPY.py new file mode 100644 index 0000000..b8704a6 --- /dev/null +++ b/chan/analysis/ChanPY.py @@ -0,0 +1,478 @@ +import sys +import os +from typing import Dict, List +from functools import reduce +from pandas import DataFrame +from datetime import datetime, timedelta, timezone + +# 外部 chan.py 与本仓库包名 `chan` 在 macOS 大小写不敏感磁盘上冲突。 +# 临时卸下本包后再导入上游,再恢复本包。 +_EXT_ROOT = os.path.abspath("/Users/jack/Project/chan.py") + + +def _load_external_chan(): + saved = {} + for key in list(sys.modules): + low = key.lower() + if low == "chan" or low.startswith("chan."): + saved[key] = sys.modules.pop(key) + inserted = False + if _EXT_ROOT not in sys.path: + sys.path.insert(0, _EXT_ROOT) + inserted = True + try: + from Chan import CChan as _CChan + from BuySellPoint.BS_Point import CBS_Point as _CBS_Point + from ChanConfig import CChanConfig as _CChanConfig + from Common.CEnum import ( + AUTYPE as _AUTYPE, + DATA_SRC as _DATA_SRC, + KL_TYPE as _KL_TYPE, + DATA_FIELD as _DATA_FIELD, + BSP_TYPE as _BSP_TYPE, + FX_TYPE as _FX_TYPE, + BI_DIR as _BI_DIR, + KLINE_DIR as _KLINE_DIR, + SEG_DIR as _SEG_DIR, + ) + from KLine.KLine_Unit import CKLine_Unit as _CKLine_Unit + from Common.CTime import CTime as _CTime + from Common.func_util import kltype_lt_day as _kltype_lt_day, str2float as _str2float + from Bi.Bi import CBi as _CBi + return { + "CChan": _CChan, + "CBS_Point": _CBS_Point, + "CChanConfig": _CChanConfig, + "AUTYPE": _AUTYPE, + "DATA_SRC": _DATA_SRC, + "KL_TYPE": _KL_TYPE, + "DATA_FIELD": _DATA_FIELD, + "BSP_TYPE": _BSP_TYPE, + "FX_TYPE": _FX_TYPE, + "BI_DIR": _BI_DIR, + "KLINE_DIR": _KLINE_DIR, + "SEG_DIR": _SEG_DIR, + "CKLine_Unit": _CKLine_Unit, + "CTime": _CTime, + "kltype_lt_day": _kltype_lt_day, + "str2float": _str2float, + "CBi": _CBi, + } + finally: + # 清除上游以 Chan/chan 注册的模块,避免污染本包 + for key in list(sys.modules): + low = key.lower() + if low == "chan" or low.startswith("chan."): + sys.modules.pop(key, None) + sys.modules.update(saved) + if inserted and _EXT_ROOT in sys.path: + try: + sys.path.remove(_EXT_ROOT) + except ValueError: + pass + + +_ext = _load_external_chan() +CChan = _ext["CChan"] +CBS_Point = _ext["CBS_Point"] +CChanConfig = _ext["CChanConfig"] +AUTYPE = _ext["AUTYPE"] +DATA_SRC = _ext["DATA_SRC"] +KL_TYPE = _ext["KL_TYPE"] +DATA_FIELD = _ext["DATA_FIELD"] +BSP_TYPE = _ext["BSP_TYPE"] +FX_TYPE = _ext["FX_TYPE"] +BI_DIR = _ext["BI_DIR"] +KLINE_DIR = _ext["KLINE_DIR"] +SEG_DIR = _ext["SEG_DIR"] +CKLine_Unit = _ext["CKLine_Unit"] +CTime = _ext["CTime"] +kltype_lt_day = _ext["kltype_lt_day"] +str2float = _ext["str2float"] +CBi = _ext["CBi"] + + +def GetColumnNameFromFieldList(fileds: str): + _dict = { + "time": DATA_FIELD.FIELD_TIME, + "open": DATA_FIELD.FIELD_OPEN, + "high": DATA_FIELD.FIELD_HIGH, + "low": DATA_FIELD.FIELD_LOW, + "close": DATA_FIELD.FIELD_CLOSE, + "volume": DATA_FIELD.FIELD_VOLUME + } + return [_dict[x] for x in fileds.split(",")] +class ChanPY(): + k_type = KL_TYPE.K_5M + config = CChanConfig({ + "bi_strict": True, + "bi_algo": "normal", + "trigger_step": True, + "skip_step": 0, + "divergence_rate": float("inf"), + "bsp2_follow_1": False, + "bsp3_follow_1": False, + "min_zs_cnt": 1, + "bs1_peak": False, + "macd_algo": "peak", + "bs_type": '1,2,3a,1p,2s,3b', + "print_warning": True, + "zs_algo": "normal", + }) + chan = CChan( + code="BTC/USDT:USDT", + data_src=DATA_SRC.CCXT, + lv_list=[k_type], + config=config, + autype=AUTYPE.QFQ, + ) + klu_list = [] + bsps = [] + chanIn = True + #def __init__(self, dataframe): + #self.klu_list = self.get_kl_data(dataframe) + #for klu in self.klu_list: + #self.chan.trigger_load({self.k_type: [klu]}) + def add_klu(self, klu): + if klu: + self.chan.trigger_load({self.k_type: [klu]}) + self.klu_list.append(klu) + def add_klu_from_dataframe(self, dataframe): + if len(dataframe) > len(self.klu_list) and len(dataframe) - len(self.klu_list) == 1: + klu = self.get_last_klu(dataframe) + self.chan.trigger_load({self.k_type: [klu]}) + self.klu_list.append(klu) + def parse_time_column(self, inp): + if len(inp) == 10: + year = int(inp[:4]) + month = int(inp[5:7]) + day = int(inp[8:10]) + hour = minute = 0 + elif len(inp) == 17: + year = int(inp[:4]) + month = int(inp[4:6]) + day = int(inp[6:8]) + hour = int(inp[8:10]) + minute = int(inp[10:12]) + elif len(inp) == 19: + year = int(inp[:4]) + month = int(inp[5:7]) + day = int(inp[8:10]) + hour = int(inp[11:13]) + minute = int(inp[14:16]) + else: + raise Exception(f"unknown time column from TradingView:{inp}") + return CTime(year, month, day, hour, minute, auto=not kltype_lt_day(self.k_type)) + + def create_item_dict(self, data, column_name): + for i in range(len(data)): + data[i] = self.parse_time_column(data[i]) if i == 0 else str2float(data[i]) + return dict(zip(column_name, data)) + def get_last_klu(self, dataframe:DataFrame): + fields = "time,open,high,low,close,volume" + item = dataframe.iloc[-1] + date = item['date'] + o = item['open'] + h = item['high'] + l = item['low'] + c = item['close'] + v = item['volume'] + #time_obj = date.fromtimestamp(date) + time_str = date.strftime('%Y-%m-%d %H:%M:%S') + item_data = [ + time_str, + o, + h, + l, + c, + v + ] + klu = CKLine_Unit(self.create_item_dict(item_data, GetColumnNameFromFieldList(fields)), autofix=True) + klu.set_idx(len(dataframe)-1) + return klu + 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) + time_str = date.strftime('%Y-%m-%d %H:%M:%S') + item_data = [ + time_str, + o, + h, + l, + c, + v + ] + klu = CKLine_Unit(self.create_item_dict(item_data, GetColumnNameFromFieldList(fields)), autofix=True) + klu.set_idx(i) + klu_list.append(klu) + return klu_list + def get_bsp_type(self, bsp_type, is_buy): + if is_buy: + if bsp_type == BSP_TYPE.T1: + return 1 + if bsp_type == BSP_TYPE.T1P: + return 2 + if bsp_type == BSP_TYPE.T2: + return 3 + if bsp_type == BSP_TYPE.T2S: + return 4 + if bsp_type == BSP_TYPE.T3A: + return 5 + if bsp_type == BSP_TYPE.T3B: + return 6 + else: + if bsp_type == BSP_TYPE.T1: + return -1 + if bsp_type == BSP_TYPE.T1P: + return -2 + if bsp_type == BSP_TYPE.T2: + return -3 + if bsp_type == BSP_TYPE.T2S: + return -4 + if bsp_type == BSP_TYPE.T3A: + return -5 + if bsp_type == BSP_TYPE.T3B: + return -6 + def get_bsps(self, dataframe:DataFrame): + fields = "time,open,high,low,close,volume" + bsps = [] + updown = [] + bi_sure = [] + if self.chanIn: + kl_data = self.get_kl_data(dataframe) + bsp_list = [] + bsp_list_pre_len = 0 + last_bsp_value = 0 + last_updown = -1 + bi_list_pre_len = 0 + pre_bi = None + zs_list_pre_len = 0 + pre_zs = None + for klu in kl_data: # 获取单根K线 + self.chan.trigger_load({self.k_type: [klu]}) # 喂给CChan新增k线 + self.last_kline = klu + bsp_list = self.chan.get_bsp() + kl_datas = self.chan.kl_datas[self.k_type] + bi_list = kl_datas.bi_list + lst = kl_datas.lst + if len(bsp_list) > 0: + last_bsp = bsp_list[-1] + #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, lst[-2].fx, bi_list[-1].dir, bi_list[-1].is_sure,klu.close) + if bsp_list_pre_len > len(bsp_list): + if abs(last_bsp_value) == 1 or abs(last_bsp_value) == 2: + bsps.append(1) + #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, 98) + else: + bsps.append(99) + else: + if bsp_list_pre_len == len(bsp_list): + if klu.idx == last_bsp.klu.idx: + last_bsp_value = self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy) + bsps.append(last_bsp_value) + #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value) + else: + bsps.append(0) + else: + last_bsp_value = self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy) + bsps.append(last_bsp_value) + #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value) + else: + bsps.append(0) + bsp_list_pre_len = len(bsp_list) + + #Check zs ----------------------------------- + zs_list = kl_datas.zs_list + if len(zs_list) > 0: + zs = zs_list[-1] + #if zs_list_pre_len > len(zs_list): + #print("No zs", zs.begin.time) + #if len(zs_list) > zs_list_pre_len: + #print(zs.begin.time, zs.end.time, zs.end.idx, zs.high, zs.low, zs.peak_high, zs.peak_low) + zs_list_pre_len = len(zs_list) + pre_zs = zs + + #Check Bi ----------------------------------- + if len(bi_list) > 0: + last_bi = bi_list[-1] + if len(bi_list) == 1: + if last_bi.dir == BI_DIR.UP: + updown.append(1) + last_updown = 1 + else: + updown.append(-1) + last_updown = -1 + else: + if last_updown == 1: + if last_bi.dir == BI_DIR.UP: + updown.append(0) + else: + updown.append(-1) + last_updown = -1 + else: + if last_bi.dir == BI_DIR.DOWN: + updown.append(0) + else: + updown.append(1) + last_updown = 1 + else: + updown.append(0) + bi_list = kl_datas.bi_list + if len(bi_list) > 0: + last_bi = bi_list[-1] + #if bi_list_pre_len > len(bi_list): + #print("Bi ", klu.time, pre_bi.idx, pre_bi.is_sure, bi_list[-1].idx, bi_list[-1].is_sure) + if last_bi.is_sure: + bi_sure.append(1) + #print(klu.time, last_bi.is_sure) + else: + bi_sure.append(0) + pre_bi = bi_list[-1] + bi_list_pre_len = len(bi_list) + else: + bi_sure.append(0) + #if bsps[-1] != 0 or updown[-1] != 0: + #print(klu.time, bsps[-1], updown[-1], bi_list[-1].is_sure) + self.chanIn = False + else: + klu = self.get_last_klu(dataframe) + if self.last_kline.time < klu.time: + self.chan.trigger_load({self.k_type: [klu]}) # 喂给CChan新增k线 + self.last_kline = klu + for index in range(0, len(bsps)): + if not (abs(bsps[index]) == 1 or abs(bsps[index]) == 2): + bsps[index] = 0 + else: + if bsps[index] == 2: + bsps[index] = 1 + else: + if bsps[index] == -2: + bsps[index] = -1 + else: + bsps[index] = 0 + #print(bsps) + #print(updown) + kl_datas = self.chan.kl_datas[self.k_type] + #for zs in kl_datas.zs_list: + #print(zs.begin.time, zs.end.time) + return bsps, updown, bi_sure + + def get_bsp_state1(self, dataframe:DataFrame): + fields = "time,open,high,low,close,volume" + bsps = [] + if self.chanIn: + kl_data = self.get_kl_data(dataframe) + self.chan.trigger_load({self.k_type: kl_data}) + bsp_list = self.chan.get_bsp() + bsp_index = 0 + for klu in kl_data: + if bsp_index >= len(bsp_list): + bsp_index = len(bsp_list) - 1 + bsp = bsp_list[bsp_index] + if klu.idx == bsp.klu.idx: + bsp_type = self.get_bsp_type(bsp.type[0], bsp.is_buy) + if abs(bsp_type) == 1 or abs(bsp_type) == 10: + bsps.append(1) + else: + bsps.append(0) + bsp_index = bsp_index + 1 + else: + bsps.append(0) + self.chanIn = False + else: + klu = CKLine_Unit(self.create_item_dict(self.get_last_item_data(dataframe), GetColumnNameFromFieldList(fields)), autofix=True) + if self.last_kline.time < klu.time: + self.chan.trigger_load({self.k_type: [klu]}) # 喂给CChan新增k线 + self.last_kline = klu + return bsps + def get_bsp_state(self, dataframe:DataFrame): + fields = "time,open,high,low,close,volume" + if self.chanIn: + kl_data = self.get_kl_data(dataframe) + bsp_list = [] + bsp_list_pre_len = 0 + last_bsp_value = 0 + last_bsp_index = 0 + for klu in kl_data: # 获取单根K线 + self.chan.trigger_load({self.k_type: [klu]}) # 喂给CChan新增k线 + self.last_kline = klu + bsp_list = self.chan.get_bsp() + kl_datas = self.chan.kl_datas[self.k_type] + bi_list = kl_datas.bi_list + lst = kl_datas.lst + if len(bsp_list) > 0: + last_bsp = bsp_list[-1] + #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, lst[-2].fx, bi_list[-1].dir, bi_list[-1].is_sure,klu.close) + if bsp_list_pre_len > len(bsp_list): + if abs(last_bsp_value) == 1: + self.bsps.append(1) + #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, 98) + else: + self.bsps.append(99) + else: + if bsp_list_pre_len == len(bsp_list): + if klu.idx == last_bsp.klu.idx: + if last_bsp.klu.idx - last_bsp_index > 3: + last_bsp_value = self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy) + self.bsps.append(last_bsp_value) + else: + self.bsps.append(0) + last_bsp_index = last_bsp.klu.idx + #if abs(last_bsp_value) == 1 or abs(last_bsp_value) == 2: + #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, "Knonw") + else: + self.bsps.append(0) + else: + if klu.idx == last_bsp.klu.idx: + if last_bsp.klu.idx - last_bsp_index > 3: + last_bsp_value = self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy) + self.bsps.append(last_bsp_value) + else: + self.bsps.append(0) + last_bsp_index = last_bsp.klu.idx + #if abs(last_bsp_value) == 1 or abs(last_bsp_value) == 2: + #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, "Knonw") + else: + self.bsps.append(0) + else: + self.bsps.append(0) + bsp_list_pre_len = len(bsp_list) + self.chanIn = False + else: + klu = self.get_last_klu(dataframe) + if self.last_kline.time < klu.time: + self.chan.trigger_load({self.k_type: [klu]}) # 喂给CChan新增k线 + self.last_kline = klu + bsp_list = self.chan.get_bsp() + last_bsp = bsp_list[-1] + if last_bsp.klu.idx == klu.idx: + self.bsps.append(self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy)) + else: + self.bsps.append(0) + for index in range(0, len(self.bsps)): + if not (abs(self.bsps[index]) == 1 or abs(self.bsps[index]) == 2): + self.bsps[index] = 0 + else: + if self.bsps[index] == 2: + self.bsps[index] = 10 + else: + if self.bsps[index] == -2: + self.bsps[index] = -10 + else: + if self.bsps[index] == 1: + self.bsps[index] = 1 + else: + if self.bsps[index] == -1: + self.bsps[index] = -1 + else: + self.bsps[index] = 0 + return self.bsps + diff --git a/chan/analysis/ChanPivotClassifier.py b/chan/analysis/ChanPivotClassifier.py new file mode 100644 index 0000000..4a5e849 --- /dev/null +++ b/chan/analysis/ChanPivotClassifier.py @@ -0,0 +1,292 @@ +""" +中枢结构特征提取 + 标签化 +Market Structure Dataset Builder — Phase 1 + +定位: 训练数据集构建工具,不是交易信号生成器。 +Feature 描述中枢内部结构,Label 记录中枢后实际演化。 +""" + +import math +import json +from typing import Optional +from ..core.ChanEnum import Chan_BI_DIR + + +class ChanPivotClassifier: + """ + 中枢结构特征提取 + 标签化 + 输入: bi_zs_list (list[ChanBIZS]) + 输出: 结构化数据集 (list[dict]) + """ + + DATASET_VERSION = "pivot_v1" + FEATURE_SCHEMA = ["duration_norm", "contraction", "shift_norm"] + LABEL_SCHEMA = {"name": "break_direction", "values": ["up", "down", "none"]} + + def __init__(self, bi_zs_list: list, symbol: str = "", timeframe: str = ""): + self.bi_zs_list = bi_zs_list + self.symbol = symbol + self.timeframe = timeframe + + # ------------------------------------------------------------------ + # Feature extraction + # ------------------------------------------------------------------ + + @staticmethod + def calc_duration(zs) -> int: + """持续时间: 第一笔首K → 最后一笔末K 的 index 差""" + bi_list = zs.bi_list + start_idx = bi_list[0].start_klc.index + end_idx = bi_list[-1].end_klc.index + return end_idx - start_idx + + @staticmethod + def calc_contraction(zs) -> float: + """收敛率: 后窗口振幅均值 / 前窗口振幅均值""" + bi_list = zs.bi_list + if len(bi_list) < 4: + return 1.0 + + n = min(3, len(bi_list) // 2) + first_ranges = [bi.high - bi.low for bi in bi_list[:n]] + last_ranges = [bi.high - bi.low for bi in bi_list[-n:]] + + first_mean = sum(first_ranges) / len(first_ranges) + last_mean = sum(last_ranges) / len(last_ranges) + + if first_mean == 0: + return 1.0 + return last_mean / first_mean + + @staticmethod + def calc_shift(zs) -> tuple[float, float]: + """重心漂移: 前后半段重心均值差 (原始值, 归一化值)""" + bi_list = zs.bi_list + mid = len(bi_list) // 2 + + first_centers = [(bi.high + bi.low) / 2 for bi in bi_list[:mid]] + last_centers = [(bi.high + bi.low) / 2 for bi in bi_list[mid:]] + + shift_raw = ( + sum(last_centers) / len(last_centers) + - sum(first_centers) / len(first_centers) + ) + + zs_height = zs.zg - zs.zd + if zs_height == 0: + shift_norm = 0.0 + else: + shift_norm = shift_raw / zs_height + + return shift_raw, shift_norm + + @staticmethod + def compute_duration_norm(duration_raw: int, historical_durations: list) -> float: + """用历史窗口均值归一化 duration""" + if not historical_durations: + return 1.0 + avg = sum(historical_durations) / len(historical_durations) + if avg == 0: + return 1.0 + return duration_raw / avg + + @staticmethod + def compute_features(zs, historical_durations: Optional[list] = None): + """计算单个中枢的全部结构特征(实时友好)""" + duration_raw = ChanPivotClassifier.calc_duration(zs) + contraction = ChanPivotClassifier.calc_contraction(zs) + shift_raw, shift_norm = ChanPivotClassifier.calc_shift(zs) + + if historical_durations is not None and len(historical_durations) > 0: + duration_norm = ChanPivotClassifier.compute_duration_norm( + duration_raw, historical_durations + ) + else: + duration_norm = 1.0 + + return { + "duration_raw": duration_raw, + "duration_norm": round(duration_norm, 4), + "contraction": round(contraction, 4), + "shift_raw": round(shift_raw, 6), + "shift_norm": round(shift_norm, 4), + "zs_height": round(zs.zg - zs.zd, 6), + } + + # ------------------------------------------------------------------ + # Label computation + # ------------------------------------------------------------------ + + @staticmethod + def _clamp(x: float, lo: float = 0.0, hi: float = 1.0) -> float: + return max(lo, min(hi, x)) + + def _compute_label(self, zs, contraction: float, shift_norm: float) -> dict: + """计算标签: up / down / none + 连续置信度""" + bi_out = zs.bi_out + + if bi_out is None: + return { + "label": "none", + "label_confidence": 0.0, + "label_detail": { + "bi_out_dir": "none", + "score_breakout": 0.0, + "score_shift": 0.0, + "score_contraction": 0.0, + }, + } + + zs_height = zs.zg - zs.zd + if zs_height == 0: + zs_height = 1e-8 + + # ---- 向上突破分数 ---- + if bi_out.dir == Chan_BI_DIR.UP: + raw_breakout = (bi_out.high - zs.gg) / zs_height + score_breakout_up = self._clamp(raw_breakout) + score_shift_up = math.tanh(self._clamp(shift_norm, -3.0, 3.0)) + score_contraction_up = max(0.0, 1.0 - contraction) + else: + score_breakout_up = 0.0 + score_shift_up = 0.0 + score_contraction_up = 0.0 + + up_score = ( + score_breakout_up * 0.5 + + score_shift_up * 0.3 + + score_contraction_up * 0.2 + ) + + # ---- 向下突破分数 ---- + if bi_out.dir == Chan_BI_DIR.DOWN: + raw_breakout = (zs.dd - bi_out.low) / zs_height + score_breakout_down = self._clamp(raw_breakout) + score_shift_down = math.tanh(self._clamp(-shift_norm, -3.0, 3.0)) + score_contraction_down = max(0.0, 1.0 - contraction) + else: + score_breakout_down = 0.0 + score_shift_down = 0.0 + score_contraction_down = 0.0 + + down_score = ( + score_breakout_down * 0.5 + + score_shift_down * 0.3 + + score_contraction_down * 0.2 + ) + + # ---- 判定 ---- + threshold = 0.15 + + if up_score > down_score and up_score > threshold: + label = "up" + confidence = up_score + detail = { + "bi_out_dir": "up", + "score_breakout": round(score_breakout_up, 4), + "score_shift": round(score_shift_up, 4), + "score_contraction": round(score_contraction_up, 4), + } + elif down_score > up_score and down_score > threshold: + label = "down" + confidence = down_score + detail = { + "bi_out_dir": "down", + "score_breakout": round(score_breakout_down, 4), + "score_shift": round(score_shift_down, 4), + "score_contraction": round(score_contraction_down, 4), + } + else: + label = "none" + confidence = max(up_score, down_score) + bi_dir = "up" if bi_out.dir == Chan_BI_DIR.UP else "down" + detail = { + "bi_out_dir": bi_dir, + "score_breakout": round(max(score_breakout_up, score_breakout_down), 4), + "score_shift": round(max(score_shift_up, score_shift_down), 4), + "score_contraction": round(max(score_contraction_up, score_contraction_down), 4), + } + + return { + "label": label, + "label_confidence": round(confidence, 4), + "label_detail": detail, + } + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + def extract(self) -> list[dict]: + """主入口:对每个中枢提取 3 特征 + 1 标签""" + + # 第一遍:计算原始值 + raw = [] + for i, zs in enumerate(self.bi_zs_list): + if not zs.is_sure or len(zs.bi_list) < 3: + continue + + duration_raw = ChanPivotClassifier.calc_duration(zs) + contraction = ChanPivotClassifier.calc_contraction(zs) + shift_raw, shift_norm = ChanPivotClassifier.calc_shift(zs) + + raw.append({ + "zs": zs, + "zs_index": i, + "duration_raw": duration_raw, + "contraction": contraction, + "shift_raw": shift_raw, + "shift_norm": shift_norm, + "zs_height": zs.zg - zs.zd, + }) + + # 第二遍:组装输出 + 计算 label + result = [] + for r in raw: + zs = r["zs"] + historical = [x["duration_raw"] for x in raw] + duration_norm = ChanPivotClassifier.compute_duration_norm( + r["duration_raw"], historical + ) + label_info = self._compute_label(zs, r["contraction"], r["shift_norm"]) + + # 时间处理 + start_time = None + end_time = None + if hasattr(zs, "start_time") and zs.start_time is not None: + start_time = str(zs.start_time) + if hasattr(zs, "end_time") and zs.end_time is not None: + end_time = str(zs.end_time) + + result.append({ + "dataset_version": self.DATASET_VERSION, + "feature_schema": self.FEATURE_SCHEMA, + "label_schema": self.LABEL_SCHEMA, + + "symbol": self.symbol, + "timeframe": self.timeframe, + "zs_index": r["zs_index"], + "zs_start_time": start_time, + "zs_end_time": end_time, + + "duration_norm": round(duration_norm, 4), + "contraction": round(r["contraction"], 4), + "shift_norm": round(r["shift_norm"], 4), + + "label": label_info["label"], + "label_confidence": label_info["label_confidence"], + "label_detail": label_info["label_detail"], + + "duration_raw": r["duration_raw"], + "shift_raw": round(r["shift_raw"], 6), + "zs_height": round(r["zs_height"], 6), + }) + + return result + + def export_json(self, path: str): + """导出为 JSON 文件""" + data = self.extract() + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False, default=str) + return len(data) diff --git a/chan/analysis/ChanPivotMonitor.py b/chan/analysis/ChanPivotMonitor.py new file mode 100644 index 0000000..671bfc4 --- /dev/null +++ b/chan/analysis/ChanPivotMonitor.py @@ -0,0 +1,145 @@ +""" +实时中枢特征跟踪器 +Real-time Pivot Feature Tracker + +定位: 观察者 — 不修改管线,只观察 bi_zs_list 中当前中枢的特征变化。 +每次管线重算后调用 update(),检测 bi_count 是否增长,若增长则重新计算 +shift / contraction / duration。 +""" + +from collections import deque +from typing import Optional +from .ChanPivotClassifier import ChanPivotClassifier + + +class ChanPivotMonitor: + """ + 实时追踪当前中枢的结构特征。 + + update() 每次管线重算后调用,对比 bi_count 判断是否有新笔加入中枢。 + 若 bi_count 增长则重新计算 3 个结构特征并返回最新值。 + """ + + def __init__(self, window_size: int = 10): + self._window_size = window_size + self._duration_history: deque[int] = deque(maxlen=window_size) + self._current_zs_id: Optional[tuple] = None + self._current_bi_count: int = 0 + self._current_is_sure: bool = False + self._current_state: Optional[dict] = None + self._duration_added_for_zs: set = set() # 已加入窗口的中枢 ID(上限 200) + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + def update(self, bi_zs_list: list) -> Optional[dict]: + """ + 主入口:检测当前中枢特征变化。 + + 参数: + bi_zs_list: 当前管线产出的笔中枢列表 + + 返回: + 特征 dict(有变化时),无变化返回 None + """ + if not bi_zs_list: + self._current_zs_id = None + self._current_bi_count = 0 + self._current_is_sure = False + self._current_state = None + return None + + zs = self._find_current_zs(bi_zs_list) + if zs is None: + return None + + zs_id = self._make_zs_id(zs) + bi_count = len(zs.bi_list) + is_sure = zs.is_sure + + # 无变化 → 跳过 + if (zs_id == self._current_zs_id + and bi_count == self._current_bi_count + and is_sure == self._current_is_sure): + return None + + # 中枢切换 → 将旧中枢 duration 加入窗口 + if zs_id != self._current_zs_id: + self._maybe_add_to_history() + + self._current_zs_id = zs_id + self._current_bi_count = bi_count + self._current_is_sure = is_sure + + features = ChanPivotClassifier.compute_features( + zs, list(self._duration_history) + ) + + self._current_state = { + "zs_id": zs_id, + "zs_index": zs.index, + "zs_dir": str(zs.dir), + "bi_count": bi_count, + "is_sure": zs.is_sure, + "zg": round(zs.zg, 6), + "zd": round(zs.zd, 6), + "gg": round(zs.gg, 6), + "dd": round(zs.dd, 6), + **features, + "start_time": str(t) if (t := getattr(zs, "start_time", None)) else None, + } + + # 中枢刚变为已确认时,将其 duration 加入滚动窗口 + if is_sure and zs_id not in self._duration_added_for_zs: + self._add_duration(features["duration_raw"]) + self._duration_added_for_zs.add(zs_id) + + return self._current_state + + def get_current(self) -> Optional[dict]: + """返回当前中枢的最新特征""" + return self._current_state + + def get_duration_history(self) -> list[int]: + """返回用于归一化的 duration 滚动窗口""" + return list(self._duration_history) + + # ------------------------------------------------------------------ + # Internal + # ------------------------------------------------------------------ + + @staticmethod + def _make_zs_id(zs) -> tuple: + """生成中枢的稳定标识(基于首笔首K线时间戳,不随 DataFrame 窗口偏移而变化)""" + bi0 = zs.bi_list[0] + return (bi0.start_klc.start_time,) + + @staticmethod + def _find_current_zs(bi_zs_list: list): + """ + 找到当前活跃中枢: + 优先取最后一个 is_sure=False(形成中)的中枢, + 没有则取最后一个 is_sure=True 的中枢。 + """ + forming = None + last_sure = None + for zs in bi_zs_list: + if len(zs.bi_list) < 3: + continue + if not zs.is_sure: + forming = zs + else: + last_sure = zs + return forming if forming is not None else last_sure + + def _add_duration(self, duration_raw: int): + """将已确认中枢的 duration 加入滚动窗口""" + self._duration_history.append(duration_raw) + + def _maybe_add_to_history(self): + """旧中枢切换前,若已确认且未记录过,则将其 duration 加入窗口""" + if (self._current_state and self._current_state["is_sure"] + and self._current_zs_id not in self._duration_added_for_zs): + self._add_duration(self._current_state["duration_raw"]) + self._duration_added_for_zs.add(self._current_zs_id) diff --git a/chan/analysis/ChanZone.py b/chan/analysis/ChanZone.py new file mode 100644 index 0000000..2245d76 --- /dev/null +++ b/chan/analysis/ChanZone.py @@ -0,0 +1,566 @@ +""" +结构价值区 (Structure Zone) 系统 + +将多时间周期的 Chan 中枢边界 (ZD/ZG/GG/DD) 和 EMA52 统一表示为带强度评分的价值区对象。 +""" + +from dataclasses import dataclass, field +from typing import List, Dict, Optional, Any +from datetime import datetime + + +# ============================================================ +# Dataclasses +# ============================================================ + +@dataclass +class RawZonePoint: + """内部中间结构:从 Chan 中枢提取的单个价格点""" + price: float + timeframe: str # '5m', '1h', '4h' 等 + structure_type: str # 'bi_zhongshu' | 'xd_zhongshu' | 'ema52' + boundary_type: str # 'ZD' | 'ZG' | 'GG' | 'DD' | 'EMA52' + source_zs_id: int # 来源 ZS 在列表中的 index(调试用) + is_sure: bool # 来源 ZS 是否已完成 + candle_time: Optional[str] = None # 来源 ZS 的 end_time(用于 recency 计算) + + +@dataclass +class StructureZone: + """统一的价值区对象""" + id: int + lower: float + upper: float + center: float # (lower + upper) / 2 + width_pct: float # (upper - lower) / center * 100 + zone_type: str # 'support' | 'resistance' | 'neutral' + timeframes: List[str] # 参与形成此区间的时间周期 + structure_types: List[str] # 参与形成的结构类型 + boundary_types: List[str] # 参与形成的边界类型 + overlap_count: int # 聚类中的原始点数 + touch_count: int # MVP: 等于 overlap_count + recency_score: float # 0.0 - 1.0, 1.0 = 最近 + ema52_distance_pct: float # 到最近 EMA52 的距离百分比 + ema52_aligned: bool # 是否有 EMA52 落在区间内 + strength_score: float # 0-100 综合评分 + confidence: float # 0.0 - 1.0 + first_seen: Optional[str] # 最早的 candle_time + last_seen: Optional[str] # 最晚的 candle_time + metadata: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class StructureZoneConfig: + """StructureZone 提取与评分配置""" + cluster_radius_pct: float = 0.5 # 价格聚类半径(百分比) + min_overlap_for_zone: int = 2 # 最少重叠点数才能形成区间 + max_zones: int = 20 # 返回的最大区间数 + recency_halflife_bars: int = 50 # recency 衰减半衰期(K线数) + zone_timeframes: List[str] = field(default_factory=lambda: ['4h', '1h', '30m', '15m', '5m']) + kl_lines_per_tf: int = 500 # 每个时间周期使用最近多少根K线 + structure_weights: Dict[str, float] = field(default_factory=lambda: { + 'bi_zhongshu': 1.0, # 笔中枢 — 最直接的价格行为 + 'xd_zhongshu': 0.8, # 线段中枢 — 较高级别但粒度较粗 + 'ema52': 0.4, # EMA — 趋势参考,弱于结构 + }) + + +# ============================================================ +# Extraction +# ============================================================ + +def extract_raw_points_from_tf_df( + tf_df_dict: Dict[str, Any], + ema_symbols: List[str], + config: StructureZoneConfig, +) -> List[RawZonePoint]: + """ + 从 ChanLun.tf_df_dict 中提取所有原始价格点。 + 仅处理 config.zone_timeframes 中存在的时间周期。 + """ + points: List[RawZonePoint] = [] + + for tf_name in config.zone_timeframes: + if tf_name not in tf_df_dict: + continue + + tf_df = tf_df_dict[tf_name] + + # 1. 笔中枢 (ChanBIZS) + try: + if hasattr(tf_df, 'seg_list') and tf_df.seg_list: + bi_zs_result = tf_df.cal_bi_zs(tf_df.seg_list) + if bi_zs_result: + _extract_from_zs_objects( + points, tf_name, 'bi_zhongshu', bi_zs_result, config.kl_lines_per_tf + ) + except Exception: + pass + + # 2. 线段中枢 (ChanZS) + try: + zs_list = getattr(tf_df, 'zs_list', None) + if zs_list: + _extract_from_zs_objects( + points, tf_name, 'xd_zhongshu', zs_list, config.kl_lines_per_tf + ) + except Exception: + pass + + # 3. EMA52 值 + for tf_name in config.zone_timeframes: + if tf_name in tf_df_dict: + try: + ema_val = tf_df_dict[tf_name].get_ema52() + if ema_val is not None and ema_val > 0: + points.append(RawZonePoint( + price=float(ema_val), + timeframe=tf_name, + structure_type='ema52', + boundary_type='EMA52', + source_zs_id=-1, + is_sure=True, + candle_time=None, + )) + except Exception: + pass + + return points + + +def _extract_from_zs_objects( + points: List[RawZonePoint], + tf_name: str, + structure_type: str, + zs_list, + kl_limit: int, +): + """从 ZS 链表中提取 ZD/ZG/GG/DD 点""" + count = 0 + node = zs_list + while hasattr(node, 'next'): + node = node.next + # 从链表头开始遍历 + head = zs_list + # 收集所有节点 + all_nodes = [] + cur = head + while cur is not None and hasattr(cur, 'next'): + all_nodes.append(cur) + cur = cur.next + # 只取最近 kl_limit 根K线内的 ZS + all_nodes = all_nodes[-kl_limit:] if len(all_nodes) > kl_limit else all_nodes + + for idx, zs in enumerate(all_nodes): + if not getattr(zs, 'is_sure', False): + continue + try: + zg = float(zs.zg) + zd = float(zs.zd) + gg = float(zs.gg) if getattr(zs, 'gg', 0) else zg + dd = float(zs.dd) if getattr(zs, 'dd', 0) else zd + end_time = str(zs.end_time) if hasattr(zs, 'end_time') and zs.end_time else None + except (ValueError, TypeError, AttributeError): + continue + + if zg <= 0 or zd <= 0: + continue + + zs_id = getattr(zs, 'index', idx) + points.append(RawZonePoint(price=zg, timeframe=tf_name, structure_type=structure_type, + boundary_type='ZG', source_zs_id=zs_id, is_sure=True, + candle_time=end_time)) + points.append(RawZonePoint(price=zd, timeframe=tf_name, structure_type=structure_type, + boundary_type='ZD', source_zs_id=zs_id, is_sure=True, + candle_time=end_time)) + points.append(RawZonePoint(price=gg, timeframe=tf_name, structure_type=structure_type, + boundary_type='GG', source_zs_id=zs_id, is_sure=True, + candle_time=end_time)) + points.append(RawZonePoint(price=dd, timeframe=tf_name, structure_type=structure_type, + boundary_type='DD', source_zs_id=zs_id, is_sure=True, + candle_time=end_time)) + + +def extract_raw_points_from_serialized( + analyses: Dict[str, Dict], + ema52_dict: Dict[str, Optional[float]], + config: StructureZoneConfig, +) -> List[RawZonePoint]: + """ + 从已序列化的分析结果中提取价格点(用于 web API,避免重复计算)。 + analyses: {'5m': {'zs_list': [...], 'bi_zs_list': [...]}, '15m': {...}, ...} + ema52_dict: {'5m': 123.45, '15m': None, ...} + """ + points: List[RawZonePoint] = [] + + for tf_name in config.zone_timeframes: + if tf_name not in analyses: + continue + + analysis = analyses[tf_name] + + # 笔中枢 + bi_zs_items = analysis.get('bi_zs_list', []) + for idx, zs in enumerate(bi_zs_items): + if not zs.get('is_sure', False): + continue + try: + zg = float(zs['zg']); zd = float(zs['zd']) + gg = float(zs.get('gg', zg)); dd = float(zs.get('dd', zd)) + end_time = zs.get('end_time') + except (ValueError, KeyError): + continue + if zg <= 0 or zd <= 0: + continue + points.append(RawZonePoint(price=zg, timeframe=tf_name, structure_type='bi_zhongshu', + boundary_type='ZG', source_zs_id=idx, is_sure=True, + candle_time=str(end_time) if end_time else None)) + points.append(RawZonePoint(price=zd, timeframe=tf_name, structure_type='bi_zhongshu', + boundary_type='ZD', source_zs_id=idx, is_sure=True, + candle_time=str(end_time) if end_time else None)) + points.append(RawZonePoint(price=gg, timeframe=tf_name, structure_type='bi_zhongshu', + boundary_type='GG', source_zs_id=idx, is_sure=True, + candle_time=str(end_time) if end_time else None)) + points.append(RawZonePoint(price=dd, timeframe=tf_name, structure_type='bi_zhongshu', + boundary_type='DD', source_zs_id=idx, is_sure=True, + candle_time=str(end_time) if end_time else None)) + + # 线段中枢 + zs_items = analysis.get('zs_list', []) + for idx, zs in enumerate(zs_items): + if not zs.get('is_sure', False): + continue + try: + zg = float(zs['zg']); zd = float(zs['zd']) + gg = float(zs.get('gg', zg)); dd = float(zs.get('dd', zd)) + end_time = zs.get('end_time') + except (ValueError, KeyError): + continue + if zg <= 0 or zd <= 0: + continue + points.append(RawZonePoint(price=zg, timeframe=tf_name, structure_type='xd_zhongshu', + boundary_type='ZG', source_zs_id=idx, is_sure=True, + candle_time=str(end_time) if end_time else None)) + points.append(RawZonePoint(price=zd, timeframe=tf_name, structure_type='xd_zhongshu', + boundary_type='ZD', source_zs_id=idx, is_sure=True, + candle_time=str(end_time) if end_time else None)) + points.append(RawZonePoint(price=gg, timeframe=tf_name, structure_type='xd_zhongshu', + boundary_type='GG', source_zs_id=idx, is_sure=True, + candle_time=str(end_time) if end_time else None)) + points.append(RawZonePoint(price=dd, timeframe=tf_name, structure_type='xd_zhongshu', + boundary_type='DD', source_zs_id=idx, is_sure=True, + candle_time=str(end_time) if end_time else None)) + + # EMA52 + for tf_name in config.zone_timeframes: + ema_val = ema52_dict.get(tf_name) + if ema_val is not None and ema_val > 0: + points.append(RawZonePoint( + price=float(ema_val), + timeframe=tf_name, + structure_type='ema52', + boundary_type='EMA52', + source_zs_id=-1, + is_sure=True, + candle_time=None, + )) + + return points + + +# ============================================================ +# Clustering +# ============================================================ + +def cluster_raw_points( + points: List[RawZonePoint], + config: StructureZoneConfig, +) -> List[List[RawZonePoint]]: + """ + 贪心单通聚类:将价格相近的 RawZonePoint 归为一组。 + 仅在 1D 价格轴上操作,O(n log n)。 + """ + if not points: + return [] + + sorted_points = sorted(points, key=lambda p: p.price) + clusters: List[List[RawZonePoint]] = [] + + for p in sorted_points: + placed = False + for cluster in reversed(clusters): + # 检查是否可以放入当前聚类(与聚类均价比较) + avg_price = sum(pt.price for pt in cluster) / len(cluster) + if abs(p.price - avg_price) / avg_price * 100 <= config.cluster_radius_pct: + cluster.append(p) + placed = True + break + if not placed: + clusters.append([p]) + + # 过滤点数不足的聚类 + return [c for c in clusters if len(c) >= config.min_overlap_for_zone] + + +# ============================================================ +# Scoring & Building +# ============================================================ + +def build_structure_zones( + clusters: List[List[RawZonePoint]], + current_price: float, + ema52_values: Dict[str, Optional[float]], + latest_candle_time: Optional[str], + config: StructureZoneConfig, +) -> List[StructureZone]: + """ + 从聚类构建 StructureZone 列表,计算所有字段和评分。 + """ + zones: List[StructureZone] = [] + + # 收集所有 EMA52 值 + ema_prices = [v for v in ema52_values.values() if v is not None and v > 0] + + for zone_id, cluster in enumerate(clusters): + prices = [p.price for p in cluster] + lower = min(prices) + upper = max(prices) + center = (lower + upper) / 2 + width_pct = (upper - lower) / center * 100 if center > 0 else 0.0 + + # 区间类型 + if upper < current_price: + zone_type = 'support' # 区间在当前价格下方 → 支撑 + elif lower > current_price: + zone_type = 'resistance' # 区间在当前价格上方 → 阻力 + else: + zone_type = 'neutral' # 区间跨越当前价格 + + timeframes = sorted(set(p.timeframe for p in cluster)) + structure_types = sorted(set(p.structure_type for p in cluster)) + boundary_types = sorted(set(p.boundary_type for p in cluster)) + overlap_count = len(cluster) + + # Recency + times = [p.candle_time for p in cluster if p.candle_time] + first_seen = min(times) if times else None + last_seen = max(times) if times else None + recency_score = _calc_recency(last_seen, latest_candle_time, config.recency_halflife_bars) + + # EMA52 alignment + ema52_distance_pct = 999.0 + ema52_aligned = False + if ema_prices: + distances = [abs(center - ep) / ep * 100 for ep in ema_prices] + ema52_distance_pct = round(min(distances), 2) + ema52_aligned = any(lower <= ep <= upper for ep in ema_prices) + + # Strength score + strength_score = _calc_strength(cluster, config, recency_score, ema52_aligned, ema52_distance_pct, width_pct) + + # Confidence + confidence = _calc_confidence(overlap_count, len(timeframes), cluster) + + zones.append(StructureZone( + id=zone_id + 1, + lower=round(lower, 2), + upper=round(upper, 2), + center=round(center, 2), + width_pct=round(width_pct, 2), + zone_type=zone_type, + timeframes=timeframes, + structure_types=structure_types, + boundary_types=boundary_types, + overlap_count=overlap_count, + touch_count=overlap_count, # MVP: 等于 overlap_count + recency_score=round(recency_score, 3), + ema52_distance_pct=ema52_distance_pct, + ema52_aligned=ema52_aligned, + strength_score=round(strength_score, 1), + confidence=round(confidence, 2), + first_seen=first_seen, + last_seen=last_seen, + )) + + # 按强度降序排列 + zones.sort(key=lambda z: z.strength_score, reverse=True) + + # 截断 + if config.max_zones > 0 and len(zones) > config.max_zones: + zones = zones[:config.max_zones] + + return zones + + +def _calc_recency( + last_seen: Optional[str], + latest_time: Optional[str], + halflife_bars: int, +) -> float: + """计算 recency 分数:越近越高""" + if not last_seen or not latest_time: + return 0.5 + + try: + # 尝试解析 ISO 格式时间 + from dateutil import parser + t_last = parser.parse(last_seen) + t_latest = parser.parse(latest_time) + offset_seconds = (t_latest - t_last).total_seconds() + if offset_seconds < 0: + return 1.0 + # 假设每根K线平均 5 分钟 + bar_seconds = 300 + offset_bars = offset_seconds / bar_seconds + # 指数衰减: 2 ^ (-offset / halflife) + score = 2.0 ** (-offset_bars / halflife_bars) + return float(score) + except Exception: + return 0.5 + + +def _calc_strength( + cluster: List[RawZonePoint], + config: StructureZoneConfig, + recency_score: float, + ema52_aligned: bool, + ema52_distance_pct: float, + width_pct: float, +) -> float: + """计算综合强度评分 (0-100)""" + + # 组件 1: 结构类型多样性 (0-40) + structure_type_counts: Dict[str, int] = {} + for p in cluster: + structure_type_counts[p.structure_type] = structure_type_counts.get(p.structure_type, 0) + 1 + total = sum(structure_type_counts.values()) + structure_score = 0.0 + for st, count in structure_type_counts.items(): + weight = config.structure_weights.get(st, 0.5) + structure_score += weight * count + structure_score = min(structure_score / max(1, total), 1.0) + c1 = structure_score * 40 + + # 组件 2: 多周期确认 (0-25) + tf_set = set(p.timeframe for p in cluster) + tf_diversity = len(tf_set) + c2 = min(tf_diversity / 5, 1.0) * 25 + + # 组件 3: 区间紧密度 (0-15) — 越窄越强 + tightness = max(0.0, 1.0 - (width_pct / 3.0)) + c3 = tightness * 15 + + # 组件 4: Recency (0-10) + c4 = recency_score * 10 + + # 组件 5: EMA52 共振 (0-10) + if ema52_aligned: + ema_proximity = max(0.0, 1.0 - (ema52_distance_pct / 2.0)) + c5 = ema_proximity * 10 + else: + c5 = 0.0 + + return c1 + c2 + c3 + c4 + c5 + + +def _calc_confidence( + overlap_count: int, + tf_count: int, + cluster: List[RawZonePoint], +) -> float: + """计算置信度 (0-1)""" + base = min(overlap_count / 6.0, 0.85) + # 多周期加分 + tf_bonus = min(tf_count / 5.0, 0.1) + # 是否所有点都来自 sure 的 ZS + all_sure = all(p.is_sure for p in cluster) + sure_bonus = 0.05 if all_sure else 0.0 + return min(base + tf_bonus + sure_bonus, 1.0) + + +# ============================================================ +# Top-level pipeline +# ============================================================ + +def analyze_structure_zones( + tf_df_dict: Dict[str, Any], + ema_symbols: List[str], + current_price: Optional[float] = None, + config: Optional[StructureZoneConfig] = None, +) -> List[StructureZone]: + """ + 一站式分析:提取 → 聚类 → 评分 → 返回排序后的 StructureZone 列表。 + """ + if config is None: + config = StructureZoneConfig() + + # 提取 + raw_points = extract_raw_points_from_tf_df(tf_df_dict, ema_symbols, config) + + if not raw_points: + return [] + + # 获取当前价格 + if current_price is None: + for tf_name in config.zone_timeframes: + if tf_name in tf_df_dict: + try: + ema_val = tf_df_dict[tf_name].get_ema52() + if ema_val and ema_val > 0: + current_price = float(ema_val) + break + except Exception: + pass + if current_price is None: + current_price = 0.0 + + # EMA52 值 + ema52_values = {} + for tf_name in config.zone_timeframes: + if tf_name in tf_df_dict: + try: + ema52_values[tf_name] = tf_df_dict[tf_name].get_ema52() + except Exception: + ema52_values[tf_name] = None + + # 最晚时间 + latest_time = None + times = [p.candle_time for p in raw_points if p.candle_time] + if times: + latest_time = max(times) + + # 聚类 + clusters = cluster_raw_points(raw_points, config) + + # 构建 & 评分 + return build_structure_zones(clusters, current_price, ema52_values, latest_time, config) + + +def analyze_structure_zones_from_serialized( + analyses: Dict[str, Dict], + ema52_dict: Dict[str, Optional[float]], + current_price: float, + config: Optional[StructureZoneConfig] = None, +) -> List[StructureZone]: + """ + 从已序列化的分析结果构建 StructureZone(用于 web API)。 + """ + if config is None: + config = StructureZoneConfig() + + raw_points = extract_raw_points_from_serialized(analyses, ema52_dict, config) + + if not raw_points: + return [] + + # 最晚时间 + latest_time = None + times = [p.candle_time for p in raw_points if p.candle_time] + if times: + latest_time = max(times) + + # EMA52 值(用于 alignment 检测) + ema_values = {tf: v for tf, v in ema52_dict.items() if v is not None and v > 0} + + clusters = cluster_raw_points(raw_points, config) + return build_structure_zones(clusters, current_price, ema_values, latest_time, config) diff --git a/chan/analysis/Find_Trend.py b/chan/analysis/Find_Trend.py new file mode 100644 index 0000000..8d392a3 --- /dev/null +++ b/chan/analysis/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/chan/analysis/__init__.py b/chan/analysis/__init__.py new file mode 100644 index 0000000..3a65746 --- /dev/null +++ b/chan/analysis/__init__.py @@ -0,0 +1,19 @@ +"""分析层:结构 + 指标的接合(MACD 状态、买卖点确认、Zone 等)。""" + +from .bsp_macd import ( + ConfirmedBSP, + bi_macd_area, + check_bi_div, + check_bi_pair_div, + confirm_bsp, +) +from .ChanMACD import ChanMACD + +__all__ = [ + "ChanMACD", + "ConfirmedBSP", + "bi_macd_area", + "check_bi_div", + "check_bi_pair_div", + "confirm_bsp", +] diff --git a/chan/analysis/bsp_macd.py b/chan/analysis/bsp_macd.py new file mode 100644 index 0000000..34cb149 --- /dev/null +++ b/chan/analysis/bsp_macd.py @@ -0,0 +1,113 @@ +"""买卖点 × MACD:几何候选在 core,背驰确认在此接合。""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, List, Optional, Sequence + +from ..core.ChanBI import ChanBI +from ..core.ChanBSP import ChanBSP +from ..core.ChanEnum import Chan_BI_DIR, Chan_BSP_DIR, Chan_BSP_TYPE +from ..indicators.store import IndicatorStore + + +def bi_macd_area(bi: ChanBI, store: Optional[IndicatorStore] = None) -> float: + """笔内同向 macdhist 累积面积。优先用 IndicatorStore,否则回退 klu.macdhist。""" + area = 0.0 + for klc in bi.klc_list: + for klu in klc.klu_list: + if store is not None: + hist = store.get(klu.idx, "macdhist", 0) or 0 + else: + hist = getattr(klu, "macdhist", 0) or 0 + try: + hist = float(hist) + except (TypeError, ValueError): + hist = 0.0 + if bi.dir == Chan_BI_DIR.UP and hist > 0: + area += hist + elif bi.dir == Chan_BI_DIR.DOWN and hist < 0: + area -= hist + return area + + +def check_bi_div( + zs, + leave_bi: ChanBI, + store: Optional[IndicatorStore] = None, +) -> bool: + """一类买卖点背驰:离开笔相对进入笔同向 MACD 柱面积收敛。""" + enter_bi = zs.bi_list[0].pre if zs.bi_list else None + if not enter_bi or enter_bi.dir != leave_bi.dir: + return False + leave_area = abs(bi_macd_area(leave_bi, store)) + enter_area = abs(bi_macd_area(enter_bi, store)) + return leave_area < enter_area + + +def check_bi_pair_div( + leave_bi: ChanBI, + compare_bi: ChanBI, + store: Optional[IndicatorStore] = None, +) -> bool: + """两笔同向力度比较(离开笔面积 < 比较笔)。""" + leave_area = abs(bi_macd_area(leave_bi, store)) + compare_area = abs(bi_macd_area(compare_bi, store)) + return leave_area < compare_area + + +@dataclass +class ConfirmedBSP: + """几何买卖点 + MACD 确认结果。""" + + bsp: ChanBSP + div_confirmed: bool + hist_area: float = 0.0 + compare_hist_area: float = 0.0 + score: float = 0.0 + macd_state: Any = None + + @property + def type(self) -> Chan_BSP_TYPE: + return self.bsp.type + + @property + def dir(self) -> Chan_BSP_DIR: + return self.bsp.dir + + @property + def bi(self) -> ChanBI: + return self.bsp.bi + + +def confirm_bsp( + geo_bsp_list: Sequence[ChanBSP], + store: Optional[IndicatorStore] = None, + require_div_for_types: Optional[Sequence[Chan_BSP_TYPE]] = None, +) -> List[ConfirmedBSP]: + """ + 将几何 BSP 升格为 ConfirmedBSP。 + 默认:B1/S1/T1 需要背驰确认;B3/S3 几何即可,div_confirmed=True。 + """ + if require_div_for_types is None: + require_div_for_types = ( + Chan_BSP_TYPE.B1, + Chan_BSP_TYPE.S1, + ) + out: List[ConfirmedBSP] = [] + for bsp in geo_bsp_list: + area = bi_macd_area(bsp.bi, store) + need_div = bsp.type in require_div_for_types + if need_div and bsp.zs is not None: + div_ok = check_bi_div(bsp.zs, bsp.bi, store) + else: + div_ok = True + out.append( + ConfirmedBSP( + bsp=bsp, + div_confirmed=div_ok, + hist_area=area, + score=1.0 if div_ok else 0.0, + ) + ) + return out diff --git a/chan/analysis/fx_strength_config.py b/chan/analysis/fx_strength_config.py new file mode 100644 index 0000000..41e9faa --- /dev/null +++ b/chan/analysis/fx_strength_config.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +分型强度检测配置文件 +用于调整分型强度计算的各项参数和权重 +""" + +class FxStrengthConfig: + """分型强度检测配置类""" + + def __init__(self): + # ===== 权重配置 (总分100分) ===== + self.price_difference_weight = 40 # 价格差异强度权重 + self.breakthrough_weight = 20 # 突破历史点位权重 + self.volume_weight = 15 # 成交量确认权重 + self.rsi_divergence_weight = 15 # RSI背离权重 + self.macd_divergence_weight = 10 # MACD背离权重 + + # ===== 价格差异参数 ===== + self.price_diff_multiplier = 1000 # 价格差异放大倍数 + self.max_price_score = 20 # 价格差异最高得分 + + # ===== 突破检测参数 ===== + self.breakthrough_lookback = 10 # 回看K线数量 + self.breakthrough_multiplier = 500 # 突破幅度放大倍数 + self.max_breakthrough_score = 20 # 突破最高得分 + + # ===== 成交量参数 ===== + self.volume_lookback = 5 # 计算平均成交量的回看期数 + self.volume_multiplier = 10 # 成交量放大倍数 + self.max_volume_score = 15 # 成交量最高得分 + self.min_volume_ratio = 1.0 # 最小成交量比率 + + # ===== RSI背离参数 ===== + self.rsi_divergence_divisor = 2 # RSI背离除数 + self.max_rsi_score = 15 # RSI最高得分 + + # ===== MACD背离参数 ===== + self.macd_divergence_multiplier = 100 # MACD背离放大倍数 + self.max_macd_score = 10 # MACD最高得分 + + # ===== 强度等级阈值 ===== + self.extreme_threshold = 80 # 极强分型阈值 + self.strong_threshold = 60 # 强分型阈值 + self.medium_threshold = 40 # 中等分型阈值 + self.weak_threshold = 20 # 弱分型阈值 + + # ===== 其他参数 ===== + self.min_strength = 0 # 最小强度分数 + self.max_strength = 100 # 最大强度分数 + + def get_strength_level_name(self, strength): + """根据强度分数获取等级名称""" + if strength >= self.extreme_threshold: + return "极强" + elif strength >= self.strong_threshold: + return "强" + elif strength >= self.medium_threshold: + return "中等" + elif strength >= self.weak_threshold: + return "弱" + else: + return "极弱" + + def is_strong_fractal(self, strength, custom_threshold=None): + """判断是否为强分型""" + threshold = custom_threshold if custom_threshold is not None else self.strong_threshold + return strength >= threshold + + def validate_config(self): + """验证配置参数的合理性""" + total_weight = (self.price_difference_weight + + self.breakthrough_weight + + self.volume_weight + + self.rsi_divergence_weight + + self.macd_divergence_weight) + + if total_weight != 100: + print(f"警告: 权重总和为{total_weight},不等于100") + + if not (0 <= self.extreme_threshold <= 100): + print(f"警告: 极强阈值{self.extreme_threshold}不在合理范围内") + + if not (self.weak_threshold < self.medium_threshold < + self.strong_threshold < self.extreme_threshold): + print("警告: 强度阈值设置不合理") + + return True + + def print_config(self): + """打印当前配置""" + print("=== 分型强度检测配置 ===") + print(f"价格差异权重: {self.price_difference_weight}分") + print(f"突破点位权重: {self.breakthrough_weight}分") + print(f"成交量权重: {self.volume_weight}分") + print(f"RSI背离权重: {self.rsi_divergence_weight}分") + print(f"MACD背离权重: {self.macd_divergence_weight}分") + print() + print("=== 强度等级阈值 ===") + print(f"极强: >={self.extreme_threshold}分") + print(f"强: {self.strong_threshold}-{self.extreme_threshold-1}分") + print(f"中等: {self.medium_threshold}-{self.strong_threshold-1}分") + print(f"弱: {self.weak_threshold}-{self.medium_threshold-1}分") + print(f"极弱: <{self.weak_threshold}分") + + +# 默认配置实例 +DEFAULT_CONFIG = FxStrengthConfig() + +# 保守配置 (更严格的分型识别) +CONSERVATIVE_CONFIG = FxStrengthConfig() +CONSERVATIVE_CONFIG.price_difference_weight = 50 +CONSERVATIVE_CONFIG.breakthrough_weight = 25 +CONSERVATIVE_CONFIG.volume_weight = 15 +CONSERVATIVE_CONFIG.rsi_divergence_weight = 10 +CONSERVATIVE_CONFIG.macd_divergence_weight = 0 +CONSERVATIVE_CONFIG.strong_threshold = 70 +CONSERVATIVE_CONFIG.extreme_threshold = 85 + +# 激进配置 (更宽松的分型识别) +AGGRESSIVE_CONFIG = FxStrengthConfig() +AGGRESSIVE_CONFIG.price_difference_weight = 30 +AGGRESSIVE_CONFIG.breakthrough_weight = 15 +AGGRESSIVE_CONFIG.volume_weight = 20 +AGGRESSIVE_CONFIG.rsi_divergence_weight = 20 +AGGRESSIVE_CONFIG.macd_divergence_weight = 15 +AGGRESSIVE_CONFIG.strong_threshold = 50 +AGGRESSIVE_CONFIG.extreme_threshold = 70 + +# 技术指标重点配置 (重视技术指标背离) +TECHNICAL_CONFIG = FxStrengthConfig() +TECHNICAL_CONFIG.price_difference_weight = 25 +TECHNICAL_CONFIG.breakthrough_weight = 15 +TECHNICAL_CONFIG.volume_weight = 10 +TECHNICAL_CONFIG.rsi_divergence_weight = 25 +TECHNICAL_CONFIG.macd_divergence_weight = 25 + + +if __name__ == "__main__": + print("=== 分型强度配置演示 ===\n") + + configs = { + "默认配置": DEFAULT_CONFIG, + "保守配置": CONSERVATIVE_CONFIG, + "激进配置": AGGRESSIVE_CONFIG, + "技术指标配置": TECHNICAL_CONFIG + } + + for name, config in configs.items(): + print(f"=== {name} ===") + config.print_config() + config.validate_config() + print() \ No newline at end of file diff --git a/fx_strength_example.py b/chan/analysis/fx_strength_example.py similarity index 97% rename from fx_strength_example.py rename to chan/analysis/fx_strength_example.py index 40814af..2462d0b 100644 --- a/fx_strength_example.py +++ b/chan/analysis/fx_strength_example.py @@ -5,9 +5,9 @@ 该文件展示如何使用ChanKLC类中新增的分型强度检测功能 """ -from ChanKLC import ChanKLC -from ChanEnum import Chan_FX_TYPE -import ChanKLU +from ..core.ChanKLC import ChanKLC +from ..core.ChanEnum import Chan_FX_TYPE +from ..core import ChanKLU def demo_fx_strength_detection(): diff --git a/realtime_fx_example.py b/chan/analysis/realtime_fx_example.py similarity index 98% rename from realtime_fx_example.py rename to chan/analysis/realtime_fx_example.py index 8c95897..3ee816c 100644 --- a/realtime_fx_example.py +++ b/chan/analysis/realtime_fx_example.py @@ -5,8 +5,8 @@ 解决KLC滞后问题,提供即时的分型信号 """ -from ChanKLU import ChanKLU -from ChanEnum import Chan_FX_TYPE +from ..core.ChanKLU import ChanKLU +from ..core.ChanEnum import Chan_FX_TYPE import pandas as pd from datetime import datetime, timedelta diff --git a/test_classifier.py b/chan/analysis/test_classifier.py similarity index 98% rename from test_classifier.py rename to chan/analysis/test_classifier.py index 1b26570..ee303e7 100644 --- a/test_classifier.py +++ b/chan/analysis/test_classifier.py @@ -9,8 +9,8 @@ import csv import sys import os -# Ensure Chan module is importable -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +# Ensure repo root is importable +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from TF_DF import TF_DF from ChanPivotClassifier import ChanPivotClassifier diff --git a/chan/core/ChanBI.py b/chan/core/ChanBI.py new file mode 100644 index 0000000..54cf257 --- /dev/null +++ b/chan/core/ChanBI.py @@ -0,0 +1,135 @@ +from decimal import Decimal +from . import ChanKLC +from .ChanEnum import Chan_BI_DIR +class ChanBI(): + def __init__(self, klc: ChanKLC, index, ddir=Chan_BI_DIR.UP): + self.start_klc = klc + self.end_klc = klc + self.next = None + self.pre = None + self.dir = ddir + self.index = index + self.is_sure = False + self.high = klc.high + self.low = klc.low + self.sure_time = None + self.klc_list = [] + self.klc_list.append(klc) + self.end_time = klc.end_time + self.start_time = klc.start_time + # macd_hist:由 analysis.bsp_macd / cal_macdhist 填充,非结构固有字段 + self.macd_hist = 0 + self.macd_div = 0 + self.seg = None + self.height = 0 + self.width = 0 + self.slop = 0 + self.fib_list = [] + self.seg_index = 0 + self.bi_zs = None + self.seg_zs = None + def set_bi_zs(self, bi_zs): + for klc in self.klc_list: + klc.set_bi_zs(bi_zs) + def set_seg(self, seg): + self.seg = seg + self.seg_index = len(seg.bi_list)-1 + def set_macdhist(self, macd_hist): + self.macd_hist = macd_hist + def set_macd_div(self, macd_div): + self.macd_div = macd_div + def cal_macd_div(self): + self.macd_div = 0.0 + if self.pre and self.pre.pre: + if self.pre.pre.macd_hist == 0: + self.macd_div = 0.0 + else: + self.macd_div = self.macd_hist / self.pre.pre.macd_hist + #print(self.start_time, self.end_time, self.macd_hist, self.pre.pre.macd_hist, self.macd_div) + def cal_macdhist(self): + """兼容:从已挂载的 klu.macdhist 累加。新代码请用 analysis.bsp_macd.bi_macd_area(bi, store)。""" + self.macd_hist = 0 + for klc in self.klc_list: + for klu in klc.klu_list: + if self.dir == Chan_BI_DIR.UP and klu.macdhist > 0: + self.macd_hist += klu.macdhist + if self.dir == Chan_BI_DIR.DOWN and klu.macdhist < 0: + self.macd_hist -= klu.macdhist + return self.macd_hist + def check_bi_zs_overlap(self): + if self.next and self.next.next: + if self.dir == Chan_BI_DIR.UP: + return self.low < self.next.next.high + else: + return self.high > self.next.next.low + else: + return False + def check_overlap(self): + if self.next and self.next.next and self.next.next.is_sure: + if self.dir == Chan_BI_DIR.UP: + return self.high > self.next.low and self.high < self.next.next.high + else: + return self.high > self.next.high and self.low > self.next.next.low + else: + return False + def set_end_klc(self, klc, sure_klc): + if self.dir == Chan_BI_DIR.UP and klc.high > self.high: + self.high = klc.high + if self.dir == Chan_BI_DIR.DOWN and klc.low < self.low: + self.low = klc.low + self.end_klc = klc + self.set_is_sure(True, sure_klc.end_time) + self.end_time = klc.end_time + self.cal_properties() + #print(self.start_time, klc.fx, "This bi is ended", len(self.klc_list), klc.index - self.start_klc.index) + def cal_properties(self): + if self.is_sure: + self.height = float(format(self.high - self.low, ".2f")) + self.width = self.end_klc.index - self.start_klc.index + self.slop = float(format(self.height / self.width, ".2f")) + fib_list = [0.0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0] + for fib in fib_list: + self.fib_list.append(float(format(self.height * fib + self.low, ".2f"))) + #print(self.end_time, self.height, self.width, self.slop, self.fib_list) + def set_is_sure(self, is_sure, time): + self.is_sure = is_sure + self.sure_time = time + def set_start_klc(self, klc, ddir): + self.start_klc = klc + self.klc_list = [] + self.klc_list.append(klc) + self.high = klc.high + self.low = klc.low + self.dir = ddir + def set_pre(self, bi): + self.pre = bi + def set_next(self, bi): + self.next = bi + def add_klc(self, klc): + added = False + if len(self.klc_list) > 0: + for index in range(0, len(self.klc_list)): + if self.klc_list[index].index == klc.index: + added = True + break + if not added: + self.klc_list.append(klc) + #print(self.start_time, klc.start_time) + #print(klc.end_time, klc.index) + self.end_klc = klc + self.end_time = klc.klu_list[-1].time + self.cal_macdhist() + self.cal_macd_div() + def append_klc_list(self, klc_list): + self.klc_list.append(klc_list) + def get_decimal(self, value): + return Decimal("{:.2f}".format(value)) + def update_bi(self, klc): + self.end_klc = None + if self.dir == Chan_BI_DIR.UP and klc.high > self.high: + self.high = klc.high + if self.dir == Chan_BI_DIR.DOWN and klc.low < self.low: + self.low = klc.low + self.is_sure = False + self.sure_time = None + #print(self.start_time, klc.start_time, klc.fx, "This bi is extended") \ No newline at end of file diff --git a/chan/core/ChanBIZS.py b/chan/core/ChanBIZS.py new file mode 100644 index 0000000..08f1ddb --- /dev/null +++ b/chan/core/ChanBIZS.py @@ -0,0 +1,161 @@ +from .ChanEnum import Chan_ZS_DIR, Chan_ZS_TYPE, Chan_BI_DIR +from . 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.start_bi = start_bi + self.bi_list = [] + self.bi_list.append(start_bi) + self.end_bi = None + self.bi_out = None + self.is_sure = False + self.zg = 0 + self.zd = 0 + self.gg = 0 + self.dd = 0 + self.dir = ddir + self.sure_time = None + self.end_klc = None + self.zs_type = Chan_ZS_TYPE.NORMAL + start_bi.set_bi_zs(self) + def set_end_bi(self, end_bi, sure_time): + self.end_bi = end_bi + self.set_end_time(end_bi.end_klc.end_time) + self.is_sure = True + self.sure_time = sure_time + end_bi.set_bi_zs(self) + #print(self.start_time, self.is_sure, len(self.bi_list), self.dir, self.zs_type) + def set_end_time(self, end_time): + self.end_time = end_time + def set_zg(self, zg): + self.zg = zg + def set_zd(self, zd): + self.zd = zd + def set_gg(self, gg): + self.gg = gg + def set_dd(self, dd): + self.dd = dd + def add_bi(self, bi: ChanBI): + if bi: + self.bi_list.append(bi) + if bi.high > self.gg: + self.gg = bi.high + if bi.low < self.dd: + self.dd = bi.low + bi.set_bi_zs(self) + self.classify_zs() + def set_pre(self, pre): + self.pre = pre + def set_next(self, next): + self.next = next + def classify_zs(self): + """ + 根据中枢内笔的高低点变化趋势,对中枢进行分类 + + 分类逻辑: + - 取中枢内向上笔的高点(peaks)和向下笔的低点(valleys) + - 比较前半段和后半段的均值,判断高点和低点的整体趋势 + + 分类结果: + - RISING 上升中枢:高点抬高 + 低点抬高 → 多方占优,可能向上突破 + - FALLING 下行中枢:高点降低 + 低点降低 → 空方占优,可能向下突破 + - CONVERGING 收敛中枢:高点降低 + 低点抬高 → 区间收窄,即将选择方向 + - DIVERGING 扩散中枢:高点抬高 + 低点降低 → 波动加剧,市场不稳定 + - NORMAL 常规中枢:无明显趋势 → 多空均衡,区间震荡 + """ + if len(self.bi_list) < 3: + self.zs_type = Chan_ZS_TYPE.NORMAL + return + + # 提取向上笔的高点(peaks)和向下笔的低点(valleys) + peaks = [bi.high for bi in self.bi_list if bi.dir == Chan_BI_DIR.UP] + valleys = [bi.low for bi in self.bi_list if bi.dir == Chan_BI_DIR.DOWN] + + high_trend = self._calc_trend(peaks) + low_trend = self._calc_trend(valleys) + + if high_trend > 0 and low_trend > 0: + self.zs_type = Chan_ZS_TYPE.RISING + elif high_trend < 0 and low_trend < 0: + self.zs_type = Chan_ZS_TYPE.FALLING + elif high_trend < 0 and low_trend > 0: + self.zs_type = Chan_ZS_TYPE.CONVERGING + elif high_trend > 0 and low_trend < 0: + self.zs_type = Chan_ZS_TYPE.DIVERGING + else: + self.zs_type = Chan_ZS_TYPE.NORMAL + + def _calc_trend(self, values): + """ + 计算序列的趋势方向 + 将序列分为前后两半,比较均值: + - 后半均值 > 前半均值 → 返回 1(上升趋势) + - 后半均值 < 前半均值 → 返回 -1(下降趋势) + - 相等或数据不足 → 返回 0(无趋势) + + 使用均值比较而非首尾比较,可以过滤单笔异常波动带来的误判 + """ + if len(values) < 2: + return 0 + mid = len(values) // 2 + first_half = values[:mid] if mid > 0 else values[:1] + second_half = values[mid:] + avg_first = sum(first_half) / len(first_half) + avg_second = sum(second_half) / len(second_half) + # 使用中枢区间的一定比例作为阈值,避免微小波动误判 + threshold = abs(avg_first) * 0.005 if avg_first != 0 else 0 + if avg_second - avg_first > threshold: + return 1 + elif avg_first - avg_second > threshold: + return -1 + else: + return 0 + + def is_weakening(self): + """ + 判断中枢是否在衰弱(即将反向突破的信号) + + 衰弱条件: + 1. 中枢内笔数 >= 5(有足够的数据判断) + 2. 最后一笔的MACD面积相比同方向前一笔出现背驰(macd_div < 1) + 3. 中枢类型为收敛型或常规型 + + 返回: True表示中枢力量衰弱,可能反向 + """ + if len(self.bi_list) < 5: + return False + last_bi = self.bi_list[-1] + # 最后一笔与同方向前一笔比较MACD面积是否背驰 + if last_bi.macd_div > 0 and last_bi.macd_div < 1.0: + return True + return False + + def get_zs_strength(self): + """ + 计算中枢强度,用于辅助判断中枢延续还是反向 + + 返回字典包含: + - type: 中枢类型 (Chan_ZS_TYPE) + - bi_count: 中枢内笔数 + - range_ratio: 中枢区间占比 = (zg - zd) / (gg - dd),越小说明中枢越紧密 + - last_bi_div: 最后一笔的MACD背驰比率 + - is_weakening: 是否衰弱 + - is_extending: 是否在延伸(笔数 >= 9 可能升级) + """ + total_range = self.gg - self.dd if self.gg != self.dd else 1 + zs_range = self.zg - self.zd if self.zg != self.zd else 0 + range_ratio = zs_range / total_range if total_range > 0 else 0 + last_bi_div = self.bi_list[-1].macd_div if len(self.bi_list) > 0 else 0 + + return { + 'type': self.zs_type, + 'bi_count': len(self.bi_list), + 'range_ratio': round(range_ratio, 4), + 'last_bi_div': round(last_bi_div, 4), + 'is_weakening': self.is_weakening(), + 'is_extending': len(self.bi_list) >= 9, # 9段可能升级 + } \ No newline at end of file diff --git a/chan/core/ChanBSP.py b/chan/core/ChanBSP.py new file mode 100644 index 0000000..0275d6c --- /dev/null +++ b/chan/core/ChanBSP.py @@ -0,0 +1,24 @@ +from . 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.end_klc + self.index = index + self.type = type + self.start_time = self.klc.start_time + self.end_time = self.klc.end_time + if sure_time: + self.is_sure = True + self.sure_time = sure_time + else: + self.is_sure = False + self.sure_time = None + self.dir = ddir + self.zs_count = zs_count + self.zs = zs + self.seg = bi.seg + def set_sure_time(self, sure_time): + self.is_sure = True + self.sure_time = sure_time \ No newline at end of file diff --git a/chan/core/ChanCTime.py b/chan/core/ChanCTime.py new file mode 100644 index 0000000..04e1bf6 --- /dev/null +++ b/chan/core/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/chan/core/ChanEnum.py b/chan/core/ChanEnum.py new file mode 100644 index 0000000..9164374 --- /dev/null +++ b/chan/core/ChanEnum.py @@ -0,0 +1,368 @@ +from enum import Enum, auto +from typing import Literal + + +class Chan_DATA_SRC(Enum): + BAO_STOCK = auto() + CCXT = auto() + CSV = auto() + +class Chan_ZS_DIR(Enum): + UP = auto() + DOWN = auto() + +class Chan_ZS_TYPE(Enum): + """中枢类型分类""" + NORMAL = auto() # 常规中枢:高低点无明显趋势,区间震荡 + RISING = auto() # 上升中枢:高点抬高,低点也抬高,重心上移 + FALLING = auto() # 下行中枢:高点降低,低点也降低,重心下移 + CONVERGING = auto() # 收敛中枢:高点降低,低点抬高,区间收窄(三角收敛) + DIVERGING = auto() # 扩散中枢:高点抬高,低点降低,区间扩大(喇叭口) +class Chan_K_DIR(Enum): + BULL = auto() + BEAR = auto() + CROSS = auto() + +class Chan_EMA_POS(Enum): + """K线与任意EMA的位置关系(与趋势方向无关的客观分类,支持threshold容差)""" + ABOVE = auto() # 完全在EMA上方(远离):low > ema + threshold + NEAR_ABOVE = auto() # 在EMA上方但接近:ema < low <= ema + threshold + CROSS_CLOSE_ABOVE = auto() # 跨越EMA,收盘在上方:close > ema, low <= ema(含threshold范围内触碰) + ON_EMA = auto() # 收盘价在EMA附近:abs(close - ema) <= threshold + CROSS_CLOSE_BELOW = auto() # 跨越EMA,收盘在下方:close < ema, high >= ema(含threshold范围内触碰) + NEAR_BELOW = auto() # 在EMA下方但接近:ema - threshold <= high < ema + BELOW = auto() # 完全在EMA下方(远离):high < ema - threshold + UNKNOWN = auto() # 未知(EMA值无效) + +class Chan_EMA_SEMANTIC(Enum): + """K线与EMA结合趋势方向的语义状态(用于交易判断)""" + STRONG_TREND = auto() # 7: 顺势K线完全在EMA趋势侧(强势,远未及EMA) + TREND_SIDE = auto() # 6: 完全在EMA趋势侧(正常趋势运行) + RECOVER = auto() # 5: 逆势后穿越EMA回到趋势侧(收复EMA,趋势恢复) + TOUCH_FAIL = auto() # 4: 逆势触碰EMA但未穿越(反弹/反抽力度不足) + DEEP_COUNTER = auto() # 3: 完全在EMA逆势侧(深度回调/反抽) + BREAK = auto() # 2: 穿越EMA,收盘在逆势侧(支撑/压力失败) + TOUCH_HOLD = auto() # 1: 触碰EMA,收盘守住趋势侧(支撑/压力有效) + WEAK_COUNTER = auto() # 8: 逆势K线完全在EMA逆势侧(弱势,远未到EMA) + APPROACHING = auto() # 9: K线接近EMA但未触碰(即将测试支撑/压力) + NEUTRAL = auto() # 0: 盘整/无法判断 +class Chan_KL_TYPE(Enum): + K_1S = auto() + K_1M = auto() + K_DAY = auto() + K_WEEK = auto() + K_MON = auto() + K_YEAR = auto() + K_5M = auto() + K_15M = auto() + K_30M = auto() + K_60M = auto() + K_1H = auto() + K_2H = auto() + K_4H = auto() + K_6H = auto() + K_8H = auto() + K_12H = auto() + K_1D = auto() + K_3D = auto() + K_3M = auto() + K_QUARTER = auto() + + +class Chan_KLINE_DIR(Enum): + UP = auto() + DOWN = auto() + COMBINE = auto() + INCLUDED = auto() +class Chan_KLU_TYPE(Enum): + BigBull = auto() + MiddleBull = auto() + SmallBull = auto() + BigBear = auto() + MiddleBear = auto() + SmallBear = auto() + Cross = auto() + +class Chan_KLU_PATTERN(Enum): + # 单根K线形态 + HAMMER = auto() # 锤子线 + INVERTED_HAMMER = auto() # 倒锤子线 + SHOOTING_STAR = auto() # 射击之星 + HANGING_MAN = auto() # 上吊线 + DOJI = auto() # 十字星 + LONG_LEGGED_DOJI = auto() # 长腿十字星 + GRAVESTONE_DOJI = auto() # 墓碑十字星 + DRAGONFLY_DOJI = auto() # 蜻蜓十字星 + MARUBOZU = auto() # 光头光脚 + SPINNING_TOP = auto() # 纺锤线 + + # 双根K线形态 + BULLISH_ENGULFING = auto() # 看涨吞没 + BEARISH_ENGULFING = auto() # 看跌吞没 + PIERCING_LINE = auto() # 刺透形态 + DARK_CLOUD_COVER = auto() # 乌云盖顶 + TWEEZER_TOP = auto() # 镊子顶 + TWEEZER_BOTTOM = auto() # 镊子底 + HARAMI = auto() # 孕线 + BULLISH_HARAMI = auto() # 看涨孕线 + BEARISH_HARAMI = auto() # 看跌孕线 + + # 三根K线形态 + MORNING_STAR = auto() # 早晨之星 + EVENING_STAR = auto() # 黄昏之星 + THREE_WHITE_SOLDIERS = auto() # 红三兵 + THREE_BLACK_CROWS = auto() # 三只乌鸦 + THREE_INNER_UP = auto() # 上升三法 + THREE_INNER_DOWN = auto() # 下降三法 + ABANDONED_BABY = auto() # 弃婴形态 + + # 多根K线形态 + DOUBLE_TOP = auto() # 双顶 + DOUBLE_BOTTOM = auto() # 双底 + TRIPLE_TOP = auto() # 三顶 + TRIPLE_BOTTOM = auto() # 三底 + HEAD_AND_SHOULDERS = auto() # 头肩顶 + INVERSE_HEAD_SHOULDERS = auto() # 头肩底 + ROUNDING_BOTTOM = auto() # 圆弧底 + ROUNDING_TOP = auto() # 圆弧顶 + + # 缺口形态 + BREAKAWAY_GAP = auto() # 突破缺口 + RUNAWAY_GAP = auto() # 持续缺口 + EXHAUSTION_GAP = auto() # 衰竭缺口 + + # 特殊形态 + ISLAND_REVERSAL = auto() # 岛形反转 + KEY_REVERSAL = auto() # 关键反转 + INSIDE_BAR = auto() # 内包线 + OUTSIDE_BAR = auto() # 外包线 + + # 趋势形态 + HIGHER_HIGH = auto() # 更高高点 + HIGHER_LOW = auto() # 更高低点 + LOWER_HIGH = auto() # 更低高点 + LOWER_LOW = auto() # 更低低点 + + # 支撑阻力形态 + SUPPORT_BOUNCE = auto() # 支撑反弹 + RESISTANCE_REJECTION = auto() # 阻力拒绝 + BREAKOUT = auto() # 突破 + BREAKDOWN = auto() # 跌破 + + # 成交量相关形态 + VOLUME_SPIKE = auto() # 成交量激增 + VOLUME_DECLINE = auto() # 成交量萎缩 + + # 未知/无形态 + UNKNOWN = auto() # 未知形态 + + +class Chan_FX_TYPE(Enum): + BOTTOM = auto() + TOP = auto() + UNKNOWN = auto() + UP = auto() + DOWN = auto() + TT = auto() + BB = auto() + PTOP = auto() + PBOTTOM = auto() +class Chan_FX(Enum): + CONTINUATION = auto() + REVERSAL = auto() + UNKNOWN = auto() +class Chan_PRICE_TREND(Enum): + UP = auto() + DOWN = auto() + FLAT = auto() + UNKNOWN = auto() +class Chan_KLC_FX(Enum): + TOP0 = auto() + TOP1 = auto() + TOP2 = auto() + TOP3 = auto() + TOP4 = auto() + TOP5 = auto() + TOP6 = auto() + TOP7 = auto() + TOP8 = auto() + BOTTOM0 = auto() + BOTTOM1 = auto() + BOTTOM2 = auto() + BOTTOM3 = auto() + BOTTOM4 = auto() + BOTTOM5 = auto() + BOTTOM6 = auto() + BOTTOM7 = auto() + BOTTOM8 = auto() + UNKNOWN = auto() +# 统一的MACD状态枚举,包含所有可能的状态 +class Chan_MACD_STATE(Enum): + """MACD状态枚举 - 包含所有可能的状态""" + # 穿越状态 + CROSS0_UP = auto() # 穿零轴后快速向上,能量柱呈现一根比一根长的排列方式 + CROSS0_DOWN = auto() # 穿零轴后快速向下,能量柱呈现一根比一根短的排列方式 + + CROSS_OS = auto() # 穿零轴后缠绕/粘合,黄白线沿着能量柱运行,黄白线在运行的过程中没有释放出反向能量柱 + CROSS_REV = auto() # 穿零轴后倒挂,MACD黄白线在穿零轴的时候与零轴的距离比较近,同时黄白线沿着能量柱运行,在运行的过程中,能量柱衰减导致它跟黄白线之间形成夹角空位,同时黄白线产生交叉并释放反向能量柱。 + + # 趋势状态 + NEAR0 = auto() + NEAR0_52 = auto() # 价格在EMA52附近/价格接触EMA52并马上离开,需要观察离开强度 + NEAR0_DIFF = auto() # MACD白线接近零轴,价格未到EMA52 + NEAR0_PERFECT = auto() # MACD白线接近零轴和价格接触或短暂击穿EMA52,而MACD黄线不穿零轴,完美形态 + NEAR0_24 = auto() # MACD黄白线接近零轴和价格在EMA24附近 + # 位置状态 + HIGH = auto() # 高位:MACD黄白线离开能量柱到高点,能量柱最大开始减弱 + HIGH_EMPTY = auto() # 高位空:MACD黄白线处于高位,能量柱衰减,与黄白线形成空间夹角 + RETURN_ZERO = auto() # 归零轴:能量柱呈现一根比一根短的排列方式 + RZ_UP = auto() # 归零轴后的零轴上涨 + RZ_DOWN = auto() # 归零轴后的零轴下跌 + UP = auto() # 穿零轴后向上 + DOWN = auto() # 穿零轴后向下 + PEAK = auto() # 峰值:MACD白线处于高位 + # 基础状态 + UNKNOWN = auto() # 未知 + START = auto() # 开始 +class Chan_MACDSEG_DIR(Enum): + ABOVE = auto() + UNDER = auto() +class Chan_MACDUNITTF_TYPE(Enum): + START = auto() + CROSS0 = auto() + NEAR0 = auto() +class Chan_MACDUNITTF_JUMP(Enum): + CONTUNE = auto() + DISCRETE = auto() +class Chan_MACDUNITTF_DIV(Enum): + CONTUNE = auto() + DISCRETE = auto() + UNDIV = auto() +class Chan_MACDHISTSET_DIR(Enum): + ABOVE = auto() + UNDER = auto() +class Chan_MACDUNITTF_DIR(Enum): + ABOVE = auto() + UNDER = auto() +class Chan_MACDHIST_STATE(Enum): + UP = auto() + DOWN = auto() + PEAK = auto() + UNKNOWN = auto() + +class Chan_BI_DIR(Enum): + UP = auto() + DOWN = auto() + +class Chan_SEG_DIR(Enum): + UP = auto() + DOWN = auto() + +class Chan_BI_TYPE(Enum): + UNKNOWN = auto() + STRICT = auto() + SUB_VALUE = auto() # 次高低点成笔 + TIAOKONG_THRED = auto() + DAHENG = auto() + TUIBI = auto() + UNSTRICT = auto() + TIAOKONG_VALUE = auto() + + +Chan_BSP_MAIN_TYPE = Literal['1', '2', '3'] + +class Chan_BSP_DIR(Enum): + BUY = auto() + SELL = auto() +class Chan_BSP_TYPE(Enum): + B1 = auto() + B2 = auto() + B3 = auto() + S1 = auto() + S2 = auto() + S3 = auto() + NONE = auto() +""" +class Chan_BSP_TYPE(Enum): + T1 = '1' + T1P = '1p' + T2 = '2' + T2S = '2s' + T3A = '3a' # 中枢在1类后面 + T3B = '3b' # 中枢在1类前面 + T3 = '3' + T3E ='3e' # T3退出点 + QJT = 'qjt' # 区间套突破 + QJT1 = 'qjt1' # 区间套一类买点 + QJT2 = 'qjt2' # 区间套一类卖点 + QJT3 = 'qjt3' # 区间套三类买点 + def main_type(self) -> Chan_BSP_MAIN_TYPE: + return self.value[0] # type: ignore + +""" +class Chan_AUTYPE(Enum): + QFQ = auto() + HFQ = auto() + NONE = auto() + + +class Chan_TREND_TYPE(Enum): + MEAN = "mean" + MAX = "max" + MIN = "min" + + +class Chan_TREND_LINE_SIDE(Enum): + INSIDE = auto() + OUTSIDE = auto() + + +class Chan_LEFT_SEG_METHOD(Enum): + ALL = auto() + PEAK = auto() + + +class Chan_FX_CHECK_METHOD(Enum): + STRICT = auto() + LOSS = auto() + HALF = auto() + TOTALLY = auto() + + +class Chan_SEG_TYPE(Enum): + BI = auto() + SEG = auto() + + +class Chan_MACD_ALGO(Enum): + AREA = auto() + PEAK = auto() + FULL_AREA = auto() + DIFF = auto() + SLOPE = auto() + AMP = auto() + VOLUMN = auto() + AMOUNT = auto() + VOLUMN_AVG = auto() + AMOUNT_AVG = auto() + TURNRATE_AVG = auto() + RSI = auto() + + +class Chan_DATA_FIELD: + FIELD_TIME = "time_key" + FIELD_OPEN = "open" + FIELD_HIGH = "high" + FIELD_LOW = "low" + FIELD_CLOSE = "close" + FIELD_VOLUME = "volume" # 成交量 + FIELD_TURNOVER = "turnover" # 成交额 + FIELD_TURNRATE = "turnover_rate" # 换手率 + +class Chan_KLC_STATE: + """笔当下状态(缠论笔定理)。任意时刻必属其一。""" + S10 = "(1, 0)" # 顶分型构造中 (1,0) + S_10 = "(-1, 0)" # 底分型构造中 (-1,0) + S11 = "(1,1)" # 向上笔延续中 + S_11 = "(-1,1)" # 向下笔延续中 + UNKNOWN = "Unknown" # 初始状态 diff --git a/chan/core/ChanKLC.py b/chan/core/ChanKLC.py new file mode 100644 index 0000000..7eac555 --- /dev/null +++ b/chan/core/ChanKLC.py @@ -0,0 +1,620 @@ +import copy +from typing import Dict, Optional + +from .ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_KLC_FX +from .ChanEnum import Chan_K_DIR, Chan_MACD_STATE, Chan_PRICE_TREND, Chan_EMA_POS +from .ChanEnum import Chan_EMA_SEMANTIC, Chan_BSP_TYPE, Chan_KLC_STATE, Chan_FX +from . import ChanKLU +from . import ChanCTime +from . import Chan_FX_Box +# 根据结合律合并K线后的K线 +class ChanKLC(): + def __init__(self, klu: ChanKLU, index, ddir=Chan_KLINE_DIR.UP): + self.start_time = klu.time + self.end_time = None + self.high = klu.high + self.low = klu.low + self.dir = ddir + self.index = index + self.klu_list = [] + self.add_klu(klu) + self.fx = Chan_FX_TYPE.UNKNOWN + self.next = None + self.pre = None + self.start_klu = klu + self.end_klu = None + self.state = "00" + self.klc_state = Chan_KLC_STATE.UNKNOWN + self.open = klu.open + self.close = klu.close + self.volume = klu.volume + self.bi = None + self.distance = 0 + self.klc_fx_type = Chan_KLC_FX.UNKNOWN + self.rsi = klu.rsi + self.volume_ratio = klu.volume_ratio + self.macdhist = klu.macdhist + self.body = klu.body + self.upper_shadow = klu.upper_shadow + self.lower_shadow = klu.lower_shadow + self.body_ratio = klu.body_ratio + self.upper_shadow_ratio = klu.upper_shadow_ratio + self.lower_shadow_ratio = klu.lower_shadow_ratio + self.candle_dir = klu.candle_dir + self.range = klu.range + self.bb_out = True + self.macd = klu.macd + self.signal = klu.signal + self.state = Chan_MACD_STATE.UNKNOWN + self.continue_div = False + self.separate_div = False + self.ema24 = klu.ema24 + self.ema26 = klu.ema26 + self.ema52 = klu.ema52 + self.ema104 = klu.ema104 + self.ema156 = klu.ema156 + self.ema208 = klu.ema208 + self.ema13 = klu.ema13 + self.ema7 = klu.ema7 + self.trend = Chan_PRICE_TREND.UNKNOWN + self.exception = klu.exception + self.klc_dir = Chan_KLINE_DIR.UP if klu.close > klu.open else Chan_KLINE_DIR.DOWN + self.ema_dir = klu.ema_dir + self.bsp = False + self.bsp_type = Chan_BSP_TYPE.NONE + # EMA状态字典:key为EMA名称,value为 {'pos': Chan_EMA_POS, 'semantic': Chan_EMA_SEMANTIC} + self.ema_status = {} + # 向后兼容:保留 ema52_status 和 ema52_pos + self.ema52_status = 0 + self.ema52_pos = Chan_EMA_POS.UNKNOWN + self.bb2633upper = klu.bb2633upper + self.bb2633lower = klu.bb2633lower + self.bb2633middle = klu.bb2633middle + self.ema5 = klu.ema5 + self.ma5 = klu.ma5 + self.fx_box = None + self.in_fx = False + self.fx_confirmed = False + self.ema52_dis = klu.high - klu.ema52 if klu.close > klu.ema52 else klu.ema52 - klu.low + self.ema26_dis = klu.high - klu.ema26 if klu.close > klu.ema26 else klu.ema26 - klu.low + self.macd_signal_dis = abs(klu.macd - klu.signal) + self.ema52_ema26_dis = abs(klu.ema52 - klu.ema26) + self.fx_type = Chan_FX.UNKNOWN + self.bi_zs = None + self.seg_zs = None + self.last_bi_zs = None + # ==================== EMA 通用计算方法 ==================== + + @staticmethod + def cal_ema_pos(high, low, close, ema_value, threshold=0): + """ + 计算K线与任意EMA的客观位置关系(与趋势方向无关,支持threshold容差) + + 参数: + high, low, close: K线的高低收盘价 + ema_value: EMA的值 + threshold: 容差值(绝对值),在此范围内视为"接近/触碰" + 例如 BTC 价格 $100,000 时 threshold=100 表示差100点视为触碰 + 返回: + Chan_EMA_POS 枚举值 + + 判断逻辑(以threshold=100, ema=97000为例): + ema_zone = [96900, 97100] (EMA上下各扩展threshold) + + ABOVE: low > 97100 K线完全在zone上方(远离EMA) + NEAR_ABOVE: 97000 < low <= 97100 K线在上方但下影线进入zone(接近EMA) + CROSS_CLOSE_ABOVE: close > 97000, low <= 97000 K线穿越EMA,收盘在上方 + ON_EMA: abs(close - 97000) <= 100 收盘价在zone内 + CROSS_CLOSE_BELOW: close < 97000, high >= 97000 K线穿越EMA,收盘在下方 + NEAR_BELOW: 96900 <= high < 97000 K线在下方但上影线进入zone(接近EMA) + BELOW: high < 96900 K线完全在zone下方(远离EMA) + """ + if ema_value is None or ema_value == 0: + return Chan_EMA_POS.UNKNOWN + + ema_upper = ema_value + threshold # EMA zone 上界 + ema_lower = ema_value - threshold # EMA zone 下界 + + # 1. 收盘价在EMA附近(zone内) + if threshold > 0 and abs(close - ema_value) <= threshold: + # 收盘价在zone内,但还需要看是否有实际穿越 + if low <= ema_value and close >= ema_value: + return Chan_EMA_POS.CROSS_CLOSE_ABOVE # 实际穿越了精确EMA线 + elif high >= ema_value and close <= ema_value: + return Chan_EMA_POS.CROSS_CLOSE_BELOW + return Chan_EMA_POS.ON_EMA + + # 2. K线实际穿越了精确的EMA线 + if close > ema_value and low <= ema_value: + return Chan_EMA_POS.CROSS_CLOSE_ABOVE + if close < ema_value and high >= ema_value: + return Chan_EMA_POS.CROSS_CLOSE_BELOW + if close == ema_value: + return Chan_EMA_POS.ON_EMA + + # 3. 没有实际穿越,检查是否"接近"(在threshold zone内) + if close > ema_value: + # K线在EMA上方 + if threshold > 0 and low <= ema_upper: + return Chan_EMA_POS.NEAR_ABOVE # 下影线进入zone,接近但未触碰 + return Chan_EMA_POS.ABOVE # 远离EMA + else: + # K线在EMA下方 + if threshold > 0 and high >= ema_lower: + return Chan_EMA_POS.NEAR_BELOW # 上影线进入zone,接近但未触碰 + return Chan_EMA_POS.BELOW # 远离EMA + + @staticmethod + def cal_ema_semantic(ema_pos, kline_dir, ema_dir): + """ + 根据客观位置 + K线方向 + 趋势方向,计算语义状态 + + 参数: + ema_pos: Chan_EMA_POS 客观位置 + kline_dir: Chan_KLINE_DIR K线方向 (UP/DOWN/COMBINE/INCLUDED) + ema_dir: int 趋势方向 (1=多头, -1=空头, 0=盘整) + 返回: + Chan_EMA_SEMANTIC 枚举值 + + 语义含义(以多头为例,空头完全对称): + TOUCH_HOLD: 触碰EMA,收盘守住趋势侧(支撑/压力有效) + BREAK: 穿越EMA,收盘在逆势侧(支撑/压力失败) + DEEP_COUNTER: 完全在EMA逆势侧(深度回调/反抽) + TOUCH_FAIL: 逆势触碰EMA但未穿越(反弹/反抽力度不足) + RECOVER: 逆势后穿越EMA回到趋势侧(收复EMA) + TREND_SIDE: 完全在EMA趋势侧(正常运行) + STRONG_TREND: 顺势K线完全在EMA趋势侧(强势,远未及EMA) + WEAK_COUNTER: 逆势K线完全在EMA逆势侧(弱势,远未到EMA) + """ + if ema_pos == Chan_EMA_POS.UNKNOWN: + return Chan_EMA_SEMANTIC.NEUTRAL + + # 统一处理:将多头/盘整和空头映射到同一套逻辑 + # is_bull=True 时,"趋势侧"=上方,"逆势侧"=下方 + # is_bull=False时,"趋势侧"=下方,"逆势侧"=上方 + is_bull = ema_dir >= 0 # 多头和盘整都按多头逻辑处理 + + # K线是否是顺势方向(多头下UP为顺势,空头下DOWN为顺势) + is_trend_kline = (kline_dir == Chan_KLINE_DIR.UP) if is_bull else (kline_dir == Chan_KLINE_DIR.DOWN) + is_counter_kline = (kline_dir == Chan_KLINE_DIR.DOWN) if is_bull else (kline_dir == Chan_KLINE_DIR.UP) + + # 位置映射:多头下 ABOVE=趋势侧, BELOW=逆势侧; 空头反过来 + trend_side = Chan_EMA_POS.ABOVE if is_bull else Chan_EMA_POS.BELOW + counter_side = Chan_EMA_POS.BELOW if is_bull else Chan_EMA_POS.ABOVE + near_trend = Chan_EMA_POS.NEAR_ABOVE if is_bull else Chan_EMA_POS.NEAR_BELOW + near_counter = Chan_EMA_POS.NEAR_BELOW if is_bull else Chan_EMA_POS.NEAR_ABOVE + cross_to_trend = Chan_EMA_POS.CROSS_CLOSE_ABOVE if is_bull else Chan_EMA_POS.CROSS_CLOSE_BELOW + cross_to_counter = Chan_EMA_POS.CROSS_CLOSE_BELOW if is_bull else Chan_EMA_POS.CROSS_CLOSE_ABOVE + + # COMBINE / INCLUDED 方向:只看位置,不区分强弱 + if not is_trend_kline and not is_counter_kline: + if ema_pos == trend_side: + return Chan_EMA_SEMANTIC.TREND_SIDE + elif ema_pos in (near_trend, cross_to_trend, Chan_EMA_POS.ON_EMA): + return Chan_EMA_SEMANTIC.APPROACHING + elif ema_pos in (near_counter, cross_to_counter): + return Chan_EMA_SEMANTIC.APPROACHING + elif ema_pos == counter_side: + return Chan_EMA_SEMANTIC.DEEP_COUNTER + return Chan_EMA_SEMANTIC.NEUTRAL + + # 逆势K线(多头下的下跌K线 / 空头下的上涨K线) + if is_counter_kline: + if ema_pos == trend_side: + return Chan_EMA_SEMANTIC.STRONG_TREND # 逆势K线仍在趋势侧(回调很浅) + elif ema_pos == near_trend: + return Chan_EMA_SEMANTIC.APPROACHING # 接近EMA,即将测试支撑/压力 + elif ema_pos == cross_to_trend: + return Chan_EMA_SEMANTIC.TOUCH_HOLD # 触碰EMA后守住趋势侧 + elif ema_pos == Chan_EMA_POS.ON_EMA: + return Chan_EMA_SEMANTIC.TOUCH_HOLD # 收盘在EMA附近,视为守住 + elif ema_pos == cross_to_counter: + return Chan_EMA_SEMANTIC.BREAK # 穿越EMA到逆势侧 + elif ema_pos == near_counter: + return Chan_EMA_SEMANTIC.BREAK # 接近EMA但收盘在逆势侧,也视为击穿 + elif ema_pos == counter_side: + return Chan_EMA_SEMANTIC.DEEP_COUNTER # 完全在逆势侧 + + # 顺势K线(多头下的上涨K线 / 空头下的下跌K线) + if is_trend_kline: + if ema_pos == counter_side: + return Chan_EMA_SEMANTIC.WEAK_COUNTER # 顺势K线却在逆势侧(弱势) + elif ema_pos == near_counter: + return Chan_EMA_SEMANTIC.APPROACHING # 从逆势侧接近EMA + elif ema_pos == cross_to_counter: + return Chan_EMA_SEMANTIC.TOUCH_FAIL # 触碰EMA但未穿越回趋势侧 + elif ema_pos == Chan_EMA_POS.ON_EMA: + return Chan_EMA_SEMANTIC.TOUCH_FAIL # 收盘在EMA附近,未确认突破 + elif ema_pos == cross_to_trend: + return Chan_EMA_SEMANTIC.RECOVER # 从逆势侧穿越回趋势侧 + elif ema_pos == near_trend: + return Chan_EMA_SEMANTIC.RECOVER # 接近趋势侧(刚收复EMA附近) + elif ema_pos == trend_side: + return Chan_EMA_SEMANTIC.TREND_SIDE # 完全在趋势侧(正常) + + return Chan_EMA_SEMANTIC.NEUTRAL + + @staticmethod + def semantic_to_int(semantic): + """将 Chan_EMA_SEMANTIC 枚举转换为整数,兼容旧的 ema52_status 数值""" + mapping = { + Chan_EMA_SEMANTIC.TOUCH_HOLD: 1, + Chan_EMA_SEMANTIC.BREAK: 2, + Chan_EMA_SEMANTIC.DEEP_COUNTER: 3, + Chan_EMA_SEMANTIC.TOUCH_FAIL: 4, + Chan_EMA_SEMANTIC.RECOVER: 5, + Chan_EMA_SEMANTIC.TREND_SIDE: 6, + Chan_EMA_SEMANTIC.STRONG_TREND: 7, + Chan_EMA_SEMANTIC.WEAK_COUNTER: 8, + Chan_EMA_SEMANTIC.APPROACHING: 9, + Chan_EMA_SEMANTIC.NEUTRAL: 0, + } + return mapping.get(semantic, 0) + + # threshold_pct: 阈值百分比,用于自动计算绝对阈值 + # 例如 0.001 表示 EMA 值的 0.1%,BTC $100,000 时 threshold = $100 + threshold_pct = 0.001 + def set_bsp_type(self, bsp_type): + if bsp_type and bsp_type != Chan_BSP_TYPE.NONE: + self.bsp_type = bsp_type + self.bsp = True + def cal_all_ema_status(self): + """ + 统一计算所有EMA与K线的位置关系和语义状态 + + threshold 自动按 EMA 值的百分比计算(cls.threshold_pct,默认0.1%) + - BTC $100,000 时:threshold ≈ $100 + - ETH $3,000 时:threshold ≈ $3 + - SOL $200 时:threshold ≈ $0.2 + + 结果存储在 self.ema_status 字典中,格式: + { + 'ema24': {'pos': Chan_EMA_POS, 'semantic': Chan_EMA_SEMANTIC, 'value': float, 'threshold': float}, + 'ema52': {...}, + ... + } + + 同时保持向后兼容:self.ema52_pos 和 self.ema52_status + """ + ema_configs = { + 'ema24': self.ema24, + 'ema52': self.ema52, + 'ema104': self.ema104, + 'ema156': self.ema156, + 'ema208': self.ema208, + } + self.ema_status = {} + for name, value in ema_configs.items(): + # 按 EMA 值的百分比自动计算阈值 + threshold = abs(value) * self.threshold_pct if value and self.threshold_pct > 0 else 0 + pos = ChanKLC.cal_ema_pos(self.high, self.low, self.close, value, threshold) + semantic = ChanKLC.cal_ema_semantic(pos, self.dir, self.ema_dir) + self.ema_status[name] = { + 'pos': pos, + 'semantic': semantic, + 'value': value, + 'threshold': threshold, + } + # 向后兼容 + self.ema52_pos = self.ema_status['ema52']['pos'] + self.ema52_status = ChanKLC.semantic_to_int(self.ema_status['ema52']['semantic']) + def get_ema_pos(self, ema_name): + """获取指定EMA的客观位置,如 klc.get_ema_pos('ema24')""" + if ema_name in self.ema_status: + return self.ema_status[ema_name]['pos'] + return Chan_EMA_POS.UNKNOWN + def check_ema_pos(self): + if len(self.ema_status) > 0: + for ema_name, pos in self.ema_status.items(): + #print(self.end_time, ema_name, pos['pos']) + if ((self.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc_fx_type == Chan_KLC_FX.TOP2) and pos['pos'] == Chan_EMA_POS.CROSS_CLOSE_BELOW) or ((self.klc_fx_type == Chan_KLC_FX.BOTTOM1 or self.klc_fx_type == Chan_KLC_FX.BOTTOM2) and pos['pos'] == Chan_EMA_POS.CROSS_CLOSE_ABOVE): + #print("---------------------") + return ema_name + return None + def get_ema_semantic(self, ema_name): + """获取指定EMA的语义状态,如 klc.get_ema_semantic('ema52')""" + if ema_name in self.ema_status: + return self.ema_status[ema_name]['semantic'] + return Chan_EMA_SEMANTIC.NEUTRAL + def set_trend(self, trend): + self.trend = trend + def to_string(self): + out = "" + start = self.start_time if self.start_time is not None else "" + end = self.end_time if self.end_time is not None else "" + price_diff = getattr(self, 'price_diff', None) + out += str(start) + " " + str(end) + " " + str(self.close) + " " + str(self.ema24) + " " + str(self.ema52) + " " + str(self.trend) + " " + str(self.close - self.ema52) + return out + def set_bi_zs(self, bi_zs): + if bi_zs: + self.bi_zs = bi_zs + def set_klc_fx_type(self, klc_fx_type): + #print(self.start_time, klc_fx_type, self.get_feature_data()['klu_macd'], self.get_feature_data()['klu_macdhist'], self.get_feature_data()['klu_rsi']) + self.klc_fx_type = klc_fx_type + #self.cal_fx() + ema_name = self.check_ema_pos() + hist_div = abs(self.macdhist - self.next.macdhist) + #print(self.end_time, self.dir, abs(self.macdhist), hist_div) + #if ema_name: + #print(self.end_time, ema_name, self.ema_status[ema_name]['semantic'], hist_div) + #self.cal_bb_out() + #print(self.pre.start_time, self.next.end_time, self.klc_fx_type) + if klc_fx_type == Chan_KLC_FX.TOP1 or klc_fx_type == Chan_KLC_FX.TOP2 or klc_fx_type == Chan_KLC_FX.BOTTOM1 or klc_fx_type == Chan_KLC_FX.BOTTOM2: + self.cal_fx_box() + self.cal_fx_type() + def cal_fx_type(self): + if self.fx == Chan_FX_TYPE.TOP and self.next: + if self.ema52_dis > self.ema26_dis: + if self.pre.macd < self.macd and self.macd < self.next.macd: + self.fx_type = Chan_FX.CONTINUATION + else: + self.fx_type = Chan_FX.REVERSAL + elif self.fx == Chan_FX_TYPE.BOTTOM and self.next: + if self.ema52_dis < self.ema26_dis: + if self.pre.macd > self.macd and self.macd > self.next.macd: + self.fx_type = Chan_FX.CONTINUATION + else: + self.fx_type = Chan_FX.REVERSAL + #if self.fx_type != Chan_FX.UNKNOWN and self.fx_type != Chan_FX.CONTINUATION: + #print(self.end_time, self.fx_type) + def cal_fx_box(self): + # 每次重算前先清空,避免旧box残留 + self.fx_box = None + start_time = None + end_time = None + high = 0 + low = 0 + display = False + if self.pre and self.next and self.next.end_time: + self.next.in_fx = True + if self.fx == Chan_FX_TYPE.TOP: + start_time = self.pre.end_time + end_time = self.next.end_time + high = self.high + low = self.pre.low if self.pre.low < self.next.low else self.next.low + if self.next.close < self.pre.low or True: + display = True + elif self.fx == Chan_FX_TYPE.BOTTOM: + start_time = self.pre.end_time + end_time = self.next.end_time + high = self.pre.high if self.pre.high > self.next.high else self.next.high + low = self.low + if self.next.close > self.pre.high or True: + display = True + if high > 0 and self.next.end_time and display: + #print(start_time, end_time, high, low) + # Chan_FX_BOX 这里导入的是模块,类名在模块内部为 Chan_FX_Box + self.fx_confirmed = True + self.fx_box = Chan_FX_Box.Chan_FX_Box(start_time, end_time, high, low) + def check_fx_confirmed(self, last_top, last_bottom): + if last_top and last_bottom and False: + if last_top.index > last_bottom.index: + if self.in_fx == False and last_top.fx_confirmed == False: + pre = last_top.pre + if pre.low > self.close: + last_top.fx_confirmed = True + if last_top.fx_box: + last_top.fx_box.end_time = self.end_time + #print(self.end_time, "fx_confirmed top") + else: + high = last_top.high + low = self.low + last_top.fx_box = Chan_FX_Box.Chan_FX_Box(last_top.pre.start_time, self.end_time, high, low) + #print(self.end_time, "fx_confirmed new box top") + elif self.in_fx == False and last_bottom.fx_confirmed == False: + pre = last_bottom.pre + if pre.high < self.close: + last_bottom.fx_confirmed = True + if last_bottom.fx_box: + last_bottom.fx_box.end_time = self.end_time + #print(self.end_time, "fx_confirmed bottom") + else: + high = self.high + low = last_bottom.low + last_bottom.fx_box = Chan_FX_Box.Chan_FX_Box(last_bottom.pre.start_time, self.end_time, high, low) + #print(self.end_time, "fx_confirmed new box bottom") + def add_klu(self, klu): + self.klu_list.append(klu) + def check_klc_state(self, last_fx_klc): + if last_fx_klc and last_fx_klc.fx == Chan_FX_TYPE.TOP: + if self.high > last_fx_klc.high: + self.klc_state = Chan_KLC_STATE.S11 + else: + self.klc_state = Chan_KLC_STATE.S_11 + elif last_fx_klc and last_fx_klc.fx == Chan_FX_TYPE.BOTTOM: + if self.low < last_fx_klc.low: + self.klc_state = Chan_KLC_STATE.S_11 + else: + self.klc_state = Chan_KLC_STATE.S11 + if self.pre and self.pre.fx == Chan_FX_TYPE.TOP: + self.klc_state = Chan_KLC_STATE.S10 + elif self.pre and self.pre.fx == Chan_FX_TYPE.BOTTOM: + self.klc_state = Chan_KLC_STATE.S_10 + #print(self.end_time, self.klc_state) + def set_end_klu(self, klu): + self.end_klu = klu + self.end_time = klu.time + self.close = klu.close + for klu in self.klu_list: + if klu.exception: + self.exception = True + print(klu.time, "exception") + if klu.separate_div > 0: + self.separate_div = True + if klu.continue_div: + self.continue_div = klu.continue_div + if klu.macd_state != Chan_MACD_STATE.UNKNOWN: + self.state = klu.macd_state + klu.set_klc(self) + self.klc_dir = Chan_KLINE_DIR.UP if self.close > self.open else Chan_KLINE_DIR.DOWN + self.cal_indicators() + self.cal_all_ema_status() + if self.open > self.high: + self.open = self.high + if self.close > self.high: + self.close = self.high + if self.close < self.low: + self.close = self.low + if self.open < self.low: + self.open = self.low + #print(self.end_time, self.open, self.close, self.high, self.low) + #print(klu.time, klu.open, klu.close, klu.high, klu.low) + def cal_fx(self): + if self.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc_fx_type == Chan_KLC_FX.TOP2: + #print(self.end_time, self.fx, self.macd, self.macdhist, len(self.klu_list)) + if self.state == Chan_MACD_STATE.HIGH_EMPTY and self.macd > 0: + #print(self.end_time, self.state, self.macd, self.klc_fx_type) + self.klc_fx_type = Chan_KLC_FX.TOP6 + if self.separate_div or self.continue_div: + self.klc_fx_type = Chan_KLC_FX.TOP7 + if self.signal > 0 and self.macd > self.signal: + self.klc_fx_type = Chan_KLC_FX.TOP8 + else: + if self.klc_fx_type == Chan_KLC_FX.BOTTOM1 or self.klc_fx_type == Chan_KLC_FX.BOTTOM2: + if self.macdhist > 0 and self.macd < 0: + self.klc_fx_type = Chan_KLC_FX.BOTTOM5 + return + if self.state == Chan_MACD_STATE.HIGH_EMPTY and self.macd < 0: + self.klc_fx_type = Chan_KLC_FX.BOTTOM6 + #print(self.end_time, self.state, self.macd, self.klc_fx_type) + if self.separate_div or self.continue_div: + self.klc_fx_type = Chan_KLC_FX.BOTTOM7 + if self.signal < 0 and self.macd < self.signal: + self.klc_fx_type = Chan_KLC_FX.BOTTOM8 + def cal_bb_out(self): + for klu in self.klu_list: + if self.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc_fx_type == Chan_KLC_FX.TOP2: + #print(self.start_time, self.klc_fx_type, klu.high, klu.bb52upper, self.macd, self.next.macd, klu.time) + if self.high >= klu.bb52upper and klu.bb52upper > 0 and self.next and self.high > self.next.high: + self.klc_fx_type = Chan_KLC_FX.TOP4 + print(self.end_time, self.klc_fx_type) + if self.klc_fx_type == Chan_KLC_FX.BOTTOM1 or self.klc_fx_type == Chan_KLC_FX.BOTTOM2: + #print(self.start_time, self.klc_fx_type, klu.low, klu.bb52lower, self.macd, self.next.macd, klu.time) + if self.low <= klu.bb52lower and klu.bb52lower > 0 and self.next and self.low < self.next.low: + self.klc_fx_type = Chan_KLC_FX.BOTTOM4 + print(self.end_time, self.klc_fx_type) + def cal_indicators(self): + for index in range(1, len(self.klu_list)): + self.volume += self.klu_list[index].volume + self.rsi += self.klu_list[index].rsi + self.volume_ratio += self.klu_list[index].volume_ratio + self.macdhist += self.klu_list[index].macdhist + self.ema26 += self.klu_list[index].ema26 + self.ema24 += self.klu_list[index].ema24 + self.ema52 += self.klu_list[index].ema52 + self.ema104 += self.klu_list[index].ema104 + self.ema156 += self.klu_list[index].ema156 + self.ema208 += self.klu_list[index].ema208 + self.ema13 += self.klu_list[index].ema13 + self.ema7 += self.klu_list[index].ema7 + self.bb2633upper += self.klu_list[index].bb2633upper + self.bb2633lower += self.klu_list[index].bb2633lower + self.bb2633middle += self.klu_list[index].bb2633middle + self.ma5 += self.klu_list[index].ma5 + self.ema5 += self.klu_list[index].ema5 + if self.ema_dir != self.klu_list[index].ema_dir: + self.ema_dir = 0 + n = len(self.klu_list) + self.rsi = self.rsi / n + self.volume_ratio = self.volume_ratio / n + self.volume = self.volume / n + self.macdhist = self.macdhist / n + self.ema26 = self.ema26 / n + self.ema24 = self.ema24 / n + self.ema52 = self.ema52 / n + self.ema104 = self.ema104 / n + self.ema156 = self.ema156 / n + self.ema208 = self.ema208 / n + self.ema13 = self.ema13 / n + self.ema7 = self.ema7 / n + self.ma5 = self.ma5 / n + self.ema5 = self.ema5 / n + self.bb2633upper = self.bb2633upper / n + self.bb2633lower = self.bb2633lower / n + self.bb2633middle = self.bb2633middle / n + if len(self.klu_list) > 0: + self.macd = self.klu_list[-1].macd + self.signal = self.klu_list[-1].signal + self.body = abs(self.close - self.open) + self.upper_shadow = self.high - max(self.close, self.open) + self.lower_shadow = min(self.close, self.open) - self.low + self.body_ratio = self.body / self.open + self.upper_shadow_ratio = self.upper_shadow / self.open + self.lower_shadow_ratio = self.lower_shadow / self.open + self.candle_dir = Chan_K_DIR.CROSS if self.close == self.open else Chan_K_DIR.BULL if self.close > self.open else Chan_K_DIR.BEAR + self.range = self.high - self.low + def set_next(self, klc): + self.next = klc + def set_pre(self, klc): + self.pre = klc + def set_state(self, state): + self.state = state + def check_klu_included(self, klu): + if self.high >= klu.high: + # high大于,low小于,左包含 + if self.low <= klu.low: + self.add_klu(klu=klu) + # gn>gn-1 + if self.dir == Chan_KLINE_DIR.UP: + # UP -> max(dn) + self.low = klu.low + else: + # DOWN -> min(gn) + self.high = klu.high + #self.print(klu, "Z") + return True + # high大于,low大于,不包含 + else: + # if self.low > klu.low + # high相等,右包含 + if self.high == klu.high: + self.add_klu(klu=klu) + # UP -> max(gn) + if self.dir == Chan_KLINE_DIR.UP: + self.high = klu.high + else: + # DOWN -> min(dn) + self.low = klu.low + return True + else: + return False + else: + # high小于,low大于,右包含 + if self.low >= klu.low: + self.add_klu(klu=klu) + # gn>gn-1 + if self.dir == Chan_KLINE_DIR.UP: + # UP -> max(gn) + self.high = klu.high + else: + # DOWN -> min(dn) + self.low = klu.low + #self.print(klu, "Y") + return True + else: + # high小于,low小于,不包含 + return False + def set_fx(self, fx: Chan_FX_TYPE): + self.fx = fx + def cal_invisible(self): + if self.fx == Chan_FX_TYPE.TOP: + if self.macdhist < 0 and self.macd > 0: + self.klc_fx_type = Chan_KLC_FX.TOP5 + else: + if self.fx == Chan_FX_TYPE.BOTTOM: + if self.macdhist > 0 and self.macd < 0: + self.klc_fx_type = Chan_KLC_FX.BOTTOM5 + def set_pre_fx(self): + if self.pre and self.pre.pre: + self.pre.fx = self.check_fx(self.pre.pre, self.pre) + def check_fx(self, k1, k2): + if k2.high > k1.high and k2.high > self.high: + return Chan_FX_TYPE.TOP + elif k2.low < k1.low and k2.low < self.low: + return Chan_FX_TYPE.BOTTOM + else: + return Chan_FX_TYPE.UNKNOWN + def set_bi(self, bi): + self.bi = bi + self.distance = self.index - bi.start_klc.index + #print(self.start_time, self.distance, bi.index, bi.dir) \ No newline at end of file diff --git a/chan/core/ChanKLU.py b/chan/core/ChanKLU.py new file mode 100644 index 0000000..6ed6676 --- /dev/null +++ b/chan/core/ChanKLU.py @@ -0,0 +1,390 @@ +from .ChanEnum import Chan_FX_TYPE, Chan_KLU_TYPE, Chan_K_DIR, Chan_MACD_STATE, Chan_MACDHIST_STATE, Chan_PRICE_TREND, Chan_KLU_PATTERN, Chan_KLC_FX +class ChanKLU: + def __init__(self, time, open, high, low, close, volume): + # _time, _close, _open, _high, _low, _extra_info={} + self.kl_type = None + self.time = time + self.close = close + self.open = open + self.high = high + self.low = low + self.volume = volume + self.idx = 0 + self.index = 0 +# 指标相关字段:非结构固有,由 indicators.attach / set_indicators 填充(兼容旧访问) + self.macd = 0 + self.signal = 0 + self.macdhist = 0 + self.klc = None + self.rsi = 0 + self.volume_ratio = 0 + self.bb52upper = 0 + self.bb52lower = 0 + # === 新增:K线类型 === + self.kline_type = None # K线类型:大阳线、大阴线、小阳线、小阴线 + self.pattern = Chan_KLU_PATTERN.UNKNOWN + + # === 新增:实时分型相关属性 === + self.pre = None # 前一根K线 + self.next = None # 后一根K线 + self.fx_type = Chan_FX_TYPE.UNKNOWN # 分型类型:0=无分型,1=顶分型,-1=底分型 + self.fx_strength = 0 # 分型强度:0-100 + self.fx_confirmed = False # 分型是否确认 + self.klu_type = None + self.range = self.high - self.low + self.body = abs(self.close - self.open) + self.upper_shadow = self.high - max(self.close, self.open) + self.lower_shadow = min(self.close, self.open) - self.low + self.body_ratio = self.body / self.range if self.range != 0 else 0 + self.upper_shadow_ratio = self.upper_shadow / self.body if self.body != 0 else float('inf') + self.lower_shadow_ratio = self.lower_shadow / self.body if self.body != 0 else float('inf') + self.exception = False + #self.cal_exception() + self.candle_dir = Chan_K_DIR.CROSS if self.close == self.open else Chan_K_DIR.BULL if self.close > self.open else Chan_K_DIR.BEAR + + self.continue_div = 0 + self.separate_div = 0 + self.near0_return = 0 + # 以下 EMA/MACD 状态字段同为兼容挂载,默认 0;正式来源为 IndicatorStore + self.ema52 = 0 + self.ema24 = 0 + self.ema26 = 0 + self.ema104 = 0 + self.ema156 = 0 + self.ema208 = 0 + self.macd_slop = 0 + self.signal_slop = 0 + self.hist_slop = 0 + self.hist_state = Chan_MACDHIST_STATE.UNKNOWN + self.macd_state = Chan_MACD_STATE.UNKNOWN + self.macd_hist_gap = 0 + self.trend = Chan_PRICE_TREND.UNKNOWN + self.seg_histset_index = 0 + # === 归零轴细化与模式/背离 === + self.zero_axis = False # 是否归零轴(穿越或接近) + self.zero_axis_state = "none" # {none,crossing,near} + self.zero_axis_side = 0 # 1:above, -1:under, 0:none + self.zero_axis_score = 0 # 0-100 综合评分 + self.mode1_touch_ema52 = False # 单边后触碰EMA52 + self.mode2_fast_to_zero = False # 快线向零收敛 + self.mode3_double_tf = False # 双周期归零(近似占位,由上层填充高周期确认) + self.mode3_dir = "none" # {long_strong_rebound, short_strong_rebound, none} + self.mode4_touch52_no_zero = False # 先触碰EMA52但黄白线未归零 + self.div_type = "none" # {bearish, bullish, hidden_bearish, hidden_bullish, none} + self.div_score = 0.0 # 背离强度(0-100) + self.ema_dir = 0 + self.get_ema_dir() + self.bb2633upper = 0 + self.bb2633lower = 0 + self.bb2633middle = 0 + self.ma5 = 0 + self.ema5 = 0 + #print(self.open, self.close, self.high, self.low, self.candle_dir, self.strength) + def set_macd_state(self, state): + self.macd_state = state + def set_pattern(self, pattern): + self.pattern = pattern + def set_seg_histset_index(self, seg_histset_index): + self.seg_histset_index = seg_histset_index + #print(self.time, self.seg_histset_index) + def to_string(self): + return f"{self.time} {self.candle_dir} {self.pattern}" + def cal_exception(self): + if self.upper_shadow_ratio > 5 or self.lower_shadow_ratio > 5: + self.exception = True + #print(self.time, self.upper_shadow_ratio, self.lower_shadow_ratio, self.body, self.lower_shadow, self.upper_shadow, self.high, self.low, self.close, self.open) + #self.exception = False + def set_trend(self, trend): + self.trend = trend + def set_separate_div(self, separate_div): + self.separate_div = separate_div + if self.klc and self.klc.pre and self.klc.next: + fx = self.check_fx_dir(self.klc.pre, self.klc.next) + if fx == Chan_FX_TYPE.TOP: + if self.macdhist > 0: + self.separate_div = separate_div + else: + self.separate_div = 0 + elif fx == Chan_FX_TYPE.BOTTOM: + if self.macdhist < 0: + self.separate_div = separate_div + else: + self.separate_div = 0 + def check_fx_dir(self, pre, next): + fx = Chan_FX_TYPE.UNKNOWN + if pre.klc_fx_type == Chan_KLC_FX.TOP1 or pre.klc_fx_type == Chan_KLC_FX.TOP2 or next.klc_fx_type == Chan_KLC_FX.TOP1 or next.klc_fx_type == Chan_KLC_FX.TOP2 or self.klc.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc.klc_fx_type == Chan_KLC_FX.TOP2: + fx = Chan_FX_TYPE.TOP + elif pre.klc_fx_type == Chan_KLC_FX.BOTTOM1 or pre.klc_fx_type == Chan_KLC_FX.BOTTOM2 or next.klc_fx_type == Chan_KLC_FX.BOTTOM1 or next.klc_fx_type == Chan_KLC_FX.BOTTOM2 or self.klc.klc_fx_type == Chan_KLC_FX.BOTTOM1 or self.klc.klc_fx_type == Chan_KLC_FX.BOTTOM2: + fx = Chan_FX_TYPE.BOTTOM + return fx + def set_next(self, next): + self.next = next + #if self.fx_type != Chan_FX_TYPE.UNKNOWN and self.fx_strength > 1: + #print(self.index, self.time, self.fx_type, self.fx_confirmed, self.fx_strength) + def set_pre(self, pre): + self.pre = pre + def set_klc(self, klc): + self.klc = klc + def set_histset(self, histset): + """设置HistSet关联""" + self.histset = histset + + def set_seg(self, seg): + """设置Seg关联""" + self.seg = seg + + def set_unittf(self, unittf): + """设置UnitTF关联""" + self.unittf = unittf + def set_idx(self, idx): + self.idx = idx + self.index = idx + def check_price_ema156(self): + if self.check_indicators(): + if self.close > self.ema156: + return 1 + elif self.close < self.ema156: + return -1 + else: + return 0 + else: + return 0 + def get_ema_dir(self): + if self.check_indicators(): + if self.ema24 > self.ema52 and self.ema52 > self.ema104 and self.ema104 > self.ema156: + self.ema_dir = 1 + elif self.ema24 < self.ema52 and self.ema52 < self.ema104 and self.ema104 < self.ema156: + self.ema_dir = -1 + else: + self.ema_dir = 0 + def check_indicators(self): + if self.ema156 == 0: + return False + else: + return True + def set_indicators(self, item): + self.macd = float(item['macd']) if 'macd' in item and item['macd'] else 0 + self.signal = float(item['macdsignal']) if 'macdsignal' in item and item['macdsignal'] else 0 + self.macdhist = float(item['macdhist']) if 'macdhist' in item and item['macdhist'] else 0 + self.ema26 = float(item['ema26']) if 'ema26' in item and item['ema26'] else 0 + self.ema52 = float(item['ema52']) if 'ema52' in item and item['ema52'] else 0 + self.ema24 = float(item['ema24']) if 'ema24' in item and item['ema24'] else 0 + self.ema104 = float(item['ema104']) if 'ema104' in item and item['ema104'] else 0 + self.ema156 = float(item['ema156']) if 'ema156' in item and item['ema156'] else 0 + self.ema208 = float(item['ema208']) if 'ema208' in item and item['ema208'] else 0 + self.ema13 = float(item['ema13']) if 'ema13' in item and item['ema13'] else 0 + self.ema7 = float(item['ema7']) if 'ema7' in item and item['ema7'] else 0 + self.rsi = float(item['rsi']) if 'rsi' in item and item['rsi'] else 0 + self.volume_ratio = float(item['volume_ratio']) if 'volume_ratio' in item and item['volume_ratio'] else 0 + self.bb52upper = float(item['bb52upper']) if 'bb52upper' in item and item['bb52upper'] else 0 + self.bb52lower = float(item['bb52lower']) if 'bb52lower' in item and item['bb52lower'] else 0 + self.bb2633upper = float(item['bb2633upper']) if 'bb2633upper' in item and item['bb2633upper'] else 0 + self.bb2633lower = float(item['bb2633lower']) if 'bb2633lower' in item and item['bb2633lower'] else 0 + self.bb2633middle = float(item['bb2633middle']) if 'bb2633middle' in item and item['bb2633middle'] else 0 + self.ma5 = float(item['ma5']) if 'ma5' in item and item['ma5'] else 0 + self.ema5 = float(item['ema5']) if 'ema5' in item and item['ema5'] else 0 + def cal_macd_state(self): + # 按定义精简实现:优先级 CROSS0 > 位置(HIGH/HE/RETURN_ZERO) > NEAR0 > UNKNOWN + # 首条或缺前一根 + if not hasattr(self, 'pre') or self.pre is None: + self.macd_state = Chan_MACD_STATE.START + return self.macd_state + + # 基本校验 + if (self.macd == 0 and self.signal == 0 and self.macdhist == 0) or self.ema52 == 0: + self.macd_state = Chan_MACD_STATE.UNKNOWN + return self.macd_state + # 归零轴判断 + if self.signal > 0: + if self.macd < self.signal: + if 0 < self.low - self.ema52 < 100: + self.near0_return = 0 + elif self.close > self.ema52 and self.low < self.ema52 and self.open > self.ema52: + self.near0_return = 0 + elif self.close < self.ema52 and self.open > self.ema52 and self.high > self.ema52 and self.low < self.ema52: + self.near0_return = 0 + elif self.close < self.ema52 and self.open < self.ema52 and self.high > self.ema52 and self.low < self.ema52: + self.near0_return = 0 + elif self.close > self.ema52 and self.open > self.ema52 and self.high > self.ema52 and self.low < self.ema52: + self.near0_return = 0 + elif self.close > self.ema52 and self.high > self.ema52 and self.low < self.ema52: + self.near0_return = 0 + else: + if self.macd > self.signal: + if 0 < self.ema52 - self.high < 100: + self.near0_return = 0 + elif self.close < self.ema52 and self.high > self.ema52 and self.open < self.ema52: + self.near0_return = 0 + elif self.close < self.ema52 and self.open > self.ema52 and self.high > self.ema52 and self.low < self.ema52: + self.near0_return = 0 + elif self.close > self.ema52 and self.open < self.ema52 and self.high > self.ema52 and self.low < self.ema52: + self.near0_return = 0 + elif self.close > self.ema52 and self.high > self.ema52 and self.open >= self.ema52 and self.low < self.ema52: + self.near0_return = 0 + elif self.close > self.ema52 and self.high > self.ema52 and self.low < self.ema52: + self.near0_return = 0 + # 向上穿越EMA52 7 + if self.close > self.ema52 and self.open < self.ema52: + self.near0_return = 0 + # 向下穿越EMA52 8 + elif self.close < self.ema52 and self.open > self.ema52: + self.near0_return = 0 + if self.pre.near0_return == 7: + # 向上穿越后的一根价格再EMA52上方 9 + if self.low > self.ema52 and self.close > self.open: + self.near0_return = 9 + if self.pre.near0_return == 8: + # 向下穿越后的一根价格再EMA52下方 10 + if self.high < self.ema52 and self.close < self.open: + self.near0_return = 10 + # CROSS0 仅以 Signal 穿越零轴判定 + if self.pre.signal >= 0 and self.signal < 0: + self.macd_state = Chan_MACD_STATE.CROSS0_DOWN + return self.macd_state + if self.pre.signal <= 0 and self.signal > 0: + self.macd_state = Chan_MACD_STATE.CROSS0_UP + return self.macd_state + # 穿零轴后的形态:缠绕/倒挂(基于前一状态为CROSS0_*) + if self.pre.macd_state == Chan_MACD_STATE.CROSS0_UP or self.pre.macd_state == Chan_MACD_STATE.CROSS0_DOWN: + direction = 1 if self.pre.macd_state == Chan_MACD_STATE.CROSS0_UP else -1 + hist_same_dir = (self.macdhist * direction) > 0 + hist_decreasing = abs(self.macdhist) < abs(self.pre.macdhist) + lines_tight = abs(self.macd - self.signal) <= 12 + # 倒挂:能量柱衰减且黄白线相对方向不利/出现反向能量释放 + if hist_decreasing and (((self.macd - self.signal) * direction) < 0 or not hist_same_dir): + self.macd_state = Chan_MACD_STATE.CROSS_REV + return self.macd_state + # 缠绕/粘合:紧贴能量柱运行,无反向能量释放 + if lines_tight and hist_same_dir: + self.macd_state = Chan_MACD_STATE.CROSS_OS + return self.macd_state + # 趋近零轴:细化 NEAR0_* 判定 + NEAR0_EPS = 15 + lines_near_zero = abs(self.macd) <= NEAR0_EPS or abs(self.signal) <= NEAR0_EPS + touch_52 = (self.ema52 != 0) and ((abs(self.close - self.ema52) <= NEAR0_EPS) or (self.low <= self.ema52 <= self.high)) + touch_24 = (self.ema24 != 0) and ((abs(self.close - self.ema24) <= NEAR0_EPS) or (self.low <= self.ema24 <= self.high)) + # 完美形态:白线接近零轴 + 价格触碰/轻破EMA52 + 黄线不穿零轴 + if abs(self.macd) <= NEAR0_EPS and touch_52 and (not (self.pre.signal >= 0 and self.signal < 0)) and (not (self.pre.signal <= 0 and self.signal > 0)): + self.macd_state = Chan_MACD_STATE.NEAR0_PERFECT + #self.near0_return = 1 + return self.macd_state + # EMA24 附近 + if lines_near_zero and touch_24: + self.macd_state = Chan_MACD_STATE.NEAR0_24 + #self.near0_return = 2 + return self.macd_state + # EMA52 附近 + if lines_near_zero and touch_52: + self.macd_state = Chan_MACD_STATE.NEAR0_52 + #self.near0_return = 3 + return self.macd_state + # 白线接近零轴但价格未至EMA52 + if abs(self.macd) <= NEAR0_EPS and not touch_52: + self.macd_state = Chan_MACD_STATE.NEAR0_DIFF + #self.near0_return = 4 + return self.macd_state + # 一般近零轴 + if lines_near_zero or touch_52: + self.macd_state = Chan_MACD_STATE.NEAR0 + #self.near0_return = 5 + return self.macd_state + + # 穿零轴后离开零轴 + if self.pre.macd_state == Chan_MACD_STATE.CROSS0_UP and ((self.macd >= self.pre.macd and self.signal >= self.pre.signal) or (abs(self.macdhist) >= abs(self.pre.macdhist))): + self.macd_state = Chan_MACD_STATE.UP + return self.macd_state + if self.pre.macd_state == Chan_MACD_STATE.CROSS0_DOWN and ((self.macd <= self.pre.macd and self.signal <= self.pre.signal) or (abs(self.macdhist) >= abs(self.pre.macdhist))): + self.macd_state = Chan_MACD_STATE.DOWN + return self.macd_state + if self.pre.macd_state == Chan_MACD_STATE.UP and self.macd > self.pre.macd and self.signal > self.pre.signal: + self.macd_state = Chan_MACD_STATE.UP + return self.macd_state + if self.pre.macd_state == Chan_MACD_STATE.DOWN and self.macd < self.pre.macd and self.signal < self.pre.signal: + self.macd_state = Chan_MACD_STATE.DOWN + return self.macd_state + # 趋势兜底:强势同步上行/下行直接进入 UP/DOWN + if self.macd > 0 and self.signal > 0 and (self.macd >= self.pre.macd and self.signal >= self.pre.signal): + self.macd_state = Chan_MACD_STATE.UP + return self.macd_state + if self.macd < 0 and self.signal < 0 and (self.macd <= self.pre.macd and self.signal <= self.pre.signal): + self.macd_state = Chan_MACD_STATE.DOWN + return self.macd_state + # 峰值:白线高位出现局部顶 + if hasattr(self.pre, 'pre') and self.pre and self.pre.pre and self.macd > 0: + if self.pre.macd > self.pre.pre.macd and self.pre.macd > self.macd: + self.macd_state = Chan_MACD_STATE.PEAK + return self.macd_state + # 高位状态的位置状态, 高位,高位空,归零轴 + if (self.pre.macd_state == Chan_MACD_STATE.UP or self.pre.macd_state == Chan_MACD_STATE.HIGH or self.pre.macd_state == Chan_MACD_STATE.RZ_UP or self.pre.macd_state == Chan_MACD_STATE.PEAK or self.pre.macd_state == Chan_MACD_STATE.HIGH_EMPTY) and self.macd > 0: + # 高位空(正区间):能量柱衰减且黄白线间距较大 + if abs(self.pre.macdhist) > 0 and abs(self.macdhist) < abs(self.pre.macdhist) and abs(self.macd - self.signal) > 5: + self.macd_state = Chan_MACD_STATE.HIGH_EMPTY + return self.macd_state + if abs(self.pre.macd - self.macd) < 10: + self.macd_state = Chan_MACD_STATE.HIGH + return self.macd_state + else: + if self.macd > self.pre.macd and self.signal > self.pre.signal: + self.macd_state = Chan_MACD_STATE.UP + return self.macd_state + elif self.macd < self.pre.macd and self.signal < self.pre.signal: + self.macd_state = Chan_MACD_STATE.RETURN_ZERO + return self.macd_state + if (self.pre.macd_state == Chan_MACD_STATE.DOWN or self.pre.macd_state == Chan_MACD_STATE.HIGH or self.pre.macd_state == Chan_MACD_STATE.RZ_DOWN or self.pre.macd_state == Chan_MACD_STATE.PEAK or self.pre.macd_state == Chan_MACD_STATE.HIGH_EMPTY) and self.macd < 0: + # 高位空(负区间):能量柱衰减且黄白线间距较大 + if abs(self.pre.macdhist) > 0 and abs(self.macdhist) < abs(self.pre.macdhist) and abs(self.macd - self.signal) > 5: + self.macd_state = Chan_MACD_STATE.HIGH_EMPTY + return self.macd_state + if abs(self.pre.macd - self.macd) < 10: + self.macd_state = Chan_MACD_STATE.HIGH + return self.macd_state + else: + if self.macd > self.pre.macd and self.signal > self.pre.signal: + self.macd_state = Chan_MACD_STATE.RETURN_ZERO + return self.macd_state + elif self.macd < self.pre.macd and self.signal < self.pre.signal: + self.macd_state = Chan_MACD_STATE.DOWN + return self.macd_state + + # 离开0轴开始上涨或者下跌阶段,高位之前的 + if self.macd > 0 and self.pre: + if (self.pre.macd_state == Chan_MACD_STATE.NEAR0 or self.pre.macd_state == Chan_MACD_STATE.RZ_UP or self.pre.macd_state == Chan_MACD_STATE.CROSS0_UP) and (self.signal > self.pre.signal or self.close > self.ema52): + self.macd_state = Chan_MACD_STATE.RZ_UP + return self.macd_state + elif self.macd < 0 and self.pre: + if (self.pre.macd_state == Chan_MACD_STATE.NEAR0 or self.pre.macd_state == Chan_MACD_STATE.RZ_DOWN or self.pre.macd_state == Chan_MACD_STATE.CROSS0_DOWN) and (self.signal < self.pre.signal or self.close < self.ema52): + self.macd_state = Chan_MACD_STATE.RZ_DOWN + return self.macd_state + # 归零轴走势 + if self.pre.macd_state == Chan_MACD_STATE.RETURN_ZERO: + if self.macd > 0: + if self.pre.macd > self.macd or abs(self.macdhist) <= abs(self.pre.macdhist) or self.signal <= self.pre.signal: + self.macd_state = Chan_MACD_STATE.RETURN_ZERO + return self.macd_state + else: + if self.pre.macd < self.macd or abs(self.macdhist) <= abs(self.pre.macdhist) or self.signal >= self.pre.signal: + self.macd_state = Chan_MACD_STATE.RETURN_ZERO + return self.macd_state + # 从 NEAR0 收敛到零轴的归零轴承接(正负两侧) + if self.pre.macd_state == Chan_MACD_STATE.NEAR0: + # 正区间朝零轴收敛 + if self.macd > 0 and self.pre.macd > 0 and self.macd <= self.pre.macd and self.signal <= self.pre.signal: + self.macd_state = Chan_MACD_STATE.RETURN_ZERO + return self.macd_state + # 负区间朝零轴收敛 + if self.macd < 0 and self.pre.macd < 0 and self.macd >= self.pre.macd and self.signal >= self.pre.signal: + self.macd_state = Chan_MACD_STATE.RETURN_ZERO + return self.macd_state + # 其余情况 + if self.pre.macd_state == Chan_MACD_STATE.UNKNOWN: + if self.macd > 0 and self.close > self.ema52 and self.pre.pre and (self.pre.pre.macd_state == Chan_MACD_STATE.UP or self.pre.pre.macd_state == Chan_MACD_STATE.RZ_UP): + self.macd_state = self.pre.pre.macd_state + return self.macd_state + elif self.macd < 0 and self.close < self.ema52 and self.pre.pre and (self.pre.pre.macd_state == Chan_MACD_STATE.DOWN or self.pre.pre.macd_state == Chan_MACD_STATE.RZ_DOWN): + self.macd_state = self.pre.pre.macd_state + return self.macd_state + else: + self.macd_state = Chan_MACD_STATE.UNKNOWN + return self.macd_state + return self.macd_state + \ No newline at end of file diff --git a/chan/core/ChanSBI.py b/chan/core/ChanSBI.py new file mode 100644 index 0000000..bb2a7eb --- /dev/null +++ b/chan/core/ChanSBI.py @@ -0,0 +1,91 @@ +import copy +from typing import Dict, Optional + +from .ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR +from . 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_seg_bi_broken(self): + broken = False + if self.fx == Chan_FX_TYPE.TOP: + if self.next.low < self.pre.high: + broken = True + elif self.fx == Chan_FX_TYPE.BOTTOM: + if self.next.high > self.pre.low: + broken = True + return broken + def check_bi_included(self, bi): + included = False + if self.high > bi.high: + # high大于,low小于,左包含 + if self.low < bi.low: + included = True + # high大于,low大于,不包含 + else: + # if self.low > bi.low + # high相等,右包含 + included = False + else: + included = False + # high小于,low大于,右包含 + #if self.low > bi.low: + #included = True + if included: + if self.pre: + if self.high > self.pre.high and self.low < self.pre.low: + included = True + if included: + self.add_bi(bi) + # gn>gn-1 + if self.dir == Chan_BI_DIR.DOWN: + # UP -> max(dn) + self.low = bi.low + else: + # DOWN -> min(gn) + self.high = bi.high + #self.print(bi, "Z") + #print(self.start_bi.start_time, bi.start_time, included) + return included \ No newline at end of file diff --git a/chan/core/ChanSEG.py b/chan/core/ChanSEG.py new file mode 100644 index 0000000..e7af768 --- /dev/null +++ b/chan/core/ChanSEG.py @@ -0,0 +1,197 @@ +import copy +from typing import Dict, Optional + +from .ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_SEG_DIR, Chan_BI_DIR, Chan_ZS_DIR +from . import ChanCTime +from .ChanBI import ChanBI +from .ChanBIZS import ChanBIZS +class ChanSEG(): + def __init__(self, start_bi: ChanBI, index, ddir=Chan_SEG_DIR.UP, pre_end_bi: ChanBI = None): + self.start_bi = start_bi + self.start_time = start_bi.start_time + self.end_time = None + self.end_bi = None + self.dir = ddir + self.low = 0 + self.high = 0 + if self.dir == Chan_SEG_DIR.UP and start_bi: + self.low = start_bi.low + else: + if start_bi: + self.high = start_bi.high + self.index = index + self.pre = None + self.next = None + self.bi_list = [] + self.bi_list.append(start_bi) + self.is_sure = False + self.sure_time = None + self.macd_hist = 0 + self.macd_div = 0 + self.start_bi.set_seg(self) + self.pre_end_bi = pre_end_bi + if self.pre_end_bi: + self.ini_seg() + def ini_seg(self): + next_bi = self.start_bi.next + for index in range(self.start_bi.index+1, self.pre_end_bi.index): + if next_bi: + self.bi_list.append(next_bi) + next_bi.set_seg(self) + next_bi = next_bi.next + def set_macdhist(self, macd_hist): + self.macd_hist = macd_hist + def set_macd_div(self, macd_div): + self.macd_div = macd_div + def set_end_bi(self, bi: ChanBI, sure_bi: ChanBI): + self.end_bi = bi + if bi and bi.is_sure: + if self.dir == Chan_SEG_DIR.UP: + self.high = bi.high + else: + self.low = bi.low + self.is_sure = True + self.end_time = bi.end_klc.end_time + if sure_bi.is_sure: + self.sure_time = sure_bi.sure_time + self.format_bi_list() + def pre_set_end_bi(self, bi: ChanBI): + self.end_bi = bi + if bi and bi.is_sure: + if self.dir == Chan_SEG_DIR.UP: + self.high = bi.high + else: + self.low = bi.low + self.end_time = bi.end_klc.end_time + self.format_bi_list() + def set_pre(self, seg): + self.pre = seg + def set_next(self, seg): + self.next = seg + def set_sure(self, sure_bi): + if sure_bi.is_sure: + self.sure_time = sure_bi.sure_time + self.is_sure = True + self.format_bi_list() + def format_bi_list(self): + self.bi_list = [] + self.bi_list.append(self.start_bi) + if self.end_bi: + next_bi = self.start_bi.next + for i in range(self.start_bi.index, self.end_bi.index): + if next_bi: + self.bi_list.append(next_bi) + next_bi.set_seg(self) + next_bi = next_bi.next + def add_bi(self, bi: ChanBI): + if len(self.bi_list) > 0: + self.bi_list.append(bi) + bi.set_seg(self) + self.end_time = bi.end_time + self.end_bi = bi + def cal_bi_zs(self): + zs_list = [] + if len(self.bi_list) > 3: + last_zs = None + if self.dir == Chan_SEG_DIR.UP: + for index in range(1, len(self.bi_list)): + bi = self.bi_list[index] + #print(bi.end_time, bi.next,"UP SEG BI ZS Index") + if bi.next == None or bi.next.next == None: + if last_zs and (bi.low > last_zs.zg or bi.high < last_zs.zd): + last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time) + continue + bi2 = bi.next + bi3 = bi.next.next + if len(zs_list) == 0 or (last_zs and last_zs.is_sure): + if bi3.is_sure and bi3.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.DOWN: + zg = min(bi.high, bi2.high, bi3.high) + zd = max(bi.low, bi2.low, bi3.low) + gg = max(bi.high, bi2.high, bi3.high) + dd = min(bi.low, bi2.low, bi3.low) + zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.UP) + zs.set_zg(zg) + zs.set_zd(zd) + zs.set_gg(gg) + zs.set_dd(dd) + zs.add_bi(bi2) + zs.add_bi(bi3) + zs_list.append(zs) + last_zs = zs + else: + if bi.index > last_zs.bi_list[-1].index and bi.dir == Chan_BI_DIR.DOWN and bi.is_sure: + if bi.low > last_zs.zg or bi.high < last_zs.zd: + #print(bi.end_time, "UP SEG BI ZS End") + last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time) + if bi3.is_sure and bi3.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.DOWN: + zg = min(bi.high, bi2.high, bi3.high) + zd = max(bi.low, bi2.low, bi3.low) + gg = max(bi.high, bi2.high, bi3.high) + dd = min(bi.low, bi2.low, bi3.low) + zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.UP) + zs.set_zg(zg) + zs.set_zd(zd) + zs.set_gg(gg) + zs.set_dd(dd) + zs.add_bi(bi2) + zs.add_bi(bi3) + zs_list.append(zs) + last_zs = zs + else: + last_zs.add_bi(bi.pre) + last_zs.add_bi(bi) + if index == len(self.bi_list) - 1 and last_zs and not last_zs.is_sure: + #print(bi.start_time, "BI", last_zs.is_sure) + last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time) + else: + for index in range(1, len(self.bi_list)): + bi = self.bi_list[index] + if bi.next == None or bi.next.next == None: + if last_zs and (bi.low > last_zs.zg or bi.high < last_zs.zd): + last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time) + continue + bi2 = bi.next + bi3 = bi.next.next + if len(zs_list) == 0 or (last_zs and last_zs.is_sure): + if bi3.is_sure and bi3.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.UP: + zg = min(bi.high, bi2.high, bi3.high) + zd = max(bi.low, bi2.low, bi3.low) + gg = max(bi.high, bi2.high, bi3.high) + dd = min(bi.low, bi2.low, bi3.low) + zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.DOWN) + zs.set_zg(zg) + zs.set_zd(zd) + zs.set_gg(gg) + zs.set_dd(dd) + zs.add_bi(bi2) + zs.add_bi(bi3) + zs_list.append(zs) + last_zs = zs + else: + if bi.index > last_zs.bi_list[-1].index and bi.dir == Chan_BI_DIR.UP and bi.is_sure: + if bi.low > last_zs.zg or bi.high < last_zs.zd: + last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time) + if bi3.is_sure and bi3.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.UP: + zg = min(bi.high, bi2.high, bi3.high) + zd = max(bi.low, bi2.low, bi3.low) + gg = max(bi.high, bi2.high, bi3.high) + dd = min(bi.low, bi2.low, bi3.low) + zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.DOWN) + zs.set_zg(zg) + zs.set_zd(zd) + zs.set_gg(gg) + zs.set_dd(dd) + zs.add_bi(bi2) + zs.add_bi(bi3) + zs_list.append(zs) + last_zs = zs + else: + last_zs.add_bi(bi.pre) + last_zs.add_bi(bi) + if index == len(self.bi_list) - 1 and last_zs and not last_zs.is_sure: + #print(bi.start_time, "BI", last_zs.is_sure) + last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time) + + #print(self.start_time, len(zs_list)) + #print(self.bi_list[-1].end_time, "end_bi") + return zs_list \ No newline at end of file diff --git a/chan/core/ChanZS.py b/chan/core/ChanZS.py new file mode 100644 index 0000000..7674417 --- /dev/null +++ b/chan/core/ChanZS.py @@ -0,0 +1,109 @@ +from typing import Dict, Optional + +from . import ChanKLC +from . import ChanSEG +from . 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.gg = 0 + self.dd = 0 + self.dir = ddir + self.sure_time = None + self.end_klc = None + self.bi_out_count = 0 + self.bi_out_list = [] + self.bi_out_seg_list = [] + self.bi_out_seg = None + self.is_extended = False + def set_last_bi_in(self, last_bi_in): + self.last_bi_in = last_bi_in + def set_bi_out(self, bi_out, bi_out_seg): + if bi_out: + #print(bi_out.start_klc.start_time, bi_out.sure_time, bi_out.dir, bi_out_seg.dir, len(self.bi_out_list)) + if len(self.bi_out_list) > 0: + last_bi = self.bi_out_list[-1] + if last_bi.index != bi_out.index: + self.bi_out_list.append(bi_out) + self.bi_out_seg_list.append(bi_out_seg) + else: + self.bi_out_list.append(bi_out) + self.bi_out_seg_list.append(bi_out_seg) + self.bi_out = bi_out + self.bi_out_seg = bi_out_seg + def set_end_klc(self, end_klc, sure_time, bi_out_count, seg): + self.end_klc = end_klc + self.set_end_time(end_klc.end_time) + self.is_sure = True + self.sure_time = sure_time + self.bi_out_count = bi_out_count + self.end_seg = seg + def set_end_seg(self, end_seg): + self.end_seg = end_seg + def set_pre(self, pre): + self.pre = pre + def set_next(self, next): + self.next = next + def set_end_time(self, end_time): + self.end_time = end_time + def add_klc(self, klc): + self.klc_list.append(klc) + def add_seg(self, seg): + self.seg_list.append(seg) + self.end_time = seg.end_time + self.end_seg = seg + def set_zg(self, zg): + self.zg = zg + def set_zd(self, zd): + self.zd = zd + def set_gg(self, gg): + self.gg = gg + def set_dd(self, dd): + self.dd = dd + def extend_zs(self, seg_list): + self.is_sure = False + self.end_seg = None + self.end_klc = None + self.sure_time = None + for seg in seg_list: + if seg.end_bi.high > self.gg: + self.set_gg(seg.end_bi.high) + if seg.end_bi.low < self.dd: + self.set_dd(seg.end_bi.low) + self.seg_list.append(seg) + self.is_extended = True + #print(self.start_time, "extend zs", seg_list[-1].end_time) + +# 大级别中枢:由多个区间重叠(扩张)的笔/线段中枢合并而成,用于显示更大级别的震荡区间 +class ChanZS_Big(): + def __init__(self, zs_list): + assert len(zs_list) >= 1 + self.zs_list = list(zs_list) + first = self.zs_list[0] + last = self.zs_list[-1] + self.start_time = first.start_time + self.end_time = last.end_time if last.end_time else None + self.start_klc = first.start_klc + self.end_klc = last.end_klc + # 大级别区间取并集:包住所有子中枢 + self.zd = min(zs.zd for zs in self.zs_list) + self.zg = max(zs.zg for zs in self.zs_list) + self.dd = min(zs.dd for zs in self.zs_list) + self.gg = max(zs.gg for zs in self.zs_list) + self.index = 0 # 由外部设置 \ No newline at end of file diff --git a/chan/core/Chan_FX_Box.py b/chan/core/Chan_FX_Box.py new file mode 100644 index 0000000..8a9d9ec --- /dev/null +++ b/chan/core/Chan_FX_Box.py @@ -0,0 +1,7 @@ +class Chan_FX_Box(): + def __init__(self, start_time, end_time, high, low): + self.start_time = start_time + self.end_time = end_time + self.high = high + self.low = low + \ No newline at end of file diff --git a/chan/core/__init__.py b/chan/core/__init__.py new file mode 100644 index 0000000..aabb4ea --- /dev/null +++ b/chan/core/__init__.py @@ -0,0 +1,23 @@ +"""纯缠论结构:K 线单元、合并、笔、段、中枢、买卖点几何。""" + +from .ChanEnum import * # noqa: F401,F403 +from .ChanKLU import ChanKLU +from .ChanKLC import ChanKLC +from .ChanBI import ChanBI +from .ChanSBI import ChanSBI +from .ChanSEG import ChanSEG +from .ChanZS import ChanZS, ChanZS_Big +from .ChanBIZS import ChanBIZS +from .ChanBSP import ChanBSP + +__all__ = [ + "ChanKLU", + "ChanKLC", + "ChanBI", + "ChanSBI", + "ChanSEG", + "ChanZS", + "ChanZS_Big", + "ChanBIZS", + "ChanBSP", +] diff --git a/chan/indicators/__init__.py b/chan/indicators/__init__.py new file mode 100644 index 0000000..d46f037 --- /dev/null +++ b/chan/indicators/__init__.py @@ -0,0 +1,13 @@ +"""指标外置层:不依赖笔/段/中枢,只产出按 idx 对齐的序列。""" + +from .config import IndicatorConfig +from .engine import IndicatorEngine +from .store import IndicatorStore +from .attach import attach_indicators_for_compat + +__all__ = [ + "IndicatorConfig", + "IndicatorEngine", + "IndicatorStore", + "attach_indicators_for_compat", +] diff --git a/chan/indicators/attach.py b/chan/indicators/attach.py new file mode 100644 index 0000000..42cf0b8 --- /dev/null +++ b/chan/indicators/attach.py @@ -0,0 +1,20 @@ +"""过渡期:把 IndicatorStore 挂到 KLU 属性上,兼容旧代码读取 klu.ema52 等。""" + +from __future__ import annotations + +from typing import Iterable + +from .store import IndicatorStore + + +def attach_indicators_for_compat(klu_list: Iterable, store: IndicatorStore) -> None: + """将指标写入 KLU(deprecated:新代码应通过 store.get(idx, name) 查询)。""" + for klu in klu_list: + idx = getattr(klu, "idx", None) + if idx is None: + continue + row = store.row(idx) + if row is None: + continue + if hasattr(klu, "set_indicators"): + klu.set_indicators(row) diff --git a/chan/indicators/config.py b/chan/indicators/config.py new file mode 100644 index 0000000..6a5bdf7 --- /dev/null +++ b/chan/indicators/config.py @@ -0,0 +1,25 @@ +"""指标参数配置(不再散落在结构流水线内)。""" + +from dataclasses import dataclass, field +from typing import List, Tuple + + +@dataclass +class IndicatorConfig: + macd_fast: int = 26 + macd_slow: int = 52 + macd_signal: int = 9 + ema_periods: Tuple[int, ...] = (5, 7, 10, 13, 24, 26, 52, 104, 156, 208) + rsi_period: int = 14 + atr_period: int = 14 + # (period, nbdevup, nbdevdn, name_suffix) + bbands: List[Tuple[int, float, float, str]] = field( + default_factory=lambda: [ + (365, 3.0, 3.0, "365"), + (120, 3.0, 3.0, "120"), + (20, 2.0, 2.0, "30"), + (20, 2.0, 2.0, "302"), + (26, 3.0, 3.0, "2633"), + ] + ) + bb_middle_sma_period: int = 90 diff --git a/chan/indicators/engine.py b/chan/indicators/engine.py new file mode 100644 index 0000000..580a708 --- /dev/null +++ b/chan/indicators/engine.py @@ -0,0 +1,74 @@ +"""从 OHLC DataFrame 计算指标,不触碰缠论结构对象。""" + +from __future__ import annotations + +from typing import Optional + +import talib.abstract as ta +import pandas as pd + +from .config import IndicatorConfig +from .store import IndicatorStore + + +def _volume_ratio(df: pd.DataFrame, window: int = 10) -> pd.Series: + vol = df["volume"].astype(float) + ma = vol.rolling(window=window).mean() + ratio = vol / ma + return ratio.fillna(1.0) + + +class IndicatorEngine: + def __init__(self, config: Optional[IndicatorConfig] = None): + self.config = config or IndicatorConfig() + + def compute(self, df: pd.DataFrame, config: Optional[IndicatorConfig] = None) -> IndicatorStore: + cfg = config or self.config + out = df.copy() + fast, slow, period = cfg.macd_fast, cfg.macd_slow, cfg.macd_signal + macd = ta.MACD(out, fastperiod=fast, slowperiod=slow, signalperiod=period) + out["macd"] = macd["macd"] + out["macdsignal"] = macd["macdsignal"] + out["macdhist"] = macd["macdhist"] + + for period_n in cfg.ema_periods: + out[f"ema{period_n}"] = ta.EMA(out, timeperiod=period_n) + + out["rsi"] = ta.RSI(out, timeperiod=cfg.rsi_period) + out["atr"] = ta.ATR(out, timeperiod=cfg.atr_period) + out["volume_ratio"] = _volume_ratio(out) + + bb_middle = ta.SMA(out, timeperiod=cfg.bb_middle_sma_period) + for bb_period, nbup, nbdn, suffix in cfg.bbands: + bb = ta.BBANDS( + out, + timeperiod=bb_period, + nbdevup=nbup, + nbdevdn=nbdn, + matype=0, + ) + bbp = (out["close"] - bb["lowerband"]) / (bb["upperband"] - bb["lowerband"]) + if suffix == "2633": + out["bb2633upper"] = bb["upperband"] + out["bb2633lower"] = bb["lowerband"] + out["bb2633middle"] = bb["middleband"] + out["bbp2633"] = bbp + elif suffix == "365": + out["bbup365"] = bb["upperband"] + out["bblow365"] = bb["lowerband"] + out["bbp365"] = bbp + elif suffix == "120": + out["bbup120"] = bb["upperband"] + out["bblow120"] = bb["lowerband"] + out["bbp120"] = bbp + elif suffix == "30": + out["bbup30"] = bb["upperband"] + out["bblow30"] = bb["lowerband"] + out["bbmiddle30"] = bb_middle + out["bbp30"] = bbp + elif suffix == "302": + out["bbup302"] = bb["upperband"] + out["bblow302"] = bb["lowerband"] + out["bbp302"] = bbp + + return IndicatorStore(out) diff --git a/chan/indicators/store.py b/chan/indicators/store.py new file mode 100644 index 0000000..15b39d1 --- /dev/null +++ b/chan/indicators/store.py @@ -0,0 +1,41 @@ +"""按 bar idx 查询指标值。""" + +from __future__ import annotations + +from typing import Any, Dict, Optional + +import pandas as pd + + +class IndicatorStore: + """以 DataFrame 列 + 行 idx 对齐的只读指标视图。""" + + def __init__(self, df: pd.DataFrame): + self._df = df + + @property + def dataframe(self) -> pd.DataFrame: + return self._df + + def __len__(self) -> int: + return len(self._df) + + def get(self, idx: int, name: str, default: Any = None) -> Any: + if idx < 0 or idx >= len(self._df): + return default + if name not in self._df.columns: + return default + val = self._df.iloc[idx][name] + if pd.isna(val): + return default + return val + + def row(self, idx: int) -> Optional[Dict[str, Any]]: + if idx < 0 or idx >= len(self._df): + return None + return self._df.iloc[idx].to_dict() + + def series(self, name: str): + if name not in self._df.columns: + return None + return self._df[name] diff --git a/chan/pipeline/ChanLun.py b/chan/pipeline/ChanLun.py new file mode 100644 index 0000000..9def22a --- /dev/null +++ b/chan/pipeline/ChanLun.py @@ -0,0 +1,188 @@ +import warnings + +# 抑制 Docker 内 technical.util 的 fillna/ffill/bfill 的 pandas FutureWarning(pandas 2.x 弃用 object 静默 downcast) +warnings.filterwarnings( + "ignore", + category=FutureWarning, + message=".*Downcasting object dtype arrays on \\.fillna.*", +) + +from datetime import timedelta +from pandas import DataFrame +from ..core.ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_MACD_STATE, Chan_PRICE_TREND, Chan_KLU_PATTERN +from ..core.ChanKLU import ChanKLU +from ..core.ChanKLC import ChanKLC +from ..core.ChanBI import ChanBI +from ..core.ChanSBI import ChanSBI +from ..core.ChanSEG import ChanSEG +from ..core.ChanZS import ChanZS +from ..core.ChanBSP import ChanBSP +import talib.abstract as ta +import pandas as pd +from technical.util import resample_to_interval +from decimal import Decimal +import numpy as np +from ..analysis.ChanMACD import ChanMACD +from .TF_DF import TF_DF +from ..analysis.ChanZone import StructureZone, StructureZoneConfig, analyze_structure_zones + +class ChanLun(): + def __init__(self): + self.time2m = 2 + self.time3m = 3 + self.time5m = 5 + self.time10m = 10 + self.time20m = 20 + self.time_m_intervals = [2, 3, 5, 10, 20] + self.time_m_symbols = ['2m', '3m', '5m', '10m', '20m'] + self.time30m = 30 + self.time45m = 45 + self.time_m15_intervals = [30, 45] + self.time_m15_symbols = ['30m', '45m'] + self.time2h = 2*60 + self.time4h = 4*60 + self.time6h = 6*60 + self.time8h = 8*60 + self.time12h = 12*60 + self.time16h = 16*60 + self.time_h_intervals = [2*60, 4*60, 6*60, 8*60, 12*60, 16*60] + self.time_h_symbols = ['2h', '4h', '6h', '8h', '12h', '16h'] + self.time2d = 2*24*60 + self.time3d = 3*24*60 + self.time_d_intervals = [2*24*60, 3*24*60] + self.time_d_symbols = ['2d', '3d'] + self.time1w = 7*24*60 + self.time2w = 14*24*60 + self.time_w_intervals = [14*24*60] + self.time_w_symbols = ['2w'] + self.time2M = 2*30*24*60 + self.time3M = 3*30*24*60 + self.time6M = 6*30*24*60 + self.time1y = 12*30*24*60 + self.time_M_intervals = [2*30*24*60, 3*30*24*60, 6*30*24*60, 12*30*24*60] + self.time_M_symbols = ['2M', '3M', '6M', '1y'] + self.time_symbols = ['1m', '2m', '3m', '5m', '10m', '15m', '20m', '30m', '45m','1h', '2h', '4h', '6h', '8h', '12h', '16h', '1d', '2d', '3d'] + self.tf_df_dict = {} + self.ema_symbols = ['5m', '15m', '30m', '45m', '1h', '2h', '4h', '8h', '12h', '1d', '2d', '3d'] + self.tf_df = TF_DF() + def init_data(self, dataframe, intervals, timeframes): + for index in range(0, len(intervals)): + timeframe = timeframes[index] + interval = intervals[index] + self.tf_df_dict[timeframe] = TF_DF(dataframe, interval, timeframe) + def init_dataframes(self, dataframe_m=None, dataframe_15m=None, dataframe_h=None, dataframe_d=None, dataframe_w=None, dataframe_M=None): + self.tf_df_dict = {} + if dataframe_m is not None: + self.tf_df_dict['1m'] = TF_DF(dataframe_m, 1, '1m') + self.init_data(dataframe_m, self.time_m_intervals, self.time_m_symbols) + if dataframe_15m is not None: + self.tf_df_dict['15m'] = TF_DF(dataframe_15m, 1, '15m') + self.init_data(dataframe_15m, self.time_m15_intervals, self.time_m15_symbols) + if dataframe_h is not None: + self.tf_df_dict['1h'] = TF_DF(dataframe_h, 1, '1h') + self.init_data(dataframe_h, self.time_h_intervals, self.time_h_symbols) + if dataframe_d is not None: + self.tf_df_dict['1d'] = TF_DF(dataframe_d, 1, '1d') + self.init_data(dataframe_d, self.time_d_intervals, self.time_d_symbols) + if dataframe_w is not None and False: + self.tf_df_dict['1w'] = TF_DF(dataframe_w, 1, '1w') + self.init_data(dataframe_w, self.time_w_intervals, self.time_w_symbols) + if dataframe_M is not None and False: + self.tf_df_dict['1M'] = TF_DF(dataframe_M, 1, '1M') + self.init_data(dataframe_M, self.time_M_intervals, self.time_M_symbols) + def get_ema52_dict(self): + if len(self.tf_df_dict) > 0: + return {key: self.tf_df_dict[key].get_ema52() for key in self.ema_symbols} + return None + def get_ema24_dict(self): + if len(self.tf_df_dict) > 0: + return {key: self.tf_df_dict[key].get_ema24() for key in self.ema_symbols} + return None + def get_current_klc_dict(self): + if len(self.tf_df_dict) > 0: + return {key: self.tf_df_dict[key].get_current_klc() for key in self.ema_symbols} + return None + def get_tf_df_by_timeframe(self, timeframe): + if timeframe in self.tf_df_dict: + return self.tf_df_dict[timeframe] + return None + def check_price_ema52(self, price): + key_list = [] + if len(self.tf_df_dict) > 0: + ema52_dict = self.get_ema52_dict() + for key in self.ema_symbols: + if ema52_dict[key] is not None: + if abs(price - ema52_dict[key]) < 100: + key_list.append(key) + return key_list + def get_ema_bsp(self, long_tf='1h', short_tf='15m'): + if long_tf in self.tf_df_dict and short_tf in self.tf_df_dict: + long_df = self.tf_df_dict[long_tf] + short_df = self.tf_df_dict[short_tf] + return long_df.get_ema_bsp(short_df) + return None + + + + + + def get_bsp_state(self, dataframe): + return self.tf_df.get_bsp_state(dataframe) + + def get_structure_zones(self, current_price=None, config=None): + if config is None: + config = StructureZoneConfig() + return analyze_structure_zones( + self.tf_df_dict, + self.ema_symbols, + current_price=current_price, + config=config, + ) + # TF_DF methods ------------------------------------------ + def get_ema_state(self, dataframe): + return self.tf_df.get_ema_state(dataframe) + def get_klu_state(self, dataframe): + return self.tf_df.get_klu_state(dataframe) + def check_fx(self, klc): + return self.tf_df.check_fx(klc) + def add_indicators1(self, df): + return self.tf_df.add_indicators(df) + def get_bi_list(self, dataframe): + return self.tf_df.get_bi_list(dataframe) + def get_kl_data(self, dataframe:DataFrame): + return self.tf_df.cal_kl_data(dataframe) + def cal_volume_ratio(self, dataframe, window=10): + return self.tf_df.cal_volume_ratio(dataframe, window) + def calculate_seg_zs(self, bi_list, seg_list): + return self.get_seg_zs_list(bi_list, seg_list) + def get_seg_list(self, bi_list): + return self.tf_df.get_seg_list(bi_list) + def cal_trend(self, klc_list): + return self.tf_df.cal_trend(klc_list) + def check_top_fx(self, last_bottom, klc): + return self.tf_df.check_top_fx(last_bottom, klc) + def check_bottom_fx(self, last_top, klc): + return self.tf_df.check_bottom_fx(last_top, klc) + def cal_bi_list(self, klc_list): + return self.tf_df.cal_bi_list(klc_list) + def find_first_bsp(self, bi_list, bi_zs_list): + return self.tf_df.find_first_bsp(bi_list, bi_zs_list) + def find_second_bsp(self, bi_list, first_bsp_list): + return self.tf_df.find_second_bsp(bi_list, first_bsp_list) + def find_all_bsp(self, bi_list, bi_zs_list): + return self.tf_df.find_all_bsp(bi_list, bi_zs_list) + def get_zs_list(self, bi_list, seg_list): + return self.tf_df.get_zs_list(bi_list, seg_list) + def cal_bi_zs(self, seg_list): + return self.tf_df.cal_bi_zs(seg_list) + def cal_bi_zs_list(self, bi_list): + #return self.tf_df.cal_bi_zs(bi_list) + return self.tf_df.cal_bi_zs_list(bi_list) + def get_bi_zs_list(self, bi_list): + return self.tf_df.get_bi_zs_list(bi_list) + def get_decimal(self, value): + return Decimal("{:.2f}".format(value)) + def get_klc_list(self, klu_list): + return self.tf_df.get_klc_list(klu_list) + def get_klu_list(self, dataframe): + return self.tf_df.cal_klu_pattern(self.get_kl_data(dataframe)) \ No newline at end of file diff --git a/chan/pipeline/TF_DF.py b/chan/pipeline/TF_DF.py new file mode 100644 index 0000000..baa5876 --- /dev/null +++ b/chan/pipeline/TF_DF.py @@ -0,0 +1,2552 @@ +from datetime import timedelta +from pandas import DataFrame +from ..core.ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_PRICE_TREND, Chan_KLU_PATTERN, Chan_K_DIR, Chan_KLC_STATE +from ..core.ChanKLU import ChanKLU +from ..core.ChanKLC import ChanKLC +from ..core.ChanBI import ChanBI +from ..core.ChanSBI import ChanSBI +from ..core.ChanSEG import ChanSEG +from ..core.ChanZS import ChanZS, ChanZS_Big +from ..core.ChanBIZS import ChanBIZS +from ..core.ChanBSP import ChanBSP +import talib.abstract as ta +import pandas as pd +from technical.util import resample_to_interval +from decimal import Decimal +import numpy as np +from typing import Optional + +from ..analysis.ChanMACD import ChanMACD +from ..analysis.bsp_macd import check_bi_div as bsp_check_bi_div, confirm_bsp +from ..indicators import IndicatorEngine, IndicatorStore, attach_indicators_for_compat + +class TF_DF(): + def __init__(self, df=None, interval=0, timeframe=None): + self.indicator_store: Optional[IndicatorStore] = None + self.confirmed_bsp_list = [] + if df is not None: + self.init_TF_DF(df, interval, timeframe) + def init_TF_DF(self, df, interval, timeframe): + self.timeframe = timeframe + self.interval = interval + # 检查 DataFrame 是否为空或没有 date 列 + if df is None or df.empty: + raise ValueError(f"DataFrame for {timeframe} is empty. Please download data first.") + if 'date' not in df.columns: + raise ValueError(f"DataFrame for {timeframe} missing 'date' column. Columns: {df.columns.tolist()}") + # interval=1 时不需要重采样 + if interval == 1: + ohlc = df.copy() + else: + ohlc = resample_to_interval(df, interval) + # 指标外置:先算 IndicatorStore,再跑结构;attach 仅兼容旧属性访问 + self.indicator_store = IndicatorEngine().compute(ohlc) + self.dataframe = self.indicator_store.dataframe + self.klu_list = [] + self.klc_list = [] + self.bi_list = [] + self.zs_list = [] + self.bsp_list = [] + self.seg_list = [] + self.klc_fx_list = [] + self.confirmed_bsp_list = [] + self.klu_list = self.cal_kl_data(self.dataframe) + attach_indicators_for_compat(self.klu_list, self.indicator_store) + self.klc_list = self.get_klc_list(self.klu_list) + self.bi_list = self.cal_bi_list(self.klc_list) + self.seg_list = self.get_seg_list(self.bi_list) + self.zs_list = self.get_zs_list(self.bi_list, self.seg_list) + self.big_zs_list = self.get_big_zs_list(self.zs_list) + self.chanmacd = ChanMACD(self.klu_list) + self.klu_list = self.chanmacd.cal_macd_state() + + + def get_ema52(self, index=-1): + if self.indicator_store is not None and self.klu_list: + idx = self.klu_list[index].idx + ema52_value = self.indicator_store.get(idx, "ema52") + if ema52_value is None or pd.isna(ema52_value): + return None + return float(ema52_value) + if self.klu_list: + ema52_value = self.klu_list[index].ema52 + # 处理NaN值 + if pd.isna(ema52_value) or ema52_value is None: + return None + return float(ema52_value) + return None + def get_ema24(self, index=-1): + if self.indicator_store is not None and self.klu_list: + idx = self.klu_list[index].idx + ema24_value = self.indicator_store.get(idx, "ema24") + if ema24_value is None or pd.isna(ema24_value): + return None + return float(ema24_value) + if self.klu_list: + ema24_value = self.klu_list[index].ema24 + # 处理NaN值 + if pd.isna(ema24_value) or ema24_value is None: + return None + return float(ema24_value) + return None + def get_current_klc(self): + if len(self.klc_list) > 0: + return self.klc_list[-2] + return None + def add_indicators(self, df): + """兼容旧调用:委托 IndicatorEngine,返回带指标列的 DataFrame。""" + store = IndicatorEngine().compute(df) + return store.dataframe + def get_klu_state(self, dataframe): + klc_list = self.get_klc_list(self.get_klu_list(dataframe)) + bi_list = self.cal_bi_list(klc_list) + klu_state_list = [] + klc_index = 0 + for index in range(0, len(dataframe)): + if klc_index == len(klc_list): + klc_index = len(klc_list) - 1 + klc = klc_list[klc_index] + if klc.end_klu and klc.end_klu.idx == index: + if klc.klc_state == Chan_KLC_STATE.S10: + klu_state_list.append("10") + #print(klc.end_time, klc.klc_fx_type) + elif klc.klc_state == Chan_KLC_STATE.S_10: + klu_state_list.append("-10") + #print(klc.end_time, klc.klc_fx_type) + elif klc.klc_state == Chan_KLC_STATE.S11: + klu_state_list.append("11") + #print(klc.end_time, klc.klc_fx_type) + elif klc.klc_state == Chan_KLC_STATE.S_11: + klu_state_list.append("-11") + #print(klc.end_time, klc.klc_fx_type) + else: + klu_state_list.append("00") + klc_index += 1 + else: + klu_state_list.append("00") + print(klu_state_list[:20]) + return klu_state_list + + def get_bsp_state(self, dataframe): + klu_list = self.get_klu_list(dataframe) + klc_list = self.get_klc_list(klu_list) + bi_list = self.cal_bi_list(klc_list) + seg_list = self.get_seg_list(bi_list) + bi_zs_list = self.cal_bi_zs(seg_list) + bsp_list = self.find_all_bsp(bi_list, bi_zs_list) + bsp_state_list = [0] * len(dataframe) + klc_index = 0 + for index in range(0, len(dataframe)): + if klc_index == len(klc_list): + klc_index = len(klc_list) - 1 + klc = klc_list[klc_index] + if klc.end_klu and klc.end_klu.idx == index: + if klc.klc_fx_type == Chan_KLC_FX.TOP2: + bi = klc.bi.pre + if bi and bi.is_sure and bi.end_klc.bsp_type == Chan_BSP_TYPE.B3: + # 第三类买点 + bsp_state_list[index] = -1 + #print(klc.end_time, "B3") + else: + bsp_state_list[index] = 0 + elif klc.klc_fx_type == Chan_KLC_FX.BOTTOM2: + bi = klc.bi.pre + if bi and bi.is_sure and bi.end_klc.bsp_type == Chan_BSP_TYPE.S3: + # 第三类卖点 + bsp_state_list[index] = 1 + #print(klc.end_time, "S3") + else: + bsp_state_list[index] = 0 + klc_index += 1 + else: + bsp_state_list[index] = 0 + return bsp_state_list + def get_ema_state(self, dataframe): + klu_list = self.get_klu_list(dataframe) + klc_list = self.get_klc_list(klu_list) + bi_list = self.cal_bi_list(klc_list) + klu_state_list = [] + for klu in klu_list: + if klu.near0_return == 1: + klu_state_list.append("1") + elif klu.near0_return == 9: + klu_state_list.append("-1") + elif klu.candle_dir == Chan_K_DIR.BULL: + klu_state_list.append("2") + elif klu.candle_dir == Chan_K_DIR.BEAR: + klu_state_list.append("-2") + else: + klu_state_list.append("0") + return klu_state_list + def check_fx1(self, klc): + if klc.pre and klc.next: + if klc.high > klc.pre.high and klc.high > klc.next.high and klc.low > klc.pre.low and klc.low > klc.next.low: + if klc.pre.pre and klc.next.next: + if klc.high > klc.pre.pre.high and klc.high > klc.next.next.high: + #if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0: + klc.set_fx(Chan_FX_TYPE.TOP) + #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "TOP") + return Chan_FX_TYPE.TOP + elif klc.low < klc.pre.low and klc.low < klc.next.low and klc.high < klc.pre.high and klc.high < klc.next.high: + #if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0: + if klc.pre.pre and klc.next.next: + if klc.low < klc.pre.pre.low and klc.low < klc.next.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.macd, klc.state, klc.fx, "BOTTOM") + return Chan_FX_TYPE.BOTTOM + return Chan_FX_TYPE.UNKNOWN + def check_fx(self, klc): + if klc.pre and klc.next: + if klc.high > klc.pre.high and klc.high > klc.next.high and klc.low > klc.pre.low and klc.low > klc.next.low: + #if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0: + klc.set_fx(Chan_FX_TYPE.TOP) + #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "TOP") + return Chan_FX_TYPE.TOP + elif klc.low < klc.pre.low and klc.low < klc.next.low and klc.high < klc.pre.high and klc.high < klc.next.high: + #if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0: + klc.set_fx(Chan_FX_TYPE.BOTTOM) + #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "BOTTOM") + return Chan_FX_TYPE.BOTTOM + return Chan_FX_TYPE.UNKNOWN + def check_fx2(self, klc): + if klc.pre and klc.next: + if klc.high > klc.pre.close and klc.close > klc.next.close and klc.close > klc.pre.close and klc.close > klc.next.close: + #if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0: + klc.set_fx(Chan_FX_TYPE.TOP) + #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "TOP") + return Chan_FX_TYPE.TOP + elif klc.low < klc.pre.close and klc.close < klc.next.close and klc.close < klc.pre.close and klc.close < klc.next.close: + #if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0: + klc.set_fx(Chan_FX_TYPE.BOTTOM) + #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "BOTTOM") + return Chan_FX_TYPE.BOTTOM + return Chan_FX_TYPE.UNKNOWN + def check_fx_pattern(self, klc): + klu_list = klc.pre.klu_list + klc.klu_list + klc.next.klu_list + + self.cal_klu_pattern(klu_list) + p = "" + for klu in klu_list: + p += klu.to_string() + #print(p) + def cal_volume_ratio(self, dataframe, window=10): + df = dataframe.copy() + # 计算过去N根K线的平均成交量 + df['avg_volume'] = df['volume'].rolling(window=window).mean() + # 计算量比 + df['volume_ratio'] = df['volume'] / df['avg_volume'] + # 填充缺失值(前N根K线) + df['volume_ratio'] = df['volume_ratio'].fillna(1.0) + return df['volume_ratio'] + def cal_trend(self, klc_list): + """ + 基于价格与EMA24/EMA52的位置关系、以及MACD/Signal/Hist的方向, + 为每个KLC打上趋势标签:'UP' / 'DOWN' / 'FLAT'。 + 仅设置 klc.trend,不影响其它字段。 + """ + if not klc_list: + return klc_list + last_trend = Chan_PRICE_TREND.UNKNOWN + # 趋势延续性:参考近 N 根已完成的KLC + lookback_n = 5 + prev_klcs = [] + for klc in klc_list: + price = getattr(klc, 'close', None) + ema24 = getattr(klc, 'ema24', None) + ema52 = getattr(klc, 'ema52', None) + macd_raw = getattr(klc, 'macd', None) + signal_raw = getattr(klc, 'signal', None) + hist_raw = getattr(klc, 'macdhist', None) + macd = macd_raw if macd_raw is not None else 0 + signal = signal_raw if signal_raw is not None else 0 + hist = hist_raw if hist_raw is not None else 0 + rsi = getattr(klc, 'rsi', None) + macd_ready = macd_raw is not None and signal_raw is not None + hist_ready = hist_raw is not None + trend = Chan_PRICE_TREND.UNKNOWN + score = 0 + try: + # 有效性 + price_valid = price is not None and price != 0 + ema24_valid = ema24 is not None and ema24 != 0 + ema52_valid = ema52 is not None and ema52 != 0 + # 多因子投票 + + # 1) 均线结构 + 价位 + if ema24_valid or ema52_valid: + ma_votes = 0 + if ema24_valid and ema52_valid: + ma_votes += 1 if ema24 > ema52 else -1 + if price_valid and ema24_valid: + ma_votes += 1 if price > ema24 else 0 + if price_valid and ema52_valid: + ma_votes += 1 if price > ema52 else -1 + # 限幅,避免相关因子重复计分 + score += max(-2, min(2, ma_votes)) + # 2) MACD结构 + if macd_ready: + score += 1 if macd >= signal else -1 + if hist_ready and hist != 0: + score += 1 if hist > 0 else -1 + # 3) 动量与均线差分斜率 + pre = getattr(klc, 'pre', None) + pre_hist = getattr(pre, 'macdhist', None) if pre else None + if pre: + pre_close = getattr(pre, 'close', None) + if price_valid and pre_close is not None: + score += 1 if price >= pre_close else -1 + pre_ema24 = getattr(pre, 'ema24', None) + pre_ema52 = getattr(pre, 'ema52', None) + if ema24_valid and ema52_valid and pre_ema24 not in (None, 0) and pre_ema52 not in (None, 0): + spread_now = ema24 - ema52 + spread_pre = pre_ema24 - pre_ema52 + score += 1 if spread_now >= spread_pre else -1 + # 3.1) MACD柱体动量趋势:考虑 macdhist 的斜率与过零 + if hist_ready and pre_hist is not None: + # 柱体斜率:上升加分,下降减分 + if hist > pre_hist: + score += 1 + elif hist < pre_hist: + score -= 1 + # 过零加权:负转正更偏多,正转负更偏空 + if pre_hist < 0 and hist > 0: + score += 1 + elif pre_hist > 0 and hist < 0: + score -= 1 + # 3.2) EMA52 突破/跌破加权 + if ema52_valid and price_valid and pre_close is not None and pre_ema52 not in (None, 0): + # 看多突破:从均线下方上破且动量配合 + if pre_close <= pre_ema52 and price > ema52 and (hist is None or pre_hist is None or hist >= pre_hist): + score += 1 + # 看空跌破:从均线上方下破且动量配合 + if pre_close >= pre_ema52 and price < ema52 and (hist is None or pre_hist is None or hist <= pre_hist): + score -= 1 + # 3.3) EMA52 支撑/阻力触碰(非强穿越) + if ema52_valid and price_valid: + low_v = getattr(klc, 'low', None) + high_v = getattr(klc, 'high', None) + if low_v is not None and high_v is not None and ema52 not in (None, 0): + # 触碰容差(相对EMA52的0.15%) + touch_tol = 0.0015 + # 作为支撑:收盘在上,最低靠近EMA52 + near_support_touch = (price > ema52) and (abs(low_v - ema52) / abs(ema52) <= touch_tol) + # 作为阻力:收盘在下,最高靠近EMA52 + near_resistance_touch = (price < ema52) and (abs(high_v - ema52) / abs(ema52) <= touch_tol) + if near_support_touch: + # 若动量不弱,则更偏多 + score += 1 if (hist is None or pre_hist is None or hist >= pre_hist) else 0 + if near_resistance_touch: + # 若动量不强,则更偏空 + score -= 1 if (hist is None or pre_hist is None or hist <= pre_hist) else 0 + # 3.4) 多次对 EMA52 的"拒绝"配合 MACD 逆向:易形成压/支并反向 + # 统计近窗口内的上/下拒绝次数: + # - 上拒绝:价格位于 EMA52 下方,最高触及/越过 EMA52 但收盘仍在下方 + # - 下拒绝:价格位于 EMA52 上方,最低触及/跌破 EMA52 但收盘仍在上方 + recent_up_rejects = 0 + recent_down_rejects = 0 + if ema52_valid: + window_rej = prev_klcs[-lookback_n:] if len(prev_klcs) > 0 else [] + rej_tol = 0.0015 + for wk in window_rej: + wk_close = getattr(wk, 'close', None) + wk_ema52 = getattr(wk, 'ema52', None) + wk_high = getattr(wk, 'high', None) + wk_low = getattr(wk, 'low', None) + if wk_close is None or wk_ema52 in (None, 0): + continue + # 上拒绝(阻力):下方多次试图上破但未站上 + if wk_close < wk_ema52 and wk_high is not None: + if wk_high >= wk_ema52 or abs(wk_high - wk_ema52) / abs(wk_ema52) <= rej_tol: + recent_up_rejects += 1 + # 下拒绝(支撑):上方多次试图下破但未跌破 + if wk_close > wk_ema52 and wk_low is not None: + if wk_low <= wk_ema52 or abs(wk_low - wk_ema52) / abs(wk_ema52) <= rej_tol: + recent_down_rejects += 1 + # 定义 MACD 的方向偏好 + macd_bias_up = macd_ready and (macd >= signal) and (not hist_ready or pre_hist is None or hist >= pre_hist) + macd_bias_down = macd_ready and (macd <= signal) and (not hist_ready or pre_hist is None or hist <= pre_hist) + # 若多次上拒绝且 MACD 偏空,则更偏向下行;若多次下拒绝且 MACD 偏多,则更偏向上行 + if recent_up_rejects >= 2 and macd_bias_down: + score -= 2 + if recent_down_rejects >= 2 and macd_bias_up: + score += 2 + # 4) RSI 辅助 + if rsi is not None: + if rsi >= 55: + score += 1 + elif rsi <= 45: + score -= 1 + # 5) 指标未就绪回退(EMA/MACD缺失时,用动量与RSI辅助,延续趋势) + has_full_ind = ema24_valid and ema52_valid and not (macd == 0 and signal == 0 and hist == 0) + if not has_full_ind: + # 仅根据价动量/RSI做轻量判断,默认延续 last_trend,除非出现强反向 + strong_up = False + strong_down = False + pre = getattr(klc, 'pre', None) + if pre: + pre_close = getattr(pre, 'close', None) + if price_valid and pre_close is not None: + strong_up = (price >= pre_close) + strong_down = (price < pre_close) + if rsi is not None: + if rsi >= 60: + strong_up = True + elif rsi <= 40: + strong_down = True + if last_trend == Chan_PRICE_TREND.UP and not strong_down: + trend = Chan_PRICE_TREND.UP + elif last_trend == Chan_PRICE_TREND.DOWN and not strong_up: + trend = Chan_PRICE_TREND.DOWN + else: + trend = Chan_PRICE_TREND.UP if strong_up and not strong_down else (Chan_PRICE_TREND.DOWN if strong_down and not strong_up else Chan_PRICE_TREND.FLAT) + else: + # 6) 震荡过滤(仅当极近EMA52且MACD贴合时判作震荡) + near_flat = False + if price_valid and ema52_valid: + near_ema52 = abs(price - ema52) / abs(ema52) <= 0.0005 # 0.05% + if macd_ready: + macd_scale = max(abs(macd), abs(signal), 1e-6) + near_macd = abs(macd - signal) / macd_scale <= 0.05 + else: + near_macd = False + near_flat = near_ema52 and near_macd + # 7) 动态阈值 + 趋势记忆(更强粘滞:趋势中容忍小幅反分) + # 引入过去 N 根KLC 的趋势延续性来动态调整翻转阈值,并结合 EMA52 支撑/阻力触碰强化门槛 + force_flip_down = False + force_flip_up = False + if near_flat: + trend = Chan_PRICE_TREND.FLAT + else: + # 计算过去窗口的趋势一致性 + window = prev_klcs[-lookback_n:] if len(prev_klcs) > 0 else [] + persist_up = 0 + persist_down = 0 + for wk in window: + if getattr(wk, 'trend', None) == Chan_PRICE_TREND.UP: + persist_up += 1 + elif getattr(wk, 'trend', None) == Chan_PRICE_TREND.DOWN: + persist_down += 1 + persist_ratio_up = (persist_up / len(window)) if len(window) > 0 else 0 + persist_ratio_down = (persist_down / len(window)) if len(window) > 0 else 0 + # 基准阈值 + down_flip_threshold = -2 + up_flip_threshold = 2 + # 若最近多为UP,则从UP翻转需更强反向信号;同理对DOWN + if last_trend == Chan_PRICE_TREND.UP and persist_ratio_up >= 0.6: + down_flip_threshold = -3 + elif last_trend == Chan_PRICE_TREND.DOWN and persist_ratio_down >= 0.6: + up_flip_threshold = 3 + # EMA52 触碰强化门槛:UP时若出现支撑触碰,下翻更难;DOWN时若出现阻力触碰,上翻更难 + if ema52_valid and price_valid: + low_v = getattr(klc, 'low', None) + high_v = getattr(klc, 'high', None) + if low_v is not None and high_v is not None and ema52 not in (None, 0): + touch_tol = 0.0015 + near_support_touch = (price > ema52) and (abs(low_v - ema52) / abs(ema52) <= touch_tol) + near_resistance_touch = (price < ema52) and (abs(high_v - ema52) / abs(ema52) <= touch_tol) + if last_trend == Chan_PRICE_TREND.UP and near_support_touch: + # 强化维持UP:进一步降低向下翻转阈值 + down_flip_threshold = min(down_flip_threshold - 1, -3) + if last_trend == Chan_PRICE_TREND.DOWN and near_resistance_touch: + # 强化维持DOWN:进一步提高向上翻转阈值 + up_flip_threshold = max(up_flip_threshold + 1, 3) + # 7.1) 复合拐头信号:MACD/Signal 同向拐头 + hist 连续减弱 + 多次未能越过 EMA52 + pre_macd = getattr(pre, 'macd', None) if pre else None + pre_signal = getattr(pre, 'signal', None) if pre else None + macd_slope = (macd - pre_macd) if (macd_ready and pre_macd is not None) else 0 + signal_slope = (signal - pre_signal) if (macd_ready and pre_signal is not None) else 0 + # hist 连续减弱(绝对值缩小) + hist_seq = [] + for wk in prev_klcs[-2:]: + val = getattr(wk, 'macdhist', None) + if val is not None: + hist_seq.append(val) + if hist is not None: + hist_seq.append(hist) + weaken_steps = 0 + for i in range(1, len(hist_seq)): + if abs(hist_seq[i]) < abs(hist_seq[i-1]): + weaken_steps += 1 + # 近窗口对 EMA52 的"未能站上/跌破"统计(放宽窗口与条件) + window_ema = prev_klcs[-4:] if len(prev_klcs) > 0 else [] + no_up_break = False + no_down_break = False + if ema52_valid: + # 未能有效上破:最近若干根收盘大多数不在 EMA52 上方,且高点多次触及/接近 + cnt_touch_up = 0 + cnt_close_above = 0 + for wk in window_ema: + wk_close = getattr(wk, 'close', None) + wk_high = getattr(wk, 'high', None) + wk_ema = getattr(wk, 'ema52', None) + if wk_close is not None and wk_ema not in (None, 0): + if wk_close > wk_ema: + cnt_close_above += 1 + if wk_high is not None and (wk_high >= wk_ema or abs(wk_high - wk_ema) / abs(wk_ema) <= 0.0015): + cnt_touch_up += 1 + no_up_break = (cnt_close_above <= 1 and cnt_touch_up >= 1 and price <= ema52) + # 未能有效下破:最近若干根收盘大多数不在 EMA52 下方,且低点多次触及/接近 + cnt_touch_down = 0 + cnt_close_below = 0 + for wk in window_ema: + wk_close = getattr(wk, 'close', None) + wk_low = getattr(wk, 'low', None) + wk_ema = getattr(wk, 'ema52', None) + if wk_close is not None and wk_ema not in (None, 0): + if wk_close < wk_ema: + cnt_close_below += 1 + if wk_low is not None and (wk_low <= wk_ema or abs(wk_low - wk_ema) / abs(wk_ema) <= 0.0015): + cnt_touch_down += 1 + no_down_break = (cnt_close_below <= 1 and cnt_touch_down >= 1 and price >= ema52) + # 若当前为UP趋势,出现明显拐头+hist减弱+未能上破EMA52,则加速看空 + if last_trend == Chan_PRICE_TREND.UP and macd_slope < 0 and signal_slope < 0 and weaken_steps >= 1 and no_up_break and macd_bias_down: + score -= 3 + down_flip_threshold = max(down_flip_threshold, 0) + force_flip_down = True + # 若当前为DOWN趋势,出现明显拐头+hist减弱+未能下破EMA52,则加速看多 + if last_trend == Chan_PRICE_TREND.DOWN and macd_slope > 0 and signal_slope > 0 and weaken_steps >= 1 and no_down_break and macd_bias_up: + score += 3 + up_flip_threshold = min(up_flip_threshold, 0) + force_flip_up = True + # 多次对 EMA52 的拒绝配合 MACD 逆向:加速反向翻转(降低相反方向阈值) + if recent_up_rejects >= 2 and macd_bias_down: + # 从 UP 向 DOWN 的翻转更容易 + down_flip_threshold = max(down_flip_threshold, -1) + if recent_down_rejects >= 2 and macd_bias_up: + # 从 DOWN 向 UP 的翻转更容易 + up_flip_threshold = min(up_flip_threshold, 1) + if force_flip_down: + trend = Chan_PRICE_TREND.DOWN + elif force_flip_up: + trend = Chan_PRICE_TREND.UP + elif last_trend == Chan_PRICE_TREND.UP: + if score <= down_flip_threshold: + trend = Chan_PRICE_TREND.DOWN + else: + trend = Chan_PRICE_TREND.UP + elif last_trend == Chan_PRICE_TREND.DOWN: + if score >= up_flip_threshold: + trend = Chan_PRICE_TREND.UP + else: + trend = Chan_PRICE_TREND.DOWN + else: + # 初始无记忆时,降低进入门槛 + if score >= 1: + trend = Chan_PRICE_TREND.UP + elif score <= -1: + trend = Chan_PRICE_TREND.DOWN + else: + trend = Chan_PRICE_TREND.FLAT + except Exception: + trend = Chan_PRICE_TREND.UNKNOWN + # 写回趋势 + if klc.end_time is None: + trend = Chan_PRICE_TREND.FLAT + if hasattr(klc, 'set_trend'): + klc.set_trend(trend) + else: + setattr(klc, 'trend', trend) + last_trend = trend + # 更新滑窗:仅向后看 + prev_klcs.append(klc) + price_diff = klc.close - klc.pre.close if klc.pre else 0 + #if klc.index > len(klc_list) - 10: + #print(klc.start_time, klc.end_time, klc.close, klc.ema24, klc.ema52, klc.macd, klc.signal, klc.macdhist, klc.trend, price_diff, score) + #print(klc.start_time, klc.end_time, klc.trend, price_diff, score) + return klc_list + def cal_kl_data(self, dataframe:DataFrame): + fields = "time,open,high,low,close,volume" + klu_list = [] + last_klu = None + 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) + # print(klu.time, klu.open, klu.high, klu.low, klu.close, klu.volume) + klu.set_idx(i) + klu_list.append(klu) + if last_klu: + last_klu.set_next(klu) + klu.set_pre(last_klu) + last_klu = klu + if 'macd' in item: + klu.set_indicators(item) + return klu_list + def get_bi_list(self, dataframe): + bi_list = self.cal_bi_list(self.get_klc_list(dataframe)) + #bi_list = self.cal_bi_list_chanlun(self.get_klc_list(dataframe)) + return bi_list + def get_kl_data(self, dataframe:DataFrame): + return self.cal_kl_data(dataframe) + def get_klc_list(self, klu_list): + klc_list = [] + last_klu = None + macd = ChanMACD(klu_list) + klu_list = macd.cal_macd_state() + ema_up_list = [] + ema_down_list = [] + ema_up_count = 0 + ema_down_count = 0 + last_klu = None + for klu in klu_list: + ema = klu.ema52 + last_ema = last_klu.ema52 if last_klu else 0 + if klu.close >= ema: + ema_up_count += 1 + elif klu.close < ema: + ema_down_count += 1 + if last_klu and last_klu.close >= last_ema and klu.close < ema: + ema_up_list.append(ema_up_count) + #print(last_klu.time, ema_up_count, "UP END") + ema_up_count = 0 + elif last_klu and last_klu.close < last_ema and klu.close >= ema: + ema_down_list.append(ema_down_count) + #print(last_klu.time, ema_down_count, "DOWN END") + ema_down_count = 0 + if len(klc_list) > 0: + last_klc = klc_list[-1] + if klu.exception: + 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.high = klu.close if klu.close > klu.open else klu.open + klc.low = klu.open if klu.close > klu.open else klu.close + 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() + #print(klu.time, klu.high, klu.low, klu.close, klu.open, klu.exception) + else: + 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 + klc_list = self.cal_trend(klc_list) + #print(ema52_up_list, ema52_down_list) + return klc_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, bi) + 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) + last_seg.add_bi(bi) + #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, bi) + 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 + last_seg.add_bi(bi) + 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") + last_seg.add_bi(bi) + + else: + last_down_sbi = ChanSBI(bi, len(down_sbi_list), bi.dir) + down_sbi_list.append(last_down_sbi) + last_seg.add_bi(bi) + #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.add_bi(bi) + + # 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, bi) + 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) + last_seg.add_bi(bi) + #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, bi) + #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 + last_seg.add_bi(bi) + 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 + last_seg.add_bi(bi) + #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) + last_seg.add_bi(bi) + #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 + last_seg.add_bi(bi) + #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, bi) + 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, bi) + 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) + """ + if len(seg_list) > 1: + seg = seg_list[-1] + last_seg = seg_list[-2] + last_seg_bi = last_seg.bi_list[-3] + bi_index = seg.start_bi.index + for i in range(bi_index, len(bi_list) - 1): + # last seg is down + if seg.dir == Chan_SEG_DIR.UP: + if bi_list[i].dir == Chan_BI_DIR.UP: + last_seg_peak = last_seg_bi.high + if bi_list[i].high > last_seg_peak: + # The confirmed + print("Last UP seg is broken, create a new seg. 1") + seg.pre_set_end_bi(bi_list[i]) + seg = ChanSEG(bi_list[i+1], len(seg_list), Chan_SEG_DIR.DOWN, bi) + seg_list.append(seg) + last_seg = seg_list[-2] + if len(last_seg.bi_list) > 3: + last_seg_bi = last_seg.bi_list[-3] + + else: + if bi_list[i].dir == Chan_BI_DIR.DOWN: + last_seg_peak = last_seg_bi.low + if bi_list[i].low < last_seg_peak: + print("Last DOWN seg is broken, create a new seg. 1") + seg.pre_set_end_bi(bi_list[i]) + seg = ChanSEG(bi_list[i+1], len(seg_list), Chan_SEG_DIR.UP, bi) + seg_list.append(seg) + last_seg = seg_list[-2] + if len(last_seg.bi_list) > 3: + last_seg_bi = last_seg.bi_list[-3] + else: + if len(seg_list) == 1: + last_seg = seg_list[-1] + bi_index = last_seg.bi_list[0].index + for i in range(bi_index, len(bi_list) - 1): + if i > bi_index + 2: + last_seg_peak = bi_list[i-2].high + # last seg is down + if last_seg.dir == Chan_SEG_DIR.DOWN: + if bi_list[i].dir == Chan_BI_DIR.UP: + if bi_list[i].high > last_seg_peak: + print("Last seg is broken, create a new seg. 2") + last_seg.pre_set_end_bi(bi_list[i-1]) + seg = ChanSEG(bi_list[i], len(seg_list), Chan_SEG_DIR.UP, bi) + seg_list.append(seg) + last_seg = seg + last_seg_bi = bi_list[i] + break + """ + #self.cal_bi_zs(seg_list) + return seg_list + def get_zs_state(self, df): + bi_list = self.cal_bi_list(self.get_klc_list(self.get_kl_data(df))) + seg_list = self.get_seg_list(bi_list) + zs_list = self.calculate_zs(seg_list) + for zs in zs_list: + last_zs = zs + return zs_list + def cal_bi_list(self, klc_list): + bi_list = [] + last_top = None + last_bottom = None + bi_klc_min = 4 + last_fx_klc = None + for klc in klc_list: + if last_fx_klc: + klc.check_klc_state(last_fx_klc) + klc.check_fx_confirmed(last_top, last_bottom) + fx = self.check_fx(klc) + if fx == Chan_FX_TYPE.TOP: + if last_bottom: + if self.check_top_fx(last_bottom, klc) == False: + fx = Chan_FX_TYPE.UNKNOWN + if fx == Chan_FX_TYPE.BOTTOM: + if last_top: + if self.check_bottom_fx(last_top, klc) == False: + #print(klc.end_time, last_top.end_time, "---") + fx = Chan_FX_TYPE.UNKNOWN + # Do nothing + if fx == Chan_FX_TYPE.UNKNOWN: + if len(bi_list) > 0: + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + #continue + if len(bi_list) > 0 and klc.end_klu: + last_bi = bi_list[-1] + #print(klc.start_time, last_bi.start_time, last_bi.end_time, last_bi.dir, last_bi.high, last_bi.low, last_bottom.end_time, "last bi") + if last_top and last_bi.dir == Chan_BI_DIR.DOWN: + if last_bottom and klc.high > last_bi.high: + #print(klc.end_time, "Top 7, 1", last_bi.start_time, klc.high, last_bi.high) + #klc.klc_fx_type = Chan_KLC_FX.TOP7 + #klc.fx = Chan_FX_TYPE.TOP + """ + last_bi.set_end_klc(last_bottom, klc) + bi = ChanBI(last_bottom, len(bi_list), Chan_BI_DIR.UP) + #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM7) + #klc.bb_out = True + last_bi.set_next(bi) + bi.set_pre(last_bi) + for klc_index in range(last_bi.end_klc.index, len(klc_list)): + bi.add_klc(klc_list[klc_index]) + bi_list.append(bi) + last_top = klc + klc.set_bi(bi) + #print(klc.start_time, bi.start_time, bi.end_time, bi.dir, bi.high, bi.low, bi.is_sure) + """ + else: + if last_bottom and last_bi.dir == Chan_BI_DIR.UP: + if last_top and klc.low < last_bi.low: + #print(klc.end_time, "Bottom 8, 2", last_bi.start_time) + #klc.klc_fx_type = Chan_KLC_FX.BOTTOM8 + #klc.fx = Chan_FX_TYPE.BOTTOM + """ + last_bi.set_end_klc(last_top, klc) + bi = ChanBI(last_top, len(bi_list), Chan_BI_DIR.DOWN) + #klc.set_klc_fx_type(Chan_KLC_FX.TOP6) + #klc.bb_out = True + last_bi.set_next(bi) + bi.set_pre(last_bi) + for klc_index in range(last_bi.end_klc.index, len(klc_list)): + bi.add_klc(klc_list[klc_index]) + bi_list.append(bi) + last_bottom = klc + klc.set_bi(bi) + #print(klc.start_time, bi.start_time, bi.end_time, bi.dir, bi.high, bi.low, bi.is_sure) + """ + else: + last_fx_klc = klc + if fx == Chan_FX_TYPE.TOP: + #print(klc.end_time, fx, klc.pre.high, klc.high, klc.pre.start_time, klc.pre.end_time) + 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: + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + #klc.set_klc_fx_type(Chan_KLC_FX.TOP3) + #print(klc.end_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, bi_list[-1].dir, "Last Top Change 1") + klc.set_klc_fx_type(Chan_KLC_FX.TOP1) + self.check_fx_pattern(klc) + #print(klc.end_time, klc.fx, "一类卖点Sell 1") + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + # 不满足结合律的分型 + else: + #klc.set_klc_fx_type(Chan_KLC_FX.TOP0) + #print(klc.end_time, klc.klc_fx_type) + if last_bottom.index + bi_klc_min > klc.index: + if last_top.high > klc.high: + #print(klc.start_time, klc.fx, "二类卖点Sell 1") + #klc.set_klc_fx_type(Chan_KLC_FX.TOP8) + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + # New TOP Found前面的UKNOWN可能出现TOP7,但是这里的也可能出现TOP8分型 + else: + # 顶分型在出现2之前超过前一个笔的顶 TOP8 + if last_top.index + bi_klc_min < 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 and False: + pre_last_bi.update_bi(klc) + bi_list.remove(last_bi) + pre_last_bi.set_next(None) + #last_top.set_fx(Chan_FX_TYPE.PTOP) + last_top = klc + last_bottom = pre_last_bi.start_klc + #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Bottom Change 1") + klc.set_klc_fx_type(Chan_KLC_FX.TOP2) + #print(klc.start_time, last_bi.start_klc.start_time, "New TOP Found reset last bi") + #klc.set_state("10") + #print(klc.start_time, klc.fx, "笔卖点Sell 1") + ###klc.set_klc_fx_type(Chan_KLC_FX.TOP2) # when bi is down but the fx is top + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + #klc.set_klc_fx_type(Chan_KLC_FX.TOP8) + #print(klc.start_time, last_bi.start_klc.start_time, "New TOP Found reset last bi") + else: + #klc.set_fx(Chan_FX_TYPE.PTOP) + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + #print(klc.end_time, klc.fx, "无效顶分型") + # 满足结合律 + 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 + #print(klc.end_time, klc.fx, bi_list[-1].dir, "Last Top Change 2") + klc.set_klc_fx_type(Chan_KLC_FX.TOP2) + self.check_fx_pattern(klc) + #bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + #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 = klc + #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 3") + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + # 初始化的时候用,其他时间不用 + else: + #klc.set_fx(Chan_FX_TYPE.TT) + #print(klc.start_time, klc.fx, "二类卖点Sell 2") + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + # last_top == None 初始化的时候用,其他时间不用 + else: + if last_bottom: + # 不满足结合律的分型 + if last_bottom.index + bi_klc_min > klc.index: + #klc.set_fx(Chan_FX_TYPE.PTOP) + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + #print(klc.start_time, klc.fx, "中枢卖点Sell 1") + else: + # First temp top and last bottom confirmed + last_top = klc + #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 4") + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + # 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) + bi.add_klc(klc) + klc.set_bi(bi_list[-1]) + #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 5") + #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: + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM3) + #print(last_bottom.start_time, last_bottom.end_time, "--------------------------------1") + #print(klc.end_time, klc.fx, "二类买点Buy 1") + else: + # A new bottom found + last_bottom = klc + #print(klc.end_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 1") + klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM1) + self.check_fx_pattern(klc) + #print(klc.end_time, klc.fx, "一类买点Buy 1") + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + # 不满足结合律的分型 + else: + #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM0) + #print(klc.end_time, klc.klc_fx_type) + if last_top.index + bi_klc_min > klc.index: + if last_bottom.low < klc.low: + #print(klc.end_time, klc.fx, "中枢买点Buy 1") + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM8) + # Found new bottom没有意义,上面UNKNOWN的时候已经是笔破坏了 + else: + #print(klc.end_time, last_bottom.end_time, "Found a new bottom") + if last_bottom.index + bi_klc_min < 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 and False: + pre_last_bi.update_bi(klc) + bi_list.remove(last_bi) + pre_last_bi.set_next(None) + last_bottom = klc + last_top = pre_last_bi.start_klc + #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Bottom Change 2") + klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM2) + #print(klc.start_time, last_bi.start_klc.start_time, "New BOTTOM Found reset last bi") + #print(klc.start_time, klc.fx, "笔买点Buy 1") + ###klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM2) # when bi is up but the fx is bottom + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM8) + else: + #klc.set_fx(Chan_FX_TYPE.UNKNOWN) + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + print(klc.end_time, klc.fx, "无效底分型") + # 满足结合律的分型 + 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 + #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 2") + klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM2) + self.check_fx_pattern(klc) + #bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + #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 + #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 3") + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + #print(klc.start_time, klc.fx, "笔买点Buy 3") + else: + #klc.set_fx(Chan_FX_TYPE.BB) + #klc.set_state('-20') + #print(klc.start_time, klc.fx, "二类买点Buy 2") + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + # last_bottom = None 初始化的时候用,其他时间不用 + else: + if last_top: + # 不满足结合律的分型 + if last_top.index + bi_klc_min > klc.index: + #klc.set_fx(Chan_FX_TYPE.PBOTTOM) + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + #print(klc.start_time, klc.fx, "中枢买点Buy 1") + else: + # First temp bottom and last top confirmed + last_bottom = klc + #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 4") + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + #print(klc.start_time, klc.fx, "一类买点Buy 1") + # 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) + #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM7) + bi_list.append(bi) + bi_list[-1].add_klc(klc) + klc.set_bi(bi_list[-1]) + #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 5") + #print(klc.start_time, klc.fx, "笔买点Buy 4") + self.get_above_zero_bsp(klc_list) + #print(bi_list[-1].start_time, bi_list[-1].end_time, len(bi_list[-1].klc_list)) + return bi_list + def get_above_zero_bsp(self, klc_list): + buy_bsp_list = [] + sell_bsp_list = [] + above_zero = False + buy_bsp = None + sell_bsp = None + for klc in klc_list: + if klc.pre and klc.pre.signal < 0 and klc.signal > 0: + above_zero = True + if klc.pre and klc.pre.signal > 0 and klc.signal < 0: + above_zero = False + if above_zero and klc.klc_fx_type == Chan_KLC_FX.BOTTOM2 and klc.macd > 0: + buy_bsp = klc + buy_bsp_list.append(klc) + #print(klc.end_time, "MACD 0轴上穿,回调笔底分型做多") + if buy_bsp and klc.pre and klc.pre.macdhist > 0 and klc.macdhist < 0: + sell_bsp = klc + sell_bsp_list.append(klc) + buy_bsp = None + #print(klc.end_time, "Sell BSP Found") + return buy_bsp_list + def check_top_fx(self, last_bottom, klc): + if (last_bottom.high > klc.pre.low or last_bottom.high > klc.next.low) and (klc.index - last_bottom.index < 100): + return False + return True + + def check_bottom_fx(self, last_top, klc): + if (last_top.low < klc.pre.high or last_top.low < klc.next.high) and (klc.index - last_top.index < 100): + return False + return True + # 线段内的中枢 + def cal_bi_zs(self, seg_list): + bi_zs_list = [] + for seg in seg_list: + zs_list = seg.cal_bi_zs() + if len(zs_list) > 0: + bi_zs_list = list(bi_zs_list) + list(zs_list) + return bi_zs_list + # 跨段不相连的中枢 + def cal_bi_zs_list(self, bi_list): + """ + 根据缠论笔中枢定义计算中枢(参照 get_zs_list 线段中枢判断规则) + 从第4根笔开始(索引3),每3根笔为一组检查 + 上涨中枢:后中枢 zd > 前中枢 zg(不重叠上移) + 下跌中枢:后中枢 zg < 前中枢 zd(不重叠下移) + 中枢可按两笔一组继续扩展到5根、7根... + """ + bi_zs_list = [] + if len(bi_list) < 3: + return bi_zs_list + + last_zs = None + start_idx = 3 + + while start_idx < len(bi_list): + if start_idx + 2 >= len(bi_list): + break + + bi1 = bi_list[start_idx] + bi2 = bi_list[start_idx + 1] + bi3 = bi_list[start_idx + 2] + + if not (bi1.is_sure and bi2.is_sure and bi3.is_sure): + start_idx += 1 + continue + + zg = min(bi1.high, bi2.high, bi3.high) + zd = max(bi1.low, bi2.low, bi3.low) + + if zg <= zd: + start_idx += 1 + continue + + valid = False + if last_zs is None: + if bi1.dir == Chan_BI_DIR.DOWN: + zs_dir = Chan_ZS_DIR.UP + valid = (bi2.dir == Chan_BI_DIR.UP and bi3.dir == Chan_BI_DIR.DOWN) + else: + zs_dir = Chan_ZS_DIR.DOWN + valid = (bi2.dir == Chan_BI_DIR.DOWN and bi3.dir == Chan_BI_DIR.UP) + else: + is_up_zs = zg > last_zs.zg + is_down_zs = zd < last_zs.zd + + if is_up_zs: + zs_dir = Chan_ZS_DIR.UP + valid = (bi1.dir == Chan_BI_DIR.DOWN and bi2.dir == Chan_BI_DIR.UP and bi3.dir == Chan_BI_DIR.DOWN) + elif is_down_zs: + zs_dir = Chan_ZS_DIR.DOWN + valid = (bi1.dir == Chan_BI_DIR.UP and bi2.dir == Chan_BI_DIR.DOWN and bi3.dir == Chan_BI_DIR.UP) + + if not valid: + start_idx += 1 + continue + gg = max(bi1.high, bi2.high, bi3.high) + dd = min(bi1.low, bi2.low, bi3.low) + zs = ChanBIZS(bi1, len(bi_zs_list), zs_dir) + zs.set_zg(zg) + zs.set_zd(zd) + zs.set_gg(gg) + zs.set_dd(dd) + zs.is_sure = False + zs.bi_list = [bi1, bi2, bi3] + + added_after_leave = [] + leave_index = start_idx + 4 + while leave_index < len(bi_list): + b = bi_list[leave_index] + if not b.is_sure: + break + if b.high >= zs.zd and b.low <= zs.zg: + added_after_leave.append(b.pre) + added_after_leave.append(b) + else: + break + leave_index += 2 + + if added_after_leave: + bis_for_zs = list(zs.bi_list) + list(added_after_leave) + bi_highs = [bi.high for bi in bis_for_zs] + bi_lows = [bi.low for bi in bis_for_zs] + zs.set_gg(max(bi_highs)) + zs.set_dd(min(bi_lows)) + zs.bi_list = bis_for_zs + bi = bis_for_zs[-1] + if bi.is_sure: + zs.set_end_bi(bi, bi.sure_time) + + start_idx = start_idx + len(added_after_leave) + else: + zs.set_end_bi(bi3, bi3.sure_time) + + if last_zs: + last_zs.set_next(zs) + zs.set_pre(last_zs) + + bi_zs_list.append(zs) + last_zs = zs + + start_idx += 4 + + if last_zs: + last_zs.is_sure = bi_list[-1].is_sure + + if last_zs and not last_zs.is_sure: + if last_zs.bi_list and len(last_zs.bi_list) > 0: + last_bi_of_zs = last_zs.bi_list[-1] + last_bi_idx = -1 + for i, bi in enumerate(bi_list): + if bi == last_bi_of_zs: + last_bi_idx = i + break + + has_leave = False + if last_bi_idx >= 0 and last_bi_idx + 1 < len(bi_list): + for i in range(last_bi_idx + 1, len(bi_list)): + bi = bi_list[i] + if bi.is_sure: + leave = (bi.low > last_zs.zg and bi.high > last_zs.zg) or \ + (bi.high < last_zs.zd and bi.low < last_zs.zd) + if leave: + has_leave = True + break + + if has_leave: + if last_bi_of_zs.is_sure: + last_zs.set_end_bi(last_bi_of_zs, last_bi_of_zs.sure_time) + return bi_zs_list + def get_zs_list(self, bi_list, seg_list=None): + """兼容入口:笔中枢列表(seg_list 保留参数以兼容旧调用)。""" + return self.get_bi_zs_list(bi_list) + def get_bi_zs_list(self, bi_list): + """ + 根据缠论笔中枢定义计算中枢(完全参照 get_seg_zs_list 线段中枢判断规则) + 从第4根笔开始(索引3),每3根笔为一组检查 + 上涨中枢:后中枢 zd > 前中枢 zg(不重叠上移) + 下跌中枢:后中枢 zg < 前中枢 zd(不重叠下移) + 盘整/扩张:后中枢与前中枢整体区间有交集 → 合并扩展 + 中枢可按两笔一组继续扩展到5根、7根... + """ + bi_zs_list = [] + if len(bi_list) < 3: + return bi_zs_list + + last_zs = None + start_idx = 3 + + while start_idx < len(bi_list): + if start_idx + 2 >= len(bi_list): + break + + bi1 = bi_list[start_idx] + bi2 = bi_list[start_idx + 1] + bi3 = bi_list[start_idx + 2] + + if not (bi1.is_sure and bi2.is_sure and bi3.is_sure): + start_idx += 1 + continue + + zg = min(bi1.high, bi2.high, bi3.high) + zd = max(bi1.low, bi2.low, bi3.low) + + if zg <= zd: + start_idx += 1 + continue + + valid = False + if last_zs is None: + if bi1.dir == Chan_BI_DIR.DOWN: + zs_dir = Chan_ZS_DIR.UP + valid = (bi2.dir == Chan_BI_DIR.UP and bi3.dir == Chan_BI_DIR.DOWN) + else: + zs_dir = Chan_ZS_DIR.DOWN + valid = (bi2.dir == Chan_BI_DIR.DOWN and bi3.dir == Chan_BI_DIR.UP) + else: + is_up_zs = zd > last_zs.zg + is_down_zs = zg < last_zs.zd + + if is_up_zs: + zs_dir = Chan_ZS_DIR.UP + valid = (bi1.dir == Chan_BI_DIR.DOWN and bi2.dir == Chan_BI_DIR.UP and bi3.dir == Chan_BI_DIR.DOWN) + elif is_down_zs: + zs_dir = Chan_ZS_DIR.DOWN + valid = (bi1.dir == Chan_BI_DIR.UP and bi2.dir == Chan_BI_DIR.DOWN and bi3.dir == Chan_BI_DIR.UP) + + create_new_zs = False + if not valid: + # 如果新中枢和前一个中枢的中枢区间有重叠,不形成新中枢,合并扩展 + if last_zs is not None: + is_in_last_zs = (zd > last_zs.zd and zd < last_zs.zg) or \ + (zg < last_zs.zg and zg > last_zs.zd) or \ + (zg > last_zs.zg and zd < last_zs.zd) or \ + (zg < last_zs.zg and zd > last_zs.zd) + if is_in_last_zs: + # 扩展当前中枢:将 bi1-bi3 加入 last_zs + for bi in [bi1, bi2, bi3]: + if bi not in last_zs.bi_list: + last_zs.add_bi(bi) + create_new_zs = False + else: + start_idx += 1 + continue + else: + start_idx += 1 + continue + else: + create_new_zs = True + + # 新中枢形成时确认前一个中枢 + if last_zs and create_new_zs: + last_bi = last_zs.bi_list[-1] + if last_bi and last_bi.is_sure: + last_zs.is_sure = True + last_zs.set_end_bi(last_bi, last_bi.sure_time) + + zs = last_zs + if create_new_zs: + gg = max(bi1.high, bi2.high, bi3.high) + dd = min(bi1.low, bi2.low, bi3.low) + zs = ChanBIZS(bi1, len(bi_zs_list), zs_dir) + zs.set_zg(zg) + zs.set_zd(zd) + zs.set_gg(gg) + zs.set_dd(dd) + zs.is_sure = False + zs.bi_list = [bi1, bi2, bi3] + + # 离开后回抽扩展检查 + added_after_leave = [] + leave_index = start_idx + 4 + while leave_index < len(bi_list): + b = bi_list[leave_index] + if not b.is_sure: + break + if b.high >= zs.zd and b.low <= zs.zg: + added_after_leave.append(b.pre) + added_after_leave.append(b) + else: + break + leave_index += 2 + + if added_after_leave: + bis_for_zs = list(zs.bi_list) + list(added_after_leave) + bi_highs = [bi.high for bi in bis_for_zs] + bi_lows = [bi.low for bi in bis_for_zs] + zs.set_gg(max(bi_highs)) + zs.set_dd(min(bi_lows)) + zs.bi_list = bis_for_zs + bi = bis_for_zs[-1] + if bi.is_sure: + zs.set_end_bi(bi, bi.sure_time) + start_idx = start_idx + len(added_after_leave) + else: + if create_new_zs: + zs.set_end_bi(bi3, bi3.sure_time) + + if create_new_zs: + if last_zs: + last_zs.set_next(zs) + zs.set_pre(last_zs) + bi_zs_list.append(zs) + last_zs = zs + + start_idx += 4 + + # 最后一个中枢:根据 bi_list 最后一笔确认状态 + if last_zs: + last_zs.is_sure = bi_list[-1].is_sure + + if last_zs and not last_zs.is_sure: + if last_zs.bi_list and len(last_zs.bi_list) > 0: + last_bi_of_zs = last_zs.bi_list[-1] + last_bi_idx = -1 + for i, bi in enumerate(bi_list): + if bi == last_bi_of_zs: + last_bi_idx = i + break + + has_leave = False + if last_bi_idx >= 0 and last_bi_idx + 1 < len(bi_list): + for i in range(last_bi_idx + 1, len(bi_list)): + bi = bi_list[i] + if bi.is_sure: + leave = (bi.low > last_zs.zg and bi.high > last_zs.zg) or \ + (bi.high < last_zs.zd and bi.low < last_zs.zd) + if leave: + has_leave = True + break + + if has_leave: + if last_bi_of_zs.is_sure: + last_zs.set_end_bi(last_bi_of_zs, last_bi_of_zs.sure_time) + + return bi_zs_list + + def cal_bi_zs_list_pure(self, bi_list): + bi_zs_list = [] + if len(bi_list) < 3: + return bi_zs_list + + def get_zs_range(bis): + zg = min(bi.high for bi in bis) + zd = max(bi.low for bi in bis) + return zg, zd + + def is_bi_overlap_range(bi, zg, zd): + return bi.high >= zd and bi.low <= zg + + def check_zs_position_filter(last_zs, zg, zd, bis): + if last_zs is None: + return True + if zg <= last_zs.zd: + return bis[0].dir == Chan_BI_DIR.UP and bis[-1].dir == Chan_BI_DIR.UP + if zd >= last_zs.zg: + return bis[0].dir == Chan_BI_DIR.DOWN and bis[-1].dir == Chan_BI_DIR.DOWN + return True + + def set_zs_bi_list(zs, bis): + zs.bi_list = list(bis) + for bi in zs.bi_list: + bi.set_bi_zs(zs) + zs.set_gg(max(bi.high for bi in zs.bi_list)) + zs.set_dd(min(bi.low for bi in zs.bi_list)) + zs.classify_zs() + + last_zs = None + start_idx = 0 + while start_idx + 2 < len(bi_list): + bi1 = bi_list[start_idx] + bi2 = bi_list[start_idx + 1] + bi3 = bi_list[start_idx + 2] + + if not (bi1.is_sure and bi2.is_sure and bi3.is_sure): + start_idx += 1 + continue + + if not (bi1.dir != bi2.dir and bi1.dir == bi3.dir): + start_idx += 1 + continue + + zg, zd = get_zs_range([bi1, bi2, bi3]) + if zg <= zd: + start_idx += 1 + continue + + bis_for_zs = [bi1, bi2, bi3] + extend_idx = start_idx + 3 + while extend_idx + 1 < len(bi_list): + leave_bi = bi_list[extend_idx] + back_bi = bi_list[extend_idx + 1] + if not (leave_bi.is_sure and back_bi.is_sure): + break + if not is_bi_overlap_range(back_bi, zg, zd): + break + bis_for_zs.append(leave_bi) + bis_for_zs.append(back_bi) + extend_idx += 2 + + if not check_zs_position_filter(last_zs, zg, zd, bis_for_zs): + start_idx += 1 + continue + + zs_dir = Chan_ZS_DIR.UP if bi1.dir == Chan_BI_DIR.DOWN else Chan_ZS_DIR.DOWN + zs = ChanBIZS(bi1, len(bi_zs_list), zs_dir) + zs.set_zg(zg) + zs.set_zd(zd) + + set_zs_bi_list(zs, bis_for_zs) + zs.set_end_bi(bis_for_zs[-1], bis_for_zs[-1].sure_time) + + if last_zs: + last_zs.set_next(zs) + zs.set_pre(last_zs) + + bi_zs_list.append(zs) + last_zs = zs + start_idx = start_idx + len(bis_for_zs) + + # 与 cal_bi_zs_list 一致:最后一笔未确认时末中枢标为未完成;若其后已出现确认的离开笔,仍按离开前最后一笔确认中枢结束 + if last_zs: + last_zs.is_sure = bi_list[-1].is_sure + + if last_zs and not last_zs.is_sure: + if last_zs.bi_list and len(last_zs.bi_list) > 0: + last_bi_of_zs = last_zs.bi_list[-1] + last_bi_idx = -1 + for i, bi in enumerate(bi_list): + if bi == last_bi_of_zs: + last_bi_idx = i + break + + has_leave = False + if last_bi_idx >= 0 and last_bi_idx + 1 < len(bi_list): + for i in range(last_bi_idx + 1, len(bi_list)): + bi = bi_list[i] + if bi.is_sure: + leave = (bi.low > last_zs.zg and bi.high > last_zs.zg) or \ + (bi.high < last_zs.zd and bi.low < last_zs.zd) + if leave: + has_leave = True + break + + if has_leave: + if last_bi_of_zs.is_sure: + last_zs.set_end_bi(last_bi_of_zs, last_bi_of_zs.sure_time) + + return bi_zs_list + def find_all_bsp(self, bi_list, bi_zs_list): + """ + 笔中枢的三类买卖点识别 + + 三类买点:中枢形成后,一笔向上离开中枢(低点 > zg), + 随后回拉的一笔低点不跌回中枢(低点 >= zg),确认支撑有效。 + 三类卖点:中枢形成后,一笔向下离开中枢(高点 < zd), + 随后反弹的一笔高点不回到中枢(高点 <= zd),确认压力有效。 + + 参数: + bi_list: 笔列表 + bi_zs_list: 笔中枢列表(二维列表,每个seg内的中枢列表) + + 返回: + bsp_list: ChanBSP 列表,包含所有识别到的三类买卖点 + """ + bsp_list = [] + if len(bi_list) < 4 or len(bi_zs_list) == 0: + return bsp_list + + for zs in bi_zs_list: + if not zs.is_sure or len(zs.bi_list) < 3: + continue + #print(zs.start_time, zs.end_time, zs.dir, zs.is_sure, len(zs.bi_list)) + # 中枢结束后的第一笔(离开笔) + last_zs_bi = zs.bi_list[-1] + if last_zs_bi.dir == Chan_BI_DIR.UP: + if last_zs_bi.is_sure and last_zs_bi.end_klc.high <= zs.zg or (last_zs_bi.next and last_zs_bi.next.is_sure and last_zs_bi.next.end_klc.low < zs.zd): + leave_bi = last_zs_bi.next + else: + leave_bi = last_zs_bi + else: + if last_zs_bi.is_sure and last_zs_bi.end_klc.low >= zs.zd or (last_zs_bi.next and last_zs_bi.next.is_sure and last_zs_bi.next.end_klc.high > zs.zg): + leave_bi = last_zs_bi.next + else: + leave_bi = last_zs_bi + #print(zs.zg, zs.zd) + if leave_bi is None or not leave_bi.is_sure: + continue + if (zs.dir == Chan_ZS_DIR.UP and leave_bi.dir == Chan_BI_DIR.UP and leave_bi.end_klc.high < zs.zg and leave_bi.end_klc.high > zs.zd) or (zs.dir == Chan_ZS_DIR.DOWN and leave_bi.dir == Chan_BI_DIR.DOWN and leave_bi.end_klc.low < zs.zg and leave_bi.end_klc.low > zs.zd): + #print("--------------------", leave_bi.dir, leave_bi.end_klc.high, leave_bi.end_klc.low, zs.zg, zs.zd) + leave_bi = leave_bi.next + # 三类买点:向上离开中枢后回拉不破 zg + #print("Leave bi:", leave_bi.start_time, leave_bi.end_time, leave_bi.dir, leave_bi.is_sure, leave_bi.low, leave_bi.high) + if leave_bi.dir == Chan_BI_DIR.UP: + first_bsp_bi_div = self.check_bi_div(zs, leave_bi) + # 确认一类卖点:离开断能量小于进入段能量 + if first_bsp_bi_div: + bsp = ChanBSP( + leave_bi, len(bsp_list), + Chan_BSP_TYPE.S1, + Chan_BSP_DIR.SELL, + leave_bi.sure_time, + zs.index+1, zs, None + ) + leave_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.S1) + bsp_list.append(bsp) + # 回拉笔 + pullback_bi = leave_bi.next + #print(pullback_bi.start_klc.start_time, pullback_bi.dir, pullback_bi.is_sure, pullback_bi.low, pullback_bi.high) + if pullback_bi and pullback_bi.is_sure and pullback_bi.dir == Chan_BI_DIR.DOWN: + if pullback_bi.low >= zs.zg: + # 确认三类买点:回拉笔的低点不跌回中枢 + bsp = ChanBSP( + pullback_bi, len(bsp_list), + Chan_BSP_TYPE.B3, + Chan_BSP_DIR.BUY, + pullback_bi.sure_time, + zs.index+1, zs, None + ) + pullback_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B3) + bsp_list.append(bsp) + # 二类卖点 + if first_bsp_bi_div: + second_bsp_bi = pullback_bi.next + if second_bsp_bi and second_bsp_bi.is_sure and second_bsp_bi.end_klc.high < leave_bi.end_klc.high: + # 确认二类卖点:一类卖点后回拉不超过一类卖点高点 + bsp = ChanBSP( + second_bsp_bi, len(bsp_list), + Chan_BSP_TYPE.S2, + Chan_BSP_DIR.SELL, + second_bsp_bi.sure_time, + zs.index+1, zs, None + ) + second_bsp_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B2) + bsp_list.append(bsp) + # 三类卖点:向下离开中枢后反弹不破 zd + elif leave_bi.dir == Chan_BI_DIR.DOWN: + first_bsp_bi_div = self.check_bi_div(zs, leave_bi) + # 确认一类买点:离开段能量小于进入段 + if first_bsp_bi_div: + bsp = ChanBSP( + leave_bi, len(bsp_list), + Chan_BSP_TYPE.B1, + Chan_BSP_DIR.BUY, + leave_bi.sure_time, + zs.index+1, zs, None + ) + leave_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B1) + bsp_list.append(bsp) + # 反弹笔 + bounce_bi = leave_bi.next + #print(bounce_bi.start_klc.start_time, bounce_bi.dir, bounce_bi.is_sure, bounce_bi.low, bounce_bi.high) + if bounce_bi and bounce_bi.is_sure and bounce_bi.dir == Chan_BI_DIR.UP: + if bounce_bi.high <= zs.zd: + # 确认三类卖点:反弹笔的高点不回到中枢 + bsp = ChanBSP( + bounce_bi, len(bsp_list), + Chan_BSP_TYPE.S3, + Chan_BSP_DIR.SELL, + bounce_bi.sure_time, + zs.index+1, zs, None + ) + bounce_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.S3) + bsp_list.append(bsp) + # 二类卖点 + if first_bsp_bi_div: + second_bsp_bi = bounce_bi.next + if second_bsp_bi and second_bsp_bi.is_sure and second_bsp_bi.end_klc.low > leave_bi.end_klc.low: + # 确认二类买点:一类买点后回拉不超过一类卖点高点 + bsp = ChanBSP( + second_bsp_bi, len(bsp_list), + Chan_BSP_TYPE.B2, + Chan_BSP_DIR.BUY, + second_bsp_bi.sure_time, + zs.index+1, zs, None + ) + second_bsp_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B2) + bsp_list.append(bsp) + self.bsp_list = bsp_list + self.confirmed_bsp_list = confirm_bsp(bsp_list, self.indicator_store) + return bsp_list + def check_bi_div(self, zs, leave_bi): + return bsp_check_bi_div(zs, leave_bi, self.indicator_store) + def find_first_bsp(self, bi_list, bi_zs_list): + """ + 笔中枢的一类买卖点识别 + + 一类买点:下跌趋势中,最后一个中枢完成后,向下离开中枢的笔创新低, + 但该笔与进入中枢前的最后一笔下跌形成底背驰(力度减弱), + 即趋势力竭的转折点。 + 一类卖点:上涨趋势中,最后一个中枢完成后,向上离开中枢的笔创新高, + 但该笔与进入中枢前的最后一笔上涨形成顶背驰(力度减弱), + 即趋势力竭的转折点。 + + 简化判断:中枢形成后,离开中枢的笔(突破笔)本身即为一类买卖点的触发笔。 + + 参数: + bi_list: 笔列表 + bi_zs_list: 笔中枢列表(扁平列表,每个元素是一个中枢对象) + + 返回: + bsp_list: ChanBSP 列表,包含所有识别到的一类买卖点 + """ + bsp_list = [] + if len(bi_list) < 4 or len(bi_zs_list) == 0: + return bsp_list + + for zs in bi_zs_list: + if not zs.is_sure or len(zs.bi_list) < 3: + continue + + # 找到中枢的最后一笔 + last_zs_bi = zs.bi_list[-1] + + # 确定离开笔:中枢最后一笔之后的第一笔 + if last_zs_bi.dir == Chan_BI_DIR.UP: + # 中枢最后一笔向上,如果没有真正离开中枢,取下一笔 + if last_zs_bi.is_sure and last_zs_bi.end_klc.high <= zs.zg: + leave_bi = last_zs_bi.next + else: + leave_bi = last_zs_bi + else: + # 中枢最后一笔向下,如果没有真正离开中枢,取下一笔 + if last_zs_bi.is_sure and last_zs_bi.end_klc.low >= zs.zd: + leave_bi = last_zs_bi.next + else: + leave_bi = last_zs_bi + + if leave_bi is None or not leave_bi.is_sure: + continue + + # 一类买点:向下离开中枢(leave_bi向下,低点 < zd),趋势力竭 + if leave_bi.dir == Chan_BI_DIR.DOWN and leave_bi.low < zs.zd: + # 背驰判断:比较离开笔与中枢内最后一笔同向笔的MACD柱状累积面积 + # 缠论原文:两段同向走势的MACD柱状面积比较,面积缩小即为背驰 + compare_bi = None + for bi in reversed(zs.bi_list): + if bi.dir == Chan_BI_DIR.DOWN and bi is not leave_bi: + compare_bi = bi + break + + is_divergence = False + if compare_bi: + # 笔的macd_hist是该笔内所有KLU的macdhist累积面积 + leave_macd_area = abs(leave_bi.macd_hist) + compare_macd_area = abs(compare_bi.macd_hist) + + # 价格创新低但MACD面积缩小 = 底背驰 + if leave_bi.low <= compare_bi.low and leave_macd_area < compare_macd_area: + is_divergence = True + # 即使没创新低,MACD面积明显缩小也算背驰 + elif leave_macd_area < compare_macd_area * 0.5: + is_divergence = True + else: + # 没有对比笔时,只要离开中枢就算一类买点 + is_divergence = True + + if is_divergence: + bsp = ChanBSP( + leave_bi, len(bsp_list), + Chan_BSP_TYPE.T1, + Chan_BSP_DIR.BUY, + leave_bi.sure_time, + 1, zs, None + ) + bsp_list.append(bsp) + + # 一类卖点:向上离开中枢(leave_bi向上,高点 > zg),趋势力竭 + elif leave_bi.dir == Chan_BI_DIR.UP and leave_bi.high > zs.zg: + # 背驰判断:比较离开笔与中枢内最后一笔同向笔的MACD柱状累积面积 + compare_bi = None + for bi in reversed(zs.bi_list): + if bi.dir == Chan_BI_DIR.UP and bi is not leave_bi: + compare_bi = bi + break + + is_divergence = False + if compare_bi: + leave_macd_area = abs(leave_bi.macd_hist) + compare_macd_area = abs(compare_bi.macd_hist) + + # 价格创新高但MACD面积缩小 = 顶背驰 + if leave_bi.high >= compare_bi.high and leave_macd_area < compare_macd_area: + is_divergence = True + # 即使没创新高,MACD面积明显缩小也算背驰 + elif leave_macd_area < compare_macd_area * 0.5: + is_divergence = True + else: + is_divergence = True + + if is_divergence: + bsp = ChanBSP( + leave_bi, len(bsp_list), + Chan_BSP_TYPE.T1, + Chan_BSP_DIR.SELL, + leave_bi.sure_time, + 1, zs, None + ) + bsp_list.append(bsp) + + return bsp_list + + def find_second_bsp(self, bi_list, first_bsp_list): + """ + 笔中枢的二类买卖点识别 + + 二类买点:一类买点出现后,价格向上反弹一笔,再回落一笔, + 回落笔的低点不跌破一类买点的低点,确认底部成立。 + 二类卖点:一类卖点出现后,价格向下回落一笔,再反弹一笔, + 反弹笔的高点不超过一类卖点的高点,确认顶部成立。 + + 参数: + bi_list: 笔列表 + first_bsp_list: 一类买卖点列表(find_first_bsp 的返回值) + + 返回: + bsp_list: ChanBSP 列表,包含所有识别到的二类买卖点 + """ + bsp_list = [] + if not first_bsp_list or len(bi_list) < 4: + return bsp_list + + for first_bsp in first_bsp_list: + trigger_bi = first_bsp.bi # 一类买卖点的触发笔 + + if first_bsp.dir == Chan_BSP_DIR.BUY: + # 一买之后:trigger_bi 向下 -> 反弹笔(向上) -> 回落笔(向下) + # 回落笔的低点 > trigger_bi 的低点 => 二类买点 + bounce_bi = trigger_bi.next # 反弹笔(向上) + if bounce_bi and bounce_bi.is_sure and bounce_bi.dir == Chan_BI_DIR.UP: + pullback_bi = bounce_bi.next # 回落笔(向下) + if pullback_bi and pullback_bi.is_sure and pullback_bi.dir == Chan_BI_DIR.DOWN: + if pullback_bi.low > trigger_bi.low: + bsp = ChanBSP( + pullback_bi, len(bsp_list), + Chan_BSP_TYPE.T2, + Chan_BSP_DIR.BUY, + pullback_bi.sure_time, + 1, first_bsp.zs, None + ) + bsp_list.append(bsp) + + elif first_bsp.dir == Chan_BSP_DIR.SELL: + # 一卖之后:trigger_bi 向上 -> 回落笔(向下) -> 反弹笔(向上) + # 反弹笔的高点 < trigger_bi 的高点 => 二类卖点 + drop_bi = trigger_bi.next # 回落笔(向下) + if drop_bi and drop_bi.is_sure and drop_bi.dir == Chan_BI_DIR.DOWN: + bounce_bi = drop_bi.next # 反弹笔(向上) + if bounce_bi and bounce_bi.is_sure and bounce_bi.dir == Chan_BI_DIR.UP: + if bounce_bi.high < trigger_bi.high: + bsp = ChanBSP( + bounce_bi, len(bsp_list), + Chan_BSP_TYPE.T2, + Chan_BSP_DIR.SELL, + bounce_bi.sure_time, + 1, first_bsp.zs, None + ) + bsp_list.append(bsp) + + return bsp_list + def calculate_seg_zs(self, seg_list): + return self.get_seg_zs_list(seg_list) + def get_seg_zs_list(self, seg_list): + """ + 根据缠论线段中枢定义计算中枢 + 从第4根线段开始(索引3),每3根线段为一组检查 + 上涨中枢:后中枢 zd > 前中枢 zg(不重叠上移) + 下跌中枢:后中枢 zg < 前中枢 zd(不重叠下移) + 盘整/扩张:后中枢与前中枢整体区间(GG/DD)有交集 + 中枢可按两段一组继续扩展到5根、7根... + """ + zs_list = [] + if len(seg_list) < 3: + return zs_list + + last_zs = None + + # 从第4根线段开始(索引3),每3根为一组 + start_idx = 3 + + while start_idx < len(seg_list): + # 取连续3个线段 + if start_idx + 2 >= len(seg_list): + break + + seg1 = seg_list[start_idx] + seg2 = seg_list[start_idx + 1] + seg3 = seg_list[start_idx + 2] + + # 三个线段都必须是已确认的 + if not (seg1.is_sure and seg2.is_sure and seg3.is_sure): + start_idx += 1 + continue + + # 计算这3个线段的中枢区间 + zg = min(seg1.high, seg2.high, seg3.high) + zd = max(seg1.low, seg2.low, seg3.low) + + if zg <= zd: + start_idx += 1 + #print(seg1.start_bi.start_klc.end_time, "not valid", zg, zd) + continue + + # 判断中枢类型(按注释定义) + # 上涨中枢:后中枢 zd > 前中枢 zg(不重叠上移) + # 下跌中枢:后中枢 zg < 前中枢 zd(不重叠下移) + # 盘整/扩张:后中枢与前中枢区间有交集 + if last_zs is None: + # 第一个中枢仅按线段形态判定方向 + if seg1.dir == Chan_SEG_DIR.DOWN: + # 下跌+上涨+下跌,对应上涨中枢 + zs_dir = Chan_ZS_DIR.UP + valid = (seg2.dir == Chan_SEG_DIR.UP and seg3.dir == Chan_SEG_DIR.DOWN) + else: + # 上涨+下跌+上涨,对应下跌中枢 + zs_dir = Chan_ZS_DIR.DOWN + valid = (seg2.dir == Chan_SEG_DIR.DOWN and seg3.dir == Chan_SEG_DIR.UP) + else: + is_up_zs = zd > last_zs.zg + is_down_zs = zg < last_zs.zd + + if is_up_zs: + # 不重叠上移 + zs_dir = Chan_ZS_DIR.UP + valid = (seg1.dir == Chan_SEG_DIR.DOWN and seg2.dir == Chan_SEG_DIR.UP and seg3.dir == Chan_SEG_DIR.DOWN) + elif is_down_zs: + # 不重叠下移 + zs_dir = Chan_ZS_DIR.DOWN + valid = (seg1.dir == Chan_SEG_DIR.UP and seg2.dir == Chan_SEG_DIR.DOWN and seg3.dir == Chan_SEG_DIR.UP) + create_new_zs = False + # 验证是否有效 + if not valid: + # 如果新中枢和前一个中枢的中枢区间有重叠,不行成新中枢需要合并两个中枢 + is_in_last_zs = (zd > last_zs.zd and zd < last_zs.zg) or (zg < last_zs.zg and zg > last_zs.zd) or (zg > last_zs.zg and zd < last_zs.zd) or (zg < last_zs.zg and zd > last_zs.zd) + if is_in_last_zs: + #print(seg1.start_time, "New zs is in last zs, not valid") + last_zs.extend_zs(seg_list[last_zs.seg_list[-1].index:(seg3.index + 1)]) + create_new_zs = False + else: + start_idx += 1 + continue + else: + create_new_zs = True + if last_zs and create_new_zs: + last_seg = last_zs.seg_list[-1] + last_bi = last_seg.end_bi + if last_bi: + last_zs.is_sure = True + last_zs.set_end_klc(last_bi.end_klc, last_bi.sure_time, 0, last_seg) + last_zs.set_end_seg(last_seg) + zs = last_zs + if create_new_zs: + # 创建新中枢 + gg = max(seg1.high, seg2.high, seg3.high) + dd = min(seg1.low, seg2.low, seg3.low) + + zs = ChanZS(seg1, len(zs_list), zs_dir) + zs.set_zg(zg) + zs.set_zd(zd) + zs.set_gg(gg) + zs.set_dd(dd) + zs.is_sure = False + zs.seg_list = [seg1, seg2, seg3] + # 若第二线段与 [zd,zg] 重叠(如离开后回抽回到前中枢)则并入扩展 + added_after_leave = [] + leave_index = start_idx + 4 + is_break = False + while leave_index < len(seg_list): + s = seg_list[leave_index] + if not s.is_sure: + break + sh = max(s.start_bi.high, s.end_bi.high) if s.end_bi else s.start_bi.high + sl = min(s.start_bi.low, s.end_bi.low) if s.end_bi else s.start_bi.low + if sh >= zs.zd and sl <= zs.zg: + added_after_leave.append(s.pre) + added_after_leave.append(s) + leave_index += 2 + else: + next_seg = s.next + if next_seg and next_seg.is_sure: + if next_seg.dir == Chan_SEG_DIR.UP: + if next_seg.high <= zs.zg and next_seg.low >= zs.zd: + leave_index += 2 + continue + else: + is_break = True + else: + if next_seg.low >= zs.zd and next_seg.low <= zs.zg: + leave_index += 2 + continue + else: + is_break = True + else: + break + if is_break: + break + if added_after_leave: + #print(len(added_after_leave)) + segs_for_zs = list(zs.seg_list) + list(added_after_leave) + seg_highs = [s.high for s in segs_for_zs] + seg_lows = [s.low for s in segs_for_zs] + zs.set_gg(max(seg_highs)) + zs.set_dd(min(seg_lows)) + zs.seg_list = segs_for_zs + seg = segs_for_zs[-1] + #if seg.end_bi: + #zs.set_end_klc(seg.end_bi.end_klc, seg.sure_time, 0, seg) + #zs.set_end_seg(seg) + #zs.is_sure = True + start_idx = start_idx + len(added_after_leave) + if last_zs and last_zs.index != zs.index: + last_zs.set_next(zs) + zs.set_pre(last_zs) + + zs_list.append(zs) + last_zs = zs + + # 移动到下一组 + start_idx += 4 + if last_zs: + last_zs.is_sure = seg_list[-1].is_sure + """ + # 处理最后一个未确认的中枢 - 不自动扩展,保持未完成状态 + if last_zs and not last_zs.is_sure: + # 获取中枢最后一个线段的索引 + if last_zs.seg_list and len(last_zs.seg_list) > 0: + last_seg_of_zs = last_zs.seg_list[-1] + # 找到这个线段在seg_list中的索引 + last_seg_idx = -1 + for i, seg in enumerate(seg_list): + if seg == last_seg_of_zs: + last_seg_idx = i + break + + # 从中枢最后一个线段之后检查是否有离开 + has_leave = False + if last_seg_idx >= 0 and last_seg_idx + 1 < len(seg_list): + for i in range(last_seg_idx + 1, len(seg_list)): + seg = seg_list[i] + if seg.is_sure: + # 检查是否离开中枢 + leave = (seg.low > last_zs.zg and seg.high > last_zs.zg) or \ + (seg.high < last_zs.zd and seg.low < last_zs.zd) + if leave: + has_leave = True + break + + if not has_leave: + # 没有离开,保持未完成状态 + pass + else: + # 有离开,确认中枢 + if last_seg_of_zs.end_bi: + #print(last_seg_of_zs.start_time, "last_seg_of_zs.end_time", last_seg_of_zs.end_time) + last_zs.set_end_klc(last_seg_of_zs.end_bi.end_klc, last_seg_of_zs.sure_time, 0, last_seg_of_zs) + last_zs.set_end_seg(last_seg_of_zs) + last_zs.is_sure = True + """ + return zs_list + + def get_big_zs_list(self, zs_list): + """ + 中枢扩张:将区间重叠的连续中枢合并为大级别中枢,便于显示更大级别的震荡区间。 + 重叠定义:两中枢 [zd,zg] 有交集,即 (zs_i.zg >= zs_j.zd and zs_i.zd <= zs_j.zg)。 + """ + big_list = [] + if len(zs_list) < 2: + return big_list + i = 0 + while i < len(zs_list): + group = [zs_list[i]] + j = i + 1 + while j < len(zs_list): + cur = zs_list[j] + # 与当前组内任一中枢有重叠即算扩张(通常只需与组内最后一个比) + last_in_group = group[-1] + overlap = (last_in_group.zg >= cur.zd and last_in_group.zd <= cur.zg) + if overlap: + group.append(cur) + j += 1 + else: + break + if len(group) >= 2: + big = ChanZS_Big(group) + big.index = len(big_list) + big_list.append(big) + i = j if len(group) >= 2 else i + 1 + return big_list + + def get_klu_list(self, dataframe): + klu_list = self.get_kl_data(dataframe) + #klu_list = self.cal_klu_pattern(klu_list) + return klu_list + def cal_klu_pattern(self, klu_list): + """ + 计算裸K的pattern - 识别反转形态 + """ + if not klu_list or len(klu_list) < 3: + return klu_list + + for i, klu in enumerate(klu_list): + # 单根K线反转模式识别 + self._detect_single_reversal_pattern(klu) + + # 双根K线形态识别 + if i >= 1: + self._detect_double_pattern(klu_list[i-1], klu) + + # 三根K线形态识别 + if i >= 2: + self._detect_triple_pattern(klu_list[i-2], klu_list[i-1], klu) + + #if klu.pattern != Chan_KLU_PATTERN.UNKNOWN: + #print(klu.time, klu.pattern, klu.lower_shadow_ratio, klu.upper_shadow_ratio, klu.body_ratio, klu.lower_shadow_ratio/klu.body_ratio, klu.upper_shadow_ratio/klu.body_ratio) + return klu_list + + def _detect_single_reversal_pattern(self, klu): + """检测单根K线反转模式""" + body = abs(klu.close - klu.open) + upper_shadow = klu.high - max(klu.close, klu.open) + lower_shadow = min(klu.close, klu.open) - klu.low + total_range = klu.high - klu.low + + # 避免除零 + if total_range == 0: + return + + body_ratio = body / total_range + upper_ratio = upper_shadow / total_range + lower_ratio = lower_shadow / total_range + #print(klu.time, upper_ratio, lower_ratio, body_ratio, upper_ratio/body_ratio, lower_ratio/body_ratio) + # 避免body_ratio为0时的除零错误 + if body_ratio == 0: + return + # 锤子线/上吊线 - 反转信号 + if lower_ratio / body_ratio >= 2: + # 锤子线:底部反转,需要前面一段 + if klu.close > klu.open and klu.pre: + klu.set_pattern(Chan_KLU_PATTERN.HAMMER) # 底部反转 + # 上吊线:顶部反转,需要前一根是上涨趋势 + elif klu.close < klu.open and klu.pre: + klu.set_pattern(Chan_KLU_PATTERN.HANGING_MAN) # 顶部反转 + + # 倒锤子线/射击之星 - 反转信号 + elif upper_ratio / body_ratio >= 2: + # 倒锤子线:底部反转,需要前一根是下跌趋势 + if klu.close > klu.open and klu.pre: + klu.set_pattern(Chan_KLU_PATTERN.INVERTED_HAMMER) # 底部反转 + # 射击之星:顶部反转,需要前一根是上涨趋势 + elif klu.close < klu.open and klu.pre: + klu.set_pattern(Chan_KLU_PATTERN.SHOOTING_STAR) # 顶部反转 + + # 十字星 - 反转信号 + elif body_ratio <= 0.1: + if upper_ratio > 0.4 and lower_ratio > 0.4: + klu.set_pattern(Chan_KLU_PATTERN.LONG_LEGGED_DOJI) # 强烈反转信号 + elif upper_ratio > 0.4 and lower_ratio <= 0.1: + # 墓碑十字星:顶部反转,需要前一根是上涨趋势 + if klu.pre and klu.pre.close > klu.pre.open: + klu.set_pattern(Chan_KLU_PATTERN.GRAVESTONE_DOJI) # 顶部反转 + elif lower_ratio > 0.4 and upper_ratio <= 0.1: + # 蜻蜓十字星:底部反转,需要前一根是下跌趋势 + if klu.pre and klu.pre.close < klu.pre.open: + klu.set_pattern(Chan_KLU_PATTERN.DRAGONFLY_DOJI) # 底部反转 + else: + klu.set_pattern(Chan_KLU_PATTERN.DOJI) # 一般反转信号 + + def _detect_double_pattern(self, prev_klu, curr_klu): + """检测两根K线形成的形态 + 包括:吞没形态(看涨/看跌)、乌云盖顶、曙光初现 + """ + # 如果前一根K线已经有形态,不再识别双K线形态 + if prev_klu.pattern != Chan_KLU_PATTERN.UNKNOWN: + return + + # 计算K线实体 + prev_body = abs(prev_klu.close - prev_klu.open) + curr_body = abs(curr_klu.close - curr_klu.open) + + # 判断K线颜色(阴阳) + prev_bullish = prev_klu.close > prev_klu.open + curr_bullish = curr_klu.close > curr_klu.open + + # 检查是否存在长期趋势(至少需要5根K线的趋势) + def check_long_trend(klu, bullish_trend=True, min_bars=5): + """检查是否存在长期趋势 + bullish_trend=True: 检查上涨趋势 + bullish_trend=False: 检查下跌趋势 + min_bars: 最少需要多少根K线形成趋势 + """ + if not klu or not klu.pre: + return False + return True + + # 使用EMA指标判断长期趋势 + if klu.ema52 > 0: + if bullish_trend and klu.close < klu.ema52: + return False + if not bullish_trend and klu.close > klu.ema52: + return False + + # 检查连续的K线方向 + count = 0 + current = klu.pre + + while current and count < min_bars: + if not current.pre: + break + + if bullish_trend: + # 上涨趋势:当前收盘价高于前一根收盘价 + if current.close <= current.pre.close: + break + else: + # 下跌趋势:当前收盘价低于前一根收盘价 + if current.close >= current.pre.close: + break + + count += 1 + current = current.pre + + return count >= min_bars + + # 1. 看涨吞没形态:前阴后阳,后者完全吞没前者 + # 要求前面有明显的下跌趋势 + if not prev_bullish and curr_bullish and \ + abs(curr_klu.open - prev_klu.close) < 10 and \ + curr_klu.close > prev_klu.open and \ + check_long_trend(prev_klu, bullish_trend=False, min_bars=5): + curr_klu.set_pattern(Chan_KLU_PATTERN.BULLISH_ENGULFING) + return + + # 2. 看跌吞没形态:前阳后阴,后者完全吞没前者 + # 要求前面有明显的上涨趋势 + if prev_bullish and not curr_bullish and \ + abs(curr_klu.open - prev_klu.close) < 10 and \ + curr_klu.close < prev_klu.open and \ + check_long_trend(prev_klu, bullish_trend=True, min_bars=5): + curr_klu.set_pattern(Chan_KLU_PATTERN.BEARISH_ENGULFING) + return + + # 3. 乌云盖顶:前阳后阴,后者开盘价高于前者最高价,收盘价在前者实体中部以下 + # 要求前面有明显的上涨趋势 + if prev_bullish and not curr_bullish and \ + curr_klu.open > prev_klu.high and \ + curr_klu.close < (prev_klu.open + prev_klu.close) / 2 and \ + curr_klu.close > prev_klu.open and \ + check_long_trend(prev_klu, bullish_trend=True, min_bars=5): + curr_klu.set_pattern(Chan_KLU_PATTERN.DARK_CLOUD_COVER) + return + + # 4. 曙光初现:前阴后阳,后者开盘价低于前者最低价,收盘价在前者实体中部以上 + # 要求前面有明显的下跌趋势 + if not prev_bullish and curr_bullish and \ + curr_klu.open < prev_klu.low and \ + curr_klu.close > (prev_klu.open + prev_klu.close) / 2 and \ + curr_klu.close < prev_klu.open and \ + check_long_trend(prev_klu, bullish_trend=False, min_bars=5): + curr_klu.set_pattern(Chan_KLU_PATTERN.PIERCING_LINE) + return + + # 平顶和平底移至三根K线形态中判断 + + def _detect_triple_pattern(self, first_klu, second_klu, third_klu): + """检测三根K线形成的形态 + 包括:早晨之星、黄昏之星、平顶、平底 + """ + # 如果前两根K线已经有形态,不再识别三K线形态 + if first_klu.pattern != Chan_KLU_PATTERN.UNKNOWN or \ + second_klu.pattern != Chan_KLU_PATTERN.UNKNOWN: + return + + # 判断K线颜色(阴阳) + first_bullish = first_klu.close > first_klu.open + second_bullish = second_klu.close > second_klu.open + third_bullish = third_klu.close > third_klu.open + + # 计算实体大小 + first_body = abs(first_klu.close - first_klu.open) + second_body = abs(second_klu.close - second_klu.open) + third_body = abs(third_klu.close - third_klu.open) + + # 检查是否存在长期趋势(至少需要5根K线的趋势) + def check_long_trend(klu, bullish_trend=True, min_bars=5): + """检查是否存在长期趋势 + bullish_trend=True: 检查上涨趋势 + bullish_trend=False: 检查下跌趋势 + min_bars: 最少需要多少根K线形成趋势 + """ + if not klu or not klu.pre: + return False + + # 使用EMA指标判断长期趋势 + if klu.ema52 > 0: + if bullish_trend and klu.close < klu.ema52: + return False + if not bullish_trend and klu.close > klu.ema52: + return False + + # 检查连续的K线方向 + count = 0 + current = klu.pre + + while current and count < min_bars: + if not current.pre: + break + + if bullish_trend: + # 上涨趋势:当前收盘价高于前一根收盘价 + if current.close <= current.pre.close: + break + else: + # 下跌趋势:当前收盘价低于前一根收盘价 + if current.close >= current.pre.close: + break + + count += 1 + current = current.pre + + return count >= min_bars + + # 1. 早晨之星:第一根阴线,第二根十字星或小实体,第三根阳线 + # 要求前面有明显的下跌趋势 + if not first_bullish and third_bullish and \ + second_body < first_body * 0.3 and \ + third_body > first_body * 0.5 and \ + max(second_klu.open, second_klu.close) < first_klu.close and \ + min(second_klu.open, second_klu.close) < third_klu.open and \ + third_klu.close > (first_klu.open + first_klu.close) / 2 and \ + check_long_trend(first_klu, bullish_trend=False, min_bars=7): + third_klu.set_pattern(Chan_KLU_PATTERN.MORNING_STAR) + return + + # 2. 黄昏之星:第一根阳线,第二根十字星或小实体,第三根阴线 + # 要求前面有明显的上涨趋势 + if first_bullish and not third_bullish and \ + second_body < first_body * 0.3 and \ + third_body > first_body * 0.5 and \ + min(second_klu.open, second_klu.close) > first_klu.close and \ + max(second_klu.open, second_klu.close) > third_klu.open and \ + third_klu.close < (first_klu.open + first_klu.close) / 2 and \ + check_long_trend(first_klu, bullish_trend=True, min_bars=7): + third_klu.set_pattern(Chan_KLU_PATTERN.EVENING_STAR) + return + + # 3. 平顶:三根K线的最高点几乎相同(上升趋势中更有意义) + # 要求前面有明显的上涨趋势 + if (abs(first_klu.high - second_klu.high) / first_klu.high < 0.0002 and + abs(second_klu.high - third_klu.high) / second_klu.high < 0.0002 and + check_long_trend(first_klu, bullish_trend=True, min_bars=7)): + # 额外确认:价格接近阻力位或关键技术指标 + is_near_resistance = False + + # 检查是否接近EMA52阻力位 + if first_klu.ema52 > 0: + resistance_level = first_klu.ema52 + if abs(first_klu.high - resistance_level) / resistance_level < 0.01: + is_near_resistance = True + + # 检查是否有成交量确认(成交量减少表示上涨动能减弱) + volume_confirmation = False + if (first_klu.volume > 0 and second_klu.volume > 0 and third_klu.volume > 0 and + third_klu.volume < second_klu.volume and second_klu.volume < first_klu.volume): + volume_confirmation = True + + if is_near_resistance or volume_confirmation: + third_klu.set_pattern(Chan_KLU_PATTERN.TWEEZER_TOP) + return + + # 4. 平底:三根K线的最低点几乎相同(下降趋势中更有意义) + # 要求前面有明显的下跌趋势 + if (abs(first_klu.low - second_klu.low) / first_klu.low < 0.0002 and + abs(second_klu.low - third_klu.low) / second_klu.low < 0.0002 and + check_long_trend(first_klu, bullish_trend=False, min_bars=7)): + # 额外确认:价格接近支撑位或关键技术指标 + is_near_support = False + + # 检查是否接近EMA52支撑位 + if first_klu.ema52 > 0: + support_level = first_klu.ema52 + if abs(first_klu.low - support_level) / support_level < 0.01: + is_near_support = True + + # 检查是否有成交量确认(成交量减少表示下跌动能减弱) + volume_confirmation = False + if (first_klu.volume > 0 and second_klu.volume > 0 and third_klu.volume > 0 and + third_klu.volume < second_klu.volume and second_klu.volume < first_klu.volume): + volume_confirmation = True + + if is_near_support or volume_confirmation: + third_klu.set_pattern(Chan_KLU_PATTERN.TWEEZER_BOTTOM) + return + + def get_decimal(self, value): + return Decimal("{:.2f}".format(value)) \ No newline at end of file diff --git a/chan/pipeline/__init__.py b/chan/pipeline/__init__.py new file mode 100644 index 0000000..214c64b --- /dev/null +++ b/chan/pipeline/__init__.py @@ -0,0 +1,6 @@ +"""编排层:多周期入口与单周期流水线。""" + +from .ChanLun import ChanLun +from .TF_DF import TF_DF + +__all__ = ["ChanLun", "TF_DF"] diff --git a/fx_strength_config.py b/fx_strength_config.py index 41e9faa..49dbafd 100644 --- a/fx_strength_config.py +++ b/fx_strength_config.py @@ -1,153 +1,5 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -分型强度检测配置文件 -用于调整分型强度计算的各项参数和权重 -""" - -class FxStrengthConfig: - """分型强度检测配置类""" - - def __init__(self): - # ===== 权重配置 (总分100分) ===== - self.price_difference_weight = 40 # 价格差异强度权重 - self.breakthrough_weight = 20 # 突破历史点位权重 - self.volume_weight = 15 # 成交量确认权重 - self.rsi_divergence_weight = 15 # RSI背离权重 - self.macd_divergence_weight = 10 # MACD背离权重 - - # ===== 价格差异参数 ===== - self.price_diff_multiplier = 1000 # 价格差异放大倍数 - self.max_price_score = 20 # 价格差异最高得分 - - # ===== 突破检测参数 ===== - self.breakthrough_lookback = 10 # 回看K线数量 - self.breakthrough_multiplier = 500 # 突破幅度放大倍数 - self.max_breakthrough_score = 20 # 突破最高得分 - - # ===== 成交量参数 ===== - self.volume_lookback = 5 # 计算平均成交量的回看期数 - self.volume_multiplier = 10 # 成交量放大倍数 - self.max_volume_score = 15 # 成交量最高得分 - self.min_volume_ratio = 1.0 # 最小成交量比率 - - # ===== RSI背离参数 ===== - self.rsi_divergence_divisor = 2 # RSI背离除数 - self.max_rsi_score = 15 # RSI最高得分 - - # ===== MACD背离参数 ===== - self.macd_divergence_multiplier = 100 # MACD背离放大倍数 - self.max_macd_score = 10 # MACD最高得分 - - # ===== 强度等级阈值 ===== - self.extreme_threshold = 80 # 极强分型阈值 - self.strong_threshold = 60 # 强分型阈值 - self.medium_threshold = 40 # 中等分型阈值 - self.weak_threshold = 20 # 弱分型阈值 - - # ===== 其他参数 ===== - self.min_strength = 0 # 最小强度分数 - self.max_strength = 100 # 最大强度分数 - - def get_strength_level_name(self, strength): - """根据强度分数获取等级名称""" - if strength >= self.extreme_threshold: - return "极强" - elif strength >= self.strong_threshold: - return "强" - elif strength >= self.medium_threshold: - return "中等" - elif strength >= self.weak_threshold: - return "弱" - else: - return "极弱" - - def is_strong_fractal(self, strength, custom_threshold=None): - """判断是否为强分型""" - threshold = custom_threshold if custom_threshold is not None else self.strong_threshold - return strength >= threshold - - def validate_config(self): - """验证配置参数的合理性""" - total_weight = (self.price_difference_weight + - self.breakthrough_weight + - self.volume_weight + - self.rsi_divergence_weight + - self.macd_divergence_weight) - - if total_weight != 100: - print(f"警告: 权重总和为{total_weight},不等于100") - - if not (0 <= self.extreme_threshold <= 100): - print(f"警告: 极强阈值{self.extreme_threshold}不在合理范围内") - - if not (self.weak_threshold < self.medium_threshold < - self.strong_threshold < self.extreme_threshold): - print("警告: 强度阈值设置不合理") - - return True - - def print_config(self): - """打印当前配置""" - print("=== 分型强度检测配置 ===") - print(f"价格差异权重: {self.price_difference_weight}分") - print(f"突破点位权重: {self.breakthrough_weight}分") - print(f"成交量权重: {self.volume_weight}分") - print(f"RSI背离权重: {self.rsi_divergence_weight}分") - print(f"MACD背离权重: {self.macd_divergence_weight}分") - print() - print("=== 强度等级阈值 ===") - print(f"极强: >={self.extreme_threshold}分") - print(f"强: {self.strong_threshold}-{self.extreme_threshold-1}分") - print(f"中等: {self.medium_threshold}-{self.strong_threshold-1}分") - print(f"弱: {self.weak_threshold}-{self.medium_threshold-1}分") - print(f"极弱: <{self.weak_threshold}分") - - -# 默认配置实例 -DEFAULT_CONFIG = FxStrengthConfig() - -# 保守配置 (更严格的分型识别) -CONSERVATIVE_CONFIG = FxStrengthConfig() -CONSERVATIVE_CONFIG.price_difference_weight = 50 -CONSERVATIVE_CONFIG.breakthrough_weight = 25 -CONSERVATIVE_CONFIG.volume_weight = 15 -CONSERVATIVE_CONFIG.rsi_divergence_weight = 10 -CONSERVATIVE_CONFIG.macd_divergence_weight = 0 -CONSERVATIVE_CONFIG.strong_threshold = 70 -CONSERVATIVE_CONFIG.extreme_threshold = 85 - -# 激进配置 (更宽松的分型识别) -AGGRESSIVE_CONFIG = FxStrengthConfig() -AGGRESSIVE_CONFIG.price_difference_weight = 30 -AGGRESSIVE_CONFIG.breakthrough_weight = 15 -AGGRESSIVE_CONFIG.volume_weight = 20 -AGGRESSIVE_CONFIG.rsi_divergence_weight = 20 -AGGRESSIVE_CONFIG.macd_divergence_weight = 15 -AGGRESSIVE_CONFIG.strong_threshold = 50 -AGGRESSIVE_CONFIG.extreme_threshold = 70 - -# 技术指标重点配置 (重视技术指标背离) -TECHNICAL_CONFIG = FxStrengthConfig() -TECHNICAL_CONFIG.price_difference_weight = 25 -TECHNICAL_CONFIG.breakthrough_weight = 15 -TECHNICAL_CONFIG.volume_weight = 10 -TECHNICAL_CONFIG.rsi_divergence_weight = 25 -TECHNICAL_CONFIG.macd_divergence_weight = 25 - - -if __name__ == "__main__": - print("=== 分型强度配置演示 ===\n") - - configs = { - "默认配置": DEFAULT_CONFIG, - "保守配置": CONSERVATIVE_CONFIG, - "激进配置": AGGRESSIVE_CONFIG, - "技术指标配置": TECHNICAL_CONFIG - } - - for name, config in configs.items(): - print(f"=== {name} ===") - config.print_config() - config.validate_config() - print() \ No newline at end of file +"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。""" +import importlib +import sys +_impl = importlib.import_module("chan.analysis.fx_strength_config") +sys.modules[__name__] = _impl diff --git a/strategies/BTC_Perpetual_Futures.py b/strategies/BTC_Perpetual_Futures.py index b4dfbdc..71ab4cd 100644 --- a/strategies/BTC_Perpetual_Futures.py +++ b/strategies/BTC_Perpetual_Futures.py @@ -3,8 +3,8 @@ from freqtrade.strategy import IStrategy, stoploss_from_absolute import sys import os sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR +from chan.pipeline.ChanLun import ChanLun +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/ChanLun_BTC.py b/strategies/ChanLun_BTC.py index e799199..0a73cea 100644 --- a/strategies/ChanLun_BTC.py +++ b/strategies/ChanLun_BTC.py @@ -5,8 +5,8 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX +from chan.pipeline.ChanLun import ChanLun +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/ChanLun_BTC_15.py b/strategies/ChanLun_BTC_15.py index 83eba63..5eeb42f 100644 --- a/strategies/ChanLun_BTC_15.py +++ b/strategies/ChanLun_BTC_15.py @@ -5,7 +5,7 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta @@ -15,7 +15,7 @@ from freqtrade.persistence import Trade from typing import Optional import logging logger = logging.getLogger(__name__) -from TF_DF import TF_DF +from chan.pipeline.TF_DF import TF_DF ### Now you can use logger.info('asfd') to log # freqtrade plot-dataframe --strategy ChanLun_BTC_15 --datadir user_data/data/binance -c ./user_data/ChanLun_SOL_15.json --timerange=20250309- diff --git a/strategies/ChanLun_BTC_15_back.py b/strategies/ChanLun_BTC_15_back.py index 876e1a8..a2b4191 100644 --- a/strategies/ChanLun_BTC_15_back.py +++ b/strategies/ChanLun_BTC_15_back.py @@ -5,9 +5,9 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanLun_Classifier import ChanLunClassifier -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX +from chan.pipeline.ChanLun import ChanLun +from chan.analysis.ChanLun_Classifier import ChanLunClassifier +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/ChanLun_BTC_1m.py b/strategies/ChanLun_BTC_1m.py index 2848540..86f7848 100644 --- a/strategies/ChanLun_BTC_1m.py +++ b/strategies/ChanLun_BTC_1m.py @@ -5,8 +5,8 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX, Chan_BSP_TYPE +from chan.pipeline.ChanLun import ChanLun +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX, Chan_BSP_TYPE # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/ChanLun_BTC_1m_old.py b/strategies/ChanLun_BTC_1m_old.py index 7da8c53..3377b28 100644 --- a/strategies/ChanLun_BTC_1m_old.py +++ b/strategies/ChanLun_BTC_1m_old.py @@ -5,8 +5,8 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX, Chan_BSP_TYPE +from chan.pipeline.ChanLun import ChanLun +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX, Chan_BSP_TYPE # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/ChanLun_BTC_30.py b/strategies/ChanLun_BTC_30.py index 5a6efe6..8b24ff0 100644 --- a/strategies/ChanLun_BTC_30.py +++ b/strategies/ChanLun_BTC_30.py @@ -5,8 +5,8 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX +from chan.pipeline.ChanLun import ChanLun +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/ChanLun_BTC_5m.py b/strategies/ChanLun_BTC_5m.py index 027c670..8d4b6fc 100644 --- a/strategies/ChanLun_BTC_5m.py +++ b/strategies/ChanLun_BTC_5m.py @@ -5,8 +5,8 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX, Chan_BSP_TYPE +from chan.pipeline.ChanLun import ChanLun +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX, Chan_BSP_TYPE # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/ChanLun_BTC_60.py b/strategies/ChanLun_BTC_60.py index 4301bf0..d86c143 100644 --- a/strategies/ChanLun_BTC_60.py +++ b/strategies/ChanLun_BTC_60.py @@ -5,8 +5,8 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX +from chan.pipeline.ChanLun import ChanLun +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/ChanLun_BTC_K.py b/strategies/ChanLun_BTC_K.py index ba514ef..d98efed 100644 --- a/strategies/ChanLun_BTC_K.py +++ b/strategies/ChanLun_BTC_K.py @@ -5,10 +5,10 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanLun_Classifier import ChanLunClassifier -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX -from ChanPY import ChanPY +from chan.pipeline.ChanLun import ChanLun +from chan.analysis.ChanLun_Classifier import ChanLunClassifier +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX +from chan.analysis.ChanPY import ChanPY # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/ChanLun_EMA52.py b/strategies/ChanLun_EMA52.py index 6af06e7..bd4bb4a 100644 --- a/strategies/ChanLun_EMA52.py +++ b/strategies/ChanLun_EMA52.py @@ -5,8 +5,8 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX +from chan.pipeline.ChanLun import ChanLun +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/ChanLun_EMA_Align.py b/strategies/ChanLun_EMA_Align.py index 5e2cca1..313bb33 100644 --- a/strategies/ChanLun_EMA_Align.py +++ b/strategies/ChanLun_EMA_Align.py @@ -5,8 +5,8 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX +from chan.pipeline.ChanLun import ChanLun +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/ChanLun_ETH_60.py b/strategies/ChanLun_ETH_60.py index fa63b89..58e5136 100644 --- a/strategies/ChanLun_ETH_60.py +++ b/strategies/ChanLun_ETH_60.py @@ -5,10 +5,10 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanLun_Classifier import ChanLunClassifier -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX -from ChanPY import ChanPY +from chan.pipeline.ChanLun import ChanLun +from chan.analysis.ChanLun_Classifier import ChanLunClassifier +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX +from chan.analysis.ChanPY import ChanPY # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/ChanLun_MACD.py b/strategies/ChanLun_MACD.py index 70fe50b..c47a21f 100644 --- a/strategies/ChanLun_MACD.py +++ b/strategies/ChanLun_MACD.py @@ -5,10 +5,10 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanLun_Classifier import ChanLunClassifier -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX -from ChanPY import ChanPY +from chan.pipeline.ChanLun import ChanLun +from chan.analysis.ChanLun_Classifier import ChanLunClassifier +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX +from chan.analysis.ChanPY import ChanPY # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta @@ -112,8 +112,8 @@ class ChanLun_MACD(IStrategy): # ====== 结构:基于 ChanMACD 的 UnitTF 结束点(与零轴定义一致) ====== try: - from ChanKLU import ChanKLU - from ChanMACD import ChanMACD + from chan.core.ChanKLU import ChanKLU + from chan.analysis.ChanMACD import ChanMACD klu_list = [] prev = None for idx, row in dataframe.iterrows(): diff --git a/strategies/ChanLun_SOL.py b/strategies/ChanLun_SOL.py index 8823435..b0f0014 100644 --- a/strategies/ChanLun_SOL.py +++ b/strategies/ChanLun_SOL.py @@ -10,8 +10,8 @@ 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")) -sys.path.append(os.path.abspath("/Users/jack/Documents/GitHub/freqtrade/user_data/Chan")) -from ChanLun import ChanLun +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from chan.pipeline.ChanLun import ChanLun # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/ChanLun_SOL_1.py b/strategies/ChanLun_SOL_1.py index f42b21b..0946d62 100644 --- a/strategies/ChanLun_SOL_1.py +++ b/strategies/ChanLun_SOL_1.py @@ -10,8 +10,8 @@ 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")) -sys.path.append(os.path.abspath("/Users/jack/Documents/GitHub/freqtrade/user_data/Chan")) -from ChanLun import ChanLun +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from chan.pipeline.ChanLun import ChanLun # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/ChanLun_SOL_15.py b/strategies/ChanLun_SOL_15.py index 40a77f6..c46c7a8 100644 --- a/strategies/ChanLun_SOL_15.py +++ b/strategies/ChanLun_SOL_15.py @@ -4,9 +4,9 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanLun_Classifier import ChanLunClassifier -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX +from chan.pipeline.ChanLun import ChanLun +from chan.analysis.ChanLun_Classifier import ChanLunClassifier +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/ChanLun_SOL_5.py b/strategies/ChanLun_SOL_5.py index 4c152ec..713079e 100644 --- a/strategies/ChanLun_SOL_5.py +++ b/strategies/ChanLun_SOL_5.py @@ -5,9 +5,9 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanLun_Classifier import ChanLunClassifier -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX +from chan.pipeline.ChanLun import ChanLun +from chan.analysis.ChanLun_Classifier import ChanLunClassifier +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/ChanLun_SOL_Optimized.py b/strategies/ChanLun_SOL_Optimized.py index b347656..5c40f2b 100644 --- a/strategies/ChanLun_SOL_Optimized.py +++ b/strategies/ChanLun_SOL_Optimized.py @@ -12,9 +12,9 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanLun_Classifier import ChanLunClassifier -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX +from chan.pipeline.ChanLun import ChanLun +from chan.analysis.ChanLun_Classifier import ChanLunClassifier +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/Chan_SOL_60.py b/strategies/Chan_SOL_60.py index fd4bc14..809b992 100644 --- a/strategies/Chan_SOL_60.py +++ b/strategies/Chan_SOL_60.py @@ -9,14 +9,14 @@ 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")) +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) #sys.path.append(os.path.abspath("/Users/jack/Documents/GitHub/freqtrade/user_data/Chan")) -from ChanEnum import Chan_AUTYPE, Chan_DATA_FIELD, Chan_FX_TYPE, Chan_KLINE_DIR, Chan_KL_TYPE, Chan_BI_DIR -from ChanKLU import ChanKLU -from ChanCTime import ChanCTime -from ChanKLC import ChanKLC -from ChanBI import ChanBI +from chan.core.ChanEnum import Chan_AUTYPE, Chan_DATA_FIELD, Chan_FX_TYPE, Chan_KLINE_DIR, Chan_KL_TYPE, Chan_BI_DIR +from chan.core.ChanKLU import ChanKLU +from chan.core.ChanCTime import ChanCTime +from chan.core.ChanKLC import ChanKLC +from chan.core.ChanBI import ChanBI # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/Deepseek_BTC.py b/strategies/Deepseek_BTC.py index fa66db6..c175222 100644 --- a/strategies/Deepseek_BTC.py +++ b/strategies/Deepseek_BTC.py @@ -9,9 +9,9 @@ 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")) +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) #sys.path.append(os.path.abspath("/Users/jack/Documents/GitHub/freqtrade/user_data/Chan")) -from ChanLun import ChanLun +from chan.pipeline.ChanLun import ChanLun # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta @@ -19,7 +19,7 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib from datetime import datetime, timedelta, timezone from freqtrade.persistence import Trade, Order from typing import Optional -from ChanPY import ChanPY +from chan.analysis.ChanPY import ChanPY import logging logger = logging.getLogger(__name__) from openai import OpenAI diff --git a/strategies/EMA26_EMA52_Cross.py b/strategies/EMA26_EMA52_Cross.py index 8254bcf..b5a3335 100644 --- a/strategies/EMA26_EMA52_Cross.py +++ b/strategies/EMA26_EMA52_Cross.py @@ -5,8 +5,8 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX +from chan.pipeline.ChanLun import ChanLun +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/EMA_Cross.py b/strategies/EMA_Cross.py index a875058..fa7fd6a 100644 --- a/strategies/EMA_Cross.py +++ b/strategies/EMA_Cross.py @@ -5,8 +5,8 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX +from chan.pipeline.ChanLun import ChanLun +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta diff --git a/strategies/ElliottWaveBTCStrategy.py b/strategies/ElliottWaveBTCStrategy.py index c2fefb4..c90b2c0 100644 --- a/strategies/ElliottWaveBTCStrategy.py +++ b/strategies/ElliottWaveBTCStrategy.py @@ -20,7 +20,7 @@ import sys import os sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun +from chan.pipeline.ChanLun import ChanLun from freqtrade.strategy import IStrategy from pandas import DataFrame diff --git a/strategies/ElliottWaveBTCStrategyV2.py b/strategies/ElliottWaveBTCStrategyV2.py index 59d89dc..093d218 100644 --- a/strategies/ElliottWaveBTCStrategyV2.py +++ b/strategies/ElliottWaveBTCStrategyV2.py @@ -21,7 +21,7 @@ Elliott Wave Strategy for BTC Perpetual Futures V2 import sys import os sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun +from chan.pipeline.ChanLun import ChanLun from freqtrade.strategy import IStrategy from pandas import DataFrame diff --git a/strategies/Template.py b/strategies/Template.py index 1840e66..e4b54c9 100644 --- a/strategies/Template.py +++ b/strategies/Template.py @@ -5,8 +5,8 @@ import sys import os # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun -from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX, Chan_BSP_TYPE +from chan.pipeline.ChanLun import ChanLun +from chan.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX, Chan_BSP_TYPE # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta