Farhad Crab — это порт оригинального советника MetaTrader FarhadCrab1.mq4 на высокоуровневый API StockSharp. Изначально алгоритм предназначался для минутных графиков пар GBP/JPY, GBP/USD и EUR/USD и комбинировал агрессивные входы по движению с динамическим сопровождением позиций. В новой реализации вся логика переписана на C#, а обмен данными происходит исключительно через подписки StockSharp.
В рабочем таймфрейме стратегия рассчитывает две скользящие средние: 9-периодную EMA от типичной цены и 9-периодную SMA от цен открытия свечи. Дополнительно ведётся дневная 55-периодная сглаженная средняя (SMMA), служащая «страховкой» от глобального разворота. Если минимум текущей свечи держится выше EMA и позиция отсутствует, открывается покупка. Если максимум остаётся ниже SMA от открытий, открывается продажа. Пересечение дневной SMMA с ценой закрытия в противоположную сторону немедленно закрывает позицию.
Система управления выходами повторяет настройки советника: отдельные значения тейк-профита (в пунктах) для длинных и коротких сделок, а также независимые параметры трейлинг-стопа. Трейлинг активируется только после движения цены на заданную величину, как и в MetaTrader. Закрытия выполняются рыночными заявками, что соответствует событиям высокоуровневого API.
Ключевые особенности
Полное соответствие индикаторной базе — EMA(9) по типичной цене, SMA(9) по ценам открытия и дневная SMMA(55).
Работа с несколькими таймфреймами — одновременная подписка на торговый и дневной таймфреймы без ручного накопления данных.
Настраиваемые выходы — тейк-профиты и трейлинг-стопы в пунктах, доступные для оптимизации.
Дневной предохранитель — строгая реализация условия закрытия при пересечении дневной SMMA и цены закрытия.
Защита позиций — однократный вызов StartProtection() в момент запуска стратегии.
Параметры
Параметр
Описание
Значение по умолчанию
OrderVolume
Объём сделки при открытии рынка.
0.1
LongTakeProfitPips
Дистанция тейк-профита для покупок в пунктах.
10
ShortTakeProfitPips
Дистанция тейк-профита для продаж в пунктах.
10
LongTrailingStopPips
Дистанция трейлинг-стопа для покупок (0 отключает).
8
ShortTrailingStopPips
Дистанция трейлинг-стопа для продаж (0 отключает).
8
DailyMaPeriod
Период дневной сглаженной средней.
55
CandleType
Основной таймфрейм стратегии (по умолчанию 1 минута).
1m
Все параметры объявлены через StrategyParam<T> и помечены для оптимизации там, где это имеет смысл, поэтому стратегию легко адаптировать под конкретный инструмент.
Правила торговли
Открытие лонга: минимум свечи находится выше EMA(9) по типичной цене, позиция отсутствует.
Открытие шорта: максимум свечи находится ниже SMA(9) по ценам открытия, позиция отсутствует.
Защитное закрытие лонга: дневная SMMA закрывается выше цены закрытия при условии, что ранее была ниже.
Защитное закрытие шорта: дневная SMMA закрывается ниже цены закрытия при условии, что ранее была выше.
Тейк-профит: достижение целевого количества пунктов закрывает позицию.
Трейлинг-стоп: после движения в прибыль на заданное расстояние отслеживается обратный ход и при откате позиция закрывается.
Особенности реализации
Вся обработка данных выполняется через SubscribeCandles().Bind(...), что исключает прямой доступ к буферам индикаторов и соответствует внутренним правилам репозитория.
Размер пункта вычисляется на основании PriceStep с учётом пятого и третьего знаков, как в MetaTrader, поэтому параметры в пунктах работают идентично.
Стоп-приказы не выставляются, вместо этого стратегия контролирует цену и закрывает позиции рыночными ордерами при наступлении условий.
Метод OnReseted очищает все поля, благодаря чему повторные запуски и оптимизации выполняются из чистого состояния.
Рекомендации по использованию
Для повторения исходных результатов используйте минутные свечи по основным валютным парам. Тем не менее параметры позволяют адаптироваться к другим инструментам и волатильности.
Стратегия всегда держит только одну позицию, что упрощает мониторинг и анализ сделок.
В условиях бокового рынка можно сократить расстояние трейлинг-стопа или сосредоточиться на тейк-профите, чтобы уменьшить число ложных выходов.
Увеличение DailyMaPeriod сделает фильтр по дневной средне менее чувствительным, что подойдёт для более долгосрочных сценариев.
Проверяемая формула: на каждой завершённой минутной свече типичная цена равна (high+low+close)/3. EMA(9) по типичной цене инициализируется средним первых девяти значений, затем EMA=(8*EMA+2*typical)/10. Отдельная SMA(9) строится по Open. Без позиции и ожидающей заявки: покупка при low > EMA, иначе продажа при high < SMA(Open). Дневная SMMA(55) прогревается на 55 закрытых дневных свечах и обновляется как (54*SMMA+close)/55. Её пересечение дневной цены закрытия в неблагоприятную сторону закрывает позицию. Тейк и трейлинг рассчитываются от фактической цены исполнения входа и проверяются по завершённым минутным свечам; новый трейлинг действует только на следующих свечах. Сокращённый DailyMaPeriod в тестах проверяет механику, но не default 55. Доходность не гарантируется.
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using System.Linq;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Farhad Crab uses EMA(9) on typical M1 prices, SMA(9) of M1 opens,
/// and daily SMMA protection with direction-specific take/trailing exits.
/// </summary>
public class FarhadCrabStrategy : Strategy
{
private readonly StrategyParam<int> _dailyMaPeriod;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<decimal> _longTakeProfitPips;
private readonly StrategyParam<decimal> _shortTakeProfitPips;
private readonly StrategyParam<decimal> _longTrailingStopPips;
private readonly StrategyParam<decimal> _shortTrailingStopPips;
private readonly StrategyParam<DataType> _candleType;
private readonly Queue<decimal> _typicalSeed = new();
private readonly Queue<decimal> _opens = new();
private readonly Queue<decimal> _dailySeed = new();
private decimal? _emaTypical;
private decimal? _dailySmma;
private decimal? _priorSmma;
private decimal? _priorDailyClose;
private decimal? _entryPrice;
private decimal? _trailingLevel;
private decimal _bestPrice;
private Order _pendingOrder;
private Order _entryOrder;
public int DailyMaPeriod { get => _dailyMaPeriod.Value; set => _dailyMaPeriod.Value = value; }
public decimal OrderVolume { get => _orderVolume.Value; set => _orderVolume.Value = value; }
public decimal LongTakeProfitPips { get => _longTakeProfitPips.Value; set => _longTakeProfitPips.Value = value; }
public decimal ShortTakeProfitPips { get => _shortTakeProfitPips.Value; set => _shortTakeProfitPips.Value = value; }
public decimal LongTrailingStopPips { get => _longTrailingStopPips.Value; set => _longTrailingStopPips.Value = value; }
public decimal ShortTrailingStopPips { get => _shortTrailingStopPips.Value; set => _shortTrailingStopPips.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public FarhadCrabStrategy()
{
_dailyMaPeriod = Param(nameof(DailyMaPeriod), 55).SetGreaterThanZero();
_orderVolume = Param(nameof(OrderVolume), 0.1m).SetGreaterThanZero();
_longTakeProfitPips = Param(nameof(LongTakeProfitPips), 10m).SetNotNegative();
_shortTakeProfitPips = Param(nameof(ShortTakeProfitPips), 10m).SetNotNegative();
_longTrailingStopPips = Param(nameof(LongTrailingStopPips), 8m).SetNotNegative();
_shortTrailingStopPips = Param(nameof(ShortTrailingStopPips), 8m).SetNotNegative();
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame());
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, TimeSpan.FromDays(1).TimeFrame())];
protected override void OnReseted()
{
base.OnReseted();
ClearState();
}
private void ClearState()
{
_typicalSeed.Clear();
_opens.Clear();
_dailySeed.Clear();
_emaTypical = null;
_dailySmma = null;
_priorSmma = null;
_priorDailyClose = null;
_entryPrice = null;
_trailingLevel = null;
_bestPrice = 0m;
_pendingOrder = null;
_entryOrder = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ClearState();
if ((Security?.PriceStep ?? 0m) <= 0m)
throw new InvalidOperationException("Farhad Crab requires a positive instrument PriceStep.");
SubscribeCandles(TimeSpan.FromDays(1).TimeFrame()).Bind(ProcessDailyCandle).Start();
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
}
private decimal PipSize()
{
var step = Security?.PriceStep ?? 0m;
return Security?.Decimals is 3 or 5 ? step * 10m : step;
}
private bool Pending => _pendingOrder is not null &&
_pendingOrder.State is not (OrderStates.Done or OrderStates.Failed);
private void ProcessDailyCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var priorSmma = _dailySmma;
var priorClose = _priorDailyClose;
if (_dailySmma is null)
{
_dailySeed.Enqueue(candle.ClosePrice);
if (_dailySeed.Count == DailyMaPeriod)
_dailySmma = _dailySeed.Average();
}
else
_dailySmma = (_dailySmma.Value * (DailyMaPeriod - 1) + candle.ClosePrice) / DailyMaPeriod;
_priorSmma = priorSmma;
_priorDailyClose = candle.ClosePrice;
if (Pending || Position == 0m || priorSmma is null || priorClose is null || _dailySmma is null)
return;
if (Position > 0m && priorSmma < priorClose && _dailySmma > candle.ClosePrice ||
Position < 0m && priorSmma > priorClose && _dailySmma < candle.ClosePrice)
Exit("Farhad Crab daily safety exit");
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var typical = (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m;
if (_emaTypical is null)
{
_typicalSeed.Enqueue(typical);
if (_typicalSeed.Count == 9)
_emaTypical = _typicalSeed.Average();
}
else
_emaTypical = (_emaTypical.Value * 8m + typical * 2m) / 10m;
_opens.Enqueue(candle.OpenPrice);
while (_opens.Count > 9)
_opens.Dequeue();
if (Pending)
return;
if (Position != 0m)
{
HandleRisk(candle);
return;
}
if (_emaTypical is null || _opens.Count < 9)
return;
if (candle.LowPrice > _emaTypical.Value)
Enter(Sides.Buy);
else if (candle.HighPrice < _opens.Average())
Enter(Sides.Sell);
}
private void Enter(Sides side)
{
var volume = OrderVolume;
if (Security?.VolumeStep is decimal step && step > 0m)
volume = decimal.Floor(volume / step) * step;
if (Security?.MinVolume is decimal min && min > 0m)
volume = Math.Max(min, volume);
if (Security?.MaxVolume is decimal max && max > 0m)
volume = Math.Min(max, volume);
if (volume > 0m)
Submit(side, volume, "Farhad Crab entry");
}
private void HandleRisk(ICandleMessage candle)
{
if (_entryPrice is not decimal entry)
return;
var positive = Position > 0m;
var pip = PipSize();
var target = (positive ? LongTakeProfitPips : ShortTakeProfitPips) * pip;
if (target > 0m && (positive ? candle.HighPrice >= entry + target :
candle.LowPrice <= entry - target))
{
Exit("Farhad Crab take profit");
return;
}
if (_trailingLevel is decimal level && (positive ? candle.LowPrice <= level :
candle.HighPrice >= level))
{
Exit("Farhad Crab trailing stop");
return;
}
var distance = (positive ? LongTrailingStopPips : ShortTrailingStopPips) * pip;
if (distance <= 0m)
return;
_bestPrice = positive ? Math.Max(_bestPrice, candle.HighPrice) :
Math.Min(_bestPrice, candle.LowPrice);
var move = positive ? _bestPrice - entry : entry - _bestPrice;
if (move < distance)
return;
var next = positive ? _bestPrice - distance : _bestPrice + distance;
if (_trailingLevel is null || (positive ? next > _trailingLevel.Value : next < _trailingLevel.Value))
_trailingLevel = next;
}
private void Exit(string reason)
{
if (!Pending && Position != 0m)
Submit(Position > 0m ? Sides.Sell : Sides.Buy, Math.Abs(Position), reason);
}
private void Submit(Sides side, decimal volume, string comment)
{
_pendingOrder = new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = side,
Volume = volume,
Comment = comment,
};
if (comment == "Farhad Crab entry")
_entryOrder = _pendingOrder;
RegisterOrder(_pendingOrder);
}
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade?.Order == _entryOrder && trade.Trade is not null)
{
_entryPrice = trade.Trade.Price;
_bestPrice = _entryPrice.Value;
_trailingLevel = null;
}
else if (Position == 0m)
{
_entryPrice = null;
_trailingLevel = null;
_bestPrice = 0m;
}
}
}
import clr
import math
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, Sides, OrderStates, OrderTypes
from StockSharp.BusinessEntities import Order
from StockSharp.Algo.Strategies import Strategy
class farhad_crab_strategy(Strategy):
"""M1 EMA-typical and SMA-open scalping with daily SMMA safety exits."""
def __init__(self):
super(farhad_crab_strategy, self).__init__()
self._daily_ma_period = self.Param("DailyMaPeriod", 55).SetGreaterThanZero()
self._order_volume = self.Param("OrderVolume", 0.1).SetGreaterThanZero()
self._long_take_profit_pips = self.Param("LongTakeProfitPips", 10.0).SetNotNegative()
self._short_take_profit_pips = self.Param("ShortTakeProfitPips", 10.0).SetNotNegative()
self._long_trailing_stop_pips = self.Param("LongTrailingStopPips", 8.0).SetNotNegative()
self._short_trailing_stop_pips = self.Param("ShortTrailingStopPips", 8.0).SetNotNegative()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1)))
self._clear_state()
@property
def candle_type(self):
return self._candle_type.Value
def _clear_state(self):
self._typical_seed = []
self._opens = []
self._daily_seed = []
self._ema_typical = None
self._daily_smma = None
self._prior_daily_close = None
self._entry_price = None
self._trailing_level = None
self._best_price = 0.0
self._pending_order = None
self._entry_order = None
def OnReseted(self):
super(farhad_crab_strategy, self).OnReseted()
self._clear_state()
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type),
(self.Security, DataType.TimeFrame(TimeSpan.FromDays(1)))]
def OnStarted2(self, time):
super(farhad_crab_strategy, self).OnStarted2(time)
self._clear_state()
if float(self.Security.PriceStep or 0) <= 0:
raise ValueError("Farhad Crab requires a positive instrument PriceStep.")
self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromDays(1))).Bind(self._process_daily).Start()
self.SubscribeCandles(self.candle_type).Bind(self._process_candle).Start()
def _pip(self):
step = float(self.Security.PriceStep)
return step * (10 if self.Security.Decimals in (3, 5) else 1)
def _pending(self):
return (self._pending_order is not None and
self._pending_order.State not in (OrderStates.Done, OrderStates.Failed))
def _process_daily(self, candle):
if candle.State != CandleStates.Finished:
return
prev_smma = self._daily_smma
prev_close = self._prior_daily_close
close = float(candle.ClosePrice)
if self._daily_smma is None:
self._daily_seed.append(close)
if len(self._daily_seed) == self._daily_ma_period.Value:
self._daily_smma = sum(self._daily_seed) / len(self._daily_seed)
else:
n = self._daily_ma_period.Value
self._daily_smma = (self._daily_smma * (n - 1) + close) / n
self._prior_daily_close = close
if self._pending() or self.Position == 0 or prev_smma is None or prev_close is None:
return
if (self.Position > 0 and prev_smma < prev_close and self._daily_smma > close or
self.Position < 0 and prev_smma > prev_close and self._daily_smma < close):
self._exit("Farhad Crab daily safety exit")
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
typical = (float(candle.HighPrice) + float(candle.LowPrice) +
float(candle.ClosePrice)) / 3.0
if self._ema_typical is None:
self._typical_seed.append(typical)
if len(self._typical_seed) == 9:
self._ema_typical = sum(self._typical_seed) / 9
else:
self._ema_typical = (self._ema_typical * 8 + typical * 2) / 10.0
self._opens.append(float(candle.OpenPrice))
if len(self._opens) > 9:
self._opens.pop(0)
if self._pending():
return
if self.Position != 0:
self._handle_risk(candle)
return
if self._ema_typical is None or len(self._opens) < 9:
return
if float(candle.LowPrice) > self._ema_typical:
self._enter(Sides.Buy)
elif float(candle.HighPrice) < sum(self._opens) / 9.0:
self._enter(Sides.Sell)
def _enter(self, side):
volume = float(self._order_volume.Value)
step = float(self.Security.VolumeStep or 0)
if step > 0:
volume = math.floor(volume / step) * step
minimum = float(self.Security.MinVolume or 0)
maximum = float(self.Security.MaxVolume or 0)
if minimum > 0:
volume = max(volume, minimum)
if maximum > 0:
volume = min(volume, maximum)
if volume > 0:
self._submit(side, volume, "Farhad Crab entry")
def _handle_risk(self, candle):
if self._entry_price is None:
return
entry = self._entry_price
long_position = self.Position > 0
pip = self._pip()
target = float(self._long_take_profit_pips.Value if long_position
else self._short_take_profit_pips.Value) * pip
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if target > 0 and (high >= entry + target if long_position
else low <= entry - target):
self._exit("Farhad Crab take profit")
return
level = self._trailing_level
if level is not None and (low <= level if long_position else high >= level):
self._exit("Farhad Crab trailing stop")
return
distance = float(self._long_trailing_stop_pips.Value if long_position
else self._short_trailing_stop_pips.Value) * pip
if distance <= 0:
return
self._best_price = (max(self._best_price, high) if long_position
else min(self._best_price, low))
move = self._best_price - entry if long_position else entry - self._best_price
if move < distance:
return
next_stop = self._best_price - distance if long_position else self._best_price + distance
if level is None or (next_stop > level if long_position else next_stop < level):
self._trailing_level = next_stop
def _exit(self, reason):
if not self._pending() and self.Position != 0:
side = Sides.Sell if self.Position > 0 else Sides.Buy
self._submit(side, abs(float(self.Position)), reason)
def _submit(self, side, volume, comment):
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = Decimal(volume)
order.Comment = comment
self._pending_order = order
if comment == "Farhad Crab entry":
self._entry_order = order
self.RegisterOrder(order)
def OnOwnTradeReceived(self, trade):
super(farhad_crab_strategy, self).OnOwnTradeReceived(trade)
if trade is not None and trade.Order == self._entry_order and trade.Trade is not None:
self._entry_price = float(trade.Trade.Price)
self._best_price = self._entry_price
self._trailing_level = None
elif self.Position == 0:
self._entry_price = None
self._best_price = 0.0
self._trailing_level = None
def CreateClone(self):
return farhad_crab_strategy()