Files
strategy_backtest/strategies/ocz_strategy.py

243 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""OCZ策略One Candle Zone突破回踩策略
策略逻辑:
1. 寻找关键阻力位最近N日最高价
2. 等待大阳线突破(实体占比>B%,涨幅>R%,放量>V1倍
3. 首次回踩到阻力位±TOL容错且缩量时买入
4. 持有期间:
- 止损:跌破阻力位立即卖出
- 止盈达到盈亏比1:2的目标价位立即卖出
- 或持有指定天数后卖出
特殊风控说明:
- 止损位 = 突破的阻力位(买入时记录)
- 止盈位 = 买入价 + (买入价 - 止损位) * 2
- 例如阻力位100元买入价105元则止损100元-5元止盈115元+10元
参数说明:
- N: 阻力回望长度默认30日
- B: 实体占比门槛默认60%
- V1: 放量倍数默认1.5倍)
- TOL: 回踩容错百分比默认1.5%
- R: 收盘涨幅门槛默认6%
- hold_days: 持有天数默认5天
- volatility_min: 最小波动率默认2.5%
- volatility_max: 最大波动率默认8.0%
"""
from typing import Dict, List, Optional
import pandas as pd
from strategies.base_strategy import BaseStrategy
from utils.logger import setup_logger
logger = setup_logger(__name__)
class OczStrategy(BaseStrategy):
"""OCZ突破回踩策略。"""
def __init__(
self,
initial_cash: float,
N: int = 30, # 阻力回望长度
B: float = 60.0, # 实体占比门槛(%
V1: float = 1.5, # 放量倍数
TOL: float = 1.5, # 回踩容错(%
R: float = 4.0, # 收盘涨幅门槛(%
hold_days: int = 5, # 持有天数
volatility_min: float = 2.5, # 最小波动率(%
volatility_max: float = 8.0, # 最大波动率(%
max_positions: int = 2,
position_pct_per_stock: float = 0.2,
stop_loss: Optional[object] = None,
take_profit: Optional[object] = None,
position_sizer: Optional[object] = None,
date_index_dict: Optional[Dict[str, Dict[str, int]]] = None,
buy_signal_index: Optional[Dict[str, List[str]]] = None,
):
"""初始化OCZ策略。"""
super().__init__(
initial_cash=initial_cash,
max_positions=max_positions,
stop_loss=stop_loss,
take_profit=take_profit,
position_sizer=position_sizer,
date_index_dict=date_index_dict,
buy_signal_index=buy_signal_index,
)
self.N = int(N)
self.B = float(B)
self.V1 = float(V1)
self.TOL = float(TOL)
self.R = float(R)
self.hold_days = int(hold_days)
self.volatility_min = float(volatility_min)
self.volatility_max = float(volatility_max)
self.position_pct_per_stock = float(position_pct_per_stock)
# 记录每只股票的突破信息(用于特殊止损止盈)
# 格式: {ts_code: {"resistance": 阻力位, "buy_price": 买入价, "stop_loss": 止损价, "take_profit": 止盈价}}
self.position_risk_info: Dict[str, Dict[str, float]] = {}
logger.info(
f"OczStrategy 初始化参数: N={self.N}, B={self.B}, V1={self.V1}, "
f"TOL={self.TOL}, R={self.R}, hold_days={self.hold_days}, "
f"max_positions={max_positions}, position_pct_per_stock={self.position_pct_per_stock}"
)
def on_bar(self, current_date: str, data_dict: Dict[str, pd.DataFrame]) -> None:
"""每个交易日的交易决策逻辑。"""
# 1. 先处理卖出逻辑(止损/止盈/持有到期)
for ts_code in list(self.positions.keys()):
df = data_dict.get(ts_code)
if df is None or df.empty:
continue
# 使用预计算的日期索引
date_index = self.date_index_dict.get(ts_code)
if date_index is None or current_date not in date_index:
continue
idx = date_index[current_date]
row = df.iloc[idx]
close = float(row["close"])
low = float(row["low"]) # 用于检查是否跌破阻力位
pos = self.positions[ts_code]
risk_info = self.position_risk_info.get(ts_code, {})
should_sell = False
sell_reason = ""
# 检查特殊止损:跌破阻力位
if risk_info and "stop_loss" in risk_info:
stop_loss_price = risk_info["stop_loss"]
if low <= stop_loss_price: # 最低价跌破止损位
should_sell = True
sell_reason = f"跌破阻力位止损({stop_loss_price:.2f})"
# 检查特殊止盈达到盈亏比1:2目标
if not should_sell and risk_info and "take_profit" in risk_info:
take_profit_price = risk_info["take_profit"]
if close >= take_profit_price: # 收盘价达到止盈位
should_sell = True
sell_reason = f"达到止盈位({take_profit_price:.2f})"
# 检查持有到期
if not should_sell and pos.days_held >= self.hold_days:
should_sell = True
sell_reason = f"持有到期({self.hold_days}天)"
# 执行卖出
if should_sell:
quantity = pos.quantity
if quantity > 0:
self.sell(ts_code, close, quantity)
# 清理风控信息
if ts_code in self.position_risk_info:
del self.position_risk_info[ts_code]
# logger.info(f"{current_date} 卖出 {ts_code} 原因: {sell_reason}") # 已移除:交易日志
# 2. 处理买入逻辑(回踩信号)
if len(self.positions) >= self.max_positions:
return
# 优化如果现金过少不足初始资金1%),直接返回
if self.cash < self.initial_cash * 0.01:
return
# 使用买入信号索引获取今天有信号的股票列表
signal_stocks = self.buy_signal_index.get(current_date, [])
if not signal_stocks:
return # 今天没有任何买入信号
candidates = [] # 候选股票列表:[(ts_code, price), ...]
# 只遍历有买入信号的股票
for ts_code in signal_stocks:
if ts_code in self.positions:
continue
df = data_dict.get(ts_code)
if df is None or df.empty:
continue
# 使用预计算的日期索引
date_index = self.date_index_dict.get(ts_code)
if date_index is None or current_date not in date_index:
continue
idx = date_index[current_date]
# 检查是否有足够的历史数据
if idx < self.N + 2:
continue
row = df.iloc[idx]
# 确认买入信号存在并获取阻力位
if "pullback_signal" in df.columns and row["pullback_signal"]:
price = float(row["close"])
# 获取突破的阻力位从预计算的resistance列中读取
resistance = float(row["resistance"]) if "resistance" in df.columns else price * 0.95
candidates.append((ts_code, price, resistance))
if not candidates:
return
# 按价格排序(可选,这里简单按顺序买入)
remain_slots = self.max_positions - len(self.positions)
selected = candidates[:remain_slots]
# 依次买入选中的股票
for item in selected:
ts_code, price, resistance = item # 解包包含阻力位的元组
# 使用仓位管理器计算买入数量(如果有的话)
if self.position_sizer is not None:
df = data_dict.get(ts_code)
quantity = self.position_sizer.calc_shares(
ts_code=ts_code,
cash=self.cash,
price=price,
remain_slots=remain_slots,
df=df,
)
else:
# 默认:按配置的比例分配仓位
target_cash = self.initial_cash * self.position_pct_per_stock
actual_cash = min(target_cash, self.cash)
# 计算能买多少股向下取整到100的整数倍
quantity = int(actual_cash // price)
quantity = (quantity // 100) * 100
if quantity <= 0:
continue
# 买入前记录当前现金和仓位
before_cash = self.cash
before_positions = len(self.positions)
self.buy(ts_code, price, quantity)
# 买入成功(现金减少且仓位增加)
if self.cash < before_cash and len(self.positions) > before_positions:
# 计算并记录止损止盈价位
stop_loss_price = resistance # 止损位 = 阻力位
risk_distance = price - resistance # 风险距离 = 买入价 - 阻力位
take_profit_price = price + risk_distance * 2 # 止盈位 = 买入价 + 风险距离 * 2 (盈亏比1:2)
# 保存风控信息
self.position_risk_info[ts_code] = {
"resistance": resistance,
"buy_price": price,
"stop_loss": stop_loss_price,
"take_profit": take_profit_price,
}
# logger.info(
# f"{current_date} 买入 {ts_code} 价格:{price:.2f} 阻力位:{resistance:.2f} "
# f"止损:{stop_loss_price:.2f}(-{risk_distance:.2f}) 止盈:{take_profit_price:.2f}(+{risk_distance*2:.2f})"
# ) # 已移除:交易日志