- 新增 Vue 3 + Vuetify 前端界面 - 新增市场分析模块 (market/) - 更新主服务器和路由 - 更新 MT5 EA 文件 - 添加 .gitignore 排除临时文件
389 lines
13 KiB
Python
389 lines
13 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
趋势分析模块
|
||
基于均线和ADX判断趋势方向和强度
|
||
"""
|
||
|
||
from collections import defaultdict
|
||
from typing import List, Dict, Optional
|
||
from datetime import datetime
|
||
import threading
|
||
|
||
from .store import KlineData, normalize_symbol
|
||
|
||
|
||
class TrendAnalyzer:
|
||
"""趋势分析器"""
|
||
|
||
# 支持的周期
|
||
PERIODS = ['H4', 'H1', 'M15', 'M5', 'M1']
|
||
|
||
# ADX阈值
|
||
ADX_TREND_THRESHOLD = 25 # ADX > 25 表示有趋势
|
||
ADX_STRONG_THRESHOLD = 40 # ADX > 40 表示强趋势
|
||
|
||
# 均线周期
|
||
MA_FAST = 10 # 快线周期
|
||
MA_SLOW = 20 # 慢线周期
|
||
|
||
def __init__(self):
|
||
# 存储各周期趋势状态: {SYMBOL: {PERIOD: TrendState}}
|
||
self._trend_states = defaultdict(lambda: defaultdict(dict))
|
||
self._lock = threading.RLock()
|
||
|
||
# 趋势转换历史
|
||
self._trend_changes = defaultdict(list)
|
||
|
||
print("[TrendAnalyzer] 趋势分析器已初始化")
|
||
|
||
def analyze_trend(self, symbol: str, period: str, klines: List[KlineData]) -> Dict:
|
||
"""
|
||
分析单个周期的趋势
|
||
|
||
Args:
|
||
symbol: 交易品种
|
||
period: 周期
|
||
klines: K线数据
|
||
|
||
Returns:
|
||
{
|
||
"trend": "up" / "down" / "sideways",
|
||
"strength": 0-100,
|
||
"adx": float,
|
||
"ma_fast": float,
|
||
"ma_slow": float,
|
||
"price": float,
|
||
"change_signal": bool, # 是否发生趋势转换
|
||
"timestamp": str
|
||
}
|
||
"""
|
||
if len(klines) < 30: # 至少需要30根K线
|
||
return {
|
||
"trend": "unknown",
|
||
"strength": 0,
|
||
"adx": 0,
|
||
"ma_fast": 0,
|
||
"ma_slow": 0,
|
||
"price": 0,
|
||
"change_signal": False,
|
||
"reason": "K线数据不足(需≥30根)",
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
|
||
# 计算均线
|
||
closes = [k.close for k in klines]
|
||
ma_fast = self._calculate_ma(closes, self.MA_FAST)
|
||
ma_slow = self._calculate_ma(closes, self.MA_SLOW)
|
||
current_price = closes[-1]
|
||
|
||
# 计算ADX
|
||
adx = self._calculate_adx(klines)
|
||
|
||
# 判断趋势方向和原因
|
||
reason_parts = []
|
||
|
||
if adx < self.ADX_TREND_THRESHOLD:
|
||
# ADX较低,震荡行情
|
||
trend = "sideways"
|
||
reason_parts.append(f"ADX={adx:.1f}<25 无明显趋势")
|
||
else:
|
||
# 根据均线和价格判断方向
|
||
if ma_fast > ma_slow and current_price > ma_fast:
|
||
trend = "up"
|
||
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) > MA{self.MA_SLOW}({ma_slow:.2f})")
|
||
reason_parts.append(f"价格({current_price:.2f}) > MA{self.MA_FAST}")
|
||
reason_parts.append(f"ADX={adx:.1f}≥25 确认趋势")
|
||
elif ma_fast < ma_slow and current_price < ma_fast:
|
||
trend = "down"
|
||
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) < MA{self.MA_SLOW}({ma_slow:.2f})")
|
||
reason_parts.append(f"价格({current_price:.2f}) < MA{self.MA_FAST}")
|
||
reason_parts.append(f"ADX={adx:.1f}≥25 确认趋势")
|
||
else:
|
||
trend = "sideways"
|
||
if ma_fast > ma_slow:
|
||
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) > MA{self.MA_SLOW}({ma_slow:.2f})")
|
||
reason_parts.append(f"但价格({current_price:.2f})低于MA{self.MA_FAST}")
|
||
else:
|
||
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) < MA{self.MA_SLOW}({ma_slow:.2f})")
|
||
reason_parts.append(f"且价格({current_price:.2f})高于MA{self.MA_FAST}")
|
||
reason_parts.append("信号矛盾,判定震荡")
|
||
|
||
reason = ";".join(reason_parts)
|
||
|
||
# 计算趋势强度 (基于ADX)
|
||
if adx >= self.ADX_STRONG_THRESHOLD:
|
||
strength = min(100, int(adx + 20))
|
||
elif adx >= self.ADX_TREND_THRESHOLD:
|
||
strength = int(adx + 10)
|
||
else:
|
||
strength = int(adx)
|
||
|
||
# 检查趋势转换
|
||
symbol_key = normalize_symbol(symbol)
|
||
change_signal = False
|
||
previous_trend = None
|
||
|
||
with self._lock:
|
||
if period in self._trend_states[symbol_key]:
|
||
previous_trend = self._trend_states[symbol_key][period].get('trend')
|
||
if previous_trend and previous_trend != trend and previous_trend != "unknown":
|
||
change_signal = True
|
||
# 记录转换历史
|
||
self._trend_changes[symbol_key].append({
|
||
"period": period,
|
||
"from_trend": previous_trend,
|
||
"to_trend": trend,
|
||
"timestamp": datetime.now().isoformat(),
|
||
"price": current_price
|
||
})
|
||
# 只保留最近20条
|
||
if len(self._trend_changes[symbol_key]) > 20:
|
||
self._trend_changes[symbol_key] = self._trend_changes[symbol_key][-20:]
|
||
|
||
# 更新状态
|
||
self._trend_states[symbol_key][period] = {
|
||
"trend": trend,
|
||
"strength": strength,
|
||
"adx": round(adx, 2),
|
||
"ma_fast": round(ma_fast, 4),
|
||
"ma_slow": round(ma_slow, 4),
|
||
"price": current_price,
|
||
"change_signal": change_signal,
|
||
"previous_trend": previous_trend,
|
||
"reason": reason,
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
|
||
return self._trend_states[symbol_key][period]
|
||
|
||
def analyze_resonance(self, symbol: str) -> Dict:
|
||
"""
|
||
分析多周期共振
|
||
|
||
Returns:
|
||
{
|
||
"resonance": "up" / "down" / "none",
|
||
"strength": 0-100,
|
||
"periods": {period: trend_state},
|
||
"aligned_count": int,
|
||
"signal": str
|
||
}
|
||
"""
|
||
symbol_key = normalize_symbol(symbol)
|
||
|
||
with self._lock:
|
||
states = dict(self._trend_states[symbol_key])
|
||
|
||
if not states:
|
||
return {
|
||
"resonance": "none",
|
||
"strength": 0,
|
||
"periods": {},
|
||
"aligned_count": 0,
|
||
"signal": "等待数据"
|
||
}
|
||
|
||
# 统计各趋势数量
|
||
up_count = sum(1 for s in states.values() if s.get('trend') == 'up')
|
||
down_count = sum(1 for s in states.values() if s.get('trend') == 'down')
|
||
sideways_count = sum(1 for s in states.values() if s.get('trend') == 'sideways')
|
||
|
||
# 计算平均强度
|
||
strengths = [s.get('strength', 0) for s in states.values() if s.get('trend') != 'sideways']
|
||
avg_strength = sum(strengths) / len(strengths) if strengths else 0
|
||
|
||
# 判断共振
|
||
total = len(states)
|
||
if up_count >= total * 0.6: # 60%以上周期趋势一致
|
||
resonance = "up"
|
||
aligned_count = up_count
|
||
signal = f"多周期向上共振 ({up_count}/{total})"
|
||
elif down_count >= total * 0.6:
|
||
resonance = "down"
|
||
aligned_count = down_count
|
||
signal = f"多周期向下共振 ({down_count}/{total})"
|
||
else:
|
||
resonance = "none"
|
||
aligned_count = max(up_count, down_count)
|
||
signal = f"趋势分歧 (↑{up_count} ↓{down_count} →{sideways_count})"
|
||
|
||
return {
|
||
"resonance": resonance,
|
||
"strength": int(avg_strength),
|
||
"periods": states,
|
||
"aligned_count": aligned_count,
|
||
"up_count": up_count,
|
||
"down_count": down_count,
|
||
"sideways_count": sideways_count,
|
||
"signal": signal
|
||
}
|
||
|
||
def get_trend_state(self, symbol: str, period: str = None) -> Dict:
|
||
"""获取趋势状态"""
|
||
symbol_key = normalize_symbol(symbol)
|
||
|
||
with self._lock:
|
||
if period:
|
||
return self._trend_states[symbol_key].get(period, {})
|
||
return dict(self._trend_states[symbol_key])
|
||
|
||
def get_trend_changes(self, symbol: str, count: int = 10) -> List[Dict]:
|
||
"""获取趋势转换历史"""
|
||
symbol_key = normalize_symbol(symbol)
|
||
|
||
with self._lock:
|
||
return self._trend_changes[symbol_key][-count:]
|
||
|
||
def _calculate_ma(self, data: List[float], period: int) -> float:
|
||
"""计算移动平均线"""
|
||
if len(data) < period:
|
||
return data[-1] if data else 0
|
||
return sum(data[-period:]) / period
|
||
|
||
def _calculate_adx(self, klines: List[KlineData], period: int = 14) -> float:
|
||
"""
|
||
计算ADX (Average Directional Index)
|
||
|
||
ADX > 25: 有趋势
|
||
ADX > 40: 强趋势
|
||
ADX < 20: 无明显趋势
|
||
"""
|
||
if len(klines) < period + 1:
|
||
return 0
|
||
|
||
# 计算 +DM 和 -DM
|
||
plus_dm = []
|
||
minus_dm = []
|
||
tr_list = []
|
||
|
||
for i in range(1, len(klines)):
|
||
high = klines[i].high
|
||
low = klines[i].low
|
||
prev_high = klines[i-1].high
|
||
prev_low = klines[i-1].low
|
||
prev_close = klines[i-1].close
|
||
|
||
# +DM
|
||
up_move = high - prev_high
|
||
down_move = prev_low - low
|
||
|
||
if up_move > down_move and up_move > 0:
|
||
plus_dm.append(up_move)
|
||
else:
|
||
plus_dm.append(0)
|
||
|
||
# -DM
|
||
if down_move > up_move and down_move > 0:
|
||
minus_dm.append(down_move)
|
||
else:
|
||
minus_dm.append(0)
|
||
|
||
# True Range
|
||
tr = max(
|
||
high - low,
|
||
abs(high - prev_close),
|
||
abs(low - prev_close)
|
||
)
|
||
tr_list.append(tr)
|
||
|
||
if len(tr_list) < period:
|
||
return 0
|
||
|
||
# 计算平滑值
|
||
atr = sum(tr_list[-period:]) / period
|
||
smoothed_plus_dm = sum(plus_dm[-period:]) / period
|
||
smoothed_minus_dm = sum(minus_dm[-period:]) / period
|
||
|
||
# 计算 +DI 和 -DI
|
||
if atr == 0:
|
||
return 0
|
||
|
||
plus_di = (smoothed_plus_dm / atr) * 100
|
||
minus_di = (smoothed_minus_dm / atr) * 100
|
||
|
||
# 计算 DX
|
||
di_sum = plus_di + minus_di
|
||
if di_sum == 0:
|
||
return 0
|
||
|
||
dx = abs(plus_di - minus_di) / di_sum * 100
|
||
|
||
return dx
|
||
|
||
def generate_trade_suggestion(self, symbol: str, pivots: List[Dict],
|
||
current_price: float) -> Optional[Dict]:
|
||
"""
|
||
基于趋势和转折点生成交易建议
|
||
|
||
Args:
|
||
symbol: 交易品种
|
||
pivots: 转折点数据
|
||
current_price: 当前价格
|
||
|
||
Returns:
|
||
交易建议 或 None
|
||
"""
|
||
symbol_key = normalize_symbol(symbol)
|
||
|
||
# 获取趋势状态
|
||
resonance = self.analyze_resonance(symbol)
|
||
|
||
if resonance['resonance'] == 'none':
|
||
return None
|
||
|
||
if resonance['strength'] < 30:
|
||
return None
|
||
|
||
trend = resonance['resonance']
|
||
|
||
# 根据趋势找最近的转折点作为止损止盈
|
||
recent_pivots = sorted(pivots, key=lambda x: x['timestamp'], reverse=True)[:10]
|
||
|
||
sl = None
|
||
tp = None
|
||
action = None
|
||
reason = ""
|
||
|
||
if trend == "up":
|
||
# 上升趋势,找最近的低点作为止损
|
||
action = "b"
|
||
low_pivots = [p for p in recent_pivots if p['direction'] == 'low']
|
||
if low_pivots:
|
||
# 找最近的低点作为止损
|
||
sl = low_pivots[0]['price']
|
||
# 止盈设为止损的1.5-2倍距离
|
||
if sl and current_price > sl:
|
||
distance = current_price - sl
|
||
tp = current_price + distance * 1.5
|
||
reason = f"多周期向上共振,建议买入,止损参考最近低点 {sl}"
|
||
else:
|
||
return None
|
||
|
||
elif trend == "down":
|
||
# 下降趋势,找最近的高点作为止损
|
||
action = "s"
|
||
high_pivots = [p for p in recent_pivots if p['direction'] == 'high']
|
||
if high_pivots:
|
||
sl = high_pivots[0]['price']
|
||
if sl and current_price < sl:
|
||
distance = sl - current_price
|
||
tp = current_price - distance * 1.5
|
||
reason = f"多周期向下共振,建议卖出,止损参考最近高点 {sl}"
|
||
else:
|
||
return None
|
||
|
||
if not all([action, sl, tp]):
|
||
return None
|
||
|
||
return {
|
||
"symbol": symbol_key,
|
||
"action": action,
|
||
"price": current_price,
|
||
"sl": round(sl, 4),
|
||
"tp": round(tp, 4),
|
||
"reason": reason,
|
||
"trend_strength": resonance['strength'],
|
||
"resonance_periods": resonance['aligned_count'],
|
||
"generated_at": datetime.now().isoformat()
|
||
} |