Martin Martingale 策略
概述
该策略重现了 MQL 原版 "Martin" 智能交易系统的运行方式,通过在当前价格附近构建对冲型的马丁格尔网格来运作。策略在多头与空头之间不断交替,并在每次反向开仓时将成交量加倍,直到整篮订单的累计利润达到设定目标。K 线仅用于驱动决策逻辑,实际下单全部通过 StockSharp 的高级 API(市价单与止损单)完成。
工作原理
- 启动时读取标的的
PriceStep,将EntryOffsetPoints与StepPoints参数换算成绝对价格距离;如果缺少价格步长,则使用 1 作为默认值。 - 当没有持仓且马丁格尔循环未激活时,策略会在最近一次收盘价附近同时挂出买入止损和卖出止损,距离为
EntryOffsetPoints * PriceStep,这与原始 MQL 版本的 10 个点相同。 - 当任意一张止损单成交后,另一张挂单会被取消。该成交被视为马丁格尔序列的第一笔交易:策略记录成交价格、方向和数量,并把内部层级计数器设置为 1。
- 随后的每根 K 线收盘时,当前收盘价会与上一次成交价比较。如果市场相对上一次交易出现了至少
martingaleLevel * StepPoints * PriceStep的反向波动,就会以市价在相反方向开仓,成交量为上一笔交易的两倍。每次成交后都会刷新“最后一笔交易”的信息。 - 未实现盈亏按
PnL + Position * (closePrice - PositionPrice)计算。当该综合盈亏超过ProfitTarget参数时,策略调用CloseAll()平掉整篮仓位,同时取消所有剩余挂单并重置循环,以便重新挂出一对止损单。 - 如果仓位被手动全部关闭,也会触发同样的重置流程:内部计数清零,下一根 K 线将重新放置止损订单。
该流程在 StockSharp 的高级 API 环境中保持了原策略的买卖交替逻辑。
参数
StepPoints:用于计算下一次反向加仓触发阈值的价格步数,默认 10,可用于优化。EntryOffsetPoints:首次买入/卖出止损的价格偏移量(按价格步数计),默认 10,与 MQL 版本一致。ProfitTarget:平掉整个马丁格尔篮子的绝对利润目标。当累积(已实现+未实现)盈亏超过该值时,所有仓位会被强制平仓。CandleType:用于驱动逻辑的 K 线订阅类型。默认是一分钟周期,但可选择任何交易所支持的DataType。
基础下单数量取自策略的 Volume 属性。每次反向开仓都会把该基数乘以 2,形成经典的马丁格尔序列。
实用提示
- 请根据经纪商的最小手数设置
Volume。由于倍数增加很快,建议配合外部风控限制总体风险敞口。 - 决策在 K 线收盘时执行,因此快速波动可能导致入场略晚于基于 tick 的 MQL 版本。不过止损挂单能够保持与原策略接近的触发价位。
- 策略会在默认图表区域绘制价格 K 线与自身成交,方便观察运行状态。
- 策略不包含自动止损,唯一的退出条件是
ProfitTarget。选择品种和周期时需评估长期单边行情带来的风险。
与 MQL 版本的差异
- StockSharp 采用净头寸模式,因此每次反向都会通过一笔市价单同时平掉旧仓并建立新仓,总体盈亏仍与对冲实现一致。
- 为遵循高级 API 的最佳实践,信号判断改为基于 K 线收盘,而非逐笔 tick。
- 为避免重复处理部分成交,策略跟踪订单的标识符,确保加倍逻辑仅在整笔订单完成时执行一次。
这些调整保证了策略在 StockSharp 框架中仍然忠实于原始 MQL 方案的交易行为。
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>
/// Martingale grid that alternates long and short entries while doubling volume.
/// </summary>
public class MartinMartingaleStrategy : Strategy
{
private readonly StrategyParam<int> _stepPoints;
private readonly StrategyParam<int> _entryOffsetPoints;
private readonly StrategyParam<decimal> _profitTarget;
private readonly StrategyParam<int> _maxLevel;
private readonly StrategyParam<DataType> _candleType;
private decimal _stepSize;
private decimal _entryOffset;
private decimal _lastTradePrice;
private decimal _lastTradeVolume;
private int _martingaleLevel;
private Sides? _lastTradeSide;
private bool _isClosing;
private decimal? _initialPrice;
/// <summary>
/// Distance in points that defines when the next reversal is triggered.
/// </summary>
public int StepPoints
{
get => _stepPoints.Value;
set => _stepPoints.Value = value;
}
/// <summary>
/// Offset in points for the initial breakout entry.
/// </summary>
public int EntryOffsetPoints
{
get => _entryOffsetPoints.Value;
set => _entryOffsetPoints.Value = value;
}
/// <summary>
/// Aggregated profit required to close the entire martingale cycle.
/// </summary>
public decimal ProfitTarget
{
get => _profitTarget.Value;
set => _profitTarget.Value = value;
}
/// <summary>
/// Maximum martingale doubling level before resetting.
/// </summary>
public int MaxLevel
{
get => _maxLevel.Value;
set => _maxLevel.Value = value;
}
/// <summary>
/// Candle type used to monitor the price.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="MartinMartingaleStrategy"/>.
/// </summary>
public MartinMartingaleStrategy()
{
_stepPoints = Param(nameof(StepPoints), 10)
.SetGreaterThanZero()
.SetDisplay("Step (points)", "Distance multiplier for reversals", "General")
;
_entryOffsetPoints = Param(nameof(EntryOffsetPoints), 10)
.SetGreaterThanZero()
.SetDisplay("Entry Offset (points)", "Offset for initial breakout entry", "General")
;
_profitTarget = Param(nameof(ProfitTarget), 5m)
.SetGreaterThanZero()
.SetDisplay("Profit Target", "Total profit to close all positions", "Risk")
;
_maxLevel = Param(nameof(MaxLevel), 5)
.SetGreaterThanZero()
.SetDisplay("Max Level", "Maximum martingale levels", "Risk")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candles for price monitoring", "Data");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetCycle();
_isClosing = false;
_initialPrice = null;
_stepSize = 0;
_entryOffset = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
UpdateStepSettings();
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;
UpdateStepSettings();
if (_stepSize <= 0m || Volume <= 0m)
return;
var price = candle.ClosePrice;
// If closing, flatten and wait
if (_isClosing)
{
if (Position == 0)
{
_isClosing = false;
ResetCycle();
}
return;
}
// If flat after a cycle, reset
if (Position == 0 && _martingaleLevel > 0)
{
ResetCycle();
}
// Check profit target
if (ProfitTarget > 0m && PnL >= ProfitTarget && Position != 0)
{
_isClosing = true;
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
return;
}
// Max level reached -> close and reset
if (_martingaleLevel >= MaxLevel && Position != 0)
{
_isClosing = true;
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
return;
}
// Initial entry: wait for breakout from first candle
if (_martingaleLevel == 0 && Position == 0)
{
if (!_initialPrice.HasValue)
{
_initialPrice = price;
return;
}
if (_entryOffset <= 0m)
return;
if (price >= _initialPrice.Value + _entryOffset)
{
BuyMarket();
_lastTradePrice = price;
_lastTradeVolume = Volume;
_lastTradeSide = Sides.Buy;
_martingaleLevel = 1;
_initialPrice = null;
}
else if (price <= _initialPrice.Value - _entryOffset)
{
SellMarket();
_lastTradePrice = price;
_lastTradeVolume = Volume;
_lastTradeSide = Sides.Sell;
_martingaleLevel = 1;
_initialPrice = null;
}
return;
}
if (_lastTradeSide is null || _martingaleLevel == 0)
return;
var threshold = _stepSize;
if (_lastTradeSide == Sides.Buy)
{
if (price <= _lastTradePrice - threshold)
{
var nextVolume = _lastTradeVolume * 2m;
var totalVolume = nextVolume + Math.Abs(Position);
SellMarket();
_lastTradePrice = price;
_lastTradeVolume = nextVolume;
_lastTradeSide = Sides.Sell;
_martingaleLevel++;
}
}
else
{
if (price >= _lastTradePrice + threshold)
{
var nextVolume = _lastTradeVolume * 2m;
var totalVolume = nextVolume + Math.Abs(Position);
BuyMarket();
_lastTradePrice = price;
_lastTradeVolume = nextVolume;
_lastTradeSide = Sides.Buy;
_martingaleLevel++;
}
}
}
private void UpdateStepSettings()
{
var priceStep = Security?.PriceStep ?? 0m;
if (priceStep <= 0m)
{
priceStep = 1m;
}
_stepSize = StepPoints * priceStep;
_entryOffset = EntryOffsetPoints * priceStep;
}
private void ResetCycle()
{
_martingaleLevel = 0;
_lastTradePrice = 0m;
_lastTradeVolume = 0m;
_lastTradeSide = null;
}
}
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_martingale_strategy(Strategy):
"""Martingale grid that alternates long/short entries while doubling volume."""
def __init__(self):
super(martin_martingale_strategy, self).__init__()
self._step_points = self.Param("StepPoints", 10) \
.SetGreaterThanZero() \
.SetDisplay("Step (points)", "Distance multiplier for reversals", "General")
self._entry_offset_points = self.Param("EntryOffsetPoints", 10) \
.SetGreaterThanZero() \
.SetDisplay("Entry Offset (points)", "Offset for initial breakout entry", "General")
self._profit_target = self.Param("ProfitTarget", 5.0) \
.SetGreaterThanZero() \
.SetDisplay("Profit Target", "Total profit to close all positions", "Risk")
self._max_level = self.Param("MaxLevel", 5) \
.SetGreaterThanZero() \
.SetDisplay("Max Level", "Maximum martingale levels", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candles for price monitoring", "Data")
self._step_size = 0.0
self._entry_offset = 0.0
self._last_trade_price = 0.0
self._last_trade_volume = 0.0
self._martingale_level = 0
self._last_trade_side = 0 # 0=none, 1=buy, -1=sell
self._is_closing = False
self._initial_price = None
@property
def StepPoints(self):
return int(self._step_points.Value)
@property
def EntryOffsetPoints(self):
return int(self._entry_offset_points.Value)
@property
def ProfitTarget(self):
return self._profit_target.Value
@property
def MaxLevel(self):
return int(self._max_level.Value)
@property
def CandleType(self):
return self._candle_type.Value
def _update_step_settings(self):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 1.0
self._step_size = self.StepPoints * step
self._entry_offset = self.EntryOffsetPoints * step
def _reset_cycle(self):
self._martingale_level = 0
self._last_trade_price = 0.0
self._last_trade_volume = 0.0
self._last_trade_side = 0
def OnStarted2(self, time):
super(martin_martingale_strategy, self).OnStarted2(time)
self._update_step_settings()
self._reset_cycle()
self._is_closing = False
self._initial_price = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._update_step_settings()
if self._step_size <= 0 or float(self.Volume) <= 0:
return
price = float(candle.ClosePrice)
# If closing, flatten and wait
if self._is_closing:
if self.Position == 0:
self._is_closing = False
self._reset_cycle()
return
# If flat after a cycle, reset
if self.Position == 0 and self._martingale_level > 0:
self._reset_cycle()
# Check profit target
if float(self.ProfitTarget) > 0 and float(self.PnL) >= float(self.ProfitTarget) and self.Position != 0:
self._is_closing = True
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
return
# Max level reached
if self._martingale_level >= self.MaxLevel and self.Position != 0:
self._is_closing = True
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
return
# Initial entry: wait for breakout from first candle
if self._martingale_level == 0 and self.Position == 0:
if self._initial_price is None:
self._initial_price = price
return
if self._entry_offset <= 0:
return
if price >= self._initial_price + self._entry_offset:
self.BuyMarket()
self._last_trade_price = price
self._last_trade_volume = float(self.Volume)
self._last_trade_side = 1
self._martingale_level = 1
self._initial_price = None
elif price <= self._initial_price - self._entry_offset:
self.SellMarket()
self._last_trade_price = price
self._last_trade_volume = float(self.Volume)
self._last_trade_side = -1
self._martingale_level = 1
self._initial_price = None
return
if self._last_trade_side == 0 or self._martingale_level == 0:
return
threshold = self._step_size
if self._last_trade_side == 1:
if price <= self._last_trade_price - threshold:
self.SellMarket()
self._last_trade_price = price
self._last_trade_volume *= 2.0
self._last_trade_side = -1
self._martingale_level += 1
else:
if price >= self._last_trade_price + threshold:
self.BuyMarket()
self._last_trade_price = price
self._last_trade_volume *= 2.0
self._last_trade_side = 1
self._martingale_level += 1
def OnReseted(self):
super(martin_martingale_strategy, self).OnReseted()
self._reset_cycle()
self._is_closing = False
self._initial_price = None
self._step_size = 0.0
self._entry_offset = 0.0
def CreateClone(self):
return martin_martingale_strategy()