Стратегия "Печати даркпула"
Торгует крупные внебиржевые сделки в направлении указанной стороны агрессора; SMA20 торгуемого инструмента служит фильтром тренда.
В Security задаётся торгуемый инструмент, в DarkPoolSecurity — отдельный поток отчётов по тому же базовому инструменту. Идентификаторы должны различаться. Поток должен передавать реальные внебиржевые сделки (IsSystem = false), цену, объём, время и OriginSide. Отсутствующий или совпадающий источник вызывает ошибку при запуске. Биржевые сделки, отчёты без стороны, некорректные цены/объёмы, дубликаты и запоздавшие отчёты не создают входы.
Сигнал требует объёма строго больше 1.5 × средний объём предыдущих 20 отчётов; текущий отчёт в среднее не входит. Покупка — по отчёту о покупке выше SMA20; продажа — по отчёту о продаже ниже SMA20. Цвет и объём свечи не подменяют сторону и активность внебиржевых сделок. Противоположный вход разворачивает всю исполненную позицию заявкой Volume + abs(Position).
SMA рассчитывается по завершённым 15-минутным свечам. Лонг закрывается ниже SMA20, шорт — выше SMA20, независимо от объёма отчётов и паузы входов. По умолчанию CooldownBars = 0; положительное значение задаёт паузу между входами в завершённых свечах. Неисполненная заявка блокирует пересекающиеся сигналы.
StopLoss = 2 задаёт штатную локальную защиту на 2% от фактической цены исполнения входа. Защита отслеживает bid/ask торгуемого инструмента и отправляет рыночную заявку на выход; между свечами нужен поток котировок. Тейк-профит не задан.
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>Trades large, direction-classified off-exchange prints from an explicitly selected source.</summary>
public class DarkPoolPrintsStrategy : Strategy
{
private readonly StrategyParam<Security> _darkPoolSecurity;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _volumeLookback;
private readonly StrategyParam<decimal> _volumeMultiplier;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<decimal> _stopLoss;
private readonly Queue<decimal> _reportVolumes = new();
private readonly HashSet<(long? id, string text)> _reportIds = new();
private DateTime _reportTime;
private decimal? _average;
private int _cooldown;
private Order _pendingOrder;
/// <summary>Dedicated off-exchange report instrument; it must differ from the execution instrument.</summary>
public Security DarkPoolSecurity { get => _darkPoolSecurity.Value; set => _darkPoolSecurity.Value = value; }
/// <summary>Trend and exit SMA length.</summary>
public int MaPeriod { get => _maPeriod.Value; set => _maPeriod.Value = value; }
/// <summary>Number of preceding reported trades in the volume baseline.</summary>
public int VolumeLookback { get => _volumeLookback.Value; set => _volumeLookback.Value = value; }
/// <summary>Required multiple of the preceding report volumes.</summary>
public decimal VolumeMultiplier { get => _volumeMultiplier.Value; set => _volumeMultiplier.Value = value; }
/// <summary>Execution instrument's candle interval.</summary>
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
/// <summary>Finished candles between entries; zero allows each qualifying report.</summary>
public int CooldownBars { get => _cooldownBars.Value; set => _cooldownBars.Value = value; }
/// <summary>Native percentage loss limit.</summary>
public decimal StopLoss { get => _stopLoss.Value; set => _stopLoss.Value = value; }
/// <summary>Initializes the dark-pool strategy.</summary>
public DarkPoolPrintsStrategy()
{
_darkPoolSecurity = Param<Security>(nameof(DarkPoolSecurity)).SetRequired()
.SetDisplay("Dark Pool Security", "Dedicated source of off-exchange trade reports", "Data");
_maPeriod = Param(nameof(MaPeriod), 20).SetGreaterThanZero()
.SetDisplay("MA Period", "Execution instrument SMA period", "Indicators");
_volumeLookback = Param(nameof(VolumeLookback), 20).SetGreaterThanZero()
.SetDisplay("Volume Lookback", "Preceding off-exchange reports for the volume baseline", "Volume");
_volumeMultiplier = Param(nameof(VolumeMultiplier), 1.5m).SetGreaterThanZero()
.SetDisplay("Volume Multiplier", "Required report-volume multiple", "Volume");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Execution instrument candle interval", "Data");
_cooldownBars = Param(nameof(CooldownBars), 0).SetNotNegative()
.SetDisplay("Cooldown Bars", "Finished candles between qualifying reports", "Trading");
_stopLoss = Param(nameof(StopLoss), 2m).SetGreaterThanZero()
.SetDisplay("Stop Loss %", "Percentage loss limit from the actual entry fill", "Risk");
OrderRegistering += order => _pendingOrder = order;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1), (DarkPoolSecurity, DataType.Ticks)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ClearRuntime();
}
private void ClearRuntime()
{
_reportVolumes.Clear();
_reportIds.Clear();
_reportTime = default;
_average = null;
_cooldown = 0;
_pendingOrder = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
if (DarkPoolSecurity is null || Security is null
|| string.Equals(DarkPoolSecurity.Id, Security.Id, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("DarkPoolSecurity must explicitly identify a separate off-exchange report source.");
base.OnStarted2(time);
ClearRuntime();
StartProtection(new Unit(), new Unit(StopLoss, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Start();
}
var sma = new SimpleMovingAverage { Length = MaPeriod };
var candles = SubscribeCandles(CandleType);
candles.Bind(sma, ProcessCandle).Start();
SubscribeTicks(DarkPoolSecurity).Bind(ProcessReport).Start();
var area = CreateChartArea();
if (area is not null)
{
DrawCandles(area, candles);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private bool IsOrderPending => _pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed);
private void ProcessCandle(ICandleMessage candle, decimal average)
{
if (candle.State != CandleStates.Finished)
return;
_average = average;
if (_cooldown > 0)
_cooldown--;
if (!IsFormedAndOnlineAndAllowTrading() || IsOrderPending)
return;
if (Position > 0m && candle.ClosePrice < average)
SellMarket(Position);
else if (Position < 0m && candle.ClosePrice > average)
BuyMarket(-Position);
}
private void ProcessReport(ITickTradeMessage report)
{
if (report.IsSystem != false || report.Price <= 0m || report.Volume <= 0m
|| report.ServerTime < _reportTime || report.Id is null && string.IsNullOrEmpty(report.StringId))
return;
if (report.ServerTime > _reportTime)
{
_reportTime = report.ServerTime;
_reportIds.Clear();
}
if (!_reportIds.Add((report.Id, report.StringId)))
return;
var baseline = _reportVolumes.Count == VolumeLookback ? _reportVolumes.Average() : (decimal?)null;
_reportVolumes.Enqueue(report.Volume);
while (_reportVolumes.Count > VolumeLookback)
_reportVolumes.Dequeue();
if (baseline is null || report.Volume <= baseline.Value * VolumeMultiplier
|| _average is not decimal average || _cooldown > 0 || IsOrderPending
|| !IsFormedAndOnlineAndAllowTrading())
return;
if (report.OriginSide == Sides.Buy && report.Price > average && Position <= 0m)
{
BuyMarket(Volume + Math.Abs(Position));
_cooldown = CooldownBars;
}
else if (report.OriginSide == Sides.Sell && report.Price < average && Position >= 0m)
{
SellMarket(Volume + Math.Abs(Position));
_cooldown = CooldownBars;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, InvalidOperationException
from StockSharp.Messages import DataType, CandleStates, OrderStates, Sides, Unit, UnitTypes, Level1Fields
from StockSharp.BusinessEntities import Security, Subscription
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class dark_pool_prints_strategy(Strategy):
"""Trades classified off-exchange prints from an explicitly selected source."""
def __init__(self):
super(dark_pool_prints_strategy, self).__init__()
self._dark_pool_security = self.Param[Security]("DarkPoolSecurity", None).SetRequired().SetDisplay(
"Dark Pool Security", "Dedicated source of off-exchange trade reports", "Data")
self._ma_period = self.Param("MaPeriod", 20).SetGreaterThanZero()
self._volume_lookback = self.Param("VolumeLookback", 20).SetGreaterThanZero()
self._volume_multiplier = self.Param("VolumeMultiplier", 1.5).SetGreaterThanZero()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._cooldown_bars = self.Param("CooldownBars", 0).SetNotNegative()
self._stop_loss = self.Param("StopLoss", 2.0).SetGreaterThanZero()
self._clear_runtime()
self.OrderRegistering += self._on_order
def _clear_runtime(self):
self._report_volumes = []
self._report_ids = set()
self._report_time = None
self._average = None
self._cooldown = 0
self._pending_order = None
def _on_order(self, order):
self._pending_order = order
def GetWorkingSecurities(self):
return [(self.Security, self._candle_type.Value), (self.Security, DataType.Level1),
(self._dark_pool_security.Value, DataType.Ticks)]
def OnReseted(self):
super(dark_pool_prints_strategy, self).OnReseted()
self._clear_runtime()
def OnStarted2(self, time):
source = self._dark_pool_security.Value
if source is None or self.Security is None or source.Id.lower() == self.Security.Id.lower():
raise InvalidOperationException("DarkPoolSecurity must explicitly identify a separate off-exchange report source")
super(dark_pool_prints_strategy, self).OnStarted2(time)
self._clear_runtime()
self.StartProtection(Unit(), Unit(float(self._stop_loss.Value), UnitTypes.Percent),
useMarketOrders=True, isLocalStop=True)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Start()
sma = SimpleMovingAverage()
sma.Length = int(self._ma_period.Value)
candles = self.SubscribeCandles(self._candle_type.Value)
candles.Bind(sma, self._process_candle).Start()
self.SubscribeTicks(source).Bind(self._process_report).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, candles)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _has_pending_order(self):
return self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed)
def _process_candle(self, candle, average):
if candle.State != CandleStates.Finished:
return
self._average = float(average)
if self._cooldown > 0:
self._cooldown -= 1
if not self.IsFormedAndOnlineAndAllowTrading() or self._has_pending_order():
return
if self.Position > 0 and float(candle.ClosePrice) < self._average:
self.SellMarket(self.Position)
elif self.Position < 0 and float(candle.ClosePrice) > self._average:
self.BuyMarket(-self.Position)
def _process_report(self, report):
if report.IsSystem != False:
return
price, volume = float(report.TradePrice), float(report.TradeVolume)
identity = (report.TradeId, report.TradeStringId)
if price <= 0 or volume <= 0 or (identity[0] is None and not identity[1]):
return
if self._report_time is not None and report.ServerTime < self._report_time:
return
if self._report_time is None or report.ServerTime > self._report_time:
self._report_time = report.ServerTime
self._report_ids.clear()
if identity in self._report_ids:
return
self._report_ids.add(identity)
lookback = int(self._volume_lookback.Value)
baseline = sum(self._report_volumes) / lookback if len(self._report_volumes) == lookback else None
self._report_volumes.append(volume)
if len(self._report_volumes) > lookback:
self._report_volumes.pop(0)
if (baseline is None or volume <= baseline * float(self._volume_multiplier.Value)
or self._average is None or self._cooldown > 0 or self._has_pending_order()
or not self.IsFormedAndOnlineAndAllowTrading()):
return
if report.OriginSide == Sides.Buy and price > self._average and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
self._cooldown = int(self._cooldown_bars.Value)
elif report.OriginSide == Sides.Sell and price < self._average and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
self._cooldown = int(self._cooldown_bars.Value)
def CreateClone(self):
return dark_pool_prints_strategy()