Files
sfgrid/core/scoring/features/emotion_features.py
T

148 lines
5.2 KiB
Python

"""
情绪弹性特征 (4维) — calculate_relaxed_emotion_features(df, market_regime)
180 日窗口,筛选 advance_ratio < 0.20 恐慌日 + advance_ratio > 0.55 贪婪日。
"""
import numpy as np
import pandas as pd
from core.scoring.config import PANIC_ADVANCE_RATIO, GREED_ADVANCE_RATIO, WINDOW_180D
def calculate_relaxed_emotion_features(ctx) -> pd.DataFrame:
"""计算情绪弹性特征 (4维)"""
kline = ctx.kline.copy()
mkt = ctx.market_regime.copy() if ctx.market_regime is not None else pd.DataFrame()
if kline.empty or mkt.empty:
return pd.DataFrame()
# 筛选恐慌日和贪婪日
panic_dates = set()
greed_dates = set()
for _, row in mkt.iterrows():
td = row['trade_date']
ar = row.get('advance_ratio', 0.5)
if ar < PANIC_ADVANCE_RATIO:
panic_dates.add(td.date() if hasattr(td, 'date') else td)
if ar > GREED_ADVANCE_RATIO:
greed_dates.add(td.date() if hasattr(td, 'date') else td)
kline = kline.sort_values(['stock_code', 'trade_date'])
kline['td'] = (kline['trade_date'].dt.date
if hasattr(kline['trade_date'], 'dt')
else pd.to_datetime(kline['trade_date']).dt.date)
candidates = ctx.candidates
# 计算全市场平均振幅 (用于恐慌/贪婪 ratio)
kline['amp'] = (kline['high'] - kline['low']) / np.where(
kline['open'] > 0, kline['open'], 1
)
market_amp = kline.groupby('td')['amp'].mean().to_dict()
features = {}
grouped = kline.groupby('stock_code')
for code, group in grouped:
if code not in candidates:
continue
if len(group) < 20:
continue
g = group.sort_values('trade_date').tail(WINDOW_180D)
closes = g['close'].values
opens = g['open'].values
highs = g['high'].values
lows = g['low'].values
dates = g['td'].values
amps = (highs - lows) / np.where(opens > 0, opens, 1)
feat = {}
# 恐慌日分析
panic_indices = [i for i, d in enumerate(dates) if d in panic_dates]
if panic_indices:
panic_amp_ratios = []
panic_drop_ratios = []
panic_rebounds = []
for pi in panic_indices:
td = dates[pi]
mkt_amp_val = market_amp.get(td, amps[pi])
if mkt_amp_val > 0:
panic_amp_ratios.append(amps[pi] / mkt_amp_val)
# 恐慌日跌幅
if pi > 0 and closes[pi - 1] > 0:
drop = (closes[pi] - closes[pi - 1]) / closes[pi - 1]
# 全市场跌幅: 用 advance_ratio 估算
ar = _get_advance_ratio(mkt, td)
market_drop = -0.02 if ar < PANIC_ADVANCE_RATIO else -0.005
if market_drop != 0:
panic_drop_ratios.append(drop / market_drop)
# 恐慌次日反弹
if pi + 1 < len(g) and closes[pi] > 0:
panic_rebounds.append(highs[pi + 1] / closes[pi] - 1)
# 64. relaxed_panic_amplitude_ratio
feat['relaxed_panic_amplitude_ratio'] = (
np.median(panic_amp_ratios) if panic_amp_ratios else 1.0
)
# 65. relaxed_panic_rebound_strength
feat['relaxed_panic_rebound_strength'] = (
np.median(panic_rebounds) if panic_rebounds else 0.0
)
else:
feat['relaxed_panic_amplitude_ratio'] = 1.0
feat['relaxed_panic_rebound_strength'] = 0.0
# 贪婪日分析
greed_indices = [i for i, d in enumerate(dates) if d in greed_dates]
if greed_indices:
greed_gains = []
greed_amp_ratios = []
for gi in greed_indices:
td = dates[gi]
if gi > 0 and closes[gi - 1] > 0:
gain = (closes[gi] - closes[gi - 1]) / closes[gi - 1]
# 全市场收益
ar = _get_advance_ratio(mkt, td)
market_gain = 0.01 if ar > GREED_ADVANCE_RATIO else 0.003
if market_gain > 0:
greed_gains.append(gain / market_gain)
mkt_amp_val = market_amp.get(td, amps[gi])
if mkt_amp_val > 0:
greed_amp_ratios.append(amps[gi] / mkt_amp_val)
# 66. relaxed_greed_relative_gain
feat['relaxed_greed_relative_gain'] = (
np.median(greed_gains) if greed_gains else 0.0
)
# 67. relaxed_greed_amplitude_ratio
feat['relaxed_greed_amplitude_ratio'] = (
np.median(greed_amp_ratios) if greed_amp_ratios else 1.0
)
else:
feat['relaxed_greed_relative_gain'] = 0.0
feat['relaxed_greed_amplitude_ratio'] = 1.0
features[code] = feat
return pd.DataFrame.from_dict(features, orient='index')
def _get_advance_ratio(mkt_df, trade_date):
"""获取指定日期的 advance_ratio"""
if mkt_df is None or mkt_df.empty:
return 0.5
td = trade_date
match = mkt_df[mkt_df['trade_date'] == td]
if not match.empty:
return match.iloc[0].get('advance_ratio', 0.5)
return 0.5