refactor(archive): move unused modules to archive/
Archive legacy framework and utility modules that are no longer referenced by the active core (datasource/ and rotation/): - framework/ -> archive/framework/ - framework_v2/ -> archive/framework_v2/ - strategies/ -> archive/strategies/ - config/ -> archive/config/ - visualization/ -> archive/visualization/ - scripts/ -> archive/scripts/ - tests/ -> archive/tests/ - run_rotation.py, run_us_rotation.py -> archive/single_files/ - compare_*.py, test_api_dates.py -> archive/single_files/
This commit is contained in:
366
archive/strategies/shared/signals/selectors.py
Normal file
366
archive/strategies/shared/signals/selectors.py
Normal file
@@ -0,0 +1,366 @@
|
||||
"""
|
||||
定制信号生成器实现
|
||||
|
||||
这些信号生成器继承framework.core.signals.SignalGenerator
|
||||
"""
|
||||
|
||||
from framework.signals import SignalGenerator
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import Dict, List, Optional, Any
|
||||
|
||||
|
||||
class TopNSelector(SignalGenerator):
|
||||
"""
|
||||
Top N选股器(定制实现)
|
||||
|
||||
用于轮动策略:
|
||||
- 按因子值排序,选出Top N标的
|
||||
- 支持分组选股(先类内竞争,再跨类排序)
|
||||
- 支持调仓阈值检查(新组合得分需超过当前组合一定比例才调仓)
|
||||
- V3: 支持动态阈值(短债动量作为过滤阈值)
|
||||
|
||||
参数:
|
||||
- select_num: 选中数量(默认3)
|
||||
- group_by: 分组键名(可选,如'market')
|
||||
- group_mapping: 分组映射字典(可选,{code: group})
|
||||
- top_per_group: 每组选中数量(默认1)
|
||||
- min_score: 最小得分阈值(可选,如0表示过滤负分)
|
||||
- rebalance_threshold: 调仓阈值(可选,新组合得分需超过当前组合X%才调仓)
|
||||
- rebalance_days: 最低调仓周期(可选,持仓至少N天才能调仓)
|
||||
- bond_threshold_config: V3动态阈值配置
|
||||
"""
|
||||
|
||||
mode = "top_n"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
select_num: int = 3,
|
||||
group_by: Optional[str] = None,
|
||||
group_mapping: Optional[Dict[str, str]] = None,
|
||||
top_per_group: int = 1,
|
||||
min_score: Optional[float] = None,
|
||||
rebalance_threshold: float = 0.0,
|
||||
rebalance_days: int = 1,
|
||||
bond_threshold_config: Optional[Dict] = None
|
||||
):
|
||||
super().__init__(
|
||||
select_num=select_num,
|
||||
group_by=group_by,
|
||||
group_mapping=group_mapping,
|
||||
top_per_group=top_per_group,
|
||||
min_score=min_score,
|
||||
rebalance_threshold=rebalance_threshold,
|
||||
rebalance_days=rebalance_days
|
||||
)
|
||||
self.select_num = select_num
|
||||
self.group_by = group_by
|
||||
self.group_mapping = group_mapping or {}
|
||||
self.top_per_group = top_per_group
|
||||
self.min_score = min_score
|
||||
self.rebalance_threshold = rebalance_threshold
|
||||
self.rebalance_days = rebalance_days
|
||||
self.bond_threshold_config = bond_threshold_config or {}
|
||||
|
||||
def _get_dynamic_threshold(self, scores: Dict[str, float]) -> float:
|
||||
"""获取动态阈值:短债动量 × ratio,无数据时退化为 min_score
|
||||
|
||||
V3动态阈值逻辑:
|
||||
- 若bond_threshold.enabled=true,阈值 = 短债动量 × ratio
|
||||
- 若短债无数据或动量<0,退化为固定min_score
|
||||
- 若enabled=false,退化为固定min_score
|
||||
"""
|
||||
cfg = self.bond_threshold_config
|
||||
if not cfg.get('enabled', False):
|
||||
return self.min_score if self.min_score is not None else 0.0
|
||||
|
||||
bond_code = cfg.get('bond_code', '931862.CSI')
|
||||
ratio = cfg.get('ratio', 1.0)
|
||||
|
||||
bond_score = scores.get(bond_code, None)
|
||||
if bond_score is None or bond_score < 0:
|
||||
return self.min_score if self.min_score is not None else 0.0
|
||||
|
||||
return bond_score * ratio
|
||||
|
||||
def generate(self, factor_data: pd.DataFrame) -> pd.DataFrame:
|
||||
"""生成Top N选股信号(支持调仓周期控制)"""
|
||||
result = pd.DataFrame(index=factor_data.index)
|
||||
|
||||
factor_cols = self._get_factor_columns(factor_data)
|
||||
|
||||
if not factor_cols:
|
||||
result['signal'] = ''
|
||||
return result
|
||||
|
||||
# Step 1: 每日目标组合(不考虑调仓周期)
|
||||
daily_target = []
|
||||
for date in factor_data.index:
|
||||
row = factor_data.loc[date]
|
||||
|
||||
# 提取得分
|
||||
scores = {}
|
||||
for col in factor_cols:
|
||||
score = row[col]
|
||||
if pd.notna(score):
|
||||
scores[col] = score
|
||||
|
||||
# V3: 过滤前检查bond是否有因子数据(用于填充守卫)
|
||||
cfg = self.bond_threshold_config
|
||||
bond_code = cfg.get('bond_code', '931862.CSI') if cfg.get('enabled') else None
|
||||
bond_has_data = bond_code in scores # scores此时是过滤前的完整字典
|
||||
|
||||
# V3: 动态阈值过滤(替代固定 min_score)
|
||||
threshold = self._get_dynamic_threshold(scores)
|
||||
scores = {k: v for k, v in scores.items() if v >= threshold}
|
||||
|
||||
# 分组选股或全局选股
|
||||
if self.group_mapping:
|
||||
selected = self._grouped_selection(scores, bond_has_data)
|
||||
else:
|
||||
selected = self._global_top_n(scores)
|
||||
|
||||
daily_target.append(','.join(selected) if selected else '')
|
||||
|
||||
# Step 2: 逐日生成信号(调仓周期控制)
|
||||
signals = self._apply_rebalance_control(daily_target, factor_data)
|
||||
|
||||
result['signal_raw'] = daily_target # 每日目标组合
|
||||
result['signal'] = signals
|
||||
|
||||
# T+1执行:信号向后移位1天
|
||||
result['signal'] = result['signal'].shift(1)
|
||||
|
||||
return result
|
||||
|
||||
def _get_factor_columns(self, data: pd.DataFrame) -> List[str]:
|
||||
"""获取因子列名"""
|
||||
exclude_cols = ['signal', 'signal_raw', 'group_info', 'combined', 'open', 'high', 'low', 'close', 'volume']
|
||||
return [col for col in data.columns if col not in exclude_cols and not col.endswith('_weighted')]
|
||||
|
||||
def _global_top_n(self, scores: Dict[str, float]) -> List[str]:
|
||||
"""全局Top N选股"""
|
||||
if not scores:
|
||||
return []
|
||||
|
||||
sorted_items = sorted(scores.items(), key=lambda x: x[1], reverse=True)
|
||||
return [item[0] for item in sorted_items[:self.select_num]]
|
||||
|
||||
def _apply_rebalance_control(self, daily_target: List[str], factor_data: pd.DataFrame) -> List[str]:
|
||||
"""应用调仓周期控制"""
|
||||
signals = []
|
||||
current_held = None
|
||||
last_rebalance_idx = 0
|
||||
|
||||
for i, target in enumerate(daily_target):
|
||||
# 初始持仓为空,等待第一个有效信号
|
||||
if current_held is None:
|
||||
if not target:
|
||||
signals.append('')
|
||||
continue
|
||||
current_held = target
|
||||
last_rebalance_idx = i
|
||||
signals.append(current_held)
|
||||
continue
|
||||
|
||||
# 检查调仓周期
|
||||
days_since = i - last_rebalance_idx
|
||||
if days_since < self.rebalance_days:
|
||||
# 未达到最低调仓周期,保持当前持仓
|
||||
signals.append(current_held)
|
||||
continue
|
||||
|
||||
# 检查是否应该调仓
|
||||
if target: # 目标信号有效
|
||||
should = self._check_rebalance(
|
||||
factor_data.iloc[i],
|
||||
current_held,
|
||||
target,
|
||||
self._get_factor_columns(factor_data)
|
||||
)
|
||||
if should:
|
||||
current_held = target
|
||||
last_rebalance_idx = i
|
||||
else:
|
||||
# V3: target为空时保持当前持仓不变(与独立脚本行为一致)
|
||||
# 在有bond数据的时期target不会为空(会被bond填充)
|
||||
# target为空仅发生在2002-2007无bond数据期
|
||||
# 保持旧持仓比突然清仓更平滑
|
||||
pass
|
||||
|
||||
signals.append(current_held)
|
||||
|
||||
return signals
|
||||
|
||||
def _check_rebalance(
|
||||
self,
|
||||
row: pd.Series,
|
||||
current_held: str,
|
||||
target: str,
|
||||
factor_cols: List[str]
|
||||
) -> bool:
|
||||
"""检查是否应该调仓(得分阈值检查)"""
|
||||
# 提取当前持仓和目标持仓的代码
|
||||
old_codes = [c for c in current_held.split(',') if c]
|
||||
new_codes = [c for c in target.split(',') if c]
|
||||
|
||||
if not new_codes or not old_codes:
|
||||
return True
|
||||
|
||||
if set(new_codes) == set(old_codes):
|
||||
return False # 组合完全相同,不调仓
|
||||
|
||||
# 计算新旧组合的总得分
|
||||
# 修复: 按实际份数计算得分(支持重复代码如"931862.CSI,931862.CSI")
|
||||
old_total = sum(float(row.get(c, 0)) for c in old_codes)
|
||||
new_total = sum(float(row.get(c, 0)) for c in new_codes)
|
||||
|
||||
# 新组合得分需超过当前组合一定比例才调仓
|
||||
# 即使 threshold=0,也要确保 new_total >= old_total
|
||||
if old_total > 0:
|
||||
return (new_total / old_total - 1) >= self.rebalance_threshold
|
||||
|
||||
return new_total > 0
|
||||
|
||||
def _grouped_selection(self, scores: Dict[str, float], bond_has_data: bool = True) -> List[str]:
|
||||
"""V3分组选股:BOND不参与竞争,空余仓位填充短债
|
||||
|
||||
V3逻辑:
|
||||
1. BOND大类标的不参与冠军竞争(它是阈值,不是候选)
|
||||
2. 选出不足 select_num 只时,用短债填充
|
||||
3. bond无数据时(2002-2007)不填充
|
||||
4. V2退化:若bond_threshold.enabled=false,BOND正常参与竞争
|
||||
"""
|
||||
if not scores:
|
||||
return []
|
||||
|
||||
cfg = self.bond_threshold_config
|
||||
bond_code = cfg.get('bond_code', '931862.CSI') if cfg.get('enabled') else None
|
||||
|
||||
# 建立 group -> (code, score) 映射
|
||||
# V3: 排除 BOND 大类(它不参与竞争)
|
||||
group_champions = {}
|
||||
for code, score in scores.items():
|
||||
group = self.group_mapping.get(code, 'default')
|
||||
|
||||
# V3: BOND大类不参与竞争
|
||||
if cfg.get('enabled') and group == 'BOND':
|
||||
continue
|
||||
|
||||
if group not in group_champions or score > group_champions[group][1]:
|
||||
group_champions[group] = (code, score)
|
||||
|
||||
# 跨类排序取 Top N
|
||||
sorted_champions = sorted(group_champions.values(), key=lambda x: x[1], reverse=True)
|
||||
selected = [code for code, score in sorted_champions[:self.select_num]]
|
||||
|
||||
# V3: 空余仓位填充短债
|
||||
# 短债填充是防御机制,但需要有数据才能填充
|
||||
# bond有数据(含负值)→ 填充 ✓(防御机制不受动量影响)
|
||||
# bond无数据(NaN)→ 不填充 ✓(2002-2007正常退化)
|
||||
if cfg.get('fill_bond', False) and bond_code and bond_has_data:
|
||||
n_bond_slots = self.select_num - len(selected)
|
||||
if n_bond_slots > 0:
|
||||
for _ in range(n_bond_slots):
|
||||
selected.append(bond_code)
|
||||
|
||||
return selected
|
||||
|
||||
|
||||
class TrendFollower(SignalGenerator):
|
||||
"""趋势跟随器(定制实现)"""
|
||||
|
||||
mode = "trend"
|
||||
|
||||
def __init__(self, entry_threshold: float = 0.02, exit_threshold: float = -0.02, select_num: int = 1):
|
||||
super().__init__(entry_threshold=entry_threshold, exit_threshold=exit_threshold, select_num=select_num)
|
||||
self.entry_threshold = entry_threshold
|
||||
self.exit_threshold = exit_threshold
|
||||
self.select_num = select_num
|
||||
|
||||
def generate(self, factor_data: pd.DataFrame) -> pd.DataFrame:
|
||||
"""生成趋势跟随信号"""
|
||||
result = pd.DataFrame(index=factor_data.index)
|
||||
|
||||
factor_cols = self._get_factor_columns(factor_data)
|
||||
|
||||
for col in factor_cols:
|
||||
trend_strength = factor_data[col]
|
||||
|
||||
result[f'{col}_entry'] = trend_strength > self.entry_threshold
|
||||
result[f'{col}_exit'] = trend_strength < self.exit_threshold
|
||||
|
||||
signals = []
|
||||
for date in result.index:
|
||||
entry_signals = []
|
||||
for col in factor_cols:
|
||||
if result.loc[date, f'{col}_entry']:
|
||||
score = factor_data.loc[date, col]
|
||||
if pd.notna(score):
|
||||
entry_signals.append((col, score))
|
||||
|
||||
entry_signals.sort(key=lambda x: x[1], reverse=True)
|
||||
selected = [item[0] for item in entry_signals[:self.select_num]]
|
||||
signals.append(','.join(selected) if selected else '')
|
||||
|
||||
result['signal'] = signals
|
||||
result['signal'] = result['signal'].shift(1)
|
||||
|
||||
return result
|
||||
|
||||
def _get_factor_columns(self, data: pd.DataFrame) -> List[str]:
|
||||
"""获取因子列名"""
|
||||
exclude_cols = ['signal', 'signal_raw', 'combined', 'open', 'high', 'low', 'close', 'volume']
|
||||
return [col for col in data.columns if col not in exclude_cols and not col.endswith('_weighted')]
|
||||
|
||||
|
||||
class ReversalTrader(SignalGenerator):
|
||||
"""反转交易器(定制实现)"""
|
||||
|
||||
mode = "reversal"
|
||||
|
||||
def __init__(self, overbought: float = 70, oversold: float = 30, reversal_threshold: float = 0.1):
|
||||
super().__init__(overbought=overbought, oversold=oversold, reversal_threshold=reversal_threshold)
|
||||
self.overbought = overbought
|
||||
self.oversold = oversold
|
||||
self.reversal_threshold = reversal_threshold
|
||||
|
||||
def generate(self, factor_data: pd.DataFrame) -> pd.DataFrame:
|
||||
"""生成反转交易信号"""
|
||||
result = pd.DataFrame(index=factor_data.index)
|
||||
|
||||
factor_cols = self._get_factor_columns(factor_data)
|
||||
|
||||
for col in factor_cols:
|
||||
reversal_signal = factor_data[col]
|
||||
|
||||
result[f'{col}_buy'] = reversal_signal > self.reversal_threshold
|
||||
result[f'{col}_sell'] = reversal_signal < -self.reversal_threshold
|
||||
|
||||
signals = []
|
||||
for date in result.index:
|
||||
buy_signals = []
|
||||
sell_signals = []
|
||||
|
||||
for col in factor_cols:
|
||||
if result.loc[date, f'{col}_buy']:
|
||||
buy_signals.append(col)
|
||||
if result.loc[date, f'{col}_sell']:
|
||||
sell_signals.append(col)
|
||||
|
||||
if buy_signals:
|
||||
signals.append(f"BUY:{','.join(buy_signals)}")
|
||||
elif sell_signals:
|
||||
signals.append(f"SELL:{','.join(sell_signals)}")
|
||||
else:
|
||||
signals.append('')
|
||||
|
||||
result['signal'] = signals
|
||||
result['signal'] = result['signal'].shift(1)
|
||||
|
||||
return result
|
||||
|
||||
def _get_factor_columns(self, data: pd.DataFrame) -> List[str]:
|
||||
"""获取因子列名"""
|
||||
exclude_cols = ['signal', 'signal_raw', 'combined', 'open', 'high', 'low', 'close', 'volume']
|
||||
return [col for col in data.columns if col not in exclude_cols and not col.endswith('_weighted')]
|
||||
Reference in New Issue
Block a user