254 lines
8.0 KiB
Python
254 lines
8.0 KiB
Python
"""
|
|
策略模块 — 信号生成、权重分配、组合构建
|
|
"""
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from typing import Dict, List, Optional, Set, Tuple
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 信号生成器
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class SignalGenerator(ABC):
|
|
"""信号生成器基类 — 将因子值转换为交易信号"""
|
|
|
|
@abstractmethod
|
|
def generate(self, factor_df: pd.DataFrame, **kwargs) -> pd.DataFrame:
|
|
"""
|
|
factor_df: DataFrame, index=date, columns=stock_codes, values=factor值
|
|
返回信号 DataFrame,同结构,1=做多, 0=平仓, -1=做空
|
|
"""
|
|
...
|
|
|
|
|
|
class QuantileSignal(SignalGenerator):
|
|
"""基于因子分位数的信号生成器"""
|
|
|
|
def __init__(
|
|
self,
|
|
n_quantiles: int = 5,
|
|
long_quantile: int = 5,
|
|
short_quantile: int = 1,
|
|
min_stocks: int = 10,
|
|
):
|
|
self.n_quantiles = n_quantiles
|
|
self.long_quantile = long_quantile
|
|
self.short_quantile = short_quantile
|
|
self.min_stocks = min_stocks
|
|
|
|
def generate(self, factor_df: pd.DataFrame, **kwargs) -> pd.DataFrame:
|
|
"""
|
|
逐截面按因子值分组:
|
|
long_quantile 档 → +1
|
|
short_quantile 档 → -1
|
|
其余 → 0
|
|
"""
|
|
signal = pd.DataFrame(0, index=factor_df.index, columns=factor_df.columns)
|
|
|
|
for date in factor_df.index:
|
|
row = factor_df.loc[date].dropna()
|
|
if len(row) < self.min_stocks:
|
|
continue
|
|
try:
|
|
bins = pd.qcut(
|
|
row, self.n_quantiles, labels=False, duplicates="drop"
|
|
)
|
|
except ValueError:
|
|
continue
|
|
signal.loc[date, bins == self.long_quantile - 1] = 1
|
|
signal.loc[date, bins == self.short_quantile - 1] = -1
|
|
|
|
return signal
|
|
|
|
|
|
class ZScoreSignal(SignalGenerator):
|
|
"""基于 Z-Score 阈值的信号生成器"""
|
|
|
|
def __init__(self, entry_z: float = 1.0, exit_z: float = 0.5):
|
|
self.entry_z = entry_z
|
|
self.exit_z = exit_z
|
|
|
|
def generate(self, factor_df: pd.DataFrame, **kwargs) -> pd.DataFrame:
|
|
"""
|
|
因子截面标准化(Z-Score):
|
|
> entry_z → +1
|
|
< -entry_z → -1
|
|
其余 → 0
|
|
"""
|
|
signal = pd.DataFrame(0, index=factor_df.index, columns=factor_df.columns)
|
|
for date in factor_df.index:
|
|
row = factor_df.loc[date]
|
|
valid = row.notna()
|
|
if valid.sum() < 3:
|
|
continue
|
|
z = (row - row[valid].mean()) / (row[valid].std() + 1e-12)
|
|
signal.loc[date, z > self.entry_z] = 1
|
|
signal.loc[date, z < -self.entry_z] = -1
|
|
return signal
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 权重分配器
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class WeightAllocator(ABC):
|
|
"""权重分配器基类"""
|
|
|
|
@abstractmethod
|
|
def allocate(
|
|
self,
|
|
signals: pd.DataFrame,
|
|
prices: pd.DataFrame,
|
|
cash: float,
|
|
positions: Dict[str, int],
|
|
**kwargs,
|
|
) -> Dict[str, float]:
|
|
"""
|
|
返回目标权重字典 {stock_code: weight}
|
|
weight 为目标持仓市值占比 (0~1)
|
|
"""
|
|
...
|
|
|
|
|
|
class EqualWeightAllocator(WeightAllocator):
|
|
"""等权分配器"""
|
|
|
|
def __init__(self, max_positions: int = 30):
|
|
self.max_positions = max_positions
|
|
|
|
def allocate(
|
|
self,
|
|
signals: pd.DataFrame,
|
|
prices: pd.DataFrame,
|
|
cash: float,
|
|
positions: Dict[str, int],
|
|
**kwargs,
|
|
) -> Dict[str, float]:
|
|
latest_signal = signals.iloc[-1] if len(signals) > 0 else pd.Series(dtype=float)
|
|
long_stocks = latest_signal[latest_signal > 0].index.tolist()
|
|
|
|
if not long_stocks:
|
|
return {}
|
|
|
|
# 限制持仓数
|
|
long_stocks = long_stocks[: self.max_positions]
|
|
weight = 1.0 / len(long_stocks)
|
|
return {s: weight for s in long_stocks}
|
|
|
|
|
|
class FactorWeightAllocator(WeightAllocator):
|
|
"""因子值加权分配器 — 因子越大权重越大"""
|
|
|
|
def __init__(self, max_positions: int = 30, min_weight: float = 0.005):
|
|
self.max_positions = max_positions
|
|
self.min_weight = min_weight
|
|
|
|
def allocate(
|
|
self,
|
|
signals: pd.DataFrame,
|
|
prices: pd.DataFrame,
|
|
cash: float,
|
|
positions: Dict[str, int],
|
|
factor_df: Optional[pd.DataFrame] = None,
|
|
**kwargs,
|
|
) -> Dict[str, float]:
|
|
if factor_df is None:
|
|
return EqualWeightAllocator(self.max_positions).allocate(
|
|
signals, prices, cash, positions
|
|
)
|
|
|
|
latest_signal = signals.iloc[-1] if len(signals) > 0 else pd.Series(dtype=float)
|
|
latest_factor = factor_df.iloc[-1] if len(factor_df) > 0 else pd.Series(dtype=float)
|
|
|
|
long_stocks = latest_signal[latest_signal > 0].index
|
|
# 按因子值排序
|
|
valid = latest_factor[long_stocks].dropna().sort_values(ascending=False)
|
|
selected = valid.head(self.max_positions)
|
|
if selected.empty:
|
|
return {}
|
|
|
|
raw_w = selected.values / (selected.values.sum() + 1e-12)
|
|
raw_w = np.clip(raw_w, self.min_weight, 1.0)
|
|
raw_w = raw_w / raw_w.sum()
|
|
return dict(zip(selected.index.tolist(), raw_w.tolist()))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 策略上下文
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@dataclass
|
|
class Strategy:
|
|
"""
|
|
量化策略 — 组合因子、信号生成器、权重分配器,定义一个完整策略。
|
|
"""
|
|
|
|
name: str
|
|
factors: List # 因子对象列表
|
|
signal_generator: SignalGenerator
|
|
weight_allocator: WeightAllocator
|
|
factor_weights: Optional[Dict[str, float]] = None # 因子合成权重
|
|
filter_universe: Optional[Set[str]] = None # 可选股票池
|
|
description: str = ""
|
|
|
|
def compute_composite_factor(self, factor_data: Dict[str, pd.DataFrame]) -> pd.DataFrame:
|
|
"""
|
|
合成多因子值。
|
|
factor_data: {factor_name: DataFrame(index=date, columns=stocks)}
|
|
"""
|
|
if not factor_data:
|
|
raise ValueError("factor_data 为空")
|
|
|
|
# 统一列(股票池)合并
|
|
all_stocks = sorted(
|
|
set().union(*[set(df.columns) for df in factor_data.values()])
|
|
)
|
|
all_dates = sorted(
|
|
set().union(*[set(df.index) for df in factor_data.values()])
|
|
)
|
|
|
|
composite = pd.DataFrame(0.0, index=all_dates, columns=all_stocks)
|
|
n_factors = len(factor_data)
|
|
|
|
for name, df in factor_data.items():
|
|
w = (
|
|
self.factor_weights.get(name, 1.0 / n_factors)
|
|
if self.factor_weights
|
|
else 1.0 / n_factors
|
|
)
|
|
aligned = df.reindex(index=all_dates, columns=all_stocks)
|
|
# 截面标准化
|
|
z = aligned.sub(aligned.mean(axis=1), axis=0).div(
|
|
aligned.std(axis=1) + 1e-12, axis=0
|
|
)
|
|
composite += w * z
|
|
|
|
return composite
|
|
|
|
def run_step(
|
|
self,
|
|
date: pd.Timestamp,
|
|
factor_data: Dict[str, pd.DataFrame],
|
|
prices: pd.DataFrame,
|
|
cash: float,
|
|
positions: Dict[str, int],
|
|
) -> Dict[str, float]:
|
|
"""
|
|
单步执行:生成信号 → 分配权重 → 返回目标持仓权重。
|
|
"""
|
|
composite = self.compute_composite_factor(factor_data)
|
|
# 仅取当前日期截面
|
|
if date in composite.index:
|
|
current_slice = composite.loc[[date]]
|
|
else:
|
|
current_slice = composite.iloc[-1:]
|
|
|
|
signals = self.signal_generator.generate(current_slice)
|
|
weights = self.weight_allocator.allocate(
|
|
signals, prices, cash, positions, factor_df=composite
|
|
)
|
|
return weights |