Стратегия Doji Arrows представляет собой адаптацию одноимённого советника MetaTrader под высокоуровневый API StockSharp. Алгоритм ожидает формирования классической свечи «доджи» и затем торгует пробой её диапазона. Доджи свидетельствует о балансе сил; закрытие следующей свечи выше максимума доджи трактуется как захват инициативы покупателями, а закрытие ниже минимума — как сигнал к продажам.
Обрабатываются только завершённые свечи выбранного таймфрейма CandleType.
Предыдущая свеча проверяется на соответствие критериям доджи. Она считается доджи, если модуль разницы между ценой открытия и закрытия не превышает DojiBodyPoints, умноженные на шаг цены инструмента. При значении параметра 0 используется один шаг цены, что воспроизводит строгую проверку на равенство из версии MQL5.
При закрытии следующей свечи выше максимума доджи отправляется рыночная заявка на покупку. При закрытии ниже минимума — рыночная заявка на продажу. Если открыта противоположная позиция, объём рыночного ордера автоматически её закрывает и разворачивает при необходимости.
Такое поведение повторяет логику исходного советника, который принимал решения один раз на старте нового бара.
Управление рисками
Перенос включает защитные механизмы оригинала:
Стоп-лосс. Параметр StopLossPoints задаёт расстояние начального стопа в шагах цены от точки входа. Значение 0 отключает фиксированный стоп.
Тейк-профит. Параметр TakeProfitPoints определяет расстояние до цели по прибыли в шагах цены. Значение 0 убирает тейк-профит.
Трейлинг-стоп. Пара TrailingStopPoints и TrailingStepPoints воспроизводит алгоритм подтягивания стопа. После того как прибыль превысит сумму TrailingStopPoints + TrailingStepPoints, стоп переносится на расстояние TrailingStopPoints от последней цены закрытия (для длинной позиции — от максимума, для короткой — от минимума). Трейлинг активируется только при положительном TrailingStopPoints.
На каждой завершённой свече проверяется, были ли достигнуты стоп или цель. При пробое уровня по максимуму/минимуму свечи позиция закрывается рыночным ордером, после чего защитные параметры сбрасываются.
Параметры
Параметр
Значение по умолчанию
Описание
StopLossPoints
30
Расстояние начального стоп-лосса в шагах цены.
TakeProfitPoints
90
Расстояние тейк-профита в шагах цены.
TrailingStopPoints
15
Шаг трейлинг-стопа в шагах цены.
TrailingStepPoints
5
Дополнительная прибыль, необходимая перед срабатыванием трейлинга, в шагах цены.
DojiBodyPoints
1
Максимальный размер тела предыдущей свечи в шагах цены, допускающий признание её доджи. Значение 0 использует один шаг цены в качестве допуска.
CandleType
1 час
Таймфрейм свечей, применяемый для сигналов.
Особенности реализации
Подписка на свечи осуществляется через SubscribeCandles(CandleType).Bind(ProcessCandle), в памяти хранится только последняя завершённая свеча.
Шаг цены берётся из Security?.PriceStep. Если брокер или источник данных его не предоставляет, используется запасное значение 1, что позволяет стратегии работать и на синтетических инструментах.
Защитные уровни пересчитываются после каждого входа; трейлинг-стоп способен выставить стоп даже при отключённом фиксированном стоп-лоссе, как и в MQL-версии.
Все операции выполняются рыночными ордерами, что соответствует исходному советнику с немедленным исполнением.
Рекомендации по применению
Перед запуском укажите Security, Portfolio и Volume стратегии.
Подберите значения параметров в шагах цены под конкретный инструмент, особенно если он торгуется с дробными пунктами.
При необходимости расширенного мани-менеджмента добавьте модули риск-контроля StockSharp — конвертация сохраняет фиксированный объём сделок из оригинала.
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>
/// Doji breakout strategy with optional fixed and trailing protection.
/// </summary>
public class DojiArrowsStrategy : Strategy
{
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<decimal> _trailingStepPoints;
private readonly StrategyParam<decimal> _dojiBodyPoints;
private readonly StrategyParam<DataType> _candleType;
private bool _hasPreviousCandle;
private decimal _prevOpen;
private decimal _prevClose;
private decimal _prevHigh;
private decimal _prevLow;
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
public decimal TrailingStopPoints
{
get => _trailingStopPoints.Value;
set => _trailingStopPoints.Value = value;
}
public decimal TrailingStepPoints
{
get => _trailingStepPoints.Value;
set => _trailingStepPoints.Value = value;
}
public decimal DojiBodyPoints
{
get => _dojiBodyPoints.Value;
set => _dojiBodyPoints.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public DojiArrowsStrategy()
{
_stopLossPoints = Param(nameof(StopLossPoints), 30m)
.SetNotNegative()
.SetDisplay("Stop Loss Points", "Stop loss distance in price steps.", "Risk")
;
_takeProfitPoints = Param(nameof(TakeProfitPoints), 90m)
.SetNotNegative()
.SetDisplay("Take Profit Points", "Take profit distance in price steps.", "Risk")
;
_trailingStopPoints = Param(nameof(TrailingStopPoints), 15m)
.SetNotNegative()
.SetDisplay("Trailing Stop Points", "Trailing distance in price steps.", "Risk")
;
_trailingStepPoints = Param(nameof(TrailingStepPoints), 5m)
.SetNotNegative()
.SetDisplay("Trailing Step Points", "Minimum profit before the trailing stop moves.", "Risk")
;
_dojiBodyPoints = Param(nameof(DojiBodyPoints), 1m)
.SetNotNegative()
.SetDisplay("Doji Body Points", "Maximum difference between open and close to treat the candle as a doji.", "Pattern")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Time frame used for signal generation.", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_hasPreviousCandle = false;
_prevOpen = 0m;
_prevClose = 0m;
_prevHigh = 0m;
_prevLow = 0m;
ResetProtection();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
SubscribeCandles(CandleType)
.Bind(ProcessCandle)
.Start();
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent)
);
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
ManageActivePosition(candle);
if (!_hasPreviousCandle)
{
CachePreviousCandle(candle);
return;
}
var step = Security?.PriceStep ?? 1m;
var tolerance = DojiBodyPoints <= 0m ? step : DojiBodyPoints * step;
var bodySize = Math.Abs(_prevOpen - _prevClose);
var isDoji = bodySize <= tolerance;
var breakoutUp = isDoji && candle.ClosePrice > _prevHigh;
var breakoutDown = isDoji && candle.ClosePrice < _prevLow;
if (breakoutUp && Position == 0)
{
BuyMarket();
}
else if (breakoutDown && Position == 0)
{
SellMarket();
}
CachePreviousCandle(candle);
}
private void ManageActivePosition(ICandleMessage candle)
{
if (Position == 0)
return;
var step = Security?.PriceStep ?? 1m;
var trailingDistance = TrailingStopPoints > 0m ? TrailingStopPoints * step : 0m;
var trailingStep = TrailingStepPoints > 0m ? TrailingStepPoints * step : 0m;
if (Position > 0)
{
if (trailingDistance > 0m && _entryPrice.HasValue)
{
var gain = candle.ClosePrice - _entryPrice.Value;
if (gain > trailingDistance + trailingStep)
{
var newStop = candle.ClosePrice - trailingDistance;
if (!_stopPrice.HasValue || newStop > _stopPrice.Value)
_stopPrice = newStop;
}
}
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
SellMarket(Math.Abs(Position));
ResetProtection();
return;
}
if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
{
SellMarket(Math.Abs(Position));
ResetProtection();
return;
}
}
else if (Position < 0)
{
if (trailingDistance > 0m && _entryPrice.HasValue)
{
var gain = _entryPrice.Value - candle.ClosePrice;
if (gain > trailingDistance + trailingStep)
{
var newStop = candle.ClosePrice + trailingDistance;
if (!_stopPrice.HasValue || newStop < _stopPrice.Value)
_stopPrice = newStop;
}
}
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetProtection();
return;
}
if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetProtection();
return;
}
}
}
private void InitializeProtection(decimal price, bool isLong, decimal step)
{
_entryPrice = price;
if (StopLossPoints > 0m)
{
var offset = StopLossPoints * step;
_stopPrice = isLong ? price - offset : price + offset;
}
else
{
_stopPrice = null;
}
if (TakeProfitPoints > 0m)
{
var offset = TakeProfitPoints * step;
_takePrice = isLong ? price + offset : price - offset;
}
else
{
_takePrice = null;
}
}
private void ResetProtection()
{
_entryPrice = null;
_stopPrice = null;
_takePrice = null;
}
private void CachePreviousCandle(ICandleMessage candle)
{
_prevOpen = candle.OpenPrice;
_prevClose = candle.ClosePrice;
_prevHigh = candle.HighPrice;
_prevLow = candle.LowPrice;
_hasPreviousCandle = true;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates
from System import TimeSpan, Math
class doji_arrows_strategy(Strategy):
def __init__(self):
super(doji_arrows_strategy, self).__init__()
self._stop_loss_points = self.Param("StopLossPoints", 30.0)
self._take_profit_points = self.Param("TakeProfitPoints", 90.0)
self._trailing_stop_points = self.Param("TrailingStopPoints", 15.0)
self._trailing_step_points = self.Param("TrailingStepPoints", 5.0)
self._doji_body_points = self.Param("DojiBodyPoints", 1.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._has_previous_candle = False
self._prev_open = 0.0
self._prev_close = 0.0
self._prev_high = 0.0
self._prev_low = 0.0
self._entry_price = None
self._stop_price = None
self._take_price = None
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(doji_arrows_strategy, self).OnStarted2(time)
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._manage_active_position(candle)
if not self._has_previous_candle:
self._cache_previous_candle(candle)
return
step = self._get_price_step()
tolerance = step if self._doji_body_points.Value <= 0 else self._doji_body_points.Value * step
body_size = abs(self._prev_open - self._prev_close)
is_doji = body_size <= tolerance
breakout_up = is_doji and float(candle.ClosePrice) > self._prev_high
breakout_down = is_doji and float(candle.ClosePrice) < self._prev_low
if breakout_up and self.Position == 0:
self.BuyMarket()
self._initialize_protection(float(candle.ClosePrice), True, step)
elif breakout_down and self.Position == 0:
self.SellMarket()
self._initialize_protection(float(candle.ClosePrice), False, step)
self._cache_previous_candle(candle)
def _manage_active_position(self, candle):
if self.Position == 0:
return
step = self._get_price_step()
trailing_distance = self._trailing_stop_points.Value * step if self._trailing_stop_points.Value > 0 else 0.0
trailing_step = self._trailing_step_points.Value * step if self._trailing_step_points.Value > 0 else 0.0
if self.Position > 0:
if trailing_distance > 0 and self._entry_price is not None:
gain = float(candle.ClosePrice) - self._entry_price
if gain > trailing_distance + trailing_step:
new_stop = float(candle.ClosePrice) - trailing_distance
if self._stop_price is None or new_stop > self._stop_price:
self._stop_price = new_stop
if self._stop_price is not None and float(candle.LowPrice) <= self._stop_price:
self.SellMarket(abs(self.Position))
self._reset_protection()
return
if self._take_price is not None and float(candle.HighPrice) >= self._take_price:
self.SellMarket(abs(self.Position))
self._reset_protection()
return
elif self.Position < 0:
if trailing_distance > 0 and self._entry_price is not None:
gain = self._entry_price - float(candle.ClosePrice)
if gain > trailing_distance + trailing_step:
new_stop = float(candle.ClosePrice) + trailing_distance
if self._stop_price is None or new_stop < self._stop_price:
self._stop_price = new_stop
if self._stop_price is not None and float(candle.HighPrice) >= self._stop_price:
self.BuyMarket(abs(self.Position))
self._reset_protection()
return
if self._take_price is not None and float(candle.LowPrice) <= self._take_price:
self.BuyMarket(abs(self.Position))
self._reset_protection()
return
def _initialize_protection(self, price, is_long, step):
self._entry_price = price
if self._stop_loss_points.Value > 0:
offset = self._stop_loss_points.Value * step
self._stop_price = price - offset if is_long else price + offset
else:
self._stop_price = None
if self._take_profit_points.Value > 0:
offset = self._take_profit_points.Value * step
self._take_price = price + offset if is_long else price - offset
else:
self._take_price = None
def _reset_protection(self):
self._entry_price = None
self._stop_price = None
self._take_price = None
def _cache_previous_candle(self, candle):
self._prev_open = float(candle.OpenPrice)
self._prev_close = float(candle.ClosePrice)
self._prev_high = float(candle.HighPrice)
self._prev_low = float(candle.LowPrice)
self._has_previous_candle = True
def _get_price_step(self):
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
return step if step > 0 else 1.0
def OnReseted(self):
super(doji_arrows_strategy, self).OnReseted()
self._has_previous_candle = False
self._prev_open = 0.0
self._prev_close = 0.0
self._prev_high = 0.0
self._prev_low = 0.0
self._reset_protection()
def CreateClone(self):
return doji_arrows_strategy()