Files
AI-Trader/market/trend_analyzer.py
T
guaiwoluo2020 51b2f30748 feat: 添加前端界面和市场分析模块
- 新增 Vue 3 + Vuetify 前端界面
- 新增市场分析模块 (market/)
- 更新主服务器和路由
- 更新 MT5 EA 文件
- 添加 .gitignore 排除临时文件
2026-03-10 17:38:13 +08:00

389 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()
}