Lacust Stop and BE
This strategy demonstrates basic position management inspired by the original MQL expert advisor lacuststopandbe.
After entering a position in the direction of the last finished candle the strategy applies several protective rules:
- Initial stop loss and take profit are placed at fixed price distances.
- When profit reaches
BreakevenGain, the stop is moved to entry price plusBreakeven. - After profit exceeds
TrailingStart, the stop trails behind price byTrailingStop. - The position is closed when the stop level or take profit level is touched.
Parameters:
CandleType– candle series used for processing.StopLoss– initial stop loss distance.TakeProfit– initial take profit distance.TrailingStart– profit required to activate trailing stop.TrailingStop– trailing stop distance from current price.BreakevenGain– profit required before moving stop to break-even.Breakeven– profit locked after moving stop to break-even.
This sample uses the high level StockSharp API and can serve as a template for porting simple MQL trade management scripts.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Enters on the last completed candle's direction; manages actual filled positions
/// through best bid/ask quotes with initial SL/TP, breakeven and monotonic trailing.
/// </summary>
public class LacustStopAndBeStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<decimal> _trailingStart;
private readonly StrategyParam<decimal> _trailingStop;
private readonly StrategyParam<decimal> _breakevenGain;
private readonly StrategyParam<decimal> _breakeven;
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
private decimal? _favorableExtreme;
private decimal _trackedVolume;
private Order _pendingOrder;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public decimal StopLoss { get => _stopLoss.Value; set => _stopLoss.Value = value; }
public decimal TakeProfit { get => _takeProfit.Value; set => _takeProfit.Value = value; }
public decimal TrailingStart { get => _trailingStart.Value; set => _trailingStart.Value = value; }
public decimal TrailingStop { get => _trailingStop.Value; set => _trailingStop.Value = value; }
public decimal BreakevenGain { get => _breakevenGain.Value; set => _breakevenGain.Value = value; }
public decimal Breakeven { get => _breakeven.Value; set => _breakeven.Value = value; }
public LacustStopAndBeStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Signal candles", "General");
_stopLoss = Param(nameof(StopLoss), 400m).SetNotNegative()
.SetDisplay("Stop Loss", "Initial risk distance in price units", "Risk");
_takeProfit = Param(nameof(TakeProfit), 2000m).SetNotNegative()
.SetDisplay("Take Profit", "Target distance in price units", "Risk");
_trailingStart = Param(nameof(TrailingStart), 300m).SetNotNegative()
.SetDisplay("Trailing Start", "Profit at which trailing starts", "Risk");
_trailingStop = Param(nameof(TrailingStop), 200m).SetGreaterThanZero()
.SetDisplay("Trailing Stop", "Trailing distance from favorable quote", "Risk");
_breakevenGain = Param(nameof(BreakevenGain), 250m).SetNotNegative()
.SetDisplay("Breakeven Gain", "Profit at which breakeven activates", "Risk");
_breakeven = Param(nameof(Breakeven), 100m).SetNotNegative()
.SetDisplay("Breakeven", "Profit locked on breakeven", "Risk");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
protected override void OnReseted()
{
base.OnReseted();
ClearPositionTracking();
_pendingOrder = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ClearPositionTracking();
_pendingOrder = null;
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var subscription = new Subscription(DataType.Level1, Security);
subscription.MarketData.BuildField = field;
SubscribeLevel1(subscription).Bind(ProcessQuote).Start();
}
}
private void ClearPositionTracking()
{
_entryPrice = _stopPrice = _takePrice = _favorableExtreme = null;
_trackedVolume = 0m;
}
protected override void OnNewPosition(Position position)
{
base.OnNewPosition(position);
ObservePosition(position);
}
protected override void OnPositionChanged(Position position)
{
base.OnPositionChanged(position);
ObservePosition(position);
}
protected override void OnPositionReceived(Position position)
{
base.OnPositionReceived(position);
ObservePosition(position);
}
private void ObservePosition(Position position)
{
if (position.Security != Security || position.Portfolio != Portfolio)
return;
var volume = position.CurrentValue ?? 0m;
if (volume == 0m)
{
ClearPositionTracking();
return;
}
if (position.AveragePrice is not decimal average || average <= 0m)
return;
if (_entryPrice == average && _trackedVolume == volume)
return;
// Retain an already tightened stop after a partial same-side reduction.
if (_entryPrice == average && _trackedVolume * volume > 0m
&& Math.Abs(volume) < Math.Abs(_trackedVolume))
{
_trackedVolume = volume;
return;
}
_entryPrice = average;
_trackedVolume = volume;
_favorableExtreme = average;
_stopPrice = StopLoss > 0m
? volume > 0m ? average - StopLoss : average + StopLoss
: null;
_takePrice = TakeProfit > 0m
? volume > 0m ? average + TakeProfit : average - TakeProfit
: null;
}
private bool HasPendingOrder()
=> _pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed);
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished || Position != 0m
|| !IsFormedAndOnlineAndAllowTrading() || HasPendingOrder())
return;
if (candle.ClosePrice > candle.OpenPrice)
_pendingOrder = BuyMarket(Volume);
else if (candle.ClosePrice < candle.OpenPrice)
_pendingOrder = SellMarket(Volume);
}
private void ProcessQuote(Level1ChangeMessage quote)
{
if (Position == 0m || _entryPrice is not decimal entry
|| !IsFormedAndOnlineAndAllowTrading() || HasPendingOrder())
return;
var isLong = Position > 0m;
var field = isLong ? Level1Fields.BestBidPrice : Level1Fields.BestAskPrice;
if (!quote.Changes.TryGetValue(field, out var raw) || raw is not decimal price || price <= 0m)
return;
var best = _favorableExtreme ?? entry;
_favorableExtreme = best = isLong ? Math.Max(best, price) : Math.Min(best, price);
var gain = isLong ? best - entry : entry - best;
if (gain >= BreakevenGain)
{
var candidate = isLong ? entry + Breakeven : entry - Breakeven;
TightenStop(isLong, candidate);
}
if (gain >= TrailingStart)
{
var candidate = isLong ? best - TrailingStop : best + TrailingStop;
TightenStop(isLong, candidate);
}
var hitStop = _stopPrice is decimal stop && (isLong ? price <= stop : price >= stop);
var hitTake = _takePrice is decimal take && (isLong ? price >= take : price <= take);
if (!hitStop && !hitTake)
return;
var exit = new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = isLong ? Sides.Sell : Sides.Buy,
Volume = Math.Abs(Position),
Comment = hitStop ? "Lacust stop / breakeven / trailing" : "Lacust take profit",
};
_pendingOrder = exit;
RegisterOrder(exit);
}
private void TightenStop(bool isLong, decimal candidate)
=> _stopPrice = _stopPrice is decimal old
? isLong ? Math.Max(old, candidate) : Math.Min(old, candidate)
: candidate;
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates, Level1Fields, OrderTypes, Sides, OrderStates
from StockSharp.BusinessEntities import Order, Subscription
from StockSharp.Algo.Strategies import Strategy
class lacust_stop_and_be_strategy(Strategy):
"""Filled-price SL/TP, breakeven and monotonic trailing on executable bid/ask."""
def __init__(self):
super(lacust_stop_and_be_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))).SetDisplay(
"Candle Type", "Signal candles", "General")
self._stop_loss = self.Param("StopLoss", 400.0).SetNotNegative().SetDisplay(
"Stop Loss", "Initial distance in price units", "Risk")
self._take_profit = self.Param("TakeProfit", 2000.0).SetNotNegative().SetDisplay(
"Take Profit", "Target distance in price units", "Risk")
self._trailing_start = self.Param("TrailingStart", 300.0).SetNotNegative().SetDisplay(
"Trailing Start", "Profit at which trailing starts", "Risk")
self._trailing_stop = self.Param("TrailingStop", 200.0).SetGreaterThanZero().SetDisplay(
"Trailing Stop", "Trailing distance from favorable quote", "Risk")
self._breakeven_gain = self.Param("BreakevenGain", 250.0).SetNotNegative().SetDisplay(
"Breakeven Gain", "Profit at which breakeven activates", "Risk")
self._breakeven = self.Param("Breakeven", 100.0).SetNotNegative().SetDisplay(
"Breakeven", "Profit locked after breakeven", "Risk")
self._entry_price = None
self._stop_price = None
self._take_price = None
self._extreme = None
self._tracked_volume = Decimal(0)
self._pending_order = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def _clear_tracking(self):
self._entry_price = self._stop_price = self._take_price = self._extreme = None
self._tracked_volume = Decimal(0)
def OnReseted(self):
super(lacust_stop_and_be_strategy, self).OnReseted()
self._clear_tracking()
self._pending_order = None
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType), (self.Security, DataType.Level1)]
def OnStarted2(self, time):
super(lacust_stop_and_be_strategy, self).OnStarted2(time)
self._clear_tracking()
self._pending_order = None
self.SubscribeCandles(self.CandleType).Bind(self.ProcessCandle).Start()
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
subscription = Subscription(DataType.Level1, self.Security)
subscription.MarketData.BuildField = field
self.SubscribeLevel1(subscription).Bind(self.ProcessQuote).Start()
def OnNewPosition(self, position):
super(lacust_stop_and_be_strategy, self).OnNewPosition(position)
self._observe_position(position)
def OnPositionChanged(self, position):
super(lacust_stop_and_be_strategy, self).OnPositionChanged(position)
self._observe_position(position)
def OnPositionReceived(self, position):
super(lacust_stop_and_be_strategy, self).OnPositionReceived(position)
self._observe_position(position)
def _observe_position(self, position):
if position.Security != self.Security or position.Portfolio != self.Portfolio:
return
volume = position.CurrentValue if position.CurrentValue is not None else Decimal(0)
if volume == 0:
self._clear_tracking()
return
average = position.AveragePrice
if average is None or average <= 0:
return
if self._entry_price == average and self._tracked_volume == volume:
return
if (self._entry_price == average and self._tracked_volume * volume > 0
and abs(volume) < abs(self._tracked_volume)):
self._tracked_volume = volume
return
self._entry_price = average
self._tracked_volume = volume
self._extreme = average
stop = Decimal(self._stop_loss.Value)
take = Decimal(self._take_profit.Value)
self._stop_price = (average - stop if volume > 0 else average + stop) if stop > 0 else None
self._take_price = (average + take if volume > 0 else average - take) if take > 0 else None
def _pending(self):
return self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed)
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished or self.Position != 0:
return
if not self.IsFormedAndOnlineAndAllowTrading() or self._pending():
return
if candle.ClosePrice > candle.OpenPrice:
self._pending_order = self.BuyMarket(self.Volume)
elif candle.ClosePrice < candle.OpenPrice:
self._pending_order = self.SellMarket(self.Volume)
def _tighten_stop(self, is_long, candidate):
if self._stop_price is None:
self._stop_price = candidate
elif is_long:
self._stop_price = max(self._stop_price, candidate)
else:
self._stop_price = min(self._stop_price, candidate)
def ProcessQuote(self, quote):
if self.Position == 0 or self._entry_price is None:
return
if not self.IsFormedAndOnlineAndAllowTrading() or self._pending():
return
is_long = self.Position > 0
field = Level1Fields.BestBidPrice if is_long else Level1Fields.BestAskPrice
if not quote.Changes.ContainsKey(field):
return
price = Decimal(quote.Changes[field])
if price <= 0:
return
entry = self._entry_price
best = self._extreme if self._extreme is not None else entry
best = max(best, price) if is_long else min(best, price)
self._extreme = best
gain = (best - entry) if is_long else (entry - best)
if gain >= Decimal(self._breakeven_gain.Value):
candidate = entry + Decimal(self._breakeven.Value) if is_long else entry - Decimal(self._breakeven.Value)
self._tighten_stop(is_long, candidate)
if gain >= Decimal(self._trailing_start.Value):
trailing = Decimal(self._trailing_stop.Value)
self._tighten_stop(is_long, best - trailing if is_long else best + trailing)
hit_stop = self._stop_price is not None and (price <= self._stop_price if is_long else price >= self._stop_price)
hit_take = self._take_price is not None and (price >= self._take_price if is_long else price <= self._take_price)
if not (hit_stop or hit_take):
return
exit_order = Order()
exit_order.Security = self.Security
exit_order.Portfolio = self.Portfolio
exit_order.Type = OrderTypes.Market
exit_order.Side = Sides.Sell if is_long else Sides.Buy
exit_order.Volume = abs(self.Position)
exit_order.Comment = "Lacust stop / breakeven / trailing" if hit_stop else "Lacust take profit"
self._pending_order = exit_order
self.RegisterOrder(exit_order)
def CreateClone(self):
return lacust_stop_and_be_strategy()