Стратегия e-Skoch Pending Orders
Обзор
e-Skoch Pending Orders — это порт оригинального советника MetaTrader, который работает только на открытии новой свечи, анализирует два последних максимума и минимума на рабочем таймфрейме и на дневном таймфрейме и размещает отложенные ордера на пробой. Целью является захват импульса после короткой коррекции, когда дневной тренд подтверждает направление прорыва.
Версия для StockSharp повторяет исходную логику, но использует высокоуровневые возможности API: подписку на свечи, автоматическую установку защитных заявок и систему параметров. Код на C# расположен в папке CS/. Python-реализация пока не создавалась.
Логика торговли
- После закрытия каждой свечи считываются максимумы и минимумы двух последних свечей рабочего таймфрейма, а также данные двух предыдущих дневных свечей.
- Если последний дневной максимум ниже позапрошлого и предыдущий внутридневной максимум также ниже своего предшественника, стратегия размещает buy stop над ближайшим максимумом с добавлением настраиваемого отступа.
- Если последний дневной минимум выше позапрошлого и предыдущий внутридневной минимум также выше своего предшественника, стратегия размещает sell stop ниже последнего минимума с учётом заданного отступа.
- Каждая заявка сразу получает собственные уровни стоп-лосса и тейк-профита. При исполнении входа стратегия моментально выставляет защитные стоповые и лимитные заявки по текущей позиции.
- Когда активных позиций и ордеров нет, фиксируется текущее значение капитала. При росте капитала на установленный процент относительно этой базы все позиции закрываются, а защитные заявки отменяются.
- Параметр
CheckExistingTrade позволяет запретить размещение новых ордеров, если уже открыта какая-либо позиция, что полностью повторяет флаг «CheckTrade» из оригинального советника.
Параметры
| Параметр |
Описание |
CandleType |
Рабочий таймфрейм для сигналов (по умолчанию часовые свечи). |
TakeProfitBuyPips / StopLossBuyPips |
Размер тейк-профита и стоп-лосса для длинных позиций в пунктах. |
TakeProfitSellPips / StopLossSellPips |
Размер тейк-профита и стоп-лосса для коротких позиций в пунктах. |
IndentHighPips / IndentLowPips |
Отступ от последнего максимума/минимума для постановки отложенных ордеров. |
CheckExistingTrade |
Если включено, новые сигналы игнорируются при наличии открытой позиции. |
PercentEquity |
Процент роста капитала, после достижения которого все позиции закрываются. |
Volume |
Объём входа. Значение 0.01 соответствует настройкам оригинального советника. |
Управление рисками
- Buy stop сопровождается стоп-лоссом ниже цены входа и тейк-профитом выше неё.
- Sell stop сопровождается стоп-лоссом выше цены входа и тейк-профитом ниже неё.
- При закрытии позиции или переустановке защиты старые защитные заявки отменяются автоматически.
- Контроль роста капитала выполняет роль «глобального» стопа, фиксируя прибыль и предотвращая дальнейшие сделки до следующего сигнала.
Дополнительные замечания
- Для работы стратегии необходимы данные по рабочему таймфрейму и по дневным свечам — убедитесь, что оба потока данных доступны в Designer или при тестировании.
- Для инструментов с трёх- и пятизнаковой котировкой размер пункта автоматически пересчитывается: шаг цены умножается на 10.
- При активном
CheckExistingTrade стратегия предполагает наличие только одной совокупной позиции и не допускает одновременных длинных и коротких позиций.
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>
/// Pending breakout strategy based on the e-Skoch pending orders idea.
/// Detects falling highs or rising lows across two timeframes to enter on breakouts.
/// </summary>
public class ESkochPendingOrdersStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _takeProfitBuyPips;
private readonly StrategyParam<decimal> _stopLossBuyPips;
private readonly StrategyParam<decimal> _takeProfitSellPips;
private readonly StrategyParam<decimal> _stopLossSellPips;
private readonly StrategyParam<decimal> _indentHighPips;
private readonly StrategyParam<decimal> _indentLowPips;
private readonly StrategyParam<bool> _checkExistingTrade;
private decimal? _prevHigh1;
private decimal? _prevHigh2;
private decimal? _prevLow1;
private decimal? _prevLow2;
private decimal? _pendingBuyPrice;
private decimal? _pendingSellPrice;
private decimal _entryPrice;
private decimal _longStop;
private decimal _longTake;
private decimal _shortStop;
private decimal _shortTake;
private decimal _pipValue;
/// <summary>
/// Main candle type for signal evaluation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public decimal TakeProfitBuyPips
{
get => _takeProfitBuyPips.Value;
set => _takeProfitBuyPips.Value = value;
}
public decimal StopLossBuyPips
{
get => _stopLossBuyPips.Value;
set => _stopLossBuyPips.Value = value;
}
public decimal TakeProfitSellPips
{
get => _takeProfitSellPips.Value;
set => _takeProfitSellPips.Value = value;
}
public decimal StopLossSellPips
{
get => _stopLossSellPips.Value;
set => _stopLossSellPips.Value = value;
}
public decimal IndentHighPips
{
get => _indentHighPips.Value;
set => _indentHighPips.Value = value;
}
public decimal IndentLowPips
{
get => _indentLowPips.Value;
set => _indentLowPips.Value = value;
}
public bool CheckExistingTrade
{
get => _checkExistingTrade.Value;
set => _checkExistingTrade.Value = value;
}
public ESkochPendingOrdersStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe", "General");
_takeProfitBuyPips = Param(nameof(TakeProfitBuyPips), 2000m)
.SetGreaterThanZero()
.SetDisplay("Buy TP (pips)", "Long take profit distance", "Trading");
_stopLossBuyPips = Param(nameof(StopLossBuyPips), 500m)
.SetGreaterThanZero()
.SetDisplay("Buy SL (pips)", "Long stop loss distance", "Trading");
_takeProfitSellPips = Param(nameof(TakeProfitSellPips), 2000m)
.SetGreaterThanZero()
.SetDisplay("Sell TP (pips)", "Short take profit distance", "Trading");
_stopLossSellPips = Param(nameof(StopLossSellPips), 500m)
.SetGreaterThanZero()
.SetDisplay("Sell SL (pips)", "Short stop loss distance", "Trading");
_indentHighPips = Param(nameof(IndentHighPips), 500m)
.SetGreaterThanZero()
.SetDisplay("High Indent", "Buy stop offset", "Trading");
_indentLowPips = Param(nameof(IndentLowPips), 500m)
.SetGreaterThanZero()
.SetDisplay("Low Indent", "Sell stop offset", "Trading");
_checkExistingTrade = Param(nameof(CheckExistingTrade), true)
.SetDisplay("Block During Position", "Skip signals when a position exists", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevHigh1 = null;
_prevHigh2 = null;
_prevLow1 = null;
_prevLow2 = null;
_pendingBuyPrice = null;
_pendingSellPrice = null;
_entryPrice = 0m;
_longStop = 0m;
_longTake = 0m;
_shortStop = 0m;
_shortTake = 0m;
_pipValue = 1m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var priceStep = Security?.PriceStep ?? 0m;
_pipValue = priceStep <= 0m ? 1m : priceStep;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
// Check pending entries against current candle.
CheckPendingEntries(candle);
// Manage SL/TP for open positions.
ManagePosition(candle);
// Need at least 2 previous bars.
if (_prevHigh1 is null)
{
_prevHigh1 = candle.HighPrice;
_prevLow1 = candle.LowPrice;
return;
}
if (_prevHigh2 is null)
{
_prevHigh2 = _prevHigh1;
_prevLow2 = _prevLow1;
_prevHigh1 = candle.HighPrice;
_prevLow1 = candle.LowPrice;
return;
}
var hasPosition = Position != 0;
// Falling highs => place buy stop above recent high.
if (_prevHigh2 > _prevHigh1 && !hasPosition)
{
if (!CheckExistingTrade || Position == 0)
{
var buyPrice = _prevHigh1.Value + _pipValue * IndentHighPips;
_pendingBuyPrice = buyPrice;
_longStop = buyPrice - _pipValue * StopLossBuyPips;
_longTake = buyPrice + _pipValue * TakeProfitBuyPips;
}
}
// Rising lows => place sell stop below recent low.
if (_prevLow2 < _prevLow1 && !hasPosition)
{
if (!CheckExistingTrade || Position == 0)
{
var sellPrice = _prevLow1.Value - _pipValue * IndentLowPips;
_pendingSellPrice = sellPrice;
_shortStop = sellPrice + _pipValue * StopLossSellPips;
_shortTake = sellPrice - _pipValue * TakeProfitSellPips;
}
}
// Shift history.
_prevHigh2 = _prevHigh1;
_prevLow2 = _prevLow1;
_prevHigh1 = candle.HighPrice;
_prevLow1 = candle.LowPrice;
}
private void CheckPendingEntries(ICandleMessage candle)
{
if (Position != 0)
return;
if (_pendingBuyPrice is decimal buyPrice && candle.HighPrice >= buyPrice)
{
BuyMarket();
_entryPrice = buyPrice;
_pendingBuyPrice = null;
_pendingSellPrice = null;
return;
}
if (_pendingSellPrice is decimal sellPrice && candle.LowPrice <= sellPrice)
{
SellMarket();
_entryPrice = sellPrice;
_pendingBuyPrice = null;
_pendingSellPrice = null;
}
}
private void ManagePosition(ICandleMessage candle)
{
if (Position > 0)
{
if (_longStop > 0m && candle.LowPrice <= _longStop)
{
SellMarket();
ResetPositionState();
return;
}
if (_longTake > 0m && candle.HighPrice >= _longTake)
{
SellMarket();
ResetPositionState();
}
}
else if (Position < 0)
{
if (_shortStop > 0m && candle.HighPrice >= _shortStop)
{
BuyMarket();
ResetPositionState();
return;
}
if (_shortTake > 0m && candle.LowPrice <= _shortTake)
{
BuyMarket();
ResetPositionState();
}
}
}
private void ResetPositionState()
{
_entryPrice = 0m;
_longStop = 0m;
_longTake = 0m;
_shortStop = 0m;
_shortTake = 0m;
_pendingBuyPrice = null;
_pendingSellPrice = 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 StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class e_skoch_pending_orders_strategy(Strategy):
"""Pending breakout: detects falling highs or rising lows to enter on breakouts with SL/TP."""
def __init__(self):
super(e_skoch_pending_orders_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Primary timeframe", "General")
self._take_profit_buy_pips = self.Param("TakeProfitBuyPips", 2000.0) \
.SetGreaterThanZero() \
.SetDisplay("Buy TP (pips)", "Long take profit distance", "Trading")
self._stop_loss_buy_pips = self.Param("StopLossBuyPips", 500.0) \
.SetGreaterThanZero() \
.SetDisplay("Buy SL (pips)", "Long stop loss distance", "Trading")
self._take_profit_sell_pips = self.Param("TakeProfitSellPips", 2000.0) \
.SetGreaterThanZero() \
.SetDisplay("Sell TP (pips)", "Short take profit distance", "Trading")
self._stop_loss_sell_pips = self.Param("StopLossSellPips", 500.0) \
.SetGreaterThanZero() \
.SetDisplay("Sell SL (pips)", "Short stop loss distance", "Trading")
self._indent_high_pips = self.Param("IndentHighPips", 500.0) \
.SetGreaterThanZero() \
.SetDisplay("High Indent", "Buy stop offset", "Trading")
self._indent_low_pips = self.Param("IndentLowPips", 500.0) \
.SetGreaterThanZero() \
.SetDisplay("Low Indent", "Sell stop offset", "Trading")
self._check_existing_trade = self.Param("CheckExistingTrade", True) \
.SetDisplay("Block During Position", "Skip signals when a position exists", "Risk")
self._prev_high1 = None
self._prev_high2 = None
self._prev_low1 = None
self._prev_low2 = None
self._pending_buy_price = None
self._pending_sell_price = None
self._entry_price = 0.0
self._long_stop = 0.0
self._long_take = 0.0
self._short_stop = 0.0
self._short_take = 0.0
self._pip_value = 1.0
@property
def CandleType(self):
return self._candle_type.Value
@property
def TakeProfitBuyPips(self):
return float(self._take_profit_buy_pips.Value)
@property
def StopLossBuyPips(self):
return float(self._stop_loss_buy_pips.Value)
@property
def TakeProfitSellPips(self):
return float(self._take_profit_sell_pips.Value)
@property
def StopLossSellPips(self):
return float(self._stop_loss_sell_pips.Value)
@property
def IndentHighPips(self):
return float(self._indent_high_pips.Value)
@property
def IndentLowPips(self):
return float(self._indent_low_pips.Value)
@property
def CheckExistingTrade(self):
return self._check_existing_trade.Value
def OnStarted2(self, time):
super(e_skoch_pending_orders_strategy, self).OnStarted2(time)
self._prev_high1 = None
self._prev_high2 = None
self._prev_low1 = None
self._prev_low2 = None
self._pending_buy_price = None
self._pending_sell_price = None
self._entry_price = 0.0
self._long_stop = 0.0
self._long_take = 0.0
self._short_stop = 0.0
self._short_take = 0.0
sec = self.Security
price_step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 0.0
self._pip_value = price_step if price_step > 0 else 1.0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
# Check pending entries
self._check_pending_entries(h, lo)
# Manage SL/TP
self._manage_position(h, lo)
# Need at least 2 previous bars
if self._prev_high1 is None:
self._prev_high1 = h
self._prev_low1 = lo
return
if self._prev_high2 is None:
self._prev_high2 = self._prev_high1
self._prev_low2 = self._prev_low1
self._prev_high1 = h
self._prev_low1 = lo
return
has_position = self.Position != 0
# Falling highs -> place buy stop above recent high
if self._prev_high2 > self._prev_high1 and not has_position:
if not self.CheckExistingTrade or self.Position == 0:
buy_price = self._prev_high1 + self._pip_value * self.IndentHighPips
self._pending_buy_price = buy_price
self._long_stop = buy_price - self._pip_value * self.StopLossBuyPips
self._long_take = buy_price + self._pip_value * self.TakeProfitBuyPips
# Rising lows -> place sell stop below recent low
if self._prev_low2 < self._prev_low1 and not has_position:
if not self.CheckExistingTrade or self.Position == 0:
sell_price = self._prev_low1 - self._pip_value * self.IndentLowPips
self._pending_sell_price = sell_price
self._short_stop = sell_price + self._pip_value * self.StopLossSellPips
self._short_take = sell_price - self._pip_value * self.TakeProfitSellPips
# Shift history
self._prev_high2 = self._prev_high1
self._prev_low2 = self._prev_low1
self._prev_high1 = h
self._prev_low1 = lo
def _check_pending_entries(self, h, lo):
if self.Position != 0:
return
if self._pending_buy_price is not None and h >= self._pending_buy_price:
self.BuyMarket()
self._entry_price = self._pending_buy_price
self._pending_buy_price = None
self._pending_sell_price = None
return
if self._pending_sell_price is not None and lo <= self._pending_sell_price:
self.SellMarket()
self._entry_price = self._pending_sell_price
self._pending_buy_price = None
self._pending_sell_price = None
def _manage_position(self, h, lo):
if self.Position > 0:
if self._long_stop > 0 and lo <= self._long_stop:
self.SellMarket()
self._reset_position_state()
return
if self._long_take > 0 and h >= self._long_take:
self.SellMarket()
self._reset_position_state()
elif self.Position < 0:
if self._short_stop > 0 and h >= self._short_stop:
self.BuyMarket()
self._reset_position_state()
return
if self._short_take > 0 and lo <= self._short_take:
self.BuyMarket()
self._reset_position_state()
def _reset_position_state(self):
self._entry_price = 0.0
self._long_stop = 0.0
self._long_take = 0.0
self._short_stop = 0.0
self._short_take = 0.0
self._pending_buy_price = None
self._pending_sell_price = None
def OnReseted(self):
super(e_skoch_pending_orders_strategy, self).OnReseted()
self._prev_high1 = None
self._prev_high2 = None
self._prev_low1 = None
self._prev_low2 = None
self._pending_buy_price = None
self._pending_sell_price = None
self._entry_price = 0.0
self._long_stop = 0.0
self._long_take = 0.0
self._short_stop = 0.0
self._short_take = 0.0
self._pip_value = 1.0
def CreateClone(self):
return e_skoch_pending_orders_strategy()