Стратегия «Open Two Pending Orders»
Общее описание
Стратегия повторяет логику одноимённого советника для MetaTrader: одновременно выставляет buy stop и sell stop ордера на одинаковом расстоянии от текущего спреда. Решение построено на высокоуровневых API StockSharp, подписывается на стакан, управляет заявками и контролирует риск. Как только один из ордеров исполняется, противоположная заявка отменяется, а открытая позиция сопровождается фиксированным стоп-лоссом, тейк-профитом и при необходимости трейлинг-стопом.
Логика торговли
- Подписаться на поток стакана и отслеживать лучшие цены Bid и Ask.
- При отсутствии позиции или активных входящих ордеров вычислить объём сделки и выставить две стоп-заявки:
- Buy stop по формуле
Ask + EntryOffsetPoints × PriceStep.
- Sell stop по формуле
Bid − EntryOffsetPoints × PriceStep.
- После исполнения стоп-ордера:
- Отменить противоположную заявку;
- Зафиксировать цену входа;
- Рассчитать уровни стоп-лосса и тейк-профита в шагах цены относительно входа.
- Пока позиция открыта, анализировать стакан:
- Для лонгов закрывать позицию по рынку при достижении стоп-лосса или тейк-профита по Bid;
- Для шортов закрывать по рынку при достижении уровней по Ask;
- После движения цены в прибыльную сторону на величину трейлинг-стопа подтягивать стоп-уровень.
- После закрытия позиции обнулить внутренние состояния и выставить новую пару встречных заявок.
Выходы выполняются рыночными приказами при касании защитных уровней, что позволяет сохранить поведение оригинального эксперта без прямой модификации заявок на сервере.
Управление капиталом
Стратегия поддерживает две схемы расчёта объёма:
- FixedVolume — фиксированное количество контрактов, задаваемое параметром.
- UseMoneyManagement — расчёт объёма по текущей стоимости портфеля, проценту риска и расстоянию до стоп-лосса. Полученное значение округляется к шагу объёма инструмента и ограничивается допустимыми минимумом и максимумом.
Параметры
| Параметр |
Описание |
UseMoneyManagement |
Включает процентное управление риском. По умолчанию true. |
RiskPercent |
Доля портфеля, которую допускается потерять в одной сделке при включённом риск-менеджменте. По умолчанию 2. |
FixedVolume |
Объём заявки при выключенном риск-менеджменте. По умолчанию 1. |
StopLossPoints |
Расстояние до стоп-лосса в шагах цены. По умолчанию 100. |
TakeProfitPoints |
Расстояние до тейк-профита в шагах цены. По умолчанию 300. |
TrailingStopPoints |
Дистанция трейлинг-стопа в шагах цены. 0 отключает трейлинг. По умолчанию 50. |
EntryOffsetPoints |
Смещение заявок от лучшей цены в шагах цены. По умолчанию 50. |
SlippagePoints |
Резерв на проскальзывание в шагах цены (информационный параметр). По умолчанию 5. |
Дополнительные замечания
- Для корректной работы необходим поток стакана выбранного инструмента.
- Стоп-лосс и тейк-профит исполняются по рынку сразу после касания соответствующего уровня Bid/Ask.
- Трейлинг-стоп активируется только после движения цены в прибыльную сторону на заданную величину.
- Код оформлен в соответствии с требованиями проекта: табуляция, комментарии на английском языке и использование высокоуровневых методов 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>
/// Strategy that simulates placing both buy stop and sell stop orders around the current price.
/// It uses candle-based breakout detection and manages the resulting position
/// with fixed stop loss, take profit and optional trailing stop levels.
/// </summary>
public class OpenTwoPendingOrdersStrategy : Strategy
{
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<decimal> _entryOffsetPoints;
private readonly StrategyParam<DataType> _candleType;
private decimal? _pendingBuyPrice;
private decimal? _pendingSellPrice;
private decimal? _entryPrice;
private decimal? _stopLevel;
private decimal? _takeLevel;
private decimal _highestSinceEntry;
private decimal _lowestSinceEntry;
private int _cooldown;
/// <summary>
/// Stop loss distance expressed in price steps.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take profit distance expressed in price steps.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Trailing stop distance expressed in price steps.
/// </summary>
public decimal TrailingStopPoints
{
get => _trailingStopPoints.Value;
set => _trailingStopPoints.Value = value;
}
/// <summary>
/// Distance in price steps used to place the pending entries away from the current price.
/// </summary>
public decimal EntryOffsetPoints
{
get => _entryOffsetPoints.Value;
set => _entryOffsetPoints.Value = value;
}
/// <summary>
/// Candle type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="OpenTwoPendingOrdersStrategy"/>.
/// </summary>
public OpenTwoPendingOrdersStrategy()
{
_stopLossPoints = Param(nameof(StopLossPoints), 5000m)
.SetDisplay("Stop Loss (steps)", "Stop loss distance in price steps", "Risk")
.SetOptimize(20m, 300m, 20m);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 8000m)
.SetDisplay("Take Profit (steps)", "Take profit distance in price steps", "Risk")
.SetOptimize(50m, 600m, 50m);
_trailingStopPoints = Param(nameof(TrailingStopPoints), 3000m)
.SetDisplay("Trailing Stop (steps)", "Trailing stop distance in price steps", "Risk")
.SetOptimize(10m, 200m, 10m);
_entryOffsetPoints = Param(nameof(EntryOffsetPoints), 1000m)
.SetDisplay("Entry Offset (steps)", "Offset from close for pending entries", "Execution")
.SetOptimize(10m, 150m, 10m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
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;
if (_cooldown > 0)
{
_cooldown--;
return;
}
var step = GetStep();
// Manage existing position
if (Position != 0 && _entryPrice.HasValue)
{
ManagePosition(candle, step);
// If position was closed, reset and set up new pending entries
if (Position == 0)
{
ResetState();
_cooldown = 20;
}
return;
}
// Check pending entries
if (_pendingBuyPrice.HasValue && _pendingSellPrice.HasValue)
{
var buyLevel = _pendingBuyPrice.Value;
var sellLevel = _pendingSellPrice.Value;
// Buy stop triggered: price went up to pending buy level
if (candle.HighPrice >= buyLevel)
{
_pendingBuyPrice = null;
_pendingSellPrice = null;
BuyMarket();
InitializePositionLevels(true, buyLevel, step);
return;
}
// Sell stop triggered: price went down to pending sell level
if (candle.LowPrice <= sellLevel)
{
_pendingBuyPrice = null;
_pendingSellPrice = null;
SellMarket();
InitializePositionLevels(false, sellLevel, step);
return;
}
}
else
{
// No pending entries, set up new ones
SetupPendingEntries(candle.ClosePrice, step);
}
}
private void SetupPendingEntries(decimal currentPrice, decimal step)
{
var offset = EntryOffsetPoints * step;
_pendingBuyPrice = currentPrice + offset;
_pendingSellPrice = currentPrice - offset;
}
private void InitializePositionLevels(bool isLong, decimal entryPrice, decimal step)
{
_entryPrice = entryPrice;
_highestSinceEntry = entryPrice;
_lowestSinceEntry = entryPrice;
_stopLevel = StopLossPoints > 0m
? entryPrice + (isLong ? -StopLossPoints : StopLossPoints) * step
: null;
_takeLevel = TakeProfitPoints > 0m
? entryPrice + (isLong ? TakeProfitPoints : -TakeProfitPoints) * step
: null;
}
private void ManagePosition(ICandleMessage candle, decimal step)
{
if (Position > 0)
{
_highestSinceEntry = Math.Max(_highestSinceEntry, candle.HighPrice);
if (_stopLevel.HasValue && candle.LowPrice <= _stopLevel.Value)
{
SellMarket();
return;
}
if (_takeLevel.HasValue && candle.HighPrice >= _takeLevel.Value)
{
SellMarket();
return;
}
UpdateTrailingStop(true, step);
}
else if (Position < 0)
{
_lowestSinceEntry = Math.Min(_lowestSinceEntry, candle.LowPrice);
if (_stopLevel.HasValue && candle.HighPrice >= _stopLevel.Value)
{
BuyMarket();
return;
}
if (_takeLevel.HasValue && candle.LowPrice <= _takeLevel.Value)
{
BuyMarket();
return;
}
UpdateTrailingStop(false, step);
}
}
private void UpdateTrailingStop(bool isLong, decimal step)
{
if (TrailingStopPoints <= 0m || _entryPrice == null)
return;
var trailingDistance = TrailingStopPoints * step;
if (trailingDistance <= 0m)
return;
if (isLong)
{
if (_highestSinceEntry - _entryPrice.Value >= trailingDistance)
{
var desiredStop = _highestSinceEntry - trailingDistance;
if (_stopLevel == null || desiredStop > _stopLevel.Value)
_stopLevel = desiredStop;
}
}
else
{
if (_entryPrice.Value - _lowestSinceEntry >= trailingDistance)
{
var desiredStop = _lowestSinceEntry + trailingDistance;
if (_stopLevel == null || desiredStop < _stopLevel.Value)
_stopLevel = desiredStop;
}
}
}
private void ResetState()
{
_pendingBuyPrice = null;
_pendingSellPrice = null;
_entryPrice = null;
_stopLevel = null;
_takeLevel = null;
_highestSinceEntry = 0m;
_lowestSinceEntry = 0m;
_cooldown = 0;
}
private decimal GetStep()
{
var step = Security?.PriceStep ?? 0m;
return step > 0m ? step : 0.01m;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
class open_two_pending_orders_strategy(Strategy):
def __init__(self):
super(open_two_pending_orders_strategy, self).__init__()
self._sl_points = self.Param("StopLossPoints", 5000.0).SetDisplay("Stop Loss (steps)", "Stop loss distance in price steps", "Risk")
self._tp_points = self.Param("TakeProfitPoints", 8000.0).SetDisplay("Take Profit (steps)", "Take profit distance in price steps", "Risk")
self._trail_points = self.Param("TrailingStopPoints", 3000.0).SetDisplay("Trailing Stop (steps)", "Trailing stop distance in price steps", "Risk")
self._entry_offset = self.Param("EntryOffsetPoints", 1000.0).SetDisplay("Entry Offset (steps)", "Offset from close for pending entries", "Execution")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))).SetDisplay("Candle Type", "Type of candles", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(open_two_pending_orders_strategy, self).OnReseted()
self._reset_state()
def _reset_state(self):
self._pending_buy = None
self._pending_sell = None
self._entry_price = None
self._stop_level = None
self._take_level = None
self._highest = 0.0
self._lowest = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(open_two_pending_orders_strategy, self).OnStarted2(time)
self._reset_state()
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def _get_step(self):
if self.Security is not None and self.Security.PriceStep is not None and self.Security.PriceStep > 0:
return float(self.Security.PriceStep)
return 0.01
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
if self._cooldown > 0:
self._cooldown -= 1
return
step = self._get_step()
# Manage existing position
if self.Position != 0 and self._entry_price is not None:
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
if self.Position > 0:
self._highest = max(self._highest, h)
if self._stop_level is not None and lo <= self._stop_level:
self.SellMarket()
elif self._take_level is not None and h >= self._take_level:
self.SellMarket()
else:
# trailing
if float(self._trail_points.Value) > 0:
trail_dist = float(self._trail_points.Value) * step
if self._highest - self._entry_price >= trail_dist:
desired = self._highest - trail_dist
if self._stop_level is None or desired > self._stop_level:
self._stop_level = desired
elif self.Position < 0:
self._lowest = min(self._lowest, lo)
if self._stop_level is not None and h >= self._stop_level:
self.BuyMarket()
elif self._take_level is not None and lo <= self._take_level:
self.BuyMarket()
else:
if float(self._trail_points.Value) > 0:
trail_dist = float(self._trail_points.Value) * step
if self._entry_price - self._lowest >= trail_dist:
desired = self._lowest + trail_dist
if self._stop_level is None or desired < self._stop_level:
self._stop_level = desired
if self.Position == 0:
self._reset_state()
self._cooldown = 20
return
# Check pending entries
if self._pending_buy is not None and self._pending_sell is not None:
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
if h >= self._pending_buy:
entry = self._pending_buy
self._pending_buy = None
self._pending_sell = None
self.BuyMarket()
self._entry_price = entry
self._highest = entry
self._lowest = entry
sl = float(self._sl_points.Value)
tp = float(self._tp_points.Value)
self._stop_level = entry - sl * step if sl > 0 else None
self._take_level = entry + tp * step if tp > 0 else None
return
if lo <= self._pending_sell:
entry = self._pending_sell
self._pending_buy = None
self._pending_sell = None
self.SellMarket()
self._entry_price = entry
self._highest = entry
self._lowest = entry
sl = float(self._sl_points.Value)
tp = float(self._tp_points.Value)
self._stop_level = entry + sl * step if sl > 0 else None
self._take_level = entry - tp * step if tp > 0 else None
return
else:
offset = float(self._entry_offset.Value) * step
self._pending_buy = float(candle.ClosePrice) + offset
self._pending_sell = float(candle.ClosePrice) - offset
def CreateClone(self):
return open_two_pending_orders_strategy()