Files
2026-03-28 00:14:34 +08:00

277 lines
10 KiB
Python
Raw Permalink 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.
"""
纯突破量化策略
逻辑: N日新高放量突破买入,严格止损止盈卖出
"""
import pandas as pd
import numpy as np
from typing import Dict, List, Optional
from vnpy.trader.app.ctaStrategy import CtaTemplate
from vnpy.trader.object import BarData, TickData
from vnpy.trader.constant import Direction
class PureBreakoutStrategy(CtaTemplate):
"""
纯突破策略
- 突破N日新高 + 放量买入
- 严格止损(跌破突破最低价5%
- 跟踪止盈(从高点回落10%
- 均线止盈(跌破均线)
- 最长持有到期卖出
- 等权配置,单票不超过5%仓位
"""
# 策略参数
author = "翼德"
parameters = [
"breakout_days", # 突破N日新高
"volume_multiple", # 放量倍数
"stop_loss_pct", # 止损百分比(相对于突破最低价)
"trailing_stop_pct", # 跟踪止盈百分比(从高点回落)
"ma_period", # 均线周期
"max_holding_days", # 最长持有天数
"max_position_pct", # 单票最大仓位百分比
]
# 策略变量
variables = [
"holdings", # 当前持仓信息字典
]
def __init__(self, cta_engine, strategy_name, setting_dict):
super().__init__(cta_engine, strategy_name, setting_dict)
# 默认参数
self.breakout_days = getattr(self, 'breakout_days', 60)
self.volume_multiple = getattr(self, 'volume_multiple', 1.5)
self.stop_loss_pct = getattr(self, 'stop_loss_pct', 0.05)
self.trailing_stop_pct = getattr(self, 'trailing_stop_pct', 0.10)
self.ma_period = getattr(self, 'ma_period', 20)
self.max_holding_days = getattr(self, 'max_holding_days', 60)
self.max_position_pct = getattr(self, 'max_position_pct', 0.05)
# 持仓信息存储
# {
# symbol: {
# 'entry_date': datetime, # 买入日期
# 'entry_price': float, # 买入价格
# 'breakout_low': float, # 突破日最低价(止损用)
# 'highest_price': float, # 持仓以来最高价(跟踪止盈用)
# 'holding_days': int, # 持有天数
# }
# }
self.holdings: Dict[str, dict] = {}
def on_init(self):
"""策略初始化"""
self.write_log(f"纯突破策略初始化,突破天数={self.breakout_days},单票最大仓位={self.max_position_pct*100:.0f}%")
self.load_bar(self.breakout_days + self.max_holding_days) # 加载足够数据计算指标
def on_start(self):
"""策略启动"""
self.write_log("策略启动")
def on_stop(self):
"""策略停止"""
self.write_log("策略停止")
def on_bar(self, bar: BarData):
"""K线推送,主逻辑"""
symbol = bar.vt_symbol
# 更新持仓天数和最高价
if symbol in self.holdings:
self._update_holding(symbol, bar)
# 检查卖出条件
if self._check_exit(symbol, bar):
self._exit_position(symbol, bar)
return
# 如果还没持仓,检查买入条件
if symbol not in self.holdings:
if self._check_entry(symbol, bar):
self._enter_position(symbol, bar)
return
def _update_holding(self, symbol: str, bar: BarData):
"""更新持仓信息"""
hold = self.holdings[symbol]
hold['holding_days'] += 1
# 更新最高价
if bar.close > hold['highest_price']:
hold['highest_price'] = bar.close
def _check_exit(self, symbol: str, bar: BarData) -> bool:
"""检查是否满足卖出条件,满足一个就卖出"""
hold = self.holdings[symbol]
# 条件1: 止损 - 跌破突破最低价的(1-stop_loss_pct)
stop_price = hold['breakout_low'] * (1 - self.stop_loss_pct)
if bar.low <= stop_price:
self.write_log(f"{symbol}: 触发止损,价格{bar.low:.2f} <= 止损价{stop_price:.2f}")
return True
# 条件2: 跟踪止盈 - 从最高点回落超过trailing_stop_pct
trailing_stop = hold['highest_price'] * (1 - self.trailing_stop_pct)
if bar.close <= trailing_stop:
self.write_log(f"{symbol}: 触发跟踪止盈,现价{bar.close:.2f} <= 止盈价{trailing_stop:.2f},最高{hold['highest_price']:.2f}")
return True
# 条件3: 均线止盈 - 收盘价跌破均线
ma = self.calculate_ma(symbol, self.ma_period)
if ma is not None and bar.close < ma:
self.write_log(f"{symbol}: 触发均线止盈,现价{bar.close:.2f} < MA{self.ma_period}={ma:.2f}")
return True
# 条件4: 最长持有天数到期
if hold['holding_days'] >= self.max_holding_days:
self.write_log(f"{symbol}: 持有到期{self.max_holding_days}天,卖出")
return True
# 都不满足,继续持有
return False
def _check_entry(self, symbol: str, bar: BarData) -> bool:
"""检查是否满足买入条件"""
# 检查是否已经达到单票最大仓位限制(整个策略层面)
current_total = self.calculate_current_total_position()
if current_total >= self.balance * self.max_position_pct:
# 已经有仓位达到上限,不开新仓
return False
# 条件1: 收盘价创N日新高
if not self._is_new_high(symbol, bar.close):
return False
# 条件2: 成交量放量相对于N日均量
if not self._is_volume_enough(symbol, bar.volume):
return False
# 两个条件都满足,可以买入
self.write_log(f"{symbol}: 满足突破条件,N={self.breakout_days}日新高,放量")
return True
def _is_new_high(self, symbol: str, current_close: float) -> bool:
"""判断是否创N日新高"""
# 获取最近N天的收盘价(不包含今天)
bars = self.get_bars(symbol, self.breakout_days)
if len(bars) < self.breakout_days:
return False # 数据不够,不操作
# 计算N日最高收盘价
highest = max(b.high for b in bars)
# 当前收盘价创新高
return current_close > highest
def _is_volume_enough(self, symbol: str, current_volume: float) -> bool:
"""判断成交量是否足够放量"""
# 获取最近N天的成交量
bars = self.get_bars(symbol, self.breakout_days)
if len(bars) < self.breakout_days:
return False
# 计算平均成交量
avg_volume = np.mean([b.volume for b in bars])
# 当前成交量大于等于倍数 * 平均
return current_volume >= avg_volume * self.volume_multiple
def calculate_ma(self, symbol: str, period: int) -> Optional[float]:
"""计算均线"""
bars = self.get_bars(symbol, period)
if len(bars) < period:
return None
close_list = [b.close for b in bars]
return np.mean(close_list)
def calculate_current_total_position(self) -> float:
"""计算当前总持仓市值"""
total = 0.0
for symbol, hold in self.holdings.items():
# 获取最新价格
last_bar = self.get_last_bar(symbol)
if last_bar:
# 持仓数量 * 最新价格
position = self.get_position(symbol)
if position:
total += abs(position.volume) * last_bar.close
return total
def _enter_position(self, symbol: str, bar: BarData):
"""开仓买入"""
# 计算目标仓位: 最大仓位对应市值
target_value = self.balance * self.max_position_pct
# 计算买入股数(A股100股整数)
price = bar.open # 突破次日开盘买入,如果是当日就是开盘
if price <= 0:
price = bar.close
target_volume = int(target_value / price / 100) * 100
if target_volume <= 0:
self.write_log(f"{symbol}: 计算得到目标股数{target_volume},不买入")
return
# 买入
self.buy(symbol, price, target_volume)
# 记录持仓信息
self.holdings[symbol] = {
'entry_date': bar.datetime,
'entry_price': price,
'breakout_low': bar.low, # 突破日最低价作为止损基准
'highest_price': bar.close, # 最高价从买入后开始计算
'holding_days': 0, # 买入当日算0天
}
self.write_log(f"{symbol}: 买入成功,数量{target_volume},价格{price:.2f},止损{bar.low*(1-self.stop_loss_pct):.2f}")
self.put_order()
def _exit_position(self, symbol: str, bar: BarData):
"""平仓卖出"""
position = self.get_position(symbol)
if position is None or position.volume <= 0:
del self.holdings[symbol]
return
# 全部卖出
price = bar.close
self.sell(symbol, price, abs(position.volume))
# 计算盈亏
hold = self.holdings[symbol]
pnl = (price - hold['entry_price']) * position.volume
pct = (price - hold['entry_price']) / hold['entry_price'] * 100
self.write_log(f"{symbol}: 卖出成功,盈亏{pnl:.2f} ({pct:.2f}%),持有{hold['holding_days']}")
# 从持仓记录删除
del self.holdings[symbol]
self.put_order()
def get_bars(self, symbol: str, days: int):
"""获取最近N根Bar(不包含当前Bar"""
# 这个接口根据vn.py实际使用调整,这里给出标准接口
return self.cta_engine.get_bars(symbol, days)
def get_last_bar(self, symbol: str):
"""获取最新一根Bar"""
bars = self.get_bars(symbol, 1)
if bars:
return bars[-1]
return None
def get_position(self, symbol: str):
"""获取当前持仓"""
return self.cta_engine.get_position(symbol)