新建回测系统,并提交
This commit is contained in:
8
risk/__init__.py
Normal file
8
risk/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""风险管理模块。
|
||||
|
||||
提供止盈止损、持仓规模优化等功能。
|
||||
"""
|
||||
from risk.stop_loss import StopLoss
|
||||
from risk.position_sizing import PositionSizing
|
||||
|
||||
__all__ = ["StopLoss", "PositionSizing"]
|
||||
BIN
risk/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
risk/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
risk/__pycache__/position_sizing.cpython-310.pyc
Normal file
BIN
risk/__pycache__/position_sizing.cpython-310.pyc
Normal file
Binary file not shown.
BIN
risk/__pycache__/stop_loss.cpython-310.pyc
Normal file
BIN
risk/__pycache__/stop_loss.cpython-310.pyc
Normal file
Binary file not shown.
263
risk/position_sizing.py
Normal file
263
risk/position_sizing.py
Normal file
@@ -0,0 +1,263 @@
|
||||
"""持仓规模优化模块。
|
||||
|
||||
支持多种仓位管理方法:
|
||||
- equal_weight: 等权分配(默认)
|
||||
- kelly: Kelly公式(需要历史胜率和盈亏比)
|
||||
- volatility_target: 波动率目标(根据股票波动率调整仓位)
|
||||
|
||||
使用方法:
|
||||
position_sizer = PositionSizing(method="equal_weight", max_positions=2)
|
||||
|
||||
# 在策略中使用
|
||||
if position_sizer.can_open(ts_code, cash, price):
|
||||
shares = position_sizer.calc_shares(ts_code, cash, price, df)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, Optional
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
class PositionSizing:
|
||||
"""持仓规模优化管理器。
|
||||
|
||||
支持多种方法:
|
||||
- equal_weight: 等权分配
|
||||
- kelly: Kelly公式
|
||||
- volatility_target: 波动率目标
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
method: str = "equal_weight",
|
||||
max_positions: int = 2,
|
||||
kelly_risk_free: float = 0.03,
|
||||
kelly_max_fraction: float = 0.25,
|
||||
volatility_target: float = 0.15,
|
||||
volatility_window: int = 20,
|
||||
):
|
||||
"""初始化持仓规模管理器。
|
||||
|
||||
参数:
|
||||
method: 仓位管理方法 ("equal_weight", "kelly", "volatility_target")
|
||||
max_positions: 最大持仓数
|
||||
kelly_risk_free: Kelly公式的无风险利率
|
||||
kelly_max_fraction: Kelly公式的最大仓位比例(防止过度杠杆)
|
||||
volatility_target: 目标波动率(年化)
|
||||
volatility_window: 计算波动率的窗口期
|
||||
"""
|
||||
self.method = method
|
||||
self.max_positions = max_positions
|
||||
self.kelly_risk_free = kelly_risk_free
|
||||
self.kelly_max_fraction = kelly_max_fraction
|
||||
self.volatility_target = volatility_target
|
||||
self.volatility_window = volatility_window
|
||||
|
||||
# 记录每只股票的历史交易统计(用于Kelly公式)
|
||||
self.trade_stats: Dict[str, Dict] = {}
|
||||
|
||||
def can_open(self, ts_code: str, cash: float, price: float, current_positions: int) -> bool:
|
||||
"""判断是否可以开新仓位。
|
||||
|
||||
参数:
|
||||
ts_code: 股票代码
|
||||
cash: 当前可用现金
|
||||
price: 当前价格
|
||||
current_positions: 当前持仓数量
|
||||
|
||||
返回:
|
||||
bool: 是否可以开仓
|
||||
"""
|
||||
# 检查仓位是否已满
|
||||
if current_positions >= self.max_positions:
|
||||
return False
|
||||
|
||||
# 检查资金是否足够买入至少 100 股
|
||||
min_cost = price * 100
|
||||
if cash < min_cost:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def calc_shares(
|
||||
self,
|
||||
ts_code: str,
|
||||
cash: float,
|
||||
price: float,
|
||||
remain_slots: int,
|
||||
df: Optional[pd.DataFrame] = None,
|
||||
) -> int:
|
||||
"""计算应该买入的股数。
|
||||
|
||||
参数:
|
||||
ts_code: 股票代码
|
||||
cash: 当前可用现金
|
||||
price: 当前价格
|
||||
remain_slots: 剩余可用仓位数
|
||||
df: 股票历史数据(用于计算波动率等指标)
|
||||
|
||||
返回:
|
||||
int: 买入股数(已取整到100的整数倍)
|
||||
"""
|
||||
if self.method == "equal_weight":
|
||||
return self._equal_weight_shares(cash, price, remain_slots)
|
||||
elif self.method == "kelly":
|
||||
return self._kelly_shares(ts_code, cash, price, remain_slots)
|
||||
elif self.method == "volatility_target":
|
||||
return self._volatility_target_shares(ts_code, cash, price, remain_slots, df)
|
||||
else:
|
||||
logger.warning(f"未知的仓位管理方法: {self.method},使用等权分配")
|
||||
return self._equal_weight_shares(cash, price, remain_slots)
|
||||
|
||||
def _equal_weight_shares(self, cash: float, price: float, remain_slots: int) -> int:
|
||||
"""等权分配:平均分配现金到剩余仓位。
|
||||
|
||||
例如:现金100万,剩余2个仓位,每个仓位分配50万
|
||||
"""
|
||||
if remain_slots <= 0:
|
||||
return 0
|
||||
|
||||
cash_per_stock = cash / remain_slots
|
||||
shares = int(cash_per_stock // price)
|
||||
|
||||
# A股规则:向下取整到100的整数倍
|
||||
shares = (shares // 100) * 100
|
||||
|
||||
return shares
|
||||
|
||||
def _kelly_shares(self, ts_code: str, cash: float, price: float, remain_slots: int) -> int:
|
||||
"""Kelly公式:根据历史胜率和盈亏比计算最优仓位。
|
||||
|
||||
Kelly% = (胜率 * 盈亏比 - 败率) / 盈亏比
|
||||
|
||||
注意:需要积累一定的交易历史才能准确计算。
|
||||
如果没有历史数据,回退到等权分配。
|
||||
"""
|
||||
stats = self.trade_stats.get(ts_code)
|
||||
|
||||
if stats is None or stats.get("total_trades", 0) < 10:
|
||||
# 交易次数不足,使用等权分配
|
||||
logger.debug(f"{ts_code} 历史交易不足,使用等权分配")
|
||||
return self._equal_weight_shares(cash, price, remain_slots)
|
||||
|
||||
win_rate = stats.get("win_rate", 0.5)
|
||||
avg_win = stats.get("avg_win", 0.05)
|
||||
avg_loss = stats.get("avg_loss", 0.05)
|
||||
|
||||
if avg_loss <= 0:
|
||||
avg_loss = 0.01 # 避免除零
|
||||
|
||||
profit_loss_ratio = avg_win / avg_loss
|
||||
kelly_fraction = (win_rate * profit_loss_ratio - (1 - win_rate)) / profit_loss_ratio
|
||||
|
||||
# Kelly公式可能给出负值或过大值,需要限制
|
||||
kelly_fraction = max(0, min(kelly_fraction, self.kelly_max_fraction))
|
||||
|
||||
# 考虑剩余仓位数,分配资金
|
||||
cash_per_stock = cash / remain_slots
|
||||
kelly_cash = cash_per_stock * kelly_fraction
|
||||
|
||||
shares = int(kelly_cash // price)
|
||||
shares = (shares // 100) * 100
|
||||
|
||||
logger.debug(
|
||||
f"{ts_code} Kelly仓位: {kelly_fraction*100:.2f}%, "
|
||||
f"胜率={win_rate*100:.1f}%, 盈亏比={profit_loss_ratio:.2f}"
|
||||
)
|
||||
|
||||
return shares
|
||||
|
||||
def _volatility_target_shares(
|
||||
self,
|
||||
ts_code: str,
|
||||
cash: float,
|
||||
price: float,
|
||||
remain_slots: int,
|
||||
df: Optional[pd.DataFrame],
|
||||
) -> int:
|
||||
"""波动率目标:根据股票波动率调整仓位。
|
||||
|
||||
波动率高的股票减小仓位,波动率低的股票增大仓位。
|
||||
目标:使每个仓位的波动率贡献接近目标波动率。
|
||||
"""
|
||||
if df is None or df.empty:
|
||||
logger.debug(f"{ts_code} 缺少历史数据,使用等权分配")
|
||||
return self._equal_weight_shares(cash, price, remain_slots)
|
||||
|
||||
# 计算历史波动率(日收益率标准差 * sqrt(252))
|
||||
if "close" not in df.columns or len(df) < self.volatility_window:
|
||||
logger.debug(f"{ts_code} 数据不足,使用等权分配")
|
||||
return self._equal_weight_shares(cash, price, remain_slots)
|
||||
|
||||
returns = df["close"].pct_change().dropna()
|
||||
|
||||
if len(returns) < self.volatility_window:
|
||||
logger.debug(f"{ts_code} 数据不足,使用等权分配")
|
||||
return self._equal_weight_shares(cash, price, remain_slots)
|
||||
|
||||
# 使用最近 volatility_window 天的数据计算波动率
|
||||
recent_volatility = returns.tail(self.volatility_window).std() * (252 ** 0.5)
|
||||
|
||||
if recent_volatility <= 0:
|
||||
logger.debug(f"{ts_code} 波动率为0,使用等权分配")
|
||||
return self._equal_weight_shares(cash, price, remain_slots)
|
||||
|
||||
# 仓位调整因子 = 目标波动率 / 实际波动率
|
||||
volatility_factor = self.volatility_target / recent_volatility
|
||||
volatility_factor = max(0.5, min(volatility_factor, 2.0)) # 限制在 [0.5, 2.0]
|
||||
|
||||
# 基础等权分配 * 波动率因子
|
||||
base_shares = self._equal_weight_shares(cash, price, remain_slots)
|
||||
adjusted_shares = int(base_shares * volatility_factor)
|
||||
adjusted_shares = (adjusted_shares // 100) * 100
|
||||
|
||||
logger.debug(
|
||||
f"{ts_code} 波动率调整: 实际={recent_volatility*100:.2f}%, "
|
||||
f"目标={self.volatility_target*100:.2f}%, 因子={volatility_factor:.2f}"
|
||||
)
|
||||
|
||||
return adjusted_shares
|
||||
|
||||
def update_trade_stats(self, ts_code: str, profit_pct: float) -> None:
|
||||
"""更新交易统计(用于Kelly公式)。
|
||||
|
||||
参数:
|
||||
ts_code: 股票代码
|
||||
profit_pct: 本次交易盈亏比例(正数为盈利,负数为亏损)
|
||||
"""
|
||||
if ts_code not in self.trade_stats:
|
||||
self.trade_stats[ts_code] = {
|
||||
"total_trades": 0,
|
||||
"wins": 0,
|
||||
"losses": 0,
|
||||
"total_win": 0.0,
|
||||
"total_loss": 0.0,
|
||||
}
|
||||
|
||||
stats = self.trade_stats[ts_code]
|
||||
stats["total_trades"] += 1
|
||||
|
||||
if profit_pct > 0:
|
||||
stats["wins"] += 1
|
||||
stats["total_win"] += profit_pct
|
||||
else:
|
||||
stats["losses"] += 1
|
||||
stats["total_loss"] += abs(profit_pct)
|
||||
|
||||
# 计算平均值
|
||||
stats["win_rate"] = stats["wins"] / stats["total_trades"]
|
||||
stats["avg_win"] = stats["total_win"] / stats["wins"] if stats["wins"] > 0 else 0.05
|
||||
stats["avg_loss"] = stats["total_loss"] / stats["losses"] if stats["losses"] > 0 else 0.05
|
||||
|
||||
def get_stats(self, ts_code: str) -> Optional[Dict]:
|
||||
"""获取某只股票的交易统计。"""
|
||||
return self.trade_stats.get(ts_code)
|
||||
|
||||
def clear_stats(self) -> None:
|
||||
"""清空所有交易统计。"""
|
||||
self.trade_stats.clear()
|
||||
200
risk/stop_loss.py
Normal file
200
risk/stop_loss.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""止盈止损模块。
|
||||
|
||||
支持多种止损策略:
|
||||
- fixed_pct: 固定百分比止损/止盈
|
||||
- atr: ATR倍数止损
|
||||
- trailing: 跟踪止盈(最高价回撤)
|
||||
|
||||
使用方法:
|
||||
stop_loss = StopLoss(method="fixed_pct", stop_pct=0.05)
|
||||
take_profit = StopLoss(method="fixed_pct", stop_pct=0.15)
|
||||
|
||||
# 在策略的 on_bar 中检查
|
||||
if stop_loss.should_exit(ts_code, high, low, close, cost_price):
|
||||
# 执行卖出
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, Optional
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
class StopLoss:
|
||||
"""止盈止损管理器。
|
||||
|
||||
支持多种方法:
|
||||
- fixed_pct: 固定百分比
|
||||
- atr: ATR倍数
|
||||
- trailing: 跟踪止盈
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
method: str = "fixed_pct",
|
||||
stop_pct: float = 0.05,
|
||||
atr_multiplier: float = 2.0,
|
||||
atr_period: int = 14,
|
||||
trailing_pct: float = 0.10,
|
||||
):
|
||||
"""初始化止损管理器。
|
||||
|
||||
参数:
|
||||
method: 止损方法 ("fixed_pct", "atr", "trailing")
|
||||
stop_pct: 固定百分比止损/止盈比例(0.05 表示 5%)
|
||||
atr_multiplier: ATR倍数
|
||||
atr_period: ATR周期
|
||||
trailing_pct: 跟踪止盈回撤比例
|
||||
"""
|
||||
self.method = method
|
||||
self.stop_pct = stop_pct
|
||||
self.atr_multiplier = atr_multiplier
|
||||
self.atr_period = atr_period
|
||||
self.trailing_pct = trailing_pct
|
||||
|
||||
# 跟踪止盈需要记录每只股票的最高价
|
||||
self.highest_price: Dict[str, float] = {}
|
||||
|
||||
def should_exit(
|
||||
self,
|
||||
ts_code: str,
|
||||
current_price: float,
|
||||
cost_price: float,
|
||||
high: Optional[float] = None,
|
||||
low: Optional[float] = None,
|
||||
atr: Optional[float] = None,
|
||||
) -> tuple[bool, str]:
|
||||
"""判断是否应该止损/止盈。
|
||||
|
||||
参数:
|
||||
ts_code: 股票代码
|
||||
current_price: 当前价格(通常是收盘价)
|
||||
cost_price: 成本价
|
||||
high: 当日最高价(用于跟踪止盈)
|
||||
low: 当日最低价(用于止损)
|
||||
atr: ATR指标值(用于ATR止损)
|
||||
|
||||
返回:
|
||||
(bool, str): (是否应该退出, 原因)
|
||||
"""
|
||||
if self.method == "fixed_pct":
|
||||
return self._fixed_pct_exit(ts_code, current_price, cost_price)
|
||||
elif self.method == "atr":
|
||||
return self._atr_exit(ts_code, current_price, cost_price, atr)
|
||||
elif self.method == "trailing":
|
||||
return self._trailing_exit(ts_code, current_price, cost_price, high)
|
||||
else:
|
||||
logger.warning(f"未知的止损方法: {self.method}")
|
||||
return False, ""
|
||||
|
||||
def _fixed_pct_exit(self, ts_code: str, current_price: float, cost_price: float) -> tuple[bool, str]:
|
||||
"""固定百分比止损/止盈。
|
||||
|
||||
止损:当前价格 < 成本价 * (1 - stop_pct)
|
||||
止盈:当前价格 > 成本价 * (1 + stop_pct)
|
||||
"""
|
||||
profit_pct = (current_price - cost_price) / cost_price
|
||||
|
||||
if profit_pct <= -self.stop_pct:
|
||||
reason = f"固定止损 {profit_pct*100:.2f}%"
|
||||
# logger.info(f"[STOP] {ts_code} 触发{reason},成本={cost_price:.2f}, 当前={current_price:.2f}") # 已移除:止损日志
|
||||
return True, reason
|
||||
|
||||
# 注意:这里的 stop_pct 实际用作止盈比例
|
||||
# 如果需要区分止损和止盈,可以增加一个 take_profit_pct 参数
|
||||
if profit_pct >= self.stop_pct:
|
||||
reason = f"固定止盈 {profit_pct*100:.2f}%"
|
||||
# logger.info(f"[STOP] {ts_code} 触发{reason},成本={cost_price:.2f}, 当前={current_price:.2f}") # 已移除:止盈日志
|
||||
return True, reason
|
||||
|
||||
return False, ""
|
||||
|
||||
def _atr_exit(self, ts_code: str, current_price: float, cost_price: float, atr: Optional[float]) -> tuple[bool, str]:
|
||||
"""ATR倍数止损。
|
||||
|
||||
止损:当前价格 < 成本价 - ATR * multiplier
|
||||
"""
|
||||
if atr is None or atr <= 0:
|
||||
logger.warning(f"{ts_code} ATR值无效,跳过ATR止损")
|
||||
return False, ""
|
||||
|
||||
stop_price = cost_price - atr * self.atr_multiplier
|
||||
|
||||
if current_price < stop_price:
|
||||
profit_pct = (current_price - cost_price) / cost_price
|
||||
reason = f"ATR止损 {profit_pct*100:.2f}% (ATR={atr:.2f})"
|
||||
# logger.info(f"[STOP] {ts_code} 触发{reason},成本={cost_price:.2f}, 止损价={stop_price:.2f}, 当前={current_price:.2f}") # 已移除
|
||||
return True, reason
|
||||
|
||||
return False, ""
|
||||
|
||||
def _trailing_exit(self, ts_code: str, current_price: float, cost_price: float, high: Optional[float]) -> tuple[bool, str]:
|
||||
"""跟踪止盈。
|
||||
|
||||
记录持仓期间的最高价,当价格从最高价回撤超过 trailing_pct 时卖出。
|
||||
"""
|
||||
# 使用当日最高价或当前价更新历史最高价
|
||||
if high is not None:
|
||||
price_to_track = max(current_price, high)
|
||||
else:
|
||||
price_to_track = current_price
|
||||
|
||||
if ts_code not in self.highest_price:
|
||||
self.highest_price[ts_code] = price_to_track
|
||||
else:
|
||||
self.highest_price[ts_code] = max(self.highest_price[ts_code], price_to_track)
|
||||
|
||||
highest = self.highest_price[ts_code]
|
||||
drawdown_from_high = (highest - current_price) / highest
|
||||
|
||||
# 只有在盈利的情况下才触发跟踪止盈
|
||||
if current_price > cost_price and drawdown_from_high >= self.trailing_pct:
|
||||
profit_pct = (current_price - cost_price) / cost_price
|
||||
reason = f"跟踪止盈 从最高点{highest:.2f}回撤{drawdown_from_high*100:.2f}%,盈利{profit_pct*100:.2f}%"
|
||||
# logger.info(f"[STOP] {ts_code} 触发{reason},成本={cost_price:.2f}, 当前={current_price:.2f}") # 已移除
|
||||
|
||||
# 清除最高价记录
|
||||
del self.highest_price[ts_code]
|
||||
return True, reason
|
||||
|
||||
return False, ""
|
||||
|
||||
def reset_tracking(self, ts_code: str) -> None:
|
||||
"""重置某只股票的跟踪状态(例如清仓后)。"""
|
||||
if ts_code in self.highest_price:
|
||||
del self.highest_price[ts_code]
|
||||
|
||||
def clear_all(self) -> None:
|
||||
"""清空所有跟踪状态。"""
|
||||
self.highest_price.clear()
|
||||
|
||||
|
||||
def calculate_atr(df: pd.DataFrame, period: int = 14) -> pd.Series:
|
||||
"""计算ATR指标(Average True Range)。
|
||||
|
||||
参数:
|
||||
df: 包含 ['high', 'low', 'close'] 的DataFrame
|
||||
period: ATR周期
|
||||
|
||||
返回:
|
||||
pd.Series: ATR值
|
||||
"""
|
||||
high = df["high"]
|
||||
low = df["low"]
|
||||
close = df["close"]
|
||||
|
||||
# True Range = max(high-low, abs(high-prev_close), abs(low-prev_close))
|
||||
tr1 = high - low
|
||||
tr2 = (high - close.shift(1)).abs()
|
||||
tr3 = (low - close.shift(1)).abs()
|
||||
|
||||
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
|
||||
|
||||
# ATR = MA(TR, period)
|
||||
atr = tr.rolling(window=period).mean()
|
||||
|
||||
return atr
|
||||
Reference in New Issue
Block a user