Martingale Breakout 策略
概览
Martingale Breakout Strategy 是 MetaTrader 智能交易系统 MartinGaleBreakout.mq5 的 StockSharp 移植版本。策略等待
异常放大的突破 K 线,只在突破方向开出一笔市价单。原始 EA 通过“魔术号”区分仓位,而在 StockSharp 中同样的逻辑由
策略自身上下文提供,因此行为完全一致。
系统依赖两个核心思想:
- 突破检测:对每根完结 K 线计算区间(
High - Low),并与前十根 K 线的平均区间比较。如果当前区间大于平均值的 三倍,同时 K 线实体占据整体区间的一半以上,则认为出现突破信号。 - 马丁格尔式恢复:持续跟踪浮动盈亏。一旦浮盈亏突破设定的亏损阈值,立即平掉全部仓位,并把下一次的盈利目标 提高到足以弥补刚才的亏损;当扩大的目标被实现后,各个阈值恢复到初始状态。
移植版本保留了所有资金管理参数:可用于保证金的账户比例、以余额百分比表示的止盈/止损以及恢复阶段放大止盈距离的 乘数。
交易逻辑
- 订阅指定类型的 K 线,并仅处理状态为
Finished的蜡烛。 - 维护一个包含最近十个区间值的环形缓冲区,用作异常检测的参考平均值。
- 根据多头和空头的平均持仓价计算浮动盈亏;当浮盈亏达到获利目标或跌破止损阈值时,立即平仓并重置恢复状态。
- 只要策略已经持仓或连接状态不允许交易,就跳过新信号。
- 出现看涨突破时,根据当前目标收益计算下单手数,使预期利润等于目标。若处于恢复阶段,止盈距离会乘以
RecoveryPointsMultiplier,对应原 EA 中的TP_Points_Multiplier。 - 检查手数是否满足交易所限制(最小值、最大值和步长),并确认所需保证金没有超过允许使用的余额份额以及可用资金。 条件满足后发送买入市价单。
- 看跌突破时执行相同流程,改为发送卖出市价单。
该流程完整复现了原始 MetaTrader 策略的行为,包括触发止损后的恢复模式。
参数
| 参数 | 说明 | 默认值 |
|---|---|---|
TakeProfitPoints |
入场价到止盈价之间的价格步数。 | 50 |
BalancePercentAvailable |
单笔交易可占用的账户余额百分比。 | 50 |
TakeProfitPercentOfBalance |
以余额百分比表示的目标盈利。 | 0.1 |
StopLossPercentOfBalance |
以余额百分比表示的止损幅度。 | 10 |
RecoveryStartFraction |
启动恢复模式前使用的止损比例。 | 0.1 |
RecoveryPointsMultiplier |
恢复阶段用于放大止盈距离的乘数。 | 1 |
CandleType |
用于生成信号的蜡烛数据类型(时间框架、Tick 蜡烛等)。 | 15 分钟蜡烛 |
其他说明
- 手数计算遵循
CalcLotWithTP的思路:根据目标盈利和预期价格波动得到所需手数,然后按照合约的手数步长归一化。 - 保证金校验与 MQL 版本中的
CheckVolumeValue以及余额比例限制保持一致,若所需保证金超过允许范围或账户可用资金, 则放弃下单。 - 在强制平仓前会取消所有挂单,以匹配原脚本
CloseAllOrders的行为。 - 区间缓冲区仅保存十个值,与原程序遍历
iHigh/iLow的结果等价,不需要额外的历史数据。
using System;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Breakout strategy that detects abnormally large candles and enters in the breakout direction.
/// Uses a simple martingale recovery: after a losing trade, the next entry is taken more aggressively.
/// </summary>
public class MartingaleBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _lookback;
private readonly StrategyParam<decimal> _breakoutMultiplier;
private readonly StrategyParam<decimal> _takeProfitPct;
private readonly StrategyParam<decimal> _stopLossPct;
private readonly StrategyParam<DataType> _candleType;
private readonly decimal[] _rangeBuffer = new decimal[10];
private int _rangeBufferCount;
private int _rangeBufferIndex;
private decimal _rangeBufferSum;
private decimal _entryPrice;
private Sides? _entrySide;
private bool _lastWasLoss;
public int Lookback
{
get => _lookback.Value;
set => _lookback.Value = value;
}
public decimal BreakoutMultiplier
{
get => _breakoutMultiplier.Value;
set => _breakoutMultiplier.Value = value;
}
public decimal TakeProfitPct
{
get => _takeProfitPct.Value;
set => _takeProfitPct.Value = value;
}
public decimal StopLossPct
{
get => _stopLossPct.Value;
set => _stopLossPct.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public MartingaleBreakoutStrategy()
{
_lookback = Param(nameof(Lookback), 10)
.SetDisplay("Lookback", "Number of candles for average range", "General");
_breakoutMultiplier = Param(nameof(BreakoutMultiplier), 3m)
.SetDisplay("Breakout Mult", "Multiplier above avg range for breakout", "General");
_takeProfitPct = Param(nameof(TakeProfitPct), 1m)
.SetDisplay("Take Profit %", "Take profit as percentage of entry price", "Trading");
_stopLossPct = Param(nameof(StopLossPct), 0.5m)
.SetDisplay("Stop Loss %", "Stop loss as percentage of entry price", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
.SetDisplay("Candle Type", "Candle type", "General");
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_rangeBufferCount = 0;
_rangeBufferIndex = 0;
_rangeBufferSum = 0m;
_entryPrice = 0m;
_entrySide = null;
_lastWasLoss = false;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var closePrice = candle.ClosePrice;
// Check exit conditions first
if (Position != 0 && _entryPrice > 0)
{
var tp = _lastWasLoss ? TakeProfitPct * 1.5m : TakeProfitPct;
var sl = StopLossPct;
if (_entrySide == Sides.Buy)
{
var pnlPct = (closePrice - _entryPrice) / _entryPrice * 100m;
if (pnlPct >= tp || pnlPct <= -sl)
{
_lastWasLoss = pnlPct < 0;
SellMarket();
_entryPrice = 0;
_entrySide = null;
UpdateRangeStatistics(candle);
return;
}
}
else if (_entrySide == Sides.Sell)
{
var pnlPct = (_entryPrice - closePrice) / _entryPrice * 100m;
if (pnlPct >= tp || pnlPct <= -sl)
{
_lastWasLoss = pnlPct < 0;
BuyMarket();
_entryPrice = 0;
_entrySide = null;
UpdateRangeStatistics(candle);
return;
}
}
}
// Entry logic - only when flat
if (Position == 0)
{
var range = candle.HighPrice - candle.LowPrice;
if (_rangeBufferCount >= _rangeBuffer.Length)
{
var avgRange = _rangeBufferSum / _rangeBuffer.Length;
if (range > avgRange * BreakoutMultiplier)
{
var body = candle.ClosePrice - candle.OpenPrice;
if (body > 0 && body > range * 0.4m)
{
// Bullish breakout
BuyMarket();
_entryPrice = closePrice;
_entrySide = Sides.Buy;
}
else if (body < 0 && Math.Abs(body) > range * 0.4m)
{
// Bearish breakout
SellMarket();
_entryPrice = closePrice;
_entrySide = Sides.Sell;
}
}
}
}
UpdateRangeStatistics(candle);
}
private void UpdateRangeStatistics(ICandleMessage candle)
{
var range = candle.HighPrice - candle.LowPrice;
if (_rangeBufferCount < _rangeBuffer.Length)
{
_rangeBuffer[_rangeBufferIndex] = range;
_rangeBufferSum += range;
_rangeBufferCount++;
_rangeBufferIndex = (_rangeBufferIndex + 1) % _rangeBuffer.Length;
return;
}
_rangeBufferSum -= _rangeBuffer[_rangeBufferIndex];
_rangeBuffer[_rangeBufferIndex] = range;
_rangeBufferSum += range;
_rangeBufferIndex = (_rangeBufferIndex + 1) % _rangeBuffer.Length;
}
/// <inheritdoc />
protected override void OnReseted()
{
Array.Clear(_rangeBuffer);
_rangeBufferCount = 0;
_rangeBufferIndex = 0;
_rangeBufferSum = 0m;
_entryPrice = 0m;
_entrySide = null;
_lastWasLoss = false;
base.OnReseted();
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class martingale_breakout_strategy(Strategy):
def __init__(self):
super(martingale_breakout_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(60)))
self._lookback = self.Param("Lookback", 10)
self._breakout_multiplier = self.Param("BreakoutMultiplier", 3.0)
self._take_profit_pct = self.Param("TakeProfitPct", 1.0)
self._stop_loss_pct = self.Param("StopLossPct", 0.5)
self._range_buffer = [0.0] * 10
self._range_buffer_count = 0
self._range_buffer_index = 0
self._range_buffer_sum = 0.0
self._entry_price = 0.0
self._entry_side = 0
self._last_was_loss = False
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def Lookback(self):
return self._lookback.Value
@Lookback.setter
def Lookback(self, value):
self._lookback.Value = value
@property
def BreakoutMultiplier(self):
return self._breakout_multiplier.Value
@BreakoutMultiplier.setter
def BreakoutMultiplier(self, value):
self._breakout_multiplier.Value = value
@property
def TakeProfitPct(self):
return self._take_profit_pct.Value
@TakeProfitPct.setter
def TakeProfitPct(self, value):
self._take_profit_pct.Value = value
@property
def StopLossPct(self):
return self._stop_loss_pct.Value
@StopLossPct.setter
def StopLossPct(self, value):
self._stop_loss_pct.Value = value
def OnReseted(self):
super(martingale_breakout_strategy, self).OnReseted()
self._range_buffer = [0.0] * 10
self._range_buffer_count = 0
self._range_buffer_index = 0
self._range_buffer_sum = 0.0
self._entry_price = 0.0
self._entry_side = 0
self._last_was_loss = False
def OnStarted2(self, time):
super(martingale_breakout_strategy, self).OnStarted2(time)
self._range_buffer = [0.0] * 10
self._range_buffer_count = 0
self._range_buffer_index = 0
self._range_buffer_sum = 0.0
self._entry_price = 0.0
self._entry_side = 0
self._last_was_loss = False
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _update_range_statistics(self, candle):
r = float(candle.HighPrice) - float(candle.LowPrice)
buf_len = len(self._range_buffer)
if self._range_buffer_count < buf_len:
self._range_buffer[self._range_buffer_index] = r
self._range_buffer_sum += r
self._range_buffer_count += 1
self._range_buffer_index = (self._range_buffer_index + 1) % buf_len
return
self._range_buffer_sum -= self._range_buffer[self._range_buffer_index]
self._range_buffer[self._range_buffer_index] = r
self._range_buffer_sum += r
self._range_buffer_index = (self._range_buffer_index + 1) % buf_len
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
buf_len = len(self._range_buffer)
# Check exit conditions first
if self.Position != 0 and self._entry_price > 0:
tp = float(self.TakeProfitPct) * 1.5 if self._last_was_loss else float(self.TakeProfitPct)
sl = float(self.StopLossPct)
if self._entry_side == 1:
pnl_pct = (close - self._entry_price) / self._entry_price * 100.0
if pnl_pct >= tp or pnl_pct <= -sl:
self._last_was_loss = pnl_pct < 0
self.SellMarket()
self._entry_price = 0.0
self._entry_side = 0
self._update_range_statistics(candle)
return
elif self._entry_side == -1:
pnl_pct = (self._entry_price - close) / self._entry_price * 100.0
if pnl_pct >= tp or pnl_pct <= -sl:
self._last_was_loss = pnl_pct < 0
self.BuyMarket()
self._entry_price = 0.0
self._entry_side = 0
self._update_range_statistics(candle)
return
# Entry logic - only when flat
if self.Position == 0:
r = float(candle.HighPrice) - float(candle.LowPrice)
if self._range_buffer_count >= buf_len:
avg_range = self._range_buffer_sum / buf_len
if r > avg_range * float(self.BreakoutMultiplier):
body = float(candle.ClosePrice) - float(candle.OpenPrice)
if body > 0 and body > r * 0.4:
self.BuyMarket()
self._entry_price = close
self._entry_side = 1
elif body < 0 and abs(body) > r * 0.4:
self.SellMarket()
self._entry_price = close
self._entry_side = -1
self._update_range_statistics(candle)
def CreateClone(self):
return martingale_breakout_strategy()