Add files to chanlun_1
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
# 缠论分析系统
|
||||
|
||||
一个基于Python实现的完整缠论技术分析系统,支持数字货币市场的实时数据分析和可视化展示。
|
||||
|
||||
## 系统特性
|
||||
|
||||
### 核心功能
|
||||
- **K线包含关系处理**:自动识别和合并K线包含关系
|
||||
- **分型识别**:精确识别顶分型和底分型,支持多级别强度验证
|
||||
- **笔构建**:基于分型生成完整的笔结构
|
||||
- **线段识别**:从笔构建线段,支持线段确认和破坏逻辑
|
||||
- **中枢检测**:识别不同级别的中枢,分析震荡区间
|
||||
- **买卖点信号**:实现三类买卖点的自动识别
|
||||
|
||||
### 数据支持
|
||||
- 支持多个主流数字货币交易所(Binance、OKX等)
|
||||
- 多时间周期分析(1分钟到1周)
|
||||
- 实时数据获取和历史数据分析
|
||||
- 数据清理和验证机制
|
||||
|
||||
### 可视化功能
|
||||
- **交互式图表**:基于Plotly的高质量图表
|
||||
- **Web界面**:Dash框架构建的现代化Web界面
|
||||
- **多层次展示**:K线、分型、笔、线段、中枢、买卖点一体化显示
|
||||
- **统计分析**:提供详细的统计信息和模式分析
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
/chanlun
|
||||
├── /data # 数据模块
|
||||
│ ├── __init__.py
|
||||
│ ├── data_fetcher.py # 数据获取(ccxt)
|
||||
│ └── data_processor.py # 数据处理和清理
|
||||
├── /core # 缠论核心逻辑
|
||||
│ ├── __init__.py
|
||||
│ ├── kline.py # K线包含关系处理
|
||||
│ ├── fractal.py # 分型识别
|
||||
│ ├── stroke.py # 笔逻辑
|
||||
│ ├── segment.py # 线段逻辑
|
||||
│ ├── central_bank.py # 中枢识别
|
||||
│ ├── trading_signal.py # 买卖点信号
|
||||
│ └── chan_analyzer.py # 综合分析器
|
||||
├── /web # Web可视化
|
||||
│ ├── __init__.py
|
||||
│ ├── app.py # Dash应用
|
||||
│ └── visualization.py # 图表生成
|
||||
├── /tests # 测试文件
|
||||
├── main.py # 主程序入口
|
||||
├── requirements.txt # 依赖包
|
||||
└── README.md # 说明文档
|
||||
```
|
||||
|
||||
## 安装和使用
|
||||
|
||||
### 环境要求
|
||||
- Python 3.8+
|
||||
- 推荐使用虚拟环境
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd chanlun
|
||||
```
|
||||
|
||||
2. **创建虚拟环境**
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Linux/Mac
|
||||
# 或
|
||||
venv\Scripts\activate # Windows
|
||||
```
|
||||
|
||||
3. **安装依赖**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 运行方式
|
||||
|
||||
#### 1. Web界面模式(推荐)
|
||||
```bash
|
||||
python main.py --mode web
|
||||
```
|
||||
然后在浏览器中访问 `http://localhost:8050`
|
||||
|
||||
#### 2. 命令行模式
|
||||
```bash
|
||||
# 基本分析
|
||||
python main.py --mode cli --symbol BTC/USDT --timeframe 1h
|
||||
|
||||
# 自定义参数
|
||||
python main.py --mode cli \
|
||||
--symbol ETH/USDT \
|
||||
--timeframe 4h \
|
||||
--limit 1000 \
|
||||
--fractal-strength 2 \
|
||||
--export
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `--mode` | 运行模式:web或cli | web |
|
||||
| `--symbol` | 交易对 | BTC/USDT |
|
||||
| `--timeframe` | 时间周期 | 1h |
|
||||
| `--limit` | K线数量 | 500 |
|
||||
| `--fractal-strength` | 分型强度 | 1 |
|
||||
| `--exchange` | 交易所 | binance |
|
||||
| `--port` | Web端口 | 8050 |
|
||||
| `--export` | 导出结果 | False |
|
||||
|
||||
## 缠论理论说明
|
||||
|
||||
### 基础概念
|
||||
|
||||
1. **K线包含关系**
|
||||
- 当一根K线的高低点完全包含另一根K线时,需要进行合并处理
|
||||
- 合并规则根据当前趋势方向确定
|
||||
|
||||
2. **分型**
|
||||
- 顶分型:中间K线高点比左右K线都高
|
||||
- 底分型:中间K线低点比左右K线都低
|
||||
- 支持不同强度的分型识别
|
||||
|
||||
3. **笔**
|
||||
- 连接相邻不同类型分型的直线
|
||||
- 必须满足严格的分型交替规则
|
||||
|
||||
4. **线段**
|
||||
- 由至少3笔组成
|
||||
- 有明确的生成和破坏规则
|
||||
|
||||
5. **中枢**
|
||||
- 至少3个线段的重叠区域
|
||||
- 是价格震荡的核心区间
|
||||
|
||||
6. **买卖点**
|
||||
- 一类:中枢突破点
|
||||
- 二类:回拉确认点
|
||||
- 三类:次级别突破点
|
||||
|
||||
## 使用示例
|
||||
|
||||
### Web界面使用
|
||||
|
||||
1. 启动Web服务
|
||||
2. 选择交易对和时间周期
|
||||
3. 调整分析参数
|
||||
4. 点击"获取数据并分析"
|
||||
5. 查看可视化结果和统计信息
|
||||
|
||||
### 编程接口
|
||||
|
||||
```python
|
||||
from data.data_fetcher import DataFetcher
|
||||
from core.chan_analyzer import ChanAnalyzer
|
||||
|
||||
# 获取数据
|
||||
fetcher = DataFetcher()
|
||||
klines = fetcher.fetch_klines('BTC/USDT', '1h', 500)
|
||||
|
||||
# 缠论分析
|
||||
analyzer = ChanAnalyzer(klines)
|
||||
results = analyzer.run_full_analysis()
|
||||
|
||||
# 获取分析结果
|
||||
fractals = analyzer.fractals
|
||||
strokes = analyzer.strokes
|
||||
segments = analyzer.segments
|
||||
central_banks = analyzer.central_banks
|
||||
trading_points = analyzer.trading_points
|
||||
|
||||
# 当前市场结构
|
||||
market_structure = analyzer.get_current_market_structure()
|
||||
```
|
||||
|
||||
## 输出说明
|
||||
|
||||
### 分析结果
|
||||
- **分型数据**:包含位置、价格、类型、强度等信息
|
||||
- **笔数据**:起止点、方向、长度、强度等
|
||||
- **线段数据**:组成笔、方向、确认状态等
|
||||
- **中枢数据**:边界价格、级别、持续时间等
|
||||
- **买卖点**:类型、价格、强度、描述等
|
||||
|
||||
### 可视化图表
|
||||
- K线图与成交量
|
||||
- 分型标记(三角形标识)
|
||||
- 笔(实线连接)
|
||||
- 线段(虚线连接)
|
||||
- 中枢(矩形区域)
|
||||
- 买卖点(箭头标识)
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **数据质量**:确保网络连接稳定,避免数据获取失败
|
||||
2. **计算复杂度**:大量数据分析可能需要较长时间
|
||||
3. **参数调整**:分型强度等参数需要根据具体情况调整
|
||||
4. **投资风险**:本系统仅供技术分析参考,不构成投资建议
|
||||
|
||||
## 技术特点
|
||||
|
||||
- **模块化设计**:各功能模块独立,便于维护和扩展
|
||||
- **面向对象**:使用OOP设计,代码结构清晰
|
||||
- **类型提示**:完整的类型注解,提高代码可读性
|
||||
- **错误处理**:完善的异常处理机制
|
||||
- **日志记录**:详细的运行日志
|
||||
- **可扩展性**:支持新增交易所和指标
|
||||
|
||||
## 开发说明
|
||||
|
||||
### 扩展新交易所
|
||||
在`data_fetcher.py`中添加新的交易所支持:
|
||||
```python
|
||||
exchange = ccxt.new_exchange_name({
|
||||
# 配置参数
|
||||
})
|
||||
```
|
||||
|
||||
### 添加新指标
|
||||
在相应模块中实现新的分析方法,遵循现有的接口设计。
|
||||
|
||||
### 自定义可视化
|
||||
在`visualization.py`中添加新的图表类型。
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用MIT许可证,详见LICENSE文件。
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交Issue和Pull Request来改进本项目。
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有问题或建议,请通过GitHub Issues联系。
|
||||
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
缠论核心模块:实现缠论的所有核心元素
|
||||
"""
|
||||
|
||||
from .kline import KLine
|
||||
from .fractal import Fractal
|
||||
from .stroke import Stroke
|
||||
from .segment import Segment
|
||||
from .central_bank import CentralBank
|
||||
from .trading_signal import TradingSignal
|
||||
from .chan_analyzer import ChanAnalyzer
|
||||
|
||||
__all__ = [
|
||||
'KLine',
|
||||
'Fractal',
|
||||
'Stroke',
|
||||
'Segment',
|
||||
'CentralBank',
|
||||
'TradingSignal',
|
||||
'ChanAnalyzer'
|
||||
]
|
||||
@@ -0,0 +1,416 @@
|
||||
"""
|
||||
中枢模块:识别价格在某个区间内的震荡模式
|
||||
中枢定义:至少由三个连续同级别重叠的线段组成
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import List, Tuple, Optional, Dict
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from .segment import SegmentElement
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CentralBankElement:
|
||||
"""中枢元素数据类"""
|
||||
segments: List[SegmentElement] # 组成中枢的线段
|
||||
high_price: float # 中枢上边界
|
||||
low_price: float # 中枢下边界
|
||||
center_price: float # 中枢中心价格
|
||||
start_time: pd.Timestamp # 中枢开始时间
|
||||
end_time: pd.Timestamp # 中枢结束时间
|
||||
duration: int # 中枢持续时间
|
||||
strength: float # 中枢强度
|
||||
level: str # 中枢级别
|
||||
confirmed: bool = False # 是否已确认
|
||||
|
||||
|
||||
class CentralBank:
|
||||
"""中枢识别和处理类"""
|
||||
|
||||
def __init__(self, segments: List[SegmentElement]):
|
||||
"""
|
||||
初始化中枢处理器
|
||||
|
||||
Args:
|
||||
segments: 线段列表
|
||||
"""
|
||||
self.segments = sorted(segments, key=lambda x: x.start_time)
|
||||
self.central_banks = []
|
||||
self.min_segments = 3 # 形成中枢的最少线段数
|
||||
|
||||
def find_overlapping_segments(self, segments: List[SegmentElement]) -> List[SegmentElement]:
|
||||
"""
|
||||
寻找重叠的线段组
|
||||
|
||||
Args:
|
||||
segments: 线段列表
|
||||
|
||||
Returns:
|
||||
重叠线段组
|
||||
"""
|
||||
if len(segments) < self.min_segments:
|
||||
return []
|
||||
|
||||
# 找到所有线段的价格区间
|
||||
overlapping = []
|
||||
|
||||
for i, seg1 in enumerate(segments[:-2]):
|
||||
overlapping_group = [seg1]
|
||||
|
||||
for j in range(i + 1, len(segments)):
|
||||
seg2 = segments[j]
|
||||
|
||||
# 检查与group中任意线段是否重叠
|
||||
has_overlap = False
|
||||
for existing_seg in overlapping_group:
|
||||
if self._segments_overlap(existing_seg, seg2):
|
||||
has_overlap = True
|
||||
break
|
||||
|
||||
if has_overlap:
|
||||
overlapping_group.append(seg2)
|
||||
else:
|
||||
break # 一旦不重叠就停止扩展
|
||||
|
||||
# 如果找到足够的重叠线段,返回这组
|
||||
if len(overlapping_group) >= self.min_segments:
|
||||
overlapping.extend(overlapping_group)
|
||||
|
||||
return overlapping
|
||||
|
||||
def _segments_overlap(self, seg1: SegmentElement, seg2: SegmentElement) -> bool:
|
||||
"""
|
||||
判断两个线段是否重叠
|
||||
|
||||
Args:
|
||||
seg1: 线段1
|
||||
seg2: 线段2
|
||||
|
||||
Returns:
|
||||
是否重叠
|
||||
"""
|
||||
# 获取每个线段的价格区间
|
||||
seg1_high = max(seg1.start_price, seg1.end_price)
|
||||
seg1_low = min(seg1.start_price, seg1.end_price)
|
||||
seg2_high = max(seg2.start_price, seg2.end_price)
|
||||
seg2_low = min(seg2.start_price, seg2.end_price)
|
||||
|
||||
# 检查区间是否重叠
|
||||
return not (seg1_high < seg2_low or seg2_high < seg1_low)
|
||||
|
||||
def calculate_overlap_zone(self, segments: List[SegmentElement]) -> Tuple[float, float]:
|
||||
"""
|
||||
计算多个线段的重叠区域
|
||||
|
||||
Args:
|
||||
segments: 线段列表
|
||||
|
||||
Returns:
|
||||
(重叠区域下边界, 重叠区域上边界)
|
||||
"""
|
||||
if not segments:
|
||||
return 0, 0
|
||||
|
||||
# 计算所有线段的价格区间
|
||||
all_highs = []
|
||||
all_lows = []
|
||||
|
||||
for seg in segments:
|
||||
all_highs.append(max(seg.start_price, seg.end_price))
|
||||
all_lows.append(min(seg.start_price, seg.end_price))
|
||||
|
||||
# 重叠区域是所有高点的最小值和所有低点的最大值
|
||||
overlap_high = min(all_highs)
|
||||
overlap_low = max(all_lows)
|
||||
|
||||
# 确保重叠区域有效
|
||||
if overlap_high > overlap_low:
|
||||
return overlap_low, overlap_high
|
||||
else:
|
||||
return 0, 0
|
||||
|
||||
def create_central_bank(self, segments: List[SegmentElement]) -> Optional[CentralBankElement]:
|
||||
"""
|
||||
创建中枢元素
|
||||
|
||||
Args:
|
||||
segments: 组成中枢的线段
|
||||
|
||||
Returns:
|
||||
中枢元素,如果无效则返回None
|
||||
"""
|
||||
if len(segments) < self.min_segments:
|
||||
return None
|
||||
|
||||
# 计算重叠区域
|
||||
low_price, high_price = self.calculate_overlap_zone(segments)
|
||||
|
||||
if low_price >= high_price:
|
||||
return None # 无有效重叠区域
|
||||
|
||||
center_price = (high_price + low_price) / 2
|
||||
start_time = min(seg.start_time for seg in segments)
|
||||
end_time = max(seg.end_time for seg in segments)
|
||||
|
||||
# 计算持续时间(简化为小时数)
|
||||
duration = int((end_time - start_time).total_seconds() / 3600)
|
||||
|
||||
# 计算中枢强度
|
||||
strength = self._calculate_central_bank_strength(segments, high_price - low_price, duration)
|
||||
|
||||
# 确定中枢级别
|
||||
level = self._determine_central_bank_level(segments)
|
||||
|
||||
return CentralBankElement(
|
||||
segments=segments,
|
||||
high_price=high_price,
|
||||
low_price=low_price,
|
||||
center_price=center_price,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
duration=duration,
|
||||
strength=strength,
|
||||
level=level
|
||||
)
|
||||
|
||||
def _calculate_central_bank_strength(self, segments: List[SegmentElement],
|
||||
height: float, duration: int) -> float:
|
||||
"""
|
||||
计算中枢强度
|
||||
|
||||
Args:
|
||||
segments: 组成中枢的线段
|
||||
height: 中枢高度
|
||||
duration: 持续时间
|
||||
|
||||
Returns:
|
||||
中枢强度
|
||||
"""
|
||||
# 基础强度:线段数量和平均强度
|
||||
base_strength = len(segments) * np.mean([seg.strength for seg in segments])
|
||||
|
||||
# 高度因子:适中的高度得分更高
|
||||
height_factor = 1 / (1 + height * 0.01) # 高度越大,因子越小
|
||||
|
||||
# 时间因子:持续时间适中得分更高
|
||||
time_factor = min(duration / 24, 2.0) # 以24小时为基准,最多2倍
|
||||
|
||||
return base_strength * height_factor * (1 + time_factor * 0.1)
|
||||
|
||||
def _determine_central_bank_level(self, segments: List[SegmentElement]) -> str:
|
||||
"""
|
||||
确定中枢级别
|
||||
|
||||
Args:
|
||||
segments: 组成中枢的线段
|
||||
|
||||
Returns:
|
||||
中枢级别
|
||||
"""
|
||||
# 简化的级别判断:根据线段数量和强度
|
||||
avg_strength = np.mean([seg.strength for seg in segments])
|
||||
segment_count = len(segments)
|
||||
|
||||
if segment_count >= 5 and avg_strength > 100:
|
||||
return "1日"
|
||||
elif segment_count >= 4 and avg_strength > 50:
|
||||
return "4小时"
|
||||
elif segment_count >= 3 and avg_strength > 20:
|
||||
return "1小时"
|
||||
else:
|
||||
return "30分钟"
|
||||
|
||||
def detect_central_banks(self) -> List[CentralBankElement]:
|
||||
"""
|
||||
检测所有中枢
|
||||
|
||||
Returns:
|
||||
中枢列表
|
||||
"""
|
||||
if len(self.segments) < self.min_segments:
|
||||
logger.warning("线段数量不足,无法形成中枢")
|
||||
return []
|
||||
|
||||
central_banks = []
|
||||
|
||||
# 使用滑动窗口寻找中枢
|
||||
for i in range(len(self.segments) - self.min_segments + 1):
|
||||
# 尝试不同长度的窗口
|
||||
for window_size in range(self.min_segments, min(8, len(self.segments) - i + 1)):
|
||||
window_segments = self.segments[i:i + window_size]
|
||||
|
||||
# 检查这些线段是否能形成中枢
|
||||
if self._can_form_central_bank(window_segments):
|
||||
central_bank = self.create_central_bank(window_segments)
|
||||
if central_bank:
|
||||
# 检查是否与已有中枢重复
|
||||
if not self._is_duplicate_central_bank(central_bank, central_banks):
|
||||
central_banks.append(central_bank)
|
||||
|
||||
self.central_banks = central_banks
|
||||
logger.info(f"检测到 {len(central_banks)} 个中枢")
|
||||
|
||||
return central_banks
|
||||
|
||||
def _can_form_central_bank(self, segments: List[SegmentElement]) -> bool:
|
||||
"""
|
||||
判断线段组是否能形成中枢
|
||||
|
||||
Args:
|
||||
segments: 线段组
|
||||
|
||||
Returns:
|
||||
是否能形成中枢
|
||||
"""
|
||||
if len(segments) < self.min_segments:
|
||||
return False
|
||||
|
||||
# 检查是否有足够的重叠
|
||||
overlap_count = 0
|
||||
for i in range(len(segments) - 1):
|
||||
for j in range(i + 1, len(segments)):
|
||||
if self._segments_overlap(segments[i], segments[j]):
|
||||
overlap_count += 1
|
||||
|
||||
# 至少需要一半的线段对重叠
|
||||
required_overlaps = len(segments) // 2
|
||||
return overlap_count >= required_overlaps
|
||||
|
||||
def _is_duplicate_central_bank(self, new_cb: CentralBankElement,
|
||||
existing_cbs: List[CentralBankElement]) -> bool:
|
||||
"""
|
||||
检查是否为重复的中枢
|
||||
|
||||
Args:
|
||||
new_cb: 新中枢
|
||||
existing_cbs: 已有中枢列表
|
||||
|
||||
Returns:
|
||||
是否重复
|
||||
"""
|
||||
for existing_cb in existing_cbs:
|
||||
# 检查时间和价格区间是否大量重叠
|
||||
time_overlap = (min(new_cb.end_time, existing_cb.end_time) -
|
||||
max(new_cb.start_time, existing_cb.start_time)).total_seconds()
|
||||
|
||||
price_overlap = (min(new_cb.high_price, existing_cb.high_price) -
|
||||
max(new_cb.low_price, existing_cb.low_price))
|
||||
|
||||
if time_overlap > 0 and price_overlap > 0:
|
||||
# 计算重叠比例
|
||||
new_duration = (new_cb.end_time - new_cb.start_time).total_seconds()
|
||||
new_height = new_cb.high_price - new_cb.low_price
|
||||
|
||||
time_overlap_ratio = time_overlap / new_duration if new_duration > 0 else 0
|
||||
price_overlap_ratio = price_overlap / new_height if new_height > 0 else 0
|
||||
|
||||
# 如果时间和价格重叠都超过70%,认为是重复
|
||||
if time_overlap_ratio > 0.7 and price_overlap_ratio > 0.7:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def analyze_central_bank_patterns(self) -> Dict:
|
||||
"""
|
||||
分析中枢模式
|
||||
|
||||
Returns:
|
||||
模式分析结果
|
||||
"""
|
||||
if not self.central_banks:
|
||||
return {}
|
||||
|
||||
# 统计不同级别的中枢
|
||||
level_counts = {}
|
||||
for cb in self.central_banks:
|
||||
level_counts[cb.level] = level_counts.get(cb.level, 0) + 1
|
||||
|
||||
# 计算平均指标
|
||||
avg_strength = np.mean([cb.strength for cb in self.central_banks])
|
||||
avg_duration = np.mean([cb.duration for cb in self.central_banks])
|
||||
avg_height = np.mean([cb.high_price - cb.low_price for cb in self.central_banks])
|
||||
|
||||
# 寻找最强中枢
|
||||
strongest_cb = max(self.central_banks, key=lambda x: x.strength) if self.central_banks else None
|
||||
|
||||
return {
|
||||
'total_central_banks': len(self.central_banks),
|
||||
'level_distribution': level_counts,
|
||||
'avg_strength': avg_strength,
|
||||
'avg_duration': avg_duration,
|
||||
'avg_height': avg_height,
|
||||
'strongest_central_bank': {
|
||||
'strength': strongest_cb.strength,
|
||||
'level': strongest_cb.level,
|
||||
'duration': strongest_cb.duration
|
||||
} if strongest_cb else None,
|
||||
'confirmed_central_banks': sum(1 for cb in self.central_banks if cb.confirmed)
|
||||
}
|
||||
|
||||
def find_central_bank_breaks(self) -> List[Dict]:
|
||||
"""
|
||||
寻找中枢突破
|
||||
|
||||
Returns:
|
||||
突破信息列表
|
||||
"""
|
||||
breaks = []
|
||||
|
||||
for cb in self.central_banks:
|
||||
# 检查中枢后续价格是否突破
|
||||
post_segments = [seg for seg in self.segments if seg.start_time > cb.end_time]
|
||||
|
||||
for seg in post_segments[:3]: # 只看后续3个线段
|
||||
if seg.direction == 1 and seg.end_price > cb.high_price:
|
||||
# 向上突破
|
||||
breaks.append({
|
||||
'central_bank': cb,
|
||||
'break_type': 'upward',
|
||||
'break_segment': seg,
|
||||
'break_strength': seg.end_price - cb.high_price
|
||||
})
|
||||
break
|
||||
elif seg.direction == -1 and seg.end_price < cb.low_price:
|
||||
# 向下突破
|
||||
breaks.append({
|
||||
'central_bank': cb,
|
||||
'break_type': 'downward',
|
||||
'break_segment': seg,
|
||||
'break_strength': cb.low_price - seg.end_price
|
||||
})
|
||||
break
|
||||
|
||||
return breaks
|
||||
|
||||
def to_dataframe(self) -> pd.DataFrame:
|
||||
"""
|
||||
将中枢转换为DataFrame
|
||||
|
||||
Returns:
|
||||
包含中枢信息的DataFrame
|
||||
"""
|
||||
if not self.central_banks:
|
||||
return pd.DataFrame()
|
||||
|
||||
data = []
|
||||
for i, cb in enumerate(self.central_banks):
|
||||
data.append({
|
||||
'central_bank_id': i,
|
||||
'start_time': cb.start_time,
|
||||
'end_time': cb.end_time,
|
||||
'high_price': cb.high_price,
|
||||
'low_price': cb.low_price,
|
||||
'center_price': cb.center_price,
|
||||
'height': cb.high_price - cb.low_price,
|
||||
'duration': cb.duration,
|
||||
'strength': cb.strength,
|
||||
'level': cb.level,
|
||||
'segment_count': len(cb.segments),
|
||||
'confirmed': cb.confirmed
|
||||
})
|
||||
|
||||
return pd.DataFrame(data)
|
||||
@@ -0,0 +1,421 @@
|
||||
"""
|
||||
缠论综合分析器:整合所有核心模块进行完整的缠论分析
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import logging
|
||||
|
||||
from .kline import KLine
|
||||
from .fractal import Fractal, FractalPoint
|
||||
from .stroke import Stroke, StrokeElement
|
||||
from .segment import Segment, SegmentElement
|
||||
from .central_bank import CentralBank, CentralBankElement
|
||||
from .trading_signal import TradingSignal, TradingPoint
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChanAnalyzer:
|
||||
"""缠论综合分析器"""
|
||||
|
||||
def __init__(self, kline_data: pd.DataFrame):
|
||||
"""
|
||||
初始化缠论分析器
|
||||
|
||||
Args:
|
||||
kline_data: 原始K线数据
|
||||
"""
|
||||
self.original_data = kline_data.copy()
|
||||
self.processed_data = None
|
||||
|
||||
# 各模块实例
|
||||
self.kline_processor = None
|
||||
self.fractal_detector = None
|
||||
self.stroke_detector = None
|
||||
self.segment_detector = None
|
||||
self.central_bank_detector = None
|
||||
self.trading_signal_detector = None
|
||||
|
||||
# 分析结果
|
||||
self.fractals = []
|
||||
self.strokes = []
|
||||
self.segments = []
|
||||
self.central_banks = []
|
||||
self.trading_points = []
|
||||
|
||||
# 分析状态
|
||||
self.is_analyzed = False
|
||||
|
||||
def run_full_analysis(self, fractal_strength: int = 1) -> Dict:
|
||||
"""
|
||||
运行完整的缠论分析
|
||||
|
||||
Args:
|
||||
fractal_strength: 分型强度要求
|
||||
|
||||
Returns:
|
||||
分析结果摘要
|
||||
"""
|
||||
logger.info("开始完整缠论分析...")
|
||||
|
||||
try:
|
||||
# 1. 处理K线包含关系
|
||||
self._process_klines()
|
||||
|
||||
# 2. 识别分型
|
||||
self._detect_fractals(fractal_strength)
|
||||
|
||||
# 3. 生成笔
|
||||
self._detect_strokes()
|
||||
|
||||
# 4. 生成线段
|
||||
self._detect_segments()
|
||||
|
||||
# 5. 识别中枢
|
||||
self._detect_central_banks()
|
||||
|
||||
# 6. 识别买卖点
|
||||
self._detect_trading_signals()
|
||||
|
||||
self.is_analyzed = True
|
||||
|
||||
# 生成分析摘要
|
||||
summary = self._generate_analysis_summary()
|
||||
|
||||
logger.info("缠论分析完成")
|
||||
return summary
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"缠论分析失败: {e}")
|
||||
raise
|
||||
|
||||
def _process_klines(self):
|
||||
"""处理K线包含关系"""
|
||||
logger.info("处理K线包含关系...")
|
||||
|
||||
self.kline_processor = KLine(self.original_data)
|
||||
self.processed_data = self.kline_processor.get_processed_data()
|
||||
|
||||
logger.info(f"K线处理完成:{len(self.original_data)} -> {len(self.processed_data)}")
|
||||
|
||||
def _detect_fractals(self, strength: int = 1):
|
||||
"""识别分型"""
|
||||
logger.info("识别分型...")
|
||||
|
||||
self.fractal_detector = Fractal(self.processed_data, min_strength=strength)
|
||||
self.fractals = self.fractal_detector.detect_fractals()
|
||||
|
||||
logger.info(f"分型识别完成:共 {len(self.fractals)} 个")
|
||||
|
||||
def _detect_strokes(self):
|
||||
"""生成笔"""
|
||||
logger.info("生成笔...")
|
||||
|
||||
if not self.fractals:
|
||||
logger.warning("没有分型,无法生成笔")
|
||||
return
|
||||
|
||||
self.stroke_detector = Stroke(self.fractals, self.processed_data)
|
||||
self.strokes = self.stroke_detector.detect_strokes()
|
||||
|
||||
logger.info(f"笔生成完成:共 {len(self.strokes)} 个")
|
||||
|
||||
def _detect_segments(self):
|
||||
"""生成线段"""
|
||||
logger.info("生成线段...")
|
||||
|
||||
if not self.strokes:
|
||||
logger.warning("没有笔,无法生成线段")
|
||||
return
|
||||
|
||||
self.segment_detector = Segment(self.strokes)
|
||||
self.segments = self.segment_detector.detect_segments()
|
||||
|
||||
logger.info(f"线段生成完成:共 {len(self.segments)} 个")
|
||||
|
||||
def _detect_central_banks(self):
|
||||
"""识别中枢"""
|
||||
logger.info("识别中枢...")
|
||||
|
||||
if not self.segments:
|
||||
logger.warning("没有线段,无法识别中枢")
|
||||
return
|
||||
|
||||
self.central_bank_detector = CentralBank(self.segments)
|
||||
self.central_banks = self.central_bank_detector.detect_central_banks()
|
||||
|
||||
logger.info(f"中枢识别完成:共 {len(self.central_banks)} 个")
|
||||
|
||||
def _detect_trading_signals(self):
|
||||
"""识别买卖点"""
|
||||
logger.info("识别买卖点...")
|
||||
|
||||
self.trading_signal_detector = TradingSignal(
|
||||
self.central_banks, self.segments, self.strokes
|
||||
)
|
||||
|
||||
# 检测中枢相关的买卖点
|
||||
central_bank_signals = []
|
||||
if self.central_banks:
|
||||
central_bank_signals = self.trading_signal_detector.detect_all_trading_points()
|
||||
|
||||
# 如果中枢数量不足,增加基于分型的买卖点识别
|
||||
fractal_signals = []
|
||||
if len(self.central_banks) < 2 and self.fractals:
|
||||
logger.info("中枢数量不足,启用分型买卖点识别")
|
||||
fractal_signals = self.trading_signal_detector.detect_fractal_based_signals(
|
||||
self.fractals, self.processed_data
|
||||
)
|
||||
|
||||
# 合并所有信号
|
||||
all_signals = central_bank_signals + fractal_signals
|
||||
|
||||
# 去重和排序
|
||||
unique_signals = []
|
||||
seen_keys = set()
|
||||
for signal in sorted(all_signals, key=lambda x: x.timestamp):
|
||||
# 创建唯一键:时间+类型+类别
|
||||
key = f"{signal.timestamp.strftime('%Y%m%d%H%M')}_{signal.signal_type}_{signal.point_class}"
|
||||
if key not in seen_keys:
|
||||
unique_signals.append(signal)
|
||||
seen_keys.add(key)
|
||||
|
||||
self.trading_points = unique_signals
|
||||
|
||||
# 统计信号
|
||||
central_count = len(central_bank_signals)
|
||||
fractal_count = len(fractal_signals)
|
||||
total_count = len(unique_signals)
|
||||
|
||||
class_counts = {'first': 0, 'second': 0, 'third': 0}
|
||||
for signal in unique_signals:
|
||||
class_counts[signal.point_class] += 1
|
||||
|
||||
logger.info(f"买卖点识别完成:中枢相关 {central_count} 个,分型相关 {fractal_count} 个,"
|
||||
f"一类 {class_counts['first']} 个,二类 {class_counts['second']} 个,"
|
||||
f"三类 {class_counts['third']} 个,总计 {total_count} 个")
|
||||
|
||||
def _generate_analysis_summary(self) -> Dict:
|
||||
"""生成分析摘要"""
|
||||
summary = {
|
||||
'data_info': {
|
||||
'original_klines': len(self.original_data),
|
||||
'processed_klines': len(self.processed_data) if self.processed_data is not None else 0,
|
||||
'date_range': {
|
||||
'start': self.original_data.index.min(),
|
||||
'end': self.original_data.index.max()
|
||||
}
|
||||
},
|
||||
'fractal_info': {
|
||||
'total': len(self.fractals),
|
||||
'top': len([f for f in self.fractals if f.fractal_type == 'top']),
|
||||
'bottom': len([f for f in self.fractals if f.fractal_type == 'bottom'])
|
||||
},
|
||||
'stroke_info': {
|
||||
'total': len(self.strokes),
|
||||
'up': len([s for s in self.strokes if s.direction == 1]),
|
||||
'down': len([s for s in self.strokes if s.direction == -1])
|
||||
},
|
||||
'segment_info': {
|
||||
'total': len(self.segments),
|
||||
'up': len([s for s in self.segments if s.direction == 1]),
|
||||
'down': len([s for s in self.segments if s.direction == -1])
|
||||
},
|
||||
'central_bank_info': {
|
||||
'total': len(self.central_banks),
|
||||
'levels': self._get_central_bank_levels()
|
||||
},
|
||||
'trading_signal_info': {
|
||||
'total': len(self.trading_points),
|
||||
'buy_points': len([p for p in self.trading_points if p.signal_type == 'buy']),
|
||||
'sell_points': len([p for p in self.trading_points if p.signal_type == 'sell']),
|
||||
'by_class': self._get_signal_class_distribution()
|
||||
}
|
||||
}
|
||||
|
||||
return summary
|
||||
|
||||
def _get_central_bank_levels(self) -> Dict:
|
||||
"""获取中枢级别分布"""
|
||||
levels = {}
|
||||
for cb in self.central_banks:
|
||||
levels[cb.level] = levels.get(cb.level, 0) + 1
|
||||
return levels
|
||||
|
||||
def _get_signal_class_distribution(self) -> Dict:
|
||||
"""获取买卖点类别分布"""
|
||||
distribution = {}
|
||||
for point in self.trading_points:
|
||||
key = f"{point.point_class}_class"
|
||||
distribution[key] = distribution.get(key, 0) + 1
|
||||
return distribution
|
||||
|
||||
def get_latest_signals(self, hours: int = 24) -> List[TradingPoint]:
|
||||
"""
|
||||
获取最近的买卖点信号
|
||||
|
||||
Args:
|
||||
hours: 最近多少小时
|
||||
|
||||
Returns:
|
||||
最近的信号列表
|
||||
"""
|
||||
if not self.is_analyzed or not self.trading_points:
|
||||
return []
|
||||
|
||||
latest_time = self.processed_data.index[-1]
|
||||
cutoff_time = latest_time - pd.Timedelta(hours=hours)
|
||||
|
||||
return [p for p in self.trading_points if p.timestamp >= cutoff_time]
|
||||
|
||||
def get_current_market_structure(self) -> Dict:
|
||||
"""
|
||||
获取当前市场结构
|
||||
|
||||
Returns:
|
||||
当前市场结构信息
|
||||
"""
|
||||
if not self.is_analyzed:
|
||||
return {}
|
||||
|
||||
current_price = self.processed_data['close'].iloc[-1]
|
||||
current_time = self.processed_data.index[-1]
|
||||
|
||||
# 最近的中枢
|
||||
recent_central_banks = [cb for cb in self.central_banks
|
||||
if (current_time - cb.end_time).total_seconds() < 7*24*3600] # 7天内
|
||||
|
||||
# 最近的线段趋势
|
||||
recent_segments = [seg for seg in self.segments
|
||||
if (current_time - seg.end_time).total_seconds() < 3*24*3600] # 3天内
|
||||
|
||||
# 当前趋势方向
|
||||
current_trend = self._determine_current_trend(recent_segments)
|
||||
|
||||
# 支撑阻力位
|
||||
support_resistance = self._calculate_support_resistance(recent_central_banks)
|
||||
|
||||
return {
|
||||
'current_price': current_price,
|
||||
'current_time': current_time,
|
||||
'trend': current_trend,
|
||||
'recent_central_banks': len(recent_central_banks),
|
||||
'support_resistance': support_resistance,
|
||||
'market_phase': self._determine_market_phase()
|
||||
}
|
||||
|
||||
def _determine_current_trend(self, recent_segments: List[SegmentElement]) -> str:
|
||||
"""确定当前趋势"""
|
||||
if not recent_segments:
|
||||
return "unclear"
|
||||
|
||||
# 按时间排序取最近3个线段
|
||||
recent_segments = sorted(recent_segments, key=lambda x: x.end_time)[-3:]
|
||||
|
||||
up_count = sum(1 for seg in recent_segments if seg.direction == 1)
|
||||
down_count = sum(1 for seg in recent_segments if seg.direction == -1)
|
||||
|
||||
if up_count > down_count:
|
||||
return "upward"
|
||||
elif down_count > up_count:
|
||||
return "downward"
|
||||
else:
|
||||
return "sideways"
|
||||
|
||||
def _calculate_support_resistance(self, central_banks: List[CentralBankElement]) -> Dict:
|
||||
"""计算支撑阻力位"""
|
||||
if not central_banks:
|
||||
return {}
|
||||
|
||||
# 按强度排序,取最强的几个中枢
|
||||
strong_cbs = sorted(central_banks, key=lambda x: x.strength, reverse=True)[:3]
|
||||
|
||||
supports = []
|
||||
resistances = []
|
||||
|
||||
for cb in strong_cbs:
|
||||
supports.append(cb.low_price)
|
||||
resistances.append(cb.high_price)
|
||||
|
||||
return {
|
||||
'support_levels': sorted(supports),
|
||||
'resistance_levels': sorted(resistances, reverse=True)
|
||||
}
|
||||
|
||||
def _determine_market_phase(self) -> str:
|
||||
"""确定市场阶段"""
|
||||
if not self.central_banks:
|
||||
return "unknown"
|
||||
|
||||
# 最近的中枢
|
||||
latest_cb = self.central_banks[-1] if self.central_banks else None
|
||||
|
||||
if not latest_cb:
|
||||
return "trending"
|
||||
|
||||
current_time = self.processed_data.index[-1]
|
||||
time_since_cb = (current_time - latest_cb.end_time).total_seconds() / 3600 # 小时
|
||||
|
||||
if time_since_cb < 24:
|
||||
return "consolidation" # 盘整
|
||||
else:
|
||||
return "trending" # 趋势
|
||||
|
||||
def export_results(self) -> Dict[str, pd.DataFrame]:
|
||||
"""
|
||||
导出所有分析结果
|
||||
|
||||
Returns:
|
||||
包含各种分析结果的DataFrame字典
|
||||
"""
|
||||
if not self.is_analyzed:
|
||||
raise ValueError("尚未进行分析,请先调用 run_full_analysis()")
|
||||
|
||||
results = {}
|
||||
|
||||
# 处理后的K线数据
|
||||
if self.processed_data is not None:
|
||||
results['klines'] = self.processed_data
|
||||
|
||||
# 分型数据
|
||||
if self.fractal_detector:
|
||||
results['fractals'] = self.fractal_detector.to_dataframe()
|
||||
|
||||
# 笔数据
|
||||
if self.stroke_detector:
|
||||
results['strokes'] = self.stroke_detector.to_dataframe()
|
||||
|
||||
# 线段数据
|
||||
if self.segment_detector:
|
||||
results['segments'] = self.segment_detector.to_dataframe()
|
||||
|
||||
# 中枢数据
|
||||
if self.central_bank_detector:
|
||||
results['central_banks'] = self.central_bank_detector.to_dataframe()
|
||||
|
||||
# 买卖点数据
|
||||
if self.trading_signal_detector:
|
||||
results['trading_signals'] = self.trading_signal_detector.to_dataframe()
|
||||
|
||||
return results
|
||||
|
||||
def get_visualization_data(self) -> Dict:
|
||||
"""
|
||||
获取可视化所需的数据
|
||||
|
||||
Returns:
|
||||
可视化数据字典
|
||||
"""
|
||||
if not self.is_analyzed:
|
||||
return {}
|
||||
|
||||
return {
|
||||
'klines': self.processed_data,
|
||||
'fractals': self.fractals,
|
||||
'strokes': self.strokes,
|
||||
'segments': self.segments,
|
||||
'central_banks': self.central_banks,
|
||||
'trading_points': self.trading_points
|
||||
}
|
||||
+683
@@ -0,0 +1,683 @@
|
||||
"""
|
||||
分型识别模块:识别顶分型和底分型
|
||||
分型定义:至少需要3根K线,中间K线的高点(或低点)比两侧都高(或低)
|
||||
增强版:包含多维度强弱程度评估
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import List, Tuple, Optional, Dict
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FractalPoint:
|
||||
"""分型点数据类"""
|
||||
index: int # 在原始数据中的位置
|
||||
timestamp: pd.Timestamp # 时间戳
|
||||
price: float # 分型价格(高点或低点)
|
||||
fractal_type: str # 'top' 或 'bottom'
|
||||
strength: int # 基础分型强度(左右确认的K线数量)
|
||||
enhanced_strength: float # 增强强度评分(0-100)
|
||||
price_dominance: float # 价格优势度(0-100)
|
||||
volume_strength: float # 成交量强度(0-100)
|
||||
trend_position: float # 趋势位置强度(0-100)
|
||||
confirmed: bool = False # 是否已确认
|
||||
|
||||
|
||||
class Fractal:
|
||||
"""分型识别类(增强版)"""
|
||||
|
||||
def __init__(self, kline_data: pd.DataFrame, min_strength: int = 1):
|
||||
"""
|
||||
初始化分型识别器
|
||||
|
||||
Args:
|
||||
kline_data: 处理包含关系后的K线数据
|
||||
min_strength: 最小分型强度(左右各需多少根K线确认)
|
||||
"""
|
||||
self.data = kline_data.copy()
|
||||
self.min_strength = max(1, min_strength) # 至少为1
|
||||
self.top_fractals = []
|
||||
self.bottom_fractals = []
|
||||
self.all_fractals = []
|
||||
|
||||
# 计算一些辅助指标
|
||||
self._calculate_auxiliary_indicators()
|
||||
|
||||
def _calculate_auxiliary_indicators(self):
|
||||
"""计算辅助技术指标"""
|
||||
# 成交量移动平均
|
||||
self.data['volume_ma'] = self.data['volume'].rolling(window=20, min_periods=1).mean()
|
||||
|
||||
# 价格振幅
|
||||
self.data['range'] = self.data['high'] - self.data['low']
|
||||
self.data['range_ma'] = self.data['range'].rolling(window=20, min_periods=1).mean()
|
||||
|
||||
# 相对位置(高点在整个区间的位置)
|
||||
window = 20
|
||||
self.data['highest'] = self.data['high'].rolling(window=window, min_periods=1).max()
|
||||
self.data['lowest'] = self.data['low'].rolling(window=window, min_periods=1).min()
|
||||
|
||||
# 趋势强度(简单移动平均斜率)
|
||||
self.data['close_ma'] = self.data['close'].rolling(window=10, min_periods=1).mean()
|
||||
self.data['trend_slope'] = self.data['close_ma'].diff(5) # 5期斜率
|
||||
|
||||
def _calculate_price_dominance(self, idx: int, fractal_type: str, strength: int) -> float:
|
||||
"""
|
||||
计算价格优势度:分型价格相对于周围价格的优势程度
|
||||
|
||||
Args:
|
||||
idx: 分型位置
|
||||
fractal_type: 分型类型
|
||||
strength: 基础强度
|
||||
|
||||
Returns:
|
||||
价格优势度(0-100)
|
||||
"""
|
||||
try:
|
||||
# 扩展检查范围
|
||||
check_range = max(strength * 2, 10)
|
||||
start_idx = max(0, idx - check_range)
|
||||
end_idx = min(len(self.data), idx + check_range + 1)
|
||||
|
||||
if fractal_type == 'top':
|
||||
center_price = self.data.iloc[idx]['high']
|
||||
range_data = self.data.iloc[start_idx:end_idx]
|
||||
max_around = range_data['high'].max()
|
||||
second_max = range_data['high'].nlargest(2).iloc[1] if len(range_data) > 1 else center_price
|
||||
|
||||
# 计算相对优势
|
||||
if max_around == center_price:
|
||||
price_gap = center_price - second_max
|
||||
avg_range = self.data.iloc[idx]['range_ma']
|
||||
dominance = min((price_gap / avg_range) * 50, 100) if avg_range > 0 else 50
|
||||
else:
|
||||
dominance = 0
|
||||
else: # bottom
|
||||
center_price = self.data.iloc[idx]['low']
|
||||
range_data = self.data.iloc[start_idx:end_idx]
|
||||
min_around = range_data['low'].min()
|
||||
second_min = range_data['low'].nsmallest(2).iloc[1] if len(range_data) > 1 else center_price
|
||||
|
||||
# 计算相对优势
|
||||
if min_around == center_price:
|
||||
price_gap = second_min - center_price
|
||||
avg_range = self.data.iloc[idx]['range_ma']
|
||||
dominance = min((price_gap / avg_range) * 50, 100) if avg_range > 0 else 50
|
||||
else:
|
||||
dominance = 0
|
||||
|
||||
return max(0, dominance)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"计算价格优势度失败: {e}")
|
||||
return 30 # 默认值
|
||||
|
||||
def _calculate_volume_strength(self, idx: int, fractal_type: str, strength: int) -> float:
|
||||
"""
|
||||
计算成交量强度:分型形成时的成交量特征
|
||||
|
||||
Args:
|
||||
idx: 分型位置
|
||||
fractal_type: 分型类型
|
||||
strength: 基础强度
|
||||
|
||||
Returns:
|
||||
成交量强度(0-100)
|
||||
"""
|
||||
try:
|
||||
center_volume = self.data.iloc[idx]['volume']
|
||||
volume_ma = self.data.iloc[idx]['volume_ma']
|
||||
|
||||
# 基础成交量比率
|
||||
volume_ratio = center_volume / volume_ma if volume_ma > 0 else 1
|
||||
base_score = min(volume_ratio * 30, 60) # 最高60分
|
||||
|
||||
# 检查分型形成过程中的成交量模式
|
||||
pattern_score = 0
|
||||
if strength >= 2:
|
||||
# 检查左右成交量是否递减(表示力度衰竭)
|
||||
left_volumes = [self.data.iloc[idx - i]['volume'] for i in range(1, strength + 1)]
|
||||
right_volumes = [self.data.iloc[idx + i]['volume'] for i in range(1, strength + 1)]
|
||||
|
||||
# 中心成交量应该相对突出
|
||||
center_prominence = sum([1 for v in left_volumes + right_volumes if center_volume > v])
|
||||
total_compared = len(left_volumes + right_volumes)
|
||||
pattern_score = (center_prominence / total_compared) * 40 if total_compared > 0 else 20
|
||||
|
||||
total_score = base_score + pattern_score
|
||||
return max(0, min(total_score, 100))
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"计算成交量强度失败: {e}")
|
||||
return 40 # 默认值
|
||||
|
||||
def _calculate_trend_position_strength(self, idx: int, fractal_type: str) -> float:
|
||||
"""
|
||||
计算趋势位置强度:分型在整体趋势中的位置优势
|
||||
|
||||
Args:
|
||||
idx: 分型位置
|
||||
fractal_type: 分型类型
|
||||
|
||||
Returns:
|
||||
趋势位置强度(0-100)
|
||||
"""
|
||||
try:
|
||||
current_price = self.data.iloc[idx]['high' if fractal_type == 'top' else 'low']
|
||||
highest = self.data.iloc[idx]['highest']
|
||||
lowest = self.data.iloc[idx]['lowest']
|
||||
trend_slope = self.data.iloc[idx]['trend_slope']
|
||||
|
||||
# 计算在价格区间中的相对位置
|
||||
price_range = highest - lowest
|
||||
if price_range > 0:
|
||||
if fractal_type == 'top':
|
||||
# 顶分型:越接近高点越强
|
||||
position_ratio = (current_price - lowest) / price_range
|
||||
else:
|
||||
# 底分型:越接近低点越强
|
||||
position_ratio = (highest - current_price) / price_range
|
||||
else:
|
||||
position_ratio = 0.5
|
||||
|
||||
position_score = position_ratio * 60 # 位置得分最高60分
|
||||
|
||||
# 趋势方向得分
|
||||
trend_score = 0
|
||||
if trend_slope is not None and not np.isnan(trend_slope):
|
||||
if fractal_type == 'top' and trend_slope < 0:
|
||||
# 顶分型形成在下降趋势中更有效
|
||||
trend_score = min(abs(trend_slope) * 1000, 40)
|
||||
elif fractal_type == 'bottom' and trend_slope > 0:
|
||||
# 底分型形成在上升趋势中更有效
|
||||
trend_score = min(abs(trend_slope) * 1000, 40)
|
||||
else:
|
||||
trend_score = 20 # 趋势方向不匹配给予中等分数
|
||||
|
||||
total_score = position_score + trend_score
|
||||
return max(0, min(total_score, 100))
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"计算趋势位置强度失败: {e}")
|
||||
return 50 # 默认值
|
||||
|
||||
def _calculate_enhanced_strength(self, idx: int, fractal_type: str, basic_strength: int) -> Dict[str, float]:
|
||||
"""
|
||||
计算增强强度评分
|
||||
|
||||
Args:
|
||||
idx: 分型位置
|
||||
fractal_type: 分型类型
|
||||
basic_strength: 基础强度
|
||||
|
||||
Returns:
|
||||
包含各维度强度的字典
|
||||
"""
|
||||
# 计算各维度强度
|
||||
price_dominance = self._calculate_price_dominance(idx, fractal_type, basic_strength)
|
||||
volume_strength = self._calculate_volume_strength(idx, fractal_type, basic_strength)
|
||||
trend_position = self._calculate_trend_position_strength(idx, fractal_type)
|
||||
|
||||
# 基础强度转换为评分(强度越高,分数越高)
|
||||
basic_score = min(basic_strength * 20, 60) # 基础强度最高60分
|
||||
|
||||
# 计算综合强度(加权平均)
|
||||
enhanced_strength = (
|
||||
basic_score * 0.3 + # 基础强度 30%
|
||||
price_dominance * 0.4 + # 价格优势 40%
|
||||
volume_strength * 0.2 + # 成交量强度 20%
|
||||
trend_position * 0.1 # 趋势位置 10%
|
||||
)
|
||||
|
||||
return {
|
||||
'enhanced_strength': enhanced_strength,
|
||||
'price_dominance': price_dominance,
|
||||
'volume_strength': volume_strength,
|
||||
'trend_position': trend_position
|
||||
}
|
||||
|
||||
def is_top_fractal(self, idx: int, strength: int = None) -> Tuple[bool, int]:
|
||||
"""
|
||||
判断指定位置是否为顶分型
|
||||
|
||||
Args:
|
||||
idx: 检查的位置索引
|
||||
strength: 检查强度,如果为None则使用类默认值
|
||||
|
||||
Returns:
|
||||
(是否为顶分型, 实际强度)
|
||||
"""
|
||||
if strength is None:
|
||||
strength = self.min_strength
|
||||
|
||||
data_len = len(self.data)
|
||||
|
||||
# 检查边界
|
||||
if idx < strength or idx >= data_len - strength:
|
||||
return False, 0
|
||||
|
||||
center_high = self.data.iloc[idx]['high']
|
||||
|
||||
# 检查左侧K线
|
||||
left_valid = True
|
||||
for i in range(1, strength + 1):
|
||||
if self.data.iloc[idx - i]['high'] >= center_high:
|
||||
left_valid = False
|
||||
break
|
||||
|
||||
# 检查右侧K线
|
||||
right_valid = True
|
||||
for i in range(1, strength + 1):
|
||||
if self.data.iloc[idx + i]['high'] >= center_high:
|
||||
right_valid = False
|
||||
break
|
||||
|
||||
is_fractal = left_valid and right_valid
|
||||
actual_strength = strength if is_fractal else 0
|
||||
|
||||
return is_fractal, actual_strength
|
||||
|
||||
def is_bottom_fractal(self, idx: int, strength: int = None) -> Tuple[bool, int]:
|
||||
"""
|
||||
判断指定位置是否为底分型
|
||||
|
||||
Args:
|
||||
idx: 检查的位置索引
|
||||
strength: 检查强度,如果为None则使用类默认值
|
||||
|
||||
Returns:
|
||||
(是否为底分型, 实际强度)
|
||||
"""
|
||||
if strength is None:
|
||||
strength = self.min_strength
|
||||
|
||||
data_len = len(self.data)
|
||||
|
||||
# 检查边界
|
||||
if idx < strength or idx >= data_len - strength:
|
||||
return False, 0
|
||||
|
||||
center_low = self.data.iloc[idx]['low']
|
||||
|
||||
# 检查左侧K线
|
||||
left_valid = True
|
||||
for i in range(1, strength + 1):
|
||||
if self.data.iloc[idx - i]['low'] <= center_low:
|
||||
left_valid = False
|
||||
break
|
||||
|
||||
# 检查右侧K线
|
||||
right_valid = True
|
||||
for i in range(1, strength + 1):
|
||||
if self.data.iloc[idx + i]['low'] <= center_low:
|
||||
right_valid = False
|
||||
break
|
||||
|
||||
is_fractal = left_valid and right_valid
|
||||
actual_strength = strength if is_fractal else 0
|
||||
|
||||
return is_fractal, actual_strength
|
||||
|
||||
def find_max_strength_fractal(self, idx: int, fractal_type: str, max_strength: int = 5) -> Tuple[bool, int]:
|
||||
"""
|
||||
寻找指定位置的最大强度分型
|
||||
|
||||
Args:
|
||||
idx: 检查位置
|
||||
fractal_type: 'top' 或 'bottom'
|
||||
max_strength: 最大检查强度
|
||||
|
||||
Returns:
|
||||
(是否为分型, 最大强度)
|
||||
"""
|
||||
max_valid_strength = 0
|
||||
|
||||
for strength in range(self.min_strength, max_strength + 1):
|
||||
if fractal_type == 'top':
|
||||
is_valid, _ = self.is_top_fractal(idx, strength)
|
||||
else:
|
||||
is_valid, _ = self.is_bottom_fractal(idx, strength)
|
||||
|
||||
if is_valid:
|
||||
max_valid_strength = strength
|
||||
else:
|
||||
break # 一旦失败就停止,因为更高强度也不会成功
|
||||
|
||||
return max_valid_strength > 0, max_valid_strength
|
||||
|
||||
def detect_fractals(self, use_max_strength: bool = True) -> List[FractalPoint]:
|
||||
"""
|
||||
检测所有分型
|
||||
|
||||
Args:
|
||||
use_max_strength: 是否使用最大强度检测
|
||||
|
||||
Returns:
|
||||
所有分型点列表
|
||||
"""
|
||||
fractals = []
|
||||
data_len = len(self.data)
|
||||
|
||||
logger.info(f"开始检测分型,数据长度: {data_len}")
|
||||
|
||||
# 遍历所有可能的分型位置
|
||||
for idx in range(self.min_strength, data_len - self.min_strength):
|
||||
timestamp = self.data.index[idx]
|
||||
|
||||
# 检测顶分型
|
||||
if use_max_strength:
|
||||
is_top, top_strength = self.find_max_strength_fractal(idx, 'top')
|
||||
else:
|
||||
is_top, top_strength = self.is_top_fractal(idx)
|
||||
|
||||
if is_top:
|
||||
# 计算增强强度
|
||||
strength_metrics = self._calculate_enhanced_strength(idx, 'top', top_strength)
|
||||
|
||||
fractal = FractalPoint(
|
||||
index=idx,
|
||||
timestamp=timestamp,
|
||||
price=self.data.iloc[idx]['high'],
|
||||
fractal_type='top',
|
||||
strength=top_strength,
|
||||
enhanced_strength=strength_metrics['enhanced_strength'],
|
||||
price_dominance=strength_metrics['price_dominance'],
|
||||
volume_strength=strength_metrics['volume_strength'],
|
||||
trend_position=strength_metrics['trend_position'],
|
||||
confirmed=True # 简化处理,认为都已确认
|
||||
)
|
||||
fractals.append(fractal)
|
||||
self.top_fractals.append(fractal)
|
||||
|
||||
# 检测底分型
|
||||
if use_max_strength:
|
||||
is_bottom, bottom_strength = self.find_max_strength_fractal(idx, 'bottom')
|
||||
else:
|
||||
is_bottom, bottom_strength = self.is_bottom_fractal(idx)
|
||||
|
||||
if is_bottom:
|
||||
# 计算增强强度
|
||||
strength_metrics = self._calculate_enhanced_strength(idx, 'bottom', bottom_strength)
|
||||
|
||||
fractal = FractalPoint(
|
||||
index=idx,
|
||||
timestamp=timestamp,
|
||||
price=self.data.iloc[idx]['low'],
|
||||
fractal_type='bottom',
|
||||
strength=bottom_strength,
|
||||
enhanced_strength=strength_metrics['enhanced_strength'],
|
||||
price_dominance=strength_metrics['price_dominance'],
|
||||
volume_strength=strength_metrics['volume_strength'],
|
||||
trend_position=strength_metrics['trend_position'],
|
||||
confirmed=True
|
||||
)
|
||||
fractals.append(fractal)
|
||||
self.bottom_fractals.append(fractal)
|
||||
|
||||
# 按时间排序
|
||||
fractals.sort(key=lambda x: x.index)
|
||||
self.all_fractals = fractals
|
||||
|
||||
logger.info(f"检测完成:顶分型 {len(self.top_fractals)} 个,底分型 {len(self.bottom_fractals)} 个")
|
||||
return fractals
|
||||
|
||||
def filter_fractals_by_strength(self, min_strength: int) -> List[FractalPoint]:
|
||||
"""
|
||||
按强度过滤分型
|
||||
|
||||
Args:
|
||||
min_strength: 最小强度要求
|
||||
|
||||
Returns:
|
||||
过滤后的分型列表
|
||||
"""
|
||||
return [f for f in self.all_fractals if f.strength >= min_strength]
|
||||
|
||||
def get_fractal_sequence(self) -> List[FractalPoint]:
|
||||
"""
|
||||
获取交替的分型序列(顶-底-顶-底...)
|
||||
|
||||
Returns:
|
||||
交替分型序列
|
||||
"""
|
||||
if not self.all_fractals:
|
||||
return []
|
||||
|
||||
sequence = []
|
||||
last_type = None
|
||||
|
||||
for fractal in self.all_fractals:
|
||||
if fractal.fractal_type != last_type:
|
||||
sequence.append(fractal)
|
||||
last_type = fractal.fractal_type
|
||||
|
||||
return sequence
|
||||
|
||||
def validate_fractal_sequence(self, sequence: List[FractalPoint]) -> bool:
|
||||
"""
|
||||
验证分型序列的有效性
|
||||
|
||||
Args:
|
||||
sequence: 分型序列
|
||||
|
||||
Returns:
|
||||
是否有效
|
||||
"""
|
||||
if len(sequence) < 2:
|
||||
return True
|
||||
|
||||
for i in range(1, len(sequence)):
|
||||
prev_fractal = sequence[i-1]
|
||||
curr_fractal = sequence[i]
|
||||
|
||||
# 检查类型是否交替
|
||||
if prev_fractal.fractal_type == curr_fractal.fractal_type:
|
||||
return False
|
||||
|
||||
# 检查价格关系是否合理
|
||||
if prev_fractal.fractal_type == 'top':
|
||||
# 顶分型后应该是底分型,且价格应该更低
|
||||
if curr_fractal.price >= prev_fractal.price:
|
||||
return False
|
||||
else:
|
||||
# 底分型后应该是顶分型,且价格应该更高
|
||||
if curr_fractal.price <= prev_fractal.price:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_fractal_statistics(self) -> Dict:
|
||||
"""
|
||||
获取分型统计信息(增强版)
|
||||
|
||||
Returns:
|
||||
统计信息字典
|
||||
"""
|
||||
if not self.all_fractals:
|
||||
return {}
|
||||
|
||||
top_count = len(self.top_fractals)
|
||||
bottom_count = len(self.bottom_fractals)
|
||||
|
||||
# 基础统计
|
||||
top_strengths = [f.strength for f in self.top_fractals]
|
||||
bottom_strengths = [f.strength for f in self.bottom_fractals]
|
||||
|
||||
# 增强强度统计
|
||||
enhanced_strengths = [f.enhanced_strength for f in self.all_fractals]
|
||||
price_dominances = [f.price_dominance for f in self.all_fractals]
|
||||
volume_strengths = [f.volume_strength for f in self.all_fractals]
|
||||
trend_positions = [f.trend_position for f in self.all_fractals]
|
||||
|
||||
# 分级统计(按增强强度)
|
||||
strong_fractals = [f for f in self.all_fractals if f.enhanced_strength >= 70]
|
||||
medium_fractals = [f for f in self.all_fractals if 40 <= f.enhanced_strength < 70]
|
||||
weak_fractals = [f for f in self.all_fractals if f.enhanced_strength < 40]
|
||||
|
||||
stats = {
|
||||
'basic_info': {
|
||||
'total_fractals': len(self.all_fractals),
|
||||
'top_fractals': top_count,
|
||||
'bottom_fractals': bottom_count,
|
||||
'avg_basic_strength': np.mean([f.strength for f in self.all_fractals]),
|
||||
'max_basic_strength': max([f.strength for f in self.all_fractals]),
|
||||
},
|
||||
'enhanced_strength': {
|
||||
'avg_enhanced_strength': np.mean(enhanced_strengths),
|
||||
'max_enhanced_strength': max(enhanced_strengths),
|
||||
'min_enhanced_strength': min(enhanced_strengths),
|
||||
'strong_count': len(strong_fractals), # 强势分型数量
|
||||
'medium_count': len(medium_fractals), # 中等分型数量
|
||||
'weak_count': len(weak_fractals), # 弱势分型数量
|
||||
},
|
||||
'dimension_analysis': {
|
||||
'avg_price_dominance': np.mean(price_dominances),
|
||||
'avg_volume_strength': np.mean(volume_strengths),
|
||||
'avg_trend_position': np.mean(trend_positions),
|
||||
},
|
||||
'top_fractals_detail': {
|
||||
'count': top_count,
|
||||
'avg_basic_strength': np.mean(top_strengths) if top_strengths else 0,
|
||||
'avg_enhanced_strength': np.mean([f.enhanced_strength for f in self.top_fractals]) if self.top_fractals else 0,
|
||||
'strong_tops': len([f for f in self.top_fractals if f.enhanced_strength >= 70]),
|
||||
},
|
||||
'bottom_fractals_detail': {
|
||||
'count': bottom_count,
|
||||
'avg_basic_strength': np.mean(bottom_strengths) if bottom_strengths else 0,
|
||||
'avg_enhanced_strength': np.mean([f.enhanced_strength for f in self.bottom_fractals]) if self.bottom_fractals else 0,
|
||||
'strong_bottoms': len([f for f in self.bottom_fractals if f.enhanced_strength >= 70]),
|
||||
}
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
def to_dataframe(self) -> pd.DataFrame:
|
||||
"""
|
||||
将分型转换为DataFrame
|
||||
|
||||
Returns:
|
||||
包含分型信息的DataFrame
|
||||
"""
|
||||
if not self.all_fractals:
|
||||
return pd.DataFrame()
|
||||
|
||||
data = []
|
||||
for fractal in self.all_fractals:
|
||||
data.append({
|
||||
'timestamp': fractal.timestamp,
|
||||
'index': fractal.index,
|
||||
'price': fractal.price,
|
||||
'type': fractal.fractal_type,
|
||||
'strength': fractal.strength,
|
||||
'enhanced_strength': fractal.enhanced_strength,
|
||||
'price_dominance': fractal.price_dominance,
|
||||
'volume_strength': fractal.volume_strength,
|
||||
'trend_position': fractal.trend_position,
|
||||
'confirmed': fractal.confirmed
|
||||
})
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
df.set_index('timestamp', inplace=True)
|
||||
return df
|
||||
|
||||
def update_fractal_confirmation(self, current_idx: int):
|
||||
"""
|
||||
更新分型确认状态
|
||||
|
||||
Args:
|
||||
current_idx: 当前处理到的K线位置
|
||||
"""
|
||||
for fractal in self.all_fractals:
|
||||
if not fractal.confirmed:
|
||||
# 检查是否已经过了足够的确认期
|
||||
required_confirmation = fractal.strength
|
||||
time_passed = current_idx - fractal.index
|
||||
|
||||
if time_passed >= required_confirmation:
|
||||
fractal.confirmed = True
|
||||
|
||||
def get_confirmed_fractals(self, current_idx: int) -> List[FractalPoint]:
|
||||
"""
|
||||
获取已确认的分型列表
|
||||
|
||||
Args:
|
||||
current_idx: 当前处理到的K线位置
|
||||
|
||||
Returns:
|
||||
已确认的分型列表
|
||||
"""
|
||||
confirmed_fractals = []
|
||||
for fractal in self.all_fractals:
|
||||
required_confirmation = fractal.strength
|
||||
time_passed = current_idx - fractal.index
|
||||
|
||||
if time_passed >= required_confirmation:
|
||||
confirmed_fractals.append(fractal)
|
||||
|
||||
return confirmed_fractals
|
||||
|
||||
def detect_real_time_fractals(self, current_idx: int, lookback_periods: int = 50) -> List[FractalPoint]:
|
||||
"""
|
||||
实时分型检测(避免使用未来数据)
|
||||
|
||||
Args:
|
||||
current_idx: 当前K线位置
|
||||
lookback_periods: 回看周期数
|
||||
|
||||
Returns:
|
||||
实时可用的分型列表
|
||||
"""
|
||||
real_time_fractals = []
|
||||
data_len = len(self.data)
|
||||
|
||||
# 只检测到当前位置之前的分型
|
||||
end_idx = min(current_idx, data_len - self.min_strength)
|
||||
start_idx = max(self.min_strength, end_idx - lookback_periods)
|
||||
|
||||
for idx in range(start_idx, end_idx):
|
||||
timestamp = self.data.index[idx]
|
||||
|
||||
# 检测顶分型(但只能检测已确认的)
|
||||
is_top, top_strength = self.find_max_strength_fractal(idx, 'top')
|
||||
if is_top and current_idx - idx >= top_strength: # 已确认
|
||||
strength_metrics = self._calculate_enhanced_strength(idx, 'top', top_strength)
|
||||
|
||||
fractal = FractalPoint(
|
||||
index=idx,
|
||||
timestamp=timestamp,
|
||||
price=self.data.iloc[idx]['high'],
|
||||
fractal_type='top',
|
||||
strength=top_strength,
|
||||
enhanced_strength=strength_metrics['enhanced_strength'],
|
||||
price_dominance=strength_metrics['price_dominance'],
|
||||
volume_strength=strength_metrics['volume_strength'],
|
||||
trend_position=strength_metrics['trend_position'],
|
||||
confirmed=True
|
||||
)
|
||||
real_time_fractals.append(fractal)
|
||||
|
||||
# 检测底分型(但只能检测已确认的)
|
||||
is_bottom, bottom_strength = self.find_max_strength_fractal(idx, 'bottom')
|
||||
if is_bottom and current_idx - idx >= bottom_strength: # 已确认
|
||||
strength_metrics = self._calculate_enhanced_strength(idx, 'bottom', bottom_strength)
|
||||
|
||||
fractal = FractalPoint(
|
||||
index=idx,
|
||||
timestamp=timestamp,
|
||||
price=self.data.iloc[idx]['low'],
|
||||
fractal_type='bottom',
|
||||
strength=bottom_strength,
|
||||
enhanced_strength=strength_metrics['enhanced_strength'],
|
||||
price_dominance=strength_metrics['price_dominance'],
|
||||
volume_strength=strength_metrics['volume_strength'],
|
||||
trend_position=strength_metrics['trend_position'],
|
||||
confirmed=True
|
||||
)
|
||||
real_time_fractals.append(fractal)
|
||||
|
||||
return real_time_fractals
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
"""
|
||||
K线包含关系处理模块
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import List, Tuple, Optional
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KLineElement:
|
||||
"""单个K线元素类"""
|
||||
|
||||
def __init__(self, high: float, low: float, open_price: float,
|
||||
close: float, volume: float, timestamp: pd.Timestamp):
|
||||
"""
|
||||
初始化K线元素
|
||||
|
||||
Args:
|
||||
high: 最高价
|
||||
low: 最低价
|
||||
open_price: 开盘价
|
||||
close: 收盘价
|
||||
volume: 成交量
|
||||
timestamp: 时间戳
|
||||
"""
|
||||
self.high = high
|
||||
self.low = low
|
||||
self.open = open_price
|
||||
self.close = close
|
||||
self.volume = volume
|
||||
self.timestamp = timestamp
|
||||
self.direction = 1 if close >= open_price else -1
|
||||
|
||||
def __repr__(self):
|
||||
return f"KLineElement(H:{self.high}, L:{self.low}, O:{self.open}, C:{self.close})"
|
||||
|
||||
|
||||
class KLine:
|
||||
"""K线包含关系处理类"""
|
||||
|
||||
def __init__(self, data: pd.DataFrame):
|
||||
"""
|
||||
初始化K线处理器
|
||||
|
||||
Args:
|
||||
data: 包含OHLCV数据的DataFrame
|
||||
"""
|
||||
self.original_data = data.copy()
|
||||
self.processed_data = None
|
||||
self.kline_elements = []
|
||||
self._process_data()
|
||||
|
||||
def _process_data(self):
|
||||
"""处理原始数据为K线元素"""
|
||||
self.kline_elements = []
|
||||
|
||||
for idx, row in self.original_data.iterrows():
|
||||
element = KLineElement(
|
||||
high=row['high'],
|
||||
low=row['low'],
|
||||
open_price=row['open'],
|
||||
close=row['close'],
|
||||
volume=row['volume'],
|
||||
timestamp=idx
|
||||
)
|
||||
self.kline_elements.append(element)
|
||||
|
||||
def has_containment(self, k1: KLineElement, k2: KLineElement) -> bool:
|
||||
"""
|
||||
判断两根K线是否存在包含关系
|
||||
|
||||
Args:
|
||||
k1: 第一根K线
|
||||
k2: 第二根K线
|
||||
|
||||
Returns:
|
||||
是否存在包含关系
|
||||
"""
|
||||
# K1包含K2:K1的高低点完全包含K2
|
||||
k1_contains_k2 = (k1.high >= k2.high and k1.low <= k2.low)
|
||||
|
||||
# K2包含K1:K2的高低点完全包含K1
|
||||
k2_contains_k1 = (k2.high >= k1.high and k2.low <= k1.low)
|
||||
|
||||
return k1_contains_k2 or k2_contains_k1
|
||||
|
||||
def merge_contained_klines(self, k1: KLineElement, k2: KLineElement,
|
||||
direction: int) -> KLineElement:
|
||||
"""
|
||||
合并包含关系的K线
|
||||
|
||||
Args:
|
||||
k1: 第一根K线
|
||||
k2: 第二根K线
|
||||
direction: 当前趋势方向(1为上升,-1为下降)
|
||||
|
||||
Returns:
|
||||
合并后的K线
|
||||
"""
|
||||
if direction > 0: # 上升趋势中
|
||||
# 取两根K线的最高点的最大值,最低点的最大值
|
||||
merged_high = max(k1.high, k2.high)
|
||||
merged_low = max(k1.low, k2.low)
|
||||
else: # 下降趋势中
|
||||
# 取两根K线的最高点的最小值,最低点的最小值
|
||||
merged_high = min(k1.high, k2.high)
|
||||
merged_low = min(k1.low, k2.low)
|
||||
|
||||
# 开盘价和收盘价使用第一根K线的值
|
||||
merged_open = k1.open
|
||||
merged_close = k2.close
|
||||
merged_volume = k1.volume + k2.volume
|
||||
merged_timestamp = k2.timestamp # 使用最后一根K线的时间
|
||||
|
||||
return KLineElement(
|
||||
high=merged_high,
|
||||
low=merged_low,
|
||||
open_price=merged_open,
|
||||
close=merged_close,
|
||||
volume=merged_volume,
|
||||
timestamp=merged_timestamp
|
||||
)
|
||||
|
||||
def handle_containment(self) -> List[KLineElement]:
|
||||
"""
|
||||
处理所有K线的包含关系
|
||||
|
||||
Returns:
|
||||
处理包含关系后的K线列表
|
||||
"""
|
||||
if len(self.kline_elements) < 2:
|
||||
return self.kline_elements.copy()
|
||||
|
||||
processed_klines = [self.kline_elements[0]] # 第一根K线
|
||||
|
||||
# 初始方向:根据前两根K线确定
|
||||
if len(self.kline_elements) >= 2:
|
||||
k1, k2 = self.kline_elements[0], self.kline_elements[1]
|
||||
if k2.high > k1.high:
|
||||
current_direction = 1 # 上升
|
||||
elif k2.high < k1.high:
|
||||
current_direction = -1 # 下降
|
||||
else:
|
||||
current_direction = 1 if k2.low >= k1.low else -1
|
||||
else:
|
||||
current_direction = 1
|
||||
|
||||
i = 1
|
||||
while i < len(self.kline_elements):
|
||||
current_k = self.kline_elements[i]
|
||||
last_processed = processed_klines[-1]
|
||||
|
||||
if self.has_containment(last_processed, current_k):
|
||||
# 存在包含关系,进行合并
|
||||
merged = self.merge_contained_klines(
|
||||
last_processed, current_k, current_direction
|
||||
)
|
||||
processed_klines[-1] = merged # 替换最后一个
|
||||
else:
|
||||
# 不存在包含关系,直接添加
|
||||
processed_klines.append(current_k)
|
||||
|
||||
# 更新方向
|
||||
if current_k.high > last_processed.high:
|
||||
current_direction = 1
|
||||
elif current_k.high < last_processed.high:
|
||||
current_direction = -1
|
||||
# 如果high相等,保持原方向
|
||||
|
||||
i += 1
|
||||
|
||||
logger.info(f"包含关系处理完成:{len(self.kline_elements)} -> {len(processed_klines)}")
|
||||
return processed_klines
|
||||
|
||||
def to_dataframe(self, processed_klines: Optional[List[KLineElement]] = None) -> pd.DataFrame:
|
||||
"""
|
||||
将处理后的K线转换为DataFrame
|
||||
|
||||
Args:
|
||||
processed_klines: 处理后的K线列表,如果为None则使用默认处理结果
|
||||
|
||||
Returns:
|
||||
包含处理后K线的DataFrame
|
||||
"""
|
||||
if processed_klines is None:
|
||||
processed_klines = self.handle_containment()
|
||||
|
||||
data = []
|
||||
for kline in processed_klines:
|
||||
data.append({
|
||||
'timestamp': kline.timestamp,
|
||||
'open': kline.open,
|
||||
'high': kline.high,
|
||||
'low': kline.low,
|
||||
'close': kline.close,
|
||||
'volume': kline.volume,
|
||||
'direction': kline.direction
|
||||
})
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
if not df.empty:
|
||||
df.set_index('timestamp', inplace=True)
|
||||
|
||||
return df
|
||||
|
||||
def get_processed_data(self) -> pd.DataFrame:
|
||||
"""
|
||||
获取处理包含关系后的数据
|
||||
|
||||
Returns:
|
||||
处理后的DataFrame
|
||||
"""
|
||||
if self.processed_data is None:
|
||||
processed_klines = self.handle_containment()
|
||||
self.processed_data = self.to_dataframe(processed_klines)
|
||||
|
||||
return self.processed_data
|
||||
|
||||
def visualize_containment(self) -> dict:
|
||||
"""
|
||||
生成包含关系可视化信息
|
||||
|
||||
Returns:
|
||||
包含可视化信息的字典
|
||||
"""
|
||||
original_count = len(self.kline_elements)
|
||||
processed_klines = self.handle_containment()
|
||||
processed_count = len(processed_klines)
|
||||
|
||||
return {
|
||||
'original_count': original_count,
|
||||
'processed_count': processed_count,
|
||||
'merged_count': original_count - processed_count,
|
||||
'merge_ratio': (original_count - processed_count) / original_count if original_count > 0 else 0
|
||||
}
|
||||
+356
@@ -0,0 +1,356 @@
|
||||
"""
|
||||
线段模块:线段由笔组成,有特殊的生成和确认规则
|
||||
线段定义:至少包含3笔,且满足特定的破坏条件
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import List, Tuple, Optional, Dict
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from .stroke import StrokeElement
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SegmentElement:
|
||||
"""线段元素数据类"""
|
||||
strokes: List[StrokeElement] # 组成线段的笔列表
|
||||
direction: int # 线段方向:1为上升,-1为下降
|
||||
start_price: float # 起始价格
|
||||
end_price: float # 结束价格
|
||||
start_time: pd.Timestamp # 开始时间
|
||||
end_time: pd.Timestamp # 结束时间
|
||||
length: float # 线段长度
|
||||
strength: float # 线段强度
|
||||
confirmed: bool = False # 是否已确认
|
||||
|
||||
|
||||
class Segment:
|
||||
"""线段识别和处理类"""
|
||||
|
||||
def __init__(self, strokes: List[StrokeElement]):
|
||||
"""
|
||||
初始化线段处理器
|
||||
|
||||
Args:
|
||||
strokes: 笔列表
|
||||
"""
|
||||
self.strokes = sorted(strokes, key=lambda x: x.start_fractal.index)
|
||||
self.segments = []
|
||||
self.up_segments = [] # 上升线段
|
||||
self.down_segments = [] # 下降线段
|
||||
|
||||
def can_form_segment(self, stroke_group: List[StrokeElement]) -> bool:
|
||||
"""
|
||||
判断笔组是否可以形成线段
|
||||
|
||||
Args:
|
||||
stroke_group: 笔组
|
||||
|
||||
Returns:
|
||||
是否可以形成线段
|
||||
"""
|
||||
if len(stroke_group) < 3:
|
||||
return False
|
||||
|
||||
# 检查是否有主导方向
|
||||
up_count = sum(1 for s in stroke_group if s.direction == 1)
|
||||
down_count = sum(1 for s in stroke_group if s.direction == -1)
|
||||
|
||||
# 主导方向的笔至少比反向笔多1个
|
||||
return abs(up_count - down_count) >= 1
|
||||
|
||||
def determine_segment_direction(self, stroke_group: List[StrokeElement]) -> int:
|
||||
"""
|
||||
确定线段方向
|
||||
|
||||
Args:
|
||||
stroke_group: 笔组
|
||||
|
||||
Returns:
|
||||
线段方向
|
||||
"""
|
||||
if not stroke_group:
|
||||
return 0
|
||||
|
||||
# 比较起点和终点价格
|
||||
start_price = stroke_group[0].start_fractal.price
|
||||
end_price = stroke_group[-1].end_fractal.price
|
||||
|
||||
if end_price > start_price:
|
||||
return 1 # 上升线段
|
||||
elif end_price < start_price:
|
||||
return -1 # 下降线段
|
||||
else:
|
||||
# 价格相等时,根据笔的数量决定
|
||||
up_count = sum(1 for s in stroke_group if s.direction == 1)
|
||||
down_count = sum(1 for s in stroke_group if s.direction == -1)
|
||||
return 1 if up_count >= down_count else -1
|
||||
|
||||
def create_segment(self, stroke_group: List[StrokeElement]) -> SegmentElement:
|
||||
"""
|
||||
创建线段元素
|
||||
|
||||
Args:
|
||||
stroke_group: 组成线段的笔组
|
||||
|
||||
Returns:
|
||||
线段元素
|
||||
"""
|
||||
if not stroke_group:
|
||||
raise ValueError("笔组不能为空")
|
||||
|
||||
direction = self.determine_segment_direction(stroke_group)
|
||||
|
||||
start_price = stroke_group[0].start_fractal.price
|
||||
end_price = stroke_group[-1].end_fractal.price
|
||||
start_time = stroke_group[0].start_fractal.timestamp
|
||||
end_time = stroke_group[-1].end_fractal.timestamp
|
||||
|
||||
length = abs(end_price - start_price)
|
||||
strength = self._calculate_segment_strength(stroke_group, length)
|
||||
|
||||
return SegmentElement(
|
||||
strokes=stroke_group,
|
||||
direction=direction,
|
||||
start_price=start_price,
|
||||
end_price=end_price,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
length=length,
|
||||
strength=strength
|
||||
)
|
||||
|
||||
def _calculate_segment_strength(self, stroke_group: List[StrokeElement],
|
||||
length: float) -> float:
|
||||
"""
|
||||
计算线段强度
|
||||
|
||||
Args:
|
||||
stroke_group: 笔组
|
||||
length: 线段长度
|
||||
|
||||
Returns:
|
||||
线段强度
|
||||
"""
|
||||
# 基础强度:价格变化幅度
|
||||
base_strength = length
|
||||
|
||||
# 笔的数量因子
|
||||
stroke_count_factor = len(stroke_group) / 3 # 3笔为基准
|
||||
|
||||
# 笔的平均强度
|
||||
avg_stroke_strength = np.mean([s.strength for s in stroke_group])
|
||||
|
||||
# 方向一致性:主导方向笔的比例
|
||||
main_direction = self.determine_segment_direction(stroke_group)
|
||||
main_direction_count = sum(1 for s in stroke_group if s.direction == main_direction)
|
||||
direction_consistency = main_direction_count / len(stroke_group)
|
||||
|
||||
# 综合强度
|
||||
strength = (base_strength *
|
||||
(1 + stroke_count_factor * 0.1) *
|
||||
(1 + avg_stroke_strength * 0.01) *
|
||||
direction_consistency)
|
||||
|
||||
return strength
|
||||
|
||||
def detect_segment_break(self, segment: SegmentElement,
|
||||
new_stroke: StrokeElement) -> bool:
|
||||
"""
|
||||
检测线段是否被破坏
|
||||
|
||||
Args:
|
||||
segment: 当前线段
|
||||
new_stroke: 新的笔
|
||||
|
||||
Returns:
|
||||
是否被破坏
|
||||
"""
|
||||
if segment.direction == 1: # 上升线段
|
||||
# 如果新笔是下降笔且跌破线段起点
|
||||
if (new_stroke.direction == -1 and
|
||||
new_stroke.end_fractal.price < segment.start_price):
|
||||
return True
|
||||
else: # 下降线段
|
||||
# 如果新笔是上升笔且涨破线段起点
|
||||
if (new_stroke.direction == 1 and
|
||||
new_stroke.end_fractal.price > segment.start_price):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def detect_segments(self) -> List[SegmentElement]:
|
||||
"""
|
||||
检测所有线段
|
||||
|
||||
Returns:
|
||||
线段列表
|
||||
"""
|
||||
if len(self.strokes) < 3:
|
||||
logger.warning("笔数量不足,无法生成线段")
|
||||
return []
|
||||
|
||||
segments = []
|
||||
current_stroke_group = []
|
||||
|
||||
for stroke in self.strokes:
|
||||
current_stroke_group.append(stroke)
|
||||
|
||||
# 当有足够笔时,尝试形成线段
|
||||
if len(current_stroke_group) >= 3:
|
||||
if self.can_form_segment(current_stroke_group):
|
||||
# 检查是否有现有线段被破坏
|
||||
if segments:
|
||||
last_segment = segments[-1]
|
||||
if self.detect_segment_break(last_segment, stroke):
|
||||
# 线段被破坏,确认上一个线段
|
||||
last_segment.confirmed = True
|
||||
|
||||
# 开始新的线段
|
||||
current_stroke_group = [stroke]
|
||||
continue
|
||||
|
||||
# 尝试扩展或创建新线段
|
||||
if len(current_stroke_group) >= 5: # 限制线段长度
|
||||
segment = self.create_segment(current_stroke_group[:-2])
|
||||
segments.append(segment)
|
||||
current_stroke_group = current_stroke_group[-2:] # 保留最后两笔
|
||||
|
||||
# 处理最后一组笔
|
||||
if len(current_stroke_group) >= 3 and self.can_form_segment(current_stroke_group):
|
||||
segment = self.create_segment(current_stroke_group)
|
||||
segments.append(segment)
|
||||
|
||||
# 分类存储
|
||||
for segment in segments:
|
||||
if segment.direction == 1:
|
||||
self.up_segments.append(segment)
|
||||
else:
|
||||
self.down_segments.append(segment)
|
||||
|
||||
self.segments = segments
|
||||
logger.info(f"检测到 {len(segments)} 个线段:上升线段 {len(self.up_segments)} 个,下降线段 {len(self.down_segments)} 个")
|
||||
|
||||
return segments
|
||||
|
||||
def get_segment_sequence(self) -> List[SegmentElement]:
|
||||
"""
|
||||
获取连续的线段序列
|
||||
|
||||
Returns:
|
||||
连续线段序列
|
||||
"""
|
||||
return sorted(self.segments, key=lambda x: x.start_time)
|
||||
|
||||
def find_segment_overlaps(self) -> List[Tuple[SegmentElement, SegmentElement]]:
|
||||
"""
|
||||
寻找线段重叠区域(可能的中枢)
|
||||
|
||||
Returns:
|
||||
重叠线段对列表
|
||||
"""
|
||||
overlaps = []
|
||||
sequence = self.get_segment_sequence()
|
||||
|
||||
for i in range(len(sequence) - 1):
|
||||
for j in range(i + 1, len(sequence)):
|
||||
seg1, seg2 = sequence[i], sequence[j]
|
||||
|
||||
# 检查价格区间是否重叠
|
||||
if (min(seg1.start_price, seg1.end_price) <= max(seg2.start_price, seg2.end_price) and
|
||||
max(seg1.start_price, seg1.end_price) >= min(seg2.start_price, seg2.end_price)):
|
||||
overlaps.append((seg1, seg2))
|
||||
|
||||
return overlaps
|
||||
|
||||
def analyze_segment_patterns(self) -> Dict:
|
||||
"""
|
||||
分析线段模式
|
||||
|
||||
Returns:
|
||||
模式分析结果
|
||||
"""
|
||||
if not self.segments:
|
||||
return {}
|
||||
|
||||
sequence = self.get_segment_sequence()
|
||||
|
||||
# 分析趋势
|
||||
trend_changes = 0
|
||||
for i in range(1, len(sequence)):
|
||||
if sequence[i].direction != sequence[i-1].direction:
|
||||
trend_changes += 1
|
||||
|
||||
# 统计信息
|
||||
avg_length = np.mean([s.length for s in self.segments])
|
||||
avg_stroke_count = np.mean([len(s.strokes) for s in self.segments])
|
||||
max_length = max([s.length for s in self.segments]) if self.segments else 0
|
||||
|
||||
# 寻找重叠区域
|
||||
overlaps = self.find_segment_overlaps()
|
||||
|
||||
return {
|
||||
'total_segments': len(self.segments),
|
||||
'up_segments': len(self.up_segments),
|
||||
'down_segments': len(self.down_segments),
|
||||
'trend_changes': trend_changes,
|
||||
'avg_length': avg_length,
|
||||
'max_length': max_length,
|
||||
'avg_stroke_count': avg_stroke_count,
|
||||
'overlaps': len(overlaps),
|
||||
'confirmed_segments': sum(1 for s in self.segments if s.confirmed)
|
||||
}
|
||||
|
||||
def filter_segments_by_strength(self, min_strength: float) -> List[SegmentElement]:
|
||||
"""
|
||||
按强度过滤线段
|
||||
|
||||
Args:
|
||||
min_strength: 最小强度要求
|
||||
|
||||
Returns:
|
||||
过滤后的线段列表
|
||||
"""
|
||||
return [s for s in self.segments if s.strength >= min_strength]
|
||||
|
||||
def filter_segments_by_length(self, min_length: float) -> List[SegmentElement]:
|
||||
"""
|
||||
按长度过滤线段
|
||||
|
||||
Args:
|
||||
min_length: 最小长度要求
|
||||
|
||||
Returns:
|
||||
过滤后的线段列表
|
||||
"""
|
||||
return [s for s in self.segments if s.length >= min_length]
|
||||
|
||||
def to_dataframe(self) -> pd.DataFrame:
|
||||
"""
|
||||
将线段转换为DataFrame
|
||||
|
||||
Returns:
|
||||
包含线段信息的DataFrame
|
||||
"""
|
||||
if not self.segments:
|
||||
return pd.DataFrame()
|
||||
|
||||
data = []
|
||||
for i, segment in enumerate(self.segments):
|
||||
data.append({
|
||||
'segment_id': i,
|
||||
'start_time': segment.start_time,
|
||||
'end_time': segment.end_time,
|
||||
'start_price': segment.start_price,
|
||||
'end_price': segment.end_price,
|
||||
'direction': segment.direction,
|
||||
'length': segment.length,
|
||||
'strength': segment.strength,
|
||||
'stroke_count': len(segment.strokes),
|
||||
'confirmed': segment.confirmed
|
||||
})
|
||||
|
||||
return pd.DataFrame(data)
|
||||
+350
@@ -0,0 +1,350 @@
|
||||
"""
|
||||
笔模块:连接相邻的顶分型和底分型形成笔
|
||||
笔的定义:由一个顶分型和一个底分型连接而成,且中间不能有其他分型
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import List, Tuple, Optional, Dict
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from .fractal import FractalPoint
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StrokeElement:
|
||||
"""笔元素数据类"""
|
||||
start_fractal: FractalPoint # 起始分型
|
||||
end_fractal: FractalPoint # 结束分型
|
||||
direction: int # 方向:1为上升笔,-1为下降笔
|
||||
length: float # 笔的长度(价格差)
|
||||
duration: int # 持续时间(K线数量)
|
||||
strength: float # 笔的强度
|
||||
|
||||
|
||||
class Stroke:
|
||||
"""笔识别和处理类"""
|
||||
|
||||
def __init__(self, fractals: List[FractalPoint], kline_data: pd.DataFrame):
|
||||
"""
|
||||
初始化笔处理器
|
||||
|
||||
Args:
|
||||
fractals: 分型点列表
|
||||
kline_data: K线数据
|
||||
"""
|
||||
self.fractals = fractals
|
||||
self.kline_data = kline_data
|
||||
self.strokes = []
|
||||
self.up_strokes = [] # 上升笔
|
||||
self.down_strokes = [] # 下降笔
|
||||
|
||||
def create_stroke(self, start_fractal: FractalPoint,
|
||||
end_fractal: FractalPoint) -> StrokeElement:
|
||||
"""
|
||||
创建笔元素
|
||||
|
||||
Args:
|
||||
start_fractal: 起始分型
|
||||
end_fractal: 结束分型
|
||||
|
||||
Returns:
|
||||
笔元素
|
||||
"""
|
||||
# 计算方向
|
||||
if start_fractal.fractal_type == 'bottom' and end_fractal.fractal_type == 'top':
|
||||
direction = 1 # 上升笔
|
||||
elif start_fractal.fractal_type == 'top' and end_fractal.fractal_type == 'bottom':
|
||||
direction = -1 # 下降笔
|
||||
else:
|
||||
raise ValueError("无效的分型组合")
|
||||
|
||||
# 计算长度
|
||||
length = abs(end_fractal.price - start_fractal.price)
|
||||
|
||||
# 计算持续时间
|
||||
duration = end_fractal.index - start_fractal.index
|
||||
|
||||
# 计算强度(可以基于多个因素)
|
||||
strength = self._calculate_stroke_strength(start_fractal, end_fractal, length, duration)
|
||||
|
||||
return StrokeElement(
|
||||
start_fractal=start_fractal,
|
||||
end_fractal=end_fractal,
|
||||
direction=direction,
|
||||
length=length,
|
||||
duration=duration,
|
||||
strength=strength
|
||||
)
|
||||
|
||||
def _calculate_stroke_strength(self, start_fractal: FractalPoint,
|
||||
end_fractal: FractalPoint,
|
||||
length: float, duration: int) -> float:
|
||||
"""
|
||||
计算笔的强度
|
||||
|
||||
Args:
|
||||
start_fractal: 起始分型
|
||||
end_fractal: 结束分型
|
||||
length: 价格长度
|
||||
duration: 时间长度
|
||||
|
||||
Returns:
|
||||
笔的强度值
|
||||
"""
|
||||
# 基础强度:价格变化幅度
|
||||
price_strength = length
|
||||
|
||||
# 分型强度加权
|
||||
fractal_strength = (start_fractal.strength + end_fractal.strength) / 2
|
||||
|
||||
# 时间因子:适中的时间长度得分更高
|
||||
time_factor = min(duration / 10, 1.0) if duration > 0 else 0
|
||||
|
||||
# 综合强度
|
||||
strength = price_strength * (1 + fractal_strength * 0.1) * (1 + time_factor * 0.1)
|
||||
|
||||
return strength
|
||||
|
||||
def validate_stroke(self, start_fractal: FractalPoint,
|
||||
end_fractal: FractalPoint) -> bool:
|
||||
"""
|
||||
验证笔的有效性
|
||||
|
||||
Args:
|
||||
start_fractal: 起始分型
|
||||
end_fractal: 结束分型
|
||||
|
||||
Returns:
|
||||
是否为有效笔
|
||||
"""
|
||||
# 检查分型类型是否正确
|
||||
valid_combinations = [
|
||||
('bottom', 'top'), # 上升笔
|
||||
('top', 'bottom') # 下降笔
|
||||
]
|
||||
|
||||
combination = (start_fractal.fractal_type, end_fractal.fractal_type)
|
||||
if combination not in valid_combinations:
|
||||
return False
|
||||
|
||||
# 检查时间顺序
|
||||
if start_fractal.index >= end_fractal.index:
|
||||
return False
|
||||
|
||||
# 检查价格关系
|
||||
if start_fractal.fractal_type == 'bottom':
|
||||
# 上升笔:结束价格应该高于起始价格
|
||||
if end_fractal.price <= start_fractal.price:
|
||||
return False
|
||||
else:
|
||||
# 下降笔:结束价格应该低于起始价格
|
||||
if end_fractal.price >= start_fractal.price:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def detect_strokes(self) -> List[StrokeElement]:
|
||||
"""
|
||||
检测所有笔
|
||||
|
||||
Returns:
|
||||
笔列表
|
||||
"""
|
||||
if len(self.fractals) < 2:
|
||||
logger.warning("分型数量不足,无法生成笔")
|
||||
return []
|
||||
|
||||
strokes = []
|
||||
|
||||
# 按时间顺序排序分型
|
||||
sorted_fractals = sorted(self.fractals, key=lambda x: x.index)
|
||||
|
||||
i = 0
|
||||
while i < len(sorted_fractals) - 1:
|
||||
start_fractal = sorted_fractals[i]
|
||||
|
||||
# 寻找下一个有效的分型来形成笔
|
||||
j = i + 1
|
||||
while j < len(sorted_fractals):
|
||||
end_fractal = sorted_fractals[j]
|
||||
|
||||
# 检查是否可以形成有效笔
|
||||
if self.validate_stroke(start_fractal, end_fractal):
|
||||
stroke = self.create_stroke(start_fractal, end_fractal)
|
||||
strokes.append(stroke)
|
||||
|
||||
# 分类存储
|
||||
if stroke.direction == 1:
|
||||
self.up_strokes.append(stroke)
|
||||
else:
|
||||
self.down_strokes.append(stroke)
|
||||
|
||||
# 从结束分型继续寻找下一笔
|
||||
i = j
|
||||
break
|
||||
|
||||
j += 1
|
||||
else:
|
||||
# 没有找到有效的结束分型,跳到下一个分型
|
||||
i += 1
|
||||
|
||||
self.strokes = strokes
|
||||
logger.info(f"检测到 {len(strokes)} 笔:上升笔 {len(self.up_strokes)} 个,下降笔 {len(self.down_strokes)} 个")
|
||||
|
||||
return strokes
|
||||
|
||||
def get_stroke_sequence(self) -> List[StrokeElement]:
|
||||
"""
|
||||
获取连续的笔序列
|
||||
|
||||
Returns:
|
||||
连续笔序列
|
||||
"""
|
||||
return sorted(self.strokes, key=lambda x: x.start_fractal.index)
|
||||
|
||||
def find_stroke_extremes(self) -> Dict[str, List[StrokeElement]]:
|
||||
"""
|
||||
寻找笔的极值点
|
||||
|
||||
Returns:
|
||||
包含最长、最短、最强笔的字典
|
||||
"""
|
||||
if not self.strokes:
|
||||
return {}
|
||||
|
||||
# 按长度排序
|
||||
by_length = sorted(self.strokes, key=lambda x: x.length, reverse=True)
|
||||
|
||||
# 按强度排序
|
||||
by_strength = sorted(self.strokes, key=lambda x: x.strength, reverse=True)
|
||||
|
||||
# 按持续时间排序
|
||||
by_duration = sorted(self.strokes, key=lambda x: x.duration, reverse=True)
|
||||
|
||||
return {
|
||||
'longest': by_length[:5], # 最长的5笔
|
||||
'strongest': by_strength[:5], # 最强的5笔
|
||||
'longest_duration': by_duration[:5] # 持续时间最长的5笔
|
||||
}
|
||||
|
||||
def analyze_stroke_patterns(self) -> Dict:
|
||||
"""
|
||||
分析笔的模式
|
||||
|
||||
Returns:
|
||||
模式分析结果
|
||||
"""
|
||||
if len(self.strokes) < 3:
|
||||
return {}
|
||||
|
||||
sequence = self.get_stroke_sequence()
|
||||
|
||||
# 分析连续同向笔(可能的延伸)
|
||||
extensions = []
|
||||
i = 0
|
||||
while i < len(sequence) - 1:
|
||||
current = sequence[i]
|
||||
next_stroke = sequence[i + 1]
|
||||
|
||||
# 检查是否为同向延伸
|
||||
if current.direction == next_stroke.direction:
|
||||
extensions.append((current, next_stroke))
|
||||
|
||||
i += 1
|
||||
|
||||
# 分析笔的趋势强度
|
||||
trend_strength = self._calculate_trend_strength(sequence)
|
||||
|
||||
# 统计平均笔长度
|
||||
avg_length = np.mean([s.length for s in self.strokes]) if self.strokes else 0
|
||||
avg_duration = np.mean([s.duration for s in self.strokes]) if self.strokes else 0
|
||||
|
||||
return {
|
||||
'total_strokes': len(self.strokes),
|
||||
'up_strokes': len(self.up_strokes),
|
||||
'down_strokes': len(self.down_strokes),
|
||||
'extensions': len(extensions),
|
||||
'avg_length': avg_length,
|
||||
'avg_duration': avg_duration,
|
||||
'trend_strength': trend_strength
|
||||
}
|
||||
|
||||
def _calculate_trend_strength(self, sequence: List[StrokeElement]) -> float:
|
||||
"""
|
||||
计算趋势强度
|
||||
|
||||
Args:
|
||||
sequence: 笔序列
|
||||
|
||||
Returns:
|
||||
趋势强度值
|
||||
"""
|
||||
if len(sequence) < 2:
|
||||
return 0
|
||||
|
||||
# 计算方向变化的频率
|
||||
direction_changes = 0
|
||||
for i in range(1, len(sequence)):
|
||||
if sequence[i].direction != sequence[i-1].direction:
|
||||
direction_changes += 1
|
||||
|
||||
# 趋势强度与方向变化成反比
|
||||
change_ratio = direction_changes / (len(sequence) - 1) if len(sequence) > 1 else 1
|
||||
trend_strength = 1 - change_ratio
|
||||
|
||||
return trend_strength
|
||||
|
||||
def filter_strokes_by_strength(self, min_strength: float) -> List[StrokeElement]:
|
||||
"""
|
||||
按强度过滤笔
|
||||
|
||||
Args:
|
||||
min_strength: 最小强度要求
|
||||
|
||||
Returns:
|
||||
过滤后的笔列表
|
||||
"""
|
||||
return [s for s in self.strokes if s.strength >= min_strength]
|
||||
|
||||
def filter_strokes_by_length(self, min_length: float) -> List[StrokeElement]:
|
||||
"""
|
||||
按长度过滤笔
|
||||
|
||||
Args:
|
||||
min_length: 最小长度要求
|
||||
|
||||
Returns:
|
||||
过滤后的笔列表
|
||||
"""
|
||||
return [s for s in self.strokes if s.length >= min_length]
|
||||
|
||||
def to_dataframe(self) -> pd.DataFrame:
|
||||
"""
|
||||
将笔转换为DataFrame
|
||||
|
||||
Returns:
|
||||
包含笔信息的DataFrame
|
||||
"""
|
||||
if not self.strokes:
|
||||
return pd.DataFrame()
|
||||
|
||||
data = []
|
||||
for i, stroke in enumerate(self.strokes):
|
||||
data.append({
|
||||
'stroke_id': i,
|
||||
'start_time': stroke.start_fractal.timestamp,
|
||||
'end_time': stroke.end_fractal.timestamp,
|
||||
'start_price': stroke.start_fractal.price,
|
||||
'end_price': stroke.end_fractal.price,
|
||||
'direction': stroke.direction,
|
||||
'length': stroke.length,
|
||||
'duration': stroke.duration,
|
||||
'strength': stroke.strength,
|
||||
'start_fractal_type': stroke.start_fractal.fractal_type,
|
||||
'end_fractal_type': stroke.end_fractal.fractal_type
|
||||
})
|
||||
|
||||
return pd.DataFrame(data)
|
||||
@@ -0,0 +1,576 @@
|
||||
"""
|
||||
买卖点信号模块:识别缠论中的各类买卖点
|
||||
包括一类买卖点、二类买卖点、三类买卖点的识别逻辑
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import List, Tuple, Optional, Dict
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from .central_bank import CentralBankElement
|
||||
from .segment import SegmentElement
|
||||
from .stroke import StrokeElement
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TradingPoint:
|
||||
"""买卖点数据类"""
|
||||
timestamp: pd.Timestamp # 信号时间
|
||||
price: float # 信号价格
|
||||
signal_type: str # 'buy' 或 'sell'
|
||||
point_class: str # 'first', 'second', 'third'
|
||||
strength: float # 信号强度
|
||||
description: str # 信号描述
|
||||
related_central_bank: Optional[CentralBankElement] = None # 相关中枢
|
||||
confirmed: bool = False # 是否已确认
|
||||
|
||||
|
||||
class TradingSignal:
|
||||
"""买卖点信号识别类"""
|
||||
|
||||
def __init__(self, central_banks: List[CentralBankElement],
|
||||
segments: List[SegmentElement],
|
||||
strokes: List[StrokeElement]):
|
||||
"""
|
||||
初始化买卖点识别器
|
||||
|
||||
Args:
|
||||
central_banks: 中枢列表
|
||||
segments: 线段列表
|
||||
strokes: 笔列表
|
||||
"""
|
||||
self.central_banks = sorted(central_banks, key=lambda x: x.start_time)
|
||||
self.segments = sorted(segments, key=lambda x: x.start_time)
|
||||
self.strokes = sorted(strokes, key=lambda x: x.start_fractal.timestamp)
|
||||
self.trading_points = []
|
||||
|
||||
def detect_first_class_points(self) -> List[TradingPoint]:
|
||||
"""
|
||||
识别一类买卖点:中枢突破点
|
||||
|
||||
Returns:
|
||||
一类买卖点列表
|
||||
"""
|
||||
first_class_points = []
|
||||
|
||||
for cb in self.central_banks:
|
||||
# 寻找中枢后的突破
|
||||
post_segments = [seg for seg in self.segments
|
||||
if seg.start_time > cb.end_time]
|
||||
|
||||
if not post_segments:
|
||||
continue
|
||||
|
||||
# 检查前3个线段中的突破
|
||||
for seg in post_segments[:3]:
|
||||
if seg.direction == 1 and seg.end_price > cb.high_price:
|
||||
# 向上突破 - 一类买点
|
||||
strength = self._calculate_breakout_strength(cb, seg, 'up')
|
||||
|
||||
point = TradingPoint(
|
||||
timestamp=seg.end_time,
|
||||
price=seg.end_price,
|
||||
signal_type='buy',
|
||||
point_class='first',
|
||||
strength=strength,
|
||||
description=f"一类买点:向上突破{cb.level}中枢",
|
||||
related_central_bank=cb
|
||||
)
|
||||
first_class_points.append(point)
|
||||
break
|
||||
|
||||
elif seg.direction == -1 and seg.end_price < cb.low_price:
|
||||
# 向下突破 - 一类卖点
|
||||
strength = self._calculate_breakout_strength(cb, seg, 'down')
|
||||
|
||||
point = TradingPoint(
|
||||
timestamp=seg.end_time,
|
||||
price=seg.end_price,
|
||||
signal_type='sell',
|
||||
point_class='first',
|
||||
strength=strength,
|
||||
description=f"一类卖点:向下突破{cb.level}中枢",
|
||||
related_central_bank=cb
|
||||
)
|
||||
first_class_points.append(point)
|
||||
break
|
||||
|
||||
return first_class_points
|
||||
|
||||
def detect_second_class_points(self) -> List[TradingPoint]:
|
||||
"""
|
||||
识别二类买卖点:回拉不进入中枢的确认点
|
||||
|
||||
Returns:
|
||||
二类买卖点列表
|
||||
"""
|
||||
second_class_points = []
|
||||
|
||||
# 先获取一类买卖点
|
||||
first_class_points = self.detect_first_class_points()
|
||||
|
||||
for first_point in first_class_points:
|
||||
cb = first_point.related_central_bank
|
||||
if not cb:
|
||||
continue
|
||||
|
||||
# 寻找一类点之后的回拉
|
||||
post_segments = [seg for seg in self.segments
|
||||
if seg.start_time > first_point.timestamp]
|
||||
|
||||
for i, seg in enumerate(post_segments[:5]): # 检查后续5个线段
|
||||
if first_point.signal_type == 'buy':
|
||||
# 一类买点后的回拉测试
|
||||
if (seg.direction == -1 and
|
||||
seg.end_price > cb.high_price and # 没有跌破中枢上边界
|
||||
i < len(post_segments) - 1): # 确保有后续线段
|
||||
|
||||
next_seg = post_segments[i + 1]
|
||||
if next_seg.direction == 1: # 回拉后再次上涨
|
||||
strength = self._calculate_pullback_strength(cb, seg, next_seg)
|
||||
|
||||
point = TradingPoint(
|
||||
timestamp=next_seg.start_time,
|
||||
price=seg.end_price,
|
||||
signal_type='buy',
|
||||
point_class='second',
|
||||
strength=strength,
|
||||
description=f"二类买点:回拉确认{cb.level}中枢支撑",
|
||||
related_central_bank=cb
|
||||
)
|
||||
second_class_points.append(point)
|
||||
|
||||
elif first_point.signal_type == 'sell':
|
||||
# 一类卖点后的反弹测试
|
||||
if (seg.direction == 1 and
|
||||
seg.end_price < cb.low_price and # 没有涨破中枢下边界
|
||||
i < len(post_segments) - 1): # 确保有后续线段
|
||||
|
||||
next_seg = post_segments[i + 1]
|
||||
if next_seg.direction == -1: # 反弹后再次下跌
|
||||
strength = self._calculate_pullback_strength(cb, seg, next_seg)
|
||||
|
||||
point = TradingPoint(
|
||||
timestamp=next_seg.start_time,
|
||||
price=seg.end_price,
|
||||
signal_type='sell',
|
||||
point_class='second',
|
||||
strength=strength,
|
||||
description=f"二类卖点:反弹确认{cb.level}中枢阻力",
|
||||
related_central_bank=cb
|
||||
)
|
||||
second_class_points.append(point)
|
||||
|
||||
return second_class_points
|
||||
|
||||
def detect_third_class_points(self) -> List[TradingPoint]:
|
||||
"""
|
||||
识别三类买卖点:次级别背驰点
|
||||
|
||||
Returns:
|
||||
三类买卖点列表
|
||||
"""
|
||||
third_class_points = []
|
||||
|
||||
# 在中枢内部寻找次级别的背驰机会
|
||||
for cb in self.central_banks:
|
||||
# 获取中枢时间范围内的笔
|
||||
internal_strokes = [stroke for stroke in self.strokes
|
||||
if (stroke.start_fractal.timestamp >= cb.start_time and
|
||||
stroke.end_fractal.timestamp <= cb.end_time)]
|
||||
|
||||
if len(internal_strokes) < 5: # 需要足够的笔进行分析
|
||||
continue
|
||||
|
||||
# 寻找背驰模式
|
||||
divergence_points = self._find_divergence_in_strokes(internal_strokes, cb)
|
||||
third_class_points.extend(divergence_points)
|
||||
|
||||
return third_class_points
|
||||
|
||||
def _find_divergence_in_strokes(self, strokes: List[StrokeElement],
|
||||
cb: CentralBankElement) -> List[TradingPoint]:
|
||||
"""
|
||||
在笔中寻找背驰模式
|
||||
|
||||
Args:
|
||||
strokes: 笔列表
|
||||
cb: 相关中枢
|
||||
|
||||
Returns:
|
||||
背驰点列表
|
||||
"""
|
||||
divergence_points = []
|
||||
|
||||
# 按方向分组
|
||||
up_strokes = [s for s in strokes if s.direction == 1]
|
||||
down_strokes = [s for s in strokes if s.direction == -1]
|
||||
|
||||
# 检查上升笔的顶背驰
|
||||
if len(up_strokes) >= 3:
|
||||
for i in range(2, len(up_strokes)):
|
||||
current_stroke = up_strokes[i]
|
||||
prev_stroke = up_strokes[i-1]
|
||||
|
||||
# 价格创新高但力度减弱
|
||||
if (current_stroke.end_fractal.price > prev_stroke.end_fractal.price and
|
||||
current_stroke.strength < prev_stroke.strength * 0.8):
|
||||
|
||||
strength = self._calculate_divergence_strength(current_stroke, prev_stroke)
|
||||
|
||||
point = TradingPoint(
|
||||
timestamp=current_stroke.end_fractal.timestamp,
|
||||
price=current_stroke.end_fractal.price,
|
||||
signal_type='sell',
|
||||
point_class='third',
|
||||
strength=strength,
|
||||
description=f"三类卖点:{cb.level}中枢内顶背驰",
|
||||
related_central_bank=cb
|
||||
)
|
||||
divergence_points.append(point)
|
||||
|
||||
# 检查下降笔的底背驰
|
||||
if len(down_strokes) >= 3:
|
||||
for i in range(2, len(down_strokes)):
|
||||
current_stroke = down_strokes[i]
|
||||
prev_stroke = down_strokes[i-1]
|
||||
|
||||
# 价格创新低但力度减弱
|
||||
if (current_stroke.end_fractal.price < prev_stroke.end_fractal.price and
|
||||
current_stroke.strength < prev_stroke.strength * 0.8):
|
||||
|
||||
strength = self._calculate_divergence_strength(current_stroke, prev_stroke)
|
||||
|
||||
point = TradingPoint(
|
||||
timestamp=current_stroke.end_fractal.timestamp,
|
||||
price=current_stroke.end_fractal.price,
|
||||
signal_type='buy',
|
||||
point_class='third',
|
||||
strength=strength,
|
||||
description=f"三类买点:{cb.level}中枢内底背驰",
|
||||
related_central_bank=cb
|
||||
)
|
||||
divergence_points.append(point)
|
||||
|
||||
return divergence_points
|
||||
|
||||
def _calculate_breakout_strength(self, cb: CentralBankElement,
|
||||
segment: SegmentElement, direction: str) -> float:
|
||||
"""
|
||||
计算突破强度
|
||||
|
||||
Args:
|
||||
cb: 中枢
|
||||
segment: 突破线段
|
||||
direction: 突破方向
|
||||
|
||||
Returns:
|
||||
突破强度
|
||||
"""
|
||||
# 基础强度:突破幅度
|
||||
if direction == 'up':
|
||||
breakout_distance = segment.end_price - cb.high_price
|
||||
base_strength = breakout_distance / cb.high_price
|
||||
else:
|
||||
breakout_distance = cb.low_price - segment.end_price
|
||||
base_strength = breakout_distance / cb.low_price
|
||||
|
||||
# 中枢强度加权
|
||||
cb_strength_factor = min(cb.strength / 100, 2.0)
|
||||
|
||||
# 线段强度加权
|
||||
segment_strength_factor = min(segment.strength / 50, 2.0)
|
||||
|
||||
# 综合强度
|
||||
total_strength = base_strength * (1 + cb_strength_factor * 0.2) * (1 + segment_strength_factor * 0.3)
|
||||
|
||||
return max(0, min(total_strength, 1.0)) # 限制在0-1之间
|
||||
|
||||
def _calculate_pullback_strength(self, cb: CentralBankElement,
|
||||
pullback_seg: SegmentElement,
|
||||
resume_seg: SegmentElement) -> float:
|
||||
"""
|
||||
计算回拉强度
|
||||
|
||||
Args:
|
||||
cb: 中枢
|
||||
pullback_seg: 回拉线段
|
||||
resume_seg: 恢复线段
|
||||
|
||||
Returns:
|
||||
回拉强度
|
||||
"""
|
||||
# 回拉深度(越浅越好)
|
||||
cb_height = cb.high_price - cb.low_price
|
||||
if pullback_seg.direction == -1: # 向下回拉
|
||||
pullback_depth = (cb.high_price - pullback_seg.end_price) / cb_height
|
||||
else: # 向上回拉
|
||||
pullback_depth = (pullback_seg.end_price - cb.low_price) / cb_height
|
||||
|
||||
# 回拉强度:深度越小越好
|
||||
pullback_strength = max(0, 1 - pullback_depth)
|
||||
|
||||
# 恢复强度
|
||||
resume_strength = min(resume_seg.strength / 30, 1.0)
|
||||
|
||||
# 综合强度
|
||||
total_strength = (pullback_strength + resume_strength) / 2
|
||||
|
||||
return max(0, min(total_strength, 1.0))
|
||||
|
||||
def _calculate_divergence_strength(self, current_stroke: StrokeElement,
|
||||
prev_stroke: StrokeElement) -> float:
|
||||
"""
|
||||
计算背驰强度
|
||||
|
||||
Args:
|
||||
current_stroke: 当前笔
|
||||
prev_stroke: 前一笔
|
||||
|
||||
Returns:
|
||||
背驰强度
|
||||
"""
|
||||
# 力度差异
|
||||
strength_ratio = prev_stroke.strength / current_stroke.strength if current_stroke.strength > 0 else 1
|
||||
|
||||
# 价格差异
|
||||
price_change = abs(current_stroke.end_fractal.price - prev_stroke.end_fractal.price)
|
||||
price_ratio = price_change / prev_stroke.start_fractal.price if prev_stroke.start_fractal.price > 0 else 0
|
||||
|
||||
# 背驰强度
|
||||
divergence_strength = min(strength_ratio * 0.3 + price_ratio * 10, 1.0)
|
||||
|
||||
return max(0, divergence_strength)
|
||||
|
||||
def detect_fractal_based_signals(self, fractals: List, processed_klines) -> List[TradingPoint]:
|
||||
"""
|
||||
基于分型识别买卖点(不依赖中枢)
|
||||
|
||||
Args:
|
||||
fractals: 分型列表
|
||||
processed_klines: 处理后的K线数据
|
||||
|
||||
Returns:
|
||||
分型买卖点列表
|
||||
"""
|
||||
signals = []
|
||||
|
||||
if not fractals or len(fractals) < 3:
|
||||
return signals
|
||||
|
||||
for i, fractal in enumerate(fractals):
|
||||
# 分析分型强度和位置
|
||||
strength_score = self._calculate_fractal_strength_score(fractal, fractals, i)
|
||||
position_score = self._calculate_fractal_position_score(fractal, processed_klines)
|
||||
|
||||
# 综合评分
|
||||
total_score = (strength_score + position_score) / 2
|
||||
|
||||
if total_score > 0.6: # 阈值可调整
|
||||
signal_type = 'buy' if fractal.fractal_type == 'bottom' else 'sell'
|
||||
point_class = self._determine_fractal_point_class(total_score)
|
||||
|
||||
description = f"分型{signal_type}点:强度{fractal.strength},评分{total_score:.2f}"
|
||||
|
||||
signal = TradingPoint(
|
||||
timestamp=fractal.timestamp,
|
||||
price=fractal.price,
|
||||
signal_type=signal_type,
|
||||
point_class=point_class,
|
||||
strength=total_score,
|
||||
description=description,
|
||||
confirmed=fractal.confirmed
|
||||
)
|
||||
signals.append(signal)
|
||||
|
||||
return signals
|
||||
|
||||
def _calculate_fractal_strength_score(self, fractal, all_fractals: List, index: int) -> float:
|
||||
"""计算分型强度评分(避免使用未来数据)"""
|
||||
base_score = min(fractal.strength / 5.0, 1.0) # 基础强度评分
|
||||
|
||||
# 只使用过去的分型进行比较,避免使用未来数据
|
||||
past_fractals = []
|
||||
for i in range(max(0, index-5), index): # 只看过去5个分型
|
||||
if i < len(all_fractals) and all_fractals[i].fractal_type == fractal.fractal_type:
|
||||
past_fractals.append(all_fractals[i])
|
||||
|
||||
if past_fractals:
|
||||
relative_strength = fractal.strength / max(f.strength for f in past_fractals)
|
||||
relative_score = min(relative_strength, 1.0)
|
||||
else:
|
||||
relative_score = 1.0
|
||||
|
||||
return (base_score + relative_score) / 2
|
||||
|
||||
def _calculate_fractal_position_score(self, fractal, processed_klines) -> float:
|
||||
"""计算分型位置评分(避免使用未来数据)"""
|
||||
if processed_klines is None or len(processed_klines) < 20:
|
||||
return 0.5
|
||||
|
||||
# 寻找分型在K线数据中的位置
|
||||
fractal_index = None
|
||||
for i, (timestamp, kline) in enumerate(processed_klines.iterrows()):
|
||||
# 修复时间戳计算兼容性问题
|
||||
time_diff = abs((pd.Timestamp(timestamp) - pd.Timestamp(fractal.timestamp)).total_seconds())
|
||||
if time_diff < 3600: # 1小时内
|
||||
fractal_index = i
|
||||
break
|
||||
|
||||
if fractal_index is None:
|
||||
return 0.5
|
||||
|
||||
# 计算相对位置(只使用过去的数据)
|
||||
start_idx = max(0, fractal_index - 20) # 只看过去20个K线
|
||||
end_idx = fractal_index + 1 # 包含当前K线,但不包含未来K线
|
||||
recent_data = processed_klines.iloc[start_idx:end_idx]
|
||||
|
||||
if fractal.fractal_type == 'bottom':
|
||||
# 底分型:看是否接近过去一段时间的低点
|
||||
min_price = recent_data['low'].min()
|
||||
score = 1.0 if abs(fractal.price - min_price) / min_price < 0.02 else 0.5
|
||||
else:
|
||||
# 顶分型:看是否接近过去一段时间的高点
|
||||
max_price = recent_data['high'].max()
|
||||
score = 1.0 if abs(fractal.price - max_price) / max_price < 0.02 else 0.5
|
||||
|
||||
return score
|
||||
|
||||
def _determine_fractal_point_class(self, score: float) -> str:
|
||||
"""根据评分确定买卖点类别"""
|
||||
if score >= 0.8:
|
||||
return "second" # 高质量分型当作二类买卖点
|
||||
else:
|
||||
return "third" # 一般分型当作三类买卖点
|
||||
|
||||
def detect_all_trading_points(self) -> List[TradingPoint]:
|
||||
"""
|
||||
检测所有买卖点(包括中枢相关和分型相关)
|
||||
"""
|
||||
all_signals = []
|
||||
|
||||
# 原有的中枢相关买卖点
|
||||
all_signals.extend(self.detect_first_class_points())
|
||||
all_signals.extend(self.detect_second_class_points())
|
||||
all_signals.extend(self.detect_third_class_points())
|
||||
|
||||
# 新增:基于分型的买卖点(当没有足够中枢时)
|
||||
if len(self.central_banks) < 2:
|
||||
logger.info("中枢数量不足,启用分型买卖点识别")
|
||||
# 需要获取分型和K线数据,这里需要从外部传入
|
||||
# 暂时先返回现有信号
|
||||
pass
|
||||
|
||||
# 按时间排序
|
||||
all_signals.sort(key=lambda x: x.timestamp)
|
||||
|
||||
# 去重(同一时间点可能有多个信号)
|
||||
unique_signals = []
|
||||
seen_times = set()
|
||||
for signal in all_signals:
|
||||
time_key = signal.timestamp.strftime('%Y%m%d%H%M')
|
||||
if time_key not in seen_times:
|
||||
unique_signals.append(signal)
|
||||
seen_times.add(time_key)
|
||||
|
||||
class_counts = {'first': 0, 'second': 0, 'third': 0}
|
||||
for signal in unique_signals:
|
||||
class_counts[signal.point_class] += 1
|
||||
|
||||
total_signals = len(unique_signals)
|
||||
logger.info(f"检测到买卖点:一类 {class_counts['first']} 个,二类 {class_counts['second']} 个,"
|
||||
f"三类 {class_counts['third']} 个,总计 {total_signals} 个")
|
||||
|
||||
return unique_signals
|
||||
|
||||
def get_signal_statistics(self) -> Dict:
|
||||
"""
|
||||
获取信号统计信息
|
||||
|
||||
Returns:
|
||||
统计信息字典
|
||||
"""
|
||||
if not self.trading_points:
|
||||
return {}
|
||||
|
||||
stats = {
|
||||
'total_signals': len(self.trading_points),
|
||||
'buy_signals': len([p for p in self.trading_points if p.signal_type == 'buy']),
|
||||
'sell_signals': len([p for p in self.trading_points if p.signal_type == 'sell']),
|
||||
'by_class': {},
|
||||
'avg_strength': np.mean([p.strength for p in self.trading_points]),
|
||||
'max_strength': max([p.strength for p in self.trading_points]),
|
||||
'confirmed_signals': len([p for p in self.trading_points if p.confirmed])
|
||||
}
|
||||
|
||||
# 按类别统计
|
||||
for point_class in ['first', 'second', 'third']:
|
||||
class_points = [p for p in self.trading_points if p.point_class == point_class]
|
||||
stats['by_class'][f'{point_class}_class'] = {
|
||||
'total': len(class_points),
|
||||
'buy': len([p for p in class_points if p.signal_type == 'buy']),
|
||||
'sell': len([p for p in class_points if p.signal_type == 'sell']),
|
||||
'avg_strength': np.mean([p.strength for p in class_points]) if class_points else 0
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
def filter_signals_by_strength(self, min_strength: float) -> List[TradingPoint]:
|
||||
"""
|
||||
按强度过滤信号
|
||||
|
||||
Args:
|
||||
min_strength: 最小强度要求
|
||||
|
||||
Returns:
|
||||
过滤后的信号列表
|
||||
"""
|
||||
return [p for p in self.trading_points if p.strength >= min_strength]
|
||||
|
||||
def get_latest_signals(self, hours: int = 24) -> List[TradingPoint]:
|
||||
"""
|
||||
获取最新的信号
|
||||
|
||||
Args:
|
||||
hours: 最近多少小时
|
||||
|
||||
Returns:
|
||||
最新信号列表
|
||||
"""
|
||||
if not self.trading_points:
|
||||
return []
|
||||
|
||||
latest_time = max([p.timestamp for p in self.trading_points])
|
||||
cutoff_time = latest_time - pd.Timedelta(hours=hours)
|
||||
|
||||
return [p for p in self.trading_points if p.timestamp >= cutoff_time]
|
||||
|
||||
def to_dataframe(self) -> pd.DataFrame:
|
||||
"""
|
||||
将买卖点转换为DataFrame
|
||||
|
||||
Returns:
|
||||
包含买卖点信息的DataFrame
|
||||
"""
|
||||
if not self.trading_points:
|
||||
return pd.DataFrame()
|
||||
|
||||
data = []
|
||||
for point in self.trading_points:
|
||||
data.append({
|
||||
'timestamp': point.timestamp,
|
||||
'price': point.price,
|
||||
'signal_type': point.signal_type,
|
||||
'point_class': point.point_class,
|
||||
'strength': point.strength,
|
||||
'description': point.description,
|
||||
'confirmed': point.confirmed,
|
||||
'related_central_bank_level': point.related_central_bank.level if point.related_central_bank else None
|
||||
})
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
df.set_index('timestamp', inplace=True)
|
||||
return df
|
||||
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
数据模块:负责获取和处理K线数据
|
||||
"""
|
||||
|
||||
from .data_fetcher import DataFetcher
|
||||
from .data_processor import DataProcessor
|
||||
|
||||
__all__ = ['DataFetcher', 'DataProcessor']
|
||||
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
数据获取模块:使用ccxt库拉取数字货币市场数据
|
||||
"""
|
||||
|
||||
import ccxt
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from datetime import datetime, timedelta
|
||||
import time
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DataFetcher:
|
||||
"""数据获取器:负责从交易所获取K线数据"""
|
||||
|
||||
def __init__(self, exchange_name: str = 'binance'):
|
||||
"""
|
||||
初始化数据获取器
|
||||
|
||||
Args:
|
||||
exchange_name: 交易所名称,默认为binance
|
||||
"""
|
||||
self.exchange_name = exchange_name
|
||||
self.exchange = self._init_exchange()
|
||||
|
||||
def _init_exchange(self) -> ccxt.Exchange:
|
||||
"""初始化交易所连接"""
|
||||
try:
|
||||
exchange_class = getattr(ccxt, self.exchange_name)
|
||||
exchange = exchange_class({
|
||||
'apiKey': '', # 对于公开数据不需要API密钥
|
||||
'secret': '',
|
||||
'timeout': 30000,
|
||||
'enableRateLimit': True,
|
||||
})
|
||||
return exchange
|
||||
except Exception as e:
|
||||
logger.error(f"初始化交易所失败: {e}")
|
||||
raise
|
||||
|
||||
def fetch_klines(self,
|
||||
symbol: str,
|
||||
timeframe: str = '1h',
|
||||
limit: int = 1000,
|
||||
since: Optional[int] = None) -> pd.DataFrame:
|
||||
"""
|
||||
获取K线数据
|
||||
|
||||
Args:
|
||||
symbol: 交易对符号,如'BTC/USDT'
|
||||
timeframe: 时间周期,如'1h', '4h', '1d'
|
||||
limit: 获取的K线数量
|
||||
since: 开始时间戳(毫秒)
|
||||
|
||||
Returns:
|
||||
包含K线数据的DataFrame
|
||||
"""
|
||||
try:
|
||||
logger.info(f"正在获取 {symbol} {timeframe} 数据,数量: {limit}")
|
||||
|
||||
# 获取原始数据
|
||||
ohlcv = self.exchange.fetch_ohlcv(
|
||||
symbol=symbol,
|
||||
timeframe=timeframe,
|
||||
limit=limit,
|
||||
since=since
|
||||
)
|
||||
|
||||
if not ohlcv:
|
||||
raise ValueError("未获取到数据")
|
||||
|
||||
# 转换为DataFrame
|
||||
df = pd.DataFrame(ohlcv, columns=[
|
||||
'timestamp', 'open', 'high', 'low', 'close', 'volume'
|
||||
])
|
||||
|
||||
# 转换时间戳为datetime
|
||||
df['datetime'] = pd.to_datetime(df['timestamp'], unit='ms')
|
||||
df.set_index('datetime', inplace=True)
|
||||
|
||||
# 确保数据类型正确
|
||||
price_columns = ['open', 'high', 'low', 'close']
|
||||
df[price_columns] = df[price_columns].astype(float)
|
||||
df['volume'] = df['volume'].astype(float)
|
||||
|
||||
logger.info(f"成功获取 {len(df)} 条K线数据")
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取K线数据失败: {e}")
|
||||
raise
|
||||
|
||||
def fetch_multiple_timeframes(self,
|
||||
symbol: str,
|
||||
timeframes: List[str],
|
||||
limit: int = 1000) -> Dict[str, pd.DataFrame]:
|
||||
"""
|
||||
获取多个时间周期的数据
|
||||
|
||||
Args:
|
||||
symbol: 交易对符号
|
||||
timeframes: 时间周期列表
|
||||
limit: 每个周期获取的数量
|
||||
|
||||
Returns:
|
||||
字典,键为时间周期,值为对应的DataFrame
|
||||
"""
|
||||
result = {}
|
||||
|
||||
for timeframe in timeframes:
|
||||
try:
|
||||
df = self.fetch_klines(symbol, timeframe, limit)
|
||||
result[timeframe] = df
|
||||
|
||||
# 避免请求过于频繁
|
||||
time.sleep(self.exchange.rateLimit / 1000)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取 {timeframe} 数据失败: {e}")
|
||||
continue
|
||||
|
||||
return result
|
||||
|
||||
def get_latest_price(self, symbol: str) -> float:
|
||||
"""
|
||||
获取最新价格
|
||||
|
||||
Args:
|
||||
symbol: 交易对符号
|
||||
|
||||
Returns:
|
||||
最新价格
|
||||
"""
|
||||
try:
|
||||
ticker = self.exchange.fetch_ticker(symbol)
|
||||
return float(ticker['last'])
|
||||
except Exception as e:
|
||||
logger.error(f"获取最新价格失败: {e}")
|
||||
raise
|
||||
|
||||
def validate_symbol(self, symbol: str) -> bool:
|
||||
"""
|
||||
验证交易对是否有效
|
||||
|
||||
Args:
|
||||
symbol: 交易对符号
|
||||
|
||||
Returns:
|
||||
是否有效
|
||||
"""
|
||||
try:
|
||||
markets = self.exchange.load_markets()
|
||||
return symbol in markets
|
||||
except Exception as e:
|
||||
logger.error(f"验证交易对失败: {e}")
|
||||
return False
|
||||
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
数据处理模块:负责K线数据的清理、格式化和预处理
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import Optional, Tuple, List
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DataProcessor:
|
||||
"""数据处理器:负责K线数据的处理和验证"""
|
||||
|
||||
@staticmethod
|
||||
def validate_klines(df: pd.DataFrame) -> bool:
|
||||
"""
|
||||
验证K线数据的完整性和正确性
|
||||
|
||||
Args:
|
||||
df: K线数据DataFrame
|
||||
|
||||
Returns:
|
||||
是否通过验证
|
||||
"""
|
||||
required_columns = ['open', 'high', 'low', 'close', 'volume']
|
||||
|
||||
# 检查必需列是否存在
|
||||
if not all(col in df.columns for col in required_columns):
|
||||
logger.error("缺少必需的列")
|
||||
return False
|
||||
|
||||
# 检查数据是否为空
|
||||
if df.empty:
|
||||
logger.error("数据为空")
|
||||
return False
|
||||
|
||||
# 检查价格关系是否正确
|
||||
invalid_rows = (
|
||||
(df['high'] < df['low']) |
|
||||
(df['high'] < df['open']) |
|
||||
(df['high'] < df['close']) |
|
||||
(df['low'] > df['open']) |
|
||||
(df['low'] > df['close'])
|
||||
)
|
||||
|
||||
if invalid_rows.any():
|
||||
logger.warning(f"发现 {invalid_rows.sum()} 行无效的价格关系")
|
||||
|
||||
# 检查是否有NaN值
|
||||
if df[required_columns].isnull().any().any():
|
||||
logger.warning("数据中包含NaN值")
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def clean_klines(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
清理K线数据
|
||||
|
||||
Args:
|
||||
df: 原始K线数据
|
||||
|
||||
Returns:
|
||||
清理后的K线数据
|
||||
"""
|
||||
df_clean = df.copy()
|
||||
|
||||
# 移除NaN值
|
||||
df_clean = df_clean.dropna()
|
||||
|
||||
# 修正无效的价格关系
|
||||
# 如果high < max(open, close),则设置high = max(open, close, low)
|
||||
df_clean['high'] = np.maximum.reduce([
|
||||
df_clean['high'],
|
||||
df_clean['open'],
|
||||
df_clean['close'],
|
||||
df_clean['low']
|
||||
])
|
||||
|
||||
# 如果low > min(open, close),则设置low = min(open, close, high)
|
||||
df_clean['low'] = np.minimum.reduce([
|
||||
df_clean['low'],
|
||||
df_clean['open'],
|
||||
df_clean['close'],
|
||||
df_clean['high']
|
||||
])
|
||||
|
||||
# 确保volume非负
|
||||
df_clean['volume'] = np.maximum(df_clean['volume'], 0)
|
||||
|
||||
# 按时间排序
|
||||
df_clean = df_clean.sort_index()
|
||||
|
||||
logger.info(f"数据清理完成,剩余 {len(df_clean)} 条记录")
|
||||
return df_clean
|
||||
|
||||
@staticmethod
|
||||
def add_technical_indicators(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
添加技术指标
|
||||
|
||||
Args:
|
||||
df: K线数据
|
||||
|
||||
Returns:
|
||||
包含技术指标的数据
|
||||
"""
|
||||
df_with_indicators = df.copy()
|
||||
|
||||
# 添加价格范围
|
||||
df_with_indicators['range'] = df_with_indicators['high'] - df_with_indicators['low']
|
||||
|
||||
# 添加实体大小
|
||||
df_with_indicators['body'] = abs(df_with_indicators['close'] - df_with_indicators['open'])
|
||||
|
||||
# 添加上影线长度
|
||||
df_with_indicators['upper_shadow'] = df_with_indicators['high'] - np.maximum(
|
||||
df_with_indicators['open'],
|
||||
df_with_indicators['close']
|
||||
)
|
||||
|
||||
# 添加下影线长度
|
||||
df_with_indicators['lower_shadow'] = np.minimum(
|
||||
df_with_indicators['open'],
|
||||
df_with_indicators['close']
|
||||
) - df_with_indicators['low']
|
||||
|
||||
# 添加K线方向
|
||||
df_with_indicators['direction'] = np.where(
|
||||
df_with_indicators['close'] > df_with_indicators['open'], 1, -1
|
||||
)
|
||||
|
||||
return df_with_indicators
|
||||
|
||||
@staticmethod
|
||||
def resample_klines(df: pd.DataFrame, new_timeframe: str) -> pd.DataFrame:
|
||||
"""
|
||||
重采样K线数据到新的时间周期
|
||||
|
||||
Args:
|
||||
df: 原始K线数据
|
||||
new_timeframe: 新的时间周期,如'4H', '1D'
|
||||
|
||||
Returns:
|
||||
重采样后的K线数据
|
||||
"""
|
||||
try:
|
||||
# 重采样规则
|
||||
agg_dict = {
|
||||
'open': 'first',
|
||||
'high': 'max',
|
||||
'low': 'min',
|
||||
'close': 'last',
|
||||
'volume': 'sum'
|
||||
}
|
||||
|
||||
# 执行重采样
|
||||
resampled = df.resample(new_timeframe).agg(agg_dict)
|
||||
|
||||
# 移除空值
|
||||
resampled = resampled.dropna()
|
||||
|
||||
logger.info(f"重采样到 {new_timeframe},得到 {len(resampled)} 条记录")
|
||||
return resampled
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"重采样失败: {e}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def calculate_returns(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
计算收益率
|
||||
|
||||
Args:
|
||||
df: K线数据
|
||||
|
||||
Returns:
|
||||
包含收益率的数据
|
||||
"""
|
||||
df_with_returns = df.copy()
|
||||
|
||||
# 计算收盘价收益率
|
||||
df_with_returns['returns'] = df_with_returns['close'].pct_change()
|
||||
|
||||
# 计算对数收益率
|
||||
df_with_returns['log_returns'] = np.log(df_with_returns['close'] / df_with_returns['close'].shift(1))
|
||||
|
||||
return df_with_returns
|
||||
|
||||
@staticmethod
|
||||
def get_data_summary(df: pd.DataFrame) -> dict:
|
||||
"""
|
||||
获取数据摘要信息
|
||||
|
||||
Args:
|
||||
df: K线数据
|
||||
|
||||
Returns:
|
||||
数据摘要字典
|
||||
"""
|
||||
summary = {
|
||||
'total_records': len(df),
|
||||
'date_range': {
|
||||
'start': df.index.min(),
|
||||
'end': df.index.max()
|
||||
},
|
||||
'price_range': {
|
||||
'min': df['low'].min(),
|
||||
'max': df['high'].max()
|
||||
},
|
||||
'volume_stats': {
|
||||
'total': df['volume'].sum(),
|
||||
'avg': df['volume'].mean(),
|
||||
'max': df['volume'].max()
|
||||
}
|
||||
}
|
||||
|
||||
if len(df) > 0:
|
||||
summary['latest_price'] = df['close'].iloc[-1]
|
||||
|
||||
return summary
|
||||
@@ -0,0 +1,156 @@
|
||||
# 未来数据使用问题修复说明
|
||||
|
||||
## 问题背景
|
||||
|
||||
在原始的买卖点计算系统中,存在使用未来数据的问题,这会导致:
|
||||
1. **回测结果偏乐观**:历史分析看起来比实际情况更好
|
||||
2. **实盘交易失效**:实际交易中无法获得未来信息
|
||||
3. **策略不可实施**:违反了时间序列分析的基本原则
|
||||
|
||||
## 发现的问题
|
||||
|
||||
### 1. 分型强度评分中的未来数据
|
||||
**位置**:`core/trading_signal.py` - `_calculate_fractal_strength_score`
|
||||
|
||||
**原始问题代码**:
|
||||
```python
|
||||
# 使用了当前分型之后的2个分型进行比较
|
||||
for i in range(max(0, index-2), min(len(all_fractals), index+3)): # ❌ index+3
|
||||
if i != index and all_fractals[i].fractal_type == fractal.fractal_type:
|
||||
nearby_fractals.append(all_fractals[i])
|
||||
```
|
||||
|
||||
**修复后代码**:
|
||||
```python
|
||||
# 只使用过去的分型进行比较
|
||||
for i in range(max(0, index-5), index): # ✅ 只看过去5个分型
|
||||
if i < len(all_fractals) and all_fractals[i].fractal_type == fractal.fractal_type:
|
||||
past_fractals.append(all_fractals[i])
|
||||
```
|
||||
|
||||
### 2. 分型位置评分中的未来数据
|
||||
**位置**:`core/trading_signal.py` - `_calculate_fractal_position_score`
|
||||
|
||||
**原始问题代码**:
|
||||
```python
|
||||
# 使用了分型时间点之后的10个K线
|
||||
start_idx = max(0, fractal_index - 10)
|
||||
end_idx = min(len(processed_klines), fractal_index + 10) # ❌ +10个未来K线
|
||||
```
|
||||
|
||||
**修复后代码**:
|
||||
```python
|
||||
# 只使用过去的数据
|
||||
start_idx = max(0, fractal_index - 20) # 只看过去20个K线
|
||||
end_idx = fractal_index + 1 # ✅ 包含当前K线,但不包含未来K线
|
||||
```
|
||||
|
||||
### 3. 分型基础检测的固有特性
|
||||
**位置**:`core/fractal.py` - 分型检测算法
|
||||
|
||||
**固有问题**:
|
||||
```python
|
||||
# 分型定义就需要左右确认
|
||||
for i in range(1, strength + 1):
|
||||
if self.data.iloc[idx + i]['high'] >= center_high: # 需要未来K线确认
|
||||
right_valid = False
|
||||
break
|
||||
```
|
||||
|
||||
**说明**:这是缠论分型的固有特性,无法避免,但我们增加了确认机制。
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 1. 修正评分算法
|
||||
- **分型强度比较**:只使用历史分型数据
|
||||
- **位置评分**:只基于过去的价格数据
|
||||
- **相对强度**:避免使用未来分型进行比较
|
||||
|
||||
### 2. 增加确认机制
|
||||
|
||||
**新增方法**:
|
||||
```python
|
||||
def update_fractal_confirmation(self, current_idx: int):
|
||||
"""更新分型确认状态"""
|
||||
for fractal in self.all_fractals:
|
||||
if not fractal.confirmed:
|
||||
required_confirmation = fractal.strength
|
||||
time_passed = current_idx - fractal.index
|
||||
|
||||
if time_passed >= required_confirmation:
|
||||
fractal.confirmed = True
|
||||
|
||||
def detect_real_time_fractals(self, current_idx: int, lookback_periods: int = 50):
|
||||
"""实时分型检测(避免使用未来数据)"""
|
||||
# 只检测到当前位置之前的分型
|
||||
# 且已经经过足够确认期的分型
|
||||
```
|
||||
|
||||
### 3. 实时交易适配
|
||||
|
||||
**确认延迟**:
|
||||
- 强度为1的分型:需要1个周期确认
|
||||
- 强度为2的分型:需要2个周期确认
|
||||
- 以此类推...
|
||||
|
||||
**信号生成时机**:
|
||||
- 历史分析:可以看到所有分型(用于分析和学习)
|
||||
- 实时交易:只能使用已确认的分型(确保策略可实施)
|
||||
|
||||
## 修复效果
|
||||
|
||||
### 1. 时间序列正确性
|
||||
- ✅ 所有计算只使用当前时间点之前的数据
|
||||
- ✅ 评分算法基于历史信息
|
||||
- ✅ 确认机制确保信号可靠性
|
||||
|
||||
### 2. 实盘交易可行性
|
||||
- ✅ 信号产生有合理延迟
|
||||
- ✅ 避免了"神奇"的前瞻性
|
||||
- ✅ 更加贴近真实交易环境
|
||||
|
||||
### 3. 回测准确性
|
||||
- ✅ 回测结果更加保守和真实
|
||||
- ✅ 消除了未来数据偏差
|
||||
- ✅ 提高了策略评估的可信度
|
||||
|
||||
## 使用建议
|
||||
|
||||
### 历史分析模式
|
||||
```python
|
||||
# 用于学习和研究
|
||||
analyzer = ChanAnalyzer(df)
|
||||
result = analyzer.run_full_analysis() # 包含所有分型
|
||||
```
|
||||
|
||||
### 实时交易模式
|
||||
```python
|
||||
# 用于实盘交易
|
||||
current_idx = len(df) - 1
|
||||
confirmed_fractals = fractal_detector.get_confirmed_fractals(current_idx)
|
||||
real_time_signals = trading_signal.detect_real_time_signals(confirmed_fractals)
|
||||
```
|
||||
|
||||
### 参数调整建议
|
||||
|
||||
**不同交易风格的设置**:
|
||||
- **日内短线**:基础强度1-2,确认周期短
|
||||
- **中线波段**:基础强度2-3,平衡确认和敏感性
|
||||
- **长线趋势**:基础强度3-5,重视确认可靠性
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **信号延迟**:修复后的系统会有合理的信号延迟,这是正确的
|
||||
2. **确认机制**:在实盘交易中必须等待分型确认
|
||||
3. **参数调整**:可能需要重新优化策略参数
|
||||
4. **回测重运行**:建议使用修复后的系统重新进行历史回测
|
||||
|
||||
## 总结
|
||||
|
||||
这次修复消除了系统中的"未来数据泄露"问题,使得:
|
||||
- 买卖点计算更加严格和可靠
|
||||
- 回测结果更加真实
|
||||
- 实盘交易策略更具可操作性
|
||||
- 符合量化交易的最佳实践
|
||||
|
||||
虽然可能会降低一些历史回测的表现,但这是为了获得更可靠和可实施的交易策略所必须的改进。
|
||||
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
缠论分析系统主程序入口
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from data.data_fetcher import DataFetcher
|
||||
from data.data_processor import DataProcessor
|
||||
from core.chan_analyzer import ChanAnalyzer
|
||||
from web.app import run_app
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout),
|
||||
logging.FileHandler('chan_analysis.log', encoding='utf-8')
|
||||
]
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run_command_line_analysis(args):
|
||||
"""运行命令行分析"""
|
||||
try:
|
||||
logger.info("开始命令行缠论分析...")
|
||||
|
||||
# 初始化组件
|
||||
data_fetcher = DataFetcher(args.exchange)
|
||||
data_processor = DataProcessor()
|
||||
|
||||
# 获取数据
|
||||
logger.info(f"获取 {args.symbol} {args.timeframe} 数据...")
|
||||
klines = data_fetcher.fetch_klines(
|
||||
symbol=args.symbol,
|
||||
timeframe=args.timeframe,
|
||||
limit=args.limit
|
||||
)
|
||||
|
||||
if klines.empty:
|
||||
logger.error("未获取到数据")
|
||||
return
|
||||
|
||||
# 数据处理
|
||||
logger.info("清理和验证数据...")
|
||||
if not data_processor.validate_klines(klines):
|
||||
logger.warning("数据验证失败,继续处理...")
|
||||
|
||||
klines = data_processor.clean_klines(klines)
|
||||
|
||||
# 缠论分析
|
||||
logger.info("开始缠论分析...")
|
||||
analyzer = ChanAnalyzer(klines)
|
||||
analysis_summary = analyzer.run_full_analysis(args.fractal_strength)
|
||||
|
||||
# 输出分析结果
|
||||
print("\n" + "="*50)
|
||||
print("缠论分析结果摘要")
|
||||
print("="*50)
|
||||
|
||||
# 数据信息
|
||||
data_info = analysis_summary['data_info']
|
||||
print(f"\n数据信息:")
|
||||
print(f" 原始K线数量: {data_info['original_klines']}")
|
||||
print(f" 处理后K线数量: {data_info['processed_klines']}")
|
||||
print(f" 数据时间范围: {data_info['date_range']['start']} 至 {data_info['date_range']['end']}")
|
||||
|
||||
# 分型信息
|
||||
fractal_info = analysis_summary['fractal_info']
|
||||
print(f"\n分型信息:")
|
||||
print(f" 总分型数量: {fractal_info['total']}")
|
||||
print(f" 顶分型: {fractal_info['top']}")
|
||||
print(f" 底分型: {fractal_info['bottom']}")
|
||||
|
||||
# 笔信息
|
||||
stroke_info = analysis_summary['stroke_info']
|
||||
print(f"\n笔信息:")
|
||||
print(f" 总笔数量: {stroke_info['total']}")
|
||||
print(f" 上升笔: {stroke_info['up']}")
|
||||
print(f" 下降笔: {stroke_info['down']}")
|
||||
|
||||
# 线段信息
|
||||
segment_info = analysis_summary['segment_info']
|
||||
print(f"\n线段信息:")
|
||||
print(f" 总线段数量: {segment_info['total']}")
|
||||
print(f" 上升线段: {segment_info['up']}")
|
||||
print(f" 下降线段: {segment_info['down']}")
|
||||
|
||||
# 中枢信息
|
||||
cb_info = analysis_summary['central_bank_info']
|
||||
print(f"\n中枢信息:")
|
||||
print(f" 总中枢数量: {cb_info['total']}")
|
||||
if cb_info['levels']:
|
||||
print(" 级别分布:")
|
||||
for level, count in cb_info['levels'].items():
|
||||
print(f" {level}: {count}个")
|
||||
|
||||
# 买卖点信息
|
||||
signal_info = analysis_summary['trading_signal_info']
|
||||
print(f"\n买卖点信息:")
|
||||
print(f" 总信号数量: {signal_info['total']}")
|
||||
print(f" 买点: {signal_info['buy_points']}")
|
||||
print(f" 卖点: {signal_info['sell_points']}")
|
||||
if signal_info['by_class']:
|
||||
print(" 类别分布:")
|
||||
for class_name, count in signal_info['by_class'].items():
|
||||
print(f" {class_name}: {count}个")
|
||||
|
||||
# 获取最新信号
|
||||
latest_signals = analyzer.get_latest_signals(24) # 最近24小时
|
||||
if latest_signals:
|
||||
print(f"\n最近24小时信号 ({len(latest_signals)}个):")
|
||||
for signal in latest_signals:
|
||||
signal_type = "买点" if signal.signal_type == 'buy' else "卖点"
|
||||
class_type = {"first": "一类", "second": "二类", "third": "三类"}.get(signal.point_class, "未知")
|
||||
print(f" {signal.timestamp.strftime('%m-%d %H:%M')} - {class_type}{signal_type} - 价格:{signal.price:.2f} - 强度:{signal.strength:.3f}")
|
||||
|
||||
# 当前市场结构
|
||||
market_structure = analyzer.get_current_market_structure()
|
||||
if market_structure:
|
||||
print(f"\n当前市场结构:")
|
||||
print(f" 当前价格: {market_structure.get('current_price', 'N/A')}")
|
||||
trend_map = {'upward': '上升', 'downward': '下降', 'sideways': '震荡', 'unclear': '不明'}
|
||||
print(f" 趋势方向: {trend_map.get(market_structure.get('trend'), '未知')}")
|
||||
phase_map = {'consolidation': '盘整', 'trending': '趋势', 'unknown': '未知'}
|
||||
print(f" 市场阶段: {phase_map.get(market_structure.get('market_phase'), '未知')}")
|
||||
|
||||
# 保存结果
|
||||
if args.export:
|
||||
export_results(analyzer, args)
|
||||
|
||||
logger.info("命令行分析完成")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"命令行分析失败: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def export_results(analyzer: ChanAnalyzer, args):
|
||||
"""导出分析结果"""
|
||||
try:
|
||||
results = analyzer.export_results()
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
export_dir = f"exports/chan_analysis_{args.symbol.replace('/', '_')}_{timestamp}"
|
||||
os.makedirs(export_dir, exist_ok=True)
|
||||
|
||||
# 保存各种分析结果
|
||||
for name, df in results.items():
|
||||
if not df.empty:
|
||||
filename = f"{export_dir}/{name}.csv"
|
||||
df.to_csv(filename, encoding='utf-8-sig')
|
||||
logger.info(f"导出 {name} 到 {filename}")
|
||||
|
||||
# 保存分析摘要
|
||||
with open(f"{export_dir}/summary.txt", 'w', encoding='utf-8') as f:
|
||||
f.write(f"缠论分析结果摘要\n")
|
||||
f.write(f"交易对: {args.symbol}\n")
|
||||
f.write(f"时间周期: {args.timeframe}\n")
|
||||
f.write(f"分析时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||
f.write(f"数据数量: {args.limit}\n")
|
||||
f.write(f"分型强度: {args.fractal_strength}\n")
|
||||
|
||||
logger.info(f"分析结果已导出到 {export_dir}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"导出结果失败: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
"""主程序入口"""
|
||||
parser = argparse.ArgumentParser(description='缠论分析系统')
|
||||
|
||||
# 运行模式
|
||||
parser.add_argument('--mode', choices=['web', 'cli'], default='web',
|
||||
help='运行模式:web(Web界面) 或 cli(命令行)')
|
||||
|
||||
# 数据参数
|
||||
parser.add_argument('--symbol', default='BTC/USDT',
|
||||
help='交易对 (默认: BTC/USDT)')
|
||||
parser.add_argument('--timeframe', default='1h',
|
||||
help='时间周期 (默认: 1h)')
|
||||
parser.add_argument('--limit', type=int, default=500,
|
||||
help='获取的K线数量 (默认: 500)')
|
||||
parser.add_argument('--exchange', default='binance',
|
||||
help='交易所 (默认: binance)')
|
||||
|
||||
# 分析参数
|
||||
parser.add_argument('--fractal-strength', type=int, default=1,
|
||||
help='分型强度要求 (默认: 1)')
|
||||
|
||||
# 输出参数
|
||||
parser.add_argument('--export', action='store_true',
|
||||
help='是否导出分析结果')
|
||||
|
||||
# Web参数
|
||||
parser.add_argument('--port', type=int, default=8050,
|
||||
help='Web服务端口 (默认: 8050)')
|
||||
parser.add_argument('--debug', action='store_true',
|
||||
help='是否启用调试模式')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
logger.info("缠论分析系统启动")
|
||||
logger.info(f"运行模式: {args.mode}")
|
||||
|
||||
try:
|
||||
if args.mode == 'web':
|
||||
logger.info(f"启动Web服务,端口: {args.port}")
|
||||
print(f"\n缠论分析系统Web界面")
|
||||
print(f"访问地址: http://localhost:{args.port}")
|
||||
print("按 Ctrl+C 停止服务\n")
|
||||
run_app(debug=args.debug, port=args.port)
|
||||
else:
|
||||
run_command_line_analysis(args)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("用户中断,程序退出")
|
||||
except Exception as e:
|
||||
logger.error(f"程序执行失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
缠论分析系统快速启动脚本
|
||||
用于快速演示系统功能
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from data.data_fetcher import DataFetcher
|
||||
from data.data_processor import DataProcessor
|
||||
from core.chan_analyzer import ChanAnalyzer
|
||||
|
||||
|
||||
def quick_demo():
|
||||
"""快速演示"""
|
||||
print("=" * 60)
|
||||
print("缠论分析系统快速演示")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
print("正在初始化组件...")
|
||||
|
||||
# 初始化数据获取器
|
||||
data_fetcher = DataFetcher()
|
||||
data_processor = DataProcessor()
|
||||
|
||||
print("正在获取BTC/USDT 1小时数据...")
|
||||
|
||||
# 获取数据
|
||||
klines = data_fetcher.fetch_klines('BTC/USDT', '1h', 200)
|
||||
|
||||
if klines.empty:
|
||||
print("❌ 数据获取失败,可能是网络问题")
|
||||
return False
|
||||
|
||||
print(f"✅ 成功获取 {len(klines)} 条K线数据")
|
||||
print(f"数据时间范围: {klines.index[0]} 至 {klines.index[-1]}")
|
||||
|
||||
# 数据处理
|
||||
print("\n正在进行数据清理和验证...")
|
||||
klines = data_processor.clean_klines(klines)
|
||||
|
||||
# 缠论分析
|
||||
print("正在进行缠论分析...")
|
||||
analyzer = ChanAnalyzer(klines)
|
||||
summary = analyzer.run_full_analysis(fractal_strength=1)
|
||||
|
||||
# 显示结果
|
||||
print("\n" + "=" * 60)
|
||||
print("缠论分析结果")
|
||||
print("=" * 60)
|
||||
|
||||
# 基础统计
|
||||
data_info = summary['data_info']
|
||||
print(f"\n📊 数据统计:")
|
||||
print(f" 原始K线: {data_info['original_klines']} 条")
|
||||
print(f" 处理后: {data_info['processed_klines']} 条")
|
||||
|
||||
# 分型统计
|
||||
fractal_info = summary['fractal_info']
|
||||
print(f"\n🔺 分型统计:")
|
||||
print(f" 总计: {fractal_info['total']} 个")
|
||||
print(f" 顶分型: {fractal_info['top']} 个")
|
||||
print(f" 底分型: {fractal_info['bottom']} 个")
|
||||
|
||||
# 笔统计
|
||||
stroke_info = summary['stroke_info']
|
||||
print(f"\n📏 笔统计:")
|
||||
print(f" 总计: {stroke_info['total']} 个")
|
||||
print(f" 上升笔: {stroke_info['up']} 个")
|
||||
print(f" 下降笔: {stroke_info['down']} 个")
|
||||
|
||||
# 线段统计
|
||||
segment_info = summary['segment_info']
|
||||
print(f"\n📈 线段统计:")
|
||||
print(f" 总计: {segment_info['total']} 个")
|
||||
print(f" 上升线段: {segment_info['up']} 个")
|
||||
print(f" 下降线段: {segment_info['down']} 个")
|
||||
|
||||
# 中枢统计
|
||||
cb_info = summary['central_bank_info']
|
||||
print(f"\n🎯 中枢统计:")
|
||||
print(f" 总计: {cb_info['total']} 个")
|
||||
if cb_info['levels']:
|
||||
for level, count in cb_info['levels'].items():
|
||||
print(f" {level}: {count} 个")
|
||||
|
||||
# 买卖点统计
|
||||
signal_info = summary['trading_signal_info']
|
||||
print(f"\n💰 买卖点统计:")
|
||||
print(f" 总计: {signal_info['total']} 个")
|
||||
print(f" 买点: {signal_info['buy_points']} 个")
|
||||
print(f" 卖点: {signal_info['sell_points']} 个")
|
||||
|
||||
# 最新信号
|
||||
latest_signals = analyzer.get_latest_signals(48) # 最近48小时
|
||||
if latest_signals:
|
||||
print(f"\n🚨 最近48小时信号 ({len(latest_signals)} 个):")
|
||||
for signal in latest_signals[-5:]: # 最多显示5个
|
||||
signal_type = "🟢买点" if signal.signal_type == 'buy' else "🔴卖点"
|
||||
class_map = {"first": "一类", "second": "二类", "third": "三类"}
|
||||
class_type = class_map.get(signal.point_class, "未知")
|
||||
time_str = signal.timestamp.strftime('%m-%d %H:%M')
|
||||
print(f" {time_str} | {class_type}{signal_type} | 价格: ${signal.price:,.2f} | 强度: {signal.strength:.3f}")
|
||||
|
||||
# 当前市场结构
|
||||
market_structure = analyzer.get_current_market_structure()
|
||||
if market_structure:
|
||||
print(f"\n📊 当前市场结构:")
|
||||
current_price = market_structure.get('current_price', 0)
|
||||
print(f" 当前价格: ${current_price:,.2f}")
|
||||
|
||||
trend_map = {
|
||||
'upward': '🟢 上升趋势',
|
||||
'downward': '🔴 下降趋势',
|
||||
'sideways': '🟡 震荡趋势',
|
||||
'unclear': '⚪ 趋势不明'
|
||||
}
|
||||
trend = trend_map.get(market_structure.get('trend'), '未知')
|
||||
print(f" 趋势方向: {trend}")
|
||||
|
||||
phase_map = {
|
||||
'consolidation': '📦 盘整阶段',
|
||||
'trending': '📈 趋势阶段',
|
||||
'unknown': '❓ 未知阶段'
|
||||
}
|
||||
phase = phase_map.get(market_structure.get('market_phase'), '未知')
|
||||
print(f" 市场阶段: {phase}")
|
||||
|
||||
# 支撑阻力
|
||||
sr = market_structure.get('support_resistance', {})
|
||||
if sr.get('support_levels'):
|
||||
supports = [f"${s:,.2f}" for s in sr['support_levels'][:3]]
|
||||
print(f" 关键支撑: {', '.join(supports)}")
|
||||
if sr.get('resistance_levels'):
|
||||
resistances = [f"${r:,.2f}" for r in sr['resistance_levels'][:3]]
|
||||
print(f" 关键阻力: {', '.join(resistances)}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("分析完成!")
|
||||
print("=" * 60)
|
||||
|
||||
# 启动Web界面提示
|
||||
print(f"\n💡 想要查看可视化图表?运行以下命令启动Web界面:")
|
||||
print(f" python main.py --mode web")
|
||||
print(f" 然后在浏览器中访问 http://localhost:8050")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 演示失败: {e}")
|
||||
print("可能的原因:")
|
||||
print("1. 网络连接问题,无法获取数据")
|
||||
print("2. 缺少必要的依赖包")
|
||||
print("3. 交易所API限制")
|
||||
print("\n解决方案:")
|
||||
print("1. 检查网络连接")
|
||||
print("2. 运行: pip install -r requirements.txt")
|
||||
print("3. 稍后重试")
|
||||
return False
|
||||
|
||||
|
||||
def check_dependencies():
|
||||
"""检查依赖包"""
|
||||
required_packages = [
|
||||
'ccxt', 'pandas', 'numpy', 'plotly', 'dash'
|
||||
]
|
||||
|
||||
missing_packages = []
|
||||
|
||||
for package in required_packages:
|
||||
try:
|
||||
__import__(package)
|
||||
except ImportError:
|
||||
missing_packages.append(package)
|
||||
|
||||
if missing_packages:
|
||||
print("❌ 缺少以下依赖包:")
|
||||
for pkg in missing_packages:
|
||||
print(f" - {pkg}")
|
||||
print("\n请运行以下命令安装:")
|
||||
print("pip install -r requirements.txt")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("正在检查依赖包...")
|
||||
|
||||
if not check_dependencies():
|
||||
sys.exit(1)
|
||||
|
||||
print("✅ 依赖包检查通过")
|
||||
|
||||
if quick_demo():
|
||||
print("\n🎉 演示成功完成!")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("\n❌ 演示失败")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,37 @@
|
||||
# 缠论分析系统依赖包
|
||||
|
||||
# 数据获取和处理
|
||||
ccxt>=4.0.0
|
||||
pandas>=1.5.0
|
||||
numpy>=1.24.0
|
||||
|
||||
# 可视化
|
||||
plotly>=5.15.0
|
||||
dash>=2.10.0
|
||||
|
||||
# Web框架
|
||||
flask>=2.3.0
|
||||
|
||||
# 日期时间处理
|
||||
python-dateutil>=2.8.0
|
||||
|
||||
# 日志
|
||||
colorlog>=6.7.0
|
||||
|
||||
# 科学计算
|
||||
scipy>=1.10.0
|
||||
|
||||
# 数据结构
|
||||
dataclasses>=0.6.0; python_version<'3.7'
|
||||
|
||||
# 类型提示
|
||||
typing-extensions>=4.0.0
|
||||
|
||||
# 开发工具(可选)
|
||||
jupyter>=1.0.0
|
||||
matplotlib>=3.7.0
|
||||
seaborn>=0.12.0
|
||||
|
||||
# 测试(可选)
|
||||
pytest>=7.0.0
|
||||
pytest-cov>=4.0.0
|
||||
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
基础功能测试
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from data.data_processor import DataProcessor
|
||||
from core.kline import KLine
|
||||
from core.fractal import Fractal
|
||||
from core.chan_analyzer import ChanAnalyzer
|
||||
|
||||
|
||||
def create_test_data():
|
||||
"""创建测试用的K线数据"""
|
||||
dates = pd.date_range(start='2024-01-01', periods=100, freq='H')
|
||||
|
||||
# 生成模拟价格数据
|
||||
np.random.seed(42)
|
||||
base_price = 50000
|
||||
prices = [base_price]
|
||||
|
||||
for i in range(99):
|
||||
change = np.random.normal(0, 100) # 价格变化
|
||||
new_price = max(prices[-1] + change, 1000) # 确保价格为正
|
||||
prices.append(new_price)
|
||||
|
||||
# 生成OHLCV数据
|
||||
data = []
|
||||
for i, date in enumerate(dates):
|
||||
if i == 0:
|
||||
open_price = prices[i]
|
||||
else:
|
||||
open_price = data[-1]['close']
|
||||
|
||||
close_price = prices[i]
|
||||
high_price = max(open_price, close_price) + np.random.uniform(0, 50)
|
||||
low_price = min(open_price, close_price) - np.random.uniform(0, 50)
|
||||
volume = np.random.uniform(1000, 10000)
|
||||
|
||||
data.append({
|
||||
'open': open_price,
|
||||
'high': high_price,
|
||||
'low': low_price,
|
||||
'close': close_price,
|
||||
'volume': volume
|
||||
})
|
||||
|
||||
df = pd.DataFrame(data, index=dates)
|
||||
return df
|
||||
|
||||
|
||||
def test_data_processor():
|
||||
"""测试数据处理器"""
|
||||
print("测试数据处理器...")
|
||||
|
||||
# 创建测试数据
|
||||
test_data = create_test_data()
|
||||
|
||||
# 验证数据
|
||||
processor = DataProcessor()
|
||||
is_valid = processor.validate_klines(test_data)
|
||||
print(f"数据验证结果: {is_valid}")
|
||||
|
||||
# 清理数据
|
||||
cleaned_data = processor.clean_klines(test_data)
|
||||
print(f"清理后数据量: {len(cleaned_data)}")
|
||||
|
||||
# 添加技术指标
|
||||
data_with_indicators = processor.add_technical_indicators(cleaned_data)
|
||||
print(f"技术指标列: {list(data_with_indicators.columns)}")
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
def test_kline_processor():
|
||||
"""测试K线处理器"""
|
||||
print("\n测试K线处理器...")
|
||||
|
||||
test_data = create_test_data()
|
||||
|
||||
# K线包含关系处理
|
||||
kline_processor = KLine(test_data)
|
||||
processed_data = kline_processor.get_processed_data()
|
||||
|
||||
print(f"原始K线数量: {len(test_data)}")
|
||||
print(f"处理后K线数量: {len(processed_data)}")
|
||||
|
||||
# 可视化信息
|
||||
viz_info = kline_processor.visualize_containment()
|
||||
print(f"合并统计: {viz_info}")
|
||||
|
||||
return processed_data
|
||||
|
||||
|
||||
def test_fractal_detector():
|
||||
"""测试分型识别器"""
|
||||
print("\n测试分型识别器...")
|
||||
|
||||
processed_data = test_kline_processor()
|
||||
|
||||
# 分型识别
|
||||
fractal_detector = Fractal(processed_data, min_strength=1)
|
||||
fractals = fractal_detector.detect_fractals()
|
||||
|
||||
print(f"检测到分型数量: {len(fractals)}")
|
||||
|
||||
# 分型统计
|
||||
stats = fractal_detector.get_fractal_statistics()
|
||||
print(f"分型统计: {stats}")
|
||||
|
||||
return fractals
|
||||
|
||||
|
||||
def test_chan_analyzer():
|
||||
"""测试综合分析器"""
|
||||
print("\n测试综合分析器...")
|
||||
|
||||
test_data = create_test_data()
|
||||
|
||||
# 完整分析
|
||||
analyzer = ChanAnalyzer(test_data)
|
||||
summary = analyzer.run_full_analysis(fractal_strength=1)
|
||||
|
||||
print("分析结果摘要:")
|
||||
for key, value in summary.items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
# 获取最新信号
|
||||
latest_signals = analyzer.get_latest_signals(24)
|
||||
print(f"\n最新信号数量: {len(latest_signals)}")
|
||||
|
||||
# 市场结构
|
||||
market_structure = analyzer.get_current_market_structure()
|
||||
print(f"当前市场结构: {market_structure}")
|
||||
|
||||
return analyzer
|
||||
|
||||
|
||||
def run_all_tests():
|
||||
"""运行所有测试"""
|
||||
print("=" * 50)
|
||||
print("缠论分析系统基础功能测试")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
# 测试各个模块
|
||||
test_data_processor()
|
||||
test_kline_processor()
|
||||
test_fractal_detector()
|
||||
analyzer = test_chan_analyzer()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("所有测试完成!")
|
||||
print("=" * 50)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = run_all_tests()
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
Web可视化模块:使用Dash创建交互式缠论分析界面
|
||||
"""
|
||||
|
||||
from .app import create_app
|
||||
from .visualization import ChanVisualizer
|
||||
|
||||
__all__ = ['create_app', 'ChanVisualizer']
|
||||
+614
@@ -0,0 +1,614 @@
|
||||
"""
|
||||
Dash Web应用:提供交互式缠论分析界面
|
||||
"""
|
||||
|
||||
import dash
|
||||
from dash import dcc, html, Input, Output, State, callback_context
|
||||
import plotly.graph_objects as go
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
from data.data_fetcher import DataFetcher
|
||||
from data.data_processor import DataProcessor
|
||||
from core.chan_analyzer import ChanAnalyzer
|
||||
from .visualization import ChanVisualizer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_app():
|
||||
"""创建Dash应用"""
|
||||
app = dash.Dash(__name__)
|
||||
|
||||
# 初始化组件
|
||||
data_fetcher = DataFetcher()
|
||||
data_processor = DataProcessor()
|
||||
visualizer = ChanVisualizer()
|
||||
|
||||
# 应用布局
|
||||
app.layout = html.Div([
|
||||
# 标题
|
||||
html.H1("缠论分析系统", className="text-center mb-4"),
|
||||
|
||||
# 控制面板
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Label("交易对:"),
|
||||
dcc.Dropdown(
|
||||
id='symbol-dropdown',
|
||||
options=[
|
||||
{'label': 'BTC/USDT', 'value': 'BTC/USDT'},
|
||||
{'label': 'ETH/USDT', 'value': 'ETH/USDT'},
|
||||
{'label': 'BNB/USDT', 'value': 'BNB/USDT'},
|
||||
{'label': 'ADA/USDT', 'value': 'ADA/USDT'},
|
||||
{'label': 'SOL/USDT', 'value': 'SOL/USDT'}
|
||||
],
|
||||
value='BTC/USDT',
|
||||
className="mb-3"
|
||||
)
|
||||
], className="col-md-2"),
|
||||
|
||||
html.Div([
|
||||
html.Label("时间周期:"),
|
||||
dcc.Dropdown(
|
||||
id='timeframe-dropdown',
|
||||
options=[
|
||||
{'label': '1分钟', 'value': '1m'},
|
||||
{'label': '5分钟', 'value': '5m'},
|
||||
{'label': '15分钟', 'value': '15m'},
|
||||
{'label': '30分钟', 'value': '30m'},
|
||||
{'label': '1小时', 'value': '1h'},
|
||||
{'label': '4小时', 'value': '4h'},
|
||||
{'label': '1天', 'value': '1d'}
|
||||
],
|
||||
value='1h',
|
||||
className="form-select"
|
||||
)
|
||||
], className="col-md-2"),
|
||||
|
||||
html.Div([
|
||||
html.Label("数据数量:"),
|
||||
dcc.Slider(
|
||||
id='limit-slider',
|
||||
min=100,
|
||||
max=1000,
|
||||
step=50,
|
||||
value=500,
|
||||
marks={i: str(i) for i in range(100, 1001, 200)},
|
||||
className="mb-3"
|
||||
)
|
||||
], className="col-md-2"),
|
||||
|
||||
html.Div([
|
||||
html.Label("基础分型强度:"),
|
||||
dcc.Slider(
|
||||
id='fractal-strength-slider',
|
||||
min=1,
|
||||
max=5,
|
||||
step=1,
|
||||
value=1,
|
||||
marks={i: str(i) for i in range(1, 6)},
|
||||
className="mb-3"
|
||||
)
|
||||
], className="col-md-2"),
|
||||
|
||||
html.Div([
|
||||
html.Label("增强强度过滤:"),
|
||||
dcc.Slider(
|
||||
id='enhanced-strength-filter',
|
||||
min=0,
|
||||
max=100,
|
||||
step=10,
|
||||
value=0,
|
||||
marks={i: str(i) for i in range(0, 101, 20)},
|
||||
className="mb-3"
|
||||
)
|
||||
], className="col-md-2"),
|
||||
|
||||
html.Div([
|
||||
html.Label("显示级别:"),
|
||||
dcc.Dropdown(
|
||||
id='display-level-dropdown',
|
||||
options=[
|
||||
{'label': '全部分型', 'value': 'all'},
|
||||
{'label': '仅强势(≥70分)', 'value': 'strong'},
|
||||
{'label': '中等以上(≥40分)', 'value': 'medium_plus'},
|
||||
{'label': '自定义过滤', 'value': 'custom'}
|
||||
],
|
||||
value='all',
|
||||
className="form-select"
|
||||
)
|
||||
], className="col-md-2")
|
||||
], className="row mb-4"),
|
||||
|
||||
# 按钮组
|
||||
html.Div([
|
||||
html.Button("获取数据并分析", id="analyze-btn",
|
||||
className="btn btn-primary me-2"),
|
||||
html.Button("刷新数据", id="refresh-btn",
|
||||
className="btn btn-secondary me-2"),
|
||||
html.Button("导出结果", id="export-btn",
|
||||
className="btn btn-success"),
|
||||
], className="text-center mb-4"),
|
||||
|
||||
# 加载状态
|
||||
dcc.Loading(
|
||||
id="loading",
|
||||
children=[
|
||||
# 主图表
|
||||
html.Div([
|
||||
dcc.Graph(id='main-chart', style={'height': '800px'})
|
||||
], className="mb-4"),
|
||||
|
||||
# 统计信息
|
||||
html.Div([
|
||||
html.H3("分析统计", className="mb-3"),
|
||||
html.Div(id='statistics-content')
|
||||
], className="mb-4"),
|
||||
|
||||
# 市场结构
|
||||
html.Div([
|
||||
html.H3("当前市场结构", className="mb-3"),
|
||||
html.Div(id='market-structure-content')
|
||||
], className="mb-4"),
|
||||
|
||||
# 最新信号
|
||||
html.Div([
|
||||
html.H3("最新买卖点信号", className="mb-3"),
|
||||
html.Div(id='latest-signals-content')
|
||||
])
|
||||
]
|
||||
),
|
||||
|
||||
# 存储数据
|
||||
dcc.Store(id='analysis-data'),
|
||||
dcc.Store(id='market-structure-data')
|
||||
], className="container-fluid p-4")
|
||||
|
||||
# 回调函数
|
||||
@app.callback(
|
||||
[Output('analysis-data', 'data'),
|
||||
Output('market-structure-data', 'data')],
|
||||
[Input('analyze-btn', 'n_clicks'),
|
||||
Input('refresh-btn', 'n_clicks')],
|
||||
[State('symbol-dropdown', 'value'),
|
||||
State('timeframe-dropdown', 'value'),
|
||||
State('limit-slider', 'value'),
|
||||
State('fractal-strength-slider', 'value'),
|
||||
State('enhanced-strength-filter', 'value'),
|
||||
State('display-level-dropdown', 'value')]
|
||||
)
|
||||
def analyze_data(analyze_clicks, refresh_clicks, symbol, timeframe, limit, fractal_strength, enhanced_strength_filter, display_level):
|
||||
if not analyze_clicks and not refresh_clicks:
|
||||
return None, None
|
||||
|
||||
try:
|
||||
# 获取数据
|
||||
fetcher = DataFetcher()
|
||||
df = fetcher.fetch_klines(symbol=symbol, timeframe=timeframe, limit=limit)
|
||||
|
||||
if df.empty:
|
||||
return None, None
|
||||
|
||||
# 进行缠论分析 - 使用正确的初始化方式
|
||||
analyzer = ChanAnalyzer(df) # 传入原始DataFrame
|
||||
result = analyzer.run_full_analysis(fractal_strength=fractal_strength)
|
||||
|
||||
# 重置索引,确保timestamp列存在,避免列名重复
|
||||
df_reset = df.reset_index()
|
||||
if 'timestamp' in df_reset.columns:
|
||||
df_reset = df_reset.drop(columns=['timestamp']) # 删除可能重复的timestamp列
|
||||
df_reset.rename(columns={'datetime': 'timestamp'}, inplace=True)
|
||||
|
||||
# 获取缠论分析的详细结果
|
||||
fractals_data = []
|
||||
if hasattr(analyzer, 'fractals') and analyzer.fractals:
|
||||
for f in analyzer.fractals:
|
||||
fractals_data.append({
|
||||
'timestamp': f.timestamp.isoformat() if hasattr(f.timestamp, 'isoformat') else str(f.timestamp),
|
||||
'price': float(f.price),
|
||||
'type': f.fractal_type,
|
||||
'strength': f.strength,
|
||||
'enhanced_strength': getattr(f, 'enhanced_strength', 0),
|
||||
'price_dominance': getattr(f, 'price_dominance', 0),
|
||||
'volume_strength': getattr(f, 'volume_strength', 0),
|
||||
'trend_position': getattr(f, 'trend_position', 0)
|
||||
})
|
||||
|
||||
strokes_data = []
|
||||
if hasattr(analyzer, 'strokes') and analyzer.strokes:
|
||||
for s in analyzer.strokes:
|
||||
strokes_data.append({
|
||||
'start_time': s.start_fractal.timestamp.isoformat() if hasattr(s.start_fractal.timestamp, 'isoformat') else str(s.start_fractal.timestamp),
|
||||
'end_time': s.end_fractal.timestamp.isoformat() if hasattr(s.end_fractal.timestamp, 'isoformat') else str(s.end_fractal.timestamp),
|
||||
'start_price': float(s.start_fractal.price),
|
||||
'end_price': float(s.end_fractal.price),
|
||||
'direction': s.direction
|
||||
})
|
||||
|
||||
central_banks_data = []
|
||||
if hasattr(analyzer, 'central_banks') and analyzer.central_banks:
|
||||
for cb in analyzer.central_banks:
|
||||
central_banks_data.append({
|
||||
'start_time': cb.start_time.isoformat() if hasattr(cb.start_time, 'isoformat') else str(cb.start_time),
|
||||
'end_time': cb.end_time.isoformat() if hasattr(cb.end_time, 'isoformat') else str(cb.end_time),
|
||||
'high_price': float(cb.high_price),
|
||||
'low_price': float(cb.low_price),
|
||||
'center_price': float(cb.center_price)
|
||||
})
|
||||
|
||||
trading_points_data = []
|
||||
if hasattr(analyzer, 'trading_points') and analyzer.trading_points:
|
||||
for tp in analyzer.trading_points:
|
||||
trading_points_data.append({
|
||||
'timestamp': tp.timestamp.isoformat() if hasattr(tp.timestamp, 'isoformat') else str(tp.timestamp),
|
||||
'price': float(tp.price),
|
||||
'signal_type': tp.signal_type,
|
||||
'point_class': tp.point_class,
|
||||
'description': tp.description
|
||||
})
|
||||
|
||||
# 序列化分析结果为简单的字典格式
|
||||
analysis_data = {
|
||||
'df': df_reset.to_dict('records'),
|
||||
'fractals': fractals_data,
|
||||
'strokes': strokes_data,
|
||||
'central_banks': central_banks_data,
|
||||
'trading_points': trading_points_data,
|
||||
'processed_klines_count': result['data_info']['processed_klines'],
|
||||
'fractals_count': result['fractal_info']['total'],
|
||||
'strokes_count': result['stroke_info']['total'],
|
||||
'segments_count': result['segment_info']['total'],
|
||||
'central_banks_count': result['central_bank_info']['total'],
|
||||
'trading_points_count': result['trading_signal_info']['total'],
|
||||
'symbol': symbol,
|
||||
'timeframe': timeframe
|
||||
}
|
||||
|
||||
# 市场结构数据
|
||||
market_data = {
|
||||
'latest_price': float(df['close'].iloc[-1]) if not df.empty else 0,
|
||||
'price_change': float(df['close'].iloc[-1] - df['close'].iloc[0]) if len(df) > 1 else 0,
|
||||
'volume_avg': float(df['volume'].mean()) if not df.empty else 0,
|
||||
'high_24h': float(df['high'].max()) if not df.empty else 0,
|
||||
'low_24h': float(df['low'].min()) if not df.empty else 0
|
||||
}
|
||||
|
||||
return analysis_data, market_data
|
||||
|
||||
except Exception as e:
|
||||
print(f"分析数据时出错: {str(e)}")
|
||||
return None, None
|
||||
|
||||
@app.callback(
|
||||
Output('main-chart', 'figure'),
|
||||
[Input('analysis-data', 'data'),
|
||||
Input('enhanced-strength-filter', 'value'),
|
||||
Input('display-level-dropdown', 'value')]
|
||||
)
|
||||
def update_main_chart(analysis_data, enhanced_strength_filter, display_level):
|
||||
if not analysis_data or not analysis_data.get('df'):
|
||||
return go.Figure()
|
||||
|
||||
# 从数据创建基本K线图
|
||||
df_records = analysis_data['df']
|
||||
df = pd.DataFrame(df_records)
|
||||
df['timestamp'] = pd.to_datetime(df['timestamp'])
|
||||
|
||||
fig = go.Figure()
|
||||
|
||||
# 添加K线图
|
||||
fig.add_trace(go.Candlestick(
|
||||
x=df['timestamp'],
|
||||
open=df['open'],
|
||||
high=df['high'],
|
||||
low=df['low'],
|
||||
close=df['close'],
|
||||
name="K线"
|
||||
))
|
||||
|
||||
# 添加分型点(带过滤功能)
|
||||
if analysis_data.get('fractals'):
|
||||
fractals = analysis_data['fractals']
|
||||
|
||||
# 根据显示级别和增强强度过滤分型
|
||||
filtered_fractals = []
|
||||
for f in fractals:
|
||||
enhanced_strength = f.get('enhanced_strength', 0)
|
||||
|
||||
# 应用显示级别过滤
|
||||
if display_level == 'strong' and enhanced_strength < 70:
|
||||
continue
|
||||
elif display_level == 'medium_plus' and enhanced_strength < 40:
|
||||
continue
|
||||
elif display_level == 'custom' and enhanced_strength < enhanced_strength_filter:
|
||||
continue
|
||||
|
||||
filtered_fractals.append(f)
|
||||
|
||||
top_fractals = [f for f in filtered_fractals if f['type'] == 'top']
|
||||
bottom_fractals = [f for f in filtered_fractals if f['type'] == 'bottom']
|
||||
|
||||
if top_fractals:
|
||||
# 根据增强强度确定大小和颜色
|
||||
sizes = [max(8, min(f.get('enhanced_strength', 30) / 5, 20)) for f in top_fractals]
|
||||
colors = [f'rgba(255, {max(0, 255 - int(f.get("enhanced_strength", 30) * 2))}, 0, 0.8)' for f in top_fractals]
|
||||
|
||||
fig.add_trace(go.Scatter(
|
||||
x=[pd.to_datetime(f['timestamp']) for f in top_fractals],
|
||||
y=[f['price'] for f in top_fractals],
|
||||
mode='markers',
|
||||
marker=dict(
|
||||
symbol='triangle-down',
|
||||
size=sizes,
|
||||
color=colors,
|
||||
line=dict(color='darkred', width=1)
|
||||
),
|
||||
name=f'顶分型({len(top_fractals)}个)',
|
||||
hovertemplate=('顶分型<br>'
|
||||
'时间: %{x}<br>'
|
||||
'价格: %{y:.2f}<br>'
|
||||
'基础强度: %{customdata[0]}<br>'
|
||||
'增强强度: %{customdata[1]:.1f}<br>'
|
||||
'价格优势: %{customdata[2]:.1f}<br>'
|
||||
'成交量强度: %{customdata[3]:.1f}<br>'
|
||||
'趋势位置: %{customdata[4]:.1f}<extra></extra>'),
|
||||
customdata=[[f['strength'],
|
||||
f.get('enhanced_strength', 0),
|
||||
f.get('price_dominance', 0),
|
||||
f.get('volume_strength', 0),
|
||||
f.get('trend_position', 0)] for f in top_fractals]
|
||||
))
|
||||
|
||||
if bottom_fractals:
|
||||
# 根据增强强度确定大小和颜色
|
||||
sizes = [max(8, min(f.get('enhanced_strength', 30) / 5, 20)) for f in bottom_fractals]
|
||||
colors = [f'rgba(0, {max(100, 255 - int(f.get("enhanced_strength", 30) * 1.5))}, 0, 0.8)' for f in bottom_fractals]
|
||||
|
||||
fig.add_trace(go.Scatter(
|
||||
x=[pd.to_datetime(f['timestamp']) for f in bottom_fractals],
|
||||
y=[f['price'] for f in bottom_fractals],
|
||||
mode='markers',
|
||||
marker=dict(
|
||||
symbol='triangle-up',
|
||||
size=sizes,
|
||||
color=colors,
|
||||
line=dict(color='darkgreen', width=1)
|
||||
),
|
||||
name=f'底分型({len(bottom_fractals)}个)',
|
||||
hovertemplate=('底分型<br>'
|
||||
'时间: %{x}<br>'
|
||||
'价格: %{y:.2f}<br>'
|
||||
'基础强度: %{customdata[0]}<br>'
|
||||
'增强强度: %{customdata[1]:.1f}<br>'
|
||||
'价格优势: %{customdata[2]:.1f}<br>'
|
||||
'成交量强度: %{customdata[3]:.1f}<br>'
|
||||
'趋势位置: %{customdata[4]:.1f}<extra></extra>'),
|
||||
customdata=[[f['strength'],
|
||||
f.get('enhanced_strength', 0),
|
||||
f.get('price_dominance', 0),
|
||||
f.get('volume_strength', 0),
|
||||
f.get('trend_position', 0)] for f in bottom_fractals]
|
||||
))
|
||||
|
||||
# 添加笔
|
||||
if analysis_data.get('strokes'):
|
||||
strokes = analysis_data['strokes']
|
||||
for i, stroke in enumerate(strokes):
|
||||
color = 'blue' if stroke['direction'] == 1 else 'purple'
|
||||
fig.add_trace(go.Scatter(
|
||||
x=[pd.to_datetime(stroke['start_time']), pd.to_datetime(stroke['end_time'])],
|
||||
y=[stroke['start_price'], stroke['end_price']],
|
||||
mode='lines',
|
||||
line=dict(color=color, width=2),
|
||||
name='笔' if i == 0 else None,
|
||||
showlegend=(i == 0),
|
||||
hovertemplate=f'笔 {"↗" if stroke["direction"] == 1 else "↘"}<br>起点: %{{x[0]}}<br>终点: %{{x[1]}}<br>价格变化: {stroke["end_price"] - stroke["start_price"]:.2f}<extra></extra>'
|
||||
))
|
||||
|
||||
# 添加中枢
|
||||
if analysis_data.get('central_banks'):
|
||||
central_banks = analysis_data['central_banks']
|
||||
for i, cb in enumerate(central_banks):
|
||||
# 中枢区域用矩形表示
|
||||
fig.add_shape(
|
||||
type="rect",
|
||||
x0=pd.to_datetime(cb['start_time']),
|
||||
x1=pd.to_datetime(cb['end_time']),
|
||||
y0=cb['low_price'],
|
||||
y1=cb['high_price'],
|
||||
fillcolor="yellow",
|
||||
opacity=0.3,
|
||||
line=dict(color="orange", width=2),
|
||||
layer="below"
|
||||
)
|
||||
|
||||
# 中枢中轴线
|
||||
fig.add_trace(go.Scatter(
|
||||
x=[pd.to_datetime(cb['start_time']), pd.to_datetime(cb['end_time'])],
|
||||
y=[cb['center_price'], cb['center_price']],
|
||||
mode='lines',
|
||||
line=dict(color='orange', width=2, dash='dash'),
|
||||
name='中枢' if i == 0 else None,
|
||||
showlegend=(i == 0),
|
||||
hovertemplate=f'中枢<br>高点: {cb["high_price"]:.2f}<br>低点: {cb["low_price"]:.2f}<br>中轴: {cb["center_price"]:.2f}<extra></extra>'
|
||||
))
|
||||
|
||||
# 添加买卖点
|
||||
if analysis_data.get('trading_points'):
|
||||
trading_points = analysis_data['trading_points']
|
||||
buy_points = [tp for tp in trading_points if tp['signal_type'] == 'buy']
|
||||
sell_points = [tp for tp in trading_points if tp['signal_type'] == 'sell']
|
||||
|
||||
if buy_points:
|
||||
colors = {'first': 'lime', 'second': 'lightgreen', 'third': 'lightblue'}
|
||||
for point_class in ['first', 'second', 'third']:
|
||||
class_points = [tp for tp in buy_points if tp['point_class'] == point_class]
|
||||
if class_points:
|
||||
fig.add_trace(go.Scatter(
|
||||
x=[pd.to_datetime(tp['timestamp']) for tp in class_points],
|
||||
y=[tp['price'] for tp in class_points],
|
||||
mode='markers',
|
||||
marker=dict(
|
||||
symbol='arrow-up',
|
||||
size=15,
|
||||
color=colors.get(point_class, 'lime'),
|
||||
line=dict(color='darkgreen', width=2)
|
||||
),
|
||||
name=f'{point_class[0].upper() + point_class[1:]}类买点',
|
||||
hovertemplate='%{fullData.name}<br>时间: %{x}<br>价格: %{y:.2f}<br>描述: %{customdata}<extra></extra>',
|
||||
customdata=[tp['description'] for tp in class_points]
|
||||
))
|
||||
|
||||
if sell_points:
|
||||
colors = {'first': 'red', 'second': 'lightcoral', 'third': 'pink'}
|
||||
for point_class in ['first', 'second', 'third']:
|
||||
class_points = [tp for tp in sell_points if tp['point_class'] == point_class]
|
||||
if class_points:
|
||||
fig.add_trace(go.Scatter(
|
||||
x=[pd.to_datetime(tp['timestamp']) for tp in class_points],
|
||||
y=[tp['price'] for tp in class_points],
|
||||
mode='markers',
|
||||
marker=dict(
|
||||
symbol='arrow-down',
|
||||
size=15,
|
||||
color=colors.get(point_class, 'red'),
|
||||
line=dict(color='darkred', width=2)
|
||||
),
|
||||
name=f'{point_class[0].upper() + point_class[1:]}类卖点',
|
||||
hovertemplate='%{fullData.name}<br>时间: %{x}<br>价格: %{y:.2f}<br>描述: %{customdata}<extra></extra>',
|
||||
customdata=[tp['description'] for tp in class_points]
|
||||
))
|
||||
|
||||
fig.update_layout(
|
||||
title=f"{analysis_data['symbol']} {analysis_data['timeframe']} 缠论分析图",
|
||||
xaxis_title="时间",
|
||||
yaxis_title="价格",
|
||||
height=700,
|
||||
xaxis_rangeslider_visible=False,
|
||||
hovermode='x unified'
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
@app.callback(
|
||||
Output('statistics-content', 'children'),
|
||||
[Input('analysis-data', 'data'),
|
||||
Input('enhanced-strength-filter', 'value'),
|
||||
Input('display-level-dropdown', 'value')]
|
||||
)
|
||||
def update_statistics(analysis_data, enhanced_strength_filter, display_level):
|
||||
if not analysis_data:
|
||||
return "暂无数据"
|
||||
|
||||
# 基础统计
|
||||
basic_stats = html.Div([
|
||||
html.H5("📊 基础统计"),
|
||||
html.P(f"交易对: {analysis_data.get('symbol', 'N/A')}"),
|
||||
html.P(f"时间周期: {analysis_data.get('timeframe', 'N/A')}"),
|
||||
html.P(f"处理后K线数量: {analysis_data.get('processed_klines_count', 0)}"),
|
||||
html.P(f"笔数量: {analysis_data.get('strokes_count', 0)}"),
|
||||
html.P(f"线段数量: {analysis_data.get('segments_count', 0)}"),
|
||||
html.P(f"中枢数量: {analysis_data.get('central_banks_count', 0)}"),
|
||||
html.P(f"买卖点数量: {analysis_data.get('trading_points_count', 0)}")
|
||||
])
|
||||
|
||||
# 分型强度统计
|
||||
fractals = analysis_data.get('fractals', [])
|
||||
if fractals:
|
||||
# 计算强度分布
|
||||
strong_fractals = [f for f in fractals if f.get('enhanced_strength', 0) >= 70]
|
||||
medium_fractals = [f for f in fractals if 40 <= f.get('enhanced_strength', 0) < 70]
|
||||
weak_fractals = [f for f in fractals if f.get('enhanced_strength', 0) < 40]
|
||||
|
||||
# 根据当前过滤条件计算显示的分型
|
||||
filtered_fractals = []
|
||||
for f in fractals:
|
||||
enhanced_strength = f.get('enhanced_strength', 0)
|
||||
if display_level == 'strong' and enhanced_strength < 70:
|
||||
continue
|
||||
elif display_level == 'medium_plus' and enhanced_strength < 40:
|
||||
continue
|
||||
elif display_level == 'custom' and enhanced_strength < enhanced_strength_filter:
|
||||
continue
|
||||
filtered_fractals.append(f)
|
||||
|
||||
# 平均强度
|
||||
avg_enhanced = sum(f.get('enhanced_strength', 0) for f in fractals) / len(fractals) if fractals else 0
|
||||
avg_price_dom = sum(f.get('price_dominance', 0) for f in fractals) / len(fractals) if fractals else 0
|
||||
avg_volume = sum(f.get('volume_strength', 0) for f in fractals) / len(fractals) if fractals else 0
|
||||
avg_trend = sum(f.get('trend_position', 0) for f in fractals) / len(fractals) if fractals else 0
|
||||
|
||||
fractal_stats = html.Div([
|
||||
html.H5("🔥 分型强度分析"),
|
||||
html.P(f"总分型数: {len(fractals)} 个"),
|
||||
html.P(f"强势分型(≥70分): {len(strong_fractals)} 个"),
|
||||
html.P(f"中等分型(40-70分): {len(medium_fractals)} 个"),
|
||||
html.P(f"弱势分型(<40分): {len(weak_fractals)} 个"),
|
||||
html.Hr(),
|
||||
html.P(f"平均增强强度: {avg_enhanced:.1f}分"),
|
||||
html.P(f"平均价格优势: {avg_price_dom:.1f}分"),
|
||||
html.P(f"平均成交量强度: {avg_volume:.1f}分"),
|
||||
html.P(f"平均趋势位置: {avg_trend:.1f}分"),
|
||||
html.Hr(),
|
||||
html.P(f"🎯 当前显示: {len(filtered_fractals)} 个分型"),
|
||||
html.P(f"过滤级别: {display_level}", className="text-muted"),
|
||||
html.P(f"过滤阈值: {enhanced_strength_filter}分", className="text-muted") if display_level == 'custom' else ""
|
||||
])
|
||||
|
||||
return html.Div([basic_stats, html.Hr(), fractal_stats])
|
||||
else:
|
||||
return basic_stats
|
||||
|
||||
@app.callback(
|
||||
Output('market-structure-content', 'children'),
|
||||
[Input('market-structure-data', 'data')]
|
||||
)
|
||||
def update_market_structure(market_data):
|
||||
if not market_data:
|
||||
return "暂无市场数据"
|
||||
|
||||
latest_price = market_data.get('latest_price', 0)
|
||||
price_change = market_data.get('price_change', 0)
|
||||
change_percent = (price_change / (latest_price - price_change)) * 100 if (latest_price - price_change) != 0 else 0
|
||||
|
||||
return html.Div([
|
||||
html.H4("市场结构"),
|
||||
html.P(f"当前价格: ${latest_price:.2f}"),
|
||||
html.P(f"价格变化: ${price_change:.2f} ({change_percent:+.2f}%)"),
|
||||
html.P(f"24小时最高: ${market_data.get('high_24h', 0):.2f}"),
|
||||
html.P(f"24小时最低: ${market_data.get('low_24h', 0):.2f}"),
|
||||
html.P(f"平均成交量: {market_data.get('volume_avg', 0):.2f}")
|
||||
])
|
||||
|
||||
@app.callback(
|
||||
Output('latest-signals-content', 'children'),
|
||||
[Input('analysis-data', 'data')]
|
||||
)
|
||||
def update_latest_signals(analysis_data):
|
||||
if not analysis_data:
|
||||
return "暂无信号数据"
|
||||
|
||||
trading_points_count = analysis_data.get('trading_points_count', 0)
|
||||
|
||||
return html.Div([
|
||||
html.H4("最新信号"),
|
||||
html.P(f"检测到 {trading_points_count} 个买卖点信号"),
|
||||
html.P("详细信号分析请查看主图表标记")
|
||||
])
|
||||
|
||||
return app
|
||||
|
||||
|
||||
# 添加CSS样式
|
||||
external_stylesheets = [
|
||||
'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css'
|
||||
]
|
||||
|
||||
|
||||
def run_app(debug=True, port=8050):
|
||||
"""运行Web应用"""
|
||||
app = create_app()
|
||||
app.run(debug=debug, port=port, host='0.0.0.0')
|
||||
@@ -0,0 +1,445 @@
|
||||
"""
|
||||
缠论可视化模块:使用Plotly生成交互式图表
|
||||
"""
|
||||
|
||||
import plotly.graph_objects as go
|
||||
import plotly.express as px
|
||||
from plotly.subplots import make_subplots
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import Dict, List, Optional
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChanVisualizer:
|
||||
"""缠论可视化器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化可视化器"""
|
||||
self.colors = {
|
||||
'up_candle': '#26a69a',
|
||||
'down_candle': '#ef5350',
|
||||
'fractal_top': '#ff6b6b',
|
||||
'fractal_bottom': '#4ecdc4',
|
||||
'stroke_up': '#2e86de',
|
||||
'stroke_down': '#f39c12',
|
||||
'segment_up': '#0984e3',
|
||||
'segment_down': '#e17055',
|
||||
'central_bank': 'rgba(155, 89, 182, 0.3)',
|
||||
'buy_signal': '#00b894',
|
||||
'sell_signal': '#d63031'
|
||||
}
|
||||
|
||||
def create_comprehensive_chart(self, data: Dict) -> go.Figure:
|
||||
"""
|
||||
创建综合缠论分析图表
|
||||
|
||||
Args:
|
||||
data: 包含所有分析数据的字典
|
||||
|
||||
Returns:
|
||||
Plotly图表对象
|
||||
"""
|
||||
if not data or 'klines' not in data:
|
||||
return go.Figure()
|
||||
|
||||
# 创建子图
|
||||
fig = make_subplots(
|
||||
rows=2, cols=1,
|
||||
shared_xaxes=True,
|
||||
vertical_spacing=0.1,
|
||||
subplot_titles=('缠论分析图', '成交量'),
|
||||
row_heights=[0.8, 0.2]
|
||||
)
|
||||
|
||||
# 添加K线图
|
||||
self._add_candlestick(fig, data['klines'])
|
||||
|
||||
# 添加分型
|
||||
if 'fractals' in data and data['fractals']:
|
||||
self._add_fractals(fig, data['fractals'])
|
||||
|
||||
# 添加笔
|
||||
if 'strokes' in data and data['strokes']:
|
||||
self._add_strokes(fig, data['strokes'])
|
||||
|
||||
# 添加线段
|
||||
if 'segments' in data and data['segments']:
|
||||
self._add_segments(fig, data['segments'])
|
||||
|
||||
# 添加中枢
|
||||
if 'central_banks' in data and data['central_banks']:
|
||||
self._add_central_banks(fig, data['central_banks'])
|
||||
|
||||
# 添加买卖点
|
||||
if 'trading_points' in data and data['trading_points']:
|
||||
self._add_trading_signals(fig, data['trading_points'])
|
||||
|
||||
# 添加成交量
|
||||
self._add_volume(fig, data['klines'])
|
||||
|
||||
# 更新布局
|
||||
self._update_layout(fig)
|
||||
|
||||
return fig
|
||||
|
||||
def _add_candlestick(self, fig: go.Figure, klines: pd.DataFrame):
|
||||
"""添加K线图"""
|
||||
fig.add_trace(
|
||||
go.Candlestick(
|
||||
x=klines.index,
|
||||
open=klines['open'],
|
||||
high=klines['high'],
|
||||
low=klines['low'],
|
||||
close=klines['close'],
|
||||
name='K线',
|
||||
increasing=dict(line=dict(color=self.colors['up_candle'])),
|
||||
decreasing=dict(line=dict(color=self.colors['down_candle']))
|
||||
),
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
def _add_fractals(self, fig: go.Figure, fractals: List):
|
||||
"""添加分型标记"""
|
||||
top_fractals = [f for f in fractals if f.fractal_type == 'top']
|
||||
bottom_fractals = [f for f in fractals if f.fractal_type == 'bottom']
|
||||
|
||||
if top_fractals:
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=[f.timestamp for f in top_fractals],
|
||||
y=[f.price for f in top_fractals],
|
||||
mode='markers',
|
||||
marker=dict(
|
||||
symbol='triangle-down',
|
||||
size=8,
|
||||
color=self.colors['fractal_top']
|
||||
),
|
||||
name='顶分型',
|
||||
hovertemplate='顶分型<br>时间: %{x}<br>价格: %{y}<br>强度: %{customdata}<extra></extra>',
|
||||
customdata=[f.strength for f in top_fractals]
|
||||
),
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
if bottom_fractals:
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=[f.timestamp for f in bottom_fractals],
|
||||
y=[f.price for f in bottom_fractals],
|
||||
mode='markers',
|
||||
marker=dict(
|
||||
symbol='triangle-up',
|
||||
size=8,
|
||||
color=self.colors['fractal_bottom']
|
||||
),
|
||||
name='底分型',
|
||||
hovertemplate='底分型<br>时间: %{x}<br>价格: %{y}<br>强度: %{customdata}<extra></extra>',
|
||||
customdata=[f.strength for f in bottom_fractals]
|
||||
),
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
def _add_strokes(self, fig: go.Figure, strokes: List):
|
||||
"""添加笔"""
|
||||
for stroke in strokes:
|
||||
color = self.colors['stroke_up'] if stroke.direction == 1 else self.colors['stroke_down']
|
||||
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=[stroke.start_fractal.timestamp, stroke.end_fractal.timestamp],
|
||||
y=[stroke.start_fractal.price, stroke.end_fractal.price],
|
||||
mode='lines',
|
||||
line=dict(color=color, width=2),
|
||||
name='笔' if stroke == strokes[0] else '',
|
||||
showlegend=stroke == strokes[0],
|
||||
hovertemplate=f'笔<br>方向: {"上升" if stroke.direction == 1 else "下降"}<br>长度: {stroke.length:.2f}<br>强度: {stroke.strength:.2f}<extra></extra>'
|
||||
),
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
def _add_segments(self, fig: go.Figure, segments: List):
|
||||
"""添加线段"""
|
||||
for segment in segments:
|
||||
color = self.colors['segment_up'] if segment.direction == 1 else self.colors['segment_down']
|
||||
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=[segment.start_time, segment.end_time],
|
||||
y=[segment.start_price, segment.end_price],
|
||||
mode='lines',
|
||||
line=dict(color=color, width=4, dash='dash'),
|
||||
name='线段' if segment == segments[0] else '',
|
||||
showlegend=segment == segments[0],
|
||||
hovertemplate=f'线段<br>方向: {"上升" if segment.direction == 1 else "下降"}<br>长度: {segment.length:.2f}<br>强度: {segment.strength:.2f}<extra></extra>'
|
||||
),
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
def _add_central_banks(self, fig: go.Figure, central_banks: List):
|
||||
"""添加中枢"""
|
||||
for cb in central_banks:
|
||||
# 添加中枢矩形区域
|
||||
fig.add_shape(
|
||||
type="rect",
|
||||
x0=cb.start_time,
|
||||
y0=cb.low_price,
|
||||
x1=cb.end_time,
|
||||
y1=cb.high_price,
|
||||
fillcolor=self.colors['central_bank'],
|
||||
opacity=0.3,
|
||||
line=dict(color="rgba(155, 89, 182, 0.8)", width=1),
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
# 添加中枢标签
|
||||
fig.add_annotation(
|
||||
x=cb.start_time + (cb.end_time - cb.start_time) / 2,
|
||||
y=cb.center_price,
|
||||
text=f"中枢({cb.level})",
|
||||
showarrow=False,
|
||||
font=dict(size=10, color="purple"),
|
||||
bgcolor="rgba(255,255,255,0.8)",
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
def _add_trading_signals(self, fig: go.Figure, trading_points: List):
|
||||
"""添加买卖点信号"""
|
||||
buy_points = [p for p in trading_points if p.signal_type == 'buy']
|
||||
sell_points = [p for p in trading_points if p.signal_type == 'sell']
|
||||
|
||||
if buy_points:
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=[p.timestamp for p in buy_points],
|
||||
y=[p.price for p in buy_points],
|
||||
mode='markers',
|
||||
marker=dict(
|
||||
symbol='triangle-up',
|
||||
size=12,
|
||||
color=self.colors['buy_signal']
|
||||
),
|
||||
name='买点',
|
||||
hovertemplate='%{customdata}<br>时间: %{x}<br>价格: %{y}<br>强度: %{text}<extra></extra>',
|
||||
customdata=[p.description for p in buy_points],
|
||||
text=[f"{p.strength:.3f}" for p in buy_points]
|
||||
),
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
if sell_points:
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=[p.timestamp for p in sell_points],
|
||||
y=[p.price for p in sell_points],
|
||||
mode='markers',
|
||||
marker=dict(
|
||||
symbol='triangle-down',
|
||||
size=12,
|
||||
color=self.colors['sell_signal']
|
||||
),
|
||||
name='卖点',
|
||||
hovertemplate='%{customdata}<br>时间: %{x}<br>价格: %{y}<br>强度: %{text}<extra></extra>',
|
||||
customdata=[p.description for p in sell_points],
|
||||
text=[f"{p.strength:.3f}" for p in sell_points]
|
||||
),
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
def _add_volume(self, fig: go.Figure, klines: pd.DataFrame):
|
||||
"""添加成交量"""
|
||||
colors = [self.colors['up_candle'] if close >= open_ else self.colors['down_candle']
|
||||
for close, open_ in zip(klines['close'], klines['open'])]
|
||||
|
||||
fig.add_trace(
|
||||
go.Bar(
|
||||
x=klines.index,
|
||||
y=klines['volume'],
|
||||
name='成交量',
|
||||
marker_color=colors,
|
||||
showlegend=False
|
||||
),
|
||||
row=2, col=1
|
||||
)
|
||||
|
||||
def _update_layout(self, fig: go.Figure):
|
||||
"""更新图表布局"""
|
||||
fig.update_layout(
|
||||
title=dict(
|
||||
text="缠论分析图表",
|
||||
x=0.5,
|
||||
font=dict(size=20)
|
||||
),
|
||||
xaxis_rangeslider_visible=False,
|
||||
height=800,
|
||||
showlegend=True,
|
||||
legend=dict(
|
||||
orientation="h",
|
||||
yanchor="bottom",
|
||||
y=1.02,
|
||||
xanchor="right",
|
||||
x=1
|
||||
),
|
||||
margin=dict(l=50, r=50, t=100, b=50),
|
||||
plot_bgcolor='white',
|
||||
paper_bgcolor='white'
|
||||
)
|
||||
|
||||
# 更新X轴
|
||||
fig.update_xaxes(
|
||||
title_text="时间",
|
||||
showgrid=True,
|
||||
gridwidth=1,
|
||||
gridcolor='lightgray'
|
||||
)
|
||||
|
||||
# 更新Y轴
|
||||
fig.update_yaxes(
|
||||
title_text="价格",
|
||||
showgrid=True,
|
||||
gridwidth=1,
|
||||
gridcolor='lightgray',
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
fig.update_yaxes(
|
||||
title_text="成交量",
|
||||
row=2, col=1
|
||||
)
|
||||
|
||||
def create_statistics_charts(self, data: Dict) -> List[go.Figure]:
|
||||
"""
|
||||
创建统计分析图表
|
||||
|
||||
Args:
|
||||
data: 分析数据
|
||||
|
||||
Returns:
|
||||
统计图表列表
|
||||
"""
|
||||
charts = []
|
||||
|
||||
# 分型强度分布
|
||||
if 'fractals' in data and data['fractals']:
|
||||
fractal_chart = self._create_fractal_strength_chart(data['fractals'])
|
||||
charts.append(fractal_chart)
|
||||
|
||||
# 买卖点类别分布
|
||||
if 'trading_points' in data and data['trading_points']:
|
||||
signal_chart = self._create_signal_distribution_chart(data['trading_points'])
|
||||
charts.append(signal_chart)
|
||||
|
||||
# 中枢级别分布
|
||||
if 'central_banks' in data and data['central_banks']:
|
||||
cb_chart = self._create_central_bank_chart(data['central_banks'])
|
||||
charts.append(cb_chart)
|
||||
|
||||
return charts
|
||||
|
||||
def _create_fractal_strength_chart(self, fractals: List) -> go.Figure:
|
||||
"""创建分型强度分布图"""
|
||||
strengths = [f.strength for f in fractals]
|
||||
types = [f.fractal_type for f in fractals]
|
||||
|
||||
df = pd.DataFrame({'strength': strengths, 'type': types})
|
||||
|
||||
fig = px.histogram(
|
||||
df,
|
||||
x='strength',
|
||||
color='type',
|
||||
title='分型强度分布',
|
||||
labels={'strength': '强度', 'type': '类型'},
|
||||
color_discrete_map={'top': self.colors['fractal_top'], 'bottom': self.colors['fractal_bottom']}
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
def _create_signal_distribution_chart(self, trading_points: List) -> go.Figure:
|
||||
"""创建买卖点分布图"""
|
||||
classes = [f"{p.point_class}类{p.signal_type}点" for p in trading_points]
|
||||
|
||||
fig = px.pie(
|
||||
values=[classes.count(c) for c in set(classes)],
|
||||
names=list(set(classes)),
|
||||
title='买卖点类别分布'
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
def _create_central_bank_chart(self, central_banks: List) -> go.Figure:
|
||||
"""创建中枢分析图"""
|
||||
levels = [cb.level for cb in central_banks]
|
||||
strengths = [cb.strength for cb in central_banks]
|
||||
|
||||
fig = go.Figure()
|
||||
|
||||
for level in set(levels):
|
||||
level_strengths = [s for l, s in zip(levels, strengths) if l == level]
|
||||
fig.add_trace(go.Box(
|
||||
y=level_strengths,
|
||||
name=level,
|
||||
boxpoints='all'
|
||||
))
|
||||
|
||||
fig.update_layout(
|
||||
title='中枢强度分布(按级别)',
|
||||
xaxis_title='中枢级别',
|
||||
yaxis_title='强度'
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
def create_market_structure_chart(self, market_structure: Dict) -> go.Figure:
|
||||
"""
|
||||
创建市场结构图
|
||||
|
||||
Args:
|
||||
market_structure: 市场结构数据
|
||||
|
||||
Returns:
|
||||
市场结构图表
|
||||
"""
|
||||
fig = go.Figure()
|
||||
|
||||
# 当前价格线
|
||||
if 'current_price' in market_structure:
|
||||
fig.add_hline(
|
||||
y=market_structure['current_price'],
|
||||
line_dash="dash",
|
||||
line_color="black",
|
||||
annotation_text=f"当前价格: {market_structure['current_price']:.2f}"
|
||||
)
|
||||
|
||||
# 支撑阻力位
|
||||
if 'support_resistance' in market_structure:
|
||||
sr = market_structure['support_resistance']
|
||||
|
||||
# 支撑位
|
||||
if 'support_levels' in sr:
|
||||
for i, support in enumerate(sr['support_levels']):
|
||||
fig.add_hline(
|
||||
y=support,
|
||||
line_dash="dot",
|
||||
line_color=self.colors['buy_signal'],
|
||||
annotation_text=f"支撑{i+1}: {support:.2f}"
|
||||
)
|
||||
|
||||
# 阻力位
|
||||
if 'resistance_levels' in sr:
|
||||
for i, resistance in enumerate(sr['resistance_levels']):
|
||||
fig.add_hline(
|
||||
y=resistance,
|
||||
line_dash="dot",
|
||||
line_color=self.colors['sell_signal'],
|
||||
annotation_text=f"阻力{i+1}: {resistance:.2f}"
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
title="市场结构分析",
|
||||
yaxis_title="价格",
|
||||
height=400
|
||||
)
|
||||
|
||||
return fig
|
||||
Reference in New Issue
Block a user