Add files to chanlun_1
This commit is contained in:
@@ -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