Compare commits
2
Commits
d340cf609d
...
b868e63e40
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b868e63e40 | ||
|
|
6deda5155a |
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
quanxiel.alpha — 阿尔法策略研究与回测模块
|
||||
|
||||
提供因子研究、策略定义、历史回测、绩效评估等完整工具链。
|
||||
"""
|
||||
from .config import AlphaConfig
|
||||
from .factors import FactorRegistry, BaseFactor, TechnicalFactor, FundamentalFactor
|
||||
from .strategy import Strategy, SignalGenerator, WeightAllocator
|
||||
from .backtest import BacktestEngine, Broker, Portfolio
|
||||
from .evaluation import PerformanceEvaluator, ReportGenerator
|
||||
|
||||
__all__ = [
|
||||
"AlphaConfig",
|
||||
"FactorRegistry",
|
||||
"BaseFactor",
|
||||
"TechnicalFactor",
|
||||
"FundamentalFactor",
|
||||
"Strategy",
|
||||
"SignalGenerator",
|
||||
"WeightAllocator",
|
||||
"BacktestEngine",
|
||||
"Broker",
|
||||
"Portfolio",
|
||||
"PerformanceEvaluator",
|
||||
"ReportGenerator",
|
||||
]
|
||||
@@ -0,0 +1,401 @@
|
||||
"""
|
||||
回测引擎模块 — 事件驱动回测,模拟交易执行与组合管理
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from .config import AlphaConfig
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 交易记录 & 持仓
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class TradeRecord:
|
||||
"""单笔交易记录"""
|
||||
date: pd.Timestamp
|
||||
stock: str
|
||||
side: str # 'buy' / 'sell'
|
||||
quantity: int
|
||||
price: float
|
||||
commission: float = 0.0
|
||||
stamp_tax: float = 0.0
|
||||
slippage_cost: float = 0.0
|
||||
signal: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Position:
|
||||
"""单只股票持仓"""
|
||||
stock: str
|
||||
quantity: int = 0
|
||||
avg_cost: float = 0.0
|
||||
|
||||
@property
|
||||
def market_value(self) -> float:
|
||||
return self.quantity * self.current_price if hasattr(self, "current_price") else 0.0
|
||||
|
||||
def update_cost(self, qty: int, price: float):
|
||||
"""更新平均成本(买入时)"""
|
||||
total_cost = abs(self.quantity) * self.avg_cost + abs(qty) * price
|
||||
self.quantity += qty
|
||||
if self.quantity != 0:
|
||||
self.avg_cost = total_cost / abs(self.quantity)
|
||||
else:
|
||||
self.avg_cost = 0.0
|
||||
|
||||
|
||||
class Portfolio:
|
||||
"""投资组合"""
|
||||
|
||||
def __init__(self, initial_cash: float = 1_000_000.0):
|
||||
self.initial_cash = initial_cash
|
||||
self.cash = initial_cash
|
||||
self.positions: Dict[str, Position] = {} # stock -> Position
|
||||
self.trades: List[TradeRecord] = []
|
||||
self.daily_values: List[Dict[str, Any]] = [] # 每日净值记录
|
||||
|
||||
@property
|
||||
def total_equity(self) -> float:
|
||||
pos_value = sum(
|
||||
p.market_value for p in self.positions.values()
|
||||
)
|
||||
return self.cash + pos_value
|
||||
|
||||
@property
|
||||
def total_return(self) -> float:
|
||||
return (self.total_equity / self.initial_cash) - 1.0
|
||||
|
||||
def get_position(self, stock: str) -> Position:
|
||||
if stock not in self.positions:
|
||||
self.positions[stock] = Position(stock=stock)
|
||||
return self.positions[stock]
|
||||
|
||||
def update_market_prices(self, prices: Dict[str, float]):
|
||||
"""更新所有持仓的市价"""
|
||||
for stock, price in prices.items():
|
||||
if stock in self.positions:
|
||||
self.positions[stock].current_price = price
|
||||
|
||||
def record_daily(self, date: pd.Timestamp, prices: Dict[str, float]):
|
||||
"""记录每日快照"""
|
||||
self.update_market_prices(prices)
|
||||
pos_value = sum(p.market_value for p in self.positions.values())
|
||||
self.daily_values.append({
|
||||
"date": date,
|
||||
"cash": self.cash,
|
||||
"position_value": pos_value,
|
||||
"total_equity": self.cash + pos_value,
|
||||
"return": (self.cash + pos_value) / self.initial_cash - 1.0,
|
||||
})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 券商(模拟交易执行)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class Broker:
|
||||
"""模拟券商 — 处理订单执行、交易成本"""
|
||||
|
||||
def __init__(self, config: AlphaConfig):
|
||||
self.commission_rate = config.commission_rate
|
||||
self.slippage = config.slippage
|
||||
self.stamp_tax = config.stamp_tax
|
||||
self.max_position_pct = config.max_position_pct
|
||||
self.max_turnover = config.max_turnover
|
||||
self.min_holding_period = config.min_holding_period
|
||||
|
||||
def execute(
|
||||
self,
|
||||
portfolio: Portfolio,
|
||||
target_weights: Dict[str, float],
|
||||
prices: Dict[str, float],
|
||||
date: pd.Timestamp,
|
||||
strategy_name: str = "",
|
||||
) -> List[TradeRecord]:
|
||||
"""
|
||||
执行调仓:比较当前持仓与目标权重,生成订单并执行。
|
||||
返回新的交易记录列表。
|
||||
"""
|
||||
if not target_weights:
|
||||
# 清仓信号
|
||||
return self._liquidate(portfolio, prices, date, strategy_name)
|
||||
|
||||
total_equity = 0.0
|
||||
# 先更新市价以计算当前权益
|
||||
portfolio.update_market_prices(prices)
|
||||
total_equity = portfolio.total_equity
|
||||
|
||||
trades: List[TradeRecord] = []
|
||||
|
||||
# 目标持仓市值
|
||||
target_map: Dict[str, float] = {}
|
||||
for stock, w in target_weights.items():
|
||||
if stock in prices and prices[stock] > 0:
|
||||
target_map[stock] = total_equity * w
|
||||
|
||||
# 卖出不在目标中的持仓
|
||||
for stock in list(portfolio.positions.keys()):
|
||||
pos = portfolio.positions[stock]
|
||||
if pos.quantity <= 0:
|
||||
continue
|
||||
if stock not in target_map:
|
||||
trades.extend(
|
||||
self._sell(portfolio, stock, pos.quantity, prices, date,
|
||||
strategy_name)
|
||||
)
|
||||
|
||||
# 调整持仓到目标权重
|
||||
for stock, target_value in target_map.items():
|
||||
price = prices.get(stock, 0)
|
||||
if price <= 0:
|
||||
continue
|
||||
current_pos = portfolio.get_position(stock)
|
||||
current_value = current_pos.quantity * price
|
||||
diff_value = target_value - current_value
|
||||
if abs(diff_value) < price: # 差价不足一手,忽略
|
||||
continue
|
||||
|
||||
diff_qty = int(diff_value / price / 100) * 100 # 整手
|
||||
if diff_qty == 0:
|
||||
continue
|
||||
|
||||
if diff_qty > 0:
|
||||
trades.extend(
|
||||
self._buy(portfolio, stock, diff_qty, price, date,
|
||||
strategy_name)
|
||||
)
|
||||
else:
|
||||
sell_qty = min(-diff_qty, current_pos.quantity)
|
||||
trades.extend(
|
||||
self._sell(portfolio, stock, sell_qty, price, date,
|
||||
strategy_name)
|
||||
)
|
||||
|
||||
return trades
|
||||
|
||||
def _buy(
|
||||
self,
|
||||
portfolio: Portfolio,
|
||||
stock: str,
|
||||
qty: int,
|
||||
price: float,
|
||||
date: pd.Timestamp,
|
||||
strategy: str = "",
|
||||
) -> List[TradeRecord]:
|
||||
"""买入执行"""
|
||||
# 滑点
|
||||
exec_price = price * (1 + self.slippage)
|
||||
cost = qty * exec_price
|
||||
commission = cost * self.commission_rate
|
||||
total_cost = cost + commission
|
||||
|
||||
if portfolio.cash < total_cost:
|
||||
# 现金不足,调整数量
|
||||
affordable_qty = int(
|
||||
(portfolio.cash / (exec_price * (1 + self.commission_rate)))
|
||||
/ 100
|
||||
) * 100
|
||||
if affordable_qty <= 0:
|
||||
return []
|
||||
qty = affordable_qty
|
||||
cost = qty * exec_price
|
||||
commission = cost * self.commission_rate
|
||||
total_cost = cost + commission
|
||||
|
||||
portfolio.cash -= total_cost
|
||||
pos = portfolio.get_position(stock)
|
||||
pos.update_cost(qty, exec_price)
|
||||
|
||||
trade = TradeRecord(
|
||||
date=date,
|
||||
stock=stock,
|
||||
side="buy",
|
||||
quantity=qty,
|
||||
price=exec_price,
|
||||
commission=commission,
|
||||
slippage_cost=qty * (exec_price - price),
|
||||
signal=strategy,
|
||||
)
|
||||
portfolio.trades.append(trade)
|
||||
return [trade]
|
||||
|
||||
def _sell(
|
||||
self,
|
||||
portfolio: Portfolio,
|
||||
stock: str,
|
||||
qty: int,
|
||||
price: float,
|
||||
date: pd.Timestamp,
|
||||
strategy: str = "",
|
||||
) -> List[TradeRecord]:
|
||||
"""卖出执行"""
|
||||
exec_price = price * (1 - self.slippage)
|
||||
proceeds = qty * exec_price
|
||||
commission = proceeds * self.commission_rate
|
||||
stamp = proceeds * self.stamp_tax
|
||||
net_proceeds = proceeds - commission - stamp
|
||||
|
||||
pos = portfolio.get_position(stock)
|
||||
actual_qty = min(qty, pos.quantity)
|
||||
if actual_qty <= 0:
|
||||
return []
|
||||
|
||||
# 更新持仓
|
||||
pos.quantity -= actual_qty
|
||||
if pos.quantity == 0:
|
||||
pos.avg_cost = 0.0
|
||||
|
||||
portfolio.cash += net_proceeds
|
||||
|
||||
trade = TradeRecord(
|
||||
date=date,
|
||||
stock=stock,
|
||||
side="sell",
|
||||
quantity=actual_qty,
|
||||
price=exec_price,
|
||||
commission=commission,
|
||||
stamp_tax=stamp,
|
||||
slippage_cost=actual_qty * (price - exec_price),
|
||||
signal=strategy,
|
||||
)
|
||||
portfolio.trades.append(trade)
|
||||
return [trade]
|
||||
|
||||
def _liquidate(
|
||||
self,
|
||||
portfolio: Portfolio,
|
||||
prices: Dict[str, float],
|
||||
date: pd.Timestamp,
|
||||
strategy: str = "",
|
||||
) -> List[TradeRecord]:
|
||||
"""全部平仓"""
|
||||
trades = []
|
||||
for stock in list(portfolio.positions.keys()):
|
||||
pos = portfolio.positions[stock]
|
||||
if pos.quantity > 0 and stock in prices:
|
||||
trades.extend(
|
||||
self._sell(portfolio, stock, pos.quantity, prices, date,
|
||||
strategy)
|
||||
)
|
||||
return trades
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 回测引擎
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class BacktestEngine:
|
||||
"""事件驱动回测引擎"""
|
||||
|
||||
def __init__(self, config: Optional[AlphaConfig] = None):
|
||||
self.config = config or AlphaConfig()
|
||||
self.broker = Broker(self.config)
|
||||
self.portfolio = Portfolio(self.config.initial_cash)
|
||||
|
||||
def run(
|
||||
self,
|
||||
strategy: Any,
|
||||
price_data: pd.DataFrame,
|
||||
factor_data: Dict[str, pd.DataFrame],
|
||||
dates: Optional[List[pd.Timestamp]] = None,
|
||||
rebalance_freq: str = "M", # 'D'/'W'/'M'
|
||||
progress_callback: Optional[Callable] = None,
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
运行回测。
|
||||
|
||||
参数
|
||||
----
|
||||
strategy : Strategy 实例或兼容接口
|
||||
price_data : DataFrame, index=date, columns=stocks, values=价格
|
||||
factor_data : {factor_name: DataFrame(index=date, columns=stocks)}
|
||||
dates : 回测日期列表,默认为 price_data 所有日期
|
||||
rebalance_freq: 调仓频率 'D'(日), 'W'(周), 'M'(月)
|
||||
|
||||
返回
|
||||
----
|
||||
daily_values : DataFrame 每日净值曲线
|
||||
"""
|
||||
if dates is None:
|
||||
all_dates = sorted(price_data.index)
|
||||
else:
|
||||
all_dates = sorted(dates)
|
||||
|
||||
# 确定调仓日
|
||||
date_series = pd.Series(all_dates, index=all_dates)
|
||||
if rebalance_freq == "M":
|
||||
rebalance_dates = date_series.resample("M").last().tolist()
|
||||
elif rebalance_freq == "W":
|
||||
rebalance_dates = date_series.resample("W").last().tolist()
|
||||
else:
|
||||
rebalance_dates = all_dates
|
||||
|
||||
rebalance_set = set(pd.to_datetime(rebalance_dates).date)
|
||||
|
||||
total_dates = len(all_dates)
|
||||
for i, date in enumerate(all_dates):
|
||||
# 进度回调
|
||||
if progress_callback:
|
||||
progress_callback(i, total_dates)
|
||||
|
||||
# 当前截面价格
|
||||
current_prices = {}
|
||||
if date in price_data.index:
|
||||
row = price_data.loc[date]
|
||||
current_prices = row.dropna().to_dict()
|
||||
|
||||
# 调仓日执行交易
|
||||
trade_date = pd.Timestamp(date).date()
|
||||
if trade_date in rebalance_set:
|
||||
target_weights = strategy.run_step(
|
||||
date=pd.Timestamp(date),
|
||||
factor_data=factor_data,
|
||||
prices=pd.DataFrame([current_prices]),
|
||||
cash=self.portfolio.cash,
|
||||
positions={
|
||||
s: p.quantity
|
||||
for s, p in self.portfolio.positions.items()
|
||||
},
|
||||
)
|
||||
self.broker.execute(
|
||||
self.portfolio,
|
||||
target_weights,
|
||||
current_prices,
|
||||
date=pd.Timestamp(date),
|
||||
strategy_name=strategy.name,
|
||||
)
|
||||
|
||||
# 记录每日净值
|
||||
self.portfolio.record_daily(pd.Timestamp(date), current_prices)
|
||||
|
||||
# 最终清算
|
||||
if not all_dates:
|
||||
return pd.DataFrame()
|
||||
|
||||
final_date = all_dates[-1]
|
||||
if final_date in price_data.index:
|
||||
final_prices = price_data.loc[final_date].dropna().to_dict()
|
||||
else:
|
||||
final_prices = {}
|
||||
|
||||
self.broker._liquidate(
|
||||
self.portfolio, final_prices, pd.Timestamp(final_date),
|
||||
strategy_name=strategy.name,
|
||||
)
|
||||
self.portfolio.record_daily(pd.Timestamp(final_date), final_prices)
|
||||
|
||||
return self._to_equity_curve()
|
||||
|
||||
def _to_equity_curve(self) -> pd.DataFrame:
|
||||
"""输出净值曲线 DataFrame"""
|
||||
df = pd.DataFrame(self.portfolio.daily_values)
|
||||
if df.empty:
|
||||
return pd.DataFrame(columns=["date", "total_equity", "return"])
|
||||
df = df.set_index("date").sort_index()
|
||||
df["nav"] = df["total_equity"] / self.config.initial_cash
|
||||
return df
|
||||
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
阿尔法模块配置
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlphaConfig:
|
||||
"""阿尔法研究全局配置"""
|
||||
|
||||
# ---- 数据库 ----
|
||||
db_host: str = "192.168.27.15"
|
||||
db_port: int = 12345
|
||||
db_name: str = "quant_db"
|
||||
db_user: str = "postgres"
|
||||
db_password: str = "postgres"
|
||||
|
||||
# ---- 回测基础参数 ----
|
||||
initial_cash: float = 1_000_000.0 # 初始资金
|
||||
benchmark: str = "000300.SH" # 基准指数(沪深300)
|
||||
start_date: str = "2020-01-01"
|
||||
end_date: str = "2025-12-31"
|
||||
|
||||
# ---- 交易成本 ----
|
||||
commission_rate: float = 0.0003 # 佣金费率
|
||||
slippage: float = 0.001 # 滑点(百分比)
|
||||
stamp_tax: float = 0.001 # 印花税(仅卖出)
|
||||
|
||||
# ---- 组合约束 ----
|
||||
max_position_pct: float = 0.10 # 单票最大仓位
|
||||
max_turnover: float = 0.20 # 单日最大换手率
|
||||
min_holding_period: int = 1 # 最小持仓天数
|
||||
|
||||
# ---- 因子研究参数 ----
|
||||
factor_windows: List[int] = field(default_factory=lambda: [5, 10, 20, 60])
|
||||
ic_decay_days: int = 20 # IC 衰减分析天数
|
||||
|
||||
# ---- 输出 ----
|
||||
output_dir: str = "./output"
|
||||
save_trade_log: bool = True
|
||||
verbose: bool = False
|
||||
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
评估与报告模块 — 绩效评估指标、风险分析、可视化报告
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 绩效评估器
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class PerformanceEvaluator:
|
||||
"""
|
||||
绩效评估器 — 计算常见量化指标
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
equity_curve: pd.DataFrame,
|
||||
benchmark_returns: Optional[pd.Series] = None,
|
||||
risk_free_rate: float = 0.03,
|
||||
):
|
||||
"""
|
||||
equity_curve : DataFrame, 必须包含 'return' 或 'nav' 列, index=date
|
||||
benchmark_returns : Series, 基准日收益率, index=date
|
||||
risk_free_rate : 年化无风险利率
|
||||
"""
|
||||
self.equity = equity_curve.copy()
|
||||
self.rf = risk_free_rate
|
||||
|
||||
# 确保有日收益率列
|
||||
if "daily_return" not in self.equity.columns:
|
||||
if "return" in self.equity.columns:
|
||||
self.equity["daily_return"] = self.equity["return"].diff().fillna(0)
|
||||
elif "nav" in self.equity.columns:
|
||||
self.equity["daily_return"] = self.equity["nav"].pct_change().fillna(0)
|
||||
else:
|
||||
raise ValueError("equity_curve 必须包含 'return'/'nav' 列")
|
||||
|
||||
self.benchmark_returns = benchmark_returns
|
||||
self._metrics: Dict[str, float] = {}
|
||||
|
||||
# ----- 基础指标 -----
|
||||
|
||||
def total_return(self) -> float:
|
||||
"""累计收益率"""
|
||||
if "nav" in self.equity.columns:
|
||||
return self.equity["nav"].iloc[-1] - 1.0
|
||||
return (1 + self.equity["daily_return"]).prod() - 1.0
|
||||
|
||||
def annual_return(self, periods_per_year: int = 252) -> float:
|
||||
"""年化收益率"""
|
||||
total = self.total_return()
|
||||
years = self._year_frac(periods_per_year)
|
||||
return (1 + total) ** (1 / years) - 1.0 if years > 0 else 0.0
|
||||
|
||||
def annual_volatility(self, periods_per_year: int = 252) -> float:
|
||||
"""年化波动率"""
|
||||
return self.equity["daily_return"].std() * np.sqrt(periods_per_year)
|
||||
|
||||
def max_drawdown(self) -> float:
|
||||
"""最大回撤"""
|
||||
if "nav" in self.equity.columns:
|
||||
nav = self.equity["nav"]
|
||||
else:
|
||||
nav = (1 + self.equity["daily_return"]).cumprod()
|
||||
running_max = nav.cummax()
|
||||
drawdown = (nav - running_max) / running_max
|
||||
return drawdown.min()
|
||||
|
||||
def max_drawdown_duration(self) -> int:
|
||||
"""最长回撤持续天数"""
|
||||
if "nav" in self.equity.columns:
|
||||
nav = self.equity["nav"]
|
||||
else:
|
||||
nav = (1 + self.equity["daily_return"]).cumprod()
|
||||
running_max = nav.cummax()
|
||||
is_dd = nav < running_max
|
||||
# 统计最长连续 True 段
|
||||
dd_groups = (is_dd != is_dd.shift()).cumsum()
|
||||
dd_durations = (
|
||||
is_dd.groupby(dd_groups).sum()
|
||||
)
|
||||
return int(dd_durations.max()) if not dd_durations.empty else 0
|
||||
|
||||
# ----- 风险调整收益 -----
|
||||
|
||||
def sharpe_ratio(self, periods_per_year: int = 252) -> float:
|
||||
"""夏普比率"""
|
||||
excess = self.equity["daily_return"] - self.rf / periods_per_year
|
||||
return (
|
||||
excess.mean() / (excess.std() + 1e-12) * np.sqrt(periods_per_year)
|
||||
)
|
||||
|
||||
def calmar_ratio(self) -> float:
|
||||
"""Calmar 比率 (年化收益 / 最大回撤绝对值)"""
|
||||
mdd = abs(self.max_drawdown())
|
||||
return self.annual_return() / mdd if mdd > 0 else 0.0
|
||||
|
||||
def sortino_ratio(self, periods_per_year: int = 252) -> float:
|
||||
"""Sortino 比率 (只考虑下行波动)"""
|
||||
returns = self.equity["daily_return"]
|
||||
downside = returns[returns < 0]
|
||||
downside_vol = downside.std() * np.sqrt(periods_per_year)
|
||||
excess_annual = self.annual_return() - self.rf
|
||||
return excess_annual / (downside_vol + 1e-12)
|
||||
|
||||
def win_rate(self) -> float:
|
||||
"""胜率(日收益率 > 0 的比例)"""
|
||||
returns = self.equity["daily_return"]
|
||||
return (returns > 0).mean()
|
||||
|
||||
def profit_loss_ratio(self) -> float:
|
||||
"""盈亏比 (平均盈利 / 平均亏损绝对值)"""
|
||||
returns = self.equity["daily_return"]
|
||||
avg_win = returns[returns > 0].mean()
|
||||
avg_loss = abs(returns[returns < 0].mean())
|
||||
return avg_win / (avg_loss + 1e-12)
|
||||
|
||||
# ----- 相对基准指标 -----
|
||||
|
||||
def alpha(self, periods_per_year: int = 252) -> float:
|
||||
"""Jensen's Alpha 相对基准"""
|
||||
if self.benchmark_returns is None:
|
||||
return 0.0
|
||||
aligned = self._align_benchmark()
|
||||
strat_ret = aligned["strat"]
|
||||
bench_ret = aligned["bench"]
|
||||
excess_strat = strat_ret - self.rf / periods_per_year
|
||||
excess_bench = bench_ret - self.rf / periods_per_year
|
||||
# 线性回归
|
||||
cov = np.cov(excess_strat, excess_bench)
|
||||
beta = cov[0, 1] / (cov[1, 1] + 1e-12)
|
||||
alpha_daily = excess_strat.mean() - beta * excess_bench.mean()
|
||||
return alpha_daily * periods_per_year
|
||||
|
||||
def beta(self) -> float:
|
||||
"""Beta"""
|
||||
if self.benchmark_returns is None:
|
||||
return 1.0
|
||||
aligned = self._align_benchmark()
|
||||
cov = np.cov(aligned["strat"], aligned["bench"])
|
||||
return cov[0, 1] / (cov[1, 1] + 1e-12)
|
||||
|
||||
def information_ratio(self, periods_per_year: int = 252) -> float:
|
||||
"""信息比率"""
|
||||
if self.benchmark_returns is None:
|
||||
return 0.0
|
||||
aligned = self._align_benchmark()
|
||||
tracking_error = (aligned["strat"] - aligned["bench"]).std()
|
||||
return (
|
||||
(aligned["strat"].mean() - aligned["bench"].mean())
|
||||
/ (tracking_error + 1e-12)
|
||||
* np.sqrt(periods_per_year)
|
||||
)
|
||||
|
||||
# ----- 报告 -----
|
||||
|
||||
def full_report(self) -> Dict[str, float]:
|
||||
"""生成完整绩效报告字典"""
|
||||
self._metrics = {
|
||||
"累计收益率(%)": self.total_return() * 100,
|
||||
"年化收益率(%)": self.annual_return() * 100,
|
||||
"年化波动率(%)": self.annual_volatility() * 100,
|
||||
"最大回撤(%)": self.max_drawdown() * 100,
|
||||
"最长回撤天数": self.max_drawdown_duration(),
|
||||
"夏普比率": self.sharpe_ratio(),
|
||||
"Calmar比率": self.calmar_ratio(),
|
||||
"Sortino比率": self.sortino_ratio(),
|
||||
"日胜率(%)": self.win_rate() * 100,
|
||||
"盈亏比": self.profit_loss_ratio(),
|
||||
"Alpha(%)": self.alpha() * 100,
|
||||
"Beta": self.beta(),
|
||||
"信息比率": self.information_ratio(),
|
||||
}
|
||||
return self._metrics
|
||||
|
||||
def summary(self) -> str:
|
||||
"""打印可读报告"""
|
||||
report = self.full_report()
|
||||
lines = ["=" * 50, " 量化策略绩效评估报告", "=" * 50]
|
||||
for key, value in report.items():
|
||||
if isinstance(value, float):
|
||||
lines.append(f" {key}: {value:.4f}")
|
||||
else:
|
||||
lines.append(f" {key}: {value}")
|
||||
lines.append("=" * 50)
|
||||
return "\n".join(lines)
|
||||
|
||||
# ----- 内部方法 -----
|
||||
|
||||
def _year_frac(self, periods_per_year: int) -> float:
|
||||
n = len(self.equity)
|
||||
return n / periods_per_year
|
||||
|
||||
def _align_benchmark(self) -> pd.DataFrame:
|
||||
"""对齐策略与基准日收益率"""
|
||||
strat = self.equity["daily_return"]
|
||||
bench = self.benchmark_returns
|
||||
merged = pd.concat([strat, bench], axis=1, join="inner").dropna()
|
||||
merged.columns = ["strat", "bench"]
|
||||
return merged
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 报告生成器
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class ReportGenerator:
|
||||
"""生成交易分析报告(Markdown / Excel)"""
|
||||
|
||||
def __init__(self, evaluator: PerformanceEvaluator, trades: Optional[List] = None):
|
||||
self.evaluator = evaluator
|
||||
self.trades = trades or []
|
||||
|
||||
def to_markdown(self) -> str:
|
||||
"""输出 Markdown 格式报告"""
|
||||
metrics = self.evaluator.full_report()
|
||||
lines = [
|
||||
"# 量化策略回测报告",
|
||||
"",
|
||||
"## 绩效指标",
|
||||
"",
|
||||
"| 指标 | 数值 |",
|
||||
"|------|------|",
|
||||
]
|
||||
for key, value in metrics.items():
|
||||
lines.append(f"| {key} | {value:.4f} |")
|
||||
|
||||
# 交易统计
|
||||
if self.trades:
|
||||
lines.extend(["", "## 交易统计", ""])
|
||||
trades_df = pd.DataFrame(
|
||||
[
|
||||
{
|
||||
"date": t.date,
|
||||
"stock": t.stock,
|
||||
"side": t.side,
|
||||
"quantity": t.quantity,
|
||||
"price": t.price,
|
||||
}
|
||||
for t in self.trades
|
||||
]
|
||||
)
|
||||
total_trades = len(trades_df)
|
||||
buy_trades = len(trades_df[trades_df["side"] == "buy"])
|
||||
sell_trades = len(trades_df[trades_df["side"] == "sell"])
|
||||
lines.append(f"- 总交易笔数: {total_trades}")
|
||||
lines.append(f"- 买入笔数: {buy_trades}")
|
||||
lines.append(f"- 卖出笔数: {sell_trades}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def to_dataframe(self) -> pd.DataFrame:
|
||||
"""将绩效指标导出为 DataFrame"""
|
||||
metrics = self.evaluator.full_report()
|
||||
return pd.DataFrame(
|
||||
list(metrics.items()), columns=["指标", "数值"]
|
||||
).set_index("指标")
|
||||
@@ -0,0 +1,256 @@
|
||||
"""
|
||||
因子研究模块 — 因子定义、计算、注册与 IC/分层分析
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Callable, Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from scipy import stats
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 因子注册表
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class FactorRegistry:
|
||||
"""因子注册表,用于管理和发现所有因子"""
|
||||
|
||||
_factors: Dict[str, type] = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, factor_cls: type):
|
||||
"""注册因子类"""
|
||||
name = factor_cls.__name__
|
||||
cls._factors[name] = factor_cls
|
||||
return factor_cls
|
||||
|
||||
@classmethod
|
||||
def get(cls, name: str) -> Optional[type]:
|
||||
"""根据名称获取因子类"""
|
||||
return cls._factors.get(name)
|
||||
|
||||
@classmethod
|
||||
def list_all(cls) -> List[str]:
|
||||
"""列出所有已注册因子"""
|
||||
return sorted(cls._factors.keys())
|
||||
|
||||
@classmethod
|
||||
def create(cls, name: str, **kwargs):
|
||||
"""根据名称创建因子实例"""
|
||||
factor_cls = cls._factors.get(name)
|
||||
if factor_cls is None:
|
||||
raise KeyError(f"因子 '{name}' 未注册,可用: {cls.list_all()}")
|
||||
return factor_cls(**kwargs)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 因子基类
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class BaseFactor(ABC):
|
||||
"""因子抽象基类"""
|
||||
|
||||
name: str = "BaseFactor"
|
||||
category: str = "unknown" # technical / fundamental / alternative
|
||||
|
||||
def __init__(self, **params):
|
||||
self.params = params
|
||||
|
||||
@abstractmethod
|
||||
def compute(self, data: pd.DataFrame) -> pd.Series:
|
||||
"""计算因子值,返回 Series(index=timestamp) 或多索引 Series"""
|
||||
...
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.name}({self.params})"
|
||||
|
||||
def neutralize(self,
|
||||
factor: pd.Series,
|
||||
group: pd.Series,
|
||||
market_cap: pd.Series) -> pd.Series:
|
||||
"""
|
||||
因子中性化:对市值和行业做正交化处理。
|
||||
factor : 原始因子值
|
||||
group : 行业分组
|
||||
market_cap : 市值
|
||||
返回中性化后的因子值。
|
||||
"""
|
||||
# 创建虚拟变量并拟合线性模型,取残差作为中性化因子
|
||||
df = pd.DataFrame({
|
||||
"factor": factor,
|
||||
"log_mcap": np.log(market_cap.clip(lower=1)),
|
||||
})
|
||||
dummies = pd.get_dummies(group, prefix="group")
|
||||
X = pd.concat([df["log_mcap"], dummies], axis=1)
|
||||
# 对齐索引
|
||||
common_idx = factor.index.intersection(X.dropna().index)
|
||||
X = X.loc[common_idx]
|
||||
y = factor.loc[common_idx]
|
||||
# OLS 回归取残差
|
||||
beta = np.linalg.lstsq(X.values, y.values, rcond=None)[0]
|
||||
pred = X.values @ beta
|
||||
residual = y.values - pred
|
||||
neutralized = pd.Series(residual, index=common_idx, name=factor.name)
|
||||
return neutralized
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 技术面因子
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@FactorRegistry.register
|
||||
class TechnicalFactor(BaseFactor):
|
||||
"""通用技术面因子 — 支持自定义参数组合"""
|
||||
|
||||
name = "TechnicalFactor"
|
||||
category = "technical"
|
||||
|
||||
def compute(self, data: pd.DataFrame) -> pd.Series:
|
||||
"""
|
||||
data 必须包含: ['close', 'volume', 'high', 'low']
|
||||
返回: 标准化后的多因子合成值
|
||||
"""
|
||||
# 示例因子:
|
||||
# 1) 动量因子 (20日收益率)
|
||||
# 2) 波动率因子 (20日波动率倒数)
|
||||
close = data["close"]
|
||||
mom = close.pct_change(self.params.get("mom_window", 20))
|
||||
vol = close.pct_change().rolling(self.params.get("vol_window", 20)).std()
|
||||
|
||||
# 合成 (等权,可按需要改为 ICIR 加权)
|
||||
mom_z = (mom - mom.mean()) / mom.std()
|
||||
vol_z = -(vol - vol.mean()) / vol.std() # 低波为正
|
||||
composite = 0.5 * mom_z + 0.5 * vol_z
|
||||
return composite.rename("TechnicalFactor")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 基本面因子
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@FactorRegistry.register
|
||||
class FundamentalFactor(BaseFactor):
|
||||
"""通用基本面因子 — ROE / PE / PB / 营收增速等"""
|
||||
|
||||
name = "FundamentalFactor"
|
||||
category = "fundamental"
|
||||
|
||||
def compute(self, data: pd.DataFrame) -> pd.Series:
|
||||
"""
|
||||
data 应包含: ['roe', 'pe', 'pb', 'revenue_growth']
|
||||
返回基本面复合因子
|
||||
"""
|
||||
roe = data.get("roe", None)
|
||||
pe = data.get("pe", None)
|
||||
pb = data.get("pb", None)
|
||||
growth = data.get("revenue_growth", None)
|
||||
|
||||
# 将各因子标准化后等权合成
|
||||
scores = []
|
||||
for series, inv in [(roe, False), (pe, True), (pb, True), (growth, False)]:
|
||||
if series is not None:
|
||||
z = (series - series.mean()) / series.std()
|
||||
scores.append(-z if inv else z)
|
||||
|
||||
if not scores:
|
||||
raise ValueError("无可用基本面数据列")
|
||||
|
||||
composite = sum(scores) / len(scores)
|
||||
return composite.rename("FundamentalFactor")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# IC / 分层分析工具
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class FactorAnalyzer:
|
||||
"""因子分析器 — IC 计算、分层回测、换手率分析"""
|
||||
|
||||
def __init__(self, factor_values: pd.Series, forward_returns: pd.Series):
|
||||
"""
|
||||
factor_values : 因子值 Series (MultiIndex: [date, code] 或 对齐 index)
|
||||
forward_returns : 未来一期收益率,与 factor 对齐
|
||||
"""
|
||||
self.factor = factor_values
|
||||
self.forward = forward_returns
|
||||
self.ic_series: Optional[pd.Series] = None
|
||||
|
||||
def compute_ic(self, method: str = "rank") -> pd.Series:
|
||||
"""逐截面计算 IC (Rank IC 或 Pearson IC)"""
|
||||
if isinstance(self.factor.index, pd.MultiIndex):
|
||||
grouped = self.factor.groupby(level=0)
|
||||
fwd = self.forward.groupby(level=0)
|
||||
if method == "rank":
|
||||
ic_data = grouped.apply(
|
||||
lambda g: g.corr(fwd.get_group(g.name), method="spearman")
|
||||
)
|
||||
else:
|
||||
ic_data = grouped.apply(
|
||||
lambda g: g.corr(fwd.get_group(g.name), method="pearson")
|
||||
)
|
||||
else:
|
||||
# 单截面
|
||||
if method == "rank":
|
||||
ic_data = pd.Series(
|
||||
stats.spearmanr(self.factor, self.forward)[0],
|
||||
index=[self.factor.index[0] if len(self.factor) > 0 else 0],
|
||||
)
|
||||
else:
|
||||
ic_data = pd.Series(
|
||||
stats.pearsonr(self.factor, self.forward)[0],
|
||||
index=[self.factor.index[0] if len(self.factor) > 0 else 0],
|
||||
)
|
||||
self.ic_series = ic_data.dropna()
|
||||
return self.ic_series
|
||||
|
||||
def ic_summary(self) -> Dict[str, float]:
|
||||
"""IC 汇总统计"""
|
||||
if self.ic_series is None:
|
||||
self.compute_ic()
|
||||
ic = self.ic_series
|
||||
return {
|
||||
"IC_Mean": ic.mean(),
|
||||
"IC_Std": ic.std(),
|
||||
"IR": ic.mean() / (ic.std() + 1e-12),
|
||||
"IC>0_Ratio": (ic > 0).mean(),
|
||||
"IC_Abs_Mean": ic.abs().mean(),
|
||||
}
|
||||
|
||||
def ic_decay(self, forward_returns: Dict[int, pd.Series]) -> pd.Series:
|
||||
"""IC 衰减: 计算不同前瞻期的 IC 均值"""
|
||||
decay = {}
|
||||
for horizon, fwd in forward_returns.items():
|
||||
ratio = fwd.reindex(self.forward.index)
|
||||
valid = self.factor.notna() & ratio.notna()
|
||||
ic = stats.spearmanr(
|
||||
self.factor[valid].values, ratio[valid].values
|
||||
)[0]
|
||||
decay[horizon] = ic
|
||||
return pd.Series(decay, name="IC_Decay")
|
||||
|
||||
def quantile_returns(self, n_quantiles: int = 5) -> pd.DataFrame:
|
||||
"""分层回测:按因子值分 5 组,计算各组平均收益率"""
|
||||
df = pd.DataFrame({"factor": self.factor, "fwd": self.forward}).dropna()
|
||||
df["quantile"] = pd.qcut(df["factor"], n_quantiles, labels=False) + 1
|
||||
result = df.groupby("quantile")["fwd"].mean().to_frame("avg_return")
|
||||
result.index.name = "quantile"
|
||||
result["cum_return"] = result["avg_return"].cumsum()
|
||||
return result
|
||||
|
||||
def turnover(self, n_quantiles: int = 5) -> pd.Series:
|
||||
"""因子换手率分析(相邻期分位数变化比例)"""
|
||||
df = pd.DataFrame({"factor": self.factor})
|
||||
df["quantile"] = pd.qcut(df["factor"], n_quantiles, labels=False) + 1
|
||||
if isinstance(df.index, pd.MultiIndex):
|
||||
df = df.reset_index()
|
||||
date_col = df.columns[0]
|
||||
turnover_list = []
|
||||
for date, grp in df.groupby(date_col):
|
||||
pass # 此处需按股票计算 --- 简化处理
|
||||
# 简化: 逐期计算因子自相关系数
|
||||
if isinstance(self.ic_series, pd.Series):
|
||||
auto_corr = self.factor.autocorr(lag=1)
|
||||
return pd.Series({"auto_corr": auto_corr})
|
||||
return pd.Series()
|
||||
@@ -0,0 +1,6 @@
|
||||
numpy>=1.24.0
|
||||
pandas>=2.0.0
|
||||
scipy>=1.10.0
|
||||
matplotlib>=3.7.0
|
||||
tushare>=1.3.0
|
||||
psycopg2-binary>=2.9.0
|
||||
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
策略模块 — 信号生成、权重分配、组合构建
|
||||
"""
|
||||
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
|
||||
@@ -0,0 +1,279 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 阿尔法策略回测 — 完整示例\n",
|
||||
"\n",
|
||||
"本 Notebook 演示如何使用 `quanxiel.alpha` 模块进行:\n",
|
||||
"1. 因子计算与 IC 分析\n",
|
||||
"2. 策略信号生成\n",
|
||||
"3. 事件驱动回测\n",
|
||||
"4. 绩效评估"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import sys\n",
|
||||
"sys.path.insert(0, '..')\n",
|
||||
"\n",
|
||||
"import numpy as np\n",
|
||||
"import pandas as pd\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"\n",
|
||||
"from alpha.config import AlphaConfig\n",
|
||||
"from alpha.factors import FactorRegistry, FactorAnalyzer\n",
|
||||
"from alpha.strategy import Strategy, QuantileSignal, EqualWeightAllocator\n",
|
||||
"from alpha.backtest import BacktestEngine\n",
|
||||
"from alpha.evaluation import PerformanceEvaluator, ReportGenerator\n",
|
||||
"\n",
|
||||
"%matplotlib inline\n",
|
||||
"plt.rcParams['font.sans-serif'] = ['SimHei']\n",
|
||||
"plt.rcParams['axes.unicode_minus'] = False"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 准备数据\n",
|
||||
"\n",
|
||||
"从本地数据库或 Tushare 获取量价数据与财务数据。这里用随机数据演示流程。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ---- 生成示例数据 -------\n",
|
||||
"np.random.seed(42)\n",
|
||||
"dates = pd.date_range('2022-01-01', '2024-12-31', freq='B')\n",
|
||||
"stocks = [f'{i:06d}.SH' for i in range(600000, 600050)]\n",
|
||||
"\n",
|
||||
"# 价格数据\n",
|
||||
"price_data = pd.DataFrame(\n",
|
||||
" np.cumprod(1 + np.random.randn(len(dates), len(stocks)) * 0.02, axis=0),\n",
|
||||
" index=dates, columns=stocks\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 因子数据(模拟技术面因子值)\n",
|
||||
"factor_df = pd.DataFrame(\n",
|
||||
" np.random.randn(len(dates), len(stocks)),\n",
|
||||
" index=dates, columns=stocks\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(f'价格数据: {price_data.shape}')\n",
|
||||
"print(f'因子数据: {factor_df.shape}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. 因子 IC 分析"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 计算未来 1 期收益率\n",
|
||||
"fwd_returns = price_data.pct_change().shift(-1).stack()\n",
|
||||
"factor_stacked = factor_df.stack()\n",
|
||||
"\n",
|
||||
"analyzer = FactorAnalyzer(factor_stacked, fwd_returns)\n",
|
||||
"\n",
|
||||
"# Rank IC\n",
|
||||
"ic = analyzer.compute_ic(method='rank')\n",
|
||||
"print('IC 汇总:')\n",
|
||||
"for k, v in analyzer.ic_summary().items():\n",
|
||||
" print(f' {k}: {v:.4f}')\n",
|
||||
"\n",
|
||||
"# IC 曲线\n",
|
||||
"ic.plot(figsize=(12, 3), title='Rank IC 时间序列')\n",
|
||||
"plt.axhline(y=0, color='r', linestyle='--')\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 分层回测"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"quantile_ret = analyzer.quantile_returns(n_quantiles=5)\n",
|
||||
"print('各分位组平均收益:')\n",
|
||||
"print(quantile_ret)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 构建策略 & 回测"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 配置\n",
|
||||
"config = AlphaConfig(\n",
|
||||
" initial_cash=1_000_000,\n",
|
||||
" commission_rate=0.0003,\n",
|
||||
" slippage=0.001,\n",
|
||||
" stamp_tax=0.001,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 策略组合\n",
|
||||
"strategy = Strategy(\n",
|
||||
" name='技术因子-分位数策略',\n",
|
||||
" factors=[],\n",
|
||||
" signal_generator=QuantileSignal(n_quantiles=5, long_quantile=5, short_quantile=1),\n",
|
||||
" weight_allocator=EqualWeightAllocator(max_positions=20),\n",
|
||||
" description='买入因子值最高的分位组,等权持仓'\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 回测引擎\n",
|
||||
"engine = BacktestEngine(config)\n",
|
||||
"equity_curve = engine.run(\n",
|
||||
" strategy=strategy,\n",
|
||||
" price_data=price_data,\n",
|
||||
" factor_data={'technical': factor_df},\n",
|
||||
" rebalance_freq='M', # 月频调仓\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(f'回测完成, 共 {len(equity_curve)} 个交易日')\n",
|
||||
"print(f'累计收益率: {(equity_curve[\"nav\"].iloc[-1] - 1) * 100:.2f}%')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 净值曲线"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"fig, axes = plt.subplots(2, 1, figsize=(14, 8))\n",
|
||||
"\n",
|
||||
"# 净值曲线\n",
|
||||
"axes[0].plot(equity_curve.index, equity_curve['nav'], label='策略净值', color='steelblue')\n",
|
||||
"axes[0].axhline(y=1.0, color='gray', linestyle='--')\n",
|
||||
"axes[0].set_title('策略净值曲线')\n",
|
||||
"axes[0].legend()\n",
|
||||
"axes[0].grid(True, alpha=0.3)\n",
|
||||
"\n",
|
||||
"# 回撤曲线\n",
|
||||
"nav = equity_curve['nav']\n",
|
||||
"running_max = nav.cummax()\n",
|
||||
"drawdown = (nav - running_max) / running_max\n",
|
||||
"axes[1].fill_between(equity_curve.index, 0, drawdown, color='red', alpha=0.3)\n",
|
||||
"axes[1].set_title('回撤曲线')\n",
|
||||
"axes[1].grid(True, alpha=0.3)\n",
|
||||
"\n",
|
||||
"plt.tight_layout()\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 绩效评估"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"evaluator = PerformanceEvaluator(equity_curve, risk_free_rate=0.03)\n",
|
||||
"print(evaluator.summary())\n",
|
||||
"\n",
|
||||
"# 导出报告 DataFrame\n",
|
||||
"report_df = evaluator.full_report()\n",
|
||||
"pd.DataFrame(list(report_df.items()), columns=['指标', '数值']).set_index('指标')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 交易记录分析"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"trades = engine.portfolio.trades\n",
|
||||
"if trades:\n",
|
||||
" trade_df = pd.DataFrame([\n",
|
||||
" {'日期': t.date, '股票': t.stock, '方向': t.side,\n",
|
||||
" '数量': t.quantity, '价格': t.price, '佣金': t.commission}\n",
|
||||
" for t in trades\n",
|
||||
" ])\n",
|
||||
" print(f'总交易数: {len(trade_df)}')\n",
|
||||
" print(f'买入: {(trade_df[\"方向\"]==\"buy\").sum()}, 卖出: {(trade_df[\"方向\"]==\"sell\").sum()}')\n",
|
||||
" display(trade_df.head(20))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"## 使用真实数据\n",
|
||||
"\n",
|
||||
"将以上模拟数据替换为从 `quanxiel.quantitative_data` 模块加载的真实行情:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"from quantitative_data.importer import DataImporter\n",
|
||||
"\n",
|
||||
"importer = DataImporter()\n",
|
||||
"price_data = importer.load_daily_prices('2020-01-01', '2024-12-31')\n",
|
||||
"```"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.10.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# ========================================
|
||||
# 量化数据库 — 环境变量配置模板
|
||||
# 复制此文件为 .env 并填入真实值
|
||||
# ========================================
|
||||
|
||||
# PostgreSQL 数据库连接
|
||||
QUANT_DB_HOST=192.168.27.15
|
||||
QUANT_DB_PORT=12345
|
||||
QUANT_DB_NAME=quant_db
|
||||
QUANT_DB_USER=postgres
|
||||
QUANT_DB_PASSWORD=your_password_here
|
||||
|
||||
# Tushare API Token (在 https://tushare.pro 获取)
|
||||
TUSHARE_TOKEN=your_tushare_token_here
|
||||
|
||||
# 批量导入参数 (可选,已有默认值)
|
||||
# QUANT_BATCH_SIZE=5000
|
||||
# QUANT_START_DATE=2010-01-01
|
||||
# QUANT_END_DATE=2025-12-31
|
||||
@@ -0,0 +1,17 @@
|
||||
# 环境变量文件 (包含敏感信息,不要提交到版本控制)
|
||||
.env
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
|
||||
# 日志文件
|
||||
*.log
|
||||
|
||||
# Jupyter
|
||||
.ipynb_checkpoints/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
@@ -1,19 +1,30 @@
|
||||
"""
|
||||
量化数据库配置文件
|
||||
|
||||
敏感信息(数据库密码、API Token)通过环境变量管理:
|
||||
- QUANT_DB_PASSWORD : 数据库密码
|
||||
- TUSHARE_TOKEN : Tushare API Token
|
||||
|
||||
设置方式:
|
||||
Windows: set QUANT_DB_PASSWORD=your_password && set TUSHARE_TOKEN=your_token
|
||||
Linux: export QUANT_DB_PASSWORD=your_password && export TUSHARE_TOKEN=your_token
|
||||
也可创建 .env 文件(参考 .env.example)
|
||||
"""
|
||||
import os
|
||||
|
||||
# PostgreSQL Docker 连接配置
|
||||
DB_CONFIG = {
|
||||
"host": "192.168.27.15",
|
||||
"port": 12345,
|
||||
"database": "quant_db",
|
||||
"user": "postgres",
|
||||
"password": "postgres", # 请修改为实际密码
|
||||
"host": os.environ.get("QUANT_DB_HOST", "192.168.27.15"),
|
||||
"port": int(os.environ.get("QUANT_DB_PORT", "12345")),
|
||||
"database": os.environ.get("QUANT_DB_NAME", "quant_db"),
|
||||
"user": os.environ.get("QUANT_DB_USER", "postgres"),
|
||||
"password": os.environ.get("QUANT_DB_PASSWORD", ""),
|
||||
}
|
||||
|
||||
# Tushare API Token
|
||||
TUSHARE_TOKEN = "你的token" # 请替换为你的tushare token
|
||||
TUSHARE_TOKEN = os.environ.get("TUSHARE_TOKEN", "")
|
||||
|
||||
# 批量导入参数
|
||||
BATCH_SIZE = 5000 # 每批次插入行数
|
||||
START_DATE = "2010-01-01" # 数据起始日期
|
||||
END_DATE = "2025-12-31" # 数据结束日期
|
||||
BATCH_SIZE = int(os.environ.get("QUANT_BATCH_SIZE", "5000")) # 每批次插入行数
|
||||
START_DATE = os.environ.get("QUANT_START_DATE", "2010-01-01") # 数据起始日期
|
||||
END_DATE = os.environ.get("QUANT_END_DATE", "2025-12-31") # 数据结束日期
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
"| index_daily | 指数日线行情 | index_daily |\n",
|
||||
"\n",
|
||||
"## 使用步骤\n",
|
||||
"1. 修改 `config.py` 中的数据库密码和 Tushare Token\n",
|
||||
"2. 逐 Cell 运行本 Notebook"
|
||||
"1. 复制 `.env.example` 为 `.env`,填入真实密码和 Token\n",
|
||||
"2. 逐 Cell 运行本 Notebook(敏感信息通过环境变量读取)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -124,7 +124,7 @@
|
||||
" print(f\"✗ 连接失败: {e}\")\n",
|
||||
" print(\"请检查:\")\n",
|
||||
" print(\" 1. Docker 容器是否已启动: docker ps | findstr postgres\")\n",
|
||||
" print(\" 2. config.py 中的连接参数是否正确\")\n",
|
||||
" print(\" 2. 环境变量 (.env) 中的连接参数是否正确\")\n",
|
||||
" print(\" 3. 防火墙是否开放 12345 端口\")"
|
||||
]
|
||||
},
|
||||
@@ -143,7 +143,7 @@
|
||||
" print(f\" 测试查询: {df.iloc[0].to_dict()}\")\n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"✗ Tushare API 连接失败: {e}\")\n",
|
||||
" print(\"请检查 config.py 中的 TUSHARE_TOKEN 是否正确\")"
|
||||
" print(\"请检查环境变量 TUSHARE_TOKEN 是否正确(或 .env 文件)\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -214,7 +214,7 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 导入交易日历 (默认从 config.py 的 START_DATE ~ END_DATE)\n",
|
||||
"# 导入交易日历 (默认从环境变量 QUANT_START_DATE ~ QUANT_END_DATE)\n",
|
||||
"import_trade_cal()"
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user