MartinGale 突破策略
概览
MartinGale Breakout Strategy 是从 MetaTrader 4 专家顾问 MartinGaleBreakout 移植到 StockSharp 的策略。策略监控选定周期内的价格区间,一旦发现异常放大的 K 线并出现单边收盘,就顺势开仓。若浮亏超过设定阈值,将进入恢复模式,通过拉大止盈距离来补回之前的亏损,模拟原始 EA 的马丁式资金管理。
策略默认使用 15 分钟 K 线,并持续维护最近 11 根已完成的 K 线。当当前 K 线的区间大于前十根 K 线平均区间的三倍且收盘价位于主导半区时,生成突破信号并在该方向开仓。仓位大小根据目标盈利金额和品种的最小报价单位计算,同时遵守可用资金比例限制。
交易流程
- 订阅指定类型的 K 线序列。
- 保存最近 11 根已完成的 K 线并计算平均波动区间。
- 当满足“上涨突破”条件(区间放大且收阳)时开多单。
- 当满足“下跌突破”条件(区间放大且收阴)时开空单。
- 开仓前确保没有持仓,且预计占用资金不超过账户余额的限定百分比。
- 持仓期间监控浮动盈亏,达到止盈或止损阈值时平仓并重置目标。
- 若触发止损:
- 市价平掉当前仓位。
- 将止盈金额增加至覆盖亏损。
- 将止损额度调整为最大允许值并进入恢复模式。
- 恢复模式达到新的止盈目标后,恢复默认参数继续交易。
参数
| 参数 | 说明 | 默认值 |
|---|---|---|
TakeProfitPoints |
基础止盈距离(以最小价格单位计)。 | 50 |
BalancePercentageAvailable |
单笔交易可使用的账户余额比例上限。 | 50% |
TakeProfitBalancePercent |
止盈目标占账户余额的百分比。 | 0.1% |
StopLossBalancePercent |
触发恢复模式前允许的最大回撤比例。 | 10% |
StartRecoveryFactor |
开启恢复模式时使用的止损比例系数。 | 0.2 |
TakeProfitPointsMultiplier |
恢复模式下的止盈距离倍数。 | 1 |
CandleType |
用于计算突破的 K 线类型。 | 15 分钟 |
风险控制
- 根据目标盈利金额、最小跳动价位以及每跳价值计算仓位大小。
- 仓位会根据交易所的最小、最大以及步进要求进行规范化处理。
- 估算开仓所需资金,若超过允许的余额比例则放弃入场。
- 恢复模式只维护单一仓位,通过拉大止盈距离来逐步弥补亏损,避免无限加仓。
提示
- 策略需要可用的投资组合数据来评估余额,请确保连接到实际或仿真账户。
- 手续费通过浮动盈亏间接反映,无需单独设置。
- 全部操作采用市价单实现,不会创建挂单。
using System;
using System.Collections.Generic;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Breakout strategy with martingale-style recovery.
/// Detects abnormally large candles relative to recent history and enters in the breakout direction.
/// After a stop-loss, enters recovery mode with a wider take-profit target.
/// </summary>
public class MartinGaleBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _requiredHistory;
private readonly StrategyParam<decimal> _breakoutFactor;
private readonly StrategyParam<decimal> _takeProfitPct;
private readonly StrategyParam<decimal> _stopLossPct;
private readonly StrategyParam<decimal> _recoveryMultiplier;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _ranges = new();
private decimal _entryPrice;
private Sides? _entrySide;
private bool _recovering;
public int RequiredHistory
{
get => _requiredHistory.Value;
set => _requiredHistory.Value = value;
}
public decimal BreakoutFactor
{
get => _breakoutFactor.Value;
set => _breakoutFactor.Value = value;
}
public decimal TakeProfitPct
{
get => _takeProfitPct.Value;
set => _takeProfitPct.Value = value;
}
public decimal StopLossPct
{
get => _stopLossPct.Value;
set => _stopLossPct.Value = value;
}
public decimal RecoveryMultiplier
{
get => _recoveryMultiplier.Value;
set => _recoveryMultiplier.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public MartinGaleBreakoutStrategy()
{
_requiredHistory = Param(nameof(RequiredHistory), 10)
.SetDisplay("Lookback", "Number of candles for average range", "General");
_breakoutFactor = Param(nameof(BreakoutFactor), 2.5m)
.SetDisplay("Breakout Factor", "Multiplier for abnormal candle detection", "General");
_takeProfitPct = Param(nameof(TakeProfitPct), 0.5m)
.SetDisplay("TP %", "Take profit percent of entry", "Trading");
_stopLossPct = Param(nameof(StopLossPct), 0.3m)
.SetDisplay("SL %", "Stop loss percent of entry", "Trading");
_recoveryMultiplier = Param(nameof(RecoveryMultiplier), 1.5m)
.SetDisplay("Recovery Mult", "TP multiplier during recovery", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle series", "General");
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_ranges.Clear();
_entryPrice = 0;
_entrySide = null;
_recovering = 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 close = candle.ClosePrice;
var range = candle.HighPrice - candle.LowPrice;
// Check exit
if (Position != 0 && _entryPrice > 0)
{
var tpPct = _recovering ? TakeProfitPct * RecoveryMultiplier : TakeProfitPct;
if (_entrySide == Sides.Buy)
{
var pnl = (close - _entryPrice) / _entryPrice * 100m;
if (pnl >= tpPct || pnl <= -StopLossPct)
{
var wasLoss = pnl < 0;
SellMarket();
_entryPrice = 0;
_entrySide = null;
_recovering = wasLoss;
AddRange(range);
return;
}
}
else if (_entrySide == Sides.Sell)
{
var pnl = (_entryPrice - close) / _entryPrice * 100m;
if (pnl >= tpPct || pnl <= -StopLossPct)
{
var wasLoss = pnl < 0;
BuyMarket();
_entryPrice = 0;
_entrySide = null;
_recovering = wasLoss;
AddRange(range);
return;
}
}
}
// Entry - only when flat
if (Position == 0 && _ranges.Count >= RequiredHistory)
{
decimal sum = 0;
for (int i = 0; i < _ranges.Count; i++)
sum += _ranges[i];
var avgRange = sum / _ranges.Count;
if (avgRange > 0 && range > avgRange * BreakoutFactor)
{
var body = candle.ClosePrice - candle.OpenPrice;
if (body > 0 && body > range * 0.4m)
{
BuyMarket();
_entryPrice = close;
_entrySide = Sides.Buy;
}
else if (body < 0 && Math.Abs(body) > range * 0.4m)
{
SellMarket();
_entryPrice = close;
_entrySide = Sides.Sell;
}
}
}
AddRange(range);
}
private void AddRange(decimal range)
{
_ranges.Add(range);
while (_ranges.Count > RequiredHistory)
_ranges.RemoveAt(0);
}
/// <inheritdoc />
protected override void OnReseted()
{
_ranges.Clear();
_entryPrice = 0;
_entrySide = null;
_recovering = 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 martin_gale_breakout_strategy(Strategy):
def __init__(self):
super(martin_gale_breakout_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._required_history = self.Param("RequiredHistory", 10)
self._breakout_factor = self.Param("BreakoutFactor", 2.5)
self._take_profit_pct = self.Param("TakeProfitPct", 0.5)
self._stop_loss_pct = self.Param("StopLossPct", 0.3)
self._recovery_multiplier = self.Param("RecoveryMultiplier", 1.5)
self._ranges = []
self._entry_price = 0.0
self._entry_side = 0
self._recovering = False
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def RequiredHistory(self):
return self._required_history.Value
@RequiredHistory.setter
def RequiredHistory(self, value):
self._required_history.Value = value
@property
def BreakoutFactor(self):
return self._breakout_factor.Value
@BreakoutFactor.setter
def BreakoutFactor(self, value):
self._breakout_factor.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
@property
def RecoveryMultiplier(self):
return self._recovery_multiplier.Value
@RecoveryMultiplier.setter
def RecoveryMultiplier(self, value):
self._recovery_multiplier.Value = value
def OnReseted(self):
super(martin_gale_breakout_strategy, self).OnReseted()
self._ranges = []
self._entry_price = 0.0
self._entry_side = 0
self._recovering = False
def OnStarted2(self, time):
super(martin_gale_breakout_strategy, self).OnStarted2(time)
self._ranges = []
self._entry_price = 0.0
self._entry_side = 0
self._recovering = False
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _add_range(self, r):
self._ranges.append(r)
req = self.RequiredHistory
while len(self._ranges) > req:
self._ranges.pop(0)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
r = float(candle.HighPrice) - float(candle.LowPrice)
# Check exit
if self.Position != 0 and self._entry_price > 0:
tp_pct = float(self.TakeProfitPct) * float(self.RecoveryMultiplier) if self._recovering else float(self.TakeProfitPct)
sl_pct = float(self.StopLossPct)
if self._entry_side == 1:
pnl = (close - self._entry_price) / self._entry_price * 100.0
if pnl >= tp_pct or pnl <= -sl_pct:
was_loss = pnl < 0
self.SellMarket()
self._entry_price = 0.0
self._entry_side = 0
self._recovering = was_loss
self._add_range(r)
return
elif self._entry_side == -1:
pnl = (self._entry_price - close) / self._entry_price * 100.0
if pnl >= tp_pct or pnl <= -sl_pct:
was_loss = pnl < 0
self.BuyMarket()
self._entry_price = 0.0
self._entry_side = 0
self._recovering = was_loss
self._add_range(r)
return
# Entry - only when flat
req = self.RequiredHistory
if self.Position == 0 and len(self._ranges) >= req:
total = sum(self._ranges)
avg_range = total / len(self._ranges)
if avg_range > 0 and r > avg_range * float(self.BreakoutFactor):
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._add_range(r)
def CreateClone(self):
return martin_gale_breakout_strategy()