Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c1232555e |
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,188 +1,5 @@
|
||||
import warnings
|
||||
|
||||
# 抑制 Docker 内 technical.util 的 fillna/ffill/bfill 的 pandas FutureWarning(pandas 2.x 弃用 object 静默 downcast)
|
||||
warnings.filterwarnings(
|
||||
"ignore",
|
||||
category=FutureWarning,
|
||||
message=".*Downcasting object dtype arrays on \\.fillna.*",
|
||||
)
|
||||
|
||||
from datetime import timedelta
|
||||
from pandas import DataFrame
|
||||
from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_MACD_STATE, Chan_PRICE_TREND, Chan_KLU_PATTERN
|
||||
from ChanKLU import ChanKLU
|
||||
from ChanKLC import ChanKLC
|
||||
from ChanBI import ChanBI
|
||||
from ChanSBI import ChanSBI
|
||||
from ChanSEG import ChanSEG
|
||||
from ChanZS import ChanZS
|
||||
from ChanBSP import ChanBSP
|
||||
import talib.abstract as ta
|
||||
import pandas as pd
|
||||
from technical.util import resample_to_interval
|
||||
from decimal import Decimal
|
||||
import numpy as np
|
||||
from ChanMACD import ChanMACD
|
||||
from TF_DF import TF_DF
|
||||
from ChanZone import StructureZone, StructureZoneConfig, analyze_structure_zones
|
||||
|
||||
class ChanLun():
|
||||
def __init__(self):
|
||||
self.time2m = 2
|
||||
self.time3m = 3
|
||||
self.time5m = 5
|
||||
self.time10m = 10
|
||||
self.time20m = 20
|
||||
self.time_m_intervals = [2, 3, 5, 10, 20]
|
||||
self.time_m_symbols = ['2m', '3m', '5m', '10m', '20m']
|
||||
self.time30m = 30
|
||||
self.time45m = 45
|
||||
self.time_m15_intervals = [30, 45]
|
||||
self.time_m15_symbols = ['30m', '45m']
|
||||
self.time2h = 2*60
|
||||
self.time4h = 4*60
|
||||
self.time6h = 6*60
|
||||
self.time8h = 8*60
|
||||
self.time12h = 12*60
|
||||
self.time16h = 16*60
|
||||
self.time_h_intervals = [2*60, 4*60, 6*60, 8*60, 12*60, 16*60]
|
||||
self.time_h_symbols = ['2h', '4h', '6h', '8h', '12h', '16h']
|
||||
self.time2d = 2*24*60
|
||||
self.time3d = 3*24*60
|
||||
self.time_d_intervals = [2*24*60, 3*24*60]
|
||||
self.time_d_symbols = ['2d', '3d']
|
||||
self.time1w = 7*24*60
|
||||
self.time2w = 14*24*60
|
||||
self.time_w_intervals = [14*24*60]
|
||||
self.time_w_symbols = ['2w']
|
||||
self.time2M = 2*30*24*60
|
||||
self.time3M = 3*30*24*60
|
||||
self.time6M = 6*30*24*60
|
||||
self.time1y = 12*30*24*60
|
||||
self.time_M_intervals = [2*30*24*60, 3*30*24*60, 6*30*24*60, 12*30*24*60]
|
||||
self.time_M_symbols = ['2M', '3M', '6M', '1y']
|
||||
self.time_symbols = ['1m', '2m', '3m', '5m', '10m', '15m', '20m', '30m', '45m','1h', '2h', '4h', '6h', '8h', '12h', '16h', '1d', '2d', '3d']
|
||||
self.tf_df_dict = {}
|
||||
self.ema_symbols = ['5m', '15m', '30m', '45m', '1h', '2h', '4h', '8h', '12h', '1d', '2d', '3d']
|
||||
self.tf_df = TF_DF()
|
||||
def init_data(self, dataframe, intervals, timeframes):
|
||||
for index in range(0, len(intervals)):
|
||||
timeframe = timeframes[index]
|
||||
interval = intervals[index]
|
||||
self.tf_df_dict[timeframe] = TF_DF(dataframe, interval, timeframe)
|
||||
def init_dataframes(self, dataframe_m=None, dataframe_15m=None, dataframe_h=None, dataframe_d=None, dataframe_w=None, dataframe_M=None):
|
||||
self.tf_df_dict = {}
|
||||
if dataframe_m is not None:
|
||||
self.tf_df_dict['1m'] = TF_DF(dataframe_m, 1, '1m')
|
||||
self.init_data(dataframe_m, self.time_m_intervals, self.time_m_symbols)
|
||||
if dataframe_15m is not None:
|
||||
self.tf_df_dict['15m'] = TF_DF(dataframe_15m, 1, '15m')
|
||||
self.init_data(dataframe_15m, self.time_m15_intervals, self.time_m15_symbols)
|
||||
if dataframe_h is not None:
|
||||
self.tf_df_dict['1h'] = TF_DF(dataframe_h, 1, '1h')
|
||||
self.init_data(dataframe_h, self.time_h_intervals, self.time_h_symbols)
|
||||
if dataframe_d is not None:
|
||||
self.tf_df_dict['1d'] = TF_DF(dataframe_d, 1, '1d')
|
||||
self.init_data(dataframe_d, self.time_d_intervals, self.time_d_symbols)
|
||||
if dataframe_w is not None and False:
|
||||
self.tf_df_dict['1w'] = TF_DF(dataframe_w, 1, '1w')
|
||||
self.init_data(dataframe_w, self.time_w_intervals, self.time_w_symbols)
|
||||
if dataframe_M is not None and False:
|
||||
self.tf_df_dict['1M'] = TF_DF(dataframe_M, 1, '1M')
|
||||
self.init_data(dataframe_M, self.time_M_intervals, self.time_M_symbols)
|
||||
def get_ema52_dict(self):
|
||||
if len(self.tf_df_dict) > 0:
|
||||
return {key: self.tf_df_dict[key].get_ema52() for key in self.ema_symbols}
|
||||
return None
|
||||
def get_ema24_dict(self):
|
||||
if len(self.tf_df_dict) > 0:
|
||||
return {key: self.tf_df_dict[key].get_ema24() for key in self.ema_symbols}
|
||||
return None
|
||||
def get_current_klc_dict(self):
|
||||
if len(self.tf_df_dict) > 0:
|
||||
return {key: self.tf_df_dict[key].get_current_klc() for key in self.ema_symbols}
|
||||
return None
|
||||
def get_tf_df_by_timeframe(self, timeframe):
|
||||
if timeframe in self.tf_df_dict:
|
||||
return self.tf_df_dict[timeframe]
|
||||
return None
|
||||
def check_price_ema52(self, price):
|
||||
key_list = []
|
||||
if len(self.tf_df_dict) > 0:
|
||||
ema52_dict = self.get_ema52_dict()
|
||||
for key in self.ema_symbols:
|
||||
if ema52_dict[key] is not None:
|
||||
if abs(price - ema52_dict[key]) < 100:
|
||||
key_list.append(key)
|
||||
return key_list
|
||||
def get_ema_bsp(self, long_tf='1h', short_tf='15m'):
|
||||
if long_tf in self.tf_df_dict and short_tf in self.tf_df_dict:
|
||||
long_df = self.tf_df_dict[long_tf]
|
||||
short_df = self.tf_df_dict[short_tf]
|
||||
return long_df.get_ema_bsp(short_df)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def get_bsp_state(self, dataframe):
|
||||
return self.tf_df.get_bsp_state(dataframe)
|
||||
|
||||
def get_structure_zones(self, current_price=None, config=None):
|
||||
if config is None:
|
||||
config = StructureZoneConfig()
|
||||
return analyze_structure_zones(
|
||||
self.tf_df_dict,
|
||||
self.ema_symbols,
|
||||
current_price=current_price,
|
||||
config=config,
|
||||
)
|
||||
# TF_DF methods ------------------------------------------
|
||||
def get_ema_state(self, dataframe):
|
||||
return self.tf_df.get_ema_state(dataframe)
|
||||
def get_klu_state(self, dataframe):
|
||||
return self.tf_df.get_klu_state(dataframe)
|
||||
def check_fx(self, klc):
|
||||
return self.tf_df.check_fx(klc)
|
||||
def add_indicators1(self, df):
|
||||
return self.tf_df.add_indicators(df)
|
||||
def get_bi_list(self, dataframe):
|
||||
return self.tf_df.get_bi_list(dataframe)
|
||||
def get_kl_data(self, dataframe:DataFrame):
|
||||
return self.tf_df.cal_kl_data(dataframe)
|
||||
def cal_volume_ratio(self, dataframe, window=10):
|
||||
return self.tf_df.cal_volume_ratio(dataframe, window)
|
||||
def calculate_seg_zs(self, bi_list, seg_list):
|
||||
return self.get_seg_zs_list(bi_list, seg_list)
|
||||
def get_seg_list(self, bi_list):
|
||||
return self.tf_df.get_seg_list(bi_list)
|
||||
def cal_trend(self, klc_list):
|
||||
return self.tf_df.cal_trend(klc_list)
|
||||
def check_top_fx(self, last_bottom, klc):
|
||||
return self.tf_df.check_top_fx(last_bottom, klc)
|
||||
def check_bottom_fx(self, last_top, klc):
|
||||
return self.tf_df.check_bottom_fx(last_top, klc)
|
||||
def cal_bi_list(self, klc_list):
|
||||
return self.tf_df.cal_bi_list(klc_list)
|
||||
def find_first_bsp(self, bi_list, bi_zs_list):
|
||||
return self.tf_df.find_first_bsp(bi_list, bi_zs_list)
|
||||
def find_second_bsp(self, bi_list, first_bsp_list):
|
||||
return self.tf_df.find_second_bsp(bi_list, first_bsp_list)
|
||||
def find_all_bsp(self, bi_list, bi_zs_list):
|
||||
return self.tf_df.find_all_bsp(bi_list, bi_zs_list)
|
||||
def get_zs_list(self, bi_list, seg_list):
|
||||
return self.tf_df.get_zs_list(bi_list, seg_list)
|
||||
def cal_bi_zs(self, seg_list):
|
||||
return self.tf_df.cal_bi_zs(seg_list)
|
||||
def cal_bi_zs_list(self, bi_list):
|
||||
#return self.tf_df.cal_bi_zs(bi_list)
|
||||
return self.tf_df.cal_bi_zs_list(bi_list)
|
||||
def get_bi_zs_list(self, bi_list):
|
||||
return self.tf_df.get_bi_zs_list(bi_list)
|
||||
def get_decimal(self, value):
|
||||
return Decimal("{:.2f}".format(value))
|
||||
def get_klc_list(self, klu_list):
|
||||
return self.tf_df.get_klc_list(klu_list)
|
||||
def get_klu_list(self, dataframe):
|
||||
return self.tf_df.cal_klu_pattern(self.get_kl_data(dataframe))
|
||||
"""兼容 shim:请优先 from chan... 导入。本文件仅保持旧路径可用。"""
|
||||
import importlib
|
||||
import sys
|
||||
_impl = importlib.import_module("chan.pipeline.ChanLun")
|
||||
sys.modules[__name__] = _impl
|
||||
|
||||
+4
-528
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
"""缠论引擎包:core(结构)/ indicators(指标)/ analysis(接合)/ pipeline(编排)。"""
|
||||
|
||||
from .pipeline.ChanLun import ChanLun
|
||||
from .pipeline.TF_DF import TF_DF
|
||||
|
||||
__all__ = ["ChanLun", "TF_DF"]
|
||||
@@ -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())
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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段可能升级
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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" # 初始状态
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 # 由外部设置
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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",
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
"""过渡期:把 IndicatorStore 挂到 KLU 属性上,兼容旧代码读取 klu.ema52 等。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable
|
||||
|
||||
from .store import IndicatorStore
|
||||
|
||||
|
||||
def attach_indicators_for_compat(klu_list: Iterable, store: IndicatorStore) -> None:
|
||||
"""将指标写入 KLU(deprecated:新代码应通过 store.get(idx, name) 查询)。"""
|
||||
for klu in klu_list:
|
||||
idx = getattr(klu, "idx", None)
|
||||
if idx is None:
|
||||
continue
|
||||
row = store.row(idx)
|
||||
if row is None:
|
||||
continue
|
||||
if hasattr(klu, "set_indicators"):
|
||||
klu.set_indicators(row)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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]
|
||||
@@ -0,0 +1,188 @@
|
||||
import warnings
|
||||
|
||||
# 抑制 Docker 内 technical.util 的 fillna/ffill/bfill 的 pandas FutureWarning(pandas 2.x 弃用 object 静默 downcast)
|
||||
warnings.filterwarnings(
|
||||
"ignore",
|
||||
category=FutureWarning,
|
||||
message=".*Downcasting object dtype arrays on \\.fillna.*",
|
||||
)
|
||||
|
||||
from datetime import timedelta
|
||||
from pandas import DataFrame
|
||||
from ..core.ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_MACD_STATE, Chan_PRICE_TREND, Chan_KLU_PATTERN
|
||||
from ..core.ChanKLU import ChanKLU
|
||||
from ..core.ChanKLC import ChanKLC
|
||||
from ..core.ChanBI import ChanBI
|
||||
from ..core.ChanSBI import ChanSBI
|
||||
from ..core.ChanSEG import ChanSEG
|
||||
from ..core.ChanZS import ChanZS
|
||||
from ..core.ChanBSP import ChanBSP
|
||||
import talib.abstract as ta
|
||||
import pandas as pd
|
||||
from technical.util import resample_to_interval
|
||||
from decimal import Decimal
|
||||
import numpy as np
|
||||
from ..analysis.ChanMACD import ChanMACD
|
||||
from .TF_DF import TF_DF
|
||||
from ..analysis.ChanZone import StructureZone, StructureZoneConfig, analyze_structure_zones
|
||||
|
||||
class ChanLun():
|
||||
def __init__(self):
|
||||
self.time2m = 2
|
||||
self.time3m = 3
|
||||
self.time5m = 5
|
||||
self.time10m = 10
|
||||
self.time20m = 20
|
||||
self.time_m_intervals = [2, 3, 5, 10, 20]
|
||||
self.time_m_symbols = ['2m', '3m', '5m', '10m', '20m']
|
||||
self.time30m = 30
|
||||
self.time45m = 45
|
||||
self.time_m15_intervals = [30, 45]
|
||||
self.time_m15_symbols = ['30m', '45m']
|
||||
self.time2h = 2*60
|
||||
self.time4h = 4*60
|
||||
self.time6h = 6*60
|
||||
self.time8h = 8*60
|
||||
self.time12h = 12*60
|
||||
self.time16h = 16*60
|
||||
self.time_h_intervals = [2*60, 4*60, 6*60, 8*60, 12*60, 16*60]
|
||||
self.time_h_symbols = ['2h', '4h', '6h', '8h', '12h', '16h']
|
||||
self.time2d = 2*24*60
|
||||
self.time3d = 3*24*60
|
||||
self.time_d_intervals = [2*24*60, 3*24*60]
|
||||
self.time_d_symbols = ['2d', '3d']
|
||||
self.time1w = 7*24*60
|
||||
self.time2w = 14*24*60
|
||||
self.time_w_intervals = [14*24*60]
|
||||
self.time_w_symbols = ['2w']
|
||||
self.time2M = 2*30*24*60
|
||||
self.time3M = 3*30*24*60
|
||||
self.time6M = 6*30*24*60
|
||||
self.time1y = 12*30*24*60
|
||||
self.time_M_intervals = [2*30*24*60, 3*30*24*60, 6*30*24*60, 12*30*24*60]
|
||||
self.time_M_symbols = ['2M', '3M', '6M', '1y']
|
||||
self.time_symbols = ['1m', '2m', '3m', '5m', '10m', '15m', '20m', '30m', '45m','1h', '2h', '4h', '6h', '8h', '12h', '16h', '1d', '2d', '3d']
|
||||
self.tf_df_dict = {}
|
||||
self.ema_symbols = ['5m', '15m', '30m', '45m', '1h', '2h', '4h', '8h', '12h', '1d', '2d', '3d']
|
||||
self.tf_df = TF_DF()
|
||||
def init_data(self, dataframe, intervals, timeframes):
|
||||
for index in range(0, len(intervals)):
|
||||
timeframe = timeframes[index]
|
||||
interval = intervals[index]
|
||||
self.tf_df_dict[timeframe] = TF_DF(dataframe, interval, timeframe)
|
||||
def init_dataframes(self, dataframe_m=None, dataframe_15m=None, dataframe_h=None, dataframe_d=None, dataframe_w=None, dataframe_M=None):
|
||||
self.tf_df_dict = {}
|
||||
if dataframe_m is not None:
|
||||
self.tf_df_dict['1m'] = TF_DF(dataframe_m, 1, '1m')
|
||||
self.init_data(dataframe_m, self.time_m_intervals, self.time_m_symbols)
|
||||
if dataframe_15m is not None:
|
||||
self.tf_df_dict['15m'] = TF_DF(dataframe_15m, 1, '15m')
|
||||
self.init_data(dataframe_15m, self.time_m15_intervals, self.time_m15_symbols)
|
||||
if dataframe_h is not None:
|
||||
self.tf_df_dict['1h'] = TF_DF(dataframe_h, 1, '1h')
|
||||
self.init_data(dataframe_h, self.time_h_intervals, self.time_h_symbols)
|
||||
if dataframe_d is not None:
|
||||
self.tf_df_dict['1d'] = TF_DF(dataframe_d, 1, '1d')
|
||||
self.init_data(dataframe_d, self.time_d_intervals, self.time_d_symbols)
|
||||
if dataframe_w is not None and False:
|
||||
self.tf_df_dict['1w'] = TF_DF(dataframe_w, 1, '1w')
|
||||
self.init_data(dataframe_w, self.time_w_intervals, self.time_w_symbols)
|
||||
if dataframe_M is not None and False:
|
||||
self.tf_df_dict['1M'] = TF_DF(dataframe_M, 1, '1M')
|
||||
self.init_data(dataframe_M, self.time_M_intervals, self.time_M_symbols)
|
||||
def get_ema52_dict(self):
|
||||
if len(self.tf_df_dict) > 0:
|
||||
return {key: self.tf_df_dict[key].get_ema52() for key in self.ema_symbols}
|
||||
return None
|
||||
def get_ema24_dict(self):
|
||||
if len(self.tf_df_dict) > 0:
|
||||
return {key: self.tf_df_dict[key].get_ema24() for key in self.ema_symbols}
|
||||
return None
|
||||
def get_current_klc_dict(self):
|
||||
if len(self.tf_df_dict) > 0:
|
||||
return {key: self.tf_df_dict[key].get_current_klc() for key in self.ema_symbols}
|
||||
return None
|
||||
def get_tf_df_by_timeframe(self, timeframe):
|
||||
if timeframe in self.tf_df_dict:
|
||||
return self.tf_df_dict[timeframe]
|
||||
return None
|
||||
def check_price_ema52(self, price):
|
||||
key_list = []
|
||||
if len(self.tf_df_dict) > 0:
|
||||
ema52_dict = self.get_ema52_dict()
|
||||
for key in self.ema_symbols:
|
||||
if ema52_dict[key] is not None:
|
||||
if abs(price - ema52_dict[key]) < 100:
|
||||
key_list.append(key)
|
||||
return key_list
|
||||
def get_ema_bsp(self, long_tf='1h', short_tf='15m'):
|
||||
if long_tf in self.tf_df_dict and short_tf in self.tf_df_dict:
|
||||
long_df = self.tf_df_dict[long_tf]
|
||||
short_df = self.tf_df_dict[short_tf]
|
||||
return long_df.get_ema_bsp(short_df)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def get_bsp_state(self, dataframe):
|
||||
return self.tf_df.get_bsp_state(dataframe)
|
||||
|
||||
def get_structure_zones(self, current_price=None, config=None):
|
||||
if config is None:
|
||||
config = StructureZoneConfig()
|
||||
return analyze_structure_zones(
|
||||
self.tf_df_dict,
|
||||
self.ema_symbols,
|
||||
current_price=current_price,
|
||||
config=config,
|
||||
)
|
||||
# TF_DF methods ------------------------------------------
|
||||
def get_ema_state(self, dataframe):
|
||||
return self.tf_df.get_ema_state(dataframe)
|
||||
def get_klu_state(self, dataframe):
|
||||
return self.tf_df.get_klu_state(dataframe)
|
||||
def check_fx(self, klc):
|
||||
return self.tf_df.check_fx(klc)
|
||||
def add_indicators1(self, df):
|
||||
return self.tf_df.add_indicators(df)
|
||||
def get_bi_list(self, dataframe):
|
||||
return self.tf_df.get_bi_list(dataframe)
|
||||
def get_kl_data(self, dataframe:DataFrame):
|
||||
return self.tf_df.cal_kl_data(dataframe)
|
||||
def cal_volume_ratio(self, dataframe, window=10):
|
||||
return self.tf_df.cal_volume_ratio(dataframe, window)
|
||||
def calculate_seg_zs(self, bi_list, seg_list):
|
||||
return self.get_seg_zs_list(bi_list, seg_list)
|
||||
def get_seg_list(self, bi_list):
|
||||
return self.tf_df.get_seg_list(bi_list)
|
||||
def cal_trend(self, klc_list):
|
||||
return self.tf_df.cal_trend(klc_list)
|
||||
def check_top_fx(self, last_bottom, klc):
|
||||
return self.tf_df.check_top_fx(last_bottom, klc)
|
||||
def check_bottom_fx(self, last_top, klc):
|
||||
return self.tf_df.check_bottom_fx(last_top, klc)
|
||||
def cal_bi_list(self, klc_list):
|
||||
return self.tf_df.cal_bi_list(klc_list)
|
||||
def find_first_bsp(self, bi_list, bi_zs_list):
|
||||
return self.tf_df.find_first_bsp(bi_list, bi_zs_list)
|
||||
def find_second_bsp(self, bi_list, first_bsp_list):
|
||||
return self.tf_df.find_second_bsp(bi_list, first_bsp_list)
|
||||
def find_all_bsp(self, bi_list, bi_zs_list):
|
||||
return self.tf_df.find_all_bsp(bi_list, bi_zs_list)
|
||||
def get_zs_list(self, bi_list, seg_list):
|
||||
return self.tf_df.get_zs_list(bi_list, seg_list)
|
||||
def cal_bi_zs(self, seg_list):
|
||||
return self.tf_df.cal_bi_zs(seg_list)
|
||||
def cal_bi_zs_list(self, bi_list):
|
||||
#return self.tf_df.cal_bi_zs(bi_list)
|
||||
return self.tf_df.cal_bi_zs_list(bi_list)
|
||||
def get_bi_zs_list(self, bi_list):
|
||||
return self.tf_df.get_bi_zs_list(bi_list)
|
||||
def get_decimal(self, value):
|
||||
return Decimal("{:.2f}".format(value))
|
||||
def get_klc_list(self, klu_list):
|
||||
return self.tf_df.get_klc_list(klu_list)
|
||||
def get_klu_list(self, dataframe):
|
||||
return self.tf_df.cal_klu_pattern(self.get_kl_data(dataframe))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
"""编排层:多周期入口与单周期流水线。"""
|
||||
|
||||
from .ChanLun import ChanLun
|
||||
from .TF_DF import TF_DF
|
||||
|
||||
__all__ = ["ChanLun", "TF_DF"]
|
||||
+5
-153
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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-
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user