refactor: 缠论引擎包化与 Web 分层(ECR-001)
将根目录引擎迁入 chanlun/ 并保留兼容 shim;拆分 TF_DF 与 web 服务; 前端模块化;strategies 改用 chanlun 导入;补充 ESS 文档与 golden 回归。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,44 +6,48 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
缠论 (Chan Theory) technical analysis system for Freqtrade. Implements Chan Zhong Shui Chan's theory for crypto/stock trading, including fractal (分型), stroke (笔), segment (线段), pivot/center (中枢), and buy/sell point (买卖点) detection.
|
缠论 (Chan Theory) technical analysis system for Freqtrade. Implements Chan Zhong Shui Chan's theory for crypto/stock trading, including fractal (分型), stroke (笔), segment (线段), pivot/center (中枢), and buy/sell point (买卖点) detection.
|
||||||
|
|
||||||
|
## Governance
|
||||||
|
|
||||||
|
- ESS 文档:`docs/PROJECT_PROFILE.md`、`docs/ECR/`、`docs/ENGINEERING_SPEC/`
|
||||||
|
- **正式引擎包**:`chanlun/`;strategies / web 已用 `from chanlun import ...`
|
||||||
|
- 根目录 `Chan*.py` / `TF_DF.py` 仍为 **兼容 shim**(旧脚本可用)
|
||||||
|
|
||||||
## Core Architecture
|
## Core Architecture
|
||||||
|
|
||||||
### Chan Theory Engine (`Chan*.py`)
|
### Chan Theory Engine (`chanlun/`)
|
||||||
|
|
||||||
|
```text
|
||||||
|
chanlun/
|
||||||
|
core/ # KLU KLC BI SBI SEG ZS BIZS BSP Enum CTime
|
||||||
|
pipeline/ # orchestrator(ChanLun) + timeframe(TF_DF) + builders/
|
||||||
|
indicators/ # ChanMACD*
|
||||||
|
analysis/ # Zone Classifier Pivot Heng PY Find_Trend ...
|
||||||
|
```
|
||||||
|
|
||||||
Data processing pipeline (each step feeds the next):
|
Data processing pipeline (each step feeds the next):
|
||||||
|
|
||||||
1. **`ChanKLU.py`** — Raw K-line unit with TA indicators (EMA, MACD, RSI, Bollinger Bands) and candlestick pattern recognition (`Chan_KLU_PATTERN`)
|
1. **`chanlun.core.ChanKLU`** — Raw K-line unit with TA indicators and pattern recognition
|
||||||
2. **`ChanKLC.py`** — Combined K-line: inclusion processing (包含处理), fractal (分型) detection. Linked-list structure with `.next`/`.pre` pointers
|
2. **`chanlun.core.ChanKLC`** — Combined K-line: inclusion + fractal; `.next`/`.pre` linked list
|
||||||
3. **`ChanBI.py`** — Stroke (笔): basic trend unit connecting alternating fractals
|
3. **`chanlun.core.ChanBI`** — Stroke (笔)
|
||||||
4. **`ChanSBI.py`** — Special Stroke: aggregates multiple BI into higher-level units with fractal detection, feeds into SEG
|
4. **`chanlun.core.ChanSBI`** — Special stroke → SEG
|
||||||
5. **`ChanSEG.py`** — Segment (线段): built from SBI strokes
|
5. **`chanlun.core.ChanSEG`** — Segment (线段)
|
||||||
6. **`ChanZS.py`** / **`ChanBIZS.py`** — Center/pivot (中枢): consolidation zones (segment-level and stroke-level)
|
6. **`chanlun.core.ChanZS`** / **`ChanBIZS`** — Centers (中枢)
|
||||||
7. **`ChanBSP.py`** — Buy/Sell points (买卖点): Type 1/2/3 signals
|
7. **`chanlun.core.ChanBSP`** — Buy/Sell points
|
||||||
8. **`ChanLun.py`** — Main orchestrator: ties all steps together, entry point
|
8. **`chanlun.pipeline.orchestrator.ChanLun`** — Orchestrator
|
||||||
9. **`TF_DF.py`** — Timeframe-aware DataFrame processor: resamples data, runs the full pipeline per timeframe, handles multi-timeframe analysis
|
9. **`chanlun.pipeline.timeframe.TF_DF`** — Timeframe facade;实现拆在 `pipeline/builders/`
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
### Services
|
### 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.
|
- **外部 DATA_SERVICE** — 行情服务(env: `DATA_SERVICE_URL`);本仓库可不含 data_provider 源码
|
||||||
- **`web/`** — Flask web UI for interactive chart visualization with Chan theory overlays. Port 8123.
|
- **`web/`** — Flask UI:`create_app()` + `api/` blueprints + `services/`;前端 `static/js/app/`。默认端口见 `web/config.py`(`FLASK_PORT`,常见 8128)
|
||||||
- **`strategies/`** — Freqtrade trading strategies using the Chan theory engine (53 strategies)
|
- **`strategies/`** — Freqtrade strategies(本 ECR 不改)
|
||||||
- **`config/`** — Freqtrade JSON config files per pair/timeframe
|
- **`config/`** — Freqtrade configs(本 ECR 不改)
|
||||||
|
|
||||||
### Data Flow
|
### Data Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
Exchange (CCXT) → data_provider (CSV cache) → Freqtrade → Strategy → ChanLun → TF_DF
|
Exchange / DATA_SERVICE → Freqtrade Strategy / web → ChanLun → TF_DF
|
||||||
→ KLU → KLC → BI → SBI → SEG → ZS → BSP
|
→ KLU → KLC → BI → SBI → SEG → ZS → BSP
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common Commands
|
## Common Commands
|
||||||
|
|||||||
@@ -1,132 +1,2 @@
|
|||||||
from decimal import Decimal
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
import ChanKLC
|
from chanlun.core.ChanBI import * # noqa: F403
|
||||||
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")
|
|
||||||
|
|||||||
+2
-161
@@ -1,161 +1,2 @@
|
|||||||
from ChanEnum import Chan_ZS_DIR, Chan_ZS_TYPE, Chan_BI_DIR
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
import ChanBI
|
from chanlun.core.ChanBIZS import * # noqa: F403
|
||||||
# 中枢
|
|
||||||
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段可能升级
|
|
||||||
}
|
|
||||||
|
|||||||
+2
-24
@@ -1,24 +1,2 @@
|
|||||||
import ChanBI
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
from ChanEnum import Chan_BSP_TYPE, Chan_BSP_DIR
|
from chanlun.core.ChanBSP import * # noqa: F403
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
+2
-44
@@ -1,44 +1,2 @@
|
|||||||
from datetime import datetime
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
|
from chanlun.core.ChanCTime import * # noqa: F403
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
+2
-368
@@ -1,368 +1,2 @@
|
|||||||
from enum import Enum, auto
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
from typing import Literal
|
from chanlun.core.ChanEnum import * # noqa: F403
|
||||||
|
|
||||||
|
|
||||||
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" # 初始状态
|
|
||||||
|
|||||||
+2
-414
@@ -1,414 +1,2 @@
|
|||||||
#!/usr/bin/env python3
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
from __future__ import annotations
|
from chanlun.analysis.ChanHeng import * # noqa: F403
|
||||||
|
|
||||||
"""
|
|
||||||
使用 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())
|
|
||||||
|
|
||||||
|
|||||||
+2
-620
@@ -1,620 +1,2 @@
|
|||||||
import copy
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
from typing import Dict, Optional
|
from chanlun.core.ChanKLC import * # noqa: F403
|
||||||
|
|
||||||
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)
|
|
||||||
|
|||||||
+2
-388
@@ -1,388 +1,2 @@
|
|||||||
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
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
class ChanKLU:
|
from chanlun.core.ChanKLU import * # noqa: F403
|
||||||
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
|
|
||||||
|
|
||||||
|
|||||||
+3
-188
@@ -1,188 +1,3 @@
|
|||||||
import warnings
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
|
from chanlun.pipeline.orchestrator import ChanLun # noqa: F401
|
||||||
# 抑制 Docker 内 technical.util 的 fillna/ffill/bfill 的 pandas FutureWarning(pandas 2.x 弃用 object 静默 downcast)
|
from chanlun.pipeline.timeframe import TF_DF # noqa: F401
|
||||||
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))
|
|
||||||
|
|||||||
+2
-529
@@ -1,529 +1,2 @@
|
|||||||
import sys
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
import os
|
from chanlun.analysis.ChanLun_Classifier import * # noqa: F403
|
||||||
#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))
|
|
||||||
|
|
||||||
|
|||||||
+2
-274
@@ -1,274 +1,2 @@
|
|||||||
from ChanKLU import ChanKLU
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
from ChanEnum import Chan_MACD_STATE, Chan_MACDSEG_DIR, Chan_MACDHISTSET_DIR, Chan_MACDUNITTF_DIR, Chan_MACDUNITTF_TYPE
|
from chanlun.indicators.ChanMACD import * # noqa: F403
|
||||||
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
|
|
||||||
|
|||||||
+2
-117
@@ -1,117 +1,2 @@
|
|||||||
from ChanEnum import Chan_MACDHISTSET_DIR, Chan_MACDUNITTF_DIV, Chan_MACD_STATE
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
|
from chanlun.indicators.ChanMACDHistSet import * # noqa: F403
|
||||||
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)
|
|
||||||
|
|
||||||
|
|||||||
+2
-49
@@ -1,49 +1,2 @@
|
|||||||
from ChanEnum import Chan_MACDSEG_DIR
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
|
from chanlun.indicators.ChanMACDSeg import * # noqa: F403
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
+2
-141
@@ -1,141 +1,2 @@
|
|||||||
from ChanEnum import Chan_MACD_STATE, Chan_MACDUNITTF_DIR, Chan_MACDHISTSET_DIR, Chan_MACDUNITTF_DIV, Chan_MACDUNITTF_TYPE
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
|
from chanlun.indicators.ChanMACDUnitTF import * # noqa: F403
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,402 +1,2 @@
|
|||||||
import sys
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
import os
|
from chanlun.analysis.ChanPY import * # noqa: F403
|
||||||
#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
|
|
||||||
|
|
||||||
|
|||||||
+2
-292
@@ -1,292 +1,2 @@
|
|||||||
"""
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
中枢结构特征提取 + 标签化
|
from chanlun.analysis.ChanPivotClassifier import * # noqa: F403
|
||||||
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)
|
|
||||||
|
|||||||
+2
-145
@@ -1,145 +1,2 @@
|
|||||||
"""
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
实时中枢特征跟踪器
|
from chanlun.analysis.ChanPivotMonitor import * # noqa: F403
|
||||||
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)
|
|
||||||
|
|||||||
+2
-91
@@ -1,91 +1,2 @@
|
|||||||
import copy
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
from typing import Dict, Optional
|
from chanlun.core.ChanSBI import * # noqa: F403
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
+2
-197
@@ -1,197 +1,2 @@
|
|||||||
import copy
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
from typing import Dict, Optional
|
from chanlun.core.ChanSEG import * # noqa: F403
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
@@ -1,108 +1,2 @@
|
|||||||
from typing import Dict, Optional
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
|
from chanlun.core.ChanZS import * # noqa: F403
|
||||||
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 # 由外部设置
|
|
||||||
|
|||||||
+2
-566
@@ -1,566 +1,2 @@
|
|||||||
"""
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
结构价值区 (Structure Zone) 系统
|
from chanlun.analysis.ChanZone import * # noqa: F403
|
||||||
|
|
||||||
将多时间周期的 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)
|
|
||||||
|
|||||||
+2
-7
@@ -1,7 +1,2 @@
|
|||||||
class Chan_FX_Box():
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
def __init__(self, start_time, end_time, high, low):
|
from chanlun.core.Chan_FX_Box import * # noqa: F403
|
||||||
self.start_time = start_time
|
|
||||||
self.end_time = end_time
|
|
||||||
self.high = high
|
|
||||||
self.low = low
|
|
||||||
|
|
||||||
|
|||||||
+2
-448
@@ -1,448 +1,2 @@
|
|||||||
import ccxt
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
import pandas as pd
|
from chanlun.analysis.Find_Trend import * # noqa: F403
|
||||||
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,11 @@
|
|||||||
|
"""缠论引擎正式包。
|
||||||
|
|
||||||
|
推荐::
|
||||||
|
from chanlun import ChanLun, TF_DF
|
||||||
|
from chanlun.core.ChanEnum import Chan_BI_DIR
|
||||||
|
"""
|
||||||
|
|
||||||
|
from chanlun.pipeline.orchestrator import ChanLun
|
||||||
|
from chanlun.pipeline.timeframe 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 chanlun.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 chanlun.core.ChanKLU import ChanKLU
|
||||||
|
from chanlun.core.ChanKLC import ChanKLC
|
||||||
|
from chanlun.core.ChanBI import ChanBI
|
||||||
|
from chanlun.core.ChanSBI import ChanSBI
|
||||||
|
from chanlun.core.ChanSEG import ChanSEG
|
||||||
|
from chanlun.core.ChanZS import ChanZS
|
||||||
|
from chanlun.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 chanlun.pipeline.orchestrator 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,402 @@
|
|||||||
|
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
|
||||||
|
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
"""
|
||||||
|
中枢结构特征提取 + 标签化
|
||||||
|
Market Structure Dataset Builder — Phase 1
|
||||||
|
|
||||||
|
定位: 训练数据集构建工具,不是交易信号生成器。
|
||||||
|
Feature 描述中枢内部结构,Label 记录中枢后实际演化。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
import json
|
||||||
|
from typing import Optional
|
||||||
|
from chanlun.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 chanlun.analysis.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,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()
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
import chanlun.core.ChanKLC as ChanKLC
|
||||||
|
from chanlun.core.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")
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
from chanlun.core.ChanEnum import Chan_ZS_DIR, Chan_ZS_TYPE, Chan_BI_DIR
|
||||||
|
import chanlun.core.ChanBI as 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 @@
|
|||||||
|
import chanlun.core.ChanBI as ChanBI
|
||||||
|
from chanlun.core.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 chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_KLC_FX
|
||||||
|
from chanlun.core.ChanEnum import Chan_K_DIR, Chan_MACD_STATE, Chan_PRICE_TREND, Chan_EMA_POS
|
||||||
|
from chanlun.core.ChanEnum import Chan_EMA_SEMANTIC, Chan_BSP_TYPE, Chan_KLC_STATE, Chan_FX
|
||||||
|
import chanlun.core.ChanKLU as ChanKLU
|
||||||
|
import chanlun.core.ChanCTime as ChanCTime
|
||||||
|
import chanlun.core.Chan_FX_Box as 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,388 @@
|
|||||||
|
from chanlun.core.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
|
||||||
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import copy
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR
|
||||||
|
import chanlun.core.ChanKLU as ChanKLU
|
||||||
|
from chanlun.core.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 chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_SEG_DIR, Chan_BI_DIR, Chan_ZS_DIR
|
||||||
|
import chanlun.core.ChanCTime as ChanCTime
|
||||||
|
from chanlun.core.ChanBI import ChanBI
|
||||||
|
from chanlun.core.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
|
||||||
|
|
||||||
|
import chanlun.core.ChanKLC as ChanKLC
|
||||||
|
import chanlun.core.ChanSEG as ChanSEG
|
||||||
|
import chanlun.core.ChanCTime as ChanCTime
|
||||||
|
from chanlun.core.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,274 @@
|
|||||||
|
from chanlun.core.ChanKLU import ChanKLU
|
||||||
|
from chanlun.core.ChanEnum import Chan_MACD_STATE, Chan_MACDSEG_DIR, Chan_MACDHISTSET_DIR, Chan_MACDUNITTF_DIR, Chan_MACDUNITTF_TYPE
|
||||||
|
from chanlun.indicators.ChanMACDSeg import ChanMACDSeg
|
||||||
|
from chanlun.indicators.ChanMACDUnitTF import ChanMACDUnitTF
|
||||||
|
from chanlun.indicators.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 chanlun.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 chanlun.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 chanlun.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,682 @@
|
|||||||
|
"""TF_DF builder mixin — 由 split_tfdf_builders 自动生成,逻辑与原 TF_DF 一致。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
from technical.util import resample_to_interval
|
||||||
|
|
||||||
|
from chanlun.core.ChanBI import ChanBI
|
||||||
|
from chanlun.core.ChanBIZS import ChanBIZS
|
||||||
|
from chanlun.core.ChanBSP import ChanBSP
|
||||||
|
from chanlun.core.ChanEnum import (
|
||||||
|
Chan_BI_DIR,
|
||||||
|
Chan_BSP_DIR,
|
||||||
|
Chan_BSP_TYPE,
|
||||||
|
Chan_FX_TYPE,
|
||||||
|
Chan_K_DIR,
|
||||||
|
Chan_KLC_FX,
|
||||||
|
Chan_KLC_STATE,
|
||||||
|
Chan_KLINE_DIR,
|
||||||
|
Chan_KLU_PATTERN,
|
||||||
|
Chan_PRICE_TREND,
|
||||||
|
Chan_SEG_DIR,
|
||||||
|
Chan_ZS_DIR,
|
||||||
|
)
|
||||||
|
from chanlun.core.ChanKLC import ChanKLC
|
||||||
|
from chanlun.core.ChanKLU import ChanKLU
|
||||||
|
from chanlun.core.ChanSBI import ChanSBI
|
||||||
|
from chanlun.core.ChanSEG import ChanSEG
|
||||||
|
from chanlun.core.ChanZS import ChanZS, ChanZS_Big
|
||||||
|
from chanlun.indicators.ChanMACD import ChanMACD
|
||||||
|
|
||||||
|
|
||||||
|
class BiBuilderMixin:
|
||||||
|
def cal_trend(self, klc_list):
|
||||||
|
"""
|
||||||
|
基于价格与EMA24/EMA52的位置关系、以及MACD/Signal/Hist的方向,
|
||||||
|
为每个KLC打上趋势标签:'UP' / 'DOWN' / 'FLAT'。
|
||||||
|
仅设置 klc.trend,不影响其它字段。
|
||||||
|
"""
|
||||||
|
if not klc_list:
|
||||||
|
return klc_list
|
||||||
|
last_trend = Chan_PRICE_TREND.UNKNOWN
|
||||||
|
# 趋势延续性:参考近 N 根已完成的KLC
|
||||||
|
lookback_n = 5
|
||||||
|
prev_klcs = []
|
||||||
|
for klc in klc_list:
|
||||||
|
price = getattr(klc, 'close', None)
|
||||||
|
ema24 = getattr(klc, 'ema24', None)
|
||||||
|
ema52 = getattr(klc, 'ema52', None)
|
||||||
|
macd_raw = getattr(klc, 'macd', None)
|
||||||
|
signal_raw = getattr(klc, 'signal', None)
|
||||||
|
hist_raw = getattr(klc, 'macdhist', None)
|
||||||
|
macd = macd_raw if macd_raw is not None else 0
|
||||||
|
signal = signal_raw if signal_raw is not None else 0
|
||||||
|
hist = hist_raw if hist_raw is not None else 0
|
||||||
|
rsi = getattr(klc, 'rsi', None)
|
||||||
|
macd_ready = macd_raw is not None and signal_raw is not None
|
||||||
|
hist_ready = hist_raw is not None
|
||||||
|
trend = Chan_PRICE_TREND.UNKNOWN
|
||||||
|
score = 0
|
||||||
|
try:
|
||||||
|
# 有效性
|
||||||
|
price_valid = price is not None and price != 0
|
||||||
|
ema24_valid = ema24 is not None and ema24 != 0
|
||||||
|
ema52_valid = ema52 is not None and ema52 != 0
|
||||||
|
# 多因子投票
|
||||||
|
|
||||||
|
# 1) 均线结构 + 价位
|
||||||
|
if ema24_valid or ema52_valid:
|
||||||
|
ma_votes = 0
|
||||||
|
if ema24_valid and ema52_valid:
|
||||||
|
ma_votes += 1 if ema24 > ema52 else -1
|
||||||
|
if price_valid and ema24_valid:
|
||||||
|
ma_votes += 1 if price > ema24 else 0
|
||||||
|
if price_valid and ema52_valid:
|
||||||
|
ma_votes += 1 if price > ema52 else -1
|
||||||
|
# 限幅,避免相关因子重复计分
|
||||||
|
score += max(-2, min(2, ma_votes))
|
||||||
|
# 2) MACD结构
|
||||||
|
if macd_ready:
|
||||||
|
score += 1 if macd >= signal else -1
|
||||||
|
if hist_ready and hist != 0:
|
||||||
|
score += 1 if hist > 0 else -1
|
||||||
|
# 3) 动量与均线差分斜率
|
||||||
|
pre = getattr(klc, 'pre', None)
|
||||||
|
pre_hist = getattr(pre, 'macdhist', None) if pre else None
|
||||||
|
if pre:
|
||||||
|
pre_close = getattr(pre, 'close', None)
|
||||||
|
if price_valid and pre_close is not None:
|
||||||
|
score += 1 if price >= pre_close else -1
|
||||||
|
pre_ema24 = getattr(pre, 'ema24', None)
|
||||||
|
pre_ema52 = getattr(pre, 'ema52', None)
|
||||||
|
if ema24_valid and ema52_valid and pre_ema24 not in (None, 0) and pre_ema52 not in (None, 0):
|
||||||
|
spread_now = ema24 - ema52
|
||||||
|
spread_pre = pre_ema24 - pre_ema52
|
||||||
|
score += 1 if spread_now >= spread_pre else -1
|
||||||
|
# 3.1) MACD柱体动量趋势:考虑 macdhist 的斜率与过零
|
||||||
|
if hist_ready and pre_hist is not None:
|
||||||
|
# 柱体斜率:上升加分,下降减分
|
||||||
|
if hist > pre_hist:
|
||||||
|
score += 1
|
||||||
|
elif hist < pre_hist:
|
||||||
|
score -= 1
|
||||||
|
# 过零加权:负转正更偏多,正转负更偏空
|
||||||
|
if pre_hist < 0 and hist > 0:
|
||||||
|
score += 1
|
||||||
|
elif pre_hist > 0 and hist < 0:
|
||||||
|
score -= 1
|
||||||
|
# 3.2) EMA52 突破/跌破加权
|
||||||
|
if ema52_valid and price_valid and pre_close is not None and pre_ema52 not in (None, 0):
|
||||||
|
# 看多突破:从均线下方上破且动量配合
|
||||||
|
if pre_close <= pre_ema52 and price > ema52 and (hist is None or pre_hist is None or hist >= pre_hist):
|
||||||
|
score += 1
|
||||||
|
# 看空跌破:从均线上方下破且动量配合
|
||||||
|
if pre_close >= pre_ema52 and price < ema52 and (hist is None or pre_hist is None or hist <= pre_hist):
|
||||||
|
score -= 1
|
||||||
|
# 3.3) EMA52 支撑/阻力触碰(非强穿越)
|
||||||
|
if ema52_valid and price_valid:
|
||||||
|
low_v = getattr(klc, 'low', None)
|
||||||
|
high_v = getattr(klc, 'high', None)
|
||||||
|
if low_v is not None and high_v is not None and ema52 not in (None, 0):
|
||||||
|
# 触碰容差(相对EMA52的0.15%)
|
||||||
|
touch_tol = 0.0015
|
||||||
|
# 作为支撑:收盘在上,最低靠近EMA52
|
||||||
|
near_support_touch = (price > ema52) and (abs(low_v - ema52) / abs(ema52) <= touch_tol)
|
||||||
|
# 作为阻力:收盘在下,最高靠近EMA52
|
||||||
|
near_resistance_touch = (price < ema52) and (abs(high_v - ema52) / abs(ema52) <= touch_tol)
|
||||||
|
if near_support_touch:
|
||||||
|
# 若动量不弱,则更偏多
|
||||||
|
score += 1 if (hist is None or pre_hist is None or hist >= pre_hist) else 0
|
||||||
|
if near_resistance_touch:
|
||||||
|
# 若动量不强,则更偏空
|
||||||
|
score -= 1 if (hist is None or pre_hist is None or hist <= pre_hist) else 0
|
||||||
|
# 3.4) 多次对 EMA52 的"拒绝"配合 MACD 逆向:易形成压/支并反向
|
||||||
|
# 统计近窗口内的上/下拒绝次数:
|
||||||
|
# - 上拒绝:价格位于 EMA52 下方,最高触及/越过 EMA52 但收盘仍在下方
|
||||||
|
# - 下拒绝:价格位于 EMA52 上方,最低触及/跌破 EMA52 但收盘仍在上方
|
||||||
|
recent_up_rejects = 0
|
||||||
|
recent_down_rejects = 0
|
||||||
|
if ema52_valid:
|
||||||
|
window_rej = prev_klcs[-lookback_n:] if len(prev_klcs) > 0 else []
|
||||||
|
rej_tol = 0.0015
|
||||||
|
for wk in window_rej:
|
||||||
|
wk_close = getattr(wk, 'close', None)
|
||||||
|
wk_ema52 = getattr(wk, 'ema52', None)
|
||||||
|
wk_high = getattr(wk, 'high', None)
|
||||||
|
wk_low = getattr(wk, 'low', None)
|
||||||
|
if wk_close is None or wk_ema52 in (None, 0):
|
||||||
|
continue
|
||||||
|
# 上拒绝(阻力):下方多次试图上破但未站上
|
||||||
|
if wk_close < wk_ema52 and wk_high is not None:
|
||||||
|
if wk_high >= wk_ema52 or abs(wk_high - wk_ema52) / abs(wk_ema52) <= rej_tol:
|
||||||
|
recent_up_rejects += 1
|
||||||
|
# 下拒绝(支撑):上方多次试图下破但未跌破
|
||||||
|
if wk_close > wk_ema52 and wk_low is not None:
|
||||||
|
if wk_low <= wk_ema52 or abs(wk_low - wk_ema52) / abs(wk_ema52) <= rej_tol:
|
||||||
|
recent_down_rejects += 1
|
||||||
|
# 定义 MACD 的方向偏好
|
||||||
|
macd_bias_up = macd_ready and (macd >= signal) and (not hist_ready or pre_hist is None or hist >= pre_hist)
|
||||||
|
macd_bias_down = macd_ready and (macd <= signal) and (not hist_ready or pre_hist is None or hist <= pre_hist)
|
||||||
|
# 若多次上拒绝且 MACD 偏空,则更偏向下行;若多次下拒绝且 MACD 偏多,则更偏向上行
|
||||||
|
if recent_up_rejects >= 2 and macd_bias_down:
|
||||||
|
score -= 2
|
||||||
|
if recent_down_rejects >= 2 and macd_bias_up:
|
||||||
|
score += 2
|
||||||
|
# 4) RSI 辅助
|
||||||
|
if rsi is not None:
|
||||||
|
if rsi >= 55:
|
||||||
|
score += 1
|
||||||
|
elif rsi <= 45:
|
||||||
|
score -= 1
|
||||||
|
# 5) 指标未就绪回退(EMA/MACD缺失时,用动量与RSI辅助,延续趋势)
|
||||||
|
has_full_ind = ema24_valid and ema52_valid and not (macd == 0 and signal == 0 and hist == 0)
|
||||||
|
if not has_full_ind:
|
||||||
|
# 仅根据价动量/RSI做轻量判断,默认延续 last_trend,除非出现强反向
|
||||||
|
strong_up = False
|
||||||
|
strong_down = False
|
||||||
|
pre = getattr(klc, 'pre', None)
|
||||||
|
if pre:
|
||||||
|
pre_close = getattr(pre, 'close', None)
|
||||||
|
if price_valid and pre_close is not None:
|
||||||
|
strong_up = (price >= pre_close)
|
||||||
|
strong_down = (price < pre_close)
|
||||||
|
if rsi is not None:
|
||||||
|
if rsi >= 60:
|
||||||
|
strong_up = True
|
||||||
|
elif rsi <= 40:
|
||||||
|
strong_down = True
|
||||||
|
if last_trend == Chan_PRICE_TREND.UP and not strong_down:
|
||||||
|
trend = Chan_PRICE_TREND.UP
|
||||||
|
elif last_trend == Chan_PRICE_TREND.DOWN and not strong_up:
|
||||||
|
trend = Chan_PRICE_TREND.DOWN
|
||||||
|
else:
|
||||||
|
trend = Chan_PRICE_TREND.UP if strong_up and not strong_down else (Chan_PRICE_TREND.DOWN if strong_down and not strong_up else Chan_PRICE_TREND.FLAT)
|
||||||
|
else:
|
||||||
|
# 6) 震荡过滤(仅当极近EMA52且MACD贴合时判作震荡)
|
||||||
|
near_flat = False
|
||||||
|
if price_valid and ema52_valid:
|
||||||
|
near_ema52 = abs(price - ema52) / abs(ema52) <= 0.0005 # 0.05%
|
||||||
|
if macd_ready:
|
||||||
|
macd_scale = max(abs(macd), abs(signal), 1e-6)
|
||||||
|
near_macd = abs(macd - signal) / macd_scale <= 0.05
|
||||||
|
else:
|
||||||
|
near_macd = False
|
||||||
|
near_flat = near_ema52 and near_macd
|
||||||
|
# 7) 动态阈值 + 趋势记忆(更强粘滞:趋势中容忍小幅反分)
|
||||||
|
# 引入过去 N 根KLC 的趋势延续性来动态调整翻转阈值,并结合 EMA52 支撑/阻力触碰强化门槛
|
||||||
|
force_flip_down = False
|
||||||
|
force_flip_up = False
|
||||||
|
if near_flat:
|
||||||
|
trend = Chan_PRICE_TREND.FLAT
|
||||||
|
else:
|
||||||
|
# 计算过去窗口的趋势一致性
|
||||||
|
window = prev_klcs[-lookback_n:] if len(prev_klcs) > 0 else []
|
||||||
|
persist_up = 0
|
||||||
|
persist_down = 0
|
||||||
|
for wk in window:
|
||||||
|
if getattr(wk, 'trend', None) == Chan_PRICE_TREND.UP:
|
||||||
|
persist_up += 1
|
||||||
|
elif getattr(wk, 'trend', None) == Chan_PRICE_TREND.DOWN:
|
||||||
|
persist_down += 1
|
||||||
|
persist_ratio_up = (persist_up / len(window)) if len(window) > 0 else 0
|
||||||
|
persist_ratio_down = (persist_down / len(window)) if len(window) > 0 else 0
|
||||||
|
# 基准阈值
|
||||||
|
down_flip_threshold = -2
|
||||||
|
up_flip_threshold = 2
|
||||||
|
# 若最近多为UP,则从UP翻转需更强反向信号;同理对DOWN
|
||||||
|
if last_trend == Chan_PRICE_TREND.UP and persist_ratio_up >= 0.6:
|
||||||
|
down_flip_threshold = -3
|
||||||
|
elif last_trend == Chan_PRICE_TREND.DOWN and persist_ratio_down >= 0.6:
|
||||||
|
up_flip_threshold = 3
|
||||||
|
# EMA52 触碰强化门槛:UP时若出现支撑触碰,下翻更难;DOWN时若出现阻力触碰,上翻更难
|
||||||
|
if ema52_valid and price_valid:
|
||||||
|
low_v = getattr(klc, 'low', None)
|
||||||
|
high_v = getattr(klc, 'high', None)
|
||||||
|
if low_v is not None and high_v is not None and ema52 not in (None, 0):
|
||||||
|
touch_tol = 0.0015
|
||||||
|
near_support_touch = (price > ema52) and (abs(low_v - ema52) / abs(ema52) <= touch_tol)
|
||||||
|
near_resistance_touch = (price < ema52) and (abs(high_v - ema52) / abs(ema52) <= touch_tol)
|
||||||
|
if last_trend == Chan_PRICE_TREND.UP and near_support_touch:
|
||||||
|
# 强化维持UP:进一步降低向下翻转阈值
|
||||||
|
down_flip_threshold = min(down_flip_threshold - 1, -3)
|
||||||
|
if last_trend == Chan_PRICE_TREND.DOWN and near_resistance_touch:
|
||||||
|
# 强化维持DOWN:进一步提高向上翻转阈值
|
||||||
|
up_flip_threshold = max(up_flip_threshold + 1, 3)
|
||||||
|
# 7.1) 复合拐头信号:MACD/Signal 同向拐头 + hist 连续减弱 + 多次未能越过 EMA52
|
||||||
|
pre_macd = getattr(pre, 'macd', None) if pre else None
|
||||||
|
pre_signal = getattr(pre, 'signal', None) if pre else None
|
||||||
|
macd_slope = (macd - pre_macd) if (macd_ready and pre_macd is not None) else 0
|
||||||
|
signal_slope = (signal - pre_signal) if (macd_ready and pre_signal is not None) else 0
|
||||||
|
# hist 连续减弱(绝对值缩小)
|
||||||
|
hist_seq = []
|
||||||
|
for wk in prev_klcs[-2:]:
|
||||||
|
val = getattr(wk, 'macdhist', None)
|
||||||
|
if val is not None:
|
||||||
|
hist_seq.append(val)
|
||||||
|
if hist is not None:
|
||||||
|
hist_seq.append(hist)
|
||||||
|
weaken_steps = 0
|
||||||
|
for i in range(1, len(hist_seq)):
|
||||||
|
if abs(hist_seq[i]) < abs(hist_seq[i-1]):
|
||||||
|
weaken_steps += 1
|
||||||
|
# 近窗口对 EMA52 的"未能站上/跌破"统计(放宽窗口与条件)
|
||||||
|
window_ema = prev_klcs[-4:] if len(prev_klcs) > 0 else []
|
||||||
|
no_up_break = False
|
||||||
|
no_down_break = False
|
||||||
|
if ema52_valid:
|
||||||
|
# 未能有效上破:最近若干根收盘大多数不在 EMA52 上方,且高点多次触及/接近
|
||||||
|
cnt_touch_up = 0
|
||||||
|
cnt_close_above = 0
|
||||||
|
for wk in window_ema:
|
||||||
|
wk_close = getattr(wk, 'close', None)
|
||||||
|
wk_high = getattr(wk, 'high', None)
|
||||||
|
wk_ema = getattr(wk, 'ema52', None)
|
||||||
|
if wk_close is not None and wk_ema not in (None, 0):
|
||||||
|
if wk_close > wk_ema:
|
||||||
|
cnt_close_above += 1
|
||||||
|
if wk_high is not None and (wk_high >= wk_ema or abs(wk_high - wk_ema) / abs(wk_ema) <= 0.0015):
|
||||||
|
cnt_touch_up += 1
|
||||||
|
no_up_break = (cnt_close_above <= 1 and cnt_touch_up >= 1 and price <= ema52)
|
||||||
|
# 未能有效下破:最近若干根收盘大多数不在 EMA52 下方,且低点多次触及/接近
|
||||||
|
cnt_touch_down = 0
|
||||||
|
cnt_close_below = 0
|
||||||
|
for wk in window_ema:
|
||||||
|
wk_close = getattr(wk, 'close', None)
|
||||||
|
wk_low = getattr(wk, 'low', None)
|
||||||
|
wk_ema = getattr(wk, 'ema52', None)
|
||||||
|
if wk_close is not None and wk_ema not in (None, 0):
|
||||||
|
if wk_close < wk_ema:
|
||||||
|
cnt_close_below += 1
|
||||||
|
if wk_low is not None and (wk_low <= wk_ema or abs(wk_low - wk_ema) / abs(wk_ema) <= 0.0015):
|
||||||
|
cnt_touch_down += 1
|
||||||
|
no_down_break = (cnt_close_below <= 1 and cnt_touch_down >= 1 and price >= ema52)
|
||||||
|
# 若当前为UP趋势,出现明显拐头+hist减弱+未能上破EMA52,则加速看空
|
||||||
|
if last_trend == Chan_PRICE_TREND.UP and macd_slope < 0 and signal_slope < 0 and weaken_steps >= 1 and no_up_break and macd_bias_down:
|
||||||
|
score -= 3
|
||||||
|
down_flip_threshold = max(down_flip_threshold, 0)
|
||||||
|
force_flip_down = True
|
||||||
|
# 若当前为DOWN趋势,出现明显拐头+hist减弱+未能下破EMA52,则加速看多
|
||||||
|
if last_trend == Chan_PRICE_TREND.DOWN and macd_slope > 0 and signal_slope > 0 and weaken_steps >= 1 and no_down_break and macd_bias_up:
|
||||||
|
score += 3
|
||||||
|
up_flip_threshold = min(up_flip_threshold, 0)
|
||||||
|
force_flip_up = True
|
||||||
|
# 多次对 EMA52 的拒绝配合 MACD 逆向:加速反向翻转(降低相反方向阈值)
|
||||||
|
if recent_up_rejects >= 2 and macd_bias_down:
|
||||||
|
# 从 UP 向 DOWN 的翻转更容易
|
||||||
|
down_flip_threshold = max(down_flip_threshold, -1)
|
||||||
|
if recent_down_rejects >= 2 and macd_bias_up:
|
||||||
|
# 从 DOWN 向 UP 的翻转更容易
|
||||||
|
up_flip_threshold = min(up_flip_threshold, 1)
|
||||||
|
if force_flip_down:
|
||||||
|
trend = Chan_PRICE_TREND.DOWN
|
||||||
|
elif force_flip_up:
|
||||||
|
trend = Chan_PRICE_TREND.UP
|
||||||
|
elif last_trend == Chan_PRICE_TREND.UP:
|
||||||
|
if score <= down_flip_threshold:
|
||||||
|
trend = Chan_PRICE_TREND.DOWN
|
||||||
|
else:
|
||||||
|
trend = Chan_PRICE_TREND.UP
|
||||||
|
elif last_trend == Chan_PRICE_TREND.DOWN:
|
||||||
|
if score >= up_flip_threshold:
|
||||||
|
trend = Chan_PRICE_TREND.UP
|
||||||
|
else:
|
||||||
|
trend = Chan_PRICE_TREND.DOWN
|
||||||
|
else:
|
||||||
|
# 初始无记忆时,降低进入门槛
|
||||||
|
if score >= 1:
|
||||||
|
trend = Chan_PRICE_TREND.UP
|
||||||
|
elif score <= -1:
|
||||||
|
trend = Chan_PRICE_TREND.DOWN
|
||||||
|
else:
|
||||||
|
trend = Chan_PRICE_TREND.FLAT
|
||||||
|
except Exception:
|
||||||
|
trend = Chan_PRICE_TREND.UNKNOWN
|
||||||
|
# 写回趋势
|
||||||
|
if klc.end_time is None:
|
||||||
|
trend = Chan_PRICE_TREND.FLAT
|
||||||
|
if hasattr(klc, 'set_trend'):
|
||||||
|
klc.set_trend(trend)
|
||||||
|
else:
|
||||||
|
setattr(klc, 'trend', trend)
|
||||||
|
last_trend = trend
|
||||||
|
# 更新滑窗:仅向后看
|
||||||
|
prev_klcs.append(klc)
|
||||||
|
price_diff = klc.close - klc.pre.close if klc.pre else 0
|
||||||
|
#if klc.index > len(klc_list) - 10:
|
||||||
|
#print(klc.start_time, klc.end_time, klc.close, klc.ema24, klc.ema52, klc.macd, klc.signal, klc.macdhist, klc.trend, price_diff, score)
|
||||||
|
#print(klc.start_time, klc.end_time, klc.trend, price_diff, score)
|
||||||
|
return klc_list
|
||||||
|
|
||||||
|
def get_bi_list(self, dataframe):
|
||||||
|
bi_list = self.cal_bi_list(self.get_klc_list(dataframe))
|
||||||
|
#bi_list = self.cal_bi_list_chanlun(self.get_klc_list(dataframe))
|
||||||
|
return bi_list
|
||||||
|
|
||||||
|
def cal_bi_list(self, klc_list):
|
||||||
|
bi_list = []
|
||||||
|
last_top = None
|
||||||
|
last_bottom = None
|
||||||
|
bi_klc_min = 4
|
||||||
|
last_fx_klc = None
|
||||||
|
for klc in klc_list:
|
||||||
|
if last_fx_klc:
|
||||||
|
klc.check_klc_state(last_fx_klc)
|
||||||
|
klc.check_fx_confirmed(last_top, last_bottom)
|
||||||
|
fx = self.check_fx(klc)
|
||||||
|
if fx == Chan_FX_TYPE.TOP:
|
||||||
|
if last_bottom:
|
||||||
|
if self.check_top_fx(last_bottom, klc) == False:
|
||||||
|
fx = Chan_FX_TYPE.UNKNOWN
|
||||||
|
if fx == Chan_FX_TYPE.BOTTOM:
|
||||||
|
if last_top:
|
||||||
|
if self.check_bottom_fx(last_top, klc) == False:
|
||||||
|
#print(klc.end_time, last_top.end_time, "---")
|
||||||
|
fx = Chan_FX_TYPE.UNKNOWN
|
||||||
|
# Do nothing
|
||||||
|
if fx == Chan_FX_TYPE.UNKNOWN:
|
||||||
|
if len(bi_list) > 0:
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
#continue
|
||||||
|
if len(bi_list) > 0 and klc.end_klu:
|
||||||
|
last_bi = bi_list[-1]
|
||||||
|
#print(klc.start_time, last_bi.start_time, last_bi.end_time, last_bi.dir, last_bi.high, last_bi.low, last_bottom.end_time, "last bi")
|
||||||
|
if last_top and last_bi.dir == Chan_BI_DIR.DOWN:
|
||||||
|
if last_bottom and klc.high > last_bi.high:
|
||||||
|
#print(klc.end_time, "Top 7, 1", last_bi.start_time, klc.high, last_bi.high)
|
||||||
|
#klc.klc_fx_type = Chan_KLC_FX.TOP7
|
||||||
|
#klc.fx = Chan_FX_TYPE.TOP
|
||||||
|
"""
|
||||||
|
last_bi.set_end_klc(last_bottom, klc)
|
||||||
|
bi = ChanBI(last_bottom, len(bi_list), Chan_BI_DIR.UP)
|
||||||
|
#klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM7)
|
||||||
|
#klc.bb_out = True
|
||||||
|
last_bi.set_next(bi)
|
||||||
|
bi.set_pre(last_bi)
|
||||||
|
for klc_index in range(last_bi.end_klc.index, len(klc_list)):
|
||||||
|
bi.add_klc(klc_list[klc_index])
|
||||||
|
bi_list.append(bi)
|
||||||
|
last_top = klc
|
||||||
|
klc.set_bi(bi)
|
||||||
|
#print(klc.start_time, bi.start_time, bi.end_time, bi.dir, bi.high, bi.low, bi.is_sure)
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
if last_bottom and last_bi.dir == Chan_BI_DIR.UP:
|
||||||
|
if last_top and klc.low < last_bi.low:
|
||||||
|
#print(klc.end_time, "Bottom 8, 2", last_bi.start_time)
|
||||||
|
#klc.klc_fx_type = Chan_KLC_FX.BOTTOM8
|
||||||
|
#klc.fx = Chan_FX_TYPE.BOTTOM
|
||||||
|
"""
|
||||||
|
last_bi.set_end_klc(last_top, klc)
|
||||||
|
bi = ChanBI(last_top, len(bi_list), Chan_BI_DIR.DOWN)
|
||||||
|
#klc.set_klc_fx_type(Chan_KLC_FX.TOP6)
|
||||||
|
#klc.bb_out = True
|
||||||
|
last_bi.set_next(bi)
|
||||||
|
bi.set_pre(last_bi)
|
||||||
|
for klc_index in range(last_bi.end_klc.index, len(klc_list)):
|
||||||
|
bi.add_klc(klc_list[klc_index])
|
||||||
|
bi_list.append(bi)
|
||||||
|
last_bottom = klc
|
||||||
|
klc.set_bi(bi)
|
||||||
|
#print(klc.start_time, bi.start_time, bi.end_time, bi.dir, bi.high, bi.low, bi.is_sure)
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
last_fx_klc = klc
|
||||||
|
if fx == Chan_FX_TYPE.TOP:
|
||||||
|
#print(klc.end_time, fx, klc.pre.high, klc.high, klc.pre.start_time, klc.pre.end_time)
|
||||||
|
if last_top:
|
||||||
|
if last_bottom:
|
||||||
|
#print(klc.start_time, last_bottom.start_time, last_top.start_time)
|
||||||
|
if last_bottom.index < last_top.index:
|
||||||
|
# Second top lower to be second sell point
|
||||||
|
if last_top.high > klc.high:
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
#klc.set_klc_fx_type(Chan_KLC_FX.TOP3)
|
||||||
|
#print(klc.end_time, klc.fx, "二类卖点Sell 1")
|
||||||
|
else:
|
||||||
|
# A new top found
|
||||||
|
#last_top.set_fx(Chan_FX_TYPE.UNKNOWN)
|
||||||
|
last_top = klc
|
||||||
|
#print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 1")
|
||||||
|
klc.set_klc_fx_type(Chan_KLC_FX.TOP1)
|
||||||
|
self.check_fx_pattern(klc)
|
||||||
|
#print(klc.end_time, klc.fx, "一类卖点Sell 1")
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
# 不满足结合律的分型
|
||||||
|
else:
|
||||||
|
#klc.set_klc_fx_type(Chan_KLC_FX.TOP0)
|
||||||
|
#print(klc.end_time, klc.klc_fx_type)
|
||||||
|
if last_bottom.index + bi_klc_min > klc.index:
|
||||||
|
if last_top.high > klc.high:
|
||||||
|
#print(klc.start_time, klc.fx, "二类卖点Sell 1")
|
||||||
|
#klc.set_klc_fx_type(Chan_KLC_FX.TOP8)
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
# New TOP Found前面的UKNOWN可能出现TOP7,但是这里的也可能出现TOP8分型
|
||||||
|
else:
|
||||||
|
# 顶分型在出现2之前超过前一个笔的顶 TOP8
|
||||||
|
if last_top.index + bi_klc_min < klc.index and len(bi_list) > 1:
|
||||||
|
pre_last_bi = bi_list[-2]
|
||||||
|
last_bi = bi_list[-1]
|
||||||
|
if pre_last_bi.is_sure and not last_bi.is_sure and pre_last_bi.dir == Chan_BI_DIR.UP and False:
|
||||||
|
pre_last_bi.update_bi(klc)
|
||||||
|
bi_list.remove(last_bi)
|
||||||
|
pre_last_bi.set_next(None)
|
||||||
|
#last_top.set_fx(Chan_FX_TYPE.PTOP)
|
||||||
|
last_top = klc
|
||||||
|
last_bottom = pre_last_bi.start_klc
|
||||||
|
#print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Bottom Change 1")
|
||||||
|
klc.set_klc_fx_type(Chan_KLC_FX.TOP2)
|
||||||
|
#print(klc.start_time, last_bi.start_klc.start_time, "New TOP Found reset last bi")
|
||||||
|
#klc.set_state("10")
|
||||||
|
#print(klc.start_time, klc.fx, "笔卖点Sell 1")
|
||||||
|
###klc.set_klc_fx_type(Chan_KLC_FX.TOP2) # when bi is down but the fx is top
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
#klc.set_klc_fx_type(Chan_KLC_FX.TOP8)
|
||||||
|
#print(klc.start_time, last_bi.start_klc.start_time, "New TOP Found reset last bi")
|
||||||
|
else:
|
||||||
|
#klc.set_fx(Chan_FX_TYPE.PTOP)
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
#print(klc.end_time, klc.fx, "无效顶分型")
|
||||||
|
# 满足结合律
|
||||||
|
else:
|
||||||
|
# New Temp TOP and last bottom confirmed ***** confirm last down bi(last bottom and last top)
|
||||||
|
last_bi = bi_list[-1]
|
||||||
|
if not last_bi.is_sure:
|
||||||
|
last_bi.set_end_klc(last_bottom, klc)
|
||||||
|
bi = ChanBI(last_bottom, len(bi_list), Chan_BI_DIR.UP)
|
||||||
|
last_bi.set_next(bi)
|
||||||
|
bi.set_pre(last_bi)
|
||||||
|
bi.add_klc(klc)
|
||||||
|
bi_list.append(bi)
|
||||||
|
last_top = klc
|
||||||
|
#print(klc.end_time, klc.fx, bi_list[-1].dir, "Last Top Change 2")
|
||||||
|
klc.set_klc_fx_type(Chan_KLC_FX.TOP2)
|
||||||
|
self.check_fx_pattern(klc)
|
||||||
|
#bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
#print(klc.start_time, last_bottom.start_time, "Normal TOP Found, Confirm down bi 4")
|
||||||
|
# last bottom = None 初始化的时候用,其他时间不用
|
||||||
|
else:
|
||||||
|
# 初始化的时候用,其他时间不用
|
||||||
|
if last_top.high < klc.high:
|
||||||
|
last_bi = bi_list[-1]
|
||||||
|
last_bi.set_start_klc(klc, Chan_BI_DIR.DOWN)
|
||||||
|
last_top = klc
|
||||||
|
#print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 3")
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
# 初始化的时候用,其他时间不用
|
||||||
|
else:
|
||||||
|
#klc.set_fx(Chan_FX_TYPE.TT)
|
||||||
|
#print(klc.start_time, klc.fx, "二类卖点Sell 2")
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
# last_top == None 初始化的时候用,其他时间不用
|
||||||
|
else:
|
||||||
|
if last_bottom:
|
||||||
|
# 不满足结合律的分型
|
||||||
|
if last_bottom.index + bi_klc_min > klc.index:
|
||||||
|
#klc.set_fx(Chan_FX_TYPE.PTOP)
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
#print(klc.start_time, klc.fx, "中枢卖点Sell 1")
|
||||||
|
else:
|
||||||
|
# First temp top and last bottom confirmed
|
||||||
|
last_top = klc
|
||||||
|
#print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 4")
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
# Last top = None, last bottom = None, create first down bi 初始化的时候用,其他时间不用
|
||||||
|
else:
|
||||||
|
# First temp top
|
||||||
|
last_top = klc
|
||||||
|
bi = ChanBI(klc, len(bi_list), Chan_BI_DIR.DOWN)
|
||||||
|
bi_list.append(bi)
|
||||||
|
bi.add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
#print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 5")
|
||||||
|
#klc.fx = Bottom ========================
|
||||||
|
else:
|
||||||
|
if last_bottom:
|
||||||
|
if last_top:
|
||||||
|
# Bottom after top and find a new bottom
|
||||||
|
if last_top.index < last_bottom.index:
|
||||||
|
# Second bottom uppper to be second buy point and confirm last bi
|
||||||
|
if last_bottom.low < klc.low:
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
#klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM3)
|
||||||
|
#print(last_bottom.start_time, last_bottom.end_time, "--------------------------------1")
|
||||||
|
#print(klc.end_time, klc.fx, "二类买点Buy 1")
|
||||||
|
else:
|
||||||
|
# A new bottom found
|
||||||
|
last_bottom = klc
|
||||||
|
#print(klc.end_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 1")
|
||||||
|
klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM1)
|
||||||
|
self.check_fx_pattern(klc)
|
||||||
|
#print(klc.end_time, klc.fx, "一类买点Buy 1")
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
# 不满足结合律的分型
|
||||||
|
else:
|
||||||
|
#klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM0)
|
||||||
|
#print(klc.end_time, klc.klc_fx_type)
|
||||||
|
if last_top.index + bi_klc_min > klc.index:
|
||||||
|
if last_bottom.low < klc.low:
|
||||||
|
#print(klc.end_time, klc.fx, "中枢买点Buy 1")
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
#klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM8)
|
||||||
|
# Found new bottom没有意义,上面UNKNOWN的时候已经是笔破坏了
|
||||||
|
else:
|
||||||
|
#print(klc.end_time, last_bottom.end_time, "Found a new bottom")
|
||||||
|
if last_bottom.index + bi_klc_min < klc.index and len(bi_list) > 1:
|
||||||
|
pre_last_bi = bi_list[-2]
|
||||||
|
last_bi = bi_list[-1]
|
||||||
|
if pre_last_bi.is_sure and not last_bi.is_sure and pre_last_bi.dir == Chan_BI_DIR.DOWN and False:
|
||||||
|
pre_last_bi.update_bi(klc)
|
||||||
|
bi_list.remove(last_bi)
|
||||||
|
pre_last_bi.set_next(None)
|
||||||
|
last_bottom = klc
|
||||||
|
last_top = pre_last_bi.start_klc
|
||||||
|
#print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Bottom Change 2")
|
||||||
|
klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM2)
|
||||||
|
#print(klc.start_time, last_bi.start_klc.start_time, "New BOTTOM Found reset last bi")
|
||||||
|
#print(klc.start_time, klc.fx, "笔买点Buy 1")
|
||||||
|
###klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM2) # when bi is up but the fx is bottom
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
#klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM8)
|
||||||
|
else:
|
||||||
|
#klc.set_fx(Chan_FX_TYPE.UNKNOWN)
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
print(klc.end_time, klc.fx, "无效底分型")
|
||||||
|
# 满足结合律的分型
|
||||||
|
else:
|
||||||
|
# New Temp Bottom and last top confirmed ***** confirm last up bi(last bottom and last top)
|
||||||
|
last_bi = bi_list[-1]
|
||||||
|
if not last_bi.is_sure:
|
||||||
|
last_bi.set_end_klc(last_top, klc)
|
||||||
|
bi = ChanBI(last_top, len(bi_list), Chan_BI_DIR.DOWN)
|
||||||
|
last_bi.set_next(bi)
|
||||||
|
bi.set_pre(last_bi)
|
||||||
|
bi.add_klc(klc)
|
||||||
|
bi_list.append(bi)
|
||||||
|
last_bottom = klc
|
||||||
|
#print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 2")
|
||||||
|
klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM2)
|
||||||
|
self.check_fx_pattern(klc)
|
||||||
|
#bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
#print(klc.start_time, last_top.start_time, "Normal Bottom Found, Confirm up bi 6")
|
||||||
|
# last_top = None 初始化的时候用,其他时间不用
|
||||||
|
else:
|
||||||
|
if last_bottom.low > klc.low:
|
||||||
|
last_bi = bi_list[-1]
|
||||||
|
last_bi.set_start_klc(klc, Chan_BI_DIR.UP)
|
||||||
|
#last_bottom.set_fx(Chan_FX_TYPE.UNKNOWN)
|
||||||
|
last_bottom = klc
|
||||||
|
#print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 3")
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
#print(klc.start_time, klc.fx, "笔买点Buy 3")
|
||||||
|
else:
|
||||||
|
#klc.set_fx(Chan_FX_TYPE.BB)
|
||||||
|
#klc.set_state('-20')
|
||||||
|
#print(klc.start_time, klc.fx, "二类买点Buy 2")
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
# last_bottom = None 初始化的时候用,其他时间不用
|
||||||
|
else:
|
||||||
|
if last_top:
|
||||||
|
# 不满足结合律的分型
|
||||||
|
if last_top.index + bi_klc_min > klc.index:
|
||||||
|
#klc.set_fx(Chan_FX_TYPE.PBOTTOM)
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
#print(klc.start_time, klc.fx, "中枢买点Buy 1")
|
||||||
|
else:
|
||||||
|
# First temp bottom and last top confirmed
|
||||||
|
last_bottom = klc
|
||||||
|
#print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 4")
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
#print(klc.start_time, klc.fx, "一类买点Buy 1")
|
||||||
|
# Last top = None, last bottom = None, create first up bi
|
||||||
|
else:
|
||||||
|
# First temp bottom and no top yet
|
||||||
|
last_bottom = klc
|
||||||
|
bi = ChanBI(klc, len(bi_list), Chan_BI_DIR.UP)
|
||||||
|
#klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM7)
|
||||||
|
bi_list.append(bi)
|
||||||
|
bi_list[-1].add_klc(klc)
|
||||||
|
klc.set_bi(bi_list[-1])
|
||||||
|
#print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 5")
|
||||||
|
#print(klc.start_time, klc.fx, "笔买点Buy 4")
|
||||||
|
self.get_above_zero_bsp(klc_list)
|
||||||
|
#print(bi_list[-1].start_time, bi_list[-1].end_time, len(bi_list[-1].klc_list))
|
||||||
|
return bi_list
|
||||||
|
|
||||||
|
def check_top_fx(self, last_bottom, klc):
|
||||||
|
if (last_bottom.high > klc.pre.low or last_bottom.high > klc.next.low) and (klc.index - last_bottom.index < 100):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_bottom_fx(self, last_top, klc):
|
||||||
|
if (last_top.low < klc.pre.high or last_top.low < klc.next.high) and (klc.index - last_top.index < 100):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
# 线段内的中枢
|
||||||
@@ -0,0 +1,412 @@
|
|||||||
|
"""TF_DF builder mixin — 由 split_tfdf_builders 自动生成,逻辑与原 TF_DF 一致。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
from technical.util import resample_to_interval
|
||||||
|
|
||||||
|
from chanlun.core.ChanBI import ChanBI
|
||||||
|
from chanlun.core.ChanBIZS import ChanBIZS
|
||||||
|
from chanlun.core.ChanBSP import ChanBSP
|
||||||
|
from chanlun.core.ChanEnum import (
|
||||||
|
Chan_BI_DIR,
|
||||||
|
Chan_BSP_DIR,
|
||||||
|
Chan_BSP_TYPE,
|
||||||
|
Chan_FX_TYPE,
|
||||||
|
Chan_K_DIR,
|
||||||
|
Chan_KLC_FX,
|
||||||
|
Chan_KLC_STATE,
|
||||||
|
Chan_KLINE_DIR,
|
||||||
|
Chan_KLU_PATTERN,
|
||||||
|
Chan_PRICE_TREND,
|
||||||
|
Chan_SEG_DIR,
|
||||||
|
Chan_ZS_DIR,
|
||||||
|
)
|
||||||
|
from chanlun.core.ChanKLC import ChanKLC
|
||||||
|
from chanlun.core.ChanKLU import ChanKLU
|
||||||
|
from chanlun.core.ChanSBI import ChanSBI
|
||||||
|
from chanlun.core.ChanSEG import ChanSEG
|
||||||
|
from chanlun.core.ChanZS import ChanZS, ChanZS_Big
|
||||||
|
from chanlun.indicators.ChanMACD import ChanMACD
|
||||||
|
|
||||||
|
|
||||||
|
class BspBuilderMixin:
|
||||||
|
def get_bsp_state(self, dataframe):
|
||||||
|
klu_list = self.get_klu_list(dataframe)
|
||||||
|
klc_list = self.get_klc_list(klu_list)
|
||||||
|
bi_list = self.cal_bi_list(klc_list)
|
||||||
|
seg_list = self.get_seg_list(bi_list)
|
||||||
|
bi_zs_list = self.cal_bi_zs(seg_list)
|
||||||
|
bsp_list = self.find_all_bsp(bi_list, bi_zs_list)
|
||||||
|
bsp_state_list = [0] * len(dataframe)
|
||||||
|
klc_index = 0
|
||||||
|
for index in range(0, len(dataframe)):
|
||||||
|
if klc_index == len(klc_list):
|
||||||
|
klc_index = len(klc_list) - 1
|
||||||
|
klc = klc_list[klc_index]
|
||||||
|
if klc.end_klu and klc.end_klu.idx == index:
|
||||||
|
if klc.klc_fx_type == Chan_KLC_FX.TOP2:
|
||||||
|
bi = klc.bi.pre
|
||||||
|
if bi and bi.is_sure and bi.end_klc.bsp_type == Chan_BSP_TYPE.B3:
|
||||||
|
# 第三类买点
|
||||||
|
bsp_state_list[index] = -1
|
||||||
|
#print(klc.end_time, "B3")
|
||||||
|
else:
|
||||||
|
bsp_state_list[index] = 0
|
||||||
|
elif klc.klc_fx_type == Chan_KLC_FX.BOTTOM2:
|
||||||
|
bi = klc.bi.pre
|
||||||
|
if bi and bi.is_sure and bi.end_klc.bsp_type == Chan_BSP_TYPE.S3:
|
||||||
|
# 第三类卖点
|
||||||
|
bsp_state_list[index] = 1
|
||||||
|
#print(klc.end_time, "S3")
|
||||||
|
else:
|
||||||
|
bsp_state_list[index] = 0
|
||||||
|
klc_index += 1
|
||||||
|
else:
|
||||||
|
bsp_state_list[index] = 0
|
||||||
|
return bsp_state_list
|
||||||
|
|
||||||
|
def get_above_zero_bsp(self, klc_list):
|
||||||
|
buy_bsp_list = []
|
||||||
|
sell_bsp_list = []
|
||||||
|
above_zero = False
|
||||||
|
buy_bsp = None
|
||||||
|
sell_bsp = None
|
||||||
|
for klc in klc_list:
|
||||||
|
if klc.pre and klc.pre.signal < 0 and klc.signal > 0:
|
||||||
|
above_zero = True
|
||||||
|
if klc.pre and klc.pre.signal > 0 and klc.signal < 0:
|
||||||
|
above_zero = False
|
||||||
|
if above_zero and klc.klc_fx_type == Chan_KLC_FX.BOTTOM2 and klc.macd > 0:
|
||||||
|
buy_bsp = klc
|
||||||
|
buy_bsp_list.append(klc)
|
||||||
|
#print(klc.end_time, "MACD 0轴上穿,回调笔底分型做多")
|
||||||
|
if buy_bsp and klc.pre and klc.pre.macdhist > 0 and klc.macdhist < 0:
|
||||||
|
sell_bsp = klc
|
||||||
|
sell_bsp_list.append(klc)
|
||||||
|
buy_bsp = None
|
||||||
|
#print(klc.end_time, "Sell BSP Found")
|
||||||
|
return buy_bsp_list
|
||||||
|
|
||||||
|
def find_all_bsp(self, bi_list, bi_zs_list):
|
||||||
|
"""
|
||||||
|
笔中枢的三类买卖点识别
|
||||||
|
|
||||||
|
三类买点:中枢形成后,一笔向上离开中枢(低点 > zg),
|
||||||
|
随后回拉的一笔低点不跌回中枢(低点 >= zg),确认支撑有效。
|
||||||
|
三类卖点:中枢形成后,一笔向下离开中枢(高点 < zd),
|
||||||
|
随后反弹的一笔高点不回到中枢(高点 <= zd),确认压力有效。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
bi_list: 笔列表
|
||||||
|
bi_zs_list: 笔中枢列表(二维列表,每个seg内的中枢列表)
|
||||||
|
|
||||||
|
返回:
|
||||||
|
bsp_list: ChanBSP 列表,包含所有识别到的三类买卖点
|
||||||
|
"""
|
||||||
|
bsp_list = []
|
||||||
|
if len(bi_list) < 4 or len(bi_zs_list) == 0:
|
||||||
|
return bsp_list
|
||||||
|
|
||||||
|
for zs in bi_zs_list:
|
||||||
|
if not zs.is_sure or len(zs.bi_list) < 3:
|
||||||
|
continue
|
||||||
|
#print(zs.start_time, zs.end_time, zs.dir, zs.is_sure, len(zs.bi_list))
|
||||||
|
# 中枢结束后的第一笔(离开笔)
|
||||||
|
last_zs_bi = zs.bi_list[-1]
|
||||||
|
if last_zs_bi.dir == Chan_BI_DIR.UP:
|
||||||
|
if last_zs_bi.is_sure and last_zs_bi.end_klc.high <= zs.zg or (last_zs_bi.next and last_zs_bi.next.is_sure and last_zs_bi.next.end_klc.low < zs.zd):
|
||||||
|
leave_bi = last_zs_bi.next
|
||||||
|
else:
|
||||||
|
leave_bi = last_zs_bi
|
||||||
|
else:
|
||||||
|
if last_zs_bi.is_sure and last_zs_bi.end_klc.low >= zs.zd or (last_zs_bi.next and last_zs_bi.next.is_sure and last_zs_bi.next.end_klc.high > zs.zg):
|
||||||
|
leave_bi = last_zs_bi.next
|
||||||
|
else:
|
||||||
|
leave_bi = last_zs_bi
|
||||||
|
#print(zs.zg, zs.zd)
|
||||||
|
if leave_bi is None or not leave_bi.is_sure:
|
||||||
|
continue
|
||||||
|
if (zs.dir == Chan_ZS_DIR.UP and leave_bi.dir == Chan_BI_DIR.UP and leave_bi.end_klc.high < zs.zg and leave_bi.end_klc.high > zs.zd) or (zs.dir == Chan_ZS_DIR.DOWN and leave_bi.dir == Chan_BI_DIR.DOWN and leave_bi.end_klc.low < zs.zg and leave_bi.end_klc.low > zs.zd):
|
||||||
|
#print("--------------------", leave_bi.dir, leave_bi.end_klc.high, leave_bi.end_klc.low, zs.zg, zs.zd)
|
||||||
|
leave_bi = leave_bi.next
|
||||||
|
# 三类买点:向上离开中枢后回拉不破 zg
|
||||||
|
#print("Leave bi:", leave_bi.start_time, leave_bi.end_time, leave_bi.dir, leave_bi.is_sure, leave_bi.low, leave_bi.high)
|
||||||
|
if leave_bi.dir == Chan_BI_DIR.UP:
|
||||||
|
first_bsp_bi_div = self.check_bi_div(zs, leave_bi)
|
||||||
|
# 确认一类卖点:离开断能量小于进入段能量
|
||||||
|
if first_bsp_bi_div:
|
||||||
|
bsp = ChanBSP(
|
||||||
|
leave_bi, len(bsp_list),
|
||||||
|
Chan_BSP_TYPE.S1,
|
||||||
|
Chan_BSP_DIR.SELL,
|
||||||
|
leave_bi.sure_time,
|
||||||
|
zs.index+1, zs, None
|
||||||
|
)
|
||||||
|
leave_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.S1)
|
||||||
|
bsp_list.append(bsp)
|
||||||
|
# 回拉笔
|
||||||
|
pullback_bi = leave_bi.next
|
||||||
|
#print(pullback_bi.start_klc.start_time, pullback_bi.dir, pullback_bi.is_sure, pullback_bi.low, pullback_bi.high)
|
||||||
|
if pullback_bi and pullback_bi.is_sure and pullback_bi.dir == Chan_BI_DIR.DOWN:
|
||||||
|
if pullback_bi.low >= zs.zg:
|
||||||
|
# 确认三类买点:回拉笔的低点不跌回中枢
|
||||||
|
bsp = ChanBSP(
|
||||||
|
pullback_bi, len(bsp_list),
|
||||||
|
Chan_BSP_TYPE.B3,
|
||||||
|
Chan_BSP_DIR.BUY,
|
||||||
|
pullback_bi.sure_time,
|
||||||
|
zs.index+1, zs, None
|
||||||
|
)
|
||||||
|
pullback_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B3)
|
||||||
|
bsp_list.append(bsp)
|
||||||
|
# 二类卖点
|
||||||
|
if first_bsp_bi_div:
|
||||||
|
second_bsp_bi = pullback_bi.next
|
||||||
|
if second_bsp_bi and second_bsp_bi.is_sure and second_bsp_bi.end_klc.high < leave_bi.end_klc.high:
|
||||||
|
# 确认二类卖点:一类卖点后回拉不超过一类卖点高点
|
||||||
|
bsp = ChanBSP(
|
||||||
|
second_bsp_bi, len(bsp_list),
|
||||||
|
Chan_BSP_TYPE.S2,
|
||||||
|
Chan_BSP_DIR.SELL,
|
||||||
|
second_bsp_bi.sure_time,
|
||||||
|
zs.index+1, zs, None
|
||||||
|
)
|
||||||
|
second_bsp_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B2)
|
||||||
|
bsp_list.append(bsp)
|
||||||
|
# 三类卖点:向下离开中枢后反弹不破 zd
|
||||||
|
elif leave_bi.dir == Chan_BI_DIR.DOWN:
|
||||||
|
first_bsp_bi_div = self.check_bi_div(zs, leave_bi)
|
||||||
|
# 确认一类买点:离开段能量小于进入段
|
||||||
|
if first_bsp_bi_div:
|
||||||
|
bsp = ChanBSP(
|
||||||
|
leave_bi, len(bsp_list),
|
||||||
|
Chan_BSP_TYPE.B1,
|
||||||
|
Chan_BSP_DIR.BUY,
|
||||||
|
leave_bi.sure_time,
|
||||||
|
zs.index+1, zs, None
|
||||||
|
)
|
||||||
|
leave_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B1)
|
||||||
|
bsp_list.append(bsp)
|
||||||
|
# 反弹笔
|
||||||
|
bounce_bi = leave_bi.next
|
||||||
|
#print(bounce_bi.start_klc.start_time, bounce_bi.dir, bounce_bi.is_sure, bounce_bi.low, bounce_bi.high)
|
||||||
|
if bounce_bi and bounce_bi.is_sure and bounce_bi.dir == Chan_BI_DIR.UP:
|
||||||
|
if bounce_bi.high <= zs.zd:
|
||||||
|
# 确认三类卖点:反弹笔的高点不回到中枢
|
||||||
|
bsp = ChanBSP(
|
||||||
|
bounce_bi, len(bsp_list),
|
||||||
|
Chan_BSP_TYPE.S3,
|
||||||
|
Chan_BSP_DIR.SELL,
|
||||||
|
bounce_bi.sure_time,
|
||||||
|
zs.index+1, zs, None
|
||||||
|
)
|
||||||
|
bounce_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.S3)
|
||||||
|
bsp_list.append(bsp)
|
||||||
|
# 二类卖点
|
||||||
|
if first_bsp_bi_div:
|
||||||
|
second_bsp_bi = bounce_bi.next
|
||||||
|
if second_bsp_bi and second_bsp_bi.is_sure and second_bsp_bi.end_klc.low > leave_bi.end_klc.low:
|
||||||
|
# 确认二类买点:一类买点后回拉不超过一类卖点高点
|
||||||
|
bsp = ChanBSP(
|
||||||
|
second_bsp_bi, len(bsp_list),
|
||||||
|
Chan_BSP_TYPE.B2,
|
||||||
|
Chan_BSP_DIR.BUY,
|
||||||
|
second_bsp_bi.sure_time,
|
||||||
|
zs.index+1, zs, None
|
||||||
|
)
|
||||||
|
second_bsp_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B2)
|
||||||
|
bsp_list.append(bsp)
|
||||||
|
return bsp_list
|
||||||
|
|
||||||
|
def check_bi_div(self, zs, leave_bi):
|
||||||
|
enter_bi = zs.bi_list[0].pre
|
||||||
|
macdhist_div = 0
|
||||||
|
if enter_bi and enter_bi.dir == leave_bi.dir:
|
||||||
|
macdhist_div = abs(leave_bi.macd_hist) - abs(enter_bi.macd_hist)
|
||||||
|
#print(enter_bi.end_time, leave_bi.end_time, macdhist_div < 0)
|
||||||
|
return macdhist_div < 0
|
||||||
|
|
||||||
|
def find_first_bsp(self, bi_list, bi_zs_list):
|
||||||
|
"""
|
||||||
|
笔中枢的一类买卖点识别
|
||||||
|
|
||||||
|
一类买点:下跌趋势中,最后一个中枢完成后,向下离开中枢的笔创新低,
|
||||||
|
但该笔与进入中枢前的最后一笔下跌形成底背驰(力度减弱),
|
||||||
|
即趋势力竭的转折点。
|
||||||
|
一类卖点:上涨趋势中,最后一个中枢完成后,向上离开中枢的笔创新高,
|
||||||
|
但该笔与进入中枢前的最后一笔上涨形成顶背驰(力度减弱),
|
||||||
|
即趋势力竭的转折点。
|
||||||
|
|
||||||
|
简化判断:中枢形成后,离开中枢的笔(突破笔)本身即为一类买卖点的触发笔。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
bi_list: 笔列表
|
||||||
|
bi_zs_list: 笔中枢列表(扁平列表,每个元素是一个中枢对象)
|
||||||
|
|
||||||
|
返回:
|
||||||
|
bsp_list: ChanBSP 列表,包含所有识别到的一类买卖点
|
||||||
|
"""
|
||||||
|
bsp_list = []
|
||||||
|
if len(bi_list) < 4 or len(bi_zs_list) == 0:
|
||||||
|
return bsp_list
|
||||||
|
|
||||||
|
for zs in bi_zs_list:
|
||||||
|
if not zs.is_sure or len(zs.bi_list) < 3:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 找到中枢的最后一笔
|
||||||
|
last_zs_bi = zs.bi_list[-1]
|
||||||
|
|
||||||
|
# 确定离开笔:中枢最后一笔之后的第一笔
|
||||||
|
if last_zs_bi.dir == Chan_BI_DIR.UP:
|
||||||
|
# 中枢最后一笔向上,如果没有真正离开中枢,取下一笔
|
||||||
|
if last_zs_bi.is_sure and last_zs_bi.end_klc.high <= zs.zg:
|
||||||
|
leave_bi = last_zs_bi.next
|
||||||
|
else:
|
||||||
|
leave_bi = last_zs_bi
|
||||||
|
else:
|
||||||
|
# 中枢最后一笔向下,如果没有真正离开中枢,取下一笔
|
||||||
|
if last_zs_bi.is_sure and last_zs_bi.end_klc.low >= zs.zd:
|
||||||
|
leave_bi = last_zs_bi.next
|
||||||
|
else:
|
||||||
|
leave_bi = last_zs_bi
|
||||||
|
|
||||||
|
if leave_bi is None or not leave_bi.is_sure:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 一类买点:向下离开中枢(leave_bi向下,低点 < zd),趋势力竭
|
||||||
|
if leave_bi.dir == Chan_BI_DIR.DOWN and leave_bi.low < zs.zd:
|
||||||
|
# 背驰判断:比较离开笔与中枢内最后一笔同向笔的MACD柱状累积面积
|
||||||
|
# 缠论原文:两段同向走势的MACD柱状面积比较,面积缩小即为背驰
|
||||||
|
compare_bi = None
|
||||||
|
for bi in reversed(zs.bi_list):
|
||||||
|
if bi.dir == Chan_BI_DIR.DOWN and bi is not leave_bi:
|
||||||
|
compare_bi = bi
|
||||||
|
break
|
||||||
|
|
||||||
|
is_divergence = False
|
||||||
|
if compare_bi:
|
||||||
|
# 笔的macd_hist是该笔内所有KLU的macdhist累积面积
|
||||||
|
leave_macd_area = abs(leave_bi.macd_hist)
|
||||||
|
compare_macd_area = abs(compare_bi.macd_hist)
|
||||||
|
|
||||||
|
# 价格创新低但MACD面积缩小 = 底背驰
|
||||||
|
if leave_bi.low <= compare_bi.low and leave_macd_area < compare_macd_area:
|
||||||
|
is_divergence = True
|
||||||
|
# 即使没创新低,MACD面积明显缩小也算背驰
|
||||||
|
elif leave_macd_area < compare_macd_area * 0.5:
|
||||||
|
is_divergence = True
|
||||||
|
else:
|
||||||
|
# 没有对比笔时,只要离开中枢就算一类买点
|
||||||
|
is_divergence = True
|
||||||
|
|
||||||
|
if is_divergence:
|
||||||
|
bsp = ChanBSP(
|
||||||
|
leave_bi, len(bsp_list),
|
||||||
|
Chan_BSP_TYPE.T1,
|
||||||
|
Chan_BSP_DIR.BUY,
|
||||||
|
leave_bi.sure_time,
|
||||||
|
1, zs, None
|
||||||
|
)
|
||||||
|
bsp_list.append(bsp)
|
||||||
|
|
||||||
|
# 一类卖点:向上离开中枢(leave_bi向上,高点 > zg),趋势力竭
|
||||||
|
elif leave_bi.dir == Chan_BI_DIR.UP and leave_bi.high > zs.zg:
|
||||||
|
# 背驰判断:比较离开笔与中枢内最后一笔同向笔的MACD柱状累积面积
|
||||||
|
compare_bi = None
|
||||||
|
for bi in reversed(zs.bi_list):
|
||||||
|
if bi.dir == Chan_BI_DIR.UP and bi is not leave_bi:
|
||||||
|
compare_bi = bi
|
||||||
|
break
|
||||||
|
|
||||||
|
is_divergence = False
|
||||||
|
if compare_bi:
|
||||||
|
leave_macd_area = abs(leave_bi.macd_hist)
|
||||||
|
compare_macd_area = abs(compare_bi.macd_hist)
|
||||||
|
|
||||||
|
# 价格创新高但MACD面积缩小 = 顶背驰
|
||||||
|
if leave_bi.high >= compare_bi.high and leave_macd_area < compare_macd_area:
|
||||||
|
is_divergence = True
|
||||||
|
# 即使没创新高,MACD面积明显缩小也算背驰
|
||||||
|
elif leave_macd_area < compare_macd_area * 0.5:
|
||||||
|
is_divergence = True
|
||||||
|
else:
|
||||||
|
is_divergence = True
|
||||||
|
|
||||||
|
if is_divergence:
|
||||||
|
bsp = ChanBSP(
|
||||||
|
leave_bi, len(bsp_list),
|
||||||
|
Chan_BSP_TYPE.T1,
|
||||||
|
Chan_BSP_DIR.SELL,
|
||||||
|
leave_bi.sure_time,
|
||||||
|
1, zs, None
|
||||||
|
)
|
||||||
|
bsp_list.append(bsp)
|
||||||
|
|
||||||
|
return bsp_list
|
||||||
|
|
||||||
|
|
||||||
|
def find_second_bsp(self, bi_list, first_bsp_list):
|
||||||
|
"""
|
||||||
|
笔中枢的二类买卖点识别
|
||||||
|
|
||||||
|
二类买点:一类买点出现后,价格向上反弹一笔,再回落一笔,
|
||||||
|
回落笔的低点不跌破一类买点的低点,确认底部成立。
|
||||||
|
二类卖点:一类卖点出现后,价格向下回落一笔,再反弹一笔,
|
||||||
|
反弹笔的高点不超过一类卖点的高点,确认顶部成立。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
bi_list: 笔列表
|
||||||
|
first_bsp_list: 一类买卖点列表(find_first_bsp 的返回值)
|
||||||
|
|
||||||
|
返回:
|
||||||
|
bsp_list: ChanBSP 列表,包含所有识别到的二类买卖点
|
||||||
|
"""
|
||||||
|
bsp_list = []
|
||||||
|
if not first_bsp_list or len(bi_list) < 4:
|
||||||
|
return bsp_list
|
||||||
|
|
||||||
|
for first_bsp in first_bsp_list:
|
||||||
|
trigger_bi = first_bsp.bi # 一类买卖点的触发笔
|
||||||
|
|
||||||
|
if first_bsp.dir == Chan_BSP_DIR.BUY:
|
||||||
|
# 一买之后:trigger_bi 向下 -> 反弹笔(向上) -> 回落笔(向下)
|
||||||
|
# 回落笔的低点 > trigger_bi 的低点 => 二类买点
|
||||||
|
bounce_bi = trigger_bi.next # 反弹笔(向上)
|
||||||
|
if bounce_bi and bounce_bi.is_sure and bounce_bi.dir == Chan_BI_DIR.UP:
|
||||||
|
pullback_bi = bounce_bi.next # 回落笔(向下)
|
||||||
|
if pullback_bi and pullback_bi.is_sure and pullback_bi.dir == Chan_BI_DIR.DOWN:
|
||||||
|
if pullback_bi.low > trigger_bi.low:
|
||||||
|
bsp = ChanBSP(
|
||||||
|
pullback_bi, len(bsp_list),
|
||||||
|
Chan_BSP_TYPE.T2,
|
||||||
|
Chan_BSP_DIR.BUY,
|
||||||
|
pullback_bi.sure_time,
|
||||||
|
1, first_bsp.zs, None
|
||||||
|
)
|
||||||
|
bsp_list.append(bsp)
|
||||||
|
|
||||||
|
elif first_bsp.dir == Chan_BSP_DIR.SELL:
|
||||||
|
# 一卖之后:trigger_bi 向上 -> 回落笔(向下) -> 反弹笔(向上)
|
||||||
|
# 反弹笔的高点 < trigger_bi 的高点 => 二类卖点
|
||||||
|
drop_bi = trigger_bi.next # 回落笔(向下)
|
||||||
|
if drop_bi and drop_bi.is_sure and drop_bi.dir == Chan_BI_DIR.DOWN:
|
||||||
|
bounce_bi = drop_bi.next # 反弹笔(向上)
|
||||||
|
if bounce_bi and bounce_bi.is_sure and bounce_bi.dir == Chan_BI_DIR.UP:
|
||||||
|
if bounce_bi.high < trigger_bi.high:
|
||||||
|
bsp = ChanBSP(
|
||||||
|
bounce_bi, len(bsp_list),
|
||||||
|
Chan_BSP_TYPE.T2,
|
||||||
|
Chan_BSP_DIR.SELL,
|
||||||
|
bounce_bi.sure_time,
|
||||||
|
1, first_bsp.zs, None
|
||||||
|
)
|
||||||
|
bsp_list.append(bsp)
|
||||||
|
|
||||||
|
return bsp_list
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
"""TF_DF builder mixin — 由 split_tfdf_builders 自动生成,逻辑与原 TF_DF 一致。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
from technical.util import resample_to_interval
|
||||||
|
|
||||||
|
from chanlun.core.ChanBI import ChanBI
|
||||||
|
from chanlun.core.ChanBIZS import ChanBIZS
|
||||||
|
from chanlun.core.ChanBSP import ChanBSP
|
||||||
|
from chanlun.core.ChanEnum import (
|
||||||
|
Chan_BI_DIR,
|
||||||
|
Chan_BSP_DIR,
|
||||||
|
Chan_BSP_TYPE,
|
||||||
|
Chan_FX_TYPE,
|
||||||
|
Chan_K_DIR,
|
||||||
|
Chan_KLC_FX,
|
||||||
|
Chan_KLC_STATE,
|
||||||
|
Chan_KLINE_DIR,
|
||||||
|
Chan_KLU_PATTERN,
|
||||||
|
Chan_PRICE_TREND,
|
||||||
|
Chan_SEG_DIR,
|
||||||
|
Chan_ZS_DIR,
|
||||||
|
)
|
||||||
|
from chanlun.core.ChanKLC import ChanKLC
|
||||||
|
from chanlun.core.ChanKLU import ChanKLU
|
||||||
|
from chanlun.core.ChanSBI import ChanSBI
|
||||||
|
from chanlun.core.ChanSEG import ChanSEG
|
||||||
|
from chanlun.core.ChanZS import ChanZS, ChanZS_Big
|
||||||
|
from chanlun.indicators.ChanMACD import ChanMACD
|
||||||
|
|
||||||
|
|
||||||
|
class IndicatorsBuilderMixin:
|
||||||
|
def get_ema52(self, index=-1):
|
||||||
|
if self.klu_list:
|
||||||
|
ema52_value = self.klu_list[index].ema52
|
||||||
|
# 处理NaN值
|
||||||
|
if pd.isna(ema52_value) or ema52_value is None:
|
||||||
|
return None
|
||||||
|
return float(ema52_value)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_ema24(self, index=-1):
|
||||||
|
if self.klu_list:
|
||||||
|
ema24_value = self.klu_list[index].ema24
|
||||||
|
# 处理NaN值
|
||||||
|
if pd.isna(ema24_value) or ema24_value is None:
|
||||||
|
return None
|
||||||
|
return float(ema24_value)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add_indicators(self, df):
|
||||||
|
fast = 26
|
||||||
|
slow = 52
|
||||||
|
period = 9
|
||||||
|
macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period)
|
||||||
|
bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0)
|
||||||
|
bb120 = ta.BBANDS(df, timeperiod=120, nbdevup=3.0, nbdevdn=3.0, matype=0)
|
||||||
|
bb30 = ta.BBANDS(df, timeperiod=41, nbdevup=2.3, nbdevdn=2.3, matype=0)
|
||||||
|
bb302 = ta.BBANDS(df, timeperiod=41, nbdevup=2.0, nbdevdn=2.0, matype=0)
|
||||||
|
bb30 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0)
|
||||||
|
bb302 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0)
|
||||||
|
bb2633 = ta.BBANDS(df, timeperiod=26, nbdevup=3.0, nbdevdn=3.0, matype=0)
|
||||||
|
# 计算布林带中轨(移动平均线)
|
||||||
|
bb30_middle = ta.SMA(df, timeperiod=90)
|
||||||
|
|
||||||
|
# 手动计算布林带 %B 指标 (BBP)
|
||||||
|
# %B = (Price - Lower Band) / (Upper Band - Lower Band)
|
||||||
|
bbp365 = (df['close'] - bb365['lowerband']) / (bb365['upperband'] - bb365['lowerband'])
|
||||||
|
bbp120 = (df['close'] - bb120['lowerband']) / (bb120['upperband'] - bb120['lowerband'])
|
||||||
|
bbp30 = (df['close'] - bb30['lowerband']) / (bb30['upperband'] - bb30['lowerband'])
|
||||||
|
bbp302 = (df['close'] - bb302['lowerband']) / (bb302['upperband'] - bb302['lowerband'])
|
||||||
|
bbp2633 = (df['close'] - bb2633['lowerband']) / (bb2633['upperband'] - bb2633['lowerband'])
|
||||||
|
df['bb2633upper'] = bb2633['upperband']
|
||||||
|
df['bb2633lower'] = bb2633['lowerband']
|
||||||
|
df['bbp2633'] = bbp2633
|
||||||
|
df['bb2633middle'] = bb2633['middleband']
|
||||||
|
df['atr'] = ta.ATR(df, timeperiod=14)
|
||||||
|
df['bbup365'] = bb365['upperband']
|
||||||
|
df['bblow365'] = bb365['lowerband']
|
||||||
|
df['bbp365'] = bbp365
|
||||||
|
df['bbup120'] = bb120['upperband']
|
||||||
|
df['bblow120'] = bb120['lowerband']
|
||||||
|
df['bbp120'] = bbp120
|
||||||
|
df['bbup30'] = bb30['upperband']
|
||||||
|
df['bblow30'] = bb30['lowerband']
|
||||||
|
df['bbmiddle30'] = bb30_middle # 添加bb30中轨
|
||||||
|
df['bbp30'] = bbp30
|
||||||
|
df['bbup302'] = bb302['upperband']
|
||||||
|
df['bblow302'] = bb302['lowerband']
|
||||||
|
df['bbp302'] = bbp302
|
||||||
|
df['macd'] = macd['macd']
|
||||||
|
df['macdsignal'] = macd['macdsignal']
|
||||||
|
df['macdhist'] = macd['macdhist']
|
||||||
|
df['ema5'] = ta.EMA(df, timeperiod=5)
|
||||||
|
df['ema10'] = ta.EMA(df, timeperiod=10)
|
||||||
|
df['ema24'] = ta.EMA(df, timeperiod=24)
|
||||||
|
df['ema52'] = ta.EMA(df, timeperiod=52)
|
||||||
|
df['ema104'] = ta.EMA(df, timeperiod=104)
|
||||||
|
df['ema156'] = ta.EMA(df, timeperiod=156)
|
||||||
|
df['ema208'] = ta.EMA(df, timeperiod=208)
|
||||||
|
df['ema26'] = ta.EMA(df, timeperiod=26)
|
||||||
|
df['ema13'] = ta.EMA(df, timeperiod=13)
|
||||||
|
df['ema7'] = ta.EMA(df, timeperiod=7)
|
||||||
|
df['rsi'] = ta.RSI(df, timeperiod=14)
|
||||||
|
df['volume_ratio'] = self.cal_volume_ratio(df)
|
||||||
|
return df
|
||||||
|
|
||||||
|
def get_ema_state(self, dataframe):
|
||||||
|
klu_list = self.get_klu_list(dataframe)
|
||||||
|
klc_list = self.get_klc_list(klu_list)
|
||||||
|
bi_list = self.cal_bi_list(klc_list)
|
||||||
|
klu_state_list = []
|
||||||
|
for klu in klu_list:
|
||||||
|
if klu.near0_return == 1:
|
||||||
|
klu_state_list.append("1")
|
||||||
|
elif klu.near0_return == 9:
|
||||||
|
klu_state_list.append("-1")
|
||||||
|
elif klu.candle_dir == Chan_K_DIR.BULL:
|
||||||
|
klu_state_list.append("2")
|
||||||
|
elif klu.candle_dir == Chan_K_DIR.BEAR:
|
||||||
|
klu_state_list.append("-2")
|
||||||
|
else:
|
||||||
|
klu_state_list.append("0")
|
||||||
|
return klu_state_list
|
||||||
|
|
||||||
|
def get_decimal(self, value):
|
||||||
|
return Decimal("{:.2f}".format(value))
|
||||||
@@ -0,0 +1,547 @@
|
|||||||
|
"""TF_DF builder mixin — 由 split_tfdf_builders 自动生成,逻辑与原 TF_DF 一致。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
from technical.util import resample_to_interval
|
||||||
|
|
||||||
|
from chanlun.core.ChanBI import ChanBI
|
||||||
|
from chanlun.core.ChanBIZS import ChanBIZS
|
||||||
|
from chanlun.core.ChanBSP import ChanBSP
|
||||||
|
from chanlun.core.ChanEnum import (
|
||||||
|
Chan_BI_DIR,
|
||||||
|
Chan_BSP_DIR,
|
||||||
|
Chan_BSP_TYPE,
|
||||||
|
Chan_FX_TYPE,
|
||||||
|
Chan_K_DIR,
|
||||||
|
Chan_KLC_FX,
|
||||||
|
Chan_KLC_STATE,
|
||||||
|
Chan_KLINE_DIR,
|
||||||
|
Chan_KLU_PATTERN,
|
||||||
|
Chan_PRICE_TREND,
|
||||||
|
Chan_SEG_DIR,
|
||||||
|
Chan_ZS_DIR,
|
||||||
|
)
|
||||||
|
from chanlun.core.ChanKLC import ChanKLC
|
||||||
|
from chanlun.core.ChanKLU import ChanKLU
|
||||||
|
from chanlun.core.ChanSBI import ChanSBI
|
||||||
|
from chanlun.core.ChanSEG import ChanSEG
|
||||||
|
from chanlun.core.ChanZS import ChanZS, ChanZS_Big
|
||||||
|
from chanlun.indicators.ChanMACD import ChanMACD
|
||||||
|
|
||||||
|
|
||||||
|
class KlineBuilderMixin:
|
||||||
|
def get_klu_state(self, dataframe):
|
||||||
|
klc_list = self.get_klc_list(self.get_klu_list(dataframe))
|
||||||
|
bi_list = self.cal_bi_list(klc_list)
|
||||||
|
klu_state_list = []
|
||||||
|
klc_index = 0
|
||||||
|
for index in range(0, len(dataframe)):
|
||||||
|
if klc_index == len(klc_list):
|
||||||
|
klc_index = len(klc_list) - 1
|
||||||
|
klc = klc_list[klc_index]
|
||||||
|
if klc.end_klu and klc.end_klu.idx == index:
|
||||||
|
if klc.klc_state == Chan_KLC_STATE.S10:
|
||||||
|
klu_state_list.append("10")
|
||||||
|
#print(klc.end_time, klc.klc_fx_type)
|
||||||
|
elif klc.klc_state == Chan_KLC_STATE.S_10:
|
||||||
|
klu_state_list.append("-10")
|
||||||
|
#print(klc.end_time, klc.klc_fx_type)
|
||||||
|
elif klc.klc_state == Chan_KLC_STATE.S11:
|
||||||
|
klu_state_list.append("11")
|
||||||
|
#print(klc.end_time, klc.klc_fx_type)
|
||||||
|
elif klc.klc_state == Chan_KLC_STATE.S_11:
|
||||||
|
klu_state_list.append("-11")
|
||||||
|
#print(klc.end_time, klc.klc_fx_type)
|
||||||
|
else:
|
||||||
|
klu_state_list.append("00")
|
||||||
|
klc_index += 1
|
||||||
|
else:
|
||||||
|
klu_state_list.append("00")
|
||||||
|
print(klu_state_list[:20])
|
||||||
|
return klu_state_list
|
||||||
|
|
||||||
|
|
||||||
|
def check_fx1(self, klc):
|
||||||
|
if klc.pre and klc.next:
|
||||||
|
if klc.high > klc.pre.high and klc.high > klc.next.high and klc.low > klc.pre.low and klc.low > klc.next.low:
|
||||||
|
if klc.pre.pre and klc.next.next:
|
||||||
|
if klc.high > klc.pre.pre.high and klc.high > klc.next.next.high:
|
||||||
|
#if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0:
|
||||||
|
klc.set_fx(Chan_FX_TYPE.TOP)
|
||||||
|
#print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "TOP")
|
||||||
|
return Chan_FX_TYPE.TOP
|
||||||
|
elif klc.low < klc.pre.low and klc.low < klc.next.low and klc.high < klc.pre.high and klc.high < klc.next.high:
|
||||||
|
#if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0:
|
||||||
|
if klc.pre.pre and klc.next.next:
|
||||||
|
if klc.low < klc.pre.pre.low and klc.low < klc.next.next.low:
|
||||||
|
klc.set_fx(Chan_FX_TYPE.BOTTOM)
|
||||||
|
#print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "BOTTOM")
|
||||||
|
return Chan_FX_TYPE.BOTTOM
|
||||||
|
return Chan_FX_TYPE.UNKNOWN
|
||||||
|
|
||||||
|
def check_fx(self, klc):
|
||||||
|
if klc.pre and klc.next:
|
||||||
|
if klc.high > klc.pre.high and klc.high > klc.next.high and klc.low > klc.pre.low and klc.low > klc.next.low:
|
||||||
|
#if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0:
|
||||||
|
klc.set_fx(Chan_FX_TYPE.TOP)
|
||||||
|
#print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "TOP")
|
||||||
|
return Chan_FX_TYPE.TOP
|
||||||
|
elif klc.low < klc.pre.low and klc.low < klc.next.low and klc.high < klc.pre.high and klc.high < klc.next.high:
|
||||||
|
#if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0:
|
||||||
|
klc.set_fx(Chan_FX_TYPE.BOTTOM)
|
||||||
|
#print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "BOTTOM")
|
||||||
|
return Chan_FX_TYPE.BOTTOM
|
||||||
|
return Chan_FX_TYPE.UNKNOWN
|
||||||
|
|
||||||
|
def check_fx2(self, klc):
|
||||||
|
if klc.pre and klc.next:
|
||||||
|
if klc.high > klc.pre.close and klc.close > klc.next.close and klc.close > klc.pre.close and klc.close > klc.next.close:
|
||||||
|
#if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0:
|
||||||
|
klc.set_fx(Chan_FX_TYPE.TOP)
|
||||||
|
#print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "TOP")
|
||||||
|
return Chan_FX_TYPE.TOP
|
||||||
|
elif klc.low < klc.pre.close and klc.close < klc.next.close and klc.close < klc.pre.close and klc.close < klc.next.close:
|
||||||
|
#if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0:
|
||||||
|
klc.set_fx(Chan_FX_TYPE.BOTTOM)
|
||||||
|
#print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "BOTTOM")
|
||||||
|
return Chan_FX_TYPE.BOTTOM
|
||||||
|
return Chan_FX_TYPE.UNKNOWN
|
||||||
|
|
||||||
|
def check_fx_pattern(self, klc):
|
||||||
|
klu_list = klc.pre.klu_list + klc.klu_list + klc.next.klu_list
|
||||||
|
|
||||||
|
self.cal_klu_pattern(klu_list)
|
||||||
|
p = ""
|
||||||
|
for klu in klu_list:
|
||||||
|
p += klu.to_string()
|
||||||
|
#print(p)
|
||||||
|
|
||||||
|
def cal_volume_ratio(self, dataframe, window=10):
|
||||||
|
df = dataframe.copy()
|
||||||
|
# 计算过去N根K线的平均成交量
|
||||||
|
df['avg_volume'] = df['volume'].rolling(window=window).mean()
|
||||||
|
# 计算量比
|
||||||
|
df['volume_ratio'] = df['volume'] / df['avg_volume']
|
||||||
|
# 填充缺失值(前N根K线)
|
||||||
|
df['volume_ratio'] = df['volume_ratio'].fillna(1.0)
|
||||||
|
return df['volume_ratio']
|
||||||
|
|
||||||
|
def cal_kl_data(self, dataframe:DataFrame):
|
||||||
|
fields = "time,open,high,low,close,volume"
|
||||||
|
klu_list = []
|
||||||
|
last_klu = None
|
||||||
|
for i in range(0, len(dataframe)):
|
||||||
|
item = dataframe.iloc[i]
|
||||||
|
date = item['date']
|
||||||
|
o = item['open']
|
||||||
|
h = item['high']
|
||||||
|
l = item['low']
|
||||||
|
c = item['close']
|
||||||
|
v = item['volume']
|
||||||
|
# time_obj = date.fromtimestamp(date)
|
||||||
|
# date = date + timedelta(hours=8)
|
||||||
|
time_str = date.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
item_data = [
|
||||||
|
time_str,
|
||||||
|
o,
|
||||||
|
h,
|
||||||
|
l,
|
||||||
|
c,
|
||||||
|
v
|
||||||
|
]
|
||||||
|
# klu = KLU(self.create_item_dict(item_data, GetColumnNameFromFieldList(fields)))
|
||||||
|
klu = ChanKLU(time_str, o, h, l, c, v)
|
||||||
|
# print(klu.time, klu.open, klu.high, klu.low, klu.close, klu.volume)
|
||||||
|
klu.set_idx(i)
|
||||||
|
klu_list.append(klu)
|
||||||
|
if last_klu:
|
||||||
|
last_klu.set_next(klu)
|
||||||
|
klu.set_pre(last_klu)
|
||||||
|
last_klu = klu
|
||||||
|
if 'macd' in item:
|
||||||
|
klu.set_indicators(item)
|
||||||
|
return klu_list
|
||||||
|
|
||||||
|
def get_kl_data(self, dataframe:DataFrame):
|
||||||
|
return self.cal_kl_data(dataframe)
|
||||||
|
|
||||||
|
def get_klc_list(self, klu_list):
|
||||||
|
klc_list = []
|
||||||
|
last_klu = None
|
||||||
|
macd = ChanMACD(klu_list)
|
||||||
|
klu_list = macd.cal_macd_state()
|
||||||
|
ema_up_list = []
|
||||||
|
ema_down_list = []
|
||||||
|
ema_up_count = 0
|
||||||
|
ema_down_count = 0
|
||||||
|
last_klu = None
|
||||||
|
for klu in klu_list:
|
||||||
|
ema = klu.ema52
|
||||||
|
last_ema = last_klu.ema52 if last_klu else 0
|
||||||
|
if klu.close >= ema:
|
||||||
|
ema_up_count += 1
|
||||||
|
elif klu.close < ema:
|
||||||
|
ema_down_count += 1
|
||||||
|
if last_klu and last_klu.close >= last_ema and klu.close < ema:
|
||||||
|
ema_up_list.append(ema_up_count)
|
||||||
|
#print(last_klu.time, ema_up_count, "UP END")
|
||||||
|
ema_up_count = 0
|
||||||
|
elif last_klu and last_klu.close < last_ema and klu.close >= ema:
|
||||||
|
ema_down_list.append(ema_down_count)
|
||||||
|
#print(last_klu.time, ema_down_count, "DOWN END")
|
||||||
|
ema_down_count = 0
|
||||||
|
if len(klc_list) > 0:
|
||||||
|
last_klc = klc_list[-1]
|
||||||
|
if klu.exception:
|
||||||
|
ddir = Chan_KLINE_DIR.DOWN
|
||||||
|
if last_klc.high < klu.high:
|
||||||
|
ddir = Chan_KLINE_DIR.UP
|
||||||
|
klc = ChanKLC(klu, index=len(klc_list), ddir=ddir)
|
||||||
|
klc.high = klu.close if klu.close > klu.open else klu.open
|
||||||
|
klc.low = klu.open if klu.close > klu.open else klu.close
|
||||||
|
klc_list.append(klc)
|
||||||
|
last_klc.set_next(klc)
|
||||||
|
klc.set_pre(last_klc)
|
||||||
|
last_klc.set_end_klu(last_klu)
|
||||||
|
klc.set_pre_fx()
|
||||||
|
#print(klu.time, klu.high, klu.low, klu.close, klu.open, klu.exception)
|
||||||
|
else:
|
||||||
|
included = last_klc.check_klu_included(klu)
|
||||||
|
if not included:
|
||||||
|
ddir = Chan_KLINE_DIR.DOWN
|
||||||
|
if last_klc.high < klu.high:
|
||||||
|
ddir = Chan_KLINE_DIR.UP
|
||||||
|
klc = ChanKLC(klu, index=len(klc_list), ddir=ddir)
|
||||||
|
klc_list.append(klc)
|
||||||
|
last_klc.set_next(klc)
|
||||||
|
klc.set_pre(last_klc)
|
||||||
|
last_klc.set_end_klu(last_klu)
|
||||||
|
klc.set_pre_fx()
|
||||||
|
else:
|
||||||
|
last_klc.add_klu(klu)
|
||||||
|
else:
|
||||||
|
ddir = Chan_KLINE_DIR.UP
|
||||||
|
if klu.open > klu.close:
|
||||||
|
ddir = Chan_KLINE_DIR.DOWN
|
||||||
|
klc = ChanKLC(klu, 0, ddir)
|
||||||
|
klc_list.append(klc)
|
||||||
|
last_klu = klu
|
||||||
|
klc_list = self.cal_trend(klc_list)
|
||||||
|
#print(ema52_up_list, ema52_down_list)
|
||||||
|
return klc_list
|
||||||
|
|
||||||
|
|
||||||
|
def get_klu_list(self, dataframe):
|
||||||
|
klu_list = self.get_kl_data(dataframe)
|
||||||
|
#klu_list = self.cal_klu_pattern(klu_list)
|
||||||
|
return klu_list
|
||||||
|
|
||||||
|
def cal_klu_pattern(self, klu_list):
|
||||||
|
"""
|
||||||
|
计算裸K的pattern - 识别反转形态
|
||||||
|
"""
|
||||||
|
if not klu_list or len(klu_list) < 3:
|
||||||
|
return klu_list
|
||||||
|
|
||||||
|
for i, klu in enumerate(klu_list):
|
||||||
|
# 单根K线反转模式识别
|
||||||
|
self._detect_single_reversal_pattern(klu)
|
||||||
|
|
||||||
|
# 双根K线形态识别
|
||||||
|
if i >= 1:
|
||||||
|
self._detect_double_pattern(klu_list[i-1], klu)
|
||||||
|
|
||||||
|
# 三根K线形态识别
|
||||||
|
if i >= 2:
|
||||||
|
self._detect_triple_pattern(klu_list[i-2], klu_list[i-1], klu)
|
||||||
|
|
||||||
|
#if klu.pattern != Chan_KLU_PATTERN.UNKNOWN:
|
||||||
|
#print(klu.time, klu.pattern, klu.lower_shadow_ratio, klu.upper_shadow_ratio, klu.body_ratio, klu.lower_shadow_ratio/klu.body_ratio, klu.upper_shadow_ratio/klu.body_ratio)
|
||||||
|
return klu_list
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_single_reversal_pattern(self, klu):
|
||||||
|
"""检测单根K线反转模式"""
|
||||||
|
body = abs(klu.close - klu.open)
|
||||||
|
upper_shadow = klu.high - max(klu.close, klu.open)
|
||||||
|
lower_shadow = min(klu.close, klu.open) - klu.low
|
||||||
|
total_range = klu.high - klu.low
|
||||||
|
|
||||||
|
# 避免除零
|
||||||
|
if total_range == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
body_ratio = body / total_range
|
||||||
|
upper_ratio = upper_shadow / total_range
|
||||||
|
lower_ratio = lower_shadow / total_range
|
||||||
|
#print(klu.time, upper_ratio, lower_ratio, body_ratio, upper_ratio/body_ratio, lower_ratio/body_ratio)
|
||||||
|
# 避免body_ratio为0时的除零错误
|
||||||
|
if body_ratio == 0:
|
||||||
|
return
|
||||||
|
# 锤子线/上吊线 - 反转信号
|
||||||
|
if lower_ratio / body_ratio >= 2:
|
||||||
|
# 锤子线:底部反转,需要前面一段
|
||||||
|
if klu.close > klu.open and klu.pre:
|
||||||
|
klu.set_pattern(Chan_KLU_PATTERN.HAMMER) # 底部反转
|
||||||
|
# 上吊线:顶部反转,需要前一根是上涨趋势
|
||||||
|
elif klu.close < klu.open and klu.pre:
|
||||||
|
klu.set_pattern(Chan_KLU_PATTERN.HANGING_MAN) # 顶部反转
|
||||||
|
|
||||||
|
# 倒锤子线/射击之星 - 反转信号
|
||||||
|
elif upper_ratio / body_ratio >= 2:
|
||||||
|
# 倒锤子线:底部反转,需要前一根是下跌趋势
|
||||||
|
if klu.close > klu.open and klu.pre:
|
||||||
|
klu.set_pattern(Chan_KLU_PATTERN.INVERTED_HAMMER) # 底部反转
|
||||||
|
# 射击之星:顶部反转,需要前一根是上涨趋势
|
||||||
|
elif klu.close < klu.open and klu.pre:
|
||||||
|
klu.set_pattern(Chan_KLU_PATTERN.SHOOTING_STAR) # 顶部反转
|
||||||
|
|
||||||
|
# 十字星 - 反转信号
|
||||||
|
elif body_ratio <= 0.1:
|
||||||
|
if upper_ratio > 0.4 and lower_ratio > 0.4:
|
||||||
|
klu.set_pattern(Chan_KLU_PATTERN.LONG_LEGGED_DOJI) # 强烈反转信号
|
||||||
|
elif upper_ratio > 0.4 and lower_ratio <= 0.1:
|
||||||
|
# 墓碑十字星:顶部反转,需要前一根是上涨趋势
|
||||||
|
if klu.pre and klu.pre.close > klu.pre.open:
|
||||||
|
klu.set_pattern(Chan_KLU_PATTERN.GRAVESTONE_DOJI) # 顶部反转
|
||||||
|
elif lower_ratio > 0.4 and upper_ratio <= 0.1:
|
||||||
|
# 蜻蜓十字星:底部反转,需要前一根是下跌趋势
|
||||||
|
if klu.pre and klu.pre.close < klu.pre.open:
|
||||||
|
klu.set_pattern(Chan_KLU_PATTERN.DRAGONFLY_DOJI) # 底部反转
|
||||||
|
else:
|
||||||
|
klu.set_pattern(Chan_KLU_PATTERN.DOJI) # 一般反转信号
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_double_pattern(self, prev_klu, curr_klu):
|
||||||
|
"""检测两根K线形成的形态
|
||||||
|
包括:吞没形态(看涨/看跌)、乌云盖顶、曙光初现
|
||||||
|
"""
|
||||||
|
# 如果前一根K线已经有形态,不再识别双K线形态
|
||||||
|
if prev_klu.pattern != Chan_KLU_PATTERN.UNKNOWN:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 计算K线实体
|
||||||
|
prev_body = abs(prev_klu.close - prev_klu.open)
|
||||||
|
curr_body = abs(curr_klu.close - curr_klu.open)
|
||||||
|
|
||||||
|
# 判断K线颜色(阴阳)
|
||||||
|
prev_bullish = prev_klu.close > prev_klu.open
|
||||||
|
curr_bullish = curr_klu.close > curr_klu.open
|
||||||
|
|
||||||
|
# 检查是否存在长期趋势(至少需要5根K线的趋势)
|
||||||
|
def check_long_trend(klu, bullish_trend=True, min_bars=5):
|
||||||
|
"""检查是否存在长期趋势
|
||||||
|
bullish_trend=True: 检查上涨趋势
|
||||||
|
bullish_trend=False: 检查下跌趋势
|
||||||
|
min_bars: 最少需要多少根K线形成趋势
|
||||||
|
"""
|
||||||
|
if not klu or not klu.pre:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 使用EMA指标判断长期趋势
|
||||||
|
if klu.ema52 > 0:
|
||||||
|
if bullish_trend and klu.close < klu.ema52:
|
||||||
|
return False
|
||||||
|
if not bullish_trend and klu.close > klu.ema52:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查连续的K线方向
|
||||||
|
count = 0
|
||||||
|
current = klu.pre
|
||||||
|
|
||||||
|
while current and count < min_bars:
|
||||||
|
if not current.pre:
|
||||||
|
break
|
||||||
|
|
||||||
|
if bullish_trend:
|
||||||
|
# 上涨趋势:当前收盘价高于前一根收盘价
|
||||||
|
if current.close <= current.pre.close:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# 下跌趋势:当前收盘价低于前一根收盘价
|
||||||
|
if current.close >= current.pre.close:
|
||||||
|
break
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
current = current.pre
|
||||||
|
|
||||||
|
return count >= min_bars
|
||||||
|
|
||||||
|
# 1. 看涨吞没形态:前阴后阳,后者完全吞没前者
|
||||||
|
# 要求前面有明显的下跌趋势
|
||||||
|
if not prev_bullish and curr_bullish and \
|
||||||
|
abs(curr_klu.open - prev_klu.close) < 10 and \
|
||||||
|
curr_klu.close > prev_klu.open and \
|
||||||
|
check_long_trend(prev_klu, bullish_trend=False, min_bars=5):
|
||||||
|
curr_klu.set_pattern(Chan_KLU_PATTERN.BULLISH_ENGULFING)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. 看跌吞没形态:前阳后阴,后者完全吞没前者
|
||||||
|
# 要求前面有明显的上涨趋势
|
||||||
|
if prev_bullish and not curr_bullish and \
|
||||||
|
abs(curr_klu.open - prev_klu.close) < 10 and \
|
||||||
|
curr_klu.close < prev_klu.open and \
|
||||||
|
check_long_trend(prev_klu, bullish_trend=True, min_bars=5):
|
||||||
|
curr_klu.set_pattern(Chan_KLU_PATTERN.BEARISH_ENGULFING)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. 乌云盖顶:前阳后阴,后者开盘价高于前者最高价,收盘价在前者实体中部以下
|
||||||
|
# 要求前面有明显的上涨趋势
|
||||||
|
if prev_bullish and not curr_bullish and \
|
||||||
|
curr_klu.open > prev_klu.high and \
|
||||||
|
curr_klu.close < (prev_klu.open + prev_klu.close) / 2 and \
|
||||||
|
curr_klu.close > prev_klu.open and \
|
||||||
|
check_long_trend(prev_klu, bullish_trend=True, min_bars=5):
|
||||||
|
curr_klu.set_pattern(Chan_KLU_PATTERN.DARK_CLOUD_COVER)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 4. 曙光初现:前阴后阳,后者开盘价低于前者最低价,收盘价在前者实体中部以上
|
||||||
|
# 要求前面有明显的下跌趋势
|
||||||
|
if not prev_bullish and curr_bullish and \
|
||||||
|
curr_klu.open < prev_klu.low and \
|
||||||
|
curr_klu.close > (prev_klu.open + prev_klu.close) / 2 and \
|
||||||
|
curr_klu.close < prev_klu.open and \
|
||||||
|
check_long_trend(prev_klu, bullish_trend=False, min_bars=5):
|
||||||
|
curr_klu.set_pattern(Chan_KLU_PATTERN.PIERCING_LINE)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 平顶和平底移至三根K线形态中判断
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_triple_pattern(self, first_klu, second_klu, third_klu):
|
||||||
|
"""检测三根K线形成的形态
|
||||||
|
包括:早晨之星、黄昏之星、平顶、平底
|
||||||
|
"""
|
||||||
|
# 如果前两根K线已经有形态,不再识别三K线形态
|
||||||
|
if first_klu.pattern != Chan_KLU_PATTERN.UNKNOWN or \
|
||||||
|
second_klu.pattern != Chan_KLU_PATTERN.UNKNOWN:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 判断K线颜色(阴阳)
|
||||||
|
first_bullish = first_klu.close > first_klu.open
|
||||||
|
second_bullish = second_klu.close > second_klu.open
|
||||||
|
third_bullish = third_klu.close > third_klu.open
|
||||||
|
|
||||||
|
# 计算实体大小
|
||||||
|
first_body = abs(first_klu.close - first_klu.open)
|
||||||
|
second_body = abs(second_klu.close - second_klu.open)
|
||||||
|
third_body = abs(third_klu.close - third_klu.open)
|
||||||
|
|
||||||
|
# 检查是否存在长期趋势(至少需要5根K线的趋势)
|
||||||
|
def check_long_trend(klu, bullish_trend=True, min_bars=5):
|
||||||
|
"""检查是否存在长期趋势
|
||||||
|
bullish_trend=True: 检查上涨趋势
|
||||||
|
bullish_trend=False: 检查下跌趋势
|
||||||
|
min_bars: 最少需要多少根K线形成趋势
|
||||||
|
"""
|
||||||
|
if not klu or not klu.pre:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 使用EMA指标判断长期趋势
|
||||||
|
if klu.ema52 > 0:
|
||||||
|
if bullish_trend and klu.close < klu.ema52:
|
||||||
|
return False
|
||||||
|
if not bullish_trend and klu.close > klu.ema52:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查连续的K线方向
|
||||||
|
count = 0
|
||||||
|
current = klu.pre
|
||||||
|
|
||||||
|
while current and count < min_bars:
|
||||||
|
if not current.pre:
|
||||||
|
break
|
||||||
|
|
||||||
|
if bullish_trend:
|
||||||
|
# 上涨趋势:当前收盘价高于前一根收盘价
|
||||||
|
if current.close <= current.pre.close:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# 下跌趋势:当前收盘价低于前一根收盘价
|
||||||
|
if current.close >= current.pre.close:
|
||||||
|
break
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
current = current.pre
|
||||||
|
|
||||||
|
return count >= min_bars
|
||||||
|
|
||||||
|
# 1. 早晨之星:第一根阴线,第二根十字星或小实体,第三根阳线
|
||||||
|
# 要求前面有明显的下跌趋势
|
||||||
|
if not first_bullish and third_bullish and \
|
||||||
|
second_body < first_body * 0.3 and \
|
||||||
|
third_body > first_body * 0.5 and \
|
||||||
|
max(second_klu.open, second_klu.close) < first_klu.close and \
|
||||||
|
min(second_klu.open, second_klu.close) < third_klu.open and \
|
||||||
|
third_klu.close > (first_klu.open + first_klu.close) / 2 and \
|
||||||
|
check_long_trend(first_klu, bullish_trend=False, min_bars=7):
|
||||||
|
third_klu.set_pattern(Chan_KLU_PATTERN.MORNING_STAR)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. 黄昏之星:第一根阳线,第二根十字星或小实体,第三根阴线
|
||||||
|
# 要求前面有明显的上涨趋势
|
||||||
|
if first_bullish and not third_bullish and \
|
||||||
|
second_body < first_body * 0.3 and \
|
||||||
|
third_body > first_body * 0.5 and \
|
||||||
|
min(second_klu.open, second_klu.close) > first_klu.close and \
|
||||||
|
max(second_klu.open, second_klu.close) > third_klu.open and \
|
||||||
|
third_klu.close < (first_klu.open + first_klu.close) / 2 and \
|
||||||
|
check_long_trend(first_klu, bullish_trend=True, min_bars=7):
|
||||||
|
third_klu.set_pattern(Chan_KLU_PATTERN.EVENING_STAR)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. 平顶:三根K线的最高点几乎相同(上升趋势中更有意义)
|
||||||
|
# 要求前面有明显的上涨趋势
|
||||||
|
if (abs(first_klu.high - second_klu.high) / first_klu.high < 0.0002 and
|
||||||
|
abs(second_klu.high - third_klu.high) / second_klu.high < 0.0002 and
|
||||||
|
check_long_trend(first_klu, bullish_trend=True, min_bars=7)):
|
||||||
|
# 额外确认:价格接近阻力位或关键技术指标
|
||||||
|
is_near_resistance = False
|
||||||
|
|
||||||
|
# 检查是否接近EMA52阻力位
|
||||||
|
if first_klu.ema52 > 0:
|
||||||
|
resistance_level = first_klu.ema52
|
||||||
|
if abs(first_klu.high - resistance_level) / resistance_level < 0.01:
|
||||||
|
is_near_resistance = True
|
||||||
|
|
||||||
|
# 检查是否有成交量确认(成交量减少表示上涨动能减弱)
|
||||||
|
volume_confirmation = False
|
||||||
|
if (first_klu.volume > 0 and second_klu.volume > 0 and third_klu.volume > 0 and
|
||||||
|
third_klu.volume < second_klu.volume and second_klu.volume < first_klu.volume):
|
||||||
|
volume_confirmation = True
|
||||||
|
|
||||||
|
if is_near_resistance or volume_confirmation:
|
||||||
|
third_klu.set_pattern(Chan_KLU_PATTERN.TWEEZER_TOP)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 4. 平底:三根K线的最低点几乎相同(下降趋势中更有意义)
|
||||||
|
# 要求前面有明显的下跌趋势
|
||||||
|
if (abs(first_klu.low - second_klu.low) / first_klu.low < 0.0002 and
|
||||||
|
abs(second_klu.low - third_klu.low) / second_klu.low < 0.0002 and
|
||||||
|
check_long_trend(first_klu, bullish_trend=False, min_bars=7)):
|
||||||
|
# 额外确认:价格接近支撑位或关键技术指标
|
||||||
|
is_near_support = False
|
||||||
|
|
||||||
|
# 检查是否接近EMA52支撑位
|
||||||
|
if first_klu.ema52 > 0:
|
||||||
|
support_level = first_klu.ema52
|
||||||
|
if abs(first_klu.low - support_level) / support_level < 0.01:
|
||||||
|
is_near_support = True
|
||||||
|
|
||||||
|
# 检查是否有成交量确认(成交量减少表示下跌动能减弱)
|
||||||
|
volume_confirmation = False
|
||||||
|
if (first_klu.volume > 0 and second_klu.volume > 0 and third_klu.volume > 0 and
|
||||||
|
third_klu.volume < second_klu.volume and second_klu.volume < first_klu.volume):
|
||||||
|
volume_confirmation = True
|
||||||
|
|
||||||
|
if is_near_support or volume_confirmation:
|
||||||
|
third_klu.set_pattern(Chan_KLU_PATTERN.TWEEZER_BOTTOM)
|
||||||
|
return
|
||||||
|
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
"""TF_DF builder mixin — 由 split_tfdf_builders 自动生成,逻辑与原 TF_DF 一致。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
from technical.util import resample_to_interval
|
||||||
|
|
||||||
|
from chanlun.core.ChanBI import ChanBI
|
||||||
|
from chanlun.core.ChanBIZS import ChanBIZS
|
||||||
|
from chanlun.core.ChanBSP import ChanBSP
|
||||||
|
from chanlun.core.ChanEnum import (
|
||||||
|
Chan_BI_DIR,
|
||||||
|
Chan_BSP_DIR,
|
||||||
|
Chan_BSP_TYPE,
|
||||||
|
Chan_FX_TYPE,
|
||||||
|
Chan_K_DIR,
|
||||||
|
Chan_KLC_FX,
|
||||||
|
Chan_KLC_STATE,
|
||||||
|
Chan_KLINE_DIR,
|
||||||
|
Chan_KLU_PATTERN,
|
||||||
|
Chan_PRICE_TREND,
|
||||||
|
Chan_SEG_DIR,
|
||||||
|
Chan_ZS_DIR,
|
||||||
|
)
|
||||||
|
from chanlun.core.ChanKLC import ChanKLC
|
||||||
|
from chanlun.core.ChanKLU import ChanKLU
|
||||||
|
from chanlun.core.ChanSBI import ChanSBI
|
||||||
|
from chanlun.core.ChanSEG import ChanSEG
|
||||||
|
from chanlun.core.ChanZS import ChanZS, ChanZS_Big
|
||||||
|
from chanlun.indicators.ChanMACD import ChanMACD
|
||||||
|
|
||||||
|
|
||||||
|
class SegBuilderMixin:
|
||||||
|
def get_seg_list(self, bi_list):
|
||||||
|
seg_list = []
|
||||||
|
up_bi_list = []
|
||||||
|
down_bi_list = []
|
||||||
|
last_up_bi = None
|
||||||
|
last_down_bi = None
|
||||||
|
last_up_sbi = None
|
||||||
|
last_down_sbi = None
|
||||||
|
last_seg = None
|
||||||
|
up_sbi_list = []
|
||||||
|
down_sbi_list = []
|
||||||
|
look_for_bottom = False
|
||||||
|
look_for_top = False
|
||||||
|
for bi in bi_list:
|
||||||
|
#print(len(up_sbi_list), len(down_sbi_list))
|
||||||
|
if len(seg_list) > 0:
|
||||||
|
# Last seg is up
|
||||||
|
if last_seg.dir == Chan_SEG_DIR.UP:
|
||||||
|
if bi.dir == Chan_BI_DIR.DOWN:
|
||||||
|
if len(down_sbi_list) > 1:
|
||||||
|
# Check down sbi inclusion
|
||||||
|
included = last_down_sbi.check_bi_included(bi)
|
||||||
|
if not included:
|
||||||
|
down_sbi = ChanSBI(bi, len(down_sbi_list), bi.dir)
|
||||||
|
last_down_sbi.set_next(down_sbi)
|
||||||
|
last_down_sbi.set_end_bi(last_down_bi)
|
||||||
|
down_sbi.set_pre(last_down_sbi)
|
||||||
|
down_sbi_list.append(down_sbi)
|
||||||
|
fx = last_down_sbi.check_fx()
|
||||||
|
# Found top
|
||||||
|
if fx == Chan_FX_TYPE.TOP:
|
||||||
|
if look_for_top:
|
||||||
|
seg_list[-2].set_sure(bi)
|
||||||
|
look_for_top = False
|
||||||
|
#print(bi.start_time, look_for_top, "UP 1")
|
||||||
|
# Has gap and search for bottom fx
|
||||||
|
if last_down_sbi.has_fx_gap:
|
||||||
|
look_for_bottom = True
|
||||||
|
last_seg.pre_set_end_bi(bi_list[last_down_sbi.start_bi.index - 1])
|
||||||
|
seg = ChanSEG(last_down_sbi.start_bi, len(seg_list), Chan_SEG_DIR.DOWN, bi)
|
||||||
|
seg_list.append(seg)
|
||||||
|
last_seg.set_next(seg)
|
||||||
|
seg.set_pre(last_seg)
|
||||||
|
last_seg = seg
|
||||||
|
up_sbi_list = []
|
||||||
|
last_up_sbi = ChanSBI(last_up_bi, len(up_sbi_list), last_up_bi.dir)
|
||||||
|
up_sbi_list.append(last_up_sbi)
|
||||||
|
#up_sbi_list.append(last_up_sbi)
|
||||||
|
#print(last_up_bi.start_time, last_up_sbi.start_bi.start_time, "Reset up sbi list 1")
|
||||||
|
#print(bi.start_time, look_for_top, "UP 2")
|
||||||
|
# No gap end SEG
|
||||||
|
else:
|
||||||
|
if look_for_bottom:
|
||||||
|
look_for_bottom = False
|
||||||
|
last_seg.set_start_bi(last_down_sbi.start_bi)
|
||||||
|
seg_list[-2].set_end_bi(bi_list[last_down_sbi.start_bi.index - 1], bi)
|
||||||
|
up_sbi_list = []
|
||||||
|
last_up_sbi = ChanSBI(last_up_bi, len(up_sbi_list), last_up_bi.dir)
|
||||||
|
up_sbi_list.append(last_up_sbi)
|
||||||
|
last_seg.add_bi(bi)
|
||||||
|
#up_sbi_list.append(last_up_sbi)
|
||||||
|
#print(last_up_bi.start_time, last_up_sbi.start_bi.start_time, "Reset up sbi list 2")
|
||||||
|
#print(bi.start_time, look_for_top, "UP 3")
|
||||||
|
else:
|
||||||
|
last_seg.set_end_bi(bi_list[last_down_sbi.start_bi.index - 1], bi)
|
||||||
|
seg = ChanSEG(last_down_sbi.start_bi, len(seg_list), Chan_SEG_DIR.DOWN, bi)
|
||||||
|
seg_list.append(seg)
|
||||||
|
last_seg.set_next(seg)
|
||||||
|
seg.set_pre(last_seg)
|
||||||
|
last_seg = seg
|
||||||
|
#print(last_down_sbi.end_bi.start_time, "Normal UP SEG", last_up_sbi.start_bi.start_time, bi.start_time)
|
||||||
|
#l_up_sbi = up_sbi_list[-1]
|
||||||
|
up_sbi_list = []
|
||||||
|
last_up_sbi = ChanSBI(last_up_bi, len(up_sbi_list), last_up_bi.dir)
|
||||||
|
up_sbi_list.append(last_up_sbi)
|
||||||
|
#up_sbi_list.append(last_up_sbi)
|
||||||
|
#print(last_up_bi.start_time, last_up_sbi.start_bi.start_time, "Reset up sbi list 3")
|
||||||
|
last_down_sbi = down_sbi
|
||||||
|
last_seg.add_bi(bi)
|
||||||
|
else:
|
||||||
|
if len(down_sbi_list) == 1:
|
||||||
|
included = last_down_sbi.check_bi_included(bi)
|
||||||
|
if not included:
|
||||||
|
down_sbi = ChanSBI(bi, len(down_sbi_list), bi.dir)
|
||||||
|
last_down_sbi.set_next(down_sbi)
|
||||||
|
last_down_sbi.set_end_bi(last_down_bi)
|
||||||
|
down_sbi.set_pre(last_down_sbi)
|
||||||
|
down_sbi_list.append(down_sbi)
|
||||||
|
last_down_sbi = down_sbi
|
||||||
|
#print(bi.start_time, look_for_top, "UP 4")
|
||||||
|
last_seg.add_bi(bi)
|
||||||
|
|
||||||
|
else:
|
||||||
|
last_down_sbi = ChanSBI(bi, len(down_sbi_list), bi.dir)
|
||||||
|
down_sbi_list.append(last_down_sbi)
|
||||||
|
last_seg.add_bi(bi)
|
||||||
|
#print(bi.start_time, look_for_top, "UP 5")
|
||||||
|
else:
|
||||||
|
if last_up_sbi:
|
||||||
|
included = last_up_sbi.check_bi_included(bi)
|
||||||
|
if not included:
|
||||||
|
up_sbi = ChanSBI(bi, len(up_sbi_list), bi.dir)
|
||||||
|
last_up_sbi.set_next(up_sbi)
|
||||||
|
last_up_sbi.set_end_bi(last_up_bi)
|
||||||
|
up_sbi.set_pre(last_up_sbi)
|
||||||
|
up_sbi_list.append(up_sbi)
|
||||||
|
last_up_sbi = up_sbi
|
||||||
|
#print(bi.start_time, look_for_top, "UP 6")
|
||||||
|
last_seg.add_bi(bi)
|
||||||
|
|
||||||
|
# Last seg is down
|
||||||
|
else:
|
||||||
|
if bi.dir == Chan_BI_DIR.UP:
|
||||||
|
if len(up_sbi_list) > 1:
|
||||||
|
# Check down sbi inclusion
|
||||||
|
included = last_up_sbi.check_bi_included(bi)
|
||||||
|
if not included:
|
||||||
|
up_sbi = ChanSBI(bi, len(up_sbi_list), bi.dir)
|
||||||
|
last_up_sbi.set_next(up_sbi)
|
||||||
|
last_up_sbi.set_end_bi(last_up_bi)
|
||||||
|
up_sbi.set_pre(last_up_sbi)
|
||||||
|
up_sbi_list.append(up_sbi)
|
||||||
|
fx = last_up_sbi.check_fx()
|
||||||
|
# Found bottom
|
||||||
|
if fx == Chan_FX_TYPE.BOTTOM:
|
||||||
|
if look_for_bottom:
|
||||||
|
seg_list[-2].set_sure(bi)
|
||||||
|
look_for_bottom = False
|
||||||
|
#print(bi.start_time, look_for_top, "DOWN 1")
|
||||||
|
# Has gap and search for bottom fx
|
||||||
|
if last_up_sbi.has_fx_gap:
|
||||||
|
look_for_top = True
|
||||||
|
last_seg.pre_set_end_bi(bi_list[last_up_sbi.start_bi.index - 1])
|
||||||
|
seg = ChanSEG(last_up_sbi.start_bi, len(seg_list), Chan_SEG_DIR.UP, bi)
|
||||||
|
seg_list.append(seg)
|
||||||
|
last_seg.set_next(seg)
|
||||||
|
seg.set_pre(last_seg)
|
||||||
|
last_seg = seg
|
||||||
|
down_sbi_list = []
|
||||||
|
last_down_sbi = ChanSBI(last_down_bi, len(down_sbi_list), last_down_bi.dir)
|
||||||
|
down_sbi_list.append(last_down_sbi)
|
||||||
|
#down_sbi_list.append(last_down_sbi)
|
||||||
|
#print(last_down_bi.start_time, last_down_sbi.start_bi.start_time, "Reset down sbi list 1")
|
||||||
|
#print(bi.start_time, look_for_top, "DOWN 2")
|
||||||
|
# No gap end SEG
|
||||||
|
else:
|
||||||
|
if look_for_top:
|
||||||
|
look_for_top = False
|
||||||
|
last_seg.set_start_bi(last_up_sbi.start_bi)
|
||||||
|
seg_list[-2].set_end_bi(bi_list[last_up_sbi.start_bi.index - 1], bi)
|
||||||
|
down_sbi_list = []
|
||||||
|
last_down_sbi = ChanSBI(last_down_bi, len(down_sbi_list), last_down_bi.dir)
|
||||||
|
down_sbi_list.append(last_down_sbi)
|
||||||
|
last_seg.add_bi(bi)
|
||||||
|
#down_sbi_list.append(last_down_sbi)
|
||||||
|
#print(last_down_bi.start_time, last_down_sbi.start_bi.start_time, "Reset down sbi list 2")
|
||||||
|
#print(bi.start_time, look_for_top, "DOWN 3")
|
||||||
|
else:
|
||||||
|
last_seg.set_end_bi(bi_list[last_up_sbi.start_bi.index - 1], bi)
|
||||||
|
seg = ChanSEG(last_up_sbi.start_bi, len(seg_list), Chan_SEG_DIR.UP, bi)
|
||||||
|
#print(last_up_sbi.start_bi.start_time)
|
||||||
|
last_seg.set_next(seg)
|
||||||
|
seg.set_pre(last_seg)
|
||||||
|
seg_list.append(seg)
|
||||||
|
last_seg = seg
|
||||||
|
#print(last_up_sbi.end_bi.start_time, "Normal DOWN SEG", last_down_sbi.start_bi.start_time, bi.start_time)
|
||||||
|
down_sbi_list = []
|
||||||
|
last_down_sbi = ChanSBI(last_down_bi, len(down_sbi_list), last_down_bi.dir)
|
||||||
|
down_sbi_list.append(last_down_sbi)
|
||||||
|
#down_sbi_list.append(last_down_sbi)
|
||||||
|
#print(last_down_bi.start_time, last_down_sbi.start_bi.start_time, "Reset down sbi list 3")
|
||||||
|
last_up_sbi = up_sbi
|
||||||
|
last_seg.add_bi(bi)
|
||||||
|
else:
|
||||||
|
if len(up_sbi_list) == 1:
|
||||||
|
#last_up_sbi = up_sbi_list[-1]
|
||||||
|
included = last_up_sbi.check_bi_included(bi)
|
||||||
|
if not included:
|
||||||
|
up_sbi = ChanSBI(bi, len(up_sbi_list), bi.dir)
|
||||||
|
last_up_sbi.set_next(up_sbi)
|
||||||
|
last_up_sbi.set_end_bi(last_up_bi)
|
||||||
|
up_sbi.set_pre(last_up_sbi)
|
||||||
|
up_sbi_list.append(up_sbi)
|
||||||
|
last_up_sbi = up_sbi
|
||||||
|
last_seg.add_bi(bi)
|
||||||
|
#print(bi.start_time, look_for_top, "DOWN 4")
|
||||||
|
else:
|
||||||
|
last_up_sbi = ChanSBI(bi, len(up_sbi_list), bi.dir)
|
||||||
|
up_sbi_list.append(last_up_sbi)
|
||||||
|
last_seg.add_bi(bi)
|
||||||
|
#print(bi.start_time, look_for_top, "DOWN 5")
|
||||||
|
else:
|
||||||
|
if last_down_sbi:
|
||||||
|
included = last_down_sbi.check_bi_included(bi)
|
||||||
|
if not included:
|
||||||
|
down_sbi = ChanSBI(bi, len(down_sbi_list), bi.dir)
|
||||||
|
last_down_sbi.set_next(down_sbi)
|
||||||
|
last_down_sbi.set_end_bi(last_down_bi)
|
||||||
|
down_sbi.set_pre(last_down_sbi)
|
||||||
|
down_sbi_list.append(down_sbi)
|
||||||
|
last_down_sbi = down_sbi
|
||||||
|
last_seg.add_bi(bi)
|
||||||
|
#print(bi.start_time, look_for_top, look_for_bottom, "DOWN 6")
|
||||||
|
# len(seg_list) = 0
|
||||||
|
else:
|
||||||
|
if bi.check_overlap():
|
||||||
|
if bi.dir == Chan_BI_DIR.UP:
|
||||||
|
seg = ChanSEG(bi, len(seg_list), Chan_SEG_DIR.UP, bi)
|
||||||
|
last_up_bi = bi
|
||||||
|
last_up_sbi = ChanSBI(bi, len(up_sbi_list), bi.dir)
|
||||||
|
seg_list.append(seg)
|
||||||
|
last_seg = seg
|
||||||
|
#print(bi.start_time, 'Create first UP SEG')
|
||||||
|
else:
|
||||||
|
seg = ChanSEG(bi, len(seg_list), Chan_SEG_DIR.DOWN, bi)
|
||||||
|
last_down_bi = bi
|
||||||
|
last_down_sbi = ChanSBI(bi, len(down_sbi_list), bi.dir)
|
||||||
|
seg_list.append(seg)
|
||||||
|
last_seg = seg
|
||||||
|
#print(bi.start_time, 'Create first DOWN SEG')
|
||||||
|
if bi.dir == Chan_BI_DIR.UP:
|
||||||
|
last_up_bi = bi
|
||||||
|
up_bi_list.append(bi)
|
||||||
|
else:
|
||||||
|
last_down_bi = bi
|
||||||
|
down_bi_list.append(bi)
|
||||||
|
"""
|
||||||
|
if len(seg_list) > 1:
|
||||||
|
seg = seg_list[-1]
|
||||||
|
last_seg = seg_list[-2]
|
||||||
|
last_seg_bi = last_seg.bi_list[-3]
|
||||||
|
bi_index = seg.start_bi.index
|
||||||
|
for i in range(bi_index, len(bi_list) - 1):
|
||||||
|
# last seg is down
|
||||||
|
if seg.dir == Chan_SEG_DIR.UP:
|
||||||
|
if bi_list[i].dir == Chan_BI_DIR.UP:
|
||||||
|
last_seg_peak = last_seg_bi.high
|
||||||
|
if bi_list[i].high > last_seg_peak:
|
||||||
|
# The confirmed
|
||||||
|
print("Last UP seg is broken, create a new seg. 1")
|
||||||
|
seg.pre_set_end_bi(bi_list[i])
|
||||||
|
seg = ChanSEG(bi_list[i+1], len(seg_list), Chan_SEG_DIR.DOWN, bi)
|
||||||
|
seg_list.append(seg)
|
||||||
|
last_seg = seg_list[-2]
|
||||||
|
if len(last_seg.bi_list) > 3:
|
||||||
|
last_seg_bi = last_seg.bi_list[-3]
|
||||||
|
|
||||||
|
else:
|
||||||
|
if bi_list[i].dir == Chan_BI_DIR.DOWN:
|
||||||
|
last_seg_peak = last_seg_bi.low
|
||||||
|
if bi_list[i].low < last_seg_peak:
|
||||||
|
print("Last DOWN seg is broken, create a new seg. 1")
|
||||||
|
seg.pre_set_end_bi(bi_list[i])
|
||||||
|
seg = ChanSEG(bi_list[i+1], len(seg_list), Chan_SEG_DIR.UP, bi)
|
||||||
|
seg_list.append(seg)
|
||||||
|
last_seg = seg_list[-2]
|
||||||
|
if len(last_seg.bi_list) > 3:
|
||||||
|
last_seg_bi = last_seg.bi_list[-3]
|
||||||
|
else:
|
||||||
|
if len(seg_list) == 1:
|
||||||
|
last_seg = seg_list[-1]
|
||||||
|
bi_index = last_seg.bi_list[0].index
|
||||||
|
for i in range(bi_index, len(bi_list) - 1):
|
||||||
|
if i > bi_index + 2:
|
||||||
|
last_seg_peak = bi_list[i-2].high
|
||||||
|
# last seg is down
|
||||||
|
if last_seg.dir == Chan_SEG_DIR.DOWN:
|
||||||
|
if bi_list[i].dir == Chan_BI_DIR.UP:
|
||||||
|
if bi_list[i].high > last_seg_peak:
|
||||||
|
print("Last seg is broken, create a new seg. 2")
|
||||||
|
last_seg.pre_set_end_bi(bi_list[i-1])
|
||||||
|
seg = ChanSEG(bi_list[i], len(seg_list), Chan_SEG_DIR.UP, bi)
|
||||||
|
seg_list.append(seg)
|
||||||
|
last_seg = seg
|
||||||
|
last_seg_bi = bi_list[i]
|
||||||
|
break
|
||||||
|
"""
|
||||||
|
#self.cal_bi_zs(seg_list)
|
||||||
|
return seg_list
|
||||||
@@ -0,0 +1,699 @@
|
|||||||
|
"""TF_DF builder mixin — 由 split_tfdf_builders 自动生成,逻辑与原 TF_DF 一致。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
from technical.util import resample_to_interval
|
||||||
|
|
||||||
|
from chanlun.core.ChanBI import ChanBI
|
||||||
|
from chanlun.core.ChanBIZS import ChanBIZS
|
||||||
|
from chanlun.core.ChanBSP import ChanBSP
|
||||||
|
from chanlun.core.ChanEnum import (
|
||||||
|
Chan_BI_DIR,
|
||||||
|
Chan_BSP_DIR,
|
||||||
|
Chan_BSP_TYPE,
|
||||||
|
Chan_FX_TYPE,
|
||||||
|
Chan_K_DIR,
|
||||||
|
Chan_KLC_FX,
|
||||||
|
Chan_KLC_STATE,
|
||||||
|
Chan_KLINE_DIR,
|
||||||
|
Chan_KLU_PATTERN,
|
||||||
|
Chan_PRICE_TREND,
|
||||||
|
Chan_SEG_DIR,
|
||||||
|
Chan_ZS_DIR,
|
||||||
|
)
|
||||||
|
from chanlun.core.ChanKLC import ChanKLC
|
||||||
|
from chanlun.core.ChanKLU import ChanKLU
|
||||||
|
from chanlun.core.ChanSBI import ChanSBI
|
||||||
|
from chanlun.core.ChanSEG import ChanSEG
|
||||||
|
from chanlun.core.ChanZS import ChanZS, ChanZS_Big
|
||||||
|
from chanlun.indicators.ChanMACD import ChanMACD
|
||||||
|
|
||||||
|
|
||||||
|
class ZsBuilderMixin:
|
||||||
|
def get_zs_state(self, df):
|
||||||
|
bi_list = self.cal_bi_list(self.get_klc_list(self.get_kl_data(df)))
|
||||||
|
seg_list = self.get_seg_list(bi_list)
|
||||||
|
zs_list = self.calculate_zs(seg_list)
|
||||||
|
for zs in zs_list:
|
||||||
|
last_zs = zs
|
||||||
|
return zs_list
|
||||||
|
|
||||||
|
def cal_bi_zs(self, seg_list):
|
||||||
|
bi_zs_list = []
|
||||||
|
for seg in seg_list:
|
||||||
|
zs_list = seg.cal_bi_zs()
|
||||||
|
if len(zs_list) > 0:
|
||||||
|
bi_zs_list = list(bi_zs_list) + list(zs_list)
|
||||||
|
return bi_zs_list
|
||||||
|
# 跨段不相连的中枢
|
||||||
|
|
||||||
|
def cal_bi_zs_list(self, bi_list):
|
||||||
|
"""
|
||||||
|
根据缠论笔中枢定义计算中枢(参照 get_zs_list 线段中枢判断规则)
|
||||||
|
从第4根笔开始(索引3),每3根笔为一组检查
|
||||||
|
上涨中枢:后中枢 zd > 前中枢 zg(不重叠上移)
|
||||||
|
下跌中枢:后中枢 zg < 前中枢 zd(不重叠下移)
|
||||||
|
中枢可按两笔一组继续扩展到5根、7根...
|
||||||
|
"""
|
||||||
|
bi_zs_list = []
|
||||||
|
if len(bi_list) < 3:
|
||||||
|
return bi_zs_list
|
||||||
|
|
||||||
|
last_zs = None
|
||||||
|
start_idx = 3
|
||||||
|
|
||||||
|
while start_idx < len(bi_list):
|
||||||
|
if start_idx + 2 >= len(bi_list):
|
||||||
|
break
|
||||||
|
|
||||||
|
bi1 = bi_list[start_idx]
|
||||||
|
bi2 = bi_list[start_idx + 1]
|
||||||
|
bi3 = bi_list[start_idx + 2]
|
||||||
|
|
||||||
|
if not (bi1.is_sure and bi2.is_sure and bi3.is_sure):
|
||||||
|
start_idx += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
zg = min(bi1.high, bi2.high, bi3.high)
|
||||||
|
zd = max(bi1.low, bi2.low, bi3.low)
|
||||||
|
|
||||||
|
if zg <= zd:
|
||||||
|
start_idx += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
valid = False
|
||||||
|
if last_zs is None:
|
||||||
|
if bi1.dir == Chan_BI_DIR.DOWN:
|
||||||
|
zs_dir = Chan_ZS_DIR.UP
|
||||||
|
valid = (bi2.dir == Chan_BI_DIR.UP and bi3.dir == Chan_BI_DIR.DOWN)
|
||||||
|
else:
|
||||||
|
zs_dir = Chan_ZS_DIR.DOWN
|
||||||
|
valid = (bi2.dir == Chan_BI_DIR.DOWN and bi3.dir == Chan_BI_DIR.UP)
|
||||||
|
else:
|
||||||
|
is_up_zs = zg > last_zs.zg
|
||||||
|
is_down_zs = zd < last_zs.zd
|
||||||
|
|
||||||
|
if is_up_zs:
|
||||||
|
zs_dir = Chan_ZS_DIR.UP
|
||||||
|
valid = (bi1.dir == Chan_BI_DIR.DOWN and bi2.dir == Chan_BI_DIR.UP and bi3.dir == Chan_BI_DIR.DOWN)
|
||||||
|
elif is_down_zs:
|
||||||
|
zs_dir = Chan_ZS_DIR.DOWN
|
||||||
|
valid = (bi1.dir == Chan_BI_DIR.UP and bi2.dir == Chan_BI_DIR.DOWN and bi3.dir == Chan_BI_DIR.UP)
|
||||||
|
|
||||||
|
if not valid:
|
||||||
|
start_idx += 1
|
||||||
|
continue
|
||||||
|
gg = max(bi1.high, bi2.high, bi3.high)
|
||||||
|
dd = min(bi1.low, bi2.low, bi3.low)
|
||||||
|
zs = ChanBIZS(bi1, len(bi_zs_list), zs_dir)
|
||||||
|
zs.set_zg(zg)
|
||||||
|
zs.set_zd(zd)
|
||||||
|
zs.set_gg(gg)
|
||||||
|
zs.set_dd(dd)
|
||||||
|
zs.is_sure = False
|
||||||
|
zs.bi_list = [bi1, bi2, bi3]
|
||||||
|
|
||||||
|
added_after_leave = []
|
||||||
|
leave_index = start_idx + 4
|
||||||
|
while leave_index < len(bi_list):
|
||||||
|
b = bi_list[leave_index]
|
||||||
|
if not b.is_sure:
|
||||||
|
break
|
||||||
|
if b.high >= zs.zd and b.low <= zs.zg:
|
||||||
|
added_after_leave.append(b.pre)
|
||||||
|
added_after_leave.append(b)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
leave_index += 2
|
||||||
|
|
||||||
|
if added_after_leave:
|
||||||
|
bis_for_zs = list(zs.bi_list) + list(added_after_leave)
|
||||||
|
bi_highs = [bi.high for bi in bis_for_zs]
|
||||||
|
bi_lows = [bi.low for bi in bis_for_zs]
|
||||||
|
zs.set_gg(max(bi_highs))
|
||||||
|
zs.set_dd(min(bi_lows))
|
||||||
|
zs.bi_list = bis_for_zs
|
||||||
|
bi = bis_for_zs[-1]
|
||||||
|
if bi.is_sure:
|
||||||
|
zs.set_end_bi(bi, bi.sure_time)
|
||||||
|
|
||||||
|
start_idx = start_idx + len(added_after_leave)
|
||||||
|
else:
|
||||||
|
zs.set_end_bi(bi3, bi3.sure_time)
|
||||||
|
|
||||||
|
if last_zs:
|
||||||
|
last_zs.set_next(zs)
|
||||||
|
zs.set_pre(last_zs)
|
||||||
|
|
||||||
|
bi_zs_list.append(zs)
|
||||||
|
last_zs = zs
|
||||||
|
|
||||||
|
start_idx += 4
|
||||||
|
|
||||||
|
if last_zs:
|
||||||
|
last_zs.is_sure = bi_list[-1].is_sure
|
||||||
|
|
||||||
|
if last_zs and not last_zs.is_sure:
|
||||||
|
if last_zs.bi_list and len(last_zs.bi_list) > 0:
|
||||||
|
last_bi_of_zs = last_zs.bi_list[-1]
|
||||||
|
last_bi_idx = -1
|
||||||
|
for i, bi in enumerate(bi_list):
|
||||||
|
if bi == last_bi_of_zs:
|
||||||
|
last_bi_idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
has_leave = False
|
||||||
|
if last_bi_idx >= 0 and last_bi_idx + 1 < len(bi_list):
|
||||||
|
for i in range(last_bi_idx + 1, len(bi_list)):
|
||||||
|
bi = bi_list[i]
|
||||||
|
if bi.is_sure:
|
||||||
|
leave = (bi.low > last_zs.zg and bi.high > last_zs.zg) or \
|
||||||
|
(bi.high < last_zs.zd and bi.low < last_zs.zd)
|
||||||
|
if leave:
|
||||||
|
has_leave = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if has_leave:
|
||||||
|
if last_bi_of_zs.is_sure:
|
||||||
|
last_zs.set_end_bi(last_bi_of_zs, last_bi_of_zs.sure_time)
|
||||||
|
return bi_zs_list
|
||||||
|
|
||||||
|
def get_bi_zs_list(self, bi_list):
|
||||||
|
"""
|
||||||
|
根据缠论笔中枢定义计算中枢(完全参照 get_seg_zs_list 线段中枢判断规则)
|
||||||
|
从第4根笔开始(索引3),每3根笔为一组检查
|
||||||
|
上涨中枢:后中枢 zd > 前中枢 zg(不重叠上移)
|
||||||
|
下跌中枢:后中枢 zg < 前中枢 zd(不重叠下移)
|
||||||
|
盘整/扩张:后中枢与前中枢整体区间有交集 → 合并扩展
|
||||||
|
中枢可按两笔一组继续扩展到5根、7根...
|
||||||
|
"""
|
||||||
|
bi_zs_list = []
|
||||||
|
if len(bi_list) < 3:
|
||||||
|
return bi_zs_list
|
||||||
|
|
||||||
|
last_zs = None
|
||||||
|
start_idx = 3
|
||||||
|
|
||||||
|
while start_idx < len(bi_list):
|
||||||
|
if start_idx + 2 >= len(bi_list):
|
||||||
|
break
|
||||||
|
|
||||||
|
bi1 = bi_list[start_idx]
|
||||||
|
bi2 = bi_list[start_idx + 1]
|
||||||
|
bi3 = bi_list[start_idx + 2]
|
||||||
|
|
||||||
|
if not (bi1.is_sure and bi2.is_sure and bi3.is_sure):
|
||||||
|
start_idx += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
zg = min(bi1.high, bi2.high, bi3.high)
|
||||||
|
zd = max(bi1.low, bi2.low, bi3.low)
|
||||||
|
|
||||||
|
if zg <= zd:
|
||||||
|
start_idx += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
valid = False
|
||||||
|
if last_zs is None:
|
||||||
|
if bi1.dir == Chan_BI_DIR.DOWN:
|
||||||
|
zs_dir = Chan_ZS_DIR.UP
|
||||||
|
valid = (bi2.dir == Chan_BI_DIR.UP and bi3.dir == Chan_BI_DIR.DOWN)
|
||||||
|
else:
|
||||||
|
zs_dir = Chan_ZS_DIR.DOWN
|
||||||
|
valid = (bi2.dir == Chan_BI_DIR.DOWN and bi3.dir == Chan_BI_DIR.UP)
|
||||||
|
else:
|
||||||
|
is_up_zs = zd > last_zs.zg
|
||||||
|
is_down_zs = zg < last_zs.zd
|
||||||
|
|
||||||
|
if is_up_zs:
|
||||||
|
zs_dir = Chan_ZS_DIR.UP
|
||||||
|
valid = (bi1.dir == Chan_BI_DIR.DOWN and bi2.dir == Chan_BI_DIR.UP and bi3.dir == Chan_BI_DIR.DOWN)
|
||||||
|
elif is_down_zs:
|
||||||
|
zs_dir = Chan_ZS_DIR.DOWN
|
||||||
|
valid = (bi1.dir == Chan_BI_DIR.UP and bi2.dir == Chan_BI_DIR.DOWN and bi3.dir == Chan_BI_DIR.UP)
|
||||||
|
|
||||||
|
create_new_zs = False
|
||||||
|
if not valid:
|
||||||
|
# 如果新中枢和前一个中枢的中枢区间有重叠,不形成新中枢,合并扩展
|
||||||
|
if last_zs is not None:
|
||||||
|
is_in_last_zs = (zd > last_zs.zd and zd < last_zs.zg) or \
|
||||||
|
(zg < last_zs.zg and zg > last_zs.zd) or \
|
||||||
|
(zg > last_zs.zg and zd < last_zs.zd) or \
|
||||||
|
(zg < last_zs.zg and zd > last_zs.zd)
|
||||||
|
if is_in_last_zs:
|
||||||
|
# 扩展当前中枢:将 bi1-bi3 加入 last_zs
|
||||||
|
for bi in [bi1, bi2, bi3]:
|
||||||
|
if bi not in last_zs.bi_list:
|
||||||
|
last_zs.add_bi(bi)
|
||||||
|
create_new_zs = False
|
||||||
|
else:
|
||||||
|
start_idx += 1
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
start_idx += 1
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
create_new_zs = True
|
||||||
|
|
||||||
|
# 新中枢形成时确认前一个中枢
|
||||||
|
if last_zs and create_new_zs:
|
||||||
|
last_bi = last_zs.bi_list[-1]
|
||||||
|
if last_bi and last_bi.is_sure:
|
||||||
|
last_zs.is_sure = True
|
||||||
|
last_zs.set_end_bi(last_bi, last_bi.sure_time)
|
||||||
|
|
||||||
|
zs = last_zs
|
||||||
|
if create_new_zs:
|
||||||
|
gg = max(bi1.high, bi2.high, bi3.high)
|
||||||
|
dd = min(bi1.low, bi2.low, bi3.low)
|
||||||
|
zs = ChanBIZS(bi1, len(bi_zs_list), zs_dir)
|
||||||
|
zs.set_zg(zg)
|
||||||
|
zs.set_zd(zd)
|
||||||
|
zs.set_gg(gg)
|
||||||
|
zs.set_dd(dd)
|
||||||
|
zs.is_sure = False
|
||||||
|
zs.bi_list = [bi1, bi2, bi3]
|
||||||
|
|
||||||
|
# 离开后回抽扩展检查
|
||||||
|
added_after_leave = []
|
||||||
|
leave_index = start_idx + 4
|
||||||
|
while leave_index < len(bi_list):
|
||||||
|
b = bi_list[leave_index]
|
||||||
|
if not b.is_sure:
|
||||||
|
break
|
||||||
|
if b.high >= zs.zd and b.low <= zs.zg:
|
||||||
|
added_after_leave.append(b.pre)
|
||||||
|
added_after_leave.append(b)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
leave_index += 2
|
||||||
|
|
||||||
|
if added_after_leave:
|
||||||
|
bis_for_zs = list(zs.bi_list) + list(added_after_leave)
|
||||||
|
bi_highs = [bi.high for bi in bis_for_zs]
|
||||||
|
bi_lows = [bi.low for bi in bis_for_zs]
|
||||||
|
zs.set_gg(max(bi_highs))
|
||||||
|
zs.set_dd(min(bi_lows))
|
||||||
|
zs.bi_list = bis_for_zs
|
||||||
|
bi = bis_for_zs[-1]
|
||||||
|
if bi.is_sure:
|
||||||
|
zs.set_end_bi(bi, bi.sure_time)
|
||||||
|
start_idx = start_idx + len(added_after_leave)
|
||||||
|
else:
|
||||||
|
if create_new_zs:
|
||||||
|
zs.set_end_bi(bi3, bi3.sure_time)
|
||||||
|
|
||||||
|
if create_new_zs:
|
||||||
|
if last_zs:
|
||||||
|
last_zs.set_next(zs)
|
||||||
|
zs.set_pre(last_zs)
|
||||||
|
bi_zs_list.append(zs)
|
||||||
|
last_zs = zs
|
||||||
|
|
||||||
|
start_idx += 4
|
||||||
|
|
||||||
|
# 最后一个中枢:根据 bi_list 最后一笔确认状态
|
||||||
|
if last_zs:
|
||||||
|
last_zs.is_sure = bi_list[-1].is_sure
|
||||||
|
|
||||||
|
if last_zs and not last_zs.is_sure:
|
||||||
|
if last_zs.bi_list and len(last_zs.bi_list) > 0:
|
||||||
|
last_bi_of_zs = last_zs.bi_list[-1]
|
||||||
|
last_bi_idx = -1
|
||||||
|
for i, bi in enumerate(bi_list):
|
||||||
|
if bi == last_bi_of_zs:
|
||||||
|
last_bi_idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
has_leave = False
|
||||||
|
if last_bi_idx >= 0 and last_bi_idx + 1 < len(bi_list):
|
||||||
|
for i in range(last_bi_idx + 1, len(bi_list)):
|
||||||
|
bi = bi_list[i]
|
||||||
|
if bi.is_sure:
|
||||||
|
leave = (bi.low > last_zs.zg and bi.high > last_zs.zg) or \
|
||||||
|
(bi.high < last_zs.zd and bi.low < last_zs.zd)
|
||||||
|
if leave:
|
||||||
|
has_leave = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if has_leave:
|
||||||
|
if last_bi_of_zs.is_sure:
|
||||||
|
last_zs.set_end_bi(last_bi_of_zs, last_bi_of_zs.sure_time)
|
||||||
|
|
||||||
|
return bi_zs_list
|
||||||
|
|
||||||
|
|
||||||
|
def cal_bi_zs_list_pure(self, bi_list):
|
||||||
|
bi_zs_list = []
|
||||||
|
if len(bi_list) < 3:
|
||||||
|
return bi_zs_list
|
||||||
|
|
||||||
|
def get_zs_range(bis):
|
||||||
|
zg = min(bi.high for bi in bis)
|
||||||
|
zd = max(bi.low for bi in bis)
|
||||||
|
return zg, zd
|
||||||
|
|
||||||
|
def is_bi_overlap_range(bi, zg, zd):
|
||||||
|
return bi.high >= zd and bi.low <= zg
|
||||||
|
|
||||||
|
def check_zs_position_filter(last_zs, zg, zd, bis):
|
||||||
|
if last_zs is None:
|
||||||
|
return True
|
||||||
|
if zg <= last_zs.zd:
|
||||||
|
return bis[0].dir == Chan_BI_DIR.UP and bis[-1].dir == Chan_BI_DIR.UP
|
||||||
|
if zd >= last_zs.zg:
|
||||||
|
return bis[0].dir == Chan_BI_DIR.DOWN and bis[-1].dir == Chan_BI_DIR.DOWN
|
||||||
|
return True
|
||||||
|
|
||||||
|
def set_zs_bi_list(zs, bis):
|
||||||
|
zs.bi_list = list(bis)
|
||||||
|
for bi in zs.bi_list:
|
||||||
|
bi.set_bi_zs(zs)
|
||||||
|
zs.set_gg(max(bi.high for bi in zs.bi_list))
|
||||||
|
zs.set_dd(min(bi.low for bi in zs.bi_list))
|
||||||
|
zs.classify_zs()
|
||||||
|
|
||||||
|
last_zs = None
|
||||||
|
start_idx = 0
|
||||||
|
while start_idx + 2 < len(bi_list):
|
||||||
|
bi1 = bi_list[start_idx]
|
||||||
|
bi2 = bi_list[start_idx + 1]
|
||||||
|
bi3 = bi_list[start_idx + 2]
|
||||||
|
|
||||||
|
if not (bi1.is_sure and bi2.is_sure and bi3.is_sure):
|
||||||
|
start_idx += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not (bi1.dir != bi2.dir and bi1.dir == bi3.dir):
|
||||||
|
start_idx += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
zg, zd = get_zs_range([bi1, bi2, bi3])
|
||||||
|
if zg <= zd:
|
||||||
|
start_idx += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
bis_for_zs = [bi1, bi2, bi3]
|
||||||
|
extend_idx = start_idx + 3
|
||||||
|
while extend_idx + 1 < len(bi_list):
|
||||||
|
leave_bi = bi_list[extend_idx]
|
||||||
|
back_bi = bi_list[extend_idx + 1]
|
||||||
|
if not (leave_bi.is_sure and back_bi.is_sure):
|
||||||
|
break
|
||||||
|
if not is_bi_overlap_range(back_bi, zg, zd):
|
||||||
|
break
|
||||||
|
bis_for_zs.append(leave_bi)
|
||||||
|
bis_for_zs.append(back_bi)
|
||||||
|
extend_idx += 2
|
||||||
|
|
||||||
|
if not check_zs_position_filter(last_zs, zg, zd, bis_for_zs):
|
||||||
|
start_idx += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
zs_dir = Chan_ZS_DIR.UP if bi1.dir == Chan_BI_DIR.DOWN else Chan_ZS_DIR.DOWN
|
||||||
|
zs = ChanBIZS(bi1, len(bi_zs_list), zs_dir)
|
||||||
|
zs.set_zg(zg)
|
||||||
|
zs.set_zd(zd)
|
||||||
|
|
||||||
|
set_zs_bi_list(zs, bis_for_zs)
|
||||||
|
zs.set_end_bi(bis_for_zs[-1], bis_for_zs[-1].sure_time)
|
||||||
|
|
||||||
|
if last_zs:
|
||||||
|
last_zs.set_next(zs)
|
||||||
|
zs.set_pre(last_zs)
|
||||||
|
|
||||||
|
bi_zs_list.append(zs)
|
||||||
|
last_zs = zs
|
||||||
|
start_idx = start_idx + len(bis_for_zs)
|
||||||
|
|
||||||
|
# 与 cal_bi_zs_list 一致:最后一笔未确认时末中枢标为未完成;若其后已出现确认的离开笔,仍按离开前最后一笔确认中枢结束
|
||||||
|
if last_zs:
|
||||||
|
last_zs.is_sure = bi_list[-1].is_sure
|
||||||
|
|
||||||
|
if last_zs and not last_zs.is_sure:
|
||||||
|
if last_zs.bi_list and len(last_zs.bi_list) > 0:
|
||||||
|
last_bi_of_zs = last_zs.bi_list[-1]
|
||||||
|
last_bi_idx = -1
|
||||||
|
for i, bi in enumerate(bi_list):
|
||||||
|
if bi == last_bi_of_zs:
|
||||||
|
last_bi_idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
has_leave = False
|
||||||
|
if last_bi_idx >= 0 and last_bi_idx + 1 < len(bi_list):
|
||||||
|
for i in range(last_bi_idx + 1, len(bi_list)):
|
||||||
|
bi = bi_list[i]
|
||||||
|
if bi.is_sure:
|
||||||
|
leave = (bi.low > last_zs.zg and bi.high > last_zs.zg) or \
|
||||||
|
(bi.high < last_zs.zd and bi.low < last_zs.zd)
|
||||||
|
if leave:
|
||||||
|
has_leave = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if has_leave:
|
||||||
|
if last_bi_of_zs.is_sure:
|
||||||
|
last_zs.set_end_bi(last_bi_of_zs, last_bi_of_zs.sure_time)
|
||||||
|
|
||||||
|
return bi_zs_list
|
||||||
|
|
||||||
|
def get_zs_list(self, bi_list, seg_list):
|
||||||
|
"""兼容历史 API:线段中枢列表。"""
|
||||||
|
return self.get_seg_zs_list(seg_list)
|
||||||
|
|
||||||
|
def calculate_seg_zs(self, seg_list):
|
||||||
|
return self.get_seg_zs_list(seg_list)
|
||||||
|
|
||||||
|
def get_seg_zs_list(self, seg_list):
|
||||||
|
"""
|
||||||
|
根据缠论线段中枢定义计算中枢
|
||||||
|
从第4根线段开始(索引3),每3根线段为一组检查
|
||||||
|
上涨中枢:后中枢 zd > 前中枢 zg(不重叠上移)
|
||||||
|
下跌中枢:后中枢 zg < 前中枢 zd(不重叠下移)
|
||||||
|
盘整/扩张:后中枢与前中枢整体区间(GG/DD)有交集
|
||||||
|
中枢可按两段一组继续扩展到5根、7根...
|
||||||
|
"""
|
||||||
|
zs_list = []
|
||||||
|
if len(seg_list) < 3:
|
||||||
|
return zs_list
|
||||||
|
|
||||||
|
last_zs = None
|
||||||
|
|
||||||
|
# 从第4根线段开始(索引3),每3根为一组
|
||||||
|
start_idx = 3
|
||||||
|
|
||||||
|
while start_idx < len(seg_list):
|
||||||
|
# 取连续3个线段
|
||||||
|
if start_idx + 2 >= len(seg_list):
|
||||||
|
break
|
||||||
|
|
||||||
|
seg1 = seg_list[start_idx]
|
||||||
|
seg2 = seg_list[start_idx + 1]
|
||||||
|
seg3 = seg_list[start_idx + 2]
|
||||||
|
|
||||||
|
# 三个线段都必须是已确认的
|
||||||
|
if not (seg1.is_sure and seg2.is_sure and seg3.is_sure):
|
||||||
|
start_idx += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 计算这3个线段的中枢区间
|
||||||
|
zg = min(seg1.high, seg2.high, seg3.high)
|
||||||
|
zd = max(seg1.low, seg2.low, seg3.low)
|
||||||
|
|
||||||
|
if zg <= zd:
|
||||||
|
start_idx += 1
|
||||||
|
#print(seg1.start_bi.start_klc.end_time, "not valid", zg, zd)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 判断中枢类型(按注释定义)
|
||||||
|
# 上涨中枢:后中枢 zd > 前中枢 zg(不重叠上移)
|
||||||
|
# 下跌中枢:后中枢 zg < 前中枢 zd(不重叠下移)
|
||||||
|
# 盘整/扩张:后中枢与前中枢区间有交集
|
||||||
|
if last_zs is None:
|
||||||
|
# 第一个中枢仅按线段形态判定方向
|
||||||
|
if seg1.dir == Chan_SEG_DIR.DOWN:
|
||||||
|
# 下跌+上涨+下跌,对应上涨中枢
|
||||||
|
zs_dir = Chan_ZS_DIR.UP
|
||||||
|
valid = (seg2.dir == Chan_SEG_DIR.UP and seg3.dir == Chan_SEG_DIR.DOWN)
|
||||||
|
else:
|
||||||
|
# 上涨+下跌+上涨,对应下跌中枢
|
||||||
|
zs_dir = Chan_ZS_DIR.DOWN
|
||||||
|
valid = (seg2.dir == Chan_SEG_DIR.DOWN and seg3.dir == Chan_SEG_DIR.UP)
|
||||||
|
else:
|
||||||
|
is_up_zs = zd > last_zs.zg
|
||||||
|
is_down_zs = zg < last_zs.zd
|
||||||
|
|
||||||
|
if is_up_zs:
|
||||||
|
# 不重叠上移
|
||||||
|
zs_dir = Chan_ZS_DIR.UP
|
||||||
|
valid = (seg1.dir == Chan_SEG_DIR.DOWN and seg2.dir == Chan_SEG_DIR.UP and seg3.dir == Chan_SEG_DIR.DOWN)
|
||||||
|
elif is_down_zs:
|
||||||
|
# 不重叠下移
|
||||||
|
zs_dir = Chan_ZS_DIR.DOWN
|
||||||
|
valid = (seg1.dir == Chan_SEG_DIR.UP and seg2.dir == Chan_SEG_DIR.DOWN and seg3.dir == Chan_SEG_DIR.UP)
|
||||||
|
create_new_zs = False
|
||||||
|
# 验证是否有效
|
||||||
|
if not valid:
|
||||||
|
# 如果新中枢和前一个中枢的中枢区间有重叠,不行成新中枢需要合并两个中枢
|
||||||
|
is_in_last_zs = (zd > last_zs.zd and zd < last_zs.zg) or (zg < last_zs.zg and zg > last_zs.zd) or (zg > last_zs.zg and zd < last_zs.zd) or (zg < last_zs.zg and zd > last_zs.zd)
|
||||||
|
if is_in_last_zs:
|
||||||
|
#print(seg1.start_time, "New zs is in last zs, not valid")
|
||||||
|
last_zs.extend_zs(seg_list[last_zs.seg_list[-1].index:(seg3.index + 1)])
|
||||||
|
create_new_zs = False
|
||||||
|
else:
|
||||||
|
start_idx += 1
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
create_new_zs = True
|
||||||
|
if last_zs and create_new_zs:
|
||||||
|
last_seg = last_zs.seg_list[-1]
|
||||||
|
last_bi = last_seg.end_bi
|
||||||
|
if last_bi:
|
||||||
|
last_zs.is_sure = True
|
||||||
|
last_zs.set_end_klc(last_bi.end_klc, last_bi.sure_time, 0, last_seg)
|
||||||
|
last_zs.set_end_seg(last_seg)
|
||||||
|
zs = last_zs
|
||||||
|
if create_new_zs:
|
||||||
|
# 创建新中枢
|
||||||
|
gg = max(seg1.high, seg2.high, seg3.high)
|
||||||
|
dd = min(seg1.low, seg2.low, seg3.low)
|
||||||
|
|
||||||
|
zs = ChanZS(seg1, len(zs_list), zs_dir)
|
||||||
|
zs.set_zg(zg)
|
||||||
|
zs.set_zd(zd)
|
||||||
|
zs.set_gg(gg)
|
||||||
|
zs.set_dd(dd)
|
||||||
|
zs.is_sure = False
|
||||||
|
zs.seg_list = [seg1, seg2, seg3]
|
||||||
|
# 若第二线段与 [zd,zg] 重叠(如离开后回抽回到前中枢)则并入扩展
|
||||||
|
added_after_leave = []
|
||||||
|
leave_index = start_idx + 4
|
||||||
|
is_break = False
|
||||||
|
while leave_index < len(seg_list):
|
||||||
|
s = seg_list[leave_index]
|
||||||
|
if not s.is_sure:
|
||||||
|
break
|
||||||
|
sh = max(s.start_bi.high, s.end_bi.high) if s.end_bi else s.start_bi.high
|
||||||
|
sl = min(s.start_bi.low, s.end_bi.low) if s.end_bi else s.start_bi.low
|
||||||
|
if sh >= zs.zd and sl <= zs.zg:
|
||||||
|
added_after_leave.append(s.pre)
|
||||||
|
added_after_leave.append(s)
|
||||||
|
leave_index += 2
|
||||||
|
else:
|
||||||
|
next_seg = s.next
|
||||||
|
if next_seg and next_seg.is_sure:
|
||||||
|
if next_seg.dir == Chan_SEG_DIR.UP:
|
||||||
|
if next_seg.high <= zs.zg and next_seg.low >= zs.zd:
|
||||||
|
leave_index += 2
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
is_break = True
|
||||||
|
else:
|
||||||
|
if next_seg.low >= zs.zd and next_seg.low <= zs.zg:
|
||||||
|
leave_index += 2
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
is_break = True
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
if is_break:
|
||||||
|
break
|
||||||
|
if added_after_leave:
|
||||||
|
#print(len(added_after_leave))
|
||||||
|
segs_for_zs = list(zs.seg_list) + list(added_after_leave)
|
||||||
|
seg_highs = [s.high for s in segs_for_zs]
|
||||||
|
seg_lows = [s.low for s in segs_for_zs]
|
||||||
|
zs.set_gg(max(seg_highs))
|
||||||
|
zs.set_dd(min(seg_lows))
|
||||||
|
zs.seg_list = segs_for_zs
|
||||||
|
seg = segs_for_zs[-1]
|
||||||
|
#if seg.end_bi:
|
||||||
|
#zs.set_end_klc(seg.end_bi.end_klc, seg.sure_time, 0, seg)
|
||||||
|
#zs.set_end_seg(seg)
|
||||||
|
#zs.is_sure = True
|
||||||
|
start_idx = start_idx + len(added_after_leave)
|
||||||
|
if last_zs and last_zs.index != zs.index:
|
||||||
|
last_zs.set_next(zs)
|
||||||
|
zs.set_pre(last_zs)
|
||||||
|
|
||||||
|
zs_list.append(zs)
|
||||||
|
last_zs = zs
|
||||||
|
|
||||||
|
# 移动到下一组
|
||||||
|
start_idx += 4
|
||||||
|
if last_zs:
|
||||||
|
last_zs.is_sure = seg_list[-1].is_sure
|
||||||
|
"""
|
||||||
|
# 处理最后一个未确认的中枢 - 不自动扩展,保持未完成状态
|
||||||
|
if last_zs and not last_zs.is_sure:
|
||||||
|
# 获取中枢最后一个线段的索引
|
||||||
|
if last_zs.seg_list and len(last_zs.seg_list) > 0:
|
||||||
|
last_seg_of_zs = last_zs.seg_list[-1]
|
||||||
|
# 找到这个线段在seg_list中的索引
|
||||||
|
last_seg_idx = -1
|
||||||
|
for i, seg in enumerate(seg_list):
|
||||||
|
if seg == last_seg_of_zs:
|
||||||
|
last_seg_idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# 从中枢最后一个线段之后检查是否有离开
|
||||||
|
has_leave = False
|
||||||
|
if last_seg_idx >= 0 and last_seg_idx + 1 < len(seg_list):
|
||||||
|
for i in range(last_seg_idx + 1, len(seg_list)):
|
||||||
|
seg = seg_list[i]
|
||||||
|
if seg.is_sure:
|
||||||
|
# 检查是否离开中枢
|
||||||
|
leave = (seg.low > last_zs.zg and seg.high > last_zs.zg) or \
|
||||||
|
(seg.high < last_zs.zd and seg.low < last_zs.zd)
|
||||||
|
if leave:
|
||||||
|
has_leave = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not has_leave:
|
||||||
|
# 没有离开,保持未完成状态
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# 有离开,确认中枢
|
||||||
|
if last_seg_of_zs.end_bi:
|
||||||
|
#print(last_seg_of_zs.start_time, "last_seg_of_zs.end_time", last_seg_of_zs.end_time)
|
||||||
|
last_zs.set_end_klc(last_seg_of_zs.end_bi.end_klc, last_seg_of_zs.sure_time, 0, last_seg_of_zs)
|
||||||
|
last_zs.set_end_seg(last_seg_of_zs)
|
||||||
|
last_zs.is_sure = True
|
||||||
|
"""
|
||||||
|
return zs_list
|
||||||
|
|
||||||
|
|
||||||
|
def get_big_zs_list(self, zs_list):
|
||||||
|
"""
|
||||||
|
中枢扩张:将区间重叠的连续中枢合并为大级别中枢,便于显示更大级别的震荡区间。
|
||||||
|
重叠定义:两中枢 [zd,zg] 有交集,即 (zs_i.zg >= zs_j.zd and zs_i.zd <= zs_j.zg)。
|
||||||
|
"""
|
||||||
|
big_list = []
|
||||||
|
if len(zs_list) < 2:
|
||||||
|
return big_list
|
||||||
|
i = 0
|
||||||
|
while i < len(zs_list):
|
||||||
|
group = [zs_list[i]]
|
||||||
|
j = i + 1
|
||||||
|
while j < len(zs_list):
|
||||||
|
cur = zs_list[j]
|
||||||
|
# 与当前组内任一中枢有重叠即算扩张(通常只需与组内最后一个比)
|
||||||
|
last_in_group = group[-1]
|
||||||
|
overlap = (last_in_group.zg >= cur.zd and last_in_group.zd <= cur.zg)
|
||||||
|
if overlap:
|
||||||
|
group.append(cur)
|
||||||
|
j += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
if len(group) >= 2:
|
||||||
|
big = ChanZS_Big(group)
|
||||||
|
big.index = len(big_list)
|
||||||
|
big_list.append(big)
|
||||||
|
i = j if len(group) >= 2 else i + 1
|
||||||
|
return big_list
|
||||||
|
|
||||||
@@ -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 chanlun.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 chanlun.core.ChanKLU import ChanKLU
|
||||||
|
from chanlun.core.ChanKLC import ChanKLC
|
||||||
|
from chanlun.core.ChanBI import ChanBI
|
||||||
|
from chanlun.core.ChanSBI import ChanSBI
|
||||||
|
from chanlun.core.ChanSEG import ChanSEG
|
||||||
|
from chanlun.core.ChanZS import ChanZS
|
||||||
|
from chanlun.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 chanlun.indicators.ChanMACD import ChanMACD
|
||||||
|
from chanlun.pipeline.timeframe import TF_DF
|
||||||
|
from chanlun.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))
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
from technical.util import resample_to_interval
|
||||||
|
|
||||||
|
from chanlun.core.ChanBI import ChanBI
|
||||||
|
from chanlun.core.ChanBIZS import ChanBIZS
|
||||||
|
from chanlun.core.ChanBSP import ChanBSP
|
||||||
|
from chanlun.core.ChanEnum import (
|
||||||
|
Chan_BI_DIR,
|
||||||
|
Chan_BSP_DIR,
|
||||||
|
Chan_BSP_TYPE,
|
||||||
|
Chan_FX_TYPE,
|
||||||
|
Chan_K_DIR,
|
||||||
|
Chan_KLC_FX,
|
||||||
|
Chan_KLC_STATE,
|
||||||
|
Chan_KLINE_DIR,
|
||||||
|
Chan_KLU_PATTERN,
|
||||||
|
Chan_PRICE_TREND,
|
||||||
|
Chan_SEG_DIR,
|
||||||
|
Chan_ZS_DIR,
|
||||||
|
)
|
||||||
|
from chanlun.core.ChanKLC import ChanKLC
|
||||||
|
from chanlun.core.ChanKLU import ChanKLU
|
||||||
|
from chanlun.core.ChanSBI import ChanSBI
|
||||||
|
from chanlun.core.ChanSEG import ChanSEG
|
||||||
|
from chanlun.core.ChanZS import ChanZS, ChanZS_Big
|
||||||
|
from chanlun.indicators.ChanMACD import ChanMACD
|
||||||
|
from chanlun.pipeline.builders.bi import BiBuilderMixin
|
||||||
|
from chanlun.pipeline.builders.bsp import BspBuilderMixin
|
||||||
|
from chanlun.pipeline.builders.indicators import IndicatorsBuilderMixin
|
||||||
|
from chanlun.pipeline.builders.kline import KlineBuilderMixin
|
||||||
|
from chanlun.pipeline.builders.seg import SegBuilderMixin
|
||||||
|
from chanlun.pipeline.builders.zs import ZsBuilderMixin
|
||||||
|
|
||||||
|
class TF_DF(IndicatorsBuilderMixin, KlineBuilderMixin, BiBuilderMixin, SegBuilderMixin, ZsBuilderMixin, BspBuilderMixin):
|
||||||
|
def __init__(self, df=None, interval=0, timeframe=None):
|
||||||
|
if df is not None:
|
||||||
|
self.init_TF_DF(df, interval, timeframe)
|
||||||
|
def init_TF_DF(self, df, interval, timeframe):
|
||||||
|
self.timeframe = timeframe
|
||||||
|
self.interval = interval
|
||||||
|
# 检查 DataFrame 是否为空或没有 date 列
|
||||||
|
if df is None or df.empty:
|
||||||
|
raise ValueError(f"DataFrame for {timeframe} is empty. Please download data first.")
|
||||||
|
if 'date' not in df.columns:
|
||||||
|
raise ValueError(f"DataFrame for {timeframe} missing 'date' column. Columns: {df.columns.tolist()}")
|
||||||
|
# interval=1 时不需要重采样
|
||||||
|
if interval == 1:
|
||||||
|
self.dataframe = df.copy()
|
||||||
|
else:
|
||||||
|
self.dataframe = resample_to_interval(df, interval)
|
||||||
|
#print(self.timeframe, len(self.dataframe))
|
||||||
|
self.dataframe = self.add_indicators(self.dataframe)
|
||||||
|
self.klu_list = []
|
||||||
|
self.klc_list = []
|
||||||
|
self.bi_list = []
|
||||||
|
self.zs_list = []
|
||||||
|
self.bsp_list = []
|
||||||
|
self.seg_list = []
|
||||||
|
self.klc_fx_list = []
|
||||||
|
self.klu_list = self.cal_kl_data(self.dataframe)
|
||||||
|
self.klc_list = self.get_klc_list(self.klu_list)
|
||||||
|
self.bi_list = self.cal_bi_list(self.klc_list)
|
||||||
|
self.seg_list = self.get_seg_list(self.bi_list)
|
||||||
|
self.zs_list = self.get_zs_list(self.bi_list, self.seg_list)
|
||||||
|
self.big_zs_list = self.get_big_zs_list(self.zs_list)
|
||||||
|
self.chanmacd = ChanMACD(self.klu_list)
|
||||||
|
self.klu_list = self.chanmacd.cal_macd_state()
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_klc(self):
|
||||||
|
if len(self.klc_list) > 0:
|
||||||
|
return self.klc_list[-2]
|
||||||
|
return None
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# ADR-001: 包布局与兼容 shim
|
||||||
|
|
||||||
|
**Status:** Accepted
|
||||||
|
**Date:** 2026-08-05
|
||||||
|
**ECR:** ECR-001
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
根目录扁平模块被 strategies 与 web 通过模块名直接 import;完全改名会破坏 Freqtrade 策略。需要正式包边界,同时零改 `strategies/`。
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
1. 正式包名:`chanlun`(`core` / `pipeline` / `indicators` / `analysis`)。
|
||||||
|
2. 根目录保留同名 shim 文件,再导出公共符号。
|
||||||
|
3. Web 使用 Flask blueprints + services;前端 JS 模块化,不引入 TS 构建。
|
||||||
|
4. `TF_DF` 保留门面类名与公开方法,内部委托 builders。
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- 策略无需修改。
|
||||||
|
- 长期可逐步引导新代码 `from chanlun import ...`。
|
||||||
|
- shim 需保持至策略侧显式迁移(另立 ECR)。
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# CHANGELOG
|
||||||
|
|
||||||
|
## 2026-08-05 — ECR-001
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 正式包 `chanlun/`(core / pipeline / builders / indicators / analysis)
|
||||||
|
- ESS 文档树 `docs/`(PROFILE / ECR-001 / SPEC / ADR / HANDOFF)
|
||||||
|
- Web `config.py`、`services/`、`api/` blueprints
|
||||||
|
- 前端 `web/static/js/app/` 模块
|
||||||
|
- Golden 回归 `tests/generate_golden.py` + fixtures
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 根目录 `Chan*.py` / `TF_DF.py` 改为兼容 shim
|
||||||
|
- `TF_DF` 实现拆至 builders,门面签名保持
|
||||||
|
- `web/app.py` 瘦身为 `create_app()`
|
||||||
|
- `index.html` 去掉巨型 inline 业务 JS
|
||||||
|
- HTTP 代理改为环境变量配置
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 恢复缺失的 `TF_DF.get_zs_list`(委托 `get_seg_zs_list`)
|
||||||
|
|
||||||
|
### 2026-08-05 — ECR-001 follow-through
|
||||||
|
|
||||||
|
- strategies / web / tests / examples → `from chanlun...` 导入;绝对 `sys.path` 改为相对仓库根
|
||||||
|
- `chart.js` → `chart_format/view/tv/sync/tables.js`
|
||||||
|
- `chan_engine` / `datafeed` / `chan_indicator` 迁入 `static/js/app/`,`chan_tv` 与主站共用
|
||||||
|
- 根目录 shim 保留作兼容;正式入口为 `chanlun` 包
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# ECR-001
|
||||||
|
|
||||||
|
**Title:** 缠论引擎包化 + Web 分层重构(行为冻结)
|
||||||
|
**Status:** Approved
|
||||||
|
**Date:** 2026-08-05
|
||||||
|
**Change Level:** L3
|
||||||
|
|
||||||
|
## Change
|
||||||
|
|
||||||
|
将根目录扁平 `Chan*.py` / `TF_DF.py` 包化为 `chanlun/`,拆分 `web/app.py` 与巨型 `index.html` inline JS;算法与 `/api/analyze` 契约冻结;`config/` / `strategies/` 不动。
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
根目录与 Web 单体过大、职责混杂,难以维护与测试;需在不影响 Freqtrade 策略导入的前提下重整结构。
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### Allowed
|
||||||
|
|
||||||
|
- 创建 `chanlun/`(core / pipeline / indicators / analysis)与根目录兼容 shim
|
||||||
|
- 拆分 `TF_DF` 为 builders + 门面(公开方法签名不变)
|
||||||
|
- Web:`config` + `services` + `api` blueprints;配置外置(proxy / DATA_SERVICE)
|
||||||
|
- 前端:`index.html` 业务 JS 外置到 `static/js/app/`;隔离未接线 TS/TSX
|
||||||
|
- 示例与笔记迁入 `examples/` / `docs/notes/`
|
||||||
|
- Golden / 契约回归测试
|
||||||
|
|
||||||
|
### Forbidden
|
||||||
|
|
||||||
|
- 修改笔 / 线段 / 中枢 / 买卖点算法语义
|
||||||
|
- 破坏 `/api/analyze` JSON 字段(可增不可删)
|
||||||
|
- 修改 `config/`、`strategies/`
|
||||||
|
- 引入 Vite/React/TS 构建
|
||||||
|
- 重做 UI 视觉或更换 TradingView
|
||||||
|
|
||||||
|
## Risk
|
||||||
|
|
||||||
|
| Risk | Mitigation |
|
||||||
|
|------|------------|
|
||||||
|
| 策略 import 断裂 | 根 shim + 冒烟导入 |
|
||||||
|
| 拆文件改算法 | 仅搬移;golden fixture |
|
||||||
|
| 前端事件遗漏 | 按块抽取 + 手工/冒烟 |
|
||||||
|
| API 字段漂移 | analyze 契约测试 |
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] `from ChanLun import ChanLun` / `from ChanEnum import ...` 仍可用
|
||||||
|
- [x] Golden:同一 fixture 下 bi/seg/zs/bsp 序列化结果与基线一致
|
||||||
|
- [x] `/api/analyze` 关键字段集合兼容(契约冒烟)
|
||||||
|
- [x] `web/app.py` 瘦身为 factory;业务在 services/api
|
||||||
|
- [x] `index.html` 不再含大体量业务 inline JS
|
||||||
|
- [x] ESS docs 齐全;TEST_REPORT / IMPLEMENTATION_REPORT / CHANGELOG
|
||||||
|
- [x] `config/`、`strategies/` 无内容变更
|
||||||
|
|
||||||
|
**Status:** Done(待 Reviewer 签核)
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
单分支 / 单 PR 回滚;shim 期可整体 `git revert`。
|
||||||
|
|
||||||
|
## Risk Review
|
||||||
|
|
||||||
|
- Path: `docs/RISK_REVIEW/ECR-001.md` — N/A(不改交易语义)
|
||||||
|
|
||||||
|
## Linked
|
||||||
|
|
||||||
|
- PRD / PRODUCT_SPEC: `docs/PRODUCT_SPEC/ECR-001-restructure.md`
|
||||||
|
- ENGINEERING_SPEC: `docs/ENGINEERING_SPEC/ECR-001-restructure.md`
|
||||||
|
- ADR: `docs/ADR/ADR-001-package-layout.md`
|
||||||
|
- EXPERIMENT: N/A
|
||||||
|
- TRACEABILITY: Yes
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# Engineering Spec: ECR-001 引擎 + Web 重构
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
| Doc | Link |
|
||||||
|
|-----|------|
|
||||||
|
| ECR | `docs/ECR/ECR-001-chan-web-restructure.md` |
|
||||||
|
| PRODUCT_SPEC | `docs/PRODUCT_SPEC/ECR-001-restructure.md` |
|
||||||
|
| ADR | `docs/ADR/ADR-001-package-layout.md` |
|
||||||
|
|
||||||
|
## Module Design
|
||||||
|
|
||||||
|
### `chanlun/core/*`
|
||||||
|
|
||||||
|
| 项 | 值 |
|
||||||
|
|----|-----|
|
||||||
|
| Responsibility | K 线单元、合并 K、笔/段/中枢/买卖点数据结构与枚举 |
|
||||||
|
| Can | 表达缠论基础对象 |
|
||||||
|
| Cannot | 拉行情、写 Flask 路由 |
|
||||||
|
| Layer | domain |
|
||||||
|
|
||||||
|
### `chanlun/pipeline/*`
|
||||||
|
|
||||||
|
| 项 | 值 |
|
||||||
|
|----|-----|
|
||||||
|
| Responsibility | `ChanLun` 编排、`TF_DF` 门面、builders |
|
||||||
|
| Can | 从 DataFrame 构建结构 |
|
||||||
|
| Cannot | 改公开方法语义 |
|
||||||
|
| Layer | application |
|
||||||
|
|
||||||
|
### `chanlun/indicators/*` / `chanlun/analysis/*`
|
||||||
|
|
||||||
|
| 项 | 值 |
|
||||||
|
|----|-----|
|
||||||
|
| Responsibility | MACD 状态、Zone/Classifier/Trend 等分析 |
|
||||||
|
| Layer | domain / application |
|
||||||
|
|
||||||
|
### Root shims (`ChanLun.py`, `ChanEnum.py`, …)
|
||||||
|
|
||||||
|
| 项 | 值 |
|
||||||
|
|----|-----|
|
||||||
|
| Responsibility | 转发到 `chanlun.*`,兼容 strategies |
|
||||||
|
| Cannot | 含业务逻辑 |
|
||||||
|
|
||||||
|
### `web/config.py` + `web/services/*` + `web/api/*`
|
||||||
|
|
||||||
|
| 项 | 值 |
|
||||||
|
|----|-----|
|
||||||
|
| Responsibility | 配置、行情、分析、序列化、HTTP |
|
||||||
|
| Cannot | 修改缠论算法 |
|
||||||
|
|
||||||
|
### `web/static/js/app/*`
|
||||||
|
|
||||||
|
| 项 | 值 |
|
||||||
|
|----|-----|
|
||||||
|
| Responsibility | 图表、overlay、UI、API 客户端 |
|
||||||
|
| Cannot | 本轮引入 React 构建 |
|
||||||
|
|
||||||
|
## Interfaces
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TF_DF:
|
||||||
|
# 公开方法签名保持与重构前一致
|
||||||
|
def get_kl_data(self, dataframe): ...
|
||||||
|
def get_bi_list(self, dataframe): ...
|
||||||
|
# ... 门面委托 builders
|
||||||
|
|
||||||
|
class ChanLun:
|
||||||
|
def init_dataframes(...): ...
|
||||||
|
```
|
||||||
|
|
||||||
|
`/api/analyze`:查询参数与 JSON 顶层字段兼容。
|
||||||
|
|
||||||
|
## Algorithm Sketch
|
||||||
|
|
||||||
|
1. 物理迁移模块到 `chanlun/`,修正 import
|
||||||
|
2. 根 shim 再导出
|
||||||
|
3. `TF_DF` 方法体迁移到 builders,门面调用
|
||||||
|
4. Web 按职责拆文件,路由注册不变
|
||||||
|
5. HTML 抽 JS;未接线 TS 入 `_unused`
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
| Case | Behavior |
|
||||||
|
|------|----------|
|
||||||
|
| 行情失败 | 保持现有 JSON error |
|
||||||
|
| 分析异常 | 保持现有日志与错误返回 |
|
||||||
|
|
||||||
|
## Config / Constants
|
||||||
|
|
||||||
|
| Name | Source |
|
||||||
|
|------|--------|
|
||||||
|
| DATA_SERVICE_URL | env / config |
|
||||||
|
| HTTP(S)_PROXY | env / config(替代硬编码 7897) |
|
||||||
|
| MACD periods | config |
|
||||||
|
|
||||||
|
## Test Plan Pointer
|
||||||
|
|
||||||
|
- `tests/test_golden_pipeline.py` + `tests/fixtures/golden_*`
|
||||||
|
- `web/tests/` analyze 契约冒烟
|
||||||
|
- `python -c "from ChanLun import ChanLun"`
|
||||||
|
|
||||||
|
## Migration / API Impact
|
||||||
|
|
||||||
|
无破坏性 API 变更;仅内部路径变化。
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Handoff
|
||||||
|
|
||||||
|
**From:** ARCHITECT
|
||||||
|
**To:** ENGINEER
|
||||||
|
**ECR:** ECR-001
|
||||||
|
**State:** design → coding
|
||||||
|
**Date:** 2026-08-05
|
||||||
|
|
||||||
|
## Artifacts
|
||||||
|
|
||||||
|
- [x] ECR (Approved)
|
||||||
|
- [x] PRODUCT_SPEC
|
||||||
|
- [x] ENGINEERING_SPEC
|
||||||
|
- [x] RISK_REVIEW (N/A)
|
||||||
|
- [x] ADR-001
|
||||||
|
- [x] TRACEABILITY
|
||||||
|
- [ ] TEST_REPORT / CODE_REVIEW / EXP (engineer / reviewer)
|
||||||
|
|
||||||
|
## Restrictions — Do not modify
|
||||||
|
|
||||||
|
- `config/`、`strategies/` 内容
|
||||||
|
- 缠论算法语义、`/api/analyze` 破坏性变更
|
||||||
|
- 计划文件本身
|
||||||
|
|
||||||
|
## Goal for receiver
|
||||||
|
|
||||||
|
按 ENGINEERING_SPEC 完成包化、TF_DF 拆分、Web 分层、前端模块化、golden 测试与文档收尾。
|
||||||
|
|
||||||
|
## Done for this hop
|
||||||
|
|
||||||
|
- [x] ESS docs 落盘
|
||||||
|
- [x] ECR Approved(计划实施授权)
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `docs/ECR/ECR-001-chan-web-restructure.md`
|
||||||
|
- `docs/ENGINEERING_SPEC/ECR-001-restructure.md`
|
||||||
|
- Plan: ESS Chan Refactor(1B+2B)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Handoff
|
||||||
|
|
||||||
|
**From:** ENGINEER
|
||||||
|
**To:** REVIEWER
|
||||||
|
**ECR:** ECR-001
|
||||||
|
**State:** coding → review
|
||||||
|
**Date:** 2026-08-05
|
||||||
|
|
||||||
|
## Artifacts
|
||||||
|
|
||||||
|
- [x] ECR (Approved / Done pending review)
|
||||||
|
- [x] PRODUCT_SPEC / ENGINEERING_SPEC / ADR / RISK N/A
|
||||||
|
- [x] IMPLEMENTATION_REPORT
|
||||||
|
- [x] TEST_REPORT
|
||||||
|
- [ ] CODE_REVIEW
|
||||||
|
|
||||||
|
## Goal for receiver
|
||||||
|
|
||||||
|
对照 ECR-001 Acceptance 做代码审阅;确认 strategies/config 无 diff;golden 与路由契约通过。
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `docs/IMPLEMENTATION_REPORT/ECR-001.md`
|
||||||
|
- `docs/TEST_REPORT/ECR-001.md`
|
||||||
|
- `docs/CHANGELOG/CHANGELOG.md`
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# IDEA: 引擎与 Web 结构重整
|
||||||
|
|
||||||
|
根目录与 `web/app.py` / `index.html` 过于臃肿,需在行为冻结前提下包化与分层。见 ECR-001。
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# IMPLEMENTATION_REPORT — ECR-001
|
||||||
|
|
||||||
|
**Date:** 2026-08-05
|
||||||
|
**Role:** ENGINEER
|
||||||
|
**Status:** Complete
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
完成缠论引擎包化、`TF_DF` builders 拆分、Web services/blueprints 分层、前端 `index.html` JS 外置模块化;算法与 `/api/analyze` 契约冻结;`config/` / `strategies/` 无改动。
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
| Area | Result |
|
||||||
|
|------|--------|
|
||||||
|
| `chanlun/` | core / pipeline(+builders) / indicators / analysis |
|
||||||
|
| Root shims | `ChanLun.py` `TF_DF.py` `ChanEnum.py` 等再导出 |
|
||||||
|
| L1 fix | 恢复 `TF_DF.get_zs_list` → `get_seg_zs_list` |
|
||||||
|
| Web | `config.py` `services/*` `api/*` `create_app()` |
|
||||||
|
| Frontend | `static/js/app/*`;未接线 TSX/TS → `_unused/` |
|
||||||
|
| Examples/notes | `examples/` `docs/notes/` |
|
||||||
|
| Tests | golden pipeline + analyze contract smoke |
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
- 代理:`CHAN_HTTP_PROXY` / `HTTP_PROXY`(默认不强制 7897)
|
||||||
|
- `DATA_SERVICE_URL` / `FLASK_PORT`
|
||||||
|
|
||||||
|
## Follow-ups(非本 ECR)
|
||||||
|
|
||||||
|
- (已完成)策略与 web/tests 改为 `from chanlun import ...`;根 shim 仅兼容旧脚本
|
||||||
|
- (已完成)`chart.js` 拆为 format/view/tv/sync/tables;chan_tv 共用 `static/js/app/`
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# PRODUCT_SPEC: ECR-001 结构重构
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
在不改变缠论计算结果与 Web 分析 API 对外语义的前提下,提升代码可维护性。
|
||||||
|
|
||||||
|
## User-visible behavior
|
||||||
|
|
||||||
|
| 项 | 期望 |
|
||||||
|
|----|------|
|
||||||
|
| 图表页 /chan_tv / 首页 | 功能与交互保持 |
|
||||||
|
| `/api/analyze` | 字段兼容;可增不可删 |
|
||||||
|
| Freqtrade 策略 | 无需改 import 路径即可加载引擎 |
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- 新买卖点规则、新 UI、新行情后端、策略超参优化
|
||||||
|
|
||||||
|
## Acceptance(产品视角)
|
||||||
|
|
||||||
|
1. 用户打开 Web 图表仍能看到笔/段/中枢/买卖点叠加。
|
||||||
|
2. 既有策略代码不因本次重构而修改。
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Project Profile — chan (缠论)
|
||||||
|
|
||||||
|
> Agent 第一次读这个文件。不要重新猜技术栈;偏离见 Forbidden + ADR。
|
||||||
|
|
||||||
|
## Type
|
||||||
|
Trading System(缠论分析引擎 + 可视化 Web;Freqtrade 策略目录独立、本 ECR 不改)
|
||||||
|
|
||||||
|
## Stack Lock
|
||||||
|
|
||||||
|
| Layer | Choice |
|
||||||
|
|-------|--------|
|
||||||
|
| Language | Python 3 |
|
||||||
|
| Engine package | `chanlun/` |
|
||||||
|
| Backend | Flask |
|
||||||
|
| Realtime | 无(请求式分析) |
|
||||||
|
| Database | 无(行情外部 DATA_SERVICE / CCXT / A 股接口) |
|
||||||
|
| Frontend | TradingView Charting Library + 原生 JS |
|
||||||
|
| Deployment | gunicorn / systemd(web) |
|
||||||
|
| Architecture Pattern | 包化引擎 + Web services/blueprints + 根目录兼容 shim |
|
||||||
|
|
||||||
|
## Forbidden(无 ADR / 无 ECR 禁止)
|
||||||
|
|
||||||
|
- 无 ADR 修改笔 / 线段 / 中枢 / 买卖点算法语义
|
||||||
|
- 无 ECR 修改 `config/`、`strategies/` 策略逻辑或参数
|
||||||
|
- 无 ECR 破坏 `/api/analyze` JSON 契约(可增不可删)
|
||||||
|
- 引入 Kafka / MongoDB / 微服务拆分(除非新 ADR)
|
||||||
|
- 本轮引入 Vite/React/TS 构建流水线
|
||||||
|
|
||||||
|
## Active anchors
|
||||||
|
|
||||||
|
- ECR: ECR-001
|
||||||
|
- EXP: N/A(本变更不改交易行为语义)
|
||||||
|
- TRACEABILITY: `docs/TRACEABILITY.md`
|
||||||
|
|
||||||
|
## Pointers
|
||||||
|
|
||||||
|
- Rules: `PROJECT_RULES.md`
|
||||||
|
- Stack detail: `TECH_STACK.md`
|
||||||
|
- Memory: `AGENT_MEMORY.md`(若存在)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Project Rules — chan
|
||||||
|
|
||||||
|
## Scope boundaries
|
||||||
|
|
||||||
|
1. `config/`、`strategies/`:Freqtrade 策略资产,默认只读;任何改动需独立 ECR。
|
||||||
|
2. `chanlun/`:缠论引擎正式包;算法变更需 L2+ ECR + 回归基线。
|
||||||
|
3. 根目录 `Chan*.py` / `TF_DF.py`:兼容 shim,保持 `from ChanLun import ChanLun` 可用。
|
||||||
|
4. `web/`:可视化与 API;契约冻结于 ECR-001。
|
||||||
|
|
||||||
|
## Change levels
|
||||||
|
|
||||||
|
- L0 docs / L1 bugfix / L2 behavior / L3 architecture — 见 ESS CHANGE_MANAGEMENT。
|
||||||
|
- 结构重构默认 L3;若触及缠论识别结果 → 升为 L2 并要求 RISK_REVIEW + EXP。
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
- 正式导入:`from chanlun import ChanLun, TF_DF`;`from chanlun.core.ChanEnum import ...`
|
||||||
|
- 根目录 `Chan*.py` / `TF_DF.py` 为兼容 shim(旧脚本可用);新代码与 strategies 应使用 `chanlun` 包
|
||||||
|
- Web URL 与 `/api/analyze` 字段名/结构保持兼容
|
||||||
|
- 策略文件通过 `sys.path.append(仓库根)` 保证能找到 `chanlun` 包
|
||||||
|
|
||||||
|
## Language
|
||||||
|
|
||||||
|
- 代码注释与提交说明优先中文。
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# RISK_REVIEW: ECR-001
|
||||||
|
|
||||||
|
**Status:** N/A
|
||||||
|
**Reason:** 本变更仅软件结构重构;缠论识别与买卖点语义冻结,不改变交易决策逻辑。无需 Experiment / Live Promote 门禁。
|
||||||
|
|
||||||
|
若后续发现 golden 差异表明算法被意外改动,升级为 L2 并重开 RISK_REVIEW。
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# STATE
|
||||||
|
|
||||||
|
**owner:** reviewer
|
||||||
|
**active_ecr:** ECR-001
|
||||||
|
**phase:** review
|
||||||
|
**updated:** 2026-08-05
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
Engineer hop complete. See IMPLEMENTATION_REPORT + TEST_REPORT.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
task_id: ECR-001
|
||||||
|
role: engineer
|
||||||
|
phase: coding
|
||||||
|
tool: cursor
|
||||||
|
|
||||||
|
input:
|
||||||
|
- docs/PROJECT_PROFILE.md
|
||||||
|
- docs/PROJECT_RULES.md
|
||||||
|
- docs/TECH_STACK.md
|
||||||
|
- docs/ECR/ECR-001-chan-web-restructure.md
|
||||||
|
- docs/ENGINEERING_SPEC/ECR-001-restructure.md
|
||||||
|
- docs/ADR/ADR-001-package-layout.md
|
||||||
|
- docs/HANDOFF/ECR-001-architect-to-engineer.md
|
||||||
|
|
||||||
|
output:
|
||||||
|
- chanlun/
|
||||||
|
- root shims
|
||||||
|
- web/services web/api web/config
|
||||||
|
- web/static/js/app/
|
||||||
|
- tests/ + docs/TEST_REPORT + IMPLEMENTATION_REPORT + CHANGELOG
|
||||||
|
|
||||||
|
forbidden:
|
||||||
|
- redesign_scope
|
||||||
|
- modify config/ or strategies/
|
||||||
|
- change chan algorithm semantics
|
||||||
|
- break /api/analyze contract
|
||||||
|
|
||||||
|
next_agent: reviewer
|
||||||
|
notes: "1B+2B; behavior freeze; shim for strategies"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Tech Stack — chan
|
||||||
|
|
||||||
|
## Engine
|
||||||
|
|
||||||
|
- Python, pandas, numpy, TA-Lib / technical
|
||||||
|
- 包名:`chanlun`
|
||||||
|
- 流水线:KLU → KLC → BI → SBI → SEG → ZS → BSP(`ChanLun` / `TF_DF` 门面)
|
||||||
|
|
||||||
|
## Web
|
||||||
|
|
||||||
|
- Flask + Jinja2 templates
|
||||||
|
- TradingView Charting Library(`web/charting_library/`)
|
||||||
|
- 前端运行时:原生 JS(`web/static/js/app/`)
|
||||||
|
- 行情:`DATA_SERVICE_URL` / CCXT / A 股数据服务
|
||||||
|
|
||||||
|
## Out of scope this release
|
||||||
|
|
||||||
|
- data_provider 仓库内重建
|
||||||
|
- React/TS 构建
|
||||||
|
- Freqtrade config/strategies 重构
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# TEST_REPORT — ECR-001
|
||||||
|
|
||||||
|
**Date:** 2026-08-05
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tests/generate_golden.py --check
|
||||||
|
python -m pytest tests/test_golden_pipeline.py web/tests/test_analyze_contract.py -q
|
||||||
|
python -c "from ChanLun import ChanLun, TF_DF; from ChanEnum import Chan_BI_DIR"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
| Case | Result |
|
||||||
|
|------|--------|
|
||||||
|
| Golden pipeline | PASS(counts klu=400 klc=208 bi=14 seg=2 zs=0 bsp=3) |
|
||||||
|
| Shim imports | PASS |
|
||||||
|
| Analyze contract keys + routes | PASS |
|
||||||
|
| pytest suite above | **5 passed** |
|
||||||
|
| `git diff config strategies` | empty(无改动) |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Golden 经 CSV 往返 + 浮点 round(10) 稳定化
|
||||||
|
- 行情服务不可达时 metadata refresh 会警告,不影响路由注册测试
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# TRACEABILITY — ECR-001
|
||||||
|
|
||||||
|
| ECR | Requirement | Spec | Code | Test |
|
||||||
|
|-----|-------------|------|------|------|
|
||||||
|
| ECR-001 | 引擎包化 + shim | ENG-001 / ADR-001 | `chanlun/` + root shims | import smoke + golden |
|
||||||
|
| ECR-001 | TF_DF 门面拆分 | ENG-001 | `chanlun/pipeline/` | golden pipeline |
|
||||||
|
| ECR-001 | Web 分层 | ENG-001 | `web/services` `web/api` | analyze contract |
|
||||||
|
| ECR-001 | 前端模块化 | ENG-001 | `web/static/js/app/` | manual / smoke |
|
||||||
|
| ECR-001 | 策略零改动 | PROFILE | no edits under strategies/ | git diff empty |
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
@@ -5,8 +7,8 @@
|
|||||||
该文件展示如何使用ChanKLC类中新增的分型强度检测功能
|
该文件展示如何使用ChanKLC类中新增的分型强度检测功能
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from ChanKLC import ChanKLC
|
from chanlun.core.ChanKLC import ChanKLC
|
||||||
from ChanEnum import Chan_FX_TYPE
|
from chanlun.core.ChanEnum import Chan_FX_TYPE
|
||||||
import ChanKLU
|
import ChanKLU
|
||||||
|
|
||||||
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
@@ -5,8 +7,8 @@
|
|||||||
解决KLC滞后问题,提供即时的分型信号
|
解决KLC滞后问题,提供即时的分型信号
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from ChanKLU import ChanKLU
|
from chanlun.core.ChanKLU import ChanKLU
|
||||||
from ChanEnum import Chan_FX_TYPE
|
from chanlun.core.ChanEnum import Chan_FX_TYPE
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
+2
-153
@@ -1,153 +1,2 @@
|
|||||||
#!/usr/bin/env python3
|
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||||
# -*- coding: utf-8 -*-
|
from chanlun.analysis.fx_strength_config import * # noqa: F403
|
||||||
"""
|
|
||||||
分型强度检测配置文件
|
|
||||||
用于调整分型强度计算的各项参数和权重
|
|
||||||
"""
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""将 strategies / web / tests 的根 shim 导入改为 chanlun 包导入。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
REPLACEMENTS = [
|
||||||
|
(r"from ChanLun import ", "from chanlun import "),
|
||||||
|
(r"from TF_DF import ", "from chanlun import "), # TF_DF also exported from chanlun
|
||||||
|
(r"from ChanEnum import ", "from chanlun.core.ChanEnum import "),
|
||||||
|
(r"from ChanLun_Classifier import ", "from chanlun.analysis.ChanLun_Classifier import "),
|
||||||
|
(r"from ChanPY import ", "from chanlun.analysis.ChanPY import "),
|
||||||
|
(r"from ChanKLU import ", "from chanlun.core.ChanKLU import "),
|
||||||
|
(r"from ChanKLC import ", "from chanlun.core.ChanKLC import "),
|
||||||
|
(r"from ChanBI import ", "from chanlun.core.ChanBI import "),
|
||||||
|
(r"from ChanCTime import ", "from chanlun.core.ChanCTime import "),
|
||||||
|
(r"from ChanMACD import ", "from chanlun.indicators.ChanMACD import "),
|
||||||
|
(r"from ChanZone import ", "from chanlun.analysis.ChanZone import "),
|
||||||
|
(r"from ChanBSP import ", "from chanlun.core.ChanBSP import "),
|
||||||
|
(r"from ChanSEG import ", "from chanlun.core.ChanSEG import "),
|
||||||
|
(r"from ChanZS import ", "from chanlun.core.ChanZS import "),
|
||||||
|
(r"from ChanBIZS import ", "from chanlun.core.ChanBIZS import "),
|
||||||
|
(r"from ChanSBI import ", "from chanlun.core.ChanSBI import "),
|
||||||
|
(r"from ChanPivotClassifier import ", "from chanlun.analysis.ChanPivotClassifier import "),
|
||||||
|
(r"from ChanPivotMonitor import ", "from chanlun.analysis.ChanPivotMonitor import "),
|
||||||
|
(r"from fx_strength_config import ", "from chanlun.analysis.fx_strength_config import "),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_file(path: Path) -> bool:
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
orig = text
|
||||||
|
for pat, repl in REPLACEMENTS:
|
||||||
|
text = re.sub(pat, repl, text)
|
||||||
|
# Fix: from chanlun import TF_DF only cases that were `from TF_DF import TF_DF`
|
||||||
|
# and `from ChanLun import ChanLun, TF_DF` already becomes from chanlun import ChanLun, TF_DF — good
|
||||||
|
# Special: from chanlun import TF_DF when file only imported TF_DF — need TF_DF in chanlun.__init__
|
||||||
|
if text == orig:
|
||||||
|
return False
|
||||||
|
path.write_text(text, encoding="utf-8")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
changed = []
|
||||||
|
targets = []
|
||||||
|
targets += list((ROOT / "strategies").glob("*.py"))
|
||||||
|
targets += list((ROOT / "web").rglob("*.py"))
|
||||||
|
targets += list((ROOT / "tests").rglob("*.py"))
|
||||||
|
for p in targets:
|
||||||
|
if "charting_library" in str(p) or "__pycache__" in str(p):
|
||||||
|
continue
|
||||||
|
if rewrite_file(p):
|
||||||
|
changed.append(str(p.relative_to(ROOT)))
|
||||||
|
print(f"updated {len(changed)} files")
|
||||||
|
for c in changed:
|
||||||
|
print(" -", c)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""一次性迁移:根目录引擎模块 → chanlun/ 包 + 根 shim。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
CORE = [
|
||||||
|
"ChanEnum.py",
|
||||||
|
"ChanCTime.py",
|
||||||
|
"ChanKLU.py",
|
||||||
|
"ChanKLC.py",
|
||||||
|
"ChanBI.py",
|
||||||
|
"ChanSBI.py",
|
||||||
|
"ChanSEG.py",
|
||||||
|
"ChanZS.py",
|
||||||
|
"ChanBIZS.py",
|
||||||
|
"ChanBSP.py",
|
||||||
|
"Chan_FX_Box.py",
|
||||||
|
]
|
||||||
|
INDICATORS = [
|
||||||
|
"ChanMACD.py",
|
||||||
|
"ChanMACDHistSet.py",
|
||||||
|
"ChanMACDSeg.py",
|
||||||
|
"ChanMACDUnitTF.py",
|
||||||
|
]
|
||||||
|
ANALYSIS = [
|
||||||
|
"ChanZone.py",
|
||||||
|
"ChanLun_Classifier.py",
|
||||||
|
"ChanPivotClassifier.py",
|
||||||
|
"ChanPivotMonitor.py",
|
||||||
|
"ChanHeng.py",
|
||||||
|
"ChanPY.py",
|
||||||
|
"Find_Trend.py",
|
||||||
|
"fx_strength_config.py",
|
||||||
|
]
|
||||||
|
PIPELINE = {
|
||||||
|
"ChanLun.py": "orchestrator.py",
|
||||||
|
"TF_DF.py": "timeframe.py",
|
||||||
|
}
|
||||||
|
|
||||||
|
MODULE_PKG: dict[str, str] = {}
|
||||||
|
for name in CORE:
|
||||||
|
MODULE_PKG[name[:-3]] = "chanlun.core"
|
||||||
|
for name in INDICATORS:
|
||||||
|
MODULE_PKG[name[:-3]] = "chanlun.indicators"
|
||||||
|
for name in ANALYSIS:
|
||||||
|
MODULE_PKG[name[:-3]] = "chanlun.analysis"
|
||||||
|
|
||||||
|
IMPORT_TARGET = {
|
||||||
|
**{k: f"{v}.{k}" for k, v in MODULE_PKG.items()},
|
||||||
|
"ChanLun": "chanlun.pipeline.orchestrator",
|
||||||
|
"TF_DF": "chanlun.pipeline.timeframe",
|
||||||
|
}
|
||||||
|
KNOWN = set(IMPORT_TARGET)
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_imports(text: str) -> str:
|
||||||
|
"""只改写已知缠论模块;绝不拆分 typing/talib 等标准 from-import。"""
|
||||||
|
out_lines = []
|
||||||
|
for line in text.splitlines(keepends=True):
|
||||||
|
nl = "\n" if line.endswith("\n") else ""
|
||||||
|
raw = line[:-1] if nl else line
|
||||||
|
indent_m = re.match(r"^(\s*)", raw)
|
||||||
|
indent = indent_m.group(1) if indent_m else ""
|
||||||
|
stripped = raw[len(indent) :]
|
||||||
|
|
||||||
|
if stripped.startswith("from ") and " import " in stripped:
|
||||||
|
m = re.match(r"^from (\S+) import (.*)$", stripped)
|
||||||
|
if m and m.group(1) in KNOWN:
|
||||||
|
raw = f"{indent}from {IMPORT_TARGET[m.group(1)]} import {m.group(2)}"
|
||||||
|
out_lines.append(raw + nl)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if stripped.startswith("import "):
|
||||||
|
rest = stripped[len("import ") :]
|
||||||
|
# 跳过 import x as y 复合以外的非已知模块整行
|
||||||
|
parts = [p.strip() for p in rest.split(",")]
|
||||||
|
if not any(p.split(" as ")[0].strip() in KNOWN for p in parts):
|
||||||
|
out_lines.append(line)
|
||||||
|
continue
|
||||||
|
new_parts = []
|
||||||
|
for p in parts:
|
||||||
|
base = p.split(" as ")[0].strip()
|
||||||
|
if base not in KNOWN:
|
||||||
|
new_parts.append(f"import {p}")
|
||||||
|
continue
|
||||||
|
target = IMPORT_TARGET[base]
|
||||||
|
if " as " in p:
|
||||||
|
new_parts.append(
|
||||||
|
f"import {target} as {p.split(' as ', 1)[1].strip()}"
|
||||||
|
)
|
||||||
|
elif base in ("ChanLun", "TF_DF"):
|
||||||
|
new_parts.append(f"from {target} import {base}")
|
||||||
|
else:
|
||||||
|
new_parts.append(f"import {target} as {base}")
|
||||||
|
# 多模块拆成多行,保持可读
|
||||||
|
out_lines.append(nl.join(indent + x for x in new_parts) + nl)
|
||||||
|
continue
|
||||||
|
|
||||||
|
out_lines.append(line)
|
||||||
|
return "".join(out_lines)
|
||||||
|
|
||||||
|
|
||||||
|
def write_shim(mod_name: str):
|
||||||
|
path = ROOT / f"{mod_name}.py"
|
||||||
|
if mod_name == "ChanLun":
|
||||||
|
body = (
|
||||||
|
'"""兼容 shim — 请优先 from chanlun import ..."""\n'
|
||||||
|
"from chanlun.pipeline.orchestrator import ChanLun # noqa: F401\n"
|
||||||
|
"from chanlun.pipeline.timeframe import TF_DF # noqa: F401\n"
|
||||||
|
)
|
||||||
|
elif mod_name == "TF_DF":
|
||||||
|
body = (
|
||||||
|
'"""兼容 shim — 请优先 from chanlun import ..."""\n'
|
||||||
|
"from chanlun.pipeline.timeframe import TF_DF # noqa: F401\n"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
target = IMPORT_TARGET[mod_name]
|
||||||
|
body = (
|
||||||
|
f'"""兼容 shim — 请优先 from chanlun import ..."""\n'
|
||||||
|
f"from {target} import * # noqa: F403\n"
|
||||||
|
)
|
||||||
|
path.write_text(body, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
for sub in ("core", "indicators", "analysis", "pipeline", "pipeline/builders"):
|
||||||
|
d = ROOT / "chanlun" / sub
|
||||||
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
(d / "__init__.py").write_text("", encoding="utf-8")
|
||||||
|
|
||||||
|
moved = []
|
||||||
|
|
||||||
|
def move_list(names, dest_pkg: Path):
|
||||||
|
for name in names:
|
||||||
|
src = ROOT / name
|
||||||
|
if not src.exists():
|
||||||
|
print("skip missing", name)
|
||||||
|
continue
|
||||||
|
dst = dest_pkg / name
|
||||||
|
text = rewrite_imports(src.read_text(encoding="utf-8"))
|
||||||
|
dst.write_text(text, encoding="utf-8")
|
||||||
|
src.unlink()
|
||||||
|
moved.append(name)
|
||||||
|
|
||||||
|
move_list(CORE, ROOT / "chanlun" / "core")
|
||||||
|
move_list(INDICATORS, ROOT / "chanlun" / "indicators")
|
||||||
|
move_list(ANALYSIS, ROOT / "chanlun" / "analysis")
|
||||||
|
|
||||||
|
for src_name, dst_name in PIPELINE.items():
|
||||||
|
src = ROOT / src_name
|
||||||
|
if not src.exists():
|
||||||
|
continue
|
||||||
|
dst = ROOT / "chanlun" / "pipeline" / dst_name
|
||||||
|
text = rewrite_imports(src.read_text(encoding="utf-8"))
|
||||||
|
dst.write_text(text, encoding="utf-8")
|
||||||
|
src.unlink()
|
||||||
|
moved.append(src_name)
|
||||||
|
|
||||||
|
# 恢复 get_zs_list(受控 L1)
|
||||||
|
tf_path = ROOT / "chanlun" / "pipeline" / "timeframe.py"
|
||||||
|
tf_text = tf_path.read_text(encoding="utf-8")
|
||||||
|
if "def get_zs_list" not in tf_text:
|
||||||
|
needle = "\tdef calculate_seg_zs(self, seg_list):\n\t\treturn self.get_seg_zs_list(seg_list)\n"
|
||||||
|
insert = (
|
||||||
|
"\tdef get_zs_list(self, bi_list, seg_list):\n"
|
||||||
|
"\t\t\"\"\"兼容历史 API:线段中枢列表。\"\"\"\n"
|
||||||
|
"\t\treturn self.get_seg_zs_list(seg_list)\n"
|
||||||
|
+ needle
|
||||||
|
)
|
||||||
|
if needle in tf_text:
|
||||||
|
tf_path.write_text(tf_text.replace(needle, insert), encoding="utf-8")
|
||||||
|
else:
|
||||||
|
tf_path.write_text(
|
||||||
|
tf_text
|
||||||
|
+ "\n\tdef get_zs_list(self, bi_list, seg_list):\n"
|
||||||
|
+ "\t\treturn self.get_seg_zs_list(seg_list)\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
print("patched get_zs_list")
|
||||||
|
|
||||||
|
for name in CORE + INDICATORS + ANALYSIS:
|
||||||
|
write_shim(name[:-3])
|
||||||
|
write_shim("ChanLun")
|
||||||
|
write_shim("TF_DF")
|
||||||
|
|
||||||
|
(ROOT / "chanlun" / "__init__.py").write_text(
|
||||||
|
'"""缠论引擎正式包。"""\n'
|
||||||
|
"from chanlun.pipeline.orchestrator import ChanLun\n"
|
||||||
|
"from chanlun.pipeline.timeframe import TF_DF\n"
|
||||||
|
'__all__ = ["ChanLun", "TF_DF"]\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
examples = ROOT / "examples"
|
||||||
|
examples.mkdir(exist_ok=True)
|
||||||
|
notes = ROOT / "docs" / "notes"
|
||||||
|
notes.mkdir(parents=True, exist_ok=True)
|
||||||
|
for f in ("fx_strength_example.py", "realtime_fx_example.py"):
|
||||||
|
p = ROOT / f
|
||||||
|
if p.exists():
|
||||||
|
dest = examples / f
|
||||||
|
if dest.exists():
|
||||||
|
dest.unlink()
|
||||||
|
shutil.move(str(p), str(dest))
|
||||||
|
for f in ("缠论.txt", "操作策略.txt", "chanlun.txt", "K线动能理论.txt"):
|
||||||
|
p = ROOT / f
|
||||||
|
if p.exists():
|
||||||
|
dest = notes / f
|
||||||
|
if dest.exists():
|
||||||
|
dest.unlink()
|
||||||
|
shutil.move(str(p), str(dest))
|
||||||
|
tc = ROOT / "test_classifier.py"
|
||||||
|
if tc.exists():
|
||||||
|
dest = ROOT / "tests" / "test_classifier.py"
|
||||||
|
if dest.exists():
|
||||||
|
dest.unlink()
|
||||||
|
shutil.move(str(tc), str(dest))
|
||||||
|
|
||||||
|
print("moved", len(moved), "modules")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""将 timeframe.TF_DF 拆为 mixin builders(方法体原样搬移)。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SRC = ROOT / "chanlun" / "pipeline" / "timeframe.py"
|
||||||
|
BUILDERS = ROOT / "chanlun" / "pipeline" / "builders"
|
||||||
|
|
||||||
|
# method name -> builder module
|
||||||
|
ASSIGN = {
|
||||||
|
"get_ema52": "indicators",
|
||||||
|
"get_ema24": "indicators",
|
||||||
|
"add_indicators": "indicators",
|
||||||
|
"get_klu_state": "kline",
|
||||||
|
"get_ema_state": "indicators",
|
||||||
|
"check_fx1": "kline",
|
||||||
|
"check_fx": "kline",
|
||||||
|
"check_fx2": "kline",
|
||||||
|
"check_fx_pattern": "kline",
|
||||||
|
"cal_volume_ratio": "kline",
|
||||||
|
"cal_kl_data": "kline",
|
||||||
|
"get_kl_data": "kline",
|
||||||
|
"get_klc_list": "kline",
|
||||||
|
"get_klu_list": "kline",
|
||||||
|
"cal_klu_pattern": "kline",
|
||||||
|
"_detect_single_reversal_pattern": "kline",
|
||||||
|
"_detect_double_pattern": "kline",
|
||||||
|
"_detect_triple_pattern": "kline",
|
||||||
|
"cal_trend": "bi",
|
||||||
|
"get_bi_list": "bi",
|
||||||
|
"cal_bi_list": "bi",
|
||||||
|
"check_top_fx": "bi",
|
||||||
|
"check_bottom_fx": "bi",
|
||||||
|
"get_bsp_state": "bsp",
|
||||||
|
"get_above_zero_bsp": "bsp",
|
||||||
|
"find_all_bsp": "bsp",
|
||||||
|
"check_bi_div": "bsp",
|
||||||
|
"find_first_bsp": "bsp",
|
||||||
|
"find_second_bsp": "bsp",
|
||||||
|
"get_zs_state": "zs",
|
||||||
|
"cal_bi_zs": "zs",
|
||||||
|
"cal_bi_zs_list": "zs",
|
||||||
|
"get_bi_zs_list": "zs",
|
||||||
|
"cal_bi_zs_list_pure": "zs",
|
||||||
|
"calculate_seg_zs": "zs",
|
||||||
|
"get_seg_zs_list": "zs",
|
||||||
|
"get_big_zs_list": "zs",
|
||||||
|
"get_zs_list": "zs",
|
||||||
|
"get_seg_list": "seg",
|
||||||
|
"get_decimal": "indicators",
|
||||||
|
}
|
||||||
|
|
||||||
|
HEADER = '''\
|
||||||
|
"""TF_DF builder mixin — 由 split_tfdf_builders 自动生成,逻辑与原 TF_DF 一致。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
from technical.util import resample_to_interval
|
||||||
|
|
||||||
|
from chanlun.core.ChanBI import ChanBI
|
||||||
|
from chanlun.core.ChanBIZS import ChanBIZS
|
||||||
|
from chanlun.core.ChanBSP import ChanBSP
|
||||||
|
from chanlun.core.ChanEnum import (
|
||||||
|
Chan_BI_DIR,
|
||||||
|
Chan_BSP_DIR,
|
||||||
|
Chan_BSP_TYPE,
|
||||||
|
Chan_FX_TYPE,
|
||||||
|
Chan_K_DIR,
|
||||||
|
Chan_KLC_FX,
|
||||||
|
Chan_KLC_STATE,
|
||||||
|
Chan_KLINE_DIR,
|
||||||
|
Chan_KLU_PATTERN,
|
||||||
|
Chan_PRICE_TREND,
|
||||||
|
Chan_SEG_DIR,
|
||||||
|
Chan_ZS_DIR,
|
||||||
|
)
|
||||||
|
from chanlun.core.ChanKLC import ChanKLC
|
||||||
|
from chanlun.core.ChanKLU import ChanKLU
|
||||||
|
from chanlun.core.ChanSBI import ChanSBI
|
||||||
|
from chanlun.core.ChanSEG import ChanSEG
|
||||||
|
from chanlun.core.ChanZS import ChanZS, ChanZS_Big
|
||||||
|
from chanlun.indicators.ChanMACD import ChanMACD
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
source = SRC.read_text(encoding="utf-8")
|
||||||
|
# Use tabs; extract method bodies by line scanning
|
||||||
|
lines = source.splitlines(keepends=True)
|
||||||
|
# find class TF_DF
|
||||||
|
class_start = None
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.startswith("class TF_DF"):
|
||||||
|
class_start = i
|
||||||
|
break
|
||||||
|
if class_start is None:
|
||||||
|
raise SystemExit("TF_DF not found")
|
||||||
|
|
||||||
|
# collect methods: name -> (start, end exclusive)
|
||||||
|
methods = []
|
||||||
|
i = class_start + 1
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i]
|
||||||
|
m = re.match(r"^\tdef ([A-Za-z_][\w]*)\(", line)
|
||||||
|
if m:
|
||||||
|
name = m.group(1)
|
||||||
|
start = i
|
||||||
|
i += 1
|
||||||
|
while i < len(lines):
|
||||||
|
if re.match(r"^\tdef [A-Za-z_]", lines[i]) or (
|
||||||
|
lines[i] and not lines[i].startswith("\t") and not lines[i].startswith(" ") and lines[i].strip()
|
||||||
|
):
|
||||||
|
break
|
||||||
|
# nested def inside method starts with \t\tdef
|
||||||
|
i += 1
|
||||||
|
methods.append((name, start, i))
|
||||||
|
continue
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
by_mod: dict[str, list[str]] = {k: [] for k in ("indicators", "kline", "bi", "seg", "zs", "bsp")}
|
||||||
|
keep_in_facade = [] # __init__, init_TF_DF, get_current_klc
|
||||||
|
|
||||||
|
for name, start, end in methods:
|
||||||
|
body = "".join(lines[start:end])
|
||||||
|
mod = ASSIGN.get(name)
|
||||||
|
if mod is None:
|
||||||
|
keep_in_facade.append((name, body))
|
||||||
|
else:
|
||||||
|
by_mod[mod].append(body)
|
||||||
|
|
||||||
|
BUILDERS.mkdir(parents=True, exist_ok=True)
|
||||||
|
(BUILDERS / "__init__.py").write_text("", encoding="utf-8")
|
||||||
|
|
||||||
|
mixin_classes = []
|
||||||
|
for mod, bodies in by_mod.items():
|
||||||
|
class_name = "".join(p.title() for p in mod.split("_")) + "BuilderMixin"
|
||||||
|
mixin_classes.append(class_name)
|
||||||
|
content = HEADER + f"class {class_name}:\n"
|
||||||
|
if not bodies:
|
||||||
|
content += "\tpass\n"
|
||||||
|
else:
|
||||||
|
content += "\n".join(bodies)
|
||||||
|
if not content.endswith("\n"):
|
||||||
|
content += "\n"
|
||||||
|
(BUILDERS / f"{mod}.py").write_text(content, encoding="utf-8")
|
||||||
|
print(f"wrote {mod}.py methods={len(bodies)} class={class_name}")
|
||||||
|
|
||||||
|
# rewrite timeframe.py facade
|
||||||
|
facade_imports = '''\
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
from technical.util import resample_to_interval
|
||||||
|
|
||||||
|
from chanlun.core.ChanBI import ChanBI
|
||||||
|
from chanlun.core.ChanBIZS import ChanBIZS
|
||||||
|
from chanlun.core.ChanBSP import ChanBSP
|
||||||
|
from chanlun.core.ChanEnum import (
|
||||||
|
Chan_BI_DIR,
|
||||||
|
Chan_BSP_DIR,
|
||||||
|
Chan_BSP_TYPE,
|
||||||
|
Chan_FX_TYPE,
|
||||||
|
Chan_K_DIR,
|
||||||
|
Chan_KLC_FX,
|
||||||
|
Chan_KLC_STATE,
|
||||||
|
Chan_KLINE_DIR,
|
||||||
|
Chan_KLU_PATTERN,
|
||||||
|
Chan_PRICE_TREND,
|
||||||
|
Chan_SEG_DIR,
|
||||||
|
Chan_ZS_DIR,
|
||||||
|
)
|
||||||
|
from chanlun.core.ChanKLC import ChanKLC
|
||||||
|
from chanlun.core.ChanKLU import ChanKLU
|
||||||
|
from chanlun.core.ChanSBI import ChanSBI
|
||||||
|
from chanlun.core.ChanSEG import ChanSEG
|
||||||
|
from chanlun.core.ChanZS import ChanZS, ChanZS_Big
|
||||||
|
from chanlun.indicators.ChanMACD import ChanMACD
|
||||||
|
from chanlun.pipeline.builders.bi import BiBuilderMixin
|
||||||
|
from chanlun.pipeline.builders.bsp import BspBuilderMixin
|
||||||
|
from chanlun.pipeline.builders.indicators import IndicatorsBuilderMixin
|
||||||
|
from chanlun.pipeline.builders.kline import KlineBuilderMixin
|
||||||
|
from chanlun.pipeline.builders.seg import SegBuilderMixin
|
||||||
|
from chanlun.pipeline.builders.zs import ZsBuilderMixin
|
||||||
|
|
||||||
|
'''
|
||||||
|
bases = ", ".join(
|
||||||
|
[
|
||||||
|
"IndicatorsBuilderMixin",
|
||||||
|
"KlineBuilderMixin",
|
||||||
|
"BiBuilderMixin",
|
||||||
|
"SegBuilderMixin",
|
||||||
|
"ZsBuilderMixin",
|
||||||
|
"BspBuilderMixin",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
facade = facade_imports + f"class TF_DF({bases}):\n"
|
||||||
|
for name, body in keep_in_facade:
|
||||||
|
facade += body
|
||||||
|
if not body.endswith("\n"):
|
||||||
|
facade += "\n"
|
||||||
|
SRC.write_text(facade, encoding="utf-8")
|
||||||
|
print("rewrote timeframe.py facade, kept", [n for n, _ in keep_in_facade])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -3,8 +3,8 @@ from freqtrade.strategy import IStrategy, stoploss_from_absolute
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
from ChanLun import ChanLun
|
from chanlun import ChanLun
|
||||||
from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR
|
from chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
from technical.util import resample_to_interval, resampled_merge
|
from technical.util import resample_to_interval, resampled_merge
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import sys
|
|||||||
import os
|
import os
|
||||||
# 添加父目录到系统路径
|
# 添加父目录到系统路径
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
from ChanLun import ChanLun
|
from chanlun import ChanLun
|
||||||
from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX
|
from chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
from technical.util import resample_to_interval, resampled_merge
|
from technical.util import resample_to_interval, resampled_merge
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import sys
|
|||||||
import os
|
import os
|
||||||
# 添加父目录到系统路径
|
# 添加父目录到系统路径
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
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 chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
from technical.util import resample_to_interval, resampled_merge
|
from technical.util import resample_to_interval, resampled_merge
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
@@ -15,7 +15,7 @@ from freqtrade.persistence import Trade
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
from TF_DF import TF_DF
|
from chanlun import TF_DF
|
||||||
### Now you can use logger.info('asfd') to log
|
### 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-
|
# 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
|
import os
|
||||||
# 添加父目录到系统路径
|
# 添加父目录到系统路径
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
from ChanLun import ChanLun
|
from chanlun import ChanLun
|
||||||
from ChanLun_Classifier import ChanLunClassifier
|
from chanlun.analysis.ChanLun_Classifier import ChanLunClassifier
|
||||||
from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX
|
from chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
from technical.util import resample_to_interval, resampled_merge
|
from technical.util import resample_to_interval, resampled_merge
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import sys
|
|||||||
import os
|
import os
|
||||||
# 添加父目录到系统路径
|
# 添加父目录到系统路径
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
from ChanLun import ChanLun
|
from chanlun import ChanLun
|
||||||
from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX, Chan_BSP_TYPE
|
from chanlun.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
|
from technical.util import resample_to_interval, resampled_merge
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import sys
|
|||||||
import os
|
import os
|
||||||
# 添加父目录到系统路径
|
# 添加父目录到系统路径
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
from ChanLun import ChanLun
|
from chanlun import ChanLun
|
||||||
from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX, Chan_BSP_TYPE
|
from chanlun.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
|
from technical.util import resample_to_interval, resampled_merge
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import sys
|
|||||||
import os
|
import os
|
||||||
# 添加父目录到系统路径
|
# 添加父目录到系统路径
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
from ChanLun import ChanLun
|
from chanlun import ChanLun
|
||||||
from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX
|
from chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
from technical.util import resample_to_interval, resampled_merge
|
from technical.util import resample_to_interval, resampled_merge
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import sys
|
|||||||
import os
|
import os
|
||||||
# 添加父目录到系统路径
|
# 添加父目录到系统路径
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
from ChanLun import ChanLun
|
from chanlun import ChanLun
|
||||||
from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX, Chan_BSP_TYPE
|
from chanlun.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
|
from technical.util import resample_to_interval, resampled_merge
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import sys
|
|||||||
import os
|
import os
|
||||||
# 添加父目录到系统路径
|
# 添加父目录到系统路径
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
from ChanLun import ChanLun
|
from chanlun import ChanLun
|
||||||
from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX
|
from chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
from technical.util import resample_to_interval, resampled_merge
|
from technical.util import resample_to_interval, resampled_merge
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import sys
|
|||||||
import os
|
import os
|
||||||
# 添加父目录到系统路径
|
# 添加父目录到系统路径
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
from ChanLun import ChanLun
|
from chanlun import ChanLun
|
||||||
from ChanLun_Classifier import ChanLunClassifier
|
from chanlun.analysis.ChanLun_Classifier import ChanLunClassifier
|
||||||
from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX
|
from chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX
|
||||||
from ChanPY import ChanPY
|
from chanlun.analysis.ChanPY import ChanPY
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
from technical.util import resample_to_interval, resampled_merge
|
from technical.util import resample_to_interval, resampled_merge
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import sys
|
|||||||
import os
|
import os
|
||||||
# 添加父目录到系统路径
|
# 添加父目录到系统路径
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
from ChanLun import ChanLun
|
from chanlun import ChanLun
|
||||||
from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX
|
from chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
from technical.util import resample_to_interval, resampled_merge
|
from technical.util import resample_to_interval, resampled_merge
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user