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>
/// Trend-following strategy converted from the FarhadCrab1 MT5 expert advisor.
/// The strategy enters on pullbacks to an EMA, manages risk with pip-based levels,
/// and closes positions based on a daily EMA crossover filter.
/// </summary>
public class FarhadCrab1Strategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _maLength;
private readonly StrategyParam<int> _maShift;
private readonly StrategyParam<int> _dailyMaLength;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _trailingStopPips;
private readonly StrategyParam<decimal> _trailingStepPips;
private readonly StrategyParam<decimal> _orderVolume;
private readonly Queue<decimal> _maValues = new();
private readonly DataType _dailyCandleType = TimeSpan.FromHours(1).TimeFrame();
private decimal? _stopLossPrice;
private decimal? _takeProfitPrice;
private decimal? _prevDailyClose;
private decimal? _prevDailyMa;
private decimal? _prevPrevDailyClose;
private decimal? _prevPrevDailyMa;
private ICandleMessage _previousCandle;
private decimal _entryPrice;
/// <summary>
/// Working candle type for the execution timeframe.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Period of the EMA used on the working timeframe.
/// </summary>
public int MaLength
{
get => _maLength.Value;
set => _maLength.Value = value;
}
/// <summary>
/// Number of completed candles to shift the EMA backwards.
/// </summary>
public int MaShift
{
get => _maShift.Value;
set => _maShift.Value = value;
}
/// <summary>
/// Period of the daily EMA that closes positions on crossovers.
/// </summary>
public int DailyMaLength
{
get => _dailyMaLength.Value;
set => _dailyMaLength.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit distance expressed in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing stop distance in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Additional pip distance required before updating the trailing stop again.
/// </summary>
public decimal TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Base order volume in lots/contracts.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="FarhadCrab1Strategy"/> class.
/// </summary>
public FarhadCrab1Strategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Execution timeframe", "General");
_maLength = Param(nameof(MaLength), 15)
.SetGreaterThanZero()
.SetDisplay("EMA Length", "EMA period on the working timeframe", "Indicators");
_maShift = Param(nameof(MaShift), 0)
.SetRange(0, 100)
.SetDisplay("EMA Shift", "Shift EMA value backwards by N candles", "Indicators");
_dailyMaLength = Param(nameof(DailyMaLength), 15)
.SetGreaterThanZero()
.SetDisplay("Daily EMA Length", "EMA period used on daily candles", "Indicators");
_stopLossPips = Param(nameof(StopLossPips), 50m)
.SetRange(0m, 500m)
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Protection");
_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
.SetRange(0m, 500m)
.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Protection");
_trailingStopPips = Param(nameof(TrailingStopPips), 10m)
.SetRange(0m, 500m)
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Protection");
_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
.SetRange(0m, 500m)
.SetDisplay("Trailing Step (pips)", "Extra gain in pips before updating the trailing stop", "Protection");
_orderVolume = Param(nameof(OrderVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Volume", "Base order volume", "Trading");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType), (Security, _dailyCandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_maValues.Clear();
_stopLossPrice = null;
_takeProfitPrice = null;
_prevDailyClose = null;
_prevDailyMa = null;
_prevPrevDailyClose = null;
_prevPrevDailyMa = null;
_previousCandle = null;
_entryPrice = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Ensure the base strategy volume reflects the configured parameter.
base.Volume = OrderVolume;
// Subscribe to the working timeframe candles with an EMA for entry decisions.
var ema = new ExponentialMovingAverage { Length = MaLength };
var candleSubscription = SubscribeCandles(CandleType);
candleSubscription
.Bind(ema, ProcessWorkingCandle)
.Start();
// Subscribe to daily candles with another EMA for exit filtering.
var dailyEma = new ExponentialMovingAverage { Length = DailyMaLength };
var dailySubscription = SubscribeCandles(_dailyCandleType);
dailySubscription
.Bind(dailyEma, ProcessDailyCandle)
.Start();
// Draw candles, indicator, and trades on the chart if charting is available.
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, candleSubscription);
DrawOwnTrades(area);
DrawIndicator(area, ema);
}
}
private void ProcessDailyCandle(ICandleMessage candle, decimal emaValue)
{
// Process only finished daily candles.
if (candle.State != CandleStates.Finished)
return;
_prevPrevDailyClose = _prevDailyClose;
_prevPrevDailyMa = _prevDailyMa;
_prevDailyClose = candle.ClosePrice;
_prevDailyMa = emaValue;
}
private void ProcessWorkingCandle(ICandleMessage candle, decimal emaValue)
{
// Use only completed candles for decision making.
if (candle.State != CandleStates.Finished)
return;
// Store EMA values so we can apply the configured shift.
UpdateMaBuffer(emaValue);
var shiftedMa = GetShiftedMaValue();
if (shiftedMa == null)
{
_previousCandle = candle;
return;
}
// Require at least one previous candle to evaluate entry conditions.
if (_previousCandle == null)
{
_previousCandle = candle;
return;
}
// Close positions when the daily EMA filter signals a crossover against us.
if (TryCloseByDailyFilter())
{
_previousCandle = candle;
return;
}
var pipSize = GetPipSize();
// Check for stop-loss or take-profit triggers before adjusting trailing stops.
if (CheckStopsAndTargets(candle))
{
_previousCandle = candle;
return;
}
// Update trailing stop levels if the position has moved far enough.
ApplyTrailingStop(candle, pipSize);
// Evaluate long entry condition: previous low above the EMA.
if (Position <= 0 && _previousCandle.LowPrice > shiftedMa.Value)
{
var volume = OrderVolume + (Position < 0 ? -Position : 0m);
if (volume > 0)
{
BuyMarket(volume);
SetRiskLevels(candle.ClosePrice, pipSize, true);
}
_previousCandle = candle;
return;
}
// Evaluate short entry condition: previous high below the EMA.
if (Position >= 0 && _previousCandle.HighPrice < shiftedMa.Value)
{
var volume = OrderVolume + (Position > 0 ? Position : 0m);
if (volume > 0)
{
SellMarket(volume);
SetRiskLevels(candle.ClosePrice, pipSize, false);
}
_previousCandle = candle;
return;
}
// Store the current candle for the next iteration.
_previousCandle = candle;
}
private bool TryCloseByDailyFilter()
{
if (_prevDailyClose == null || _prevDailyMa == null || _prevPrevDailyClose == null || _prevPrevDailyMa == null)
return false;
var prevClose = _prevDailyClose.Value;
var prevMa = _prevDailyMa.Value;
var prev2Close = _prevPrevDailyClose.Value;
var prev2Ma = _prevPrevDailyMa.Value;
// Bearish crossover: EMA moved above the daily close -> exit long positions.
if (Position > 0 && prevMa > prevClose && prev2Ma < prev2Close)
{
SellMarket(Position);
ResetRiskLevels();
return true;
}
// Bullish crossover: EMA moved below the daily close -> exit short positions.
if (Position < 0 && prevMa < prevClose && prev2Ma > prev2Close)
{
BuyMarket(-Position);
ResetRiskLevels();
return true;
}
return false;
}
private bool CheckStopsAndTargets(ICandleMessage candle)
{
if (Position > 0)
{
if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
{
SellMarket(Position);
ResetRiskLevels();
return true;
}
if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
{
SellMarket(Position);
ResetRiskLevels();
return true;
}
}
else if (Position < 0)
{
if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
{
BuyMarket(-Position);
ResetRiskLevels();
return true;
}
if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
{
BuyMarket(-Position);
ResetRiskLevels();
return true;
}
}
else if (_stopLossPrice.HasValue || _takeProfitPrice.HasValue)
{
// Clear stored levels when the position is flat.
ResetRiskLevels();
}
return false;
}
private void ApplyTrailingStop(ICandleMessage candle, decimal pipSize)
{
if (TrailingStopPips <= 0m)
return;
var entryPrice = _entryPrice;
var threshold = (TrailingStopPips + TrailingStepPips) * pipSize;
if (Position > 0)
{
var profit = candle.ClosePrice - entryPrice;
if (profit > threshold)
{
var minStop = candle.ClosePrice - threshold;
var candidate = candle.ClosePrice - TrailingStopPips * pipSize;
if (!_stopLossPrice.HasValue || _stopLossPrice.Value < minStop)
_stopLossPrice = candidate;
}
}
else if (Position < 0)
{
var profit = entryPrice - candle.ClosePrice;
if (profit > threshold)
{
var maxStop = candle.ClosePrice + threshold;
var candidate = candle.ClosePrice + TrailingStopPips * pipSize;
if (!_stopLossPrice.HasValue || _stopLossPrice.Value > maxStop)
_stopLossPrice = candidate;
}
}
}
private void SetRiskLevels(decimal executionPrice, decimal pipSize, bool isLong)
{
_entryPrice = executionPrice;
if (StopLossPips > 0m && pipSize > 0m)
_stopLossPrice = isLong
? executionPrice - StopLossPips * pipSize
: executionPrice + StopLossPips * pipSize;
else
_stopLossPrice = null;
if (TakeProfitPips > 0m && pipSize > 0m)
_takeProfitPrice = isLong
? executionPrice + TakeProfitPips * pipSize
: executionPrice - TakeProfitPips * pipSize;
else
_takeProfitPrice = null;
}
private void ResetRiskLevels()
{
_stopLossPrice = null;
_takeProfitPrice = null;
}
private decimal GetPipSize()
{
var security = Security;
if (security == null)
return 0.0001m;
var step = security.PriceStep ?? 0.0001m;
var decimals = security.Decimals;
if (decimals == 3 || decimals == 5)
return step * 10m;
return step;
}
private void UpdateMaBuffer(decimal emaValue)
{
_maValues.Enqueue(emaValue);
var maxCount = Math.Max(MaShift + 1, 1);
while (_maValues.Count > maxCount)
_maValues.Dequeue();
}
private decimal? GetShiftedMaValue()
{
var count = _maValues.Count;
var targetIndex = count - MaShift - 1;
if (targetIndex < 0)
return null;
var index = 0;
foreach (var value in _maValues)
{
if (index == targetIndex)
return value;
index++;
}
return 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 collections import deque
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class farhad_crab1_strategy(Strategy):
"""
FarhadCrab1: Trend-following on EMA pullbacks with trailing stop,
daily EMA crossover filter for exits, and pip-based SL/TP.
"""
def __init__(self):
super(farhad_crab1_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Execution timeframe", "General")
self._ma_length = self.Param("MaLength", 15) \
.SetDisplay("EMA Length", "EMA period", "Indicators")
self._ma_shift = self.Param("MaShift", 0) \
.SetDisplay("EMA Shift", "Shift EMA backwards by N candles", "Indicators")
self._daily_ma_length = self.Param("DailyMaLength", 15) \
.SetDisplay("Daily EMA Length", "EMA period on daily candles", "Indicators")
self._stop_loss_pips = self.Param("StopLossPips", 50.0) \
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Protection")
self._take_profit_pips = self.Param("TakeProfitPips", 50.0) \
.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Protection")
self._trailing_stop_pips = self.Param("TrailingStopPips", 10.0) \
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance", "Protection")
self._trailing_step_pips = self.Param("TrailingStepPips", 5.0) \
.SetDisplay("Trailing Step (pips)", "Extra gain before updating trailing stop", "Protection")
self._daily_candle_type = DataType.TimeFrame(TimeSpan.FromHours(1))
self._ma_values = deque()
self._stop_loss_price = None
self._take_profit_price = None
self._prev_daily_close = None
self._prev_daily_ma = None
self._prev_prev_daily_close = None
self._prev_prev_daily_ma = None
self._previous_candle = None
self._entry_price = 0.0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(farhad_crab1_strategy, self).OnReseted()
self._ma_values.clear()
self._stop_loss_price = None
self._take_profit_price = None
self._prev_daily_close = None
self._prev_daily_ma = None
self._prev_prev_daily_close = None
self._prev_prev_daily_ma = None
self._previous_candle = None
self._entry_price = 0.0
def OnStarted2(self, time):
super(farhad_crab1_strategy, self).OnStarted2(time)
ema = ExponentialMovingAverage()
ema.Length = self._ma_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(ema, self._process_working_candle).Start()
daily_ema = ExponentialMovingAverage()
daily_ema.Length = self._daily_ma_length.Value
daily_sub = self.SubscribeCandles(self._daily_candle_type)
daily_sub.Bind(daily_ema, self._process_daily_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ema)
self.DrawOwnTrades(area)
def _process_daily_candle(self, candle, ema_val):
if candle.State != CandleStates.Finished:
return
self._prev_prev_daily_close = self._prev_daily_close
self._prev_prev_daily_ma = self._prev_daily_ma
self._prev_daily_close = float(candle.ClosePrice)
self._prev_daily_ma = float(ema_val)
def _process_working_candle(self, candle, ema_val):
if candle.State != CandleStates.Finished:
return
ema_val = float(ema_val)
self._update_ma_buffer(ema_val)
shifted_ma = self._get_shifted_ma()
if shifted_ma is None:
self._previous_candle = candle
return
if self._previous_candle is None:
self._previous_candle = candle
return
if self._try_close_by_daily_filter():
self._previous_candle = candle
return
pip_size = self._get_pip_size()
close = float(candle.ClosePrice)
if self._check_stops(candle):
self._previous_candle = candle
return
self._apply_trailing(candle, pip_size)
prev_low = float(self._previous_candle.LowPrice)
prev_high = float(self._previous_candle.HighPrice)
if self.Position <= 0 and prev_low > shifted_ma:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._set_risk_levels(close, pip_size, True)
self._previous_candle = candle
return
if self.Position >= 0 and prev_high < shifted_ma:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._set_risk_levels(close, pip_size, False)
self._previous_candle = candle
return
self._previous_candle = candle
def _try_close_by_daily_filter(self):
if (self._prev_daily_close is None or self._prev_daily_ma is None or
self._prev_prev_daily_close is None or self._prev_prev_daily_ma is None):
return False
if (self.Position > 0 and
self._prev_daily_ma > self._prev_daily_close and
self._prev_prev_daily_ma < self._prev_prev_daily_close):
self.SellMarket()
self._reset_risk()
return True
if (self.Position < 0 and
self._prev_daily_ma < self._prev_daily_close and
self._prev_prev_daily_ma > self._prev_prev_daily_close):
self.BuyMarket()
self._reset_risk()
return True
return False
def _check_stops(self, candle):
low = float(candle.LowPrice)
high = float(candle.HighPrice)
if self.Position > 0:
if self._stop_loss_price is not None and low <= self._stop_loss_price:
self.SellMarket()
self._reset_risk()
return True
if self._take_profit_price is not None and high >= self._take_profit_price:
self.SellMarket()
self._reset_risk()
return True
elif self.Position < 0:
if self._stop_loss_price is not None and high >= self._stop_loss_price:
self.BuyMarket()
self._reset_risk()
return True
if self._take_profit_price is not None and low <= self._take_profit_price:
self.BuyMarket()
self._reset_risk()
return True
elif self._stop_loss_price is not None or self._take_profit_price is not None:
self._reset_risk()
return False
def _apply_trailing(self, candle, pip_size):
ts_pips = float(self._trailing_stop_pips.Value)
if ts_pips <= 0:
return
step_pips = float(self._trailing_step_pips.Value)
threshold = (ts_pips + step_pips) * pip_size
close = float(candle.ClosePrice)
if self.Position > 0:
profit = close - self._entry_price
if profit > threshold:
candidate = close - ts_pips * pip_size
min_stop = close - threshold
if self._stop_loss_price is None or self._stop_loss_price < min_stop:
self._stop_loss_price = candidate
elif self.Position < 0:
profit = self._entry_price - close
if profit > threshold:
candidate = close + ts_pips * pip_size
max_stop = close + threshold
if self._stop_loss_price is None or self._stop_loss_price > max_stop:
self._stop_loss_price = candidate
def _set_risk_levels(self, price, pip_size, is_long):
self._entry_price = price
sl_pips = float(self._stop_loss_pips.Value)
tp_pips = float(self._take_profit_pips.Value)
if sl_pips > 0 and pip_size > 0:
self._stop_loss_price = price - sl_pips * pip_size if is_long else price + sl_pips * pip_size
else:
self._stop_loss_price = None
if tp_pips > 0 and pip_size > 0:
self._take_profit_price = price + tp_pips * pip_size if is_long else price - tp_pips * pip_size
else:
self._take_profit_price = None
def _reset_risk(self):
self._stop_loss_price = None
self._take_profit_price = None
def _get_pip_size(self):
if self.Security is None:
return 0.0001
step = float(self.Security.PriceStep) if self.Security.PriceStep is not None else 0.0001
decimals = self.Security.Decimals
if decimals == 3 or decimals == 5:
return step * 10
return step
def _update_ma_buffer(self, ema_val):
self._ma_values.append(ema_val)
max_count = max(self._ma_shift.Value + 1, 1)
while len(self._ma_values) > max_count:
self._ma_values.popleft()
def _get_shifted_ma(self):
count = len(self._ma_values)
target = count - self._ma_shift.Value - 1
if target < 0:
return None
idx = 0
for v in self._ma_values:
if idx == target:
return v
idx += 1
return None
def CreateClone(self):
return farhad_crab1_strategy()