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;
using StockSharp.Algo;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Alternating long/short strategy converted from the original MT45 MQL expert.
/// </summary>
public class MT45Strategy : Strategy
{
private readonly StrategyParam<decimal> _stopPoints;
private readonly StrategyParam<decimal> _takePoints;
private readonly StrategyParam<decimal> _baseVolume;
private readonly StrategyParam<decimal> _multiplier;
private readonly StrategyParam<decimal> _maxVolume;
private readonly StrategyParam<DataType> _candleType;
private Sides _nextSide;
private Sides? _pendingSide;
private bool _entryPending;
private decimal _entryPrice;
private decimal _lastTradeVolume;
private decimal _nextVolume;
private decimal _prevPosition;
private decimal _pointValue;
/// <summary>
/// Stop-loss distance expressed in price steps.
/// </summary>
public decimal StopPoints
{
get => _stopPoints.Value;
set => _stopPoints.Value = value;
}
/// <summary>
/// Take-profit distance expressed in price steps.
/// </summary>
public decimal TakePoints
{
get => _takePoints.Value;
set => _takePoints.Value = value;
}
/// <summary>
/// Base trading volume used after profitable trades.
/// </summary>
public decimal BaseVolume
{
get => _baseVolume.Value;
set => _baseVolume.Value = value;
}
/// <summary>
/// Multiplier applied to the next volume after a losing trade.
/// </summary>
public decimal MartingaleMultiplier
{
get => _multiplier.Value;
set => _multiplier.Value = value;
}
/// <summary>
/// Maximum allowed trading volume.
/// </summary>
public decimal MaxVolume
{
get => _maxVolume.Value;
set => _maxVolume.Value = value;
}
/// <summary>
/// Candle type used to detect new bars.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public MT45Strategy()
{
_stopPoints = Param(nameof(StopPoints), 600m)
.SetDisplay("Stop Points", "Distance to stop loss measured in price steps", "Risk")
.SetOptimize(100m, 1500m, 50m);
_takePoints = Param(nameof(TakePoints), 700m)
.SetDisplay("Take Points", "Distance to take profit measured in price steps", "Risk")
.SetOptimize(100m, 2000m, 50m);
_baseVolume = Param(nameof(BaseVolume), 1m)
.SetDisplay("Base Volume", "Initial trade volume used by the strategy", "Trading");
_multiplier = Param(nameof(MartingaleMultiplier), 2m)
.SetDisplay("Martingale Multiplier", "Volume multiplier applied after a losing trade", "Trading")
.SetOptimize(1m, 5m, 0.5m);
_maxVolume = Param(nameof(MaxVolume), 10m)
.SetDisplay("Max Volume", "Upper limit for martingale scaling", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle series used to trigger new trades", "General");
_nextSide = Sides.Buy;
_nextVolume = BaseVolume;
_lastTradeVolume = BaseVolume;
Volume = BaseVolume;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_nextSide = Sides.Buy;
_pendingSide = null;
_entryPending = false;
_entryPrice = 0m;
_lastTradeVolume = BaseVolume;
_nextVolume = BaseVolume;
_prevPosition = 0m;
_pointValue = 0m;
Volume = BaseVolume;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pointValue = Security?.PriceStep ?? 1m;
Volume = BaseVolume;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
StartProtection(
takeProfit: CreatePriceUnit(TakePoints),
stopLoss: CreatePriceUnit(StopPoints));
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_entryPending || Position != 0)
return;
var volume = _nextVolume;
if (volume <= 0m)
return;
var side = _nextSide;
_pendingSide = side;
// Alternate between long and short trades every finished bar.
if (side == Sides.Buy)
{
BuyMarket();
}
else
{
SellMarket();
}
_entryPending = true;
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade.Order == null || trade.Trade == null)
return;
var newPosition = Position;
var previousPosition = _prevPosition;
_prevPosition = newPosition;
if (previousPosition == 0m && newPosition != 0m)
{
// Store entry price and volume for later profit calculation.
_entryPrice = trade.Trade.Price;
_lastTradeVolume = Math.Abs(newPosition);
_entryPending = false;
if (_pendingSide.HasValue)
{
_nextSide = Opposite(_pendingSide.Value);
_pendingSide = null;
}
}
else if (previousPosition != 0m && newPosition == 0m)
{
// Position closed: evaluate the result and adjust the next volume.
var direction = previousPosition > 0m ? Sides.Buy : Sides.Sell;
UpdateNextVolume(direction, trade.Trade.Price, Math.Abs(previousPosition));
_entryPrice = 0m;
_entryPending = false;
}
}
private void UpdateNextVolume(Sides direction, decimal exitPrice, decimal volume)
{
if (volume <= 0m)
return;
var profit = direction == Sides.Buy
? (exitPrice - _entryPrice) * volume
: (_entryPrice - exitPrice) * volume;
if (profit < 0m)
{
var scaled = _lastTradeVolume * MartingaleMultiplier;
_nextVolume = scaled > MaxVolume ? BaseVolume : scaled;
}
else
{
_nextVolume = BaseVolume;
}
Volume = _nextVolume;
}
private Unit CreatePriceUnit(decimal points)
{
if (points <= 0m || _pointValue <= 0m)
return default;
return new Unit(points * _pointValue, UnitTypes.Absolute);
}
private static Sides Opposite(Sides side)
{
return side == Sides.Buy ? Sides.Sell : Sides.Buy;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal, Math as CMath
from StockSharp.Messages import DataType, CandleStates, Sides, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class mt45_strategy(Strategy):
def __init__(self):
super(mt45_strategy, self).__init__()
self._stop_points = self.Param("StopPoints", Decimal(600))
self._take_points = self.Param("TakePoints", Decimal(700))
self._base_volume = self.Param("BaseVolume", Decimal(1))
self._multiplier = self.Param("MartingaleMultiplier", Decimal(2))
self._max_volume = self.Param("MaxVolume", Decimal(10))
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._next_side = Sides.Buy
self._pending_side = None
self._entry_pending = False
self._entry_price = Decimal(0)
self._last_trade_volume = Decimal(1)
self._next_volume = Decimal(1)
self._prev_position = Decimal(0)
self._point_value = Decimal(0)
self.Volume = Decimal(1)
@property
def StopPoints(self):
return self._stop_points.Value
@property
def TakePoints(self):
return self._take_points.Value
@property
def BaseVolume(self):
return self._base_volume.Value
@property
def MartingaleMultiplier(self):
return self._multiplier.Value
@property
def MaxVolume(self):
return self._max_volume.Value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(mt45_strategy, self).OnStarted2(time)
sec = self.Security
self._point_value = sec.PriceStep if sec is not None and sec.PriceStep is not None else Decimal(1)
self.Volume = self.BaseVolume
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)
tp = self._create_price_unit(self.TakePoints)
sl = self._create_price_unit(self.StopPoints)
if tp is not None and sl is not None:
self.StartProtection(tp, sl)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._entry_pending or self.Position != Decimal(0):
return
volume = self._next_volume
if volume <= Decimal(0):
return
side = self._next_side
self._pending_side = side
if side == Sides.Buy:
self.BuyMarket()
else:
self.SellMarket()
self._entry_pending = True
def OnOwnTradeReceived(self, trade):
super(mt45_strategy, self).OnOwnTradeReceived(trade)
if trade.Order is None or trade.Trade is None:
return
new_position = self.Position
previous_position = self._prev_position
self._prev_position = new_position
if previous_position == Decimal(0) and new_position != Decimal(0):
self._entry_price = trade.Trade.Price
self._last_trade_volume = CMath.Abs(new_position)
self._entry_pending = False
if self._pending_side is not None:
self._next_side = Sides.Sell if self._pending_side == Sides.Buy else Sides.Buy
self._pending_side = None
elif previous_position != Decimal(0) and new_position == Decimal(0):
direction = Sides.Buy if previous_position > Decimal(0) else Sides.Sell
self._update_next_volume(direction, trade.Trade.Price, CMath.Abs(previous_position))
self._entry_price = Decimal(0)
self._entry_pending = False
def _update_next_volume(self, direction, exit_price, volume):
if volume <= Decimal(0):
return
if direction == Sides.Buy:
profit = (exit_price - self._entry_price) * volume
else:
profit = (self._entry_price - exit_price) * volume
if profit < Decimal(0):
scaled = self._last_trade_volume * self.MartingaleMultiplier
self._next_volume = self.BaseVolume if scaled > self.MaxVolume else scaled
else:
self._next_volume = self.BaseVolume
self.Volume = self._next_volume
def _create_price_unit(self, points):
if points <= Decimal(0) or self._point_value <= Decimal(0):
return None
return Unit(points * self._point_value, UnitTypes.Absolute)
def OnReseted(self):
super(mt45_strategy, self).OnReseted()
self._next_side = Sides.Buy
self._pending_side = None
self._entry_pending = False
self._entry_price = Decimal(0)
self._last_trade_volume = self.BaseVolume
self._next_volume = self.BaseVolume
self._prev_position = Decimal(0)
self._point_value = Decimal(0)
self.Volume = self.BaseVolume
def CreateClone(self):
return mt45_strategy()