148 lines
5.2 KiB
Python
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
|