20PRExp-3 突破策略
20PRExp-3 是一个基于日内通道的突破策略。它在每根完成的 5 分钟K线上重新计算当日的最高价、最低价与中线,并利用 30 分钟周期的成交量放大来确认动能。只有当价格有效突破最新通道边界时才会开仓。进场后策略继续跟踪 Parabolic SAR、动态跟踪止损以及基于风险百分比的仓位管理,与原始的 MetaTrader 5 EA 保持一致。
思路概述
- 日内通道:持续维护当日的最高、最低及中间价位。
- 突破确认:仅当收盘价位于通道外侧且日内振幅超过
GapPoints * PriceStep时考虑进场。 - 成交量过滤:比较最近两根完成的 30 分钟K线的tick成交量,必须放大到至少 1.5 倍。
- 时间过滤:
SessionStartHour之前不允许开新仓,以规避夜间低流动性波动。 - 风险对称:多头止损设置在当日低点,空头止损设置在当日高点。止盈与追踪止损的距离均以价格点数衡量。
所需数据
- 5 分钟K线:用于生成交易信号并计算 Parabolic SAR。
- 30 分钟K线:用于成交量放大过滤。
- 日内高低点通过 5 分钟数据实时计算,无需单独订阅日线。
入场规则
- 等待一根完成的 5 分钟K线且当前时间已经过启动小时。
- 更新当日高点、低点、中线以及通道宽度。
- 检查通道宽度是否大于
GapPoints * PriceStep。 - 计算成交量比例 = 最近完成的 30 分钟成交量 / 前一根 30 分钟成交量,要求大于 1.5。
- 做多:收盘价位于或高于当前日高 → 开多仓。
- 做空:收盘价位于或低于当前日低 → 开空仓。
- 同一时间只允许一笔持仓,已有持仓时不再开新仓。
仓位管理
- 初始止损:多头使用日内低点,空头使用日内高点(在进场时锁定)。
- 止盈:可选,距离为
TakeProfitPoints * PriceStep。 - Parabolic SAR 反转:当 SAR 穿越上一根K线的收盘价时立即平仓。
- 追踪止损:在浮盈超过
TrailingStopPoints * PriceStep后启动,仅当价格进一步前进TrailingStepPoints * PriceStep时才上移。 - 镜像式止盈:每次上移追踪止损时,同时将止盈调整到距离当前收盘价相同的另一侧。
风险控制
- 通过
RiskPercent根据账户当前权益和止损距离计算下单量。 - 如无法获得账户权益,则退化为使用
Volume + |Position|,若仍不可用则最少交易 1 手。
参数说明
| 参数 | 默认值 | 说明 |
|---|---|---|
CandleType |
5 分钟K线 | 信号与 Parabolic SAR 的主时间框架。 |
VolumeCandleType |
30 分钟K线 | 用于成交量过滤的时间框架。 |
TakeProfitPoints |
20 | 止盈距离(点)。设为 0 可关闭止盈。 |
TrailingStopPoints |
10 | 启动追踪止损所需的点数。 |
TrailingStepPoints |
10 | 再次上调追踪止损所需的额外点数。 |
RiskPercent |
5 | 每笔交易可承受的权益百分比损失。 |
GapPoints |
50 | 开启突破交易所需的最小日内通道宽度。 |
SessionStartHour |
7 | 允许开仓的最早小时(0–23)。 |
额外说明
- Parabolic SAR 的加速因子(0.005)与最大值(0.01)保持与原EA一致。
- 计算中的日内中线可用于图表展示,帮助观察价格在通道中的位置。
- 成交量过滤依赖已完成的 30 分钟数据,因此在回测和实时环境中都具有稳定性。
- 代码内的注释均使用英文,符合仓库规范。
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// 20PRExp-3 breakout strategy ported from MetaTrader 5.
/// Tracks the current day's range, waits for volume expansion, and trades breakouts beyond the high or low.
/// </summary>
public class TwentyPrExpThreeStrategy : Strategy
{
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<decimal> _trailingStepPoints;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<decimal> _gapPoints;
private readonly StrategyParam<int> _sessionStartHour;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<DataType> _volumeCandleType;
// Daily levels that are recalculated every trading day.
private decimal _dailyHigh;
private decimal _dailyLow;
private decimal _dailyMid;
private decimal _dailyRange;
private DateTime _currentDay;
// Previous candle close needed for Parabolic SAR exit condition.
private decimal _previousClose;
private bool _hasPreviousClose;
// Last two 30-minute volumes for expansion filter.
private decimal _currentVolumeBar;
private decimal _previousVolumeBar;
// Position management state.
private decimal _longEntryPrice;
private decimal _longStop;
private decimal _longTake;
private decimal _shortEntryPrice;
private decimal _shortStop;
private decimal _shortTake;
/// <summary>
/// Take profit distance in price points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Trailing stop distance in price points.
/// </summary>
public decimal TrailingStopPoints
{
get => _trailingStopPoints.Value;
set => _trailingStopPoints.Value = value;
}
/// <summary>
/// Minimum progress in points before the trailing stop is moved again.
/// </summary>
public decimal TrailingStepPoints
{
get => _trailingStepPoints.Value;
set => _trailingStepPoints.Value = value;
}
/// <summary>
/// Percentage of portfolio equity to risk per trade.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Minimum daily channel width in points before breakouts are allowed.
/// </summary>
public decimal GapPoints
{
get => _gapPoints.Value;
set => _gapPoints.Value = value;
}
/// <summary>
/// Hour (0-23) after which new positions are allowed.
/// </summary>
public int SessionStartHour
{
get => _sessionStartHour.Value;
set => _sessionStartHour.Value = value;
}
/// <summary>
/// Primary candle type used for signals.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Higher timeframe candle type used for the volume filter.
/// </summary>
public DataType VolumeCandleType
{
get => _volumeCandleType.Value;
set => _volumeCandleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="TwentyPrExpThreeStrategy"/>.
/// </summary>
public TwentyPrExpThreeStrategy()
{
_takeProfitPoints = Param(nameof(TakeProfitPoints), 20m)
.SetDisplay("Take Profit (pts)", "Target distance in points", "Risk Management")
;
_trailingStopPoints = Param(nameof(TrailingStopPoints), 10m)
.SetDisplay("Trailing Stop (pts)", "Trailing stop distance", "Risk Management")
;
_trailingStepPoints = Param(nameof(TrailingStepPoints), 10m)
.SetDisplay("Trailing Step (pts)", "Minimum advance before moving trailing stop", "Risk Management")
;
_riskPercent = Param(nameof(RiskPercent), 5m)
.SetDisplay("Risk %", "Portfolio percentage to risk per trade", "Position Sizing")
;
_gapPoints = Param(nameof(GapPoints), 100m)
.SetDisplay("Range Filter (pts)", "Minimum daily range in points", "Filters")
;
_sessionStartHour = Param(nameof(SessionStartHour), 12)
.SetDisplay("Session Start Hour", "Hour after which breakout trades are enabled", "Filters");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe for the strategy", "General");
_volumeCandleType = Param(nameof(VolumeCandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Volume Candle Type", "Higher timeframe for tick volume filter", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType), (Security, VolumeCandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_dailyHigh = 0m;
_dailyLow = 0m;
_dailyMid = 0m;
_dailyRange = 0m;
_currentDay = default;
_previousClose = 0m;
_hasPreviousClose = false;
_currentVolumeBar = 0m;
_previousVolumeBar = 0m;
ResetLongState();
ResetShortState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Parabolic SAR parameters mirror the original expert advisor values.
var parabolicSar = new ParabolicSar
{
Acceleration = 0.005m,
AccelerationMax = 0.01m
};
var mainSubscription = SubscribeCandles(CandleType);
mainSubscription
.Bind(parabolicSar, ProcessMainCandle)
.Start();
var volumeSubscription = SubscribeCandles(VolumeCandleType);
volumeSubscription
.Bind(ProcessVolumeCandle)
.Start();
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent));
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, mainSubscription);
DrawIndicator(area, parabolicSar);
DrawOwnTrades(area);
}
}
private void ProcessVolumeCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
// Shift the last two finished 30-minute volumes to approximate tick volume expansion.
_previousVolumeBar = _currentVolumeBar;
_currentVolumeBar = candle.TotalVolume;
}
private void ProcessMainCandle(ICandleMessage candle, decimal sarValue)
{
if (candle.State != CandleStates.Finished)
return;
UpdateDailyLevels(candle);
if (Position != 0)
{
UpdatePreviousClose(candle);
return;
}
var signal = GetTradeSignal(candle);
if (signal > 0)
BuyMarket();
else if (signal < 0)
SellMarket();
UpdatePreviousClose(candle);
}
private void UpdateDailyLevels(ICandleMessage candle)
{
var candleDay = candle.OpenTime.Date;
if (_currentDay != candleDay)
{
_currentDay = candleDay;
_dailyHigh = candle.HighPrice;
_dailyLow = candle.LowPrice;
}
else
{
if (candle.HighPrice > _dailyHigh)
_dailyHigh = candle.HighPrice;
if (_dailyLow == 0m || candle.LowPrice < _dailyLow)
_dailyLow = candle.LowPrice;
}
_dailyMid = (_dailyHigh + _dailyLow) / 2m;
_dailyRange = _dailyHigh - _dailyLow;
}
private void ManageOpenPosition(ICandleMessage candle, decimal sarValue)
{
if (Position > 0)
{
// Close longs when Parabolic SAR crosses above the previous close.
if (_hasPreviousClose && sarValue > _previousClose)
{
if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
ResetLongState();
ResetShortState();
return;
}
UpdateLongTrailing(candle);
CheckLongTargets(candle);
}
else if (Position < 0)
{
// Close shorts when Parabolic SAR crosses below the previous close.
if (_hasPreviousClose && sarValue < _previousClose)
{
if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
ResetLongState();
ResetShortState();
return;
}
UpdateShortTrailing(candle);
CheckShortTargets(candle);
}
}
private void UpdateLongTrailing(ICandleMessage candle)
{
if (TrailingStopPoints <= 0m || _longEntryPrice <= 0m)
return;
var pointValue = GetPointValue();
var trailingDistance = TrailingStopPoints * pointValue;
if (trailingDistance <= 0m)
return;
var profit = candle.ClosePrice - _longEntryPrice;
if (profit <= trailingDistance)
return;
var newStop = candle.ClosePrice - trailingDistance;
var minStep = TrailingStepPoints > 0m ? TrailingStepPoints * pointValue : 0m;
if (_longStop > 0m && minStep > 0m && newStop - _longStop < minStep)
return;
_longStop = newStop;
_longTake = TrailingStopPoints > 0m ? candle.ClosePrice + trailingDistance : _longTake;
}
private void UpdateShortTrailing(ICandleMessage candle)
{
if (TrailingStopPoints <= 0m || _shortEntryPrice <= 0m)
return;
var pointValue = GetPointValue();
var trailingDistance = TrailingStopPoints * pointValue;
if (trailingDistance <= 0m)
return;
var profit = _shortEntryPrice - candle.ClosePrice;
if (profit <= trailingDistance)
return;
var newStop = candle.ClosePrice + trailingDistance;
var minStep = TrailingStepPoints > 0m ? TrailingStepPoints * pointValue : 0m;
if (_shortStop > 0m && minStep > 0m && _shortStop - newStop < minStep)
return;
_shortStop = newStop;
_shortTake = TrailingStopPoints > 0m ? candle.ClosePrice - trailingDistance : _shortTake;
}
private void CheckLongTargets(ICandleMessage candle)
{
var position = Position;
if (position <= 0m)
return;
if (_longStop > 0m && candle.LowPrice <= _longStop)
{
SellMarket();
ResetLongState();
return;
}
if (_longTake > 0m && candle.HighPrice >= _longTake)
{
SellMarket();
ResetLongState();
}
}
private void CheckShortTargets(ICandleMessage candle)
{
var position = Position;
if (position >= 0m)
return;
var volume = Math.Abs(position);
if (_shortStop > 0m && candle.HighPrice >= _shortStop)
{
BuyMarket();
ResetShortState();
return;
}
if (_shortTake > 0m && candle.LowPrice <= _shortTake)
{
BuyMarket();
ResetShortState();
}
}
private int GetTradeSignal(ICandleMessage candle)
{
var pointValue = GetPointValue();
var rangeThreshold = GapPoints * pointValue;
var hasRange = _dailyRange > 0m && _dailyRange > rangeThreshold;
var hasVolumeHistory = _previousVolumeBar > 0m && _currentVolumeBar > 0m;
var volumeRatio = hasVolumeHistory ? _currentVolumeBar / _previousVolumeBar : 0m;
if (!hasRange)
return 0;
if (candle.ClosePrice >= _dailyHigh && _dailyHigh > 0m)
return 1;
if (candle.ClosePrice <= _dailyLow && _dailyLow > 0m)
return -1;
return 0;
}
private void TryEnterLong(decimal entryPrice)
{
if (_dailyLow <= 0m)
return;
var stopPrice = _dailyLow;
var stopDistance = entryPrice - stopPrice;
if (stopDistance <= 0m)
return;
var volume = CalculatePositionSize(stopDistance);
if (volume <= 0m)
return;
BuyMarket();
_longEntryPrice = entryPrice;
_longStop = stopPrice;
_longTake = TakeProfitPoints > 0m ? entryPrice + TakeProfitPoints * GetPointValue() : 0m;
ResetShortState();
}
private void TryEnterShort(decimal entryPrice)
{
if (_dailyHigh <= 0m)
return;
var stopPrice = _dailyHigh;
var stopDistance = stopPrice - entryPrice;
if (stopDistance <= 0m)
return;
var volume = CalculatePositionSize(stopDistance);
if (volume <= 0m)
return;
SellMarket();
_shortEntryPrice = entryPrice;
_shortStop = stopPrice;
_shortTake = TakeProfitPoints > 0m ? entryPrice - TakeProfitPoints * GetPointValue() : 0m;
ResetLongState();
}
private decimal CalculatePositionSize(decimal stopDistance)
{
if (stopDistance <= 0m)
return 0m;
var portfolioValue = Portfolio?.CurrentValue ?? 0m;
var riskFraction = RiskPercent / 100m;
if (riskFraction > 0m && portfolioValue > 0m)
{
var riskAmount = portfolioValue * riskFraction;
var sized = riskAmount / stopDistance;
if (sized > 0m)
return sized;
}
var fallback = Volume + Math.Abs(Position);
return fallback > 0m ? fallback : 1m;
}
private decimal GetPointValue()
{
var step = Security?.PriceStep;
return step.HasValue && step.Value > 0m ? step.Value : 1m;
}
private void UpdatePreviousClose(ICandleMessage candle)
{
_previousClose = candle.ClosePrice;
_hasPreviousClose = true;
}
private void ResetLongState()
{
_longEntryPrice = 0m;
_longStop = 0m;
_longTake = 0m;
}
private void ResetShortState()
{
_shortEntryPrice = 0m;
_shortStop = 0m;
_shortTake = 0m;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import ParabolicSar
from StockSharp.Algo.Strategies import Strategy
class twenty_pr_exp_three_strategy(Strategy):
def __init__(self):
super(twenty_pr_exp_three_strategy, self).__init__()
self._take_profit_points = self.Param("TakeProfitPoints", 20.0)
self._trailing_stop_points = self.Param("TrailingStopPoints", 10.0)
self._trailing_step_points = self.Param("TrailingStepPoints", 10.0)
self._risk_percent = self.Param("RiskPercent", 5.0)
self._gap_points = self.Param("GapPoints", 100.0)
self._session_start_hour = self.Param("SessionStartHour", 12)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._volume_candle_type = self.Param("VolumeCandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._daily_high = 0.0
self._daily_low = 0.0
self._daily_mid = 0.0
self._daily_range = 0.0
self._current_day = None
self._previous_close = 0.0
self._has_previous_close = False
self._current_volume_bar = 0.0
self._previous_volume_bar = 0.0
self._long_entry_price = 0.0
self._long_stop = 0.0
self._long_take = 0.0
self._short_entry_price = 0.0
self._short_stop = 0.0
self._short_take = 0.0
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@TakeProfitPoints.setter
def TakeProfitPoints(self, value):
self._take_profit_points.Value = value
@property
def TrailingStopPoints(self):
return self._trailing_stop_points.Value
@TrailingStopPoints.setter
def TrailingStopPoints(self, value):
self._trailing_stop_points.Value = value
@property
def TrailingStepPoints(self):
return self._trailing_step_points.Value
@TrailingStepPoints.setter
def TrailingStepPoints(self, value):
self._trailing_step_points.Value = value
@property
def RiskPercent(self):
return self._risk_percent.Value
@RiskPercent.setter
def RiskPercent(self, value):
self._risk_percent.Value = value
@property
def GapPoints(self):
return self._gap_points.Value
@GapPoints.setter
def GapPoints(self, value):
self._gap_points.Value = value
@property
def SessionStartHour(self):
return self._session_start_hour.Value
@SessionStartHour.setter
def SessionStartHour(self, value):
self._session_start_hour.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def VolumeCandleType(self):
return self._volume_candle_type.Value
@VolumeCandleType.setter
def VolumeCandleType(self, value):
self._volume_candle_type.Value = value
def _get_point_value(self):
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if step <= 0.0:
step = 1.0
return step
def OnStarted2(self, time):
super(twenty_pr_exp_three_strategy, self).OnStarted2(time)
self._daily_high = 0.0
self._daily_low = 0.0
self._daily_mid = 0.0
self._daily_range = 0.0
self._current_day = None
self._previous_close = 0.0
self._has_previous_close = False
self._current_volume_bar = 0.0
self._previous_volume_bar = 0.0
self._reset_long_state()
self._reset_short_state()
sar = ParabolicSar()
sar.Acceleration = 0.005
sar.AccelerationMax = 0.01
main_sub = self.SubscribeCandles(self.CandleType)
main_sub.Bind(sar, self._process_main_candle).Start()
volume_sub = self.SubscribeCandles(self.VolumeCandleType)
volume_sub.Bind(self._process_volume_candle).Start()
self.StartProtection(
Unit(2, UnitTypes.Percent),
Unit(1, UnitTypes.Percent))
def _process_volume_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._previous_volume_bar = self._current_volume_bar
self._current_volume_bar = float(candle.TotalVolume)
def _process_main_candle(self, candle, sar_value):
if candle.State != CandleStates.Finished:
return
self._update_daily_levels(candle)
if self.Position != 0:
self._update_previous_close(candle)
return
signal = self._get_trade_signal(candle)
if signal > 0:
self.BuyMarket()
elif signal < 0:
self.SellMarket()
self._update_previous_close(candle)
def _update_daily_levels(self, candle):
candle_day = candle.OpenTime.Date
if self._current_day is None or self._current_day != candle_day:
self._current_day = candle_day
self._daily_high = float(candle.HighPrice)
self._daily_low = float(candle.LowPrice)
else:
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if high > self._daily_high:
self._daily_high = high
if self._daily_low == 0.0 or low < self._daily_low:
self._daily_low = low
self._daily_mid = (self._daily_high + self._daily_low) / 2.0
self._daily_range = self._daily_high - self._daily_low
def _get_trade_signal(self, candle):
point_value = self._get_point_value()
range_threshold = float(self.GapPoints) * point_value
has_range = self._daily_range > 0.0 and self._daily_range > range_threshold
close = float(candle.ClosePrice)
if not has_range:
return 0
if close >= self._daily_high and self._daily_high > 0.0:
return 1
if close <= self._daily_low and self._daily_low > 0.0:
return -1
return 0
def _update_previous_close(self, candle):
self._previous_close = float(candle.ClosePrice)
self._has_previous_close = True
def _reset_long_state(self):
self._long_entry_price = 0.0
self._long_stop = 0.0
self._long_take = 0.0
def _reset_short_state(self):
self._short_entry_price = 0.0
self._short_stop = 0.0
self._short_take = 0.0
def OnReseted(self):
super(twenty_pr_exp_three_strategy, self).OnReseted()
self._daily_high = 0.0
self._daily_low = 0.0
self._daily_mid = 0.0
self._daily_range = 0.0
self._current_day = None
self._previous_close = 0.0
self._has_previous_close = False
self._current_volume_bar = 0.0
self._previous_volume_bar = 0.0
self._reset_long_state()
self._reset_short_state()
def CreateClone(self):
return twenty_pr_exp_three_strategy()