797 lines
26 KiB
Python
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': '数据不足,无法分析',
|
|
}
|