The Fluctuate Strategy is a StockSharp port of the MetaTrader expert advisor "Fluctuate". It reproduces the original grid-like behaviour using the high-level API: a candle subscription drives all decisions, market entries are performed with BuyMarket / SellMarket, and recovery orders are placed with stop orders. Long and short exposure are tracked separately to mimic the hedging-style position accounting used in MetaTrader, while the actual StockSharp position remains netted.
Core idea
Every time a new candle closes, the strategy compares the last two close prices. A higher close opens a market buy, a lower close opens a market sell. If both closes are equal the bar is ignored.
Each filled position receives a fixed stop-loss and take-profit (expressed in pips). The strategy also records the exact fill price and the net volume added by the trade.
After an entry, an opposite stop order is armed StepPips away from the last fill (plus a small spread buffer). Its volume is derived from the previous trade and the LotCoefficient, optionally using the cumulative exposure when MultiplyLotCoefficient = true.
When the stop order triggers, it cancels the previous pending order, updates the internal exposure statistics and immediately schedules a new recovery stop order in the other direction. This reproduces the averaging / martingale loop present in the MQL implementation.
Trailing protection raises (or lowers) the stop once price moves at least TrailingStopPips + TrailingStepPips in favour of the position. This emulates the original EA which required an extra profit buffer before tightening the stop.
Trading workflow
Signal detection. The candle feed is subscribed via SubscribeCandles. Only finished candles are processed. The strategy refuses to trade outside the [StartHour, EndHour) time window or when the equity guard is triggered.
Initial position sizing. Depending on PositionSizingMode the first trade in a sequence either uses a fixed lot (FixedVolume) or a risk-based lot (RiskPercent). In risk mode the allowed risk (percentage of current equity) is divided by the monetary loss that would occur if the stop-loss is hit. Price step and step-price are used to convert pips to currency.
Exposure accounting. Separate accumulators track long and short volume, average price and the extreme price reached since entry. This allows the strategy to keep both sides "open" internally even though StockSharp uses netting.
Recovery orders. After every fill the algorithm computes the next stop-order volume:
When MultiplyLotCoefficient = false the new volume equals LastVolume × LotCoefficient.
When true the total absolute exposure is multiplied by LotCoefficient.
The volume is normalised to exchange constraints (step, min and max volume) and rejected when it would exceed MaxTotalVolume or the number of active positions plus orders would exceed MaxPositions.
Profit target & equity guard. Aggregated unrealised PnL is calculated by translating price differences into currency using PriceStep/StepPrice. If it reaches ProfitTarget, all positions are closed and pending orders are cancelled. Trading is also suspended when equity drops below MinEquityPercent of the initial balance.
Trailing logic. For long positions the highest price seen since entry is recorded. Once it exceeds the entry price by TrailingStopPips + TrailingStepPips, a trailing stop is set TrailingStopPips behind the high. Short positions apply the symmetric rule with the lowest price. Trailing updates override the fixed stop-loss.
Risk management details
Stop / take profit. Both are optional (set the pip value to zero to disable). They are recalculated for the aggregated long or short exposure whenever a new trade adds volume.
Max positions. Counts the number of open sides (long + short) plus the active recovery stop order. When the limit is reached, the strategy refuses to submit new stop orders.
Max total volume. Limits the sum of absolute open volume and the volume of the active recovery order.
CloseAllAtStart. Optional safety switch to flatten the book before the strategy starts trading.
Parameters
Name
Description
Default
CandleType
Primary timeframe used for signal detection.
1-minute time frame
StopLossPips
Distance between entry price and stop-loss (pips). 0 disables the stop.
50
TakeProfitPips
Distance between entry price and take-profit (pips). 0 disables the take-profit.
Trading window end hour (exclusive, exchange time).
20
PositionSizingMode
FixedVolume for static lots, RiskPercent for percent-of-equity sizing.
FixedVolume
VolumeOrRisk
Fixed lot size (when FixedVolume) or risk percentage (when RiskPercent).
1.0
Implementation notes
Stop-order prices use a minimal spread approximation (PriceStep when available) because MetaTrader required the order to be outside the freeze level. Adjust StepPips if the actual spread is wider.
The strategy cancels any remaining recovery order whenever a new trade fills. This matches the original EA which deleted all pending orders after an execution.
Because StockSharp portfolios are netted, hedged exposure is simulated internally. The actual broker position will always reflect the net quantity.
Risk-based position sizing requires valid PriceStep and StepPrice values from the instrument description.
Usage tips
Select an appropriate candle type that matches the original EA testing timeframe (typically M5 or M15) for best fidelity.
Double-check exchange volume limits: if the normalised recovery volume becomes zero, the strategy will stop adding new legs.
When PositionSizingMode = RiskPercent, ensure the portfolio contains up-to-date equity information; otherwise the strategy falls back to the fixed lot size.
Combine with StockSharp's built-in StrategyProtection (enabled via StartProtection()) to add additional account-level safeguards if needed.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Fluctuate strategy using EMA crossover with stop-loss and take-profit.
/// Buys when fast EMA crosses above slow EMA, sells on reverse cross.
/// </summary>
public class FluctuateStrategy : Strategy
{
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private ExponentialMovingAverage _fast;
private ExponentialMovingAverage _slow;
private decimal _prevFast;
private decimal _prevSlow;
private decimal _entryPrice;
private int _cooldown;
/// <summary>
/// Fast EMA period.
/// </summary>
public int FastPeriod
{
get => _fastPeriod.Value;
set => _fastPeriod.Value = value;
}
/// <summary>
/// Slow EMA period.
/// </summary>
public int SlowPeriod
{
get => _slowPeriod.Value;
set => _slowPeriod.Value = value;
}
/// <summary>
/// Stop-loss distance in price steps.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance in price steps.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Initialize <see cref="FluctuateStrategy"/>.
/// </summary>
public FluctuateStrategy()
{
_fastPeriod = Param(nameof(FastPeriod), 50)
.SetGreaterThanZero()
.SetDisplay("Fast Period", "Fast EMA period", "Indicator");
_slowPeriod = Param(nameof(SlowPeriod), 200)
.SetGreaterThanZero()
.SetDisplay("Slow Period", "Slow EMA period", "Indicator");
_stopLossPoints = Param(nameof(StopLossPoints), 200)
.SetNotNegative()
.SetDisplay("Stop Loss", "Stop-loss distance in price steps", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 400)
.SetNotNegative()
.SetDisplay("Take Profit", "Take-profit distance in price steps", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fast = null;
_slow = null;
_prevFast = 0;
_prevSlow = 0;
_entryPrice = 0;
_cooldown = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fast = new ExponentialMovingAverage { Length = FastPeriod };
_slow = new ExponentialMovingAverage { Length = SlowPeriod };
var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
subscription.Bind(_fast, _slow, ProcessCandle);
subscription.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_fast.IsFormed || !_slow.IsFormed)
{
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
if (_cooldown > 0)
{
_cooldown--;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
var close = candle.ClosePrice;
var step = Security?.PriceStep ?? 1m;
// Check SL/TP
if (Position > 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step)
{
SellMarket();
_entryPrice = 0;
_cooldown = 50;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step)
{
SellMarket();
_entryPrice = 0;
_cooldown = 50;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
}
else if (Position < 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step)
{
BuyMarket();
_entryPrice = 0;
_cooldown = 50;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step)
{
BuyMarket();
_entryPrice = 0;
_cooldown = 50;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
}
// EMA crossover buy signal
if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
_entryPrice = close;
_cooldown = 50;
}
// EMA crossover sell signal
else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
_entryPrice = close;
_cooldown = 50;
}
_prevFast = fastValue;
_prevSlow = slowValue;
}
}
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 ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class fluctuate_strategy(Strategy):
def __init__(self):
super(fluctuate_strategy, self).__init__()
self._fast_period = self.Param("FastPeriod", 50) \
.SetDisplay("Fast Period", "Fast EMA period", "Indicator")
self._slow_period = self.Param("SlowPeriod", 200) \
.SetDisplay("Slow Period", "Slow EMA period", "Indicator")
self._stop_loss_points = self.Param("StopLossPoints", 200) \
.SetDisplay("Stop Loss", "Stop-loss distance in price steps", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 400) \
.SetDisplay("Take Profit", "Take-profit distance in price steps", "Risk")
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
@property
def fast_period(self):
return self._fast_period.Value
@property
def slow_period(self):
return self._slow_period.Value
@property
def stop_loss_points(self):
return self._stop_loss_points.Value
@property
def take_profit_points(self):
return self._take_profit_points.Value
def OnReseted(self):
super(fluctuate_strategy, self).OnReseted()
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(fluctuate_strategy, self).OnStarted2(time)
self._fast = ExponentialMovingAverage()
self._fast.Length = self.fast_period
self._slow = ExponentialMovingAverage()
self._slow.Length = self.slow_period
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(5)))
subscription.Bind(self._fast, self._slow, self._process_candle)
subscription.Start()
def _process_candle(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_value)
slow_val = float(slow_value)
if not self._fast.IsFormed or not self._slow.IsFormed:
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self._cooldown > 0:
self._cooldown -= 1
self._prev_fast = fast_val
self._prev_slow = slow_val
return
close = float(candle.ClosePrice)
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
# Check SL/TP
if self.Position > 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close <= self._entry_price - self.stop_loss_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close >= self._entry_price + self.take_profit_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
return
elif self.Position < 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close >= self._entry_price + self.stop_loss_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close <= self._entry_price - self.take_profit_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
return
# EMA crossover buy signal
if self._prev_fast <= self._prev_slow and fast_val > slow_val and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._entry_price = close
self._cooldown = 50
# EMA crossover sell signal
elif self._prev_fast >= self._prev_slow and fast_val < slow_val and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._entry_price = close
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
def CreateClone(self):
return fluctuate_strategy()