Author SHA1 Message Date
PorterandCursor 2c1232555e refactor: 缠论引擎迁入 chan/ 分层解耦,指标外置
将核心结构、指标与分析拆到 chan/{core,indicators,analysis,pipeline};
根目录保留兼容 shim;strategies 改为从 chan 包导入;买卖点经 bsp_macd 与 MACD 接合。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 14:47:13 +08:00
90 changed files with 9067 additions and 8535 deletions
+37 -21
View File
@@ -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
+5 -132
View File
@@ -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")
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
import importlib
import sys
_impl = importlib.import_module("chan.core.ChanBI")
sys.modules[__name__] = _impl
+5 -161
View File
@@ -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段可能升级
}
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
import importlib
import sys
_impl = importlib.import_module("chan.core.ChanBIZS")
sys.modules[__name__] = _impl
+5 -24
View File
@@ -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
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
import importlib
import sys
_impl = importlib.import_module("chan.core.ChanBSP")
sys.modules[__name__] = _impl
+5 -44
View File
@@ -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
+5 -368
View File
@@ -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
+4 -413
View File
@@ -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
+5 -620
View File
@@ -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)
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
import importlib
import sys
_impl = importlib.import_module("chan.core.ChanKLC")
sys.modules[__name__] = _impl
+5 -388
View File
@@ -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
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
import importlib
import sys
_impl = importlib.import_module("chan.core.ChanKLU")
sys.modules[__name__] = _impl
+5 -188
View File
@@ -1,188 +1,5 @@
import warnings
# 抑制 Docker 内 technical.util 的 fillna/ffill/bfill 的 pandas FutureWarningpandas 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))
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
import importlib
import sys
_impl = importlib.import_module("chan.pipeline.ChanLun")
sys.modules[__name__] = _impl
+4 -528
View File
@@ -1,529 +1,5 @@
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
import importlib
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))
_impl = importlib.import_module("chan.analysis.ChanLun_Classifier")
sys.modules[__name__] = _impl
+5 -274
View File
@@ -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
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
import importlib
import sys
_impl = importlib.import_module("chan.analysis.ChanMACD")
sys.modules[__name__] = _impl
+5 -117
View File
@@ -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)
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
import importlib
import sys
_impl = importlib.import_module("chan.analysis.ChanMACDHistSet")
sys.modules[__name__] = _impl
+5 -49
View File
@@ -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
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
import importlib
import sys
_impl = importlib.import_module("chan.analysis.ChanMACDSeg")
sys.modules[__name__] = _impl
+5 -141
View File
@@ -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))
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
import importlib
import sys
_impl = importlib.import_module("chan.analysis.ChanMACDUnitTF")
sys.modules[__name__] = _impl
+4 -401
View File
@@ -1,402 +1,5 @@
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
import importlib
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
_impl = importlib.import_module("chan.analysis.ChanPY")
sys.modules[__name__] = _impl
+5 -292
View File
@@ -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
+5 -145
View File
@@ -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
+5 -91
View File
@@ -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
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
import importlib
import sys
_impl = importlib.import_module("chan.core.ChanSBI")
sys.modules[__name__] = _impl
+5 -197
View File
@@ -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
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
import importlib
import sys
_impl = importlib.import_module("chan.core.ChanSEG")
sys.modules[__name__] = _impl
+5 -108
View File
@@ -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 # 由外部设置
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
import importlib
import sys
_impl = importlib.import_module("chan.core.ChanZS")
sys.modules[__name__] = _impl
+5 -566
View File
@@ -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
+5 -7
View File
@@ -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
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
import importlib
import sys
_impl = importlib.import_module("chan.core.Chan_FX_Box")
sys.modules[__name__] = _impl
+5 -448
View File
@@ -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()
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
import importlib
import sys
_impl = importlib.import_module("chan.analysis.Find_Trend")
sys.modules[__name__] = _impl
+5 -2583
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
"""缠论引擎包:core(结构)/ indicators(指标)/ analysis(接合)/ pipeline(编排)。"""
from .pipeline.ChanLun import ChanLun
from .pipeline.TF_DF import TF_DF
__all__ = ["ChanLun", "TF_DF"]
+414
View File
@@ -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())
+529
View File
@@ -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))
+274
View File
@@ -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
+117
View File
@@ -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)
+49
View File
@@ -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
+141
View File
@@ -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))
+478
View File
@@ -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
+292
View File
@@ -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)
+145
View File
@@ -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)
+566
View File
@@ -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)
+448
View File
@@ -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()
+19
View File
@@ -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",
]
+113
View File
@@ -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
+153
View File
@@ -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()
@@ -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():
@@ -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
@@ -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
+135
View File
@@ -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")
+161
View File
@@ -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段可能升级
}
+24
View File
@@ -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
+44
View File
@@ -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
+368
View File
@@ -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" # 初始状态
+620
View File
@@ -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)
+390
View File
@@ -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
+91
View File
@@ -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
+197
View File
@@ -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
+109
View File
@@ -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 # 由外部设置
+7
View File
@@ -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
+23
View File
@@ -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",
]
+13
View File
@@ -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",
]
+20
View File
@@ -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:
"""将指标写入 KLUdeprecated:新代码应通过 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)
+25
View File
@@ -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
+74
View File
@@ -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)
+41
View File
@@ -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]
+188
View File
@@ -0,0 +1,188 @@
import warnings
# 抑制 Docker 内 technical.util 的 fillna/ffill/bfill 的 pandas FutureWarningpandas 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))
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
"""编排层:多周期入口与单周期流水线。"""
from .ChanLun import ChanLun
from .TF_DF import TF_DF
__all__ = ["ChanLun", "TF_DF"]
+5 -153
View File
@@ -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()
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
import importlib
import sys
_impl = importlib.import_module("chan.analysis.fx_strength_config")
sys.modules[__name__] = _impl
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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-
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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
+6 -6
View File
@@ -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():
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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
+6 -6
View File
@@ -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
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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