single_stock.py
"""
单票量化策略模块
该模块仅负责单票分析判断,不做全局股票池遍历、数据批量获取
"""
import logging
from typing import Dict, List, Any
import pandas as pd
# 单票量化策略模块日志
logger = logging.getLogger("AIQuant.single_stock")
logger.setLevel(logging.INFO)
# 导入策略配置
from config.strategy_config import SINGLE_STOCK_CONFIG
class StockAnalysisResult:
"""
股票分析结果类
标准化输出结构
"""
def __init__(self,
symbol: str,
score: float,
reasons: List[str],
rating: str,
signal: str,
metrics: Dict[str, float]):
"""
初始化股票分析结果
Args:
symbol: 股票代码
score: 综合评分(0-100)
reasons: 评分理由(分点字符串)
rating: 标的等级(S/A/B/C)
signal: 交易信号(买入/持有/卖出)
metrics: 关键指标数值(字典)
"""
self.symbol = symbol
self.score = score
self.reasons = reasons
self.rating = rating
self.signal = signal
self.metrics = metrics
def to_dict(self) -> Dict[str, Any]:
"""
转换为字典格式
Returns:
分析结果字典
"""
return {
"股票代码": self.symbol,
"综合评分": self.score,
"评分理由": self.reasons,
"标的等级": self.rating,
"交易信号": self.signal,
"关键指标": self.metrics
}
def __str__(self) -> str:
"""
字符串表示
Returns:
字符串表示
"""
return (f"股票代码: {self.symbol}, 综合评分: {self.score:.2f}, "
f"标的等级: {self.rating}, 交易信号: {self.signal}, "
f"关键指标: {self.metrics}")
class SingleStockAnalyzer:
"""
单票量化分析器
仅负责单票分析判断,不做全局股票池遍历、数据批量获取
"""
def __init__(self, config: Dict[str, Any] = None):
"""
初始化单票量化分析器
Args:
config: 分析器配置参数
"""
self.config = config or {}
self.logger = logger
def analyze(
self,
symbol: str,
kline_data: pd.DataFrame
) -> StockAnalysisResult:
"""
分析单只股票
Args:
symbol: 股票代码(str)
kline_data: 单票完整 K 线数据(DataFrame,含 OHLCV+成交量+均线等基础指标)
Returns:
股票分析结果(StockAnalysisResult)
"""
self.logger.info(f"开始分析股票: {symbol}")
self.logger.debug(f"K 线数据形状: {kline_data.shape}")
# 初始化分析结果
score = 0.0
reasons = []
rating = "C"
signal = "卖出"
metrics = {}
try:
# 1. 数据质量检查
if self._check_data_quality(kline_data):
reasons.append("数据质量良好")
score += 10
else:
reasons.append("数据质量较差")
score += 2
# 2. 趋势分析
trend_score, trend_reasons = self._analyze_trend(kline_data)
score += trend_score
reasons.extend(trend_reasons)
# 3. 技术指标分析
tech_score, tech_reasons, tech_metrics = \
self._analyze_technical_indicators(kline_data)
score += tech_score
reasons.extend(tech_reasons)
metrics.update(tech_metrics)
# 4. 成交量分析
volume_score, volume_reasons, volume_metrics = \
self._analyze_volume(kline_data)
score += volume_score
reasons.extend(volume_reasons)
metrics.update(volume_metrics)
# 5. 风险评估
risk_score, risk_reasons, risk_metrics = self._analyze_risk(
kline_data)
score += risk_score
reasons.extend(risk_reasons)
metrics.update(risk_metrics)
# 6. 计算综合评分和等级
score = min(max(score, 0), 100)
rating = self._calculate_rating(score)
# 7. 确定交易信号
signal = self._determine_trading_signal(score, kline_data)
self.logger.info(
f"股票分析完成: {symbol} -> 评分: {score:.2f}, "
f"等级: {rating}, 信号: {signal}")
except Exception as e:
self.logger.error(f"分析股票 {symbol} 时出错: {e}")
reasons.append(f"分析过程中出现错误: {str(e)}")
return StockAnalysisResult(
symbol=symbol,
score=score,
reasons=reasons,
rating=rating,
signal=signal,
metrics=metrics
)
def _check_data_quality(self, kline_data: pd.DataFrame) -> bool:
"""
检查数据质量
Args:
kline_data: K 线数据
Returns:
数据质量是否良好的布尔值
"""
# 检查数据完整性
if kline_data.empty:
return False
# 检查必要列是否存在
required_columns = ["open", "high", "low", "close", "volume"]
for col in required_columns:
if col not in kline_data.columns:
return False
# 检查数据点数
if len(kline_data) < SINGLE_STOCK_CONFIG[
"data_quality"]["min_data_points"]:
return False
# 检查是否有缺失值
missing_ratio = kline_data.isnull().sum().sum(
) / (kline_data.shape[0] * kline_data.shape[1])
if missing_ratio > SINGLE_STOCK_CONFIG[
"data_quality"]["max_missing_ratio"]:
return False
return True
def _analyze_trend(self, kline_data: pd.DataFrame) -> (float, List[str]):
"""
分析趋势
Args:
kline_data: K 线数据
Returns:
(趋势得分, 趋势分析理由)
"""
score = 0.0
reasons = []
# 检查长期趋势
long_term_window = SINGLE_STOCK_CONFIG["trend"]["long_term_window"]
if len(kline_data) >= long_term_window:
kline_data[f"ma{long_term_window}"] = kline_data["close"].rolling(
window=long_term_window).mean()
current_price = kline_data["close"].iloc[-1]
ma_long = kline_data[f"ma{long_term_window}"].iloc[-1]
if current_price > ma_long:
score += 20
reasons.append(f"价格在 {long_term_window} 日均线上方,处于上升趋势")
else:
score += 5
reasons.append(f"价格在 {long_term_window} 日均线下方,处于下降趋势")
# 检查中期趋势
short_term_window = SINGLE_STOCK_CONFIG["trend"]["short_term_window"]
if len(kline_data) >= short_term_window:
kline_data[f"ma{short_term_window}"] = kline_data["close"].rolling(
window=short_term_window).mean()
current_price = kline_data["close"].iloc[-1]
ma_short = kline_data[f"ma{short_term_window}"].iloc[-1]
if current_price > ma_short:
score += 15
reasons.append(f"价格在 {short_term_window} 日均线上方,中期趋势向上")
else:
score += 5
reasons.append(f"价格在 {short_term_window} 日均线下方,中期趋势向下")
# 应用权重
score *= SINGLE_STOCK_CONFIG["trend"]["trend_score_weight"]
return score, reasons
def _analyze_technical_indicators(self,
kline_data: pd.DataFrame) -> (
float, List[str], Dict[str, float]
):
"""
分析技术指标
Args:
kline_data: K 线数据
Returns:
(技术指标得分, 技术指标分析理由, 技术指标数值)
"""
score = 0.0
reasons = []
metrics = {}
# 计算 RSI
rsi_window = SINGLE_STOCK_CONFIG["technical"]["rsi_window"]
if len(kline_data) >= rsi_window:
delta = kline_data["close"].diff()
gain = (delta.where(delta > 0, 0)).rolling(
window=rsi_window).mean()
loss = (-delta.where(delta < 0, 0)
).rolling(window=rsi_window).mean()
rs = gain / loss
rsi = 100 - (100 / (1 + rs))
metrics["rsi"] = rsi.iloc[-1]
rsi_normal_range = SINGLE_STOCK_CONFIG["technical"][
"rsi_normal_range"]
rsi_overbought = SINGLE_STOCK_CONFIG["technical"]["rsi_overbought"]
rsi_oversold = SINGLE_STOCK_CONFIG["technical"]["rsi_oversold"]
if rsi_normal_range[0] < rsi.iloc[-1] < rsi_normal_range[1]:
score += 15
reasons.append(f"RSI 值为 {rsi.iloc[-1]:.2f},处于正常区间")
elif rsi.iloc[-1] <= rsi_oversold:
score += 5
reasons.append(f"RSI 值为 {rsi.iloc[-1]:.2f},处于超卖区")
elif rsi.iloc[-1] >= rsi_overbought:
score += 5
reasons.append(f"RSI 值为 {rsi.iloc[-1]:.2f},处于超买区")
# 计算 MACD
macd_fast = SINGLE_STOCK_CONFIG["technical"]["macd_fast"]
macd_slow = SINGLE_STOCK_CONFIG["technical"]["macd_slow"]
macd_signal = SINGLE_STOCK_CONFIG["technical"]["macd_signal"]
if len(kline_data) >= macd_slow:
kline_data[f"ema{macd_fast}"] = kline_data["close"].ewm(
span=macd_fast, adjust=False).mean()
kline_data[f"ema{macd_slow}"] = kline_data["close"].ewm(
span=macd_slow, adjust=False).mean()
kline_data["macd"] = kline_data[f"ema{macd_fast}"] - \
kline_data[f"ema{macd_slow}"]
kline_data["macd_signal"] = kline_data["macd"].ewm(
span=macd_signal, adjust=False).mean()
metrics["macd"] = kline_data["macd"].iloc[-1]
metrics["macd_signal"] = kline_data["macd_signal"].iloc[-1]
if kline_data["macd"].iloc[-1] > \
kline_data["macd_signal"].iloc[-1]:
score += 15
reasons.append("MACD 金叉,买入信号")
else:
score += 5
reasons.append("MACD 死叉,卖出信号")
# 应用权重
score *= SINGLE_STOCK_CONFIG["technical"]["technical_score_weight"]
return score, reasons, metrics
def _analyze_volume(self,
kline_data: pd.DataFrame) -> (float,
List[str],
Dict[str, float]):
"""
分析成交量
Args:
kline_data: K 线数据
Returns:
(成交量得分, 成交量分析理由, 成交量指标数值)
"""
score = 0.0
reasons = []
metrics = {}
# 计算成交量均值
volume_window = SINGLE_STOCK_CONFIG["volume"]["volume_window"]
if len(kline_data) >= volume_window:
avg_volume = kline_data["volume"].rolling(
window=volume_window).mean()
current_volume = kline_data["volume"].iloc[-1]
metrics["current_volume"] = current_volume
metrics["avg_volume"] = avg_volume.iloc[-1]
volume_increase_ratio = SINGLE_STOCK_CONFIG[
"volume"]["volume_increase_ratio"]
volume_decrease_ratio = SINGLE_STOCK_CONFIG[
"volume"]["volume_decrease_ratio"]
if current_volume > avg_volume.iloc[-1] * volume_increase_ratio:
score += 10
reasons.append("成交量放大,市场关注增加")
elif current_volume < avg_volume.iloc[-1] * volume_decrease_ratio:
score += 2
reasons.append("成交量萎缩,市场关注减少")
else:
score += 5
reasons.append("成交量正常")
# 应用权重
score *= SINGLE_STOCK_CONFIG["volume"]["volume_score_weight"]
return score, reasons, metrics
def _analyze_risk(self,
kline_data: pd.DataFrame) -> (float,
List[str],
Dict[str, float]):
"""
分析风险
Args:
kline_data: K 线数据
Returns:
(风险评估得分, 风险评估理由, 风险指标数值)
"""
score = 0.0
reasons = []
metrics = {}
# 计算波动率
if len(kline_data) >= 10:
returns = kline_data["close"].pct_change()
volatility = returns.rolling(
window=10).std() * (252 ** 0.5) # 年化波动率
metrics["volatility"] = volatility.iloc[-1]
if volatility.iloc[-1] < 0.3:
score += 10
reasons.append("波动率较低,风险可控")
elif volatility.iloc[-1] < 0.5:
score += 5
reasons.append("波动率适中,风险中等")
else:
score += 2
reasons.append("波动率较高,风险较大")
# 计算最大回撤
if len(kline_data) >= 20:
roll_max = kline_data["close"].rolling(window=20).max()
daily_drawdown = kline_data["close"] / roll_max - 1.0
max_drawdown = daily_drawdown.rolling(window=20).min()
metrics["max_drawdown"] = max_drawdown.iloc[-1]
if max_drawdown.iloc[-1] > -0.1:
score += 10
reasons.append("最大回撤较小,风险可控")
elif max_drawdown.iloc[-1] > -0.2:
score += 5
reasons.append("最大回撤适中,风险中等")
else:
score += 2
reasons.append("最大回撤较大,风险较大")
return score, reasons, metrics
def _calculate_rating(self, score: float) -> str:
"""
根据评分计算标的等级
Args:
score: 综合评分
Returns:
标的等级(S/A/B/C)
"""
if score >= 85:
return "S"
elif score >= 70:
return "A"
elif score >= 50:
return "B"
else:
return "C"
def _determine_trading_signal(self,
score: float,
kline_data: pd.DataFrame) -> str:
"""
确定交易信号
Args:
score: 综合评分
kline_data: K 线数据
Returns:
交易信号(买入/持有/卖出)
"""
if score >= 50:
return "买入"
elif score >= 40:
return "持有"
else:
return "卖出"