tradusai/analysis/momentum_analyzer.py
2025-12-16 23:26:19 +08:00

797 lines
26 KiB
Python

"""
Technical Indicator Synthesis - Momentum and Volume-Price Analysis
Provides structured technical analysis for LLM decision making
"""
import logging
from typing import Dict, Any, List, Optional
import pandas as pd
import numpy as np
from .indicators import TechnicalIndicators
from .config import config
logger = logging.getLogger(__name__)
class MomentumAnalyzer:
"""
Technical indicator synthesis and divergence detection
Provides:
1. Momentum analysis (RSI, MACD, Stochastic)
2. Volume-Price relationship
3. Divergence detection (bullish/bearish)
4. Overbought/Oversold conditions
5. LLM-ready structured output
"""
@staticmethod
def analyze_momentum(df: pd.DataFrame, timeframe: str = '1h') -> Dict[str, Any]:
"""
Perform comprehensive momentum analysis
Args:
df: DataFrame with OHLCV data
timeframe: Timeframe for context
Returns:
Complete momentum analysis for LLM
"""
if df.empty or len(df) < 50:
return MomentumAnalyzer._empty_result(timeframe)
# Ensure indicators are calculated
if 'rsi' not in df.columns:
df = TechnicalIndicators.add_all_indicators(df)
latest = df.iloc[-1]
current_price = float(latest['close'])
# 1. RSI Analysis
rsi_analysis = MomentumAnalyzer._analyze_rsi(df)
# 2. MACD Analysis
macd_analysis = MomentumAnalyzer._analyze_macd(df)
# 3. Stochastic Analysis
stoch_analysis = MomentumAnalyzer._analyze_stochastic(df)
# 4. Volume-Price Analysis
volume_analysis = MomentumAnalyzer._analyze_volume_price(df)
# 5. Divergence Detection
divergences = MomentumAnalyzer._detect_divergences(df)
# 6. Overall Momentum Assessment
assessment = MomentumAnalyzer._assess_overall_momentum(
rsi_analysis, macd_analysis, stoch_analysis, volume_analysis, divergences
)
# 7. Generate Summary
summary = MomentumAnalyzer._generate_summary(
rsi_analysis, macd_analysis, volume_analysis, divergences, assessment
)
return {
'timeframe': timeframe,
'current_price': round(current_price, 2),
# Individual indicators
'rsi': rsi_analysis,
'macd': macd_analysis,
'stochastic': stoch_analysis,
# Volume analysis
'volume': volume_analysis,
# Divergences
'divergences': divergences,
# Overall assessment
'assessment': assessment,
# LLM-ready summary
'summary': summary,
}
@staticmethod
def analyze_multi_timeframe_momentum(
mtf_data: Dict[str, pd.DataFrame]
) -> Dict[str, Any]:
"""
Analyze momentum across multiple timeframes
Args:
mtf_data: Dict mapping timeframe to DataFrame
Returns:
Multi-timeframe momentum analysis
"""
results = {}
momentum_directions = {}
for tf, df in mtf_data.items():
if df.empty:
continue
momentum_info = MomentumAnalyzer.analyze_momentum(df, tf)
results[tf] = momentum_info
momentum_directions[tf] = momentum_info['assessment'].get('direction', 'neutral')
# Calculate cross-timeframe alignment
alignment = MomentumAnalyzer._calculate_mtf_alignment(momentum_directions)
# Find divergence confluence
divergence_confluence = MomentumAnalyzer._find_divergence_confluence(results)
return {
'timeframes': results,
'alignment': alignment,
'divergence_confluence': divergence_confluence,
'summary': MomentumAnalyzer._generate_mtf_summary(
results, alignment, divergence_confluence
),
}
@staticmethod
def _analyze_rsi(df: pd.DataFrame) -> Dict[str, Any]:
"""Analyze RSI indicator"""
latest = df.iloc[-1]
rsi = float(latest.get('rsi', 50))
# RSI trend (last 5 periods)
if len(df) >= 5:
rsi_values = df['rsi'].tail(5).values
rsi_change = rsi_values[-1] - rsi_values[0]
if rsi_change > 5:
trend = 'rising'
trend_cn = '上升'
elif rsi_change < -5:
trend = 'falling'
trend_cn = '下降'
else:
trend = 'flat'
trend_cn = '平稳'
else:
trend = 'unknown'
trend_cn = '未知'
rsi_change = 0
# RSI zone
if rsi >= 80:
zone = 'extreme_overbought'
zone_cn = '极度超买'
signal = 'bearish'
elif rsi >= 70:
zone = 'overbought'
zone_cn = '超买'
signal = 'bearish_warning'
elif rsi >= 60:
zone = 'strong'
zone_cn = '强势'
signal = 'bullish'
elif rsi >= 40:
zone = 'neutral'
zone_cn = '中性'
signal = 'neutral'
elif rsi >= 30:
zone = 'weak'
zone_cn = '弱势'
signal = 'bearish'
elif rsi >= 20:
zone = 'oversold'
zone_cn = '超卖'
signal = 'bullish_warning'
else:
zone = 'extreme_oversold'
zone_cn = '极度超卖'
signal = 'bullish'
return {
'value': round(rsi, 1),
'zone': zone,
'zone_cn': zone_cn,
'trend': trend,
'trend_cn': trend_cn,
'change': round(rsi_change, 1),
'signal': signal,
}
@staticmethod
def _analyze_macd(df: pd.DataFrame) -> Dict[str, Any]:
"""Analyze MACD indicator"""
if len(df) < 2:
return {'signal': 'unknown'}
latest = df.iloc[-1]
prev = df.iloc[-2]
macd_line = float(latest.get('macd', 0))
signal_line = float(latest.get('macd_signal', 0))
histogram = float(latest.get('macd_hist', 0))
prev_histogram = float(prev.get('macd_hist', 0))
# Cross detection
if histogram > 0 and prev_histogram <= 0:
cross = 'golden_cross'
cross_cn = '金叉'
elif histogram < 0 and prev_histogram >= 0:
cross = 'death_cross'
cross_cn = '死叉'
else:
cross = 'none'
cross_cn = '无交叉'
# Histogram momentum
if histogram > 0:
if histogram > prev_histogram:
momentum = 'bullish_expanding'
momentum_cn = '多头动能增强'
else:
momentum = 'bullish_contracting'
momentum_cn = '多头动能减弱'
else:
if histogram < prev_histogram:
momentum = 'bearish_expanding'
momentum_cn = '空头动能增强'
else:
momentum = 'bearish_contracting'
momentum_cn = '空头动能减弱'
# Signal
if cross == 'golden_cross':
signal = 'bullish'
elif cross == 'death_cross':
signal = 'bearish'
elif histogram > 0 and histogram > prev_histogram:
signal = 'bullish'
elif histogram < 0 and histogram < prev_histogram:
signal = 'bearish'
else:
signal = 'neutral'
return {
'macd_line': round(macd_line, 4),
'signal_line': round(signal_line, 4),
'histogram': round(histogram, 4),
'cross': cross,
'cross_cn': cross_cn,
'momentum': momentum,
'momentum_cn': momentum_cn,
'signal': signal,
}
@staticmethod
def _analyze_stochastic(df: pd.DataFrame) -> Dict[str, Any]:
"""Analyze Stochastic oscillator"""
latest = df.iloc[-1]
stoch_k = float(latest.get('stoch_k', 50))
stoch_d = float(latest.get('stoch_d', 50))
# Zone
if stoch_k >= 80:
zone = 'overbought'
zone_cn = '超买'
elif stoch_k <= 20:
zone = 'oversold'
zone_cn = '超卖'
else:
zone = 'neutral'
zone_cn = '中性'
# Cross
if len(df) >= 2:
prev = df.iloc[-2]
prev_k = float(prev.get('stoch_k', 50))
prev_d = float(prev.get('stoch_d', 50))
if stoch_k > stoch_d and prev_k <= prev_d:
cross = 'bullish'
cross_cn = '金叉'
elif stoch_k < stoch_d and prev_k >= prev_d:
cross = 'bearish'
cross_cn = '死叉'
else:
cross = 'none'
cross_cn = ''
else:
cross = 'unknown'
cross_cn = '未知'
return {
'k': round(stoch_k, 1),
'd': round(stoch_d, 1),
'zone': zone,
'zone_cn': zone_cn,
'cross': cross,
'cross_cn': cross_cn,
}
@staticmethod
def _analyze_volume_price(df: pd.DataFrame) -> Dict[str, Any]:
"""Analyze volume-price relationship"""
if len(df) < 20:
return {'status': 'insufficient_data'}
latest = df.iloc[-1]
current_volume = float(latest['volume'])
avg_volume = float(df['volume'].tail(20).mean())
volume_ratio = current_volume / avg_volume if avg_volume > 0 else 1
# Price change
price_change = (float(latest['close']) - float(df.iloc[-2]['close'])) / float(df.iloc[-2]['close']) * 100
# OBV analysis
obv = float(latest.get('obv', 0))
if len(df) >= 10:
obv_10_ago = float(df.iloc[-10].get('obv', 0))
obv_trend = 'rising' if obv > obv_10_ago else 'falling' if obv < obv_10_ago else 'flat'
obv_trend_cn = '上升' if obv_trend == 'rising' else '下降' if obv_trend == 'falling' else '平稳'
else:
obv_trend = 'unknown'
obv_trend_cn = '未知'
# Volume status
if volume_ratio > 2.0:
volume_status = 'extremely_high'
volume_status_cn = '异常放量'
elif volume_ratio > 1.5:
volume_status = 'high'
volume_status_cn = '明显放量'
elif volume_ratio > 1.1:
volume_status = 'above_average'
volume_status_cn = '温和放量'
elif volume_ratio < 0.5:
volume_status = 'very_low'
volume_status_cn = '显著缩量'
elif volume_ratio < 0.8:
volume_status = 'low'
volume_status_cn = '温和缩量'
else:
volume_status = 'normal'
volume_status_cn = '正常'
# Volume-price confirmation
if price_change > 0.5 and volume_ratio > 1.2:
confirmation = 'bullish_confirmed'
confirmation_cn = '量价齐升(看涨确认)'
elif price_change < -0.5 and volume_ratio > 1.2:
confirmation = 'bearish_confirmed'
confirmation_cn = '放量下跌(看跌确认)'
elif price_change > 0.5 and volume_ratio < 0.8:
confirmation = 'bullish_weak'
confirmation_cn = '缩量上涨(动能不足)'
elif price_change < -0.5 and volume_ratio < 0.8:
confirmation = 'bearish_exhaustion'
confirmation_cn = '缩量下跌(卖压减弱)'
else:
confirmation = 'neutral'
confirmation_cn = '量价中性'
return {
'current_volume': round(current_volume, 2),
'avg_volume': round(avg_volume, 2),
'volume_ratio': round(volume_ratio, 2),
'volume_status': volume_status,
'volume_status_cn': volume_status_cn,
'price_change_pct': round(price_change, 2),
'obv_trend': obv_trend,
'obv_trend_cn': obv_trend_cn,
'confirmation': confirmation,
'confirmation_cn': confirmation_cn,
}
@staticmethod
def _detect_divergences(df: pd.DataFrame) -> Dict[str, Any]:
"""Detect bullish and bearish divergences"""
if len(df) < 30:
return {'detected': False, 'divergences': []}
divergences = []
# RSI divergence
rsi_divergence = MomentumAnalyzer._check_rsi_divergence(df)
if rsi_divergence:
divergences.append(rsi_divergence)
# MACD divergence
macd_divergence = MomentumAnalyzer._check_macd_divergence(df)
if macd_divergence:
divergences.append(macd_divergence)
# OBV divergence
obv_divergence = MomentumAnalyzer._check_obv_divergence(df)
if obv_divergence:
divergences.append(obv_divergence)
# Determine overall divergence signal
bullish_count = sum(1 for d in divergences if d['type'] == 'bullish')
bearish_count = sum(1 for d in divergences if d['type'] == 'bearish')
if bullish_count > bearish_count and bullish_count > 0:
overall = 'bullish'
overall_cn = '看涨背离'
elif bearish_count > bullish_count and bearish_count > 0:
overall = 'bearish'
overall_cn = '看跌背离'
else:
overall = 'none'
overall_cn = '无明显背离'
return {
'detected': len(divergences) > 0,
'divergences': divergences,
'bullish_count': bullish_count,
'bearish_count': bearish_count,
'overall': overall,
'overall_cn': overall_cn,
'strength': 'strong' if len(divergences) >= 2 else 'moderate' if len(divergences) == 1 else 'none',
}
@staticmethod
def _check_rsi_divergence(df: pd.DataFrame, lookback: int = 20) -> Optional[Dict[str, Any]]:
"""Check for RSI divergence"""
recent = df.tail(lookback)
prices = recent['close'].values
rsi = recent['rsi'].values
# Find local extrema
price_lows_idx = MomentumAnalyzer._find_local_minima(prices)
price_highs_idx = MomentumAnalyzer._find_local_maxima(prices)
# Bullish divergence: Price making lower lows, RSI making higher lows
if len(price_lows_idx) >= 2:
idx1, idx2 = price_lows_idx[-2], price_lows_idx[-1]
if prices[idx2] < prices[idx1] and rsi[idx2] > rsi[idx1]:
return {
'type': 'bullish',
'type_cn': '看涨背离',
'indicator': 'RSI',
'description': '价格创新低但RSI抬高,可能反转向上',
}
# Bearish divergence: Price making higher highs, RSI making lower highs
if len(price_highs_idx) >= 2:
idx1, idx2 = price_highs_idx[-2], price_highs_idx[-1]
if prices[idx2] > prices[idx1] and rsi[idx2] < rsi[idx1]:
return {
'type': 'bearish',
'type_cn': '看跌背离',
'indicator': 'RSI',
'description': '价格创新高但RSI走低,可能反转向下',
}
return None
@staticmethod
def _check_macd_divergence(df: pd.DataFrame, lookback: int = 20) -> Optional[Dict[str, Any]]:
"""Check for MACD histogram divergence"""
recent = df.tail(lookback)
prices = recent['close'].values
macd_hist = recent['macd_hist'].values
price_lows_idx = MomentumAnalyzer._find_local_minima(prices)
price_highs_idx = MomentumAnalyzer._find_local_maxima(prices)
# Bullish divergence
if len(price_lows_idx) >= 2:
idx1, idx2 = price_lows_idx[-2], price_lows_idx[-1]
if prices[idx2] < prices[idx1] and macd_hist[idx2] > macd_hist[idx1]:
return {
'type': 'bullish',
'type_cn': '看涨背离',
'indicator': 'MACD',
'description': '价格创新低但MACD柱状图抬高,动能减弱',
}
# Bearish divergence
if len(price_highs_idx) >= 2:
idx1, idx2 = price_highs_idx[-2], price_highs_idx[-1]
if prices[idx2] > prices[idx1] and macd_hist[idx2] < macd_hist[idx1]:
return {
'type': 'bearish',
'type_cn': '看跌背离',
'indicator': 'MACD',
'description': '价格创新高但MACD柱状图走低,动能减弱',
}
return None
@staticmethod
def _check_obv_divergence(df: pd.DataFrame, lookback: int = 20) -> Optional[Dict[str, Any]]:
"""Check for OBV divergence"""
recent = df.tail(lookback)
prices = recent['close'].values
obv = recent['obv'].values
price_lows_idx = MomentumAnalyzer._find_local_minima(prices)
price_highs_idx = MomentumAnalyzer._find_local_maxima(prices)
# Bullish divergence
if len(price_lows_idx) >= 2:
idx1, idx2 = price_lows_idx[-2], price_lows_idx[-1]
if prices[idx2] < prices[idx1] and obv[idx2] > obv[idx1]:
return {
'type': 'bullish',
'type_cn': '看涨背离',
'indicator': 'OBV',
'description': '价格创新低但OBV抬高,资金流入',
}
# Bearish divergence
if len(price_highs_idx) >= 2:
idx1, idx2 = price_highs_idx[-2], price_highs_idx[-1]
if prices[idx2] > prices[idx1] and obv[idx2] < obv[idx1]:
return {
'type': 'bearish',
'type_cn': '看跌背离',
'indicator': 'OBV',
'description': '价格创新高但OBV走低,资金流出',
}
return None
@staticmethod
def _find_local_minima(arr: np.ndarray, window: int = 3) -> List[int]:
"""Find local minima indices"""
minima = []
for i in range(window, len(arr) - window):
if all(arr[i] <= arr[i-j] for j in range(1, window+1)) and \
all(arr[i] <= arr[i+j] for j in range(1, window+1)):
minima.append(i)
return minima
@staticmethod
def _find_local_maxima(arr: np.ndarray, window: int = 3) -> List[int]:
"""Find local maxima indices"""
maxima = []
for i in range(window, len(arr) - window):
if all(arr[i] >= arr[i-j] for j in range(1, window+1)) and \
all(arr[i] >= arr[i+j] for j in range(1, window+1)):
maxima.append(i)
return maxima
@staticmethod
def _assess_overall_momentum(
rsi: Dict[str, Any],
macd: Dict[str, Any],
stoch: Dict[str, Any],
volume: Dict[str, Any],
divergences: Dict[str, Any]
) -> Dict[str, Any]:
"""Assess overall momentum direction and strength"""
# Collect signals
signals = []
# RSI signal
rsi_signal = rsi.get('signal', 'neutral')
if rsi_signal in ['bullish', 'bullish_warning']:
signals.append(1)
elif rsi_signal in ['bearish', 'bearish_warning']:
signals.append(-1)
else:
signals.append(0)
# MACD signal
macd_signal = macd.get('signal', 'neutral')
if macd_signal == 'bullish':
signals.append(1)
elif macd_signal == 'bearish':
signals.append(-1)
else:
signals.append(0)
# Volume confirmation
confirmation = volume.get('confirmation', 'neutral')
if confirmation in ['bullish_confirmed']:
signals.append(1)
elif confirmation in ['bearish_confirmed']:
signals.append(-1)
else:
signals.append(0)
# Divergence override
div_overall = divergences.get('overall', 'none')
divergence_weight = 0
if div_overall == 'bullish':
divergence_weight = 1
elif div_overall == 'bearish':
divergence_weight = -1
# Calculate overall direction
avg_signal = np.mean(signals)
combined = avg_signal * 0.7 + divergence_weight * 0.3
if combined > 0.3:
direction = 'bullish'
direction_cn = '看涨'
elif combined < -0.3:
direction = 'bearish'
direction_cn = '看跌'
else:
direction = 'neutral'
direction_cn = '中性'
# Strength based on signal consistency
bullish_signals = sum(1 for s in signals if s > 0)
bearish_signals = sum(1 for s in signals if s < 0)
max_aligned = max(bullish_signals, bearish_signals)
if max_aligned == len(signals):
strength = 'strong'
strength_cn = ''
elif max_aligned >= len(signals) - 1:
strength = 'moderate'
strength_cn = '中等'
else:
strength = 'weak'
strength_cn = ''
return {
'direction': direction,
'direction_cn': direction_cn,
'strength': strength,
'strength_cn': strength_cn,
'combined_score': round(combined, 2),
'signal_alignment': max_aligned / len(signals) if signals else 0,
'has_divergence_warning': div_overall != 'none',
}
@staticmethod
def _calculate_mtf_alignment(directions: Dict[str, str]) -> Dict[str, Any]:
"""Calculate multi-timeframe momentum alignment"""
if not directions:
return {'status': 'no_data'}
bullish = sum(1 for d in directions.values() if d == 'bullish')
bearish = sum(1 for d in directions.values() if d == 'bearish')
total = len(directions)
if bullish >= total * 0.7:
status = 'aligned_bullish'
status_cn = '多周期看涨一致'
elif bearish >= total * 0.7:
status = 'aligned_bearish'
status_cn = '多周期看跌一致'
elif bullish > bearish:
status = 'mixed_bullish'
status_cn = '偏向看涨但有分歧'
elif bearish > bullish:
status = 'mixed_bearish'
status_cn = '偏向看跌但有分歧'
else:
status = 'conflicting'
status_cn = '多空分歧严重'
return {
'status': status,
'status_cn': status_cn,
'bullish_count': bullish,
'bearish_count': bearish,
'total': total,
'alignment_score': max(bullish, bearish) / total if total > 0 else 0,
}
@staticmethod
def _find_divergence_confluence(results: Dict[str, Dict]) -> Dict[str, Any]:
"""Find divergences appearing across multiple timeframes"""
bullish_tfs = []
bearish_tfs = []
for tf, data in results.items():
div_info = data.get('divergences', {})
if div_info.get('overall') == 'bullish':
bullish_tfs.append(tf)
elif div_info.get('overall') == 'bearish':
bearish_tfs.append(tf)
has_confluence = len(bullish_tfs) >= 2 or len(bearish_tfs) >= 2
if len(bullish_tfs) >= 2:
confluence_type = 'bullish'
confluence_type_cn = '多周期看涨背离'
elif len(bearish_tfs) >= 2:
confluence_type = 'bearish'
confluence_type_cn = '多周期看跌背离'
else:
confluence_type = 'none'
confluence_type_cn = '无背离共振'
return {
'has_confluence': has_confluence,
'type': confluence_type,
'type_cn': confluence_type_cn,
'bullish_timeframes': bullish_tfs,
'bearish_timeframes': bearish_tfs,
}
@staticmethod
def _generate_summary(
rsi: Dict[str, Any],
macd: Dict[str, Any],
volume: Dict[str, Any],
divergences: Dict[str, Any],
assessment: Dict[str, Any]
) -> str:
"""Generate LLM-readable summary"""
parts = []
# Overall assessment
direction_cn = assessment.get('direction_cn', '中性')
strength_cn = assessment.get('strength_cn', '')
parts.append(f"动能: {direction_cn}({strength_cn})")
# RSI
rsi_value = rsi.get('value', 50)
rsi_zone_cn = rsi.get('zone_cn', '中性')
parts.append(f"RSI={rsi_value:.0f}({rsi_zone_cn})")
# MACD
macd_momentum_cn = macd.get('momentum_cn', '')
if macd_momentum_cn:
parts.append(f"MACD: {macd_momentum_cn}")
# Volume
confirmation_cn = volume.get('confirmation_cn', '')
if confirmation_cn:
parts.append(f"量价: {confirmation_cn}")
# Divergence
if divergences.get('detected'):
overall_cn = divergences.get('overall_cn', '')
indicators = [d['indicator'] for d in divergences.get('divergences', [])]
parts.append(f"背离: {overall_cn}({','.join(indicators)})")
return "; ".join(parts)
@staticmethod
def _generate_mtf_summary(
results: Dict[str, Dict],
alignment: Dict[str, Any],
divergence_confluence: Dict[str, Any]
) -> str:
"""Generate multi-timeframe summary"""
parts = []
# Alignment
status_cn = alignment.get('status_cn', '未知')
parts.append(f"动能一致性: {status_cn}")
# Key timeframe momentum
key_tfs = ['1h', '4h', '1d']
tf_parts = []
for tf in key_tfs:
if tf in results:
direction_cn = results[tf]['assessment'].get('direction_cn', '?')
tf_parts.append(f"{tf}={direction_cn}")
if tf_parts:
parts.append(f"关键周期: {', '.join(tf_parts)}")
# Divergence confluence
if divergence_confluence.get('has_confluence'):
type_cn = divergence_confluence.get('type_cn', '')
parts.append(f"重要: {type_cn}")
return "; ".join(parts)
@staticmethod
def _empty_result(timeframe: str) -> Dict[str, Any]:
"""Return empty result when data is insufficient"""
return {
'timeframe': timeframe,
'error': 'insufficient_data',
'rsi': {'value': 50, 'zone_cn': '数据不足'},
'macd': {'signal': 'unknown'},
'stochastic': {},
'volume': {},
'divergences': {'detected': False},
'assessment': {
'direction': 'unknown',
'direction_cn': '数据不足',
'strength': 'unknown',
},
'summary': '数据不足,无法分析',
}