新建回测系统,并提交
This commit is contained in:
16
strategies/10分钟 均线红绿灯策略.txt
Normal file
16
strategies/10分钟 均线红绿灯策略.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
10分钟 均线红绿灯策略
|
||||
|
||||
流程:
|
||||
|
||||
流程再钉一次,避免周期混用:
|
||||
日线 15:00 收市后
|
||||
运行「初筛公式」→ 得到自选池(46 只左右)
|
||||
次日 09:25 以前
|
||||
把「再筛公式」仍在日线周期跑一遍(可夜盘定时选股):
|
||||
市值、振幅、SLOPE(DAYMA10,3) 都是昨日日 K 数据
|
||||
→ 输出「今日观察池」(≤10 只)
|
||||
09:30-10:00 切 1-min 周期
|
||||
只对「观察池」做分时预警:
|
||||
用 #DAYCLOSE 拿当天实时日级收盘
|
||||
用 DAYMA10:=ROUND(MA(#DAYCLOSE,10),2) 拿实时日 10 日线
|
||||
踩线、站回逻辑都在 1-min 里完成
|
||||
48
strategies/OCZ策略.txt
Normal file
48
strategies/OCZ策略.txt
Normal file
@@ -0,0 +1,48 @@
|
||||
{参数}
|
||||
N:=30; {阻力回望长度}
|
||||
B:=60; {实体占比%}
|
||||
V1:=1.5; {放量倍数}
|
||||
TOL:=1.5; {回踩容错%}
|
||||
R:=4; {收盘涨幅门槛 %}
|
||||
ATR周期:=14; {ATR周期}
|
||||
VOL30:=MA(VOL,30); {30日均量}
|
||||
|
||||
|
||||
{1. 关键阻力 = 最近 N 日最高}
|
||||
阻力:HHV(H,N),NODRAW;
|
||||
|
||||
{2. ONE CANDLE 突破}
|
||||
实体:=ABS(C-O);
|
||||
总幅:=H-L;
|
||||
突破:=C>REF(阻力,1) AND 实体/总幅*100>B AND (C/REF(C,1)-1)*100>=R AND VOL>MA(VOL,N)*V1;
|
||||
|
||||
DRAWICON(突破,L*0.96,34);
|
||||
|
||||
{3. 波动率过滤(2.5%-8%)}
|
||||
真实波幅:=MA(H-L,30);
|
||||
波动率:=真实波幅/C*100;
|
||||
波动够:波动率>=2.5 AND 波动率<=8.0,NODRAW;
|
||||
|
||||
{4. 市值过滤(30亿-120亿)}
|
||||
流通市值:=FINANCE(40)/10000; {单位:亿}
|
||||
市值合适:=流通市值>=30 AND 流通市值<=120;
|
||||
|
||||
|
||||
{3. 画阻力线}
|
||||
{阻力线:阻力,COLORYELLOW,DOTLINE;}
|
||||
DRAWSL(突破,REF(阻力,1),0,5,2)COLORYELLOW;
|
||||
DRAWICON(突破,H*1.05,1); {洋红箭头}
|
||||
|
||||
|
||||
{4. 首次回踩}
|
||||
BAR突:=BARSLAST(突破);
|
||||
D2ZL:=REF(阻力,2),NODRAW;
|
||||
回踩区:=BETWEEN(L,D2ZL*0.985,D2ZL*1.015); {±1.5% 区间}
|
||||
{回踩区:=BETWEEN(L,阻力*(1-TOL/100),阻力*(1+TOL/100));}
|
||||
收回:=C>D2ZL;
|
||||
|
||||
{回踩成交量}
|
||||
|
||||
缩量:=VOL<REF(VOL,BAR突) ; { BAR突=突破日距今天数 }
|
||||
|
||||
回踩信号:BAR突=1 AND 收回 AND 回踩区 AND 缩量 ,NODRAW ;
|
||||
5
strategies/__init__.py
Normal file
5
strategies/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""策略包初始化文件。"""
|
||||
|
||||
from utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
BIN
strategies/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
strategies/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
strategies/__pycache__/base_strategy.cpython-310.pyc
Normal file
BIN
strategies/__pycache__/base_strategy.cpython-310.pyc
Normal file
Binary file not shown.
BIN
strategies/__pycache__/ma_cross.cpython-310.pyc
Normal file
BIN
strategies/__pycache__/ma_cross.cpython-310.pyc
Normal file
Binary file not shown.
BIN
strategies/__pycache__/ocz_strategy.cpython-310.pyc
Normal file
BIN
strategies/__pycache__/ocz_strategy.cpython-310.pyc
Normal file
Binary file not shown.
292
strategies/base_strategy.py
Normal file
292
strategies/base_strategy.py
Normal file
@@ -0,0 +1,292 @@
|
||||
"""策略基类与回测主循环实现。
|
||||
|
||||
BaseStrategy 定义:
|
||||
- 账户与持仓管理;
|
||||
- 买卖接口;
|
||||
- 回测主循环 run_backtest;
|
||||
- 集成止盈止损和仓位管理钩子;
|
||||
子类只需实现 on_bar,在每个交易日根据行情数据决策买卖。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Position:
|
||||
"""单只股票的持仓信息。"""
|
||||
|
||||
ts_code: str
|
||||
quantity: int
|
||||
cost: float # 成本价
|
||||
days_held: int = 0
|
||||
|
||||
|
||||
class BaseStrategy(ABC):
|
||||
"""回测策略抽象基类。
|
||||
|
||||
- 管理资金与持仓;
|
||||
- 提供买卖接口;
|
||||
- 实现回测主循环;
|
||||
- 支持止盈止损和仓位管理;
|
||||
- 子类只需实现 on_bar 方法。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
initial_cash: float,
|
||||
max_positions: int = 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,
|
||||
):
|
||||
"""初始化策略。
|
||||
|
||||
参数:
|
||||
initial_cash: 初始资金
|
||||
max_positions: 最大持仓数
|
||||
stop_loss: 止损管理器(StopLoss实例)
|
||||
take_profit: 止盈管理器(StopLoss实例)
|
||||
position_sizer: 仓位管理器(PositionSizing实例)
|
||||
date_index_dict: 日期索引字典 {ts_code: {date: idx}}
|
||||
buy_signal_index: 买入信号索引 {date: [ts_code1, ts_code2, ...]}
|
||||
"""
|
||||
self.initial_cash = float(initial_cash)
|
||||
self.cash: float = float(initial_cash)
|
||||
self.max_positions: int = int(max_positions)
|
||||
self.positions: Dict[str, Position] = {}
|
||||
self.equity_curve: List[Dict] = []
|
||||
self.trade_count: int = 0 # 交易次数统计(买入+卖出)
|
||||
|
||||
# 交易历史记录(用于计算胜率和盈亏比)
|
||||
self.trade_history: List[Dict] = [] # 记录每笔完整交易(买入->卖出)
|
||||
|
||||
# 风险管理模块(可选)
|
||||
self.stop_loss = stop_loss
|
||||
self.take_profit = take_profit
|
||||
self.position_sizer = position_sizer
|
||||
|
||||
# 性能优化:日期索引字典(避免deepcopypandas.DataFrame.attrs)
|
||||
self.date_index_dict = date_index_dict if date_index_dict is not None else {}
|
||||
self.buy_signal_index = buy_signal_index if buy_signal_index is not None else {}
|
||||
|
||||
# ------------ 需要子类实现的接口 ------------
|
||||
@abstractmethod
|
||||
def on_bar(self, current_date: str, data_dict: Dict[str, pd.DataFrame]) -> None:
|
||||
"""每个交易日调用一次,由子类实现交易逻辑。"""
|
||||
|
||||
# ------------ 买卖接口 ------------
|
||||
def buy(self, ts_code: str, price: float, quantity: int) -> None:
|
||||
"""以指定价格和数量买入股票。
|
||||
|
||||
A股交易规则:
|
||||
- 最小交易单位为 1 手(100 股);
|
||||
- 买入数量必须为 100 的整数倍。
|
||||
"""
|
||||
# 向下取整到 100 的整数倍
|
||||
quantity = (quantity // 100) * 100
|
||||
if quantity <= 0:
|
||||
return
|
||||
cost_amount = float(price) * int(quantity)
|
||||
if cost_amount > self.cash:
|
||||
return
|
||||
|
||||
self.cash -= cost_amount
|
||||
if ts_code in self.positions:
|
||||
pos = self.positions[ts_code]
|
||||
total_cost = pos.cost * pos.quantity + cost_amount
|
||||
total_qty = pos.quantity + quantity
|
||||
pos.cost = total_cost / total_qty
|
||||
pos.quantity = total_qty
|
||||
# 继续累积 days_held
|
||||
else:
|
||||
self.positions[ts_code] = Position(ts_code=ts_code, quantity=quantity, cost=float(price))
|
||||
|
||||
self.trade_count += 1 # 记录买入交易
|
||||
|
||||
def sell(self, ts_code: str, price: float, quantity: int) -> None:
|
||||
"""以指定价格和数量卖出股票。"""
|
||||
if ts_code not in self.positions:
|
||||
return
|
||||
pos = self.positions[ts_code]
|
||||
sell_qty = min(quantity, pos.quantity)
|
||||
if sell_qty <= 0:
|
||||
return
|
||||
|
||||
# 计算盈亏(用于更新仓位管理器统计)
|
||||
profit_pct = (price - pos.cost) / pos.cost
|
||||
profit_amount = (price - pos.cost) * sell_qty
|
||||
|
||||
# 记录交易历史
|
||||
self.trade_history.append({
|
||||
"ts_code": ts_code,
|
||||
"buy_price": pos.cost,
|
||||
"sell_price": price,
|
||||
"quantity": sell_qty,
|
||||
"profit_pct": profit_pct,
|
||||
"profit_amount": profit_amount,
|
||||
"is_win": profit_pct > 0,
|
||||
})
|
||||
|
||||
self.cash += float(price) * sell_qty
|
||||
pos.quantity -= sell_qty
|
||||
if pos.quantity == 0:
|
||||
del self.positions[ts_code]
|
||||
# 清理跟踪止盈状态
|
||||
if self.stop_loss is not None:
|
||||
self.stop_loss.reset_tracking(ts_code)
|
||||
if self.take_profit is not None:
|
||||
self.take_profit.reset_tracking(ts_code)
|
||||
|
||||
# 更新仓位管理器的交易统计(用于Kelly公式)
|
||||
if self.position_sizer is not None and hasattr(self.position_sizer, 'update_trade_stats'):
|
||||
self.position_sizer.update_trade_stats(ts_code, profit_pct)
|
||||
|
||||
self.trade_count += 1 # 记录卖出交易
|
||||
|
||||
# ------------ 回测主循环 ------------
|
||||
def _calc_market_value(self, current_date: str, data_dict: Dict[str, pd.DataFrame]) -> float:
|
||||
"""计算当前持仓市值。"""
|
||||
total = 0.0
|
||||
for ts_code, pos in self.positions.items():
|
||||
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 not None and current_date in date_index:
|
||||
idx = date_index[current_date]
|
||||
price = float(df.iloc[idx]["close"])
|
||||
else:
|
||||
# 备用方案:若当日无行情,则以最近一条收盘价估值
|
||||
price = float(df["close"].iloc[-1])
|
||||
|
||||
total += price * pos.quantity
|
||||
return total
|
||||
|
||||
def _update_days_held(self, current_date: str, data_dict: Dict[str, pd.DataFrame]) -> None:
|
||||
"""默认每天将所有持仓的持有天数 +1。
|
||||
|
||||
若子类需要更复杂逻辑,可以覆盖或在 on_bar 中自行管理。
|
||||
"""
|
||||
for pos in self.positions.values():
|
||||
pos.days_held += 1
|
||||
|
||||
def _check_stop_loss_take_profit(self, current_date: str, data_dict: Dict[str, pd.DataFrame]) -> None:
|
||||
"""检查所有持仓的止盈止损条件。
|
||||
|
||||
在每个交易日开盘前或收盘后调用,自动触发止损/止盈卖出。
|
||||
子类可以覆盖此方法以实现自定义逻辑。
|
||||
"""
|
||||
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]
|
||||
|
||||
pos = self.positions[ts_code]
|
||||
close = float(row["close"])
|
||||
high = float(row["high"]) if "high" in df.columns else None
|
||||
low = float(row["low"]) if "low" in df.columns else None
|
||||
|
||||
# 计算ATR(如果需要)
|
||||
atr = None
|
||||
if self.stop_loss is not None and self.stop_loss.method == "atr":
|
||||
from risk.stop_loss import calculate_atr
|
||||
atr_series = calculate_atr(df, self.stop_loss.atr_period)
|
||||
if not atr_series.empty:
|
||||
atr = float(atr_series.iloc[-1])
|
||||
|
||||
# 检查止损
|
||||
if self.stop_loss is not None:
|
||||
should_exit, reason = self.stop_loss.should_exit(
|
||||
ts_code=ts_code,
|
||||
current_price=close,
|
||||
cost_price=pos.cost,
|
||||
high=high,
|
||||
low=low,
|
||||
atr=atr,
|
||||
)
|
||||
if should_exit:
|
||||
quantity = pos.quantity
|
||||
self.sell(ts_code, close, quantity)
|
||||
# logger.info(f"{current_date} 止损卖出 {ts_code} 数量 {quantity} 价格 {close:.2f} 原因: {reason}") # 已移除:止损日志改用进度条
|
||||
continue # 已卖出,不再检查止盈
|
||||
|
||||
# 检查止盈
|
||||
if self.take_profit is not None:
|
||||
should_exit, reason = self.take_profit.should_exit(
|
||||
ts_code=ts_code,
|
||||
current_price=close,
|
||||
cost_price=pos.cost,
|
||||
high=high,
|
||||
low=low,
|
||||
atr=atr,
|
||||
)
|
||||
if should_exit:
|
||||
quantity = pos.quantity
|
||||
self.sell(ts_code, close, quantity)
|
||||
# logger.info(f"{current_date} 止盈卖出 {ts_code} 数量 {quantity} 价格 {close:.2f} 原因: {reason}") # 已移除:止盈日志改用进度条
|
||||
|
||||
def run_backtest(self, data_dict: Dict[str, pd.DataFrame], calendar: List[str]) -> pd.DataFrame:
|
||||
"""主回测入口。
|
||||
|
||||
参数:
|
||||
- data_dict: {ts_code: DataFrame},每个 df 至少含 ['trade_date', 'open', 'high', 'low', 'close'];
|
||||
- calendar: 统一交易日列表(升序,字符串 YYYYMMDD)。
|
||||
|
||||
返回:
|
||||
- 资金曲线 DataFrame,列为 ['trade_date', 'total_asset', 'cash', 'market_value']。
|
||||
"""
|
||||
from tqdm import tqdm
|
||||
|
||||
logger.info(f"回测开始,共 {len(calendar)} 个交易日")
|
||||
|
||||
# 使用进度条显示回测进度
|
||||
for current_date in tqdm(calendar, desc="回测进度", unit="日"):
|
||||
# 更新持有天数
|
||||
self._update_days_held(current_date, data_dict)
|
||||
|
||||
# 检查止盈止损(在策略决策前)
|
||||
self._check_stop_loss_take_profit(current_date, data_dict)
|
||||
|
||||
# 策略决策
|
||||
try:
|
||||
self.on_bar(current_date, data_dict)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.error(f"on_bar 异常: {e}")
|
||||
|
||||
# 计算当日资产
|
||||
market_value = self._calc_market_value(current_date, data_dict)
|
||||
total_asset = self.cash + market_value
|
||||
|
||||
self.equity_curve.append(
|
||||
{
|
||||
"trade_date": current_date,
|
||||
"total_asset": total_asset,
|
||||
"cash": self.cash,
|
||||
"market_value": market_value,
|
||||
}
|
||||
)
|
||||
|
||||
logger.info("回测结束")
|
||||
return pd.DataFrame(self.equity_curve)
|
||||
193
strategies/ma_cross.py
Normal file
193
strategies/ma_cross.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""示例策略:均线交叉(买入持有 5 天,最多 2 只)。
|
||||
|
||||
策略规则(可通过 config.settings 配置参数):
|
||||
- 使用短期均线 ma_short 和长期均线 ma_long;
|
||||
- 当短期均线向上突破长期均线(金叉)时,若当前仓位未满且无该股持仓,则买入;
|
||||
- 持有达到 hold_days 天,或出现均线死叉(短期下穿长期)时卖出;
|
||||
- 最多持有 max_positions 只股票,按可用资金等权分配买入;
|
||||
- 支持可选的止盈止损和仓位管理(通过 BaseStrategy 集成)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
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 MaCrossStrategy(BaseStrategy):
|
||||
"""均线交叉示例策略。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
initial_cash: float,
|
||||
ma_short: int,
|
||||
ma_long: int,
|
||||
hold_days: int = 5,
|
||||
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,
|
||||
):
|
||||
"""初始化均线交叉策略。
|
||||
|
||||
参数:
|
||||
initial_cash: 初始资金
|
||||
ma_short: 短期均线周期
|
||||
ma_long: 长期均线周期
|
||||
hold_days: 持有天数
|
||||
max_positions: 最大持仓数
|
||||
position_pct_per_stock: 每只个股占总资金的比例(0.2 = 20%)
|
||||
stop_loss: 止损管理器(可选)
|
||||
take_profit: 止盈管理器(可选)
|
||||
position_sizer: 仓位管理器(可选)
|
||||
date_index_dict: 日期索引字典 {ts_code: {date: idx}}
|
||||
buy_signal_index: 买入信号索引 {date: [ts_code1, ts_code2, ...]}
|
||||
"""
|
||||
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.ma_short = int(ma_short)
|
||||
self.ma_long = int(ma_long)
|
||||
self.hold_days = int(hold_days)
|
||||
self.position_pct_per_stock = float(position_pct_per_stock)
|
||||
|
||||
# 输出策略实例化参数(验证参数读取)
|
||||
logger.info(f"MaCrossStrategy 初始化参数: ma_short={self.ma_short}, ma_long={self.ma_long}, "
|
||||
f"hold_days={self.hold_days}, max_positions={max_positions}, "
|
||||
f"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"])
|
||||
|
||||
pos = self.positions[ts_code]
|
||||
|
||||
# 性能优化:直接读取预计算的死叉信号
|
||||
death_cross = bool(row["death_cross"]) if "death_cross" in df.columns else False
|
||||
|
||||
# 满足持有天数或死叉则卖出
|
||||
if pos.days_held >= self.hold_days or death_cross:
|
||||
quantity = pos.quantity
|
||||
if quantity > 0:
|
||||
self.sell(ts_code, close, quantity)
|
||||
# logger.info(f"{current_date} 卖出 {ts_code} 数量 {quantity} 价格 {close}") # 已移除:交易日志改用进度条
|
||||
|
||||
# 2. 再处理买入逻辑(金叉 + 放量,按成交量排序选择前N只)
|
||||
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, volume, price), ...]
|
||||
|
||||
# 只需遍历有买入信号的股票(从3776只减少到99%以上)
|
||||
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]
|
||||
row = data_dict[ts_code].iloc[idx]
|
||||
|
||||
# 满足条件,加入候选列表
|
||||
price = float(row["close"])
|
||||
volume = float(row["vol"])
|
||||
candidates.append((ts_code, volume, price))
|
||||
|
||||
# 第二步:按成交量倒序排序,选择成交量最大的前N只
|
||||
if not candidates:
|
||||
return # 没有满足条件的股票
|
||||
|
||||
# 按成交量降序排序
|
||||
candidates.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
# 计算还能买入几只
|
||||
remain_slots = self.max_positions - len(self.positions)
|
||||
selected = candidates[:remain_slots] # 选择前N只
|
||||
|
||||
# 第三步:依次买入选中的股票
|
||||
for ts_code, volume, price in selected:
|
||||
# 使用仓位管理器计算买入数量(如果有的话)
|
||||
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:
|
||||
# 默认:按配置的比例分配仓位
|
||||
# 目标:使用初始资金的 position_pct_per_stock 比例(例如50%)
|
||||
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:
|
||||
# actual_quantity = self.positions[ts_code].quantity
|
||||
# actual_cost = before_cash - self.cash
|
||||
# logger.info(
|
||||
# f"{current_date} 买入 {ts_code} 数量 {actual_quantity} "
|
||||
# f"价格 {price:.2f} 成本 {actual_cost:.2f} 成交量 {volume:.0f} "
|
||||
# f"剩余现金 {self.cash:.2f} 当前持仓数 {len(self.positions)}/{self.max_positions}"
|
||||
# )
|
||||
242
strategies/ocz_strategy.py
Normal file
242
strategies/ocz_strategy.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""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})"
|
||||
# ) # 已移除:交易日志
|
||||
Reference in New Issue
Block a user