|
|
|
"""
|
|
智能分析系统(股票) - 股票市场数据分析系统
|
|
修改:熊猫大侠
|
|
版本:v2.1.0
|
|
许可证:MIT License
|
|
"""
|
|
|
|
import time
|
|
import traceback
|
|
import pandas as pd
|
|
import numpy as np
|
|
from datetime import datetime, timedelta
|
|
import os
|
|
import requests
|
|
from typing import Dict, List, Optional, Tuple
|
|
from dotenv import load_dotenv
|
|
import logging
|
|
import math
|
|
import json
|
|
import threading
|
|
|
|
|
|
thread_local = threading.local()
|
|
|
|
|
|
class StockAnalyzer:
|
|
"""
|
|
股票分析器 - 原有API保持不变,内部实现增强
|
|
"""
|
|
|
|
def __init__(self, initial_cash=1000000):
|
|
|
|
logging.basicConfig(level=logging.INFO,
|
|
format='%(asctime)s - %(levelname)s - %(message)s')
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
|
|
load_dotenv()
|
|
|
|
|
|
self.openai_api_key = os.getenv('OPENAI_API_KEY', os.getenv('OPENAI_API_KEY'))
|
|
self.openai_api_url = os.getenv('OPENAI_API_URL', 'https://api.openai.com/v1')
|
|
self.openai_model = os.getenv('OPENAI_API_MODEL', 'gemini-2.0-pro-exp-02-05')
|
|
self.news_model = os.getenv('NEWS_MODEL')
|
|
|
|
|
|
self.params = {
|
|
'ma_periods': {'short': 5, 'medium': 20, 'long': 60},
|
|
'rsi_period': 14,
|
|
'bollinger_period': 20,
|
|
'bollinger_std': 2,
|
|
'volume_ma_period': 20,
|
|
'atr_period': 14
|
|
}
|
|
|
|
|
|
self.data_cache = {}
|
|
|
|
|
|
self.json_match_flag = True
|
|
def get_stock_data(self, stock_code, market_type='A', start_date=None, end_date=None):
|
|
"""获取股票数据"""
|
|
import akshare as ak
|
|
|
|
self.logger.info(f"开始获取股票 {stock_code} 数据,市场类型: {market_type}")
|
|
|
|
cache_key = f"{stock_code}_{market_type}_{start_date}_{end_date}_price"
|
|
if cache_key in self.data_cache:
|
|
cached_df = self.data_cache[cache_key]
|
|
|
|
|
|
result = cached_df.copy()
|
|
|
|
if 'date' in result.columns and not pd.api.types.is_datetime64_any_dtype(result['date']):
|
|
try:
|
|
result['date'] = pd.to_datetime(result['date'])
|
|
except Exception as e:
|
|
self.logger.warning(f"无法将日期列转换为datetime格式: {str(e)}")
|
|
return result
|
|
|
|
if start_date is None:
|
|
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
|
|
if end_date is None:
|
|
end_date = datetime.now().strftime('%Y%m%d')
|
|
|
|
try:
|
|
|
|
if market_type == 'A':
|
|
df = ak.stock_zh_a_hist(
|
|
symbol=stock_code,
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
adjust="qfq"
|
|
)
|
|
elif market_type == 'HK':
|
|
df = ak.stock_hk_daily(
|
|
symbol=stock_code,
|
|
adjust="qfq"
|
|
)
|
|
elif market_type == 'US':
|
|
df = ak.stock_us_hist(
|
|
symbol=stock_code,
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
adjust="qfq"
|
|
)
|
|
else:
|
|
raise ValueError(f"不支持的市场类型: {market_type}")
|
|
|
|
|
|
df = df.rename(columns={
|
|
"日期": "date",
|
|
"开盘": "open",
|
|
"收盘": "close",
|
|
"最高": "high",
|
|
"最低": "low",
|
|
"成交量": "volume",
|
|
"成交额": "amount"
|
|
})
|
|
|
|
|
|
df['date'] = pd.to_datetime(df['date'])
|
|
|
|
|
|
numeric_columns = ['open', 'close', 'high', 'low', 'volume']
|
|
for col in numeric_columns:
|
|
if col in df.columns:
|
|
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
|
|
|
|
df = df.dropna()
|
|
|
|
result = df.sort_values('date')
|
|
|
|
|
|
self.data_cache[cache_key] = result.copy()
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"获取股票数据失败: {e}")
|
|
raise Exception(f"获取股票数据失败: {e}")
|
|
|
|
def get_north_flow_history(self, stock_code, start_date=None, end_date=None):
|
|
"""获取单个股票的北向资金历史持股数据"""
|
|
try:
|
|
import akshare as ak
|
|
|
|
|
|
if start_date is None and end_date is None:
|
|
|
|
north_hist_data = ak.stock_hsgt_hist_em(symbol=stock_code)
|
|
else:
|
|
north_hist_data = ak.stock_hsgt_hist_em(symbol=stock_code, start_date=start_date, end_date=end_date)
|
|
|
|
if north_hist_data.empty:
|
|
return {"history": []}
|
|
|
|
|
|
history = []
|
|
for _, row in north_hist_data.iterrows():
|
|
history.append({
|
|
"date": row.get('日期', ''),
|
|
"holding": float(row.get('持股数', 0)) if '持股数' in row else 0,
|
|
"ratio": float(row.get('持股比例', 0)) if '持股比例' in row else 0,
|
|
"change": float(row.get('持股变动', 0)) if '持股变动' in row else 0,
|
|
"market_value": float(row.get('持股市值', 0)) if '持股市值' in row else 0
|
|
})
|
|
|
|
return {"history": history}
|
|
except Exception as e:
|
|
self.logger.error(f"获取北向资金历史数据出错: {str(e)}")
|
|
return {"history": []}
|
|
|
|
def calculate_ema(self, series, period):
|
|
"""计算指数移动平均线"""
|
|
return series.ewm(span=period, adjust=False).mean()
|
|
|
|
def calculate_rsi(self, series, period):
|
|
"""计算RSI指标"""
|
|
delta = series.diff()
|
|
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
|
|
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
|
|
rs = gain / loss
|
|
return 100 - (100 / (1 + rs))
|
|
|
|
def calculate_macd(self, series):
|
|
"""计算MACD指标"""
|
|
exp1 = series.ewm(span=12, adjust=False).mean()
|
|
exp2 = series.ewm(span=26, adjust=False).mean()
|
|
macd = exp1 - exp2
|
|
signal = macd.ewm(span=9, adjust=False).mean()
|
|
hist = macd - signal
|
|
return macd, signal, hist
|
|
|
|
def calculate_bollinger_bands(self, series, period, std_dev):
|
|
"""计算布林带"""
|
|
middle = series.rolling(window=period).mean()
|
|
std = series.rolling(window=period).std()
|
|
upper = middle + (std * std_dev)
|
|
lower = middle - (std * std_dev)
|
|
return upper, middle, lower
|
|
|
|
def calculate_atr(self, df, period):
|
|
"""计算ATR指标"""
|
|
high = df['high']
|
|
low = df['low']
|
|
close = df['close'].shift(1)
|
|
|
|
tr1 = high - low
|
|
tr2 = abs(high - close)
|
|
tr3 = abs(low - close)
|
|
|
|
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
|
|
return tr.rolling(window=period).mean()
|
|
|
|
def format_indicator_data(self, df):
|
|
"""格式化指标数据,控制小数位数"""
|
|
|
|
|
|
price_columns = ['open', 'close', 'high', 'low', 'MA5', 'MA20', 'MA60', 'BB_upper', 'BB_middle', 'BB_lower']
|
|
for col in price_columns:
|
|
if col in df.columns:
|
|
df[col] = df[col].round(2)
|
|
|
|
|
|
macd_columns = ['MACD', 'Signal', 'MACD_hist']
|
|
for col in macd_columns:
|
|
if col in df.columns:
|
|
df[col] = df[col].round(3)
|
|
|
|
|
|
other_columns = ['RSI', 'Volatility', 'ROC', 'Volume_Ratio']
|
|
for col in other_columns:
|
|
if col in df.columns:
|
|
df[col] = df[col].round(2)
|
|
|
|
return df
|
|
|
|
def calculate_indicators(self, df):
|
|
"""计算技术指标"""
|
|
|
|
try:
|
|
|
|
df['MA5'] = self.calculate_ema(df['close'], self.params['ma_periods']['short'])
|
|
df['MA20'] = self.calculate_ema(df['close'], self.params['ma_periods']['medium'])
|
|
df['MA60'] = self.calculate_ema(df['close'], self.params['ma_periods']['long'])
|
|
|
|
|
|
df['RSI'] = self.calculate_rsi(df['close'], self.params['rsi_period'])
|
|
|
|
|
|
df['MACD'], df['Signal'], df['MACD_hist'] = self.calculate_macd(df['close'])
|
|
|
|
|
|
df['BB_upper'], df['BB_middle'], df['BB_lower'] = self.calculate_bollinger_bands(
|
|
df['close'],
|
|
self.params['bollinger_period'],
|
|
self.params['bollinger_std']
|
|
)
|
|
|
|
|
|
df['Volume_MA'] = df['volume'].rolling(window=self.params['volume_ma_period']).mean()
|
|
df['Volume_Ratio'] = df['volume'] / df['Volume_MA']
|
|
|
|
|
|
df['ATR'] = self.calculate_atr(df, self.params['atr_period'])
|
|
df['Volatility'] = df['ATR'] / df['close'] * 100
|
|
|
|
|
|
df['ROC'] = df['close'].pct_change(periods=10) * 100
|
|
|
|
|
|
df = self.format_indicator_data(df)
|
|
|
|
return df
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"计算技术指标时出错: {str(e)}")
|
|
raise
|
|
|
|
def calculate_score(self, df, market_type='A'):
|
|
"""
|
|
计算股票评分 - 使用时空共振交易系统增强
|
|
根据不同的市场特征调整评分权重和标准
|
|
"""
|
|
try:
|
|
score = 0
|
|
latest = df.iloc[-1]
|
|
prev_days = min(30, len(df) - 1)
|
|
|
|
|
|
|
|
weights = {
|
|
'trend': 0.30,
|
|
'volatility': 0.15,
|
|
'technical': 0.25,
|
|
'volume': 0.20,
|
|
'momentum': 0.10
|
|
}
|
|
|
|
|
|
if market_type == 'US':
|
|
|
|
weights['trend'] = 0.35
|
|
weights['volatility'] = 0.10
|
|
weights['momentum'] = 0.15
|
|
elif market_type == 'HK':
|
|
|
|
weights['volatility'] = 0.20
|
|
weights['volume'] = 0.25
|
|
|
|
|
|
trend_score = 0
|
|
|
|
|
|
if latest['MA5'] > latest['MA20'] and latest['MA20'] > latest['MA60']:
|
|
|
|
trend_score += 15
|
|
elif latest['MA5'] > latest['MA20']:
|
|
|
|
trend_score += 10
|
|
elif latest['MA20'] > latest['MA60']:
|
|
|
|
trend_score += 5
|
|
|
|
|
|
if latest['close'] > latest['MA5']:
|
|
trend_score += 5
|
|
if latest['close'] > latest['MA20']:
|
|
trend_score += 5
|
|
if latest['close'] > latest['MA60']:
|
|
trend_score += 5
|
|
|
|
|
|
trend_score = min(30, trend_score)
|
|
|
|
|
|
volatility_score = 0
|
|
|
|
|
|
volatility = latest['Volatility']
|
|
if 1.0 <= volatility <= 2.5:
|
|
|
|
volatility_score += 15
|
|
elif 2.5 < volatility <= 4.0:
|
|
|
|
volatility_score += 10
|
|
elif volatility < 1.0:
|
|
|
|
volatility_score += 5
|
|
else:
|
|
|
|
volatility_score += 0
|
|
|
|
|
|
technical_score = 0
|
|
|
|
|
|
rsi = latest['RSI']
|
|
if 40 <= rsi <= 60:
|
|
|
|
technical_score += 7
|
|
elif 30 <= rsi < 40 or 60 < rsi <= 70:
|
|
|
|
technical_score += 10
|
|
elif rsi < 30:
|
|
|
|
technical_score += 8
|
|
elif rsi > 70:
|
|
|
|
technical_score += 2
|
|
|
|
|
|
if latest['MACD'] > latest['Signal'] and latest['MACD_hist'] > 0:
|
|
|
|
technical_score += 10
|
|
elif latest['MACD'] > latest['Signal']:
|
|
|
|
technical_score += 8
|
|
elif latest['MACD'] < latest['Signal'] and latest['MACD_hist'] < 0:
|
|
|
|
technical_score += 0
|
|
elif latest['MACD_hist'] > df.iloc[-2]['MACD_hist']:
|
|
|
|
technical_score += 5
|
|
|
|
|
|
bb_position = (latest['close'] - latest['BB_lower']) / (latest['BB_upper'] - latest['BB_lower'])
|
|
if 0.3 <= bb_position <= 0.7:
|
|
|
|
technical_score += 3
|
|
elif bb_position < 0.2:
|
|
|
|
technical_score += 5
|
|
elif bb_position > 0.8:
|
|
|
|
technical_score += 1
|
|
|
|
|
|
technical_score = min(25, technical_score)
|
|
|
|
|
|
volume_score = 0
|
|
|
|
|
|
recent_vol_ratio = [df.iloc[-i]['Volume_Ratio'] for i in range(1, min(6, len(df)))]
|
|
avg_vol_ratio = sum(recent_vol_ratio) / len(recent_vol_ratio)
|
|
|
|
if avg_vol_ratio > 1.5 and latest['close'] > df.iloc[-2]['close']:
|
|
|
|
volume_score += 20
|
|
elif avg_vol_ratio > 1.2 and latest['close'] > df.iloc[-2]['close']:
|
|
|
|
volume_score += 15
|
|
elif avg_vol_ratio < 0.8 and latest['close'] < df.iloc[-2]['close']:
|
|
|
|
volume_score += 10
|
|
elif avg_vol_ratio > 1.2 and latest['close'] < df.iloc[-2]['close']:
|
|
|
|
volume_score += 0
|
|
else:
|
|
|
|
volume_score += 8
|
|
|
|
|
|
momentum_score = 0
|
|
|
|
|
|
roc = latest['ROC']
|
|
if roc > 5:
|
|
|
|
momentum_score += 10
|
|
elif 2 <= roc <= 5:
|
|
|
|
momentum_score += 8
|
|
elif 0 <= roc < 2:
|
|
|
|
momentum_score += 5
|
|
elif -2 <= roc < 0:
|
|
|
|
momentum_score += 3
|
|
else:
|
|
|
|
momentum_score += 0
|
|
|
|
|
|
final_score = (
|
|
trend_score * weights['trend'] / 0.30 +
|
|
volatility_score * weights['volatility'] / 0.15 +
|
|
technical_score * weights['technical'] / 0.25 +
|
|
volume_score * weights['volume'] / 0.20 +
|
|
momentum_score * weights['momentum'] / 0.10
|
|
)
|
|
|
|
|
|
if market_type == 'US':
|
|
|
|
|
|
is_earnings_season = self._is_earnings_season()
|
|
if is_earnings_season:
|
|
|
|
final_score = 0.9 * final_score + 5
|
|
|
|
elif market_type == 'HK':
|
|
|
|
|
|
a_share_linkage = self._check_a_share_linkage(df)
|
|
if a_share_linkage > 0.7:
|
|
|
|
mainland_sentiment = self._get_mainland_market_sentiment()
|
|
if mainland_sentiment > 0:
|
|
final_score += 5
|
|
else:
|
|
final_score -= 5
|
|
|
|
|
|
final_score = max(0, min(100, round(final_score)))
|
|
|
|
|
|
self.score_details = {
|
|
'trend': trend_score,
|
|
'volatility': volatility_score,
|
|
'technical': technical_score,
|
|
'volume': volume_score,
|
|
'momentum': momentum_score,
|
|
'total': final_score
|
|
}
|
|
|
|
return final_score
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error calculating score: {str(e)}")
|
|
|
|
return 50
|
|
|
|
def calculate_position_size(self, stock_code, risk_percent=2.0, stop_loss_percent=5.0):
|
|
"""
|
|
根据风险管理原则计算最佳仓位大小
|
|
实施时空共振系统的“仓位大小公式”
|
|
|
|
参数:
|
|
stock_code: 要分析的股票代码
|
|
risk_percent: 在此交易中承担风险的总资本百分比(默认为2%)
|
|
stop_loss_percent: 从入场点的止损百分比(默认为5%)
|
|
|
|
返回:
|
|
仓位大小占总资本的百分比
|
|
"""
|
|
try:
|
|
|
|
df = self.get_stock_data(stock_code)
|
|
df = self.calculate_indicators(df)
|
|
|
|
|
|
latest = df.iloc[-1]
|
|
volatility = latest['Volatility']
|
|
|
|
|
|
volatility_factor = 1.0
|
|
if volatility > 4.0:
|
|
volatility_factor = 0.6
|
|
elif volatility > 2.5:
|
|
volatility_factor = 0.8
|
|
elif volatility < 1.0:
|
|
volatility_factor = 1.2
|
|
|
|
|
|
|
|
position_size = (risk_percent) / (stop_loss_percent * volatility_factor)
|
|
|
|
|
|
position_size = min(position_size, 25.0)
|
|
|
|
return position_size
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error calculating position size: {str(e)}")
|
|
|
|
return 5.0
|
|
|
|
def get_recommendation(self, score, market_type='A', technical_data=None, news_data=None):
|
|
"""
|
|
根据得分和附加信息生成投资建议
|
|
使用时空共振交易系统策略增强
|
|
"""
|
|
try:
|
|
|
|
if score >= 85:
|
|
base_recommendation = '强烈建议买入'
|
|
confidence = 'high'
|
|
action = 'strong_buy'
|
|
elif score >= 70:
|
|
base_recommendation = '建议买入'
|
|
confidence = 'medium_high'
|
|
action = 'buy'
|
|
elif score >= 55:
|
|
base_recommendation = '谨慎买入'
|
|
confidence = 'medium'
|
|
action = 'cautious_buy'
|
|
elif score >= 45:
|
|
base_recommendation = '持观望态度'
|
|
confidence = 'medium'
|
|
action = 'hold'
|
|
elif score >= 30:
|
|
base_recommendation = '谨慎持有'
|
|
confidence = 'medium'
|
|
action = 'cautious_hold'
|
|
elif score >= 15:
|
|
base_recommendation = '建议减仓'
|
|
confidence = 'medium_high'
|
|
action = 'reduce'
|
|
else:
|
|
base_recommendation = '建议卖出'
|
|
confidence = 'high'
|
|
action = 'sell'
|
|
|
|
|
|
market_adjustment = ""
|
|
if market_type == 'US':
|
|
|
|
if self._is_earnings_season():
|
|
if confidence == 'high' or confidence == 'medium_high':
|
|
confidence = 'medium'
|
|
market_adjustment = "(财报季临近,波动可能加大,建议适当控制仓位)"
|
|
|
|
elif market_type == 'HK':
|
|
|
|
mainland_sentiment = self._get_mainland_market_sentiment()
|
|
if mainland_sentiment < -0.3 and (action == 'buy' or action == 'strong_buy'):
|
|
action = 'cautious_buy'
|
|
confidence = 'medium'
|
|
market_adjustment = "(受大陆市场情绪影响,建议控制风险)"
|
|
|
|
elif market_type == 'A':
|
|
|
|
if technical_data and 'Volatility' in technical_data:
|
|
vol = technical_data.get('Volatility', 0)
|
|
if vol > 4.0 and (action == 'buy' or action == 'strong_buy'):
|
|
action = 'cautious_buy'
|
|
confidence = 'medium'
|
|
market_adjustment = "(市场波动较大,建议分批买入)"
|
|
|
|
|
|
sentiment_adjustment = ""
|
|
if news_data and 'market_sentiment' in news_data:
|
|
sentiment = news_data.get('market_sentiment', 'neutral')
|
|
|
|
if sentiment == 'bullish' and action in ['hold', 'cautious_hold']:
|
|
action = 'cautious_buy'
|
|
sentiment_adjustment = "(市场氛围积极,可适当提高仓位)"
|
|
|
|
elif sentiment == 'bearish' and action in ['buy', 'cautious_buy']:
|
|
action = 'hold'
|
|
sentiment_adjustment = "(市场氛围悲观,建议等待更好买点)"
|
|
elif self.json_match_flag==False:
|
|
import re
|
|
|
|
|
|
sentiment_pattern = r'(bullish|neutral|bearish)'
|
|
sentiment_match = re.search(sentiment_pattern, news_data.get('original_content', ''))
|
|
|
|
if sentiment_match:
|
|
sentiment_map = {
|
|
'bullish': 'bullish',
|
|
'neutral': 'neutral',
|
|
'bearish': 'bearish'
|
|
}
|
|
sentiment = sentiment_map.get(sentiment_match.group(1), 'neutral')
|
|
|
|
if sentiment == 'bullish' and action in ['hold', 'cautious_hold']:
|
|
action = 'cautious_buy'
|
|
sentiment_adjustment = "(市场氛围积极,可适当提高仓位)"
|
|
elif sentiment == 'bearish' and action in ['buy', 'cautious_buy']:
|
|
action = 'hold'
|
|
sentiment_adjustment = "(市场氛围悲观,建议等待更好买点)"
|
|
|
|
|
|
|
|
technical_adjustment = ""
|
|
if technical_data:
|
|
rsi = technical_data.get('RSI', 50)
|
|
macd_signal = technical_data.get('MACD_signal', 'neutral')
|
|
|
|
|
|
if rsi > 80 and action in ['buy', 'strong_buy']:
|
|
action = 'hold'
|
|
technical_adjustment = "(RSI指标显示超买,建议等待回调)"
|
|
elif rsi < 20 and action in ['sell', 'reduce']:
|
|
action = 'hold'
|
|
technical_adjustment = "(RSI指标显示超卖,可能存在反弹机会)"
|
|
|
|
|
|
if macd_signal == 'bullish' and action in ['hold', 'cautious_hold']:
|
|
action = 'cautious_buy'
|
|
if not technical_adjustment:
|
|
technical_adjustment = "(MACD显示买入信号)"
|
|
elif macd_signal == 'bearish' and action in ['cautious_buy', 'buy']:
|
|
action = 'hold'
|
|
if not technical_adjustment:
|
|
technical_adjustment = "(MACD显示卖出信号)"
|
|
|
|
|
|
action_to_recommendation = {
|
|
'strong_buy': '强烈建议买入',
|
|
'buy': '建议买入',
|
|
'cautious_buy': '谨慎买入',
|
|
'hold': '持观望态度',
|
|
'cautious_hold': '谨慎持有',
|
|
'reduce': '建议减仓',
|
|
'sell': '建议卖出'
|
|
}
|
|
|
|
final_recommendation = action_to_recommendation.get(action, base_recommendation)
|
|
|
|
|
|
adjustments = " ".join(filter(None, [market_adjustment, sentiment_adjustment, technical_adjustment]))
|
|
|
|
if adjustments:
|
|
return f"{final_recommendation} {adjustments}"
|
|
else:
|
|
return final_recommendation
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error generating investment recommendation: {str(e)}")
|
|
|
|
return "无法提供明确建议,请结合多种因素谨慎决策"
|
|
|
|
def check_consecutive_losses(self, trade_history, max_consecutive_losses=3):
|
|
"""
|
|
实施“冷静期风险控制” - 连续亏损后停止交易
|
|
|
|
参数:
|
|
trade_history: 最近交易结果列表 (True 表示盈利, False 表示亏损)
|
|
max_consecutive_losses: 允许的最大连续亏损次数
|
|
|
|
返回:
|
|
Boolean: True 如果应该暂停交易, False 如果可以继续交易
|
|
"""
|
|
consecutive_losses = 0
|
|
|
|
|
|
for trade in reversed(trade_history):
|
|
if not trade:
|
|
consecutive_losses += 1
|
|
else:
|
|
break
|
|
|
|
|
|
return consecutive_losses >= max_consecutive_losses
|
|
|
|
def check_profit_taking(self, current_profit_percent, threshold=20.0):
|
|
"""
|
|
当回报超过阈值时,实施获利了结机制
|
|
属于“能量守恒维度”的一部分
|
|
|
|
参数:
|
|
current_profit_percent: 当前利润百分比
|
|
threshold: 用于获利了结的利润百分比阈值
|
|
|
|
返回:
|
|
Float: 减少仓位的百分比 (0.0-1.0)
|
|
"""
|
|
if current_profit_percent >= threshold:
|
|
|
|
return 0.5
|
|
|
|
return 0.0
|
|
|
|
def _is_earnings_season(self):
|
|
"""检查当前是否处于财报季(辅助函数)"""
|
|
from datetime import datetime
|
|
current_month = datetime.now().month
|
|
|
|
return current_month in [1, 4, 7, 10]
|
|
|
|
def _check_a_share_linkage(self, df, window=20):
|
|
"""检查港股与A股的联动性(辅助函数)"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
correlation = 0.6
|
|
return correlation
|
|
except:
|
|
return 0.5
|
|
|
|
def _get_mainland_market_sentiment(self):
|
|
"""获取中国大陆市场情绪(辅助函数)"""
|
|
|
|
try:
|
|
|
|
sentiment = 0.2
|
|
return sentiment
|
|
except:
|
|
return 0
|
|
|
|
def get_stock_news(self, stock_code, market_type='A', limit=5):
|
|
"""
|
|
获取股票相关新闻和实时信息,通过OpenAI API调用function calling方式获取
|
|
参数:
|
|
stock_code: 股票代码
|
|
market_type: 市场类型 (A/HK/US)
|
|
limit: 返回的新闻条数上限
|
|
返回:
|
|
包含新闻和公告的字典
|
|
"""
|
|
try:
|
|
self.logger.info(f"获取股票 {stock_code} 的相关新闻和信息")
|
|
|
|
|
|
cache_key = f"{stock_code}_{market_type}_news"
|
|
if cache_key in self.data_cache and (
|
|
datetime.now() - self.data_cache[cache_key]['timestamp']).seconds < 3600:
|
|
|
|
return self.data_cache[cache_key]['data']
|
|
|
|
|
|
stock_info = self.get_stock_info(stock_code)
|
|
stock_name = stock_info.get('股票名称', '未知')
|
|
industry = stock_info.get('行业', '未知')
|
|
|
|
|
|
market_name = "A股" if market_type == 'A' else "港股" if market_type == 'HK' else "美股"
|
|
query = f"""请帮我搜索以下股票的最新相关新闻和信息:
|
|
股票名称: {stock_name}
|
|
股票代码: {stock_code}
|
|
市场: {market_name}
|
|
行业: {industry}
|
|
|
|
请使用search_news工具搜索相关新闻,然后只需要返回JSON格式。
|
|
按照以下格式的JSON数据返回:
|
|
{{
|
|
"news": [
|
|
{{"title": "新闻标题", "date": "YYYY-MM-DD", "source": "新闻来源", "summary": "新闻摘要"}},
|
|
...
|
|
],
|
|
"announcements": [
|
|
{{"title": "公告标题", "date": "YYYY-MM-DD", "type": "公告类型"}},
|
|
...
|
|
],
|
|
"industry_news": [
|
|
{{"title": "行业新闻标题", "date": "YYYY-MM-DD", "summary": "新闻摘要"}},
|
|
...
|
|
],
|
|
"market_sentiment": "市场情绪(bullish/slightly_bullish/neutral/slightly_bearish/bearish)"
|
|
}}
|
|
注意只返回json数据,不要返回其他内容。
|
|
"""
|
|
|
|
|
|
tools = [
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "search_news",
|
|
"description": "搜索股票相关的新闻和信息",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {
|
|
"type": "string",
|
|
"description": "搜索查询词,用于查找相关新闻"
|
|
}
|
|
},
|
|
"required": ["query"]
|
|
}
|
|
}
|
|
}
|
|
]
|
|
|
|
|
|
import queue
|
|
import threading
|
|
import json
|
|
import openai
|
|
import requests
|
|
|
|
result_queue = queue.Queue()
|
|
|
|
def search_news(query):
|
|
"""实际执行搜索的函数"""
|
|
try:
|
|
|
|
serp_api_key = os.getenv('SERP_API_KEY')
|
|
if not serp_api_key:
|
|
self.logger.error("未找到SERP_API_KEY环境变量")
|
|
return {"error": "未配置搜索API密钥"}
|
|
|
|
|
|
search_query = f"{stock_name} {stock_code} {market_name} 最新新闻 公告"
|
|
|
|
|
|
url = "https://serpapi.com/search"
|
|
params = {
|
|
"engine": "google",
|
|
"q": search_query,
|
|
"api_key": serp_api_key,
|
|
"tbm": "nws",
|
|
"num": limit * 2
|
|
}
|
|
|
|
response = requests.get(url, params=params)
|
|
search_results = response.json()
|
|
|
|
|
|
news_results = []
|
|
if "news_results" in search_results:
|
|
for item in search_results["news_results"][:limit]:
|
|
news_results.append({
|
|
"title": item.get("title", ""),
|
|
"date": item.get("date", ""),
|
|
"source": item.get("source", ""),
|
|
"link": item.get("link", ""),
|
|
"snippet": item.get("snippet", "")
|
|
})
|
|
|
|
|
|
industry_query = f"{industry} {market_name} 行业动态 最新消息"
|
|
industry_params = {
|
|
"engine": "google",
|
|
"q": industry_query,
|
|
"api_key": serp_api_key,
|
|
"tbm": "nws",
|
|
"num": limit
|
|
}
|
|
|
|
industry_response = requests.get(url, params=industry_params)
|
|
industry_results = industry_response.json()
|
|
|
|
|
|
industry_news = []
|
|
if "news_results" in industry_results:
|
|
for item in industry_results["news_results"][:limit]:
|
|
industry_news.append({
|
|
"title": item.get("title", ""),
|
|
"date": item.get("date", ""),
|
|
"source": item.get("source", ""),
|
|
"summary": item.get("snippet", "")
|
|
})
|
|
|
|
|
|
|
|
announcements = []
|
|
|
|
|
|
|
|
sentiment_keywords = {
|
|
'bullish': ['上涨', '增长', '利好', '突破', '强势', '看好', '机会', '利润'],
|
|
'slightly_bullish': ['回升', '改善', '企稳', '向好', '期待'],
|
|
'neutral': ['稳定', '平稳', '持平', '不变'],
|
|
'slightly_bearish': ['回调', '承压', '谨慎', '风险', '下滑'],
|
|
'bearish': ['下跌', '亏损', '跌破', '利空', '警惕', '危机', '崩盘']
|
|
}
|
|
|
|
|
|
sentiment_scores = {k: 0 for k in sentiment_keywords.keys()}
|
|
all_text = " ".join([n.get("title", "") + " " + n.get("snippet", "") for n in news_results])
|
|
|
|
for sentiment, keywords in sentiment_keywords.items():
|
|
for keyword in keywords:
|
|
if keyword in all_text:
|
|
sentiment_scores[sentiment] += 1
|
|
|
|
|
|
if not sentiment_scores or all(score == 0 for score in sentiment_scores.values()):
|
|
market_sentiment = "neutral"
|
|
else:
|
|
market_sentiment = max(sentiment_scores.items(), key=lambda x: x[1])[0]
|
|
|
|
return {
|
|
"news": news_results,
|
|
"announcements": announcements,
|
|
"industry_news": industry_news,
|
|
"market_sentiment": market_sentiment
|
|
}
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"搜索新闻时出错: {str(e)}")
|
|
return {"error": str(e)}
|
|
|
|
def call_api():
|
|
try:
|
|
messages = [{"role": "user", "content": query}]
|
|
|
|
|
|
response = openai.ChatCompletion.create(
|
|
model=self.news_model,
|
|
messages=messages,
|
|
tools=tools,
|
|
tool_choice="auto",
|
|
temperature=0.7,
|
|
max_tokens=1000,
|
|
stream=False,
|
|
timeout=120
|
|
)
|
|
|
|
|
|
message = response["choices"][0]["message"]
|
|
|
|
if "tool_calls" in message:
|
|
|
|
tool_calls = message["tool_calls"]
|
|
|
|
|
|
messages.append(message)
|
|
|
|
for tool_call in tool_calls:
|
|
function_name = tool_call["function"]["name"]
|
|
function_args = json.loads(tool_call["function"]["arguments"])
|
|
|
|
|
|
if function_name == "search_news":
|
|
search_query = function_args.get("query", f"{stock_name} {stock_code} 新闻")
|
|
function_response = search_news(search_query)
|
|
|
|
|
|
messages.append({
|
|
"tool_call_id": tool_call["id"],
|
|
"role": "tool",
|
|
"name": function_name,
|
|
"content": json.dumps(function_response, ensure_ascii=False)
|
|
})
|
|
|
|
|
|
second_response = openai.ChatCompletion.create(
|
|
model=self.news_model,
|
|
messages=messages,
|
|
temperature=0.7,
|
|
max_tokens=4000,
|
|
stream=False,
|
|
timeout=120
|
|
)
|
|
|
|
result_queue.put(second_response)
|
|
else:
|
|
|
|
result_queue.put(response)
|
|
|
|
except Exception as e:
|
|
result_queue.put(e)
|
|
|
|
|
|
api_thread = threading.Thread(target=call_api)
|
|
api_thread.daemon = True
|
|
api_thread.start()
|
|
|
|
|
|
try:
|
|
result = result_queue.get(timeout=240)
|
|
|
|
|
|
if isinstance(result, Exception):
|
|
self.logger.error(f"获取新闻API调用失败: {str(result)}")
|
|
raise result
|
|
|
|
|
|
content = result["choices"][0]["message"]["content"].strip()
|
|
|
|
|
|
try:
|
|
|
|
news_data = json.loads(content)
|
|
except json.JSONDecodeError:
|
|
|
|
import re
|
|
json_match = re.search(r'```json\s*([\s\S]*?)\s*```', content)
|
|
if json_match:
|
|
json_str = json_match.group(1)
|
|
news_data = json.loads(json_str)
|
|
self.json_match_flag = True
|
|
else:
|
|
|
|
self.logger.info(f"无法提取JSON,直接返回响应{content}")
|
|
self.json_match_flag = False
|
|
news_data = {}
|
|
news_data['original_content'] = content
|
|
|
|
|
|
if not isinstance(news_data, dict):
|
|
news_data = {}
|
|
|
|
for key in ['news', 'announcements', 'industry_news']:
|
|
if key not in news_data:
|
|
news_data[key] = []
|
|
|
|
if 'market_sentiment' not in news_data:
|
|
news_data['market_sentiment'] = 'neutral'
|
|
|
|
|
|
news_data['timestamp'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
|
|
self.data_cache[cache_key] = {
|
|
'data': news_data,
|
|
'timestamp': datetime.now()
|
|
}
|
|
|
|
return news_data
|
|
|
|
except queue.Empty:
|
|
self.logger.warning("获取新闻API调用超时")
|
|
return {
|
|
'news': [],
|
|
'announcements': [],
|
|
'industry_news': [],
|
|
'market_sentiment': 'neutral',
|
|
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
}
|
|
except Exception as e:
|
|
self.logger.error(f"处理新闻数据时出错: {str(e)}")
|
|
return {
|
|
'news': [],
|
|
'announcements': [],
|
|
'industry_news': [],
|
|
'market_sentiment': 'neutral',
|
|
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
}
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"获取股票新闻时出错: {str(e)}")
|
|
|
|
return {
|
|
'news': [],
|
|
'announcements': [],
|
|
'industry_news': [],
|
|
'market_sentiment': 'neutral',
|
|
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_ai_analysis(self, df, stock_code, market_type='A'):
|
|
"""
|
|
使用AI进行增强分析
|
|
结合技术指标、实时新闻和行业信息
|
|
|
|
参数:
|
|
df: 股票历史数据DataFrame
|
|
stock_code: 股票代码
|
|
market_type: 市场类型(A/HK/US)
|
|
|
|
返回:
|
|
AI生成的分析报告文本
|
|
"""
|
|
try:
|
|
import openai
|
|
import threading
|
|
import queue
|
|
|
|
|
|
openai.api_key = self.openai_api_key
|
|
openai.api_base = self.openai_api_url
|
|
|
|
|
|
recent_data = df.tail(20).to_dict('records')
|
|
|
|
|
|
technical_summary = {
|
|
'trend': 'upward' if df.iloc[-1]['MA5'] > df.iloc[-1]['MA20'] else 'downward',
|
|
'volatility': f"{df.iloc[-1]['Volatility']:.2f}%",
|
|
'volume_trend': 'increasing' if df.iloc[-1]['Volume_Ratio'] > 1 else 'decreasing',
|
|
'rsi_level': df.iloc[-1]['RSI'],
|
|
'macd_signal': 'bullish' if df.iloc[-1]['MACD'] > df.iloc[-1]['Signal'] else 'bearish',
|
|
'bb_position': self._calculate_bb_position(df)
|
|
}
|
|
|
|
|
|
sr_levels = self.identify_support_resistance(df)
|
|
|
|
|
|
stock_info = self.get_stock_info(stock_code)
|
|
stock_name = stock_info.get('股票名称', '未知')
|
|
industry = stock_info.get('行业', '未知')
|
|
|
|
|
|
self.logger.info(f"获取 {stock_code} 的相关新闻和市场信息")
|
|
news_data = self.get_stock_news(stock_code, market_type)
|
|
|
|
|
|
score = self.calculate_score(df, market_type)
|
|
score_details = getattr(self, 'score_details', {'total': score})
|
|
|
|
|
|
|
|
tech_data = {
|
|
'RSI': technical_summary['rsi_level'],
|
|
'MACD_signal': technical_summary['macd_signal'],
|
|
'Volatility': df.iloc[-1]['Volatility']
|
|
}
|
|
recommendation = self.get_recommendation(score, market_type, tech_data, news_data)
|
|
|
|
|
|
prompt = f"""作为专业的股票分析师,请对{stock_name}({stock_code})进行全面分析:
|
|
|
|
1. 基本信息:
|
|
- 股票名称: {stock_name}
|
|
- 股票代码: {stock_code}
|
|
- 行业: {industry}
|
|
- 市场类型: {"A股" if market_type == 'A' else "港股" if market_type == 'HK' else "美股"}
|
|
|
|
2. 技术指标摘要:
|
|
- 趋势: {technical_summary['trend']}
|
|
- 波动率: {technical_summary['volatility']}
|
|
- 成交量趋势: {technical_summary['volume_trend']}
|
|
- RSI: {technical_summary['rsi_level']:.2f}
|
|
- MACD信号: {technical_summary['macd_signal']}
|
|
- 布林带位置: {technical_summary['bb_position']}
|
|
|
|
3. 支撑与压力位:
|
|
- 短期支撑位: {', '.join([str(level) for level in sr_levels['support_levels']['short_term']])}
|
|
- 中期支撑位: {', '.join([str(level) for level in sr_levels['support_levels']['medium_term']])}
|
|
- 短期压力位: {', '.join([str(level) for level in sr_levels['resistance_levels']['short_term']])}
|
|
- 中期压力位: {', '.join([str(level) for level in sr_levels['resistance_levels']['medium_term']])}
|
|
|
|
4. 综合评分: {score_details['total']}分
|
|
- 趋势评分: {score_details.get('trend', 0)}
|
|
- 波动率评分: {score_details.get('volatility', 0)}
|
|
- 技术指标评分: {score_details.get('technical', 0)}
|
|
- 成交量评分: {score_details.get('volume', 0)}
|
|
- 动量评分: {score_details.get('momentum', 0)}
|
|
|
|
5. 投资建议: {recommendation}"""
|
|
|
|
|
|
if hasattr(self, 'json_match_flag') and not self.json_match_flag and 'original_content' in news_data:
|
|
|
|
prompt += f"""
|
|
|
|
6. 相关新闻和市场信息:
|
|
{news_data.get('original_content', '无法获取相关新闻')}
|
|
"""
|
|
else:
|
|
|
|
prompt += f"""
|
|
|
|
6. 近期相关新闻:
|
|
{self._format_news_for_prompt(news_data.get('news', []))}
|
|
|
|
7. 公司公告:
|
|
{self._format_announcements_for_prompt(news_data.get('announcements', []))}
|
|
|
|
8. 行业动态:
|
|
{self._format_news_for_prompt(news_data.get('industry_news', []))}
|
|
|
|
9. 市场情绪: {news_data.get('market_sentiment', 'neutral')}
|
|
|
|
请提供以下内容:
|
|
1. 技术面分析 - 详细分析价格走势、支撑压力位、主要技术指标的信号
|
|
2. 行业和市场环境 - 结合新闻和行业动态分析公司所处环境
|
|
3. 风险因素 - 识别潜在风险点
|
|
4. 具体交易策略 - 给出明确的买入/卖出建议,包括入场点、止损位和目标价位
|
|
5. 短期(1周)、中期(1-3个月)和长期(半年)展望
|
|
|
|
请基于数据给出客观分析,不要过度乐观或悲观。分析应该包含具体数据和百分比,避免模糊表述。
|
|
"""
|
|
|
|
messages = [{"role": "user", "content": prompt}]
|
|
|
|
|
|
result_queue = queue.Queue()
|
|
|
|
def call_api():
|
|
try:
|
|
response = openai.ChatCompletion.create(
|
|
model=self.openai_model,
|
|
messages=messages,
|
|
temperature=0.8,
|
|
max_tokens=4000,
|
|
stream=False,
|
|
timeout=180
|
|
)
|
|
result_queue.put(response)
|
|
except Exception as e:
|
|
result_queue.put(e)
|
|
|
|
|
|
api_thread = threading.Thread(target=call_api)
|
|
api_thread.daemon = True
|
|
api_thread.start()
|
|
|
|
|
|
try:
|
|
result = result_queue.get(timeout=240)
|
|
|
|
|
|
if isinstance(result, Exception):
|
|
raise result
|
|
|
|
|
|
assistant_reply = result["choices"][0]["message"]["content"].strip()
|
|
return assistant_reply
|
|
|
|
except queue.Empty:
|
|
return "AI分析超时,无法获取分析结果。请稍后再试。"
|
|
except Exception as e:
|
|
return f"AI分析过程中发生错误: {str(e)}"
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"AI分析发生错误: {str(e)}")
|
|
return f"AI分析过程中发生错误,请稍后再试。错误信息: {str(e)}"
|
|
|
|
def _calculate_bb_position(self, df):
|
|
"""计算价格在布林带中的位置"""
|
|
latest = df.iloc[-1]
|
|
bb_width = latest['BB_upper'] - latest['BB_lower']
|
|
if bb_width == 0:
|
|
return "middle"
|
|
|
|
position = (latest['close'] - latest['BB_lower']) / bb_width
|
|
|
|
if position < 0.2:
|
|
return "near lower band (potential oversold)"
|
|
elif position < 0.4:
|
|
return "below middle band"
|
|
elif position < 0.6:
|
|
return "near middle band"
|
|
elif position < 0.8:
|
|
return "above middle band"
|
|
else:
|
|
return "near upper band (potential overbought)"
|
|
|
|
def _format_news_for_prompt(self, news_list):
|
|
"""格式化新闻列表为prompt字符串"""
|
|
if not news_list:
|
|
return " 无最新相关新闻"
|
|
|
|
formatted = ""
|
|
for i, news in enumerate(news_list[:3]):
|
|
date = news.get('date', '')
|
|
title = news.get('title', '')
|
|
source = news.get('source', '')
|
|
formatted += f" {i + 1}. [{date}] {title} (来源: {source})\n"
|
|
|
|
return formatted
|
|
|
|
def _format_announcements_for_prompt(self, announcements):
|
|
"""格式化公告列表为prompt字符串"""
|
|
if not announcements:
|
|
return " 无最新公告"
|
|
|
|
formatted = ""
|
|
for i, ann in enumerate(announcements[:3]):
|
|
date = ann.get('date', '')
|
|
title = ann.get('title', '')
|
|
type_ = ann.get('type', '')
|
|
formatted += f" {i + 1}. [{date}] {title} (类型: {type_})\n"
|
|
|
|
return formatted
|
|
|
|
|
|
def analyze_stock(self, stock_code, market_type='A'):
|
|
"""分析单个股票"""
|
|
try:
|
|
|
|
|
|
df = self.get_stock_data(stock_code, market_type)
|
|
self.logger.info(f"获取股票数据完成")
|
|
|
|
df = self.calculate_indicators(df)
|
|
self.logger.info(f"计算技术指标完成")
|
|
|
|
score = self.calculate_score(df)
|
|
self.logger.info(f"评分系统完成")
|
|
|
|
latest = df.iloc[-1]
|
|
prev = df.iloc[-2]
|
|
|
|
|
|
stock_info = self.get_stock_info(stock_code)
|
|
stock_name = stock_info.get('股票名称', '未知')
|
|
industry = stock_info.get('行业', '未知')
|
|
|
|
|
|
report = {
|
|
'stock_code': stock_code,
|
|
'stock_name': stock_name,
|
|
'industry': industry,
|
|
'analysis_date': datetime.now().strftime('%Y-%m-%d'),
|
|
'score': score,
|
|
'price': latest['close'],
|
|
'price_change': (latest['close'] - prev['close']) / prev['close'] * 100,
|
|
'ma_trend': 'UP' if latest['MA5'] > latest['MA20'] else 'DOWN',
|
|
'rsi': latest['RSI'],
|
|
'macd_signal': 'BUY' if latest['MACD'] > latest['Signal'] else 'SELL',
|
|
'volume_status': '放量' if latest['Volume_Ratio'] > 1.5 else '平量',
|
|
'recommendation': self.get_recommendation(score),
|
|
'ai_analysis': self.get_ai_analysis(df, stock_code)
|
|
}
|
|
|
|
return report
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"分析股票时出错: {str(e)}")
|
|
raise
|
|
|
|
|
|
def scan_market(self, stock_list, min_score=60, market_type='A'):
|
|
"""扫描市场,寻找符合条件的股票"""
|
|
recommendations = []
|
|
total_stocks = len(stock_list)
|
|
|
|
self.logger.info(f"开始市场扫描,共 {total_stocks} 只股票")
|
|
start_time = time.time()
|
|
processed = 0
|
|
|
|
|
|
batch_size = 10
|
|
for i in range(0, total_stocks, batch_size):
|
|
batch = stock_list[i:i + batch_size]
|
|
batch_results = []
|
|
|
|
for stock_code in batch:
|
|
try:
|
|
|
|
report = self.quick_analyze_stock(stock_code, market_type)
|
|
if report['score'] >= min_score:
|
|
batch_results.append(report)
|
|
except Exception as e:
|
|
self.logger.error(f"分析股票 {stock_code} 时出错: {str(e)}")
|
|
continue
|
|
|
|
|
|
recommendations.extend(batch_results)
|
|
|
|
|
|
processed += len(batch)
|
|
elapsed = time.time() - start_time
|
|
remaining = (elapsed / processed) * (total_stocks - processed) if processed > 0 else 0
|
|
|
|
self.logger.info(
|
|
f"已处理 {processed}/{total_stocks} 只股票,耗时 {elapsed:.1f}秒,预计剩余 {remaining:.1f}秒")
|
|
|
|
|
|
recommendations.sort(key=lambda x: x['score'], reverse=True)
|
|
|
|
total_time = time.time() - start_time
|
|
self.logger.info(
|
|
f"市场扫描完成,共分析 {total_stocks} 只股票,找到 {len(recommendations)} 只符合条件的股票,总耗时 {total_time:.1f}秒")
|
|
|
|
return recommendations
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def quick_analyze_stock(self, stock_code, market_type='A'):
|
|
"""快速分析股票,用于市场扫描"""
|
|
try:
|
|
|
|
df = self.get_stock_data(stock_code, market_type)
|
|
|
|
|
|
df = self.calculate_indicators(df)
|
|
|
|
|
|
score = self.calculate_score(df)
|
|
|
|
|
|
latest = df.iloc[-1]
|
|
prev = df.iloc[-2] if len(df) > 1 else latest
|
|
|
|
|
|
try:
|
|
stock_info = self.get_stock_info(stock_code)
|
|
stock_name = stock_info.get('股票名称', '未知')
|
|
industry = stock_info.get('行业', '未知')
|
|
|
|
|
|
self.logger.info(f"股票 {stock_code} 信息: 名称={stock_name}, 行业={industry}")
|
|
except Exception as e:
|
|
self.logger.error(f"获取股票 {stock_code} 信息时出错: {str(e)}")
|
|
stock_name = '未知'
|
|
industry = '未知'
|
|
|
|
|
|
report = {
|
|
'stock_code': stock_code,
|
|
'stock_name': stock_name,
|
|
'industry': industry,
|
|
'analysis_date': datetime.now().strftime('%Y-%m-%d'),
|
|
'score': score,
|
|
'price': float(latest['close']),
|
|
'price_change': float((latest['close'] - prev['close']) / prev['close'] * 100),
|
|
'ma_trend': 'UP' if latest['MA5'] > latest['MA20'] else 'DOWN',
|
|
'rsi': float(latest['RSI']),
|
|
'macd_signal': 'BUY' if latest['MACD'] > latest['Signal'] else 'SELL',
|
|
'volume_status': 'HIGH' if latest['Volume_Ratio'] > 1.5 else 'NORMAL',
|
|
'recommendation': self.get_recommendation(score)
|
|
}
|
|
|
|
return report
|
|
except Exception as e:
|
|
self.logger.error(f"快速分析股票 {stock_code} 时出错: {str(e)}")
|
|
raise
|
|
|
|
|
|
|
|
def get_stock_info(self, stock_code):
|
|
"""获取股票基本信息"""
|
|
import akshare as ak
|
|
|
|
cache_key = f"{stock_code}_info"
|
|
if cache_key in self.data_cache:
|
|
return self.data_cache[cache_key]
|
|
|
|
try:
|
|
|
|
stock_info = ak.stock_individual_info_em(symbol=stock_code)
|
|
|
|
|
|
info_dict = {}
|
|
for _, row in stock_info.iterrows():
|
|
|
|
if len(row) >= 2:
|
|
info_dict[row.iloc[0]] = row.iloc[1]
|
|
|
|
|
|
try:
|
|
stock_name = ak.stock_info_a_code_name()
|
|
|
|
|
|
if '代码' in stock_name.columns and '名称' in stock_name.columns:
|
|
|
|
matched_stocks = stock_name[stock_name['代码'] == stock_code]
|
|
if not matched_stocks.empty:
|
|
name = matched_stocks['名称'].values[0]
|
|
else:
|
|
self.logger.warning(f"未找到股票代码 {stock_code} 的名称信息")
|
|
name = "未知"
|
|
else:
|
|
|
|
possible_code_columns = ['代码', 'code', 'symbol', '股票代码', 'stock_code']
|
|
possible_name_columns = ['名称', 'name', '股票名称', 'stock_name']
|
|
|
|
code_col = next((col for col in possible_code_columns if col in stock_name.columns), None)
|
|
name_col = next((col for col in possible_name_columns if col in stock_name.columns), None)
|
|
|
|
if code_col and name_col:
|
|
matched_stocks = stock_name[stock_name[code_col] == stock_code]
|
|
if not matched_stocks.empty:
|
|
name = matched_stocks[name_col].values[0]
|
|
else:
|
|
name = "未知"
|
|
else:
|
|
self.logger.warning(f"股票信息DataFrame结构不符合预期: {stock_name.columns.tolist()}")
|
|
name = "未知"
|
|
except Exception as e:
|
|
self.logger.error(f"获取股票名称时出错: {str(e)}")
|
|
name = "未知"
|
|
|
|
info_dict['股票名称'] = name
|
|
|
|
|
|
if '行业' not in info_dict:
|
|
info_dict['行业'] = "未知"
|
|
if '地区' not in info_dict:
|
|
info_dict['地区'] = "未知"
|
|
|
|
|
|
self.logger.info(f"获取到股票信息: 名称={name}, 行业={info_dict.get('行业', '未知')}")
|
|
|
|
self.data_cache[cache_key] = info_dict
|
|
return info_dict
|
|
except Exception as e:
|
|
self.logger.error(f"获取股票信息失败: {str(e)}")
|
|
return {"股票名称": "未知", "行业": "未知", "地区": "未知"}
|
|
|
|
def identify_support_resistance(self, df):
|
|
"""识别支撑位和压力位"""
|
|
latest_price = df['close'].iloc[-1]
|
|
|
|
|
|
support_levels = [df['BB_lower'].iloc[-1]]
|
|
resistance_levels = [df['BB_upper'].iloc[-1]]
|
|
|
|
|
|
if latest_price < df['MA5'].iloc[-1]:
|
|
resistance_levels.append(df['MA5'].iloc[-1])
|
|
else:
|
|
support_levels.append(df['MA5'].iloc[-1])
|
|
|
|
if latest_price < df['MA20'].iloc[-1]:
|
|
resistance_levels.append(df['MA20'].iloc[-1])
|
|
else:
|
|
support_levels.append(df['MA20'].iloc[-1])
|
|
|
|
|
|
price_digits = len(str(int(latest_price)))
|
|
base = 10 ** (price_digits - 1)
|
|
|
|
lower_integer = math.floor(latest_price / base) * base
|
|
upper_integer = math.ceil(latest_price / base) * base
|
|
|
|
if lower_integer < latest_price:
|
|
support_levels.append(lower_integer)
|
|
if upper_integer > latest_price:
|
|
resistance_levels.append(upper_integer)
|
|
|
|
|
|
support_levels = sorted(set([round(x, 2) for x in support_levels if x < latest_price]), reverse=True)
|
|
resistance_levels = sorted(set([round(x, 2) for x in resistance_levels if x > latest_price]))
|
|
|
|
|
|
short_term_support = support_levels[:1] if support_levels else []
|
|
medium_term_support = support_levels[1:2] if len(support_levels) > 1 else []
|
|
short_term_resistance = resistance_levels[:1] if resistance_levels else []
|
|
medium_term_resistance = resistance_levels[1:2] if len(resistance_levels) > 1 else []
|
|
|
|
return {
|
|
'support_levels': {
|
|
'short_term': short_term_support,
|
|
'medium_term': medium_term_support
|
|
},
|
|
'resistance_levels': {
|
|
'short_term': short_term_resistance,
|
|
'medium_term': medium_term_resistance
|
|
}
|
|
}
|
|
|
|
def calculate_technical_score(self, df):
|
|
"""计算技术面评分 (0-40分)"""
|
|
try:
|
|
score = 0
|
|
|
|
if len(df) < 2:
|
|
self.logger.warning("数据不足,无法计算技术面评分")
|
|
return {'total': 0, 'trend': 0, 'indicators': 0, 'support_resistance': 0, 'volatility_volume': 0}
|
|
|
|
latest = df.iloc[-1]
|
|
prev = df.iloc[-2]
|
|
prev_close = prev['close']
|
|
|
|
|
|
trend_score = 0
|
|
|
|
|
|
if latest['MA5'] > latest['MA20'] > latest['MA60']:
|
|
trend_score += 5
|
|
elif latest['MA5'] < latest['MA20'] < latest['MA60']:
|
|
trend_score = 0
|
|
else:
|
|
if latest['MA5'] > latest['MA20']:
|
|
trend_score += 3
|
|
if latest['MA20'] > latest['MA60']:
|
|
trend_score += 2
|
|
|
|
|
|
if latest['close'] > latest['MA5']:
|
|
trend_score += 3
|
|
elif latest['close'] > latest['MA20']:
|
|
trend_score += 2
|
|
|
|
|
|
trend_score = min(trend_score, 10)
|
|
score += trend_score
|
|
|
|
|
|
indicator_score = 0
|
|
|
|
|
|
if 40 <= latest['RSI'] <= 60:
|
|
indicator_score += 2
|
|
elif 30 <= latest['RSI'] < 40 or 60 < latest['RSI'] <= 70:
|
|
indicator_score += 4
|
|
elif latest['RSI'] < 30:
|
|
indicator_score += 5
|
|
elif latest['RSI'] > 70:
|
|
indicator_score += 0
|
|
|
|
|
|
if latest['MACD'] > latest['Signal']:
|
|
indicator_score += 3
|
|
else:
|
|
|
|
if latest['MACD_hist'] > prev['MACD_hist']:
|
|
indicator_score += 1
|
|
|
|
|
|
indicator_score = max(0, min(indicator_score, 10))
|
|
score += indicator_score
|
|
|
|
|
|
sr_score = 0
|
|
|
|
|
|
middle_price = latest['close']
|
|
upper_band = latest['BB_upper']
|
|
lower_band = latest['BB_lower']
|
|
|
|
|
|
upper_distance = (upper_band - middle_price) / middle_price * 100
|
|
lower_distance = (middle_price - lower_band) / middle_price * 100
|
|
|
|
if lower_distance < 2:
|
|
sr_score += 5
|
|
elif lower_distance < 5:
|
|
sr_score += 3
|
|
|
|
if upper_distance > 5:
|
|
sr_score += 5
|
|
elif upper_distance > 2:
|
|
sr_score += 2
|
|
|
|
|
|
sr_score = min(sr_score, 10)
|
|
score += sr_score
|
|
|
|
|
|
vol_score = 0
|
|
|
|
|
|
if latest['Volatility'] < 2:
|
|
vol_score += 3
|
|
elif latest['Volatility'] < 4:
|
|
vol_score += 2
|
|
|
|
|
|
if 'Volume_Ratio' in df.columns:
|
|
if latest['Volume_Ratio'] > 1.5 and latest['close'] > prev_close:
|
|
vol_score += 4
|
|
elif latest['Volume_Ratio'] < 0.8 and latest['close'] < prev_close:
|
|
vol_score += 3
|
|
elif latest['Volume_Ratio'] > 1 and latest['close'] > prev_close:
|
|
vol_score += 2
|
|
|
|
|
|
vol_score = min(vol_score, 10)
|
|
score += vol_score
|
|
|
|
|
|
technical_scores = {
|
|
'total': score,
|
|
'trend': trend_score,
|
|
'indicators': indicator_score,
|
|
'support_resistance': sr_score,
|
|
'volatility_volume': vol_score
|
|
}
|
|
|
|
return technical_scores
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"计算技术面评分时出错: {str(e)}")
|
|
self.logger.error(f"错误详情: {traceback.format_exc()}")
|
|
return {'total': 0, 'trend': 0, 'indicators': 0, 'support_resistance': 0, 'volatility_volume': 0}
|
|
|
|
def perform_enhanced_analysis(self, stock_code, market_type='A'):
|
|
"""执行增强版分析"""
|
|
try:
|
|
|
|
start_time = time.time()
|
|
self.logger.info(f"开始执行股票 {stock_code} 的增强分析")
|
|
|
|
|
|
df = self.get_stock_data(stock_code, market_type)
|
|
data_time = time.time()
|
|
self.logger.info(f"获取股票数据耗时: {data_time - start_time:.2f}秒")
|
|
|
|
|
|
df = self.calculate_indicators(df)
|
|
indicator_time = time.time()
|
|
self.logger.info(f"计算技术指标耗时: {indicator_time - data_time:.2f}秒")
|
|
|
|
|
|
latest = df.iloc[-1]
|
|
prev = df.iloc[-2] if len(df) > 1 else latest
|
|
|
|
|
|
sr_levels = self.identify_support_resistance(df)
|
|
|
|
|
|
technical_score = self.calculate_technical_score(df)
|
|
|
|
|
|
stock_info = self.get_stock_info(stock_code)
|
|
|
|
|
|
if 'total' not in technical_score:
|
|
technical_score['total'] = 0
|
|
|
|
|
|
enhanced_report = {
|
|
'basic_info': {
|
|
'stock_code': stock_code,
|
|
'stock_name': stock_info.get('股票名称', '未知'),
|
|
'industry': stock_info.get('行业', '未知'),
|
|
'analysis_date': datetime.now().strftime('%Y-%m-%d')
|
|
},
|
|
'price_data': {
|
|
'current_price': float(latest['close']),
|
|
'price_change': float((latest['close'] - prev['close']) / prev['close'] * 100),
|
|
'price_change_value': float(latest['close'] - prev['close'])
|
|
},
|
|
'technical_analysis': {
|
|
'trend': {
|
|
'ma_trend': 'UP' if latest['MA5'] > latest['MA20'] else 'DOWN',
|
|
'ma_status': "多头排列" if latest['MA5'] > latest['MA20'] > latest['MA60'] else
|
|
"空头排列" if latest['MA5'] < latest['MA20'] < latest['MA60'] else
|
|
"交叉状态",
|
|
'ma_values': {
|
|
'ma5': float(latest['MA5']),
|
|
'ma20': float(latest['MA20']),
|
|
'ma60': float(latest['MA60'])
|
|
}
|
|
},
|
|
'indicators': {
|
|
|
|
'rsi': float(latest['RSI']) if 'RSI' in latest else 50.0,
|
|
'macd': float(latest['MACD']) if 'MACD' in latest else 0.0,
|
|
'macd_signal': float(latest['Signal']) if 'Signal' in latest else 0.0,
|
|
'macd_histogram': float(latest['MACD_hist']) if 'MACD_hist' in latest else 0.0,
|
|
'volatility': float(latest['Volatility']) if 'Volatility' in latest else 0.0
|
|
},
|
|
'volume': {
|
|
'current_volume': float(latest['volume']) if 'volume' in latest else 0.0,
|
|
'volume_ratio': float(latest['Volume_Ratio']) if 'Volume_Ratio' in latest else 1.0,
|
|
'volume_status': '放量' if 'Volume_Ratio' in latest and latest['Volume_Ratio'] > 1.5 else '平量'
|
|
},
|
|
'support_resistance': sr_levels
|
|
},
|
|
'scores': technical_score,
|
|
'recommendation': {
|
|
'action': self.get_recommendation(technical_score['total']),
|
|
'key_points': []
|
|
},
|
|
'ai_analysis': self.get_ai_analysis(df, stock_code)
|
|
}
|
|
|
|
|
|
self._validate_and_fix_report(enhanced_report)
|
|
|
|
|
|
end_time = time.time()
|
|
self.logger.info(f"执行增强分析总耗时: {end_time - start_time:.2f}秒")
|
|
|
|
return enhanced_report
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"执行增强版分析时出错: {str(e)}")
|
|
self.logger.error(traceback.format_exc())
|
|
|
|
|
|
return {
|
|
'basic_info': {
|
|
'stock_code': stock_code,
|
|
'stock_name': '分析失败',
|
|
'industry': '未知',
|
|
'analysis_date': datetime.now().strftime('%Y-%m-%d')
|
|
},
|
|
'price_data': {
|
|
'current_price': 0.0,
|
|
'price_change': 0.0,
|
|
'price_change_value': 0.0
|
|
},
|
|
'technical_analysis': {
|
|
'trend': {
|
|
'ma_trend': 'UNKNOWN',
|
|
'ma_status': '未知',
|
|
'ma_values': {'ma5': 0.0, 'ma20': 0.0, 'ma60': 0.0}
|
|
},
|
|
'indicators': {
|
|
'rsi': 50.0,
|
|
'macd': 0.0,
|
|
'macd_signal': 0.0,
|
|
'macd_histogram': 0.0,
|
|
'volatility': 0.0
|
|
},
|
|
'volume': {
|
|
'current_volume': 0.0,
|
|
'volume_ratio': 0.0,
|
|
'volume_status': 'NORMAL'
|
|
},
|
|
'support_resistance': {
|
|
'support_levels': {'short_term': [], 'medium_term': []},
|
|
'resistance_levels': {'short_term': [], 'medium_term': []}
|
|
}
|
|
},
|
|
'scores': {'total': 0},
|
|
'recommendation': {'action': '分析出错,无法提供建议'},
|
|
'ai_analysis': f"分析过程中出错: {str(e)}"
|
|
}
|
|
|
|
return error_report
|
|
|
|
|
|
def _validate_and_fix_report(self, report):
|
|
"""确保分析报告结构完整"""
|
|
|
|
required_sections = ['basic_info', 'price_data', 'technical_analysis', 'scores', 'recommendation',
|
|
'ai_analysis']
|
|
for section in required_sections:
|
|
if section not in report:
|
|
self.logger.warning(f"报告缺少 {section} 部分,添加空对象")
|
|
report[section] = {}
|
|
|
|
|
|
if 'technical_analysis' in report:
|
|
tech = report['technical_analysis']
|
|
if not isinstance(tech, dict):
|
|
report['technical_analysis'] = {}
|
|
tech = report['technical_analysis']
|
|
|
|
|
|
if 'indicators' not in tech or not isinstance(tech['indicators'], dict):
|
|
tech['indicators'] = {
|
|
'rsi': 50.0,
|
|
'macd': 0.0,
|
|
'macd_signal': 0.0,
|
|
'macd_histogram': 0.0,
|
|
'volatility': 0.0
|
|
}
|
|
|
|
|
|
for key, value in tech['indicators'].items():
|
|
try:
|
|
tech['indicators'][key] = float(value)
|
|
except (TypeError, ValueError):
|
|
tech['indicators'][key] = 0.0 |