Files
AI-Trader/market/trend_analyzer.py
T

389 lines
13 KiB
Python
Raw Normal View History

#!/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()
}