Главная
/
Примеры стратегий
Открыть на GitHub
Стратегия «Two Pair Correlation»
Обзор
Two Pair Correlation переносит советник MetaTrader «2-Pair Correlation EA» (каталог MQL/52043) на высокоуровневый API StockSharp. Стратегия следит за ценами bid двух скоррелированных криптоинструментов (BTCUSD как основной, ETHUSD как хедж) и строит рыночно-нейтральную позицию, когда их спред выходит за границы допустимого коридора.
Основные этапы
Контроль риска – капитал портфеля отслеживается непрерывно. Если просадка от исторического максимума превышает MaxDrawdownPercent, открытие новых сделок блокируется до тех пор, пока капитал не восстановится выше RecoveryPercent от пика.
Фильтр волатильности – обе бумаги формируют 5-минутные свечи и подаются в индикатор AverageTrueRange длиной AtrPeriod. Если хотя бы одно значение ATR превышает PriceDifferenceThreshold * 0.01, сигнал считается небезопасным и пропускается.
Оценка спреда – стратегия подписывается на Level1 по обоим инструментам и на каждом обновлении сравнивает их bid. При Bid(BTCUSD) - Bid(ETHUSD) > PriceDifferenceThreshold открывается покупка BTCUSD и продажа ETHUSD. При спреде ниже -PriceDifferenceThreshold выполняется зеркальная комбинация.
Динамический объем – объём каждой ноги рассчитывается из RiskPercent текущей стоимости портфеля, делённой на искусственный стоп StopLossPips * PriceStep. Полученное значение приводится к биржевым ограничениям по шагу и диапазону объёмов.
Выход по прибыли – суммарная нереализованная прибыль обеих позиций оценивается в валюте счёта. Когда она достигает MinimumTotalProfit, пара закрывается полностью независимо от исходного направления.
Требуемые данные
Level1 (лучшие bid/ask) по основному инструменту (Security) и хеджу (SecondSecurity).
Свечи типа AtrCandleType (по умолчанию 5 минут) для обеих бумаг – источник данных для ATR.
Убедитесь, что инструменты предоставляют корректные PriceStep, StepPrice, VolumeStep, а также допустимые минимальные и максимальные объёмы – от этого зависит точность расчёта лотов и прибыли.
Параметры
Имя
Тип
Значение по умолчанию
Описание
SecondSecurity
Security
—
Хеджирующий инструмент (ETHUSD в оригинале).
MaxDrawdownPercent
decimal
20
Просадка, при которой новые входы блокируются.
RiskPercent
decimal
2
Доля капитала, используемая для расчёта объёма сделки.
PriceDifferenceThreshold
decimal
100
Порог расхождения bid-цен для открытия пары.
MinimumTotalProfit
decimal
0.30
Цель по совокупной прибыли в валюте счёта.
AtrPeriod
int
14
Длина ATR-фильтра волатильности.
RecoveryPercent
decimal
95
Процент восстановления от пика, после которого торговля возобновляется.
StopLossPips
int
50
Искусственный стоп для пересчёта RiskPercent в объёмы.
AtrCandleType
DataType
TimeSpan.FromMinutes(5).TimeFrame()
Тип свечей, используемых при вычислении ATR.
Файлы
CS/TwoPairCorrelationStrategy.cs — реализация стратегии.
README.md — документация на английском языке.
README_zh.md — документация на китайском языке.
README_ru.md — документация на русском языке.
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>
/// Mean-reversion strategy with ATR volatility filter and drawdown control.
/// Simplified from the two-pair correlation EA to single security.
/// </summary>
public class TwoPairCorrelationStrategy : Strategy
{
private readonly StrategyParam<decimal> _maxDrawdownPercent;
private readonly StrategyParam<decimal> _priceDifferenceThreshold;
private readonly StrategyParam<decimal> _minimumTotalProfit;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<DataType> _candleType;
private AverageTrueRange _atr;
private SimpleMovingAverage _sma;
private decimal _atrValue;
private decimal _entryPrice;
private decimal _peakEquity;
private bool _tradingPaused;
/// <summary>
/// Maximum drawdown percentage that pauses new entries.
/// </summary>
public decimal MaxDrawdownPercent
{
get => _maxDrawdownPercent.Value;
set => _maxDrawdownPercent.Value = value;
}
/// <summary>
/// Price deviation threshold from SMA for entry.
/// </summary>
public decimal PriceDifferenceThreshold
{
get => _priceDifferenceThreshold.Value;
set => _priceDifferenceThreshold.Value = value;
}
/// <summary>
/// Floating profit target for closing.
/// </summary>
public decimal MinimumTotalProfit
{
get => _minimumTotalProfit.Value;
set => _minimumTotalProfit.Value = value;
}
/// <summary>
/// ATR period for volatility filter.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// Candle type for signals and ATR.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public TwoPairCorrelationStrategy()
{
_maxDrawdownPercent = Param(nameof(MaxDrawdownPercent), 20m)
.SetGreaterThanZero()
.SetDisplay("Max Drawdown %", "Maximum drawdown before trading is paused", "Risk")
.SetOptimize(5m, 50m, 5m);
_priceDifferenceThreshold = Param(nameof(PriceDifferenceThreshold), 5m)
.SetGreaterThanZero()
.SetDisplay("Price Deviation", "Distance from SMA required to enter", "Signals")
.SetOptimize(1m, 20m, 1m);
_minimumTotalProfit = Param(nameof(MinimumTotalProfit), 3m)
.SetGreaterThanZero()
.SetDisplay("Profit Target", "Floating profit required to close position", "Risk")
.SetOptimize(1m, 10m, 1m);
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "Number of candles for volatility filter", "Indicators")
.SetOptimize(5, 40, 1);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Candle series for signals", "Indicators");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_atr = null;
_sma = null;
_atrValue = 0m;
_entryPrice = 0m;
_peakEquity = 0m;
_tradingPaused = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_peakEquity = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
_atr = new AverageTrueRange { Length = AtrPeriod };
_sma = new SimpleMovingAverage { Length = 20 };
SubscribeCandles(CandleType)
.Bind(_atr, _sma, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal atrValue, decimal smaValue)
{
if (candle.State != CandleStates.Finished)
return;
if (_atr == null || _sma == null || !_atr.IsFormed || !_sma.IsFormed)
return;
_atrValue = atrValue;
var price = candle.ClosePrice;
// Drawdown control
UpdateDrawdownState();
// Check profit target
if (Position != 0 && _entryPrice > 0m)
{
var pnl = Position > 0
? price - _entryPrice
: _entryPrice - price;
var profitTarget = Math.Max(MinimumTotalProfit, _atrValue * 0.5m);
if (profitTarget > 0m && pnl >= profitTarget)
{
if (Position > 0)
SellMarket(Math.Abs(Position));
else
BuyMarket(Math.Abs(Position));
_entryPrice = 0m;
return;
}
}
if (_tradingPaused)
return;
if (Position != 0)
return;
var deviation = price - smaValue;
var entryThreshold = Math.Max(PriceDifferenceThreshold, _atrValue);
if (deviation > entryThreshold)
{
SellMarket();
_entryPrice = price;
}
else if (deviation < -entryThreshold)
{
BuyMarket();
_entryPrice = price;
}
}
private void UpdateDrawdownState()
{
if (Portfolio == null)
return;
var equity = Portfolio.CurrentValue ?? Portfolio.BeginValue ?? 0m;
if (equity <= 0m)
return;
if (equity > _peakEquity)
_peakEquity = equity;
if (MaxDrawdownPercent <= 0m || _peakEquity <= 0m)
{
_tradingPaused = false;
return;
}
var drawdown = (_peakEquity - equity) / _peakEquity * 100m;
if (!_tradingPaused && drawdown >= MaxDrawdownPercent)
{
_tradingPaused = true;
}
else if (_tradingPaused && drawdown < MaxDrawdownPercent * 0.5m)
{
_tradingPaused = false;
}
}
}
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.Indicators import AverageTrueRange, SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class two_pair_correlation_strategy(Strategy):
def __init__(self):
super(two_pair_correlation_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._price_difference_threshold = self.Param("PriceDifferenceThreshold", 5.0)
self._minimum_total_profit = self.Param("MinimumTotalProfit", 3.0)
self._atr_period = self.Param("AtrPeriod", 14)
self._atr_value = 0.0
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def PriceDifferenceThreshold(self):
return self._price_difference_threshold.Value
@PriceDifferenceThreshold.setter
def PriceDifferenceThreshold(self, value):
self._price_difference_threshold.Value = value
@property
def MinimumTotalProfit(self):
return self._minimum_total_profit.Value
@MinimumTotalProfit.setter
def MinimumTotalProfit(self, value):
self._minimum_total_profit.Value = value
@property
def AtrPeriod(self):
return self._atr_period.Value
@AtrPeriod.setter
def AtrPeriod(self, value):
self._atr_period.Value = value
def OnReseted(self):
super(two_pair_correlation_strategy, self).OnReseted()
self._atr_value = 0.0
self._entry_price = 0.0
def OnStarted2(self, time):
super(two_pair_correlation_strategy, self).OnStarted2(time)
self._atr_value = 0.0
self._entry_price = 0.0
atr = AverageTrueRange()
atr.Length = self.AtrPeriod
sma = SimpleMovingAverage()
sma.Length = 20
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(atr, sma, self._process_candle).Start()
def _process_candle(self, candle, atr_value, sma_value):
if candle.State != CandleStates.Finished:
return
atr_val = float(atr_value)
sma_val = float(sma_value)
price = float(candle.ClosePrice)
self._atr_value = atr_val
# Check profit target
if self.Position != 0 and self._entry_price > 0:
if self.Position > 0:
pnl = price - self._entry_price
else:
pnl = self._entry_price - price
profit_target = max(float(self.MinimumTotalProfit), atr_val * 0.5)
if profit_target > 0 and pnl >= profit_target:
if self.Position > 0:
self.SellMarket()
else:
self.BuyMarket()
self._entry_price = 0.0
return
if self.Position != 0:
return
deviation = price - sma_val
entry_threshold = max(float(self.PriceDifferenceThreshold), atr_val)
if deviation > entry_threshold:
self.SellMarket()
self._entry_price = price
elif deviation < -entry_threshold:
self.BuyMarket()
self._entry_price = price
def CreateClone(self):
return two_pair_correlation_strategy()