update
This commit is contained in:
parent
75edbdca8d
commit
5cfe9eae83
569
backend/app/stock_agent/analysis_tools.py
Normal file
569
backend/app/stock_agent/analysis_tools.py
Normal file
@ -0,0 +1,569 @@
|
||||
"""
|
||||
股票分析工具模块
|
||||
|
||||
提供各种辅助计算功能:
|
||||
- ATR计算
|
||||
- 多周期共振分析
|
||||
- 基本面评分
|
||||
- 支撑阻力位识别
|
||||
"""
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import Dict, Any, List, Tuple, Optional
|
||||
from app.utils.logger import logger
|
||||
|
||||
|
||||
class StockAnalysisTools:
|
||||
"""股票分析工具类"""
|
||||
|
||||
@staticmethod
|
||||
def calculate_atr(df: pd.DataFrame, period: int = 14) -> float:
|
||||
"""
|
||||
计算ATR (真实波动幅度)
|
||||
|
||||
Args:
|
||||
df: 包含 high, low, close 的数据
|
||||
period: ATR周期
|
||||
|
||||
Returns:
|
||||
ATR值
|
||||
"""
|
||||
if df is None or len(df) < period + 1:
|
||||
return 0.0
|
||||
|
||||
try:
|
||||
high = df['high'].values
|
||||
low = df['low'].values
|
||||
close = df['close'].values
|
||||
|
||||
tr_list = []
|
||||
for i in range(1, len(df)):
|
||||
tr1 = high[i] - low[i]
|
||||
tr2 = abs(high[i] - close[i-1])
|
||||
tr3 = abs(low[i] - close[i-1])
|
||||
tr = max(tr1, tr2, tr3)
|
||||
tr_list.append(tr)
|
||||
|
||||
if len(tr_list) < period:
|
||||
return 0.0
|
||||
|
||||
atr = pd.Series(tr_list).rolling(window=period).mean().iloc[-1]
|
||||
return float(atr) if not np.isnan(atr) else 0.0
|
||||
except Exception as e:
|
||||
logger.warning(f"ATR计算失败: {e}")
|
||||
return 0.0
|
||||
|
||||
@staticmethod
|
||||
def calculate_volume_ratio(df: pd.DataFrame, period: int = 20) -> float:
|
||||
"""
|
||||
计算量比(当前成交量 / 过去N周期平均成交量)
|
||||
|
||||
Args:
|
||||
df: 包含 volume 的数据
|
||||
period: 均量周期
|
||||
|
||||
Returns:
|
||||
量比值
|
||||
"""
|
||||
if df is None or len(df) < period + 1:
|
||||
return 1.0
|
||||
|
||||
try:
|
||||
current_vol = df['volume'].iloc[-1]
|
||||
avg_vol = df['volume'].iloc[-period-1:-1].mean()
|
||||
|
||||
if avg_vol > 0:
|
||||
return float(current_vol / avg_vol)
|
||||
return 1.0
|
||||
except Exception as e:
|
||||
logger.warning(f"量比计算失败: {e}")
|
||||
return 1.0
|
||||
|
||||
@staticmethod
|
||||
def detect_trend(df: pd.DataFrame) -> Dict[str, Any]:
|
||||
"""
|
||||
检测趋势方向和强度
|
||||
|
||||
使用EMA系统判断趋势
|
||||
|
||||
Returns:
|
||||
{
|
||||
'direction': 'uptrend'/'downtrend'/'neutral',
|
||||
'strength': 'strong'/'medium'/'weak',
|
||||
'ema_alignment': 'bullish'/'bearish'/'mixed',
|
||||
'price_vs_ema20': float, # 百分比
|
||||
'signals': List[str]
|
||||
}
|
||||
"""
|
||||
if df is None or len(df) < 200:
|
||||
return {
|
||||
'direction': 'neutral',
|
||||
'strength': 'weak',
|
||||
'ema_alignment': 'mixed',
|
||||
'price_vs_ema20': 0.0,
|
||||
'signals': []
|
||||
}
|
||||
|
||||
try:
|
||||
# 计算EMA
|
||||
close = df['close']
|
||||
ema20 = close.ewm(span=20, adjust=False).mean().iloc[-1]
|
||||
ema50 = close.ewm(span=50, adjust=False).mean().iloc[-1]
|
||||
ema200 = close.ewm(span=200, adjust=False).mean().iloc[-1]
|
||||
|
||||
current_price = close.iloc[-1]
|
||||
|
||||
# EMA排列判断
|
||||
if ema20 > ema50 > ema200:
|
||||
ema_alignment = 'bullish'
|
||||
direction = 'uptrend'
|
||||
elif ema20 < ema50 < ema200:
|
||||
ema_alignment = 'bearish'
|
||||
direction = 'downtrend'
|
||||
else:
|
||||
ema_alignment = 'mixed'
|
||||
direction = 'neutral'
|
||||
|
||||
# 价格与EMA20的关系
|
||||
price_vs_ema20 = ((current_price - ema20) / ema20 * 100) if ema20 > 0 else 0
|
||||
|
||||
# 趋势强度判断
|
||||
strength = 'weak'
|
||||
signals = []
|
||||
|
||||
if ema_alignment == 'bullish':
|
||||
if price_vs_ema20 > 1:
|
||||
strength = 'strong'
|
||||
signals.append("强势上涨:价格站稳 EMA20 之上")
|
||||
elif price_vs_ema20 > 0:
|
||||
strength = 'medium'
|
||||
signals.append("上涨趋势:价格在 EMA20 附近")
|
||||
else:
|
||||
strength = 'weak'
|
||||
signals.append("上涨趋势减弱:价格跌破 EMA20")
|
||||
|
||||
elif ema_alignment == 'bearish':
|
||||
if price_vs_ema20 < -1:
|
||||
strength = 'strong'
|
||||
signals.append("强势下跌:价格跌破 EMA20 之下")
|
||||
elif price_vs_ema20 < 0:
|
||||
strength = 'medium'
|
||||
signals.append("下跌趋势:价格在 EMA20 附近")
|
||||
else:
|
||||
strength = 'weak'
|
||||
signals.append("下跌趋势减弱:价格站上 EMA20")
|
||||
else:
|
||||
signals.append("震荡市:EMA 交织")
|
||||
|
||||
return {
|
||||
'direction': direction,
|
||||
'strength': strength,
|
||||
'ema_alignment': ema_alignment,
|
||||
'price_vs_ema20': round(price_vs_ema20, 2),
|
||||
'signals': signals
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"趋势检测失败: {e}")
|
||||
return {
|
||||
'direction': 'neutral',
|
||||
'strength': 'weak',
|
||||
'ema_alignment': 'mixed',
|
||||
'price_vs_ema20': 0.0,
|
||||
'signals': []
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def calculate_multi_timeframe_resonance(data: Dict[str, pd.DataFrame]) -> Dict[str, Any]:
|
||||
"""
|
||||
计算多周期共振强度
|
||||
|
||||
Args:
|
||||
data: 包含多个周期的数据 {'1w': df, '1d': df, '1h': df}
|
||||
|
||||
Returns:
|
||||
{
|
||||
'score': 0-100,
|
||||
'level': 'strong'/'medium'/'weak',
|
||||
'weekly_trend': str,
|
||||
'daily_trend': str,
|
||||
'hourly_trend': str,
|
||||
'resonance_type': str,
|
||||
'analysis': str
|
||||
}
|
||||
"""
|
||||
trends = {}
|
||||
|
||||
# 获取各周期趋势
|
||||
for tf_name in ['1w', '1d', '1h']:
|
||||
df = data.get(tf_name)
|
||||
if df is not None and len(df) > 0:
|
||||
trend_info = StockAnalysisTools.detect_trend(df)
|
||||
trends[tf_name] = trend_info['direction']
|
||||
else:
|
||||
trends[tf_name] = 'neutral'
|
||||
|
||||
weekly_trend = trends.get('1w', 'neutral')
|
||||
daily_trend = trends.get('1d', 'neutral')
|
||||
hourly_trend = trends.get('1h', 'neutral')
|
||||
|
||||
# 计算共振得分
|
||||
score = 0
|
||||
analysis_parts = []
|
||||
|
||||
# 大周期共振(周线+日线)
|
||||
if weekly_trend == daily_trend and weekly_trend != 'neutral':
|
||||
score += 40
|
||||
analysis_parts.append(f"✅ 大周期共振({weekly_trend})")
|
||||
elif weekly_trend != 'neutral' and daily_trend != 'neutral':
|
||||
analysis_parts.append(f"⚠️ 大周期分歧(周线{weekly_trend} vs 日线{daily_trend})")
|
||||
else:
|
||||
analysis_parts.append(f"➖ 大周期不明确")
|
||||
|
||||
# 主周期共振(日线+1h)
|
||||
if daily_trend == hourly_trend and daily_trend != 'neutral':
|
||||
score += 35
|
||||
analysis_parts.append(f"✅ 主周期共振({daily_trend})")
|
||||
elif daily_trend != 'neutral' and hourly_trend != 'neutral':
|
||||
analysis_parts.append(f"⚠️ 主周期分歧(日线{daily_trend} vs 1h{hourly_trend})")
|
||||
else:
|
||||
analysis_parts.append(f"➖ 主周期不明确")
|
||||
|
||||
# 全周期共振
|
||||
if weekly_trend == daily_trend == hourly_trend and weekly_trend != 'neutral':
|
||||
score += 25
|
||||
analysis_parts.append(f"🔥 全周期共振({weekly_trend})")
|
||||
|
||||
# 确定共振等级
|
||||
if score >= 70:
|
||||
level = 'strong'
|
||||
resonance_type = 'all_timeframe_aligned' if score >= 90 else 'strong'
|
||||
elif score >= 40:
|
||||
level = 'medium'
|
||||
resonance_type = 'large_timeframe_aligned'
|
||||
elif score >= 20:
|
||||
level = 'weak'
|
||||
resonance_type = 'partial_alignment'
|
||||
else:
|
||||
level = 'weak'
|
||||
resonance_type = 'no_resonance'
|
||||
|
||||
return {
|
||||
'score': score,
|
||||
'level': level,
|
||||
'weekly_trend': weekly_trend,
|
||||
'daily_trend': daily_trend,
|
||||
'hourly_trend': hourly_trend,
|
||||
'resonance_type': resonance_type,
|
||||
'analysis': ' | '.join(analysis_parts)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def calculate_fundamental_score(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
计算基本面评分(0-100)
|
||||
|
||||
Args:
|
||||
data: 基本面数据
|
||||
|
||||
Returns:
|
||||
{
|
||||
'score': 0-100,
|
||||
'grade': 'A'/'B'/'C'/'D',
|
||||
'valuation': {'score': 0-25, 'grade': 'A'/'B'/'C'/'D'},
|
||||
'profitability': {'score': 0-35, 'grade': 'A'/'B'/'C'/'D'},
|
||||
'growth': {'score': 0-25, 'grade': 'A'/'B'/'C'/'D'},
|
||||
'financial_health': {'score': 0-15, 'grade': 'A'/'B'/'C'/'D'},
|
||||
'summary': str
|
||||
}
|
||||
"""
|
||||
if not data:
|
||||
return {
|
||||
'score': 0,
|
||||
'grade': 'D',
|
||||
'summary': '无基本面数据'
|
||||
}
|
||||
|
||||
score = 0
|
||||
breakdown = {}
|
||||
|
||||
# 估值评分 (25分)
|
||||
val_score = 0
|
||||
pe = data.get('pe_ratio', 0)
|
||||
pb = data.get('pb_ratio', 0)
|
||||
peg = data.get('peg_ratio', 0)
|
||||
|
||||
if 0 < pe < 15:
|
||||
val_score += 10
|
||||
elif 15 <= pe <= 25:
|
||||
val_score += 7
|
||||
elif pe > 40:
|
||||
val_score += 0
|
||||
else:
|
||||
val_score += 5
|
||||
|
||||
if 0 < pb < 1:
|
||||
val_score += 10
|
||||
elif 1 <= pb <= 3:
|
||||
val_score += 7
|
||||
elif pb > 5:
|
||||
val_score += 0
|
||||
else:
|
||||
val_score += 5
|
||||
|
||||
if peg and 0 < peg < 1:
|
||||
val_score += 5
|
||||
elif peg and 1 <= peg <= 2:
|
||||
val_score += 3
|
||||
elif peg and peg > 2:
|
||||
val_score += 0
|
||||
else:
|
||||
val_score += 2
|
||||
|
||||
breakdown['valuation'] = {
|
||||
'score': val_score,
|
||||
'grade': 'A' if val_score >= 20 else 'B' if val_score >= 15 else 'C' if val_score >= 10 else 'D'
|
||||
}
|
||||
score += val_score
|
||||
|
||||
# 盈利能力评分 (35分)
|
||||
prof_score = 0
|
||||
roe = data.get('roe', 0)
|
||||
net_margin = data.get('profit_margin', 0)
|
||||
|
||||
if roe > 20:
|
||||
prof_score += 20
|
||||
elif roe > 15:
|
||||
prof_score += 15
|
||||
elif roe > 10:
|
||||
prof_score += 10
|
||||
elif roe > 0:
|
||||
prof_score += 5
|
||||
|
||||
if net_margin > 20:
|
||||
prof_score += 15
|
||||
elif net_margin > 10:
|
||||
prof_score += 10
|
||||
elif net_margin > 5:
|
||||
prof_score += 5
|
||||
elif net_margin > 0:
|
||||
prof_score += 2
|
||||
|
||||
breakdown['profitability'] = {
|
||||
'score': prof_score,
|
||||
'grade': 'A' if prof_score >= 30 else 'B' if prof_score >= 20 else 'C' if prof_score >= 10 else 'D'
|
||||
}
|
||||
score += prof_score
|
||||
|
||||
# 成长性评分 (25分)
|
||||
growth_score = 0
|
||||
revenue_growth = data.get('revenue_growth', 0)
|
||||
earnings_growth = data.get('earnings_growth', 0)
|
||||
|
||||
if revenue_growth > 30:
|
||||
growth_score += 13
|
||||
elif revenue_growth > 20:
|
||||
growth_score += 10
|
||||
elif revenue_growth > 10:
|
||||
growth_score += 5
|
||||
elif revenue_growth > 0:
|
||||
growth_score += 2
|
||||
|
||||
if earnings_growth > 30:
|
||||
growth_score += 12
|
||||
elif earnings_growth > 20:
|
||||
growth_score += 8
|
||||
elif earnings_growth > 10:
|
||||
growth_score += 5
|
||||
elif earnings_growth > 0:
|
||||
growth_score += 2
|
||||
elif earnings_growth < 0:
|
||||
growth_score -= 5 # 负增长扣分
|
||||
|
||||
breakdown['growth'] = {
|
||||
'score': max(0, growth_score),
|
||||
'grade': 'A' if growth_score >= 20 else 'B' if growth_score >= 15 else 'C' if growth_score >= 10 else 'D'
|
||||
}
|
||||
score += max(0, growth_score)
|
||||
|
||||
# 财务健康评分 (15分)
|
||||
health_score = 0
|
||||
debt_ratio = data.get('debt_to_equity', 0)
|
||||
current_ratio = data.get('current_ratio', 0)
|
||||
|
||||
if debt_ratio < 1:
|
||||
health_score += 8
|
||||
elif debt_ratio < 2:
|
||||
health_score += 5
|
||||
elif debt_ratio < 3:
|
||||
health_score += 2
|
||||
else:
|
||||
health_score += 0
|
||||
|
||||
if current_ratio > 2:
|
||||
health_score += 7
|
||||
elif current_ratio > 1.5:
|
||||
health_score += 5
|
||||
elif current_ratio > 1:
|
||||
health_score += 2
|
||||
else:
|
||||
health_score += 0
|
||||
|
||||
breakdown['financial_health'] = {
|
||||
'score': health_score,
|
||||
'grade': 'A' if health_score >= 12 else 'B' if health_score >= 8 else 'C' if health_score >= 5 else 'D'
|
||||
}
|
||||
score += health_score
|
||||
|
||||
# 确定总等级
|
||||
grade = 'A' if score >= 80 else 'B' if score >= 60 else 'C' if score >= 40 else 'D'
|
||||
|
||||
# 生成摘要
|
||||
summary_parts = []
|
||||
if breakdown['valuation']['grade'] == 'A':
|
||||
summary_parts.append("估值低估")
|
||||
elif breakdown['valuation']['grade'] == 'D':
|
||||
summary_parts.append("估值高估")
|
||||
|
||||
if breakdown['profitability']['grade'] == 'A':
|
||||
summary_parts.append("盈利优秀")
|
||||
elif breakdown['profitability']['grade'] == 'D':
|
||||
summary_parts.append("盈利较差")
|
||||
|
||||
if breakdown['growth']['grade'] == 'A':
|
||||
summary_parts.append("高成长")
|
||||
elif breakdown['growth']['grade'] == 'D':
|
||||
summary_parts.append("低成长")
|
||||
|
||||
if breakdown['financial_health']['grade'] == 'A':
|
||||
summary_parts.append("财务健康")
|
||||
elif breakdown['financial_health']['grade'] == 'D':
|
||||
summary_parts.append("财务风险")
|
||||
|
||||
return {
|
||||
'score': score,
|
||||
'grade': grade,
|
||||
'breakdown': breakdown,
|
||||
'summary': ' | '.join(summary_parts) if summary_parts else '一般'
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def identify_key_levels(df: pd.DataFrame, lookback: int = 60) -> Dict[str, List[float]]:
|
||||
"""
|
||||
识别关键支撑位和阻力位
|
||||
|
||||
Args:
|
||||
df: K线数据
|
||||
lookback: 回看周期
|
||||
|
||||
Returns:
|
||||
{
|
||||
'support': [支撑位1, 支撑位2, ...],
|
||||
'resistance': [阻力位1, 阻力位2, ...]
|
||||
}
|
||||
"""
|
||||
if df is None or len(df) < lookback:
|
||||
return {'support': [], 'resistance': []}
|
||||
|
||||
try:
|
||||
recent = df.tail(lookback)
|
||||
highs = recent['high'].values
|
||||
lows = recent['low'].values
|
||||
|
||||
# 找局部高点和低点
|
||||
from scipy.signal import argrelextrema
|
||||
from numpy import array
|
||||
|
||||
high_indices = argrelextrema(array(highs), np.greater, order=5)
|
||||
low_indices = argrelextrema(array(lows), np.less, order=5)
|
||||
|
||||
resistance_levels = sorted([highs[i] for i in high_indices], reverse=True)[:3]
|
||||
support_levels = sorted([lows[i] for i in low_indices])[:3]
|
||||
|
||||
return {
|
||||
'resistance': resistance_levels,
|
||||
'support': support_levels
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"关键位识别失败: {e}")
|
||||
return {'support': [], 'resistance': []}
|
||||
|
||||
@staticmethod
|
||||
def calculate_stop_loss_take_profit(
|
||||
entry_price: float,
|
||||
atr: float,
|
||||
direction: str,
|
||||
key_levels: Dict[str, List[float]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
计算止损止盈价格
|
||||
|
||||
Args:
|
||||
entry_price: 入场价格
|
||||
atr: ATR值
|
||||
direction: 'long'/'short'
|
||||
key_levels: 支撑阻力位
|
||||
|
||||
Returns:
|
||||
{
|
||||
'stop_loss': float,
|
||||
'take_profit': float,
|
||||
'method': str,
|
||||
'risk_reward_ratio': float
|
||||
}
|
||||
"""
|
||||
if direction == 'long':
|
||||
# 做多
|
||||
# 止损:入场价 - 1.5×ATR,或设在前支撑位下方
|
||||
sl_atr = entry_price - 1.5 * atr
|
||||
sl_support = None
|
||||
|
||||
if key_levels and key_levels.get('support'):
|
||||
closest_support = [s for s in key_levels['support'] if s < entry_price]
|
||||
if closest_support:
|
||||
sl_support = min(closest_support) * 0.995 # 支撑位下方0.5%
|
||||
|
||||
# 选择更保守的止损(更远的)
|
||||
if sl_support and sl_support < sl_atr:
|
||||
stop_loss = sl_atr
|
||||
else:
|
||||
stop_loss = sl_atr
|
||||
|
||||
# 止盈:入场价 + 3×ATR(风险收益比1:2)
|
||||
take_profit = entry_price + 3 * atr
|
||||
|
||||
else:
|
||||
# 做空
|
||||
# 止损:入场价 + 1.5×ATR,或设在前阻力位上方
|
||||
sl_atr = entry_price + 1.5 * atr
|
||||
sl_resistance = None
|
||||
|
||||
if key_levels and key_levels.get('resistance'):
|
||||
closest_resistance = [r for r in key_levels['resistance'] if r > entry_price]
|
||||
if closest_resistance:
|
||||
sl_resistance = min(closest_resistance) * 1.005 # 阻力位上方0.5%
|
||||
|
||||
# 选择更保守的止损
|
||||
if sl_resistance and sl_resistance > sl_atr:
|
||||
stop_loss = sl_atr
|
||||
else:
|
||||
stop_loss = sl_atr
|
||||
|
||||
# 止盈:入场价 - 3×ATR
|
||||
take_profit = entry_price - 3 * atr
|
||||
|
||||
# 计算风险收益比
|
||||
if direction == 'long':
|
||||
risk = entry_price - stop_loss
|
||||
reward = take_profit - entry_price
|
||||
else:
|
||||
risk = stop_loss - entry_price
|
||||
reward = entry_price - take_profit
|
||||
|
||||
risk_reward_ratio = reward / risk if risk > 0 else 0
|
||||
|
||||
return {
|
||||
'stop_loss': round(stop_loss, 2),
|
||||
'take_profit': round(take_profit, 2),
|
||||
'method': 'ATR-based (1:2 risk-reward)',
|
||||
'risk_reward_ratio': round(risk_reward_ratio, 2)
|
||||
}
|
||||
@ -308,6 +308,45 @@ class StockMarketSignalAnalyzer:
|
||||
- 必须同时输出 `entry_price`(建议入场价)和 `entry_type`(入场方式)
|
||||
- 入场方式由你的市场分析判断,不是简单的价格距离计算
|
||||
|
||||
## 止损止盈计算规则
|
||||
使用ATR(真实波动幅度)动态计算止损止盈:
|
||||
|
||||
**做多**:
|
||||
- 止损 = 入场价 - 1.5 × ATR(14日)
|
||||
- 止盈 = 入场价 + 3 × ATR(14日) (风险收益比 1:2)
|
||||
- 如果附近有明显支撑位,可将止损设在支撑位下方
|
||||
|
||||
**做空**:
|
||||
- 止损 = 入场价 + 1.5 × ATR(14日)
|
||||
- 止盈 = 入场价 - 3 × ATR(14日) (风险收益比 1:2)
|
||||
- 如果附近有明显阻力位,可将止损设在阻力位上方
|
||||
|
||||
## 信号输出条件(严格遵守)
|
||||
**满足以下条件才输出信号(否则 signals 返回空数组)**:
|
||||
|
||||
### 做多信号条件:
|
||||
1. ✅ 趋势:EMA20 > EMA50 > EMA200(或处于回调中的上升趋势)
|
||||
2. ✅ 价格:站稳 EMA20 之上或回调到 EMA20 附近
|
||||
3. ✅ 量价:放量上涨或缩量回调后重新放量
|
||||
4. ✅ 共振:多周期共振得分 >= 40(至少大周期一致)
|
||||
5. ✅ 基本面:评分 >= C级(40分以上)
|
||||
6. ✅ 新闻:无重大负面消息(财报暴雷、监管处罚等)
|
||||
|
||||
### 做空信号条件:
|
||||
1. ✅ 趋势:EMA20 < EMA50 < EMA200(或处于反弹中的下降趋势)
|
||||
2. ✅ 价格:跌破 EMA20 或反弹到 EMA20 附近
|
||||
3. ✅ 量价:放量下跌或缩量反弹后继续下跌
|
||||
4. ✅ 共振:多周期共振得分 >= 40(至少大周期一致)
|
||||
5. ✅ 基本面:评分 >= C级(40分以上)
|
||||
6. ✅ 新闻:无重大正面消息
|
||||
|
||||
### 禁止输出信号的情况:
|
||||
- ❌ 趋势不明确(EMA 交织,震荡市)
|
||||
- ❌ 多周期方向完全相反
|
||||
- ❌ 基本面差(D级,<40分)
|
||||
- ❌ 有重大负面新闻(做多时)或重大正面新闻(做空时)
|
||||
- ❌ 技术指标矛盾(如RSI超买但MACD死叉)
|
||||
|
||||
## 输出格式
|
||||
请严格按照以下 JSON 格式输出:
|
||||
|
||||
@ -416,128 +455,89 @@ class StockMarketSignalAnalyzer:
|
||||
symbols: List[str] = None,
|
||||
fundamental_data: Dict[str, Any] = None,
|
||||
news_data: List[Dict[str, Any]] = None) -> str:
|
||||
"""准备市场上下文信息(技术面 + 基本面 + 新闻)"""
|
||||
"""准备市场上下文信息(结构化版本,减少token消耗)"""
|
||||
from app.stock_agent.analysis_tools import StockAnalysisTools
|
||||
|
||||
context_parts = []
|
||||
|
||||
# 当前价格和24h变化(使用日线数据)
|
||||
# 当前价格和24h变化
|
||||
df_1d = data.get('1d')
|
||||
if df_1d is None or len(df_1d) == 0:
|
||||
df_1h = data.get('1h') # 备用:使用1h数据
|
||||
if df_1d is None or len(df_1d) == 0:
|
||||
return "" # 没有数据就返回空
|
||||
return ""
|
||||
|
||||
current_price = float(df_1d.iloc[-1]['close'])
|
||||
price_change_24h = self._calculate_price_change_24h(df_1d)
|
||||
context_parts.append(f"当前价格: ${current_price:,.2f} ({price_change_24h})")
|
||||
context_parts.append(f"**当前价格**: ${current_price:,.2f} ({price_change_24h})")
|
||||
|
||||
# 多周期数据
|
||||
for tf_name, df in data.items():
|
||||
if df is None or len(df) == 0:
|
||||
continue
|
||||
# ===== 趋势分析(预计算) =====
|
||||
context_parts.append(f"\n**## 趋势分析**")
|
||||
trend_info = StockAnalysisTools.detect_trend(df_1d)
|
||||
context_parts.append(f"- 方向: {trend_info['direction']} ({trend_info['strength']})")
|
||||
context_parts.append(f"- EMA排列: {trend_info['ema_alignment']}")
|
||||
context_parts.append(f"- 价格相对EMA20: {trend_info['price_vs_ema20']:+.2f}%")
|
||||
|
||||
latest = df.iloc[-1]
|
||||
context_parts.append(f"\n## {tf_name} 数据")
|
||||
context_parts.append(f"开: {latest['open']}, 高: {latest['high']}, 低: {latest['low']}, 收: {latest['close']}")
|
||||
context_parts.append(f"成交量: {latest.get('volume', 'N/A')}")
|
||||
# 多周期共振(预计算)
|
||||
context_parts.append(f"\n**## 多周期共振**")
|
||||
resonance = StockAnalysisTools.calculate_multi_timeframe_resonance(data)
|
||||
context_parts.append(f"- 共振等级: {resonance['level'].upper()} (得分: {resonance['score']}/100)")
|
||||
context_parts.append(f"- 共振分析: {resonance['analysis']}")
|
||||
context_parts.append(f"- 周线: {resonance['weekly_trend']} | 日线: {resonance['daily_trend']} | 1h: {resonance['hourly_trend']}")
|
||||
|
||||
# 技术指标
|
||||
if 'rsi' in df.columns:
|
||||
rsi = df['rsi'].iloc[-1]
|
||||
context_parts.append(f"RSI: {rsi:.2f}")
|
||||
if 'macd' in df.columns:
|
||||
macd = df['macd'].iloc[-1]
|
||||
signal = df['macd_signal'].iloc[-1]
|
||||
context_parts.append(f"MACD: {macd:.4f}, 信号线: {signal:.4f}")
|
||||
if 'bb_upper' in df.columns:
|
||||
bb_upper = df['bb_upper'].iloc[-1]
|
||||
bb_lower = df['bb_lower'].iloc[-1]
|
||||
context_parts.append(f"布林带: 上轨 {bb_upper:.2f}, 下轨 {bb_lower:.2f}")
|
||||
# ===== 技术指标(只显示日线关键指标) =====
|
||||
context_parts.append(f"\n**## 技术指标(日线)**")
|
||||
latest = df_1d.iloc[-1]
|
||||
|
||||
# 均线系统(使用日线数据)
|
||||
context_parts.append(f"\n## 均线系统")
|
||||
df_1d = data.get('1d')
|
||||
if df_1d is not None and len(df_1d) > 0:
|
||||
latest = df_1d.iloc[-1]
|
||||
context_parts.append(f"MA5: {latest.get('ma5', 'N/A')}")
|
||||
context_parts.append(f"MA10: {latest.get('ma10', 'N/A')}")
|
||||
context_parts.append(f"MA20: {latest.get('ma20', 'N/A')}")
|
||||
context_parts.append(f"MA50: {latest.get('ma50', 'N/A')}")
|
||||
# RSI
|
||||
if 'rsi' in df_1d.columns:
|
||||
rsi = df_1d['rsi'].iloc[-1]
|
||||
context_parts.append(f"- RSI(14): {rsi:.1f} {'(超买⚠️)' if rsi > 70 else '(超卖💡)' if rsi < 30 else '(正常)'}")
|
||||
|
||||
# EMA 均线(用于趋势判断)
|
||||
ema20 = latest.get('ema20', None)
|
||||
ema50 = latest.get('ema50', None)
|
||||
ema200 = latest.get('ema200', None)
|
||||
if ema20 is not None:
|
||||
context_parts.append(f"EMA20: {ema20:.2f}")
|
||||
if ema50 is not None:
|
||||
context_parts.append(f"EMA50: {ema50:.2f}")
|
||||
if ema200 is not None:
|
||||
context_parts.append(f"EMA200: {ema200:.2f}")
|
||||
# MACD
|
||||
if 'macd' in df_1d.columns:
|
||||
macd = df_1d['macd'].iloc[-1]
|
||||
signal = df_1d['macd_signal'].iloc[-1]
|
||||
context_parts.append(f"- MACD: {macd:.4f} (信号: {signal:.4f})")
|
||||
|
||||
# 判断 MA 排列
|
||||
ma5 = latest.get('ma5', 0)
|
||||
ma10 = latest.get('ma10', 0)
|
||||
ma20 = latest.get('ma20', 0)
|
||||
ma50 = latest.get('ma50', 0)
|
||||
# 布林带
|
||||
if 'bb_upper' in df_1d.columns:
|
||||
bb_upper = df_1d['bb_upper'].iloc[-1]
|
||||
bb_lower = df_1d['bb_lower'].iloc[-1]
|
||||
bb_position = (current_price - bb_lower) / (bb_upper - bb_lower) * 100 if bb_upper != bb_lower else 50
|
||||
context_parts.append(f"- 布林带: [{bb_lower:.2f}, {bb_upper:.2f}] (位置: {bb_position:.0f}%)")
|
||||
|
||||
if all([ma5, ma10, ma20, ma50]):
|
||||
if ma5 > ma10 > ma20 > ma50:
|
||||
context_parts.append("MA排列: 多头排列 📈")
|
||||
elif ma5 < ma10 < ma20 < ma50:
|
||||
context_parts.append("MA排列: 空头排列 📉")
|
||||
else:
|
||||
context_parts.append("MA排列: 交织,方向不明")
|
||||
# 量价分析
|
||||
volume_ratio = StockAnalysisTools.calculate_volume_ratio(df_1d, 20)
|
||||
context_parts.append(f"- 量比: {volume_ratio:.2f}x {'放量📊' if volume_ratio > 1.5 else '缩量📉' if volume_ratio < 0.7 else '平量'}")
|
||||
|
||||
# EMA 排列(更重要的趋势判断)
|
||||
if all([ema20, ema50, ema200]):
|
||||
if ema20 > ema50 > ema200:
|
||||
context_parts.append("EMA排列: 多头排列 (长期趋势向上) 📈")
|
||||
elif ema20 < ema50 < ema200:
|
||||
context_parts.append("EMA排列: 空头排列 (长期趋势向下) 📉")
|
||||
else:
|
||||
context_parts.append("EMA排列: 交织 (震荡市) ➡️")
|
||||
# ATR(用于止损止盈)
|
||||
atr = StockAnalysisTools.calculate_atr(df_1d, 14)
|
||||
atr_pct = (atr / current_price * 100) if current_price > 0 else 0
|
||||
context_parts.append(f"- ATR(14): ${atr:.2f} ({atr_pct:.2f}%)")
|
||||
|
||||
# 价格与 EMA20 的关系(趋势判断关键)
|
||||
if ema20 is not None:
|
||||
if latest['close'] > ema20:
|
||||
context_parts.append("价格位置: 站稳 EMA20 之上 (多头强势)")
|
||||
elif latest['close'] < ema20:
|
||||
context_parts.append("价格位置: 跌破 EMA20 (空头强势)")
|
||||
else:
|
||||
context_parts.append("价格位置: 接近 EMA20")
|
||||
# ===== 关键支撑阻力位 =====
|
||||
context_parts.append(f"\n**## 关键位**")
|
||||
key_levels = StockAnalysisTools.identify_key_levels(df_1d, lookback=60)
|
||||
if key_levels['resistance']:
|
||||
context_parts.append(f"- 阻力位: ${', '.join(f'{r:.2f}' for r in key_levels['resistance'][:3])}")
|
||||
else:
|
||||
context_parts.append(f"- 阻力位: 未明确")
|
||||
if key_levels['support']:
|
||||
context_parts.append(f"- 支撑位: ${', '.join(f'{s:.2f}' for s in key_levels['support'][:3])}")
|
||||
else:
|
||||
context_parts.append(f"- 支撑位: 未明确")
|
||||
|
||||
# 量比分析(使用日线数据)
|
||||
df_1d = data.get('1d')
|
||||
if df_1d is not None and len(df_1d) >= 20:
|
||||
vol_latest = df_1d['volume'].iloc[-1]
|
||||
vol_ma20 = df_1d['volume'].iloc[-20:-1].mean()
|
||||
volume_ratio = vol_latest / vol_ma20 if vol_ma20 > 0 else 1
|
||||
context_parts.append(f"\n## 量价分析")
|
||||
context_parts.append(f"最新成交量: {vol_latest:.0f}")
|
||||
context_parts.append(f"20周期均量: {vol_ma20:.0f}")
|
||||
context_parts.append(f"量比: {volume_ratio:.2f}")
|
||||
|
||||
if volume_ratio > 1.5:
|
||||
context_parts.append("量价状态: 放量 📊")
|
||||
elif volume_ratio < 0.7:
|
||||
context_parts.append("量价状态: 缩量 📉")
|
||||
else:
|
||||
context_parts.append("量价状态: 平量 ➖")
|
||||
|
||||
# 波动率分析
|
||||
volatility_analysis = self._analyze_volatility(data)
|
||||
if volatility_analysis:
|
||||
context_parts.append(f"\n## 波动率分析")
|
||||
context_parts.append(volatility_analysis)
|
||||
|
||||
# 基本面分析
|
||||
# ===== 基本面分析(评分) =====
|
||||
if fundamental_data:
|
||||
context_parts.append(f"\n## 基本面分析")
|
||||
context_parts.append(self._format_fundamental_data(fundamental_data))
|
||||
context_parts.append(f"\n**## 基本面分析**")
|
||||
fund_score = StockAnalysisTools.calculate_fundamental_score(fundamental_data)
|
||||
context_parts.append(f"- 综合评分: {fund_score['grade']}级 ({fund_score['score']}/100)")
|
||||
context_parts.append(f"- 估值: {fund_score['breakdown']['valuation']['grade']}级 | 盈利: {fund_score['breakdown']['profitability']['grade']}级")
|
||||
context_parts.append(f"- 成长: {fund_score['breakdown']['growth']['grade']}级 | 财务: {fund_score['breakdown']['financial_health']['grade']}级")
|
||||
context_parts.append(f"- 摘要: {fund_score['summary']}")
|
||||
|
||||
# 新闻舆情
|
||||
# ===== 新闻舆情 =====
|
||||
if news_data:
|
||||
context_parts.append(f"\n## 最新新闻")
|
||||
context_parts.append(f"\n**## 最新新闻**")
|
||||
context_parts.append(self._format_news_data(news_data))
|
||||
|
||||
return "\n".join(context_parts)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user