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()