Send Close Order — порт эксперта MetaTrader 4 «SendCloseOrder», выпущенного Владимиром Хлыстовым в 2009 году. Исходный скрипт наносит на график четыре трендовые линии, построенные по фракталам Билла Вильямса, и открывает либо закрывает рыночные ордера при касании цены этих проекций. Версия под StockSharp полностью повторяет торговую логику, автоматически пересчитывает линии и работает на любой свечной серии.
Логика торговли
Определение фракталов — каждая завершённая свеча сдвигает пятибарное окно. Когда окно заполнено, средняя свеча проверяется на соответствие условиям фрактала Билла Вильямса. Подтверждённые максимумы и минимумы сохраняются по времени.
Перестроение линий
Sell line соединяет два последних восходящих фрактала, между которыми есть нисходящий фрактал, образуя сопротивление.
Close #1 — это sell line, сдвинутая вверх на 15 пунктов (15 × Security.PriceStep), выполняет роль выхода из длинных позиций.
Buy line соединяет два последних нисходящих фрактала, между которыми есть восходящий фрактал, формируя поддержку.
Close #2 — buy line, сдвинутая вниз на 15 пунктов, является линией закрытия шортов.
Фильтрация сигналов — четыре линии экстраполируются к времени закрытия свечи. Если рассчитанная цена попадает в диапазон High-Low свечи (с допуском в две минимальные цены шага), срабатывает соответствующее действие.
Управление ордерами
Касание Close #1 или Close #2 приводит к немедленному закрытию всей позиции через ClosePosition().
Касание Sell или Buy линии открывает рыночный ордер объёмом TradeVolume, если итоговая позиция не превысит MaxOrders × TradeVolume. При наличии противоположной позиции ордер сначала её компенсирует и только затем добавляет новый вход, имитируя хеджинговый режим MetaTrader.
Параметры
Имя
Значение по умолчанию
Описание
EnableSellLine
true
Разрешить сделки при касании линии сопротивления.
EnableBuyLine
true
Разрешить сделки при касании линии поддержки.
EnableCloseLongLine
true
Разрешить закрытие длинных позиций по верхней линии (Close #1).
EnableCloseShortLine
true
Разрешить закрытие коротких позиций по нижней линии (Close #2).
MaxOrders
1
Максимальное число наращиваемых входов в одном направлении.
TradeVolume
0.1
Объём каждого рыночного ордера.
CandleType
таймфрейм 1 час
Свечная серия для расчёта фракталов.
Отличия от версии MetaTrader
В StockSharp линии пересчитываются автоматически при появлении нового фрактала. В MetaTrader их приходилось перерисовывать вручную.
Используется агрегированная нетто-позиция, поэтому одновременные лонги и шорты не поддерживаются.
Касание определяется по диапазону завершённой свечи с допуском по шагу цены, а не по мгновенным котировкам Bid/Ask.
Графические объекты (линии и подписи) не создаются, реализована только торговая логика.
Рекомендации по использованию
Стратегия работает на любом инструменте с доступными свечами и корректным PriceStep. Если шаг цены не задан, используется значение 0.0001.
Увеличивайте MaxOrders, чтобы повторить наращивание позиции из оригинального советника, и подбирайте TradeVolume с учётом минимального лота.
Сдвиг линий фиксирован на уровне 15 пунктов — при изменении параметров в MetaTrader требуется корректировка кода.
Предоставлена только C#-версия. Перевод на Python можно добавить позднее при необходимости.
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// SendCloseOrder: Fractal high/low breakout with EMA filter and ATR stops.
/// </summary>
public class SendCloseOrderStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _emaLength;
private readonly StrategyParam<int> _atrLength;
private decimal _entryPrice;
private decimal _fractalHigh;
private decimal _fractalLow;
private decimal _prev2High;
private decimal _prev1High;
private decimal _prev2Low;
private decimal _prev1Low;
private int _barCount;
public SendCloseOrderStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe.", "General");
_emaLength = Param(nameof(EmaLength), 50)
.SetDisplay("EMA Length", "Trend filter.", "Indicators");
_atrLength = Param(nameof(AtrLength), 14)
.SetDisplay("ATR Length", "ATR period.", "Indicators");
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int EmaLength
{
get => _emaLength.Value;
set => _emaLength.Value = value;
}
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = 0;
_fractalHigh = 0;
_fractalLow = 0;
_prev2High = 0;
_prev1High = 0;
_prev2Low = 0;
_prev1Low = 0;
_barCount = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_entryPrice = 0;
_fractalHigh = 0;
_fractalLow = 0;
_prev2High = 0;
_prev1High = 0;
_prev2Low = 0;
_prev1Low = 0;
_barCount = 0;
var ema = new ExponentialMovingAverage { Length = EmaLength };
var atr = new AverageTrueRange { Length = AtrLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ema, atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal emaVal, decimal atrVal)
{
if (candle.State != CandleStates.Finished)
return;
_barCount++;
var high = candle.HighPrice;
var low = candle.LowPrice;
var close = candle.ClosePrice;
// Detect fractal high: prev1High > prev2High and prev1High > current high
if (_barCount > 3 && _prev1High > _prev2High && _prev1High > high)
_fractalHigh = _prev1High;
// Detect fractal low: prev1Low < prev2Low and prev1Low < current low
if (_barCount > 3 && _prev1Low < _prev2Low && _prev1Low < low)
_fractalLow = _prev1Low;
_prev2High = _prev1High;
_prev1High = high;
_prev2Low = _prev1Low;
_prev1Low = low;
if (_fractalHigh == 0 || _fractalLow == 0 || atrVal <= 0)
return;
if (Position > 0)
{
if (close >= _entryPrice + atrVal * 3m || close <= _entryPrice - atrVal * 1.5m)
{
SellMarket();
_entryPrice = 0;
}
}
else if (Position < 0)
{
if (close <= _entryPrice - atrVal * 3m || close >= _entryPrice + atrVal * 1.5m)
{
BuyMarket();
_entryPrice = 0;
}
}
if (Position == 0)
{
if (close > _fractalHigh && close > emaVal)
{
_entryPrice = close;
BuyMarket();
}
else if (close < _fractalLow && close < emaVal)
{
_entryPrice = close;
SellMarket();
}
}
}
}
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
from StockSharp.Algo.Indicators import ExponentialMovingAverage, AverageTrueRange
class send_close_order_strategy(Strategy):
def __init__(self):
super(send_close_order_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe.", "General")
self._ema_length = self.Param("EmaLength", 50) \
.SetDisplay("EMA Length", "Trend filter.", "Indicators")
self._atr_length = self.Param("AtrLength", 14) \
.SetDisplay("ATR Length", "ATR period.", "Indicators")
self._entry_price = 0.0
self._fractal_high = 0.0
self._fractal_low = 0.0
self._prev2_high = 0.0
self._prev1_high = 0.0
self._prev2_low = 0.0
self._prev1_low = 0.0
self._bar_count = 0
@property
def CandleType(self):
return self._candle_type.Value
@property
def EmaLength(self):
return self._ema_length.Value
@property
def AtrLength(self):
return self._atr_length.Value
def OnStarted2(self, time):
super(send_close_order_strategy, self).OnStarted2(time)
self._entry_price = 0.0
self._fractal_high = 0.0
self._fractal_low = 0.0
self._prev2_high = 0.0
self._prev1_high = 0.0
self._prev2_low = 0.0
self._prev1_low = 0.0
self._bar_count = 0
self._ema = ExponentialMovingAverage()
self._ema.Length = self.EmaLength
self._atr = AverageTrueRange()
self._atr.Length = self.AtrLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._ema, self._atr, self.ProcessCandle).Start()
def ProcessCandle(self, candle, ema_val, atr_val):
if candle.State != CandleStates.Finished:
return
ev = float(ema_val)
av = float(atr_val)
self._bar_count += 1
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
# Detect fractal high: prev1High > prev2High and prev1High > current high
if self._bar_count > 3 and self._prev1_high > self._prev2_high and self._prev1_high > high:
self._fractal_high = self._prev1_high
# Detect fractal low: prev1Low < prev2Low and prev1Low < current low
if self._bar_count > 3 and self._prev1_low < self._prev2_low and self._prev1_low < low:
self._fractal_low = self._prev1_low
self._prev2_high = self._prev1_high
self._prev1_high = high
self._prev2_low = self._prev1_low
self._prev1_low = low
if self._fractal_high == 0 or self._fractal_low == 0 or av <= 0:
return
if self.Position > 0:
if close >= self._entry_price + av * 3.0 or close <= self._entry_price - av * 1.5:
self.SellMarket()
self._entry_price = 0.0
elif self.Position < 0:
if close <= self._entry_price - av * 3.0 or close >= self._entry_price + av * 1.5:
self.BuyMarket()
self._entry_price = 0.0
if self.Position == 0:
if close > self._fractal_high and close > ev:
self._entry_price = close
self.BuyMarket()
elif close < self._fractal_low and close < ev:
self._entry_price = close
self.SellMarket()
def OnReseted(self):
super(send_close_order_strategy, self).OnReseted()
self._entry_price = 0.0
self._fractal_high = 0.0
self._fractal_low = 0.0
self._prev2_high = 0.0
self._prev1_high = 0.0
self._prev2_low = 0.0
self._prev1_low = 0.0
self._bar_count = 0
def CreateClone(self):
return send_close_order_strategy()