马丁格尔交易模拟器策略
概述
MartingaleTradeSimulatorStrategy 在 StockSharp 中重现了 MetaTrader 上的 “Martingale Trade Simulator” 专家顾问。该策略提供手动交易面板功能:即时发送市价单、按键触发马丁格尔加仓、以及在无须额外脚本的情况下管理移动止损。所有开关都通过参数实时响应,非常适合在策略测试器中进行交互式实验。
工作原理
手动市价按钮
- 参数
Buy与Sell对应面板上的买入 / 卖出按钮。当参数被设置为true时,策略会按Order Volume的数量发送市价单,然后自动把参数重置为false。 - 策略只使用市价单,不挂出任何挂单,完全模拟原 EA 在可视化测试中的行为。
马丁格尔加仓
- 启用
Enable Martingale后,可通过把Martingale参数切换为true来触发一次加仓检查。 - 策略会根据当前持仓方向判断是否需要加仓:
- 多头持仓: 若最新卖价低于已成交买单中的最低价格至少
Martingale Step (points),则发送新的买入市价单。 - 空头持仓: 若最新买价高于已成交卖单中的最高价格至少
Martingale Step (points),则发送新的卖出市价单。
- 多头持仓: 若最新卖价低于已成交买单中的最低价格至少
- 每一笔加仓的手数等于
Order Volume × (Martingale Multiplier)^N,其中N为当前方向连续入场的次数。 - 一旦进入马丁格尔模式,策略会根据最新的加权平均持仓价重新计算止盈价,并在其基础上加上(或减去)
Martingale TP Offset (points),以覆盖累计亏损。
移动止损
- 参数
Enable Trailing控制是否启用移动止损。 - 移动止损初始位于距离市场价
Trailing Stop (points)的位置,只有当价格至少向有利方向移动Trailing Step (points)后才会前移。 - 当市场价触及移动止损时,策略立即发送反向市价单平掉全部仓位。
止损与止盈
Stop Loss (points)与Take Profit (points)重现了原专家顾问的基础风控选项。- 多头情况下止损位于平均建仓价下方,止盈位于上方;空头则相反。
- 所有风控都通过市价单执行,确保策略兼容 StockSharp 支持的各类连接器。
参数说明
| 参数 | 说明 | 默认值 |
|---|---|---|
Order Volume |
手动市价单的基础手数。 | 1 |
Stop Loss (points) |
止损距离,设为 0 表示关闭止损。 | 500 |
Take Profit (points) |
止盈距离,设为 0 表示关闭止盈。 | 500 |
Enable Trailing |
是否启用移动止损。 | true |
Trailing Stop (points) |
移动止损与价格之间的距离。 | 50 |
Trailing Step (points) |
移动止损前移所需的最小盈利幅度。 | 20 |
Enable Martingale |
允许使用 Martingale 按钮进行马丁格尔加仓。 |
true |
Martingale Multiplier |
每一级加仓的手数乘数。 | 1.2 |
Martingale Step (points) |
触发加仓所需的最小不利价格偏移。 | 150 |
Martingale TP Offset (points) |
重新计算止盈时额外添加的点数。 | 50 |
Buy |
设为 true 发送市价买单(自动复位)。 |
false |
Sell |
设为 true 发送市价卖单(自动复位)。 |
false |
Martingale |
设为 true 触发马丁格尔加仓检查(自动复位)。 |
false |
使用步骤
- 选择交易品种,设置
Order Volume,启动策略(可在测试或实盘模式下运行)。 - 将
Buy或Sell参数设为true,即可模拟面板上的买入 / 卖出按钮。 - 首次成交后,当价格向不利方向移动时,把
Martingale参数切换为true,策略会检查是否满足加仓条件并按乘数扩大手数。 - 结合
Enable Trailing与风险参数,可以完全复刻原 EA 的操作,或尝试不同的实验配置。
备注
- 策略依赖 Level1 行情(买一 / 卖一 / 最新成交价)来评估市况。
- C# 源码中的注释均使用英文,以符合仓库规范。
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>
/// Manual martingale simulator that reproduces the "Martingale Trade Simulator" expert advisor.
/// Provides buy/sell buttons, optional martingale averaging and trailing stop automation.
/// </summary>
public class MartingaleTradeSimulatorStrategy : Strategy
{
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<bool> _enableTrailing;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<decimal> _trailingStepPoints;
private readonly StrategyParam<bool> _enableMartingale;
private readonly StrategyParam<decimal> _martingaleMultiplier;
private readonly StrategyParam<decimal> _martingaleStepPoints;
private readonly StrategyParam<decimal> _martingaleTakeProfitOffset;
private readonly StrategyParam<bool> _buyRequest;
private readonly StrategyParam<bool> _sellRequest;
private readonly StrategyParam<bool> _martingaleRequest;
private decimal? _lastTradePrice;
private decimal? _bestBidPrice;
private decimal? _bestAskPrice;
private decimal? _longTrailingStop;
private decimal? _shortTrailingStop;
private decimal? _lowestLongPrice;
private decimal? _highestShortPrice;
private decimal? _longTakeProfit;
private decimal? _shortTakeProfit;
private int _longEntriesCount;
private int _shortEntriesCount;
private decimal _previousPosition;
private bool _longMartingaleActive;
private bool _shortMartingaleActive;
/// <summary>
/// Volume used for manual market orders.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in price points.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance expressed in price points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Enables the trailing stop automation.
/// </summary>
public bool EnableTrailing
{
get => _enableTrailing.Value;
set => _enableTrailing.Value = value;
}
/// <summary>
/// Distance from price to the trailing stop in points.
/// </summary>
public decimal TrailingStopPoints
{
get => _trailingStopPoints.Value;
set => _trailingStopPoints.Value = value;
}
/// <summary>
/// Minimal step required to move the trailing stop in points.
/// </summary>
public decimal TrailingStepPoints
{
get => _trailingStepPoints.Value;
set => _trailingStepPoints.Value = value;
}
/// <summary>
/// Enables martingale averaging logic.
/// </summary>
public bool EnableMartingale
{
get => _enableMartingale.Value;
set => _enableMartingale.Value = value;
}
/// <summary>
/// Multiplier applied to the volume of each martingale order.
/// </summary>
public decimal MartingaleMultiplier
{
get => _martingaleMultiplier.Value;
set => _martingaleMultiplier.Value = value;
}
/// <summary>
/// Price step in points before a new martingale order can be placed.
/// </summary>
public decimal MartingaleStepPoints
{
get => _martingaleStepPoints.Value;
set => _martingaleStepPoints.Value = value;
}
/// <summary>
/// Offset in points added to the averaged take-profit price.
/// </summary>
public decimal MartingaleTakeProfitOffset
{
get => _martingaleTakeProfitOffset.Value;
set => _martingaleTakeProfitOffset.Value = value;
}
/// <summary>
/// Manual trigger for a market buy order.
/// </summary>
public bool BuyRequest
{
get => _buyRequest.Value;
set => _buyRequest.Value = value;
}
/// <summary>
/// Manual trigger for a market sell order.
/// </summary>
public bool SellRequest
{
get => _sellRequest.Value;
set => _sellRequest.Value = value;
}
/// <summary>
/// Manual trigger for martingale averaging.
/// </summary>
public bool MartingaleRequest
{
get => _martingaleRequest.Value;
set => _martingaleRequest.Value = value;
}
/// <summary>
/// Initializes <see cref="MartingaleTradeSimulatorStrategy"/>.
/// </summary>
public MartingaleTradeSimulatorStrategy()
{
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Base volume for manual market orders.", "Manual Controls");
_stopLossPoints = Param(nameof(StopLossPoints), 500m)
.SetNotNegative()
.SetDisplay("Stop Loss (points)", "Distance from entry to protective stop.", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 500m)
.SetNotNegative()
.SetDisplay("Take Profit (points)", "Distance from entry to protective target.", "Risk");
_enableTrailing = Param(nameof(EnableTrailing), true)
.SetDisplay("Enable Trailing", "Turn the trailing stop automation on or off.", "Trailing")
;
_trailingStopPoints = Param(nameof(TrailingStopPoints), 50m)
.SetNotNegative()
.SetDisplay("Trailing Stop (points)", "Distance of the trailing stop from market price.", "Trailing");
_trailingStepPoints = Param(nameof(TrailingStepPoints), 20m)
.SetNotNegative()
.SetDisplay("Trailing Step (points)", "Minimal gain required to move the trailing stop.", "Trailing");
_enableMartingale = Param(nameof(EnableMartingale), true)
.SetDisplay("Enable Martingale", "Allow averaging orders using martingale sizing.", "Martingale")
;
_martingaleMultiplier = Param(nameof(MartingaleMultiplier), 1.2m)
.SetGreaterThanZero()
.SetDisplay("Martingale Multiplier", "Volume multiplier for each averaging order.", "Martingale");
_martingaleStepPoints = Param(nameof(MartingaleStepPoints), 150m)
.SetNotNegative()
.SetDisplay("Martingale Step (points)", "Minimal adverse move before adding a new order.", "Martingale");
_martingaleTakeProfitOffset = Param(nameof(MartingaleTakeProfitOffset), 50m)
.SetNotNegative()
.SetDisplay("Martingale TP Offset (points)", "Extra distance added to averaged take-profit.", "Martingale");
_buyRequest = Param(nameof(BuyRequest), false)
.SetDisplay("Buy", "Set to true to send a market buy order.", "Manual Controls")
;
_sellRequest = Param(nameof(SellRequest), false)
.SetDisplay("Sell", "Set to true to send a market sell order.", "Manual Controls")
;
_martingaleRequest = Param(nameof(MartingaleRequest), false)
.SetDisplay("Martingale", "Set to true to evaluate and place an averaging order.", "Manual Controls")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe", "General");
}
private SimpleMovingAverage _smaFast = null!;
private SimpleMovingAverage _smaSlow = null!;
private readonly StrategyParam<DataType> _candleType;
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_lastTradePrice = null;
_bestBidPrice = null;
_bestAskPrice = null;
_longTrailingStop = null;
_shortTrailingStop = null;
_lowestLongPrice = null;
_highestShortPrice = null;
_longTakeProfit = null;
_shortTakeProfit = null;
_longEntriesCount = 0;
_shortEntriesCount = 0;
_previousPosition = 0m;
_longMartingaleActive = false;
_shortMartingaleActive = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_smaFast = new SimpleMovingAverage { Length = 10 };
_smaSlow = new SimpleMovingAverage { Length = 30 };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_smaFast, _smaSlow, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal fast, decimal slow)
{
if (candle.State != CandleStates.Finished)
return;
_lastTradePrice = candle.ClosePrice;
if (fast > slow && Position <= 0)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
BuyMarket(OrderVolume);
}
else if (fast < slow && Position >= 0)
{
if (Position > 0)
SellMarket(Position);
SellMarket(OrderVolume);
}
}
private void ProcessMartingaleCommand()
{
if (!MartingaleRequest)
return;
MartingaleRequest = false;
if (!EnableMartingale)
return;
if (!IsOnline)
return;
if (Security == null || Portfolio == null)
return;
var step = GetPriceStep() * MartingaleStepPoints;
if (step <= 0m)
return;
if (Position > 0)
{
var ask = GetAskPrice();
if (ask == null)
return;
var referencePrice = _lowestLongPrice ?? _lastTradePrice;
if (referencePrice == null)
return;
if (referencePrice.Value - ask.Value >= step)
{
var volume = CalculateNextVolume(true);
if (volume > 0m)
{
BuyMarket(volume);
_longMartingaleActive = true;
}
}
}
else if (Position < 0)
{
var bid = GetBidPrice();
if (bid == null)
return;
var referencePrice = _highestShortPrice ?? _lastTradePrice;
if (referencePrice == null)
return;
if (bid.Value - referencePrice.Value >= step)
{
var volume = CalculateNextVolume(false);
if (volume > 0m)
{
SellMarket(volume);
_shortMartingaleActive = true;
}
}
}
}
private void ManageRisk()
{
if (Position == 0)
{
_longTrailingStop = null;
_shortTrailingStop = null;
return;
}
var marketPrice = GetMarketPrice();
if (marketPrice == null)
return;
var step = GetPriceStep();
var positionPrice = _lastTradePrice;
if (positionPrice == null)
return;
if (Position > 0)
{
ApplyLongProtection(marketPrice.Value, positionPrice.Value, step);
}
else
{
ApplyShortProtection(marketPrice.Value, positionPrice.Value, step);
}
}
private void ApplyLongProtection(decimal marketPrice, decimal positionPrice, decimal priceStep)
{
if (StopLossPoints > 0m)
{
var stopPrice = positionPrice - StopLossPoints * priceStep;
if (marketPrice <= stopPrice)
SellMarket(Math.Abs(Position));
}
var takePrice = _longMartingaleActive ? _longTakeProfit : (TakeProfitPoints > 0m ? positionPrice + TakeProfitPoints * priceStep : null);
if (takePrice != null && marketPrice >= takePrice.Value)
SellMarket(Math.Abs(Position));
if (!EnableTrailing || TrailingStopPoints <= 0m)
{
_longTrailingStop = null;
return;
}
var trailingDistance = TrailingStopPoints * priceStep;
var trailingStep = TrailingStepPoints * priceStep;
if (_longTrailingStop == null)
{
_longTrailingStop = marketPrice - trailingDistance;
}
else
{
var candidate = marketPrice - trailingDistance;
if (candidate - _longTrailingStop.Value >= trailingStep)
_longTrailingStop = candidate;
}
if (_longTrailingStop != null && marketPrice <= _longTrailingStop.Value)
SellMarket(Math.Abs(Position));
}
private void ApplyShortProtection(decimal marketPrice, decimal positionPrice, decimal priceStep)
{
if (StopLossPoints > 0m)
{
var stopPrice = positionPrice + StopLossPoints * priceStep;
if (marketPrice >= stopPrice)
BuyMarket(Math.Abs(Position));
}
var takePrice = _shortMartingaleActive ? _shortTakeProfit : (TakeProfitPoints > 0m ? positionPrice - TakeProfitPoints * priceStep : null);
if (takePrice != null && marketPrice <= takePrice.Value)
BuyMarket(Math.Abs(Position));
if (!EnableTrailing || TrailingStopPoints <= 0m)
{
_shortTrailingStop = null;
return;
}
var trailingDistance = TrailingStopPoints * priceStep;
var trailingStep = TrailingStepPoints * priceStep;
if (_shortTrailingStop == null)
{
_shortTrailingStop = marketPrice + trailingDistance;
}
else
{
var candidate = marketPrice + trailingDistance;
if (_shortTrailingStop.Value - candidate >= trailingStep)
_shortTrailingStop = candidate;
}
if (_shortTrailingStop != null && marketPrice >= _shortTrailingStop.Value)
BuyMarket(Math.Abs(Position));
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
var price = trade.Trade?.Price;
if (price is null)
return;
if (Position > 0)
{
_longMartingaleActive = _longMartingaleActive && Position > 0;
_shortMartingaleActive = false;
_shortTrailingStop = null;
_shortTakeProfit = null;
if (trade.Order.Side == Sides.Buy)
{
_lowestLongPrice = _lowestLongPrice.HasValue ? Math.Min(_lowestLongPrice.Value, price.Value) : price.Value;
UpdateLongTakeProfit();
}
else if (Position <= 0)
{
ResetLongState();
}
}
else if (Position < 0)
{
_shortMartingaleActive = _shortMartingaleActive && Position < 0;
_longMartingaleActive = false;
_longTrailingStop = null;
_longTakeProfit = null;
if (trade.Order.Side == Sides.Sell)
{
_highestShortPrice = _highestShortPrice.HasValue ? Math.Max(_highestShortPrice.Value, price.Value) : price.Value;
UpdateShortTakeProfit();
}
else if (Position >= 0)
{
ResetShortState();
}
}
else
{
ResetLongState();
ResetShortState();
}
}
/// <inheritdoc />
protected override void OnPositionReceived(Position position)
{
base.OnPositionReceived(position);
var delta = Position - _previousPosition;
if (Position > 0)
{
if (_previousPosition <= 0m)
{
_longEntriesCount = 1;
}
else if (delta > 0m)
{
_longEntriesCount++;
}
else if (delta < 0m)
{
_longEntriesCount = Math.Max(1, _longEntriesCount - 1);
}
_shortEntriesCount = 0;
}
else if (Position < 0)
{
if (_previousPosition >= 0m)
{
_shortEntriesCount = 1;
}
else if (delta < 0m)
{
_shortEntriesCount++;
}
else if (delta > 0m)
{
_shortEntriesCount = Math.Max(1, _shortEntriesCount - 1);
}
_longEntriesCount = 0;
}
else
{
_longEntriesCount = 0;
_shortEntriesCount = 0;
}
if (Position == 0m)
{
ResetLongState();
ResetShortState();
}
_previousPosition = Position;
}
private void UpdateLongTakeProfit()
{
if (!_longMartingaleActive)
return;
var positionPrice = _lastTradePrice;
if (positionPrice == null)
return;
var offset = MartingaleTakeProfitOffset * GetPriceStep();
_longTakeProfit = positionPrice.Value + offset;
}
private void UpdateShortTakeProfit()
{
if (!_shortMartingaleActive)
return;
var positionPrice = _lastTradePrice;
if (positionPrice == null)
return;
var offset = MartingaleTakeProfitOffset * GetPriceStep();
_shortTakeProfit = positionPrice.Value - offset;
}
private decimal? GetMarketPrice()
{
if (_lastTradePrice != null)
return _lastTradePrice;
if (_bestBidPrice != null && _bestAskPrice != null)
return (_bestBidPrice.Value + _bestAskPrice.Value) / 2m;
return _bestBidPrice ?? _bestAskPrice;
}
private decimal? GetBidPrice()
{
return _bestBidPrice ?? _lastTradePrice;
}
private decimal? GetAskPrice()
{
return _bestAskPrice ?? _lastTradePrice;
}
private decimal GetPriceStep()
{
var step = Security?.PriceStep;
return step is null || step == 0m ? 1m : step.Value;
}
private decimal CalculateNextVolume(bool isLong)
{
var entries = isLong ? _longEntriesCount : _shortEntriesCount;
var multiplier = MartingaleMultiplier;
if (multiplier <= 0m)
return 0m;
var power = entries;
var factor = (decimal)Math.Pow((double)multiplier, power);
return OrderVolume * factor;
}
private void ResetLongState()
{
_longMartingaleActive = false;
_longTrailingStop = null;
_longTakeProfit = null;
_lowestLongPrice = null;
}
private void ResetShortState()
{
_shortMartingaleActive = false;
_shortTrailingStop = null;
_shortTakeProfit = null;
_highestShortPrice = 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.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class martingale_trade_simulator_strategy(Strategy):
"""
Martingale Trade Simulator: simplified to SMA crossover entry.
The full C# version has manual buy/sell buttons, trailing stop, and martingale averaging.
"""
def __init__(self):
super(martingale_trade_simulator_strategy, self).__init__()
self._fast_period = self.Param("FastPeriod", 10).SetDisplay("Fast SMA", "Fast SMA period", "Indicators")
self._slow_period = self.Param("SlowPeriod", 30).SetDisplay("Slow SMA", "Slow SMA period", "Indicators")
self._cooldown_bars = self.Param("CooldownBars", 10).SetDisplay("Cooldown", "Bars between signals", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Candles", "General")
self._prev_fast = 0.0
self._prev_slow = 0.0
self._cooldown = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(martingale_trade_simulator_strategy, self).OnReseted()
self._prev_fast = 0.0
self._prev_slow = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(martingale_trade_simulator_strategy, self).OnStarted2(time)
fast = SimpleMovingAverage()
fast.Length = self._fast_period.Value
slow = SimpleMovingAverage()
slow.Length = self._slow_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(fast, slow, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, fast)
self.DrawIndicator(area, slow)
self.DrawOwnTrades(area)
def _process_candle(self, candle, fast_val, slow_val):
if candle.State != CandleStates.Finished:
return
fast = float(fast_val)
slow = float(slow_val)
if self._cooldown > 0:
self._cooldown -= 1
self._prev_fast = fast
self._prev_slow = slow
return
if self._prev_fast > 0 and self._prev_slow > 0:
if self._prev_fast <= self._prev_slow and fast > slow and self.Position <= 0:
self.BuyMarket()
self._cooldown = self._cooldown_bars.Value
elif self._prev_fast >= self._prev_slow and fast < slow and self.Position >= 0:
self.SellMarket()
self._cooldown = self._cooldown_bars.Value
self._prev_fast = fast
self._prev_slow = slow
def CreateClone(self):
return martingale_trade_simulator_strategy()