Стратегия Well Martin
Описание
Well Martin – это стратегия возврата к среднему, которая использует индикаторы Bollinger Bands и Average Directional Index (ADX). Покупка выполняется при уходе цены ниже нижней полосы Боллинджера при низком значении ADX. Продажа выполняется при выходе цены выше верхней полосы при тех же условиях. Выход из позиции происходит при достижении противоположной полосы или уровней тейк‑профита и стоп‑лосса.
Параметры
- CandleType – тип свечей для расчётов.
- BollingerPeriod – период полос Боллинджера.
- BollingerWidth – множитель стандартного отклонения.
- AdxPeriod – период индикатора ADX.
- AdxLevel – порог ADX; сделки открываются только если значение ниже порога.
- Volume – объём каждой сделки.
- TakeProfit – целевая прибыль в ценовых пунктах.
- StopLoss – допустимый убыток в ценовых пунктах.
Логика работы
- Подписка на поток свечей и расчёт индикаторов Bollinger Bands и ADX.
- При отсутствии позиции:
- Покупка, если цена закрытия ниже нижней полосы и ADX ниже порога.
- Продажа, если цена закрытия выше верхней полосы и ADX ниже порога.
- Запоминается направление последней сделки и новые входы разрешаются только в том же направлении либо при отсутствии сделок.
- При длинной позиции выход выполняется при касании верхней полосы, достижении тейк‑профита или стоп‑лосса.
- При короткой позиции выход выполняется при касании нижней полосы, достижении тейк‑профита или стоп‑лосса.
Примечания
В текущей реализации используется фиксированный объём. В оригинальном коде MQL объём увеличивался после убыточной сделки; эту функциональность можно добавить при необходимости.
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;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Mean-reversion Bollinger entries gated by formed ADX; the last executed
/// entry direction limits later entries. Native SL/TP uses real entry fills.
/// </summary>
public class WellMartinStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _bollingerPeriod;
private readonly StrategyParam<decimal> _bollingerWidth;
private readonly StrategyParam<int> _adxPeriod;
private readonly StrategyParam<decimal> _adxLevel;
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<decimal> _stopLoss;
private readonly Queue<decimal> _closes = new();
private Order _pending, _entryOrder;
private Sides? _lastExecutedEntrySide;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int BollingerPeriod { get => _bollingerPeriod.Value; set => _bollingerPeriod.Value = value; }
public decimal BollingerWidth { get => _bollingerWidth.Value; set => _bollingerWidth.Value = value; }
public int AdxPeriod { get => _adxPeriod.Value; set => _adxPeriod.Value = value; }
public decimal AdxLevel { get => _adxLevel.Value; set => _adxLevel.Value = value; }
public decimal TakeProfit { get => _takeProfit.Value; set => _takeProfit.Value = value; }
public decimal StopLoss { get => _stopLoss.Value; set => _stopLoss.Value = value; }
public WellMartinStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Signal candle timeframe", "General");
_bollingerPeriod = Param(nameof(BollingerPeriod), 84).SetGreaterThanZero()
.SetDisplay("Bollinger Period", "Mean and deviation period", "Indicator");
_bollingerWidth = Param(nameof(BollingerWidth), 1.8m).SetGreaterThanZero()
.SetDisplay("Bollinger Width", "Deviation multiplier", "Indicator");
_adxPeriod = Param(nameof(AdxPeriod), 14).SetGreaterThanZero()
.SetDisplay("ADX Period", "Trend strength smoothing period", "Indicator");
_adxLevel = Param(nameof(AdxLevel), 25m).SetNotNegative()
.SetDisplay("ADX Level", "Maximum allowed trend strength", "Indicator");
_takeProfit = Param(nameof(TakeProfit), 1200m).SetNotNegative()
.SetDisplay("Take Profit", "Price-unit target", "Risk");
_stopLoss = Param(nameof(StopLoss), 1400m).SetNotNegative()
.SetDisplay("Stop Loss", "Price-unit risk", "Risk");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
_closes.Clear();
_pending = _entryOrder = null;
_lastExecutedEntrySide = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_closes.Clear();
_pending = _entryOrder = null;
_lastExecutedEntrySide = null;
StartProtection(
takeProfit: new Unit(TakeProfit, UnitTypes.Absolute),
stopLoss: new Unit(StopLoss, UnitTypes.Absolute));
var adx = new AverageDirectionalIndex { Length = AdxPeriod };
SubscribeCandles(CandleType).BindEx(adx, ProcessCandle).Start();
}
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
// Submitted orders are not fills: direction changes only after own trade execution.
if (trade.Order is not null && trade.Order == _entryOrder)
_lastExecutedEntrySide = trade.Order.Side;
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue adxValue)
{
if (candle.State != CandleStates.Finished)
return;
_closes.Enqueue(candle.ClosePrice);
if (_closes.Count > BollingerPeriod)
_closes.Dequeue();
if (_closes.Count < BollingerPeriod)
return;
if (!adxValue.IsFormed || adxValue is not AverageDirectionalIndexValue adx
|| adx.MovingAverage is not decimal strength)
return;
var mean = _closes.Average();
var variance = _closes.Average(price => (price - mean) * (price - mean));
var deviation = (decimal)Math.Sqrt((double)variance);
var lower = mean - BollingerWidth * deviation;
var upper = mean + BollingerWidth * deviation;
if (!IsFormedAndOnlineAndAllowTrading()
|| (_pending is not null && _pending.State is not (OrderStates.Done or OrderStates.Failed)))
return;
if (Position > 0m)
{
if (candle.ClosePrice >= upper)
_pending = SellMarket(Math.Abs(Position));
return;
}
if (Position < 0m)
{
if (candle.ClosePrice <= lower)
_pending = BuyMarket(Math.Abs(Position));
return;
}
if (strength >= AdxLevel)
return;
if (candle.ClosePrice < lower
&& (_lastExecutedEntrySide is null || _lastExecutedEntrySide == Sides.Buy))
{
_entryOrder = _pending = BuyMarket(Volume);
}
else if (candle.ClosePrice > upper
&& (_lastExecutedEntrySide is null || _lastExecutedEntrySide == Sides.Sell))
{
_entryOrder = _pending = SellMarket(Volume);
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, OrderStates, Sides, Unit, UnitTypes
from StockSharp.Algo.Indicators import AverageDirectionalIndex
from StockSharp.Algo.Strategies import Strategy
class well_martin_strategy(Strategy):
"""Bollinger mean reversion, low ADX filter, actual-fill direction and native protection."""
def __init__(self):
super(well_martin_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Signal timeframe", "General")
self._bollinger_period = self.Param("BollingerPeriod", 84).SetGreaterThanZero() \
.SetDisplay("Bollinger Period", "Mean and deviation period", "Indicator")
self._bollinger_width = self.Param("BollingerWidth", 1.8).SetGreaterThanZero() \
.SetDisplay("Bollinger Width", "Deviation multiplier", "Indicator")
self._adx_period = self.Param("AdxPeriod", 14).SetGreaterThanZero() \
.SetDisplay("ADX Period", "Trend strength smoothing", "Indicator")
self._adx_level = self.Param("AdxLevel", 25.0).SetNotNegative() \
.SetDisplay("ADX Level", "Maximum trend strength", "Indicator")
self._take_profit = self.Param("TakeProfit", 1200.0).SetNotNegative() \
.SetDisplay("Take Profit", "Price-unit target", "Risk")
self._stop_loss = self.Param("StopLoss", 1400.0).SetNotNegative() \
.SetDisplay("Stop Loss", "Price-unit risk", "Risk")
self._reset_history()
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def _reset_history(self):
self._closes = []
self._pending = None
self._entry_order = None
self._last_executed_entry_side = None
def OnReseted(self):
super(well_martin_strategy, self).OnReseted()
self._reset_history()
def OnStarted2(self, time):
super(well_martin_strategy, self).OnStarted2(time)
self._reset_history()
self.StartProtection(
takeProfit=Unit(self._take_profit.Value, UnitTypes.Absolute),
stopLoss=Unit(self._stop_loss.Value, UnitTypes.Absolute))
adx = AverageDirectionalIndex()
adx.Length = int(self._adx_period.Value)
self.SubscribeCandles(self.CandleType).BindEx(adx, self.ProcessCandle).Start()
def OnOwnTradeReceived(self, trade):
super(well_martin_strategy, self).OnOwnTradeReceived(trade)
if trade.Order is not None and trade.Order == self._entry_order:
self._last_executed_entry_side = trade.Order.Side
def ProcessCandle(self, candle, adx_value):
if candle.State != CandleStates.Finished:
return
length = int(self._bollinger_period.Value)
self._closes.append(float(candle.ClosePrice))
if len(self._closes) > length:
self._closes.pop(0)
if len(self._closes) < length or not adx_value.IsFormed:
return
strength_raw = adx_value.MovingAverage
if strength_raw is None:
return
strength = float(strength_raw)
mean = sum(self._closes) / length
variance = sum((price - mean) ** 2 for price in self._closes) / length
deviation = variance ** 0.5
lower = mean - float(self._bollinger_width.Value) * deviation
upper = mean + float(self._bollinger_width.Value) * deviation
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._pending is not None and self._pending.State not in (OrderStates.Done, OrderStates.Failed):
return
price = float(candle.ClosePrice)
if self.Position > 0:
if price >= upper:
self._pending = self.SellMarket(abs(self.Position))
return
if self.Position < 0:
if price <= lower:
self._pending = self.BuyMarket(abs(self.Position))
return
if strength >= float(self._adx_level.Value):
return
if price < lower and self._last_executed_entry_side in (None, Sides.Buy):
self._entry_order = self._pending = self.BuyMarket(self.Volume)
elif price > upper and self._last_executed_entry_side in (None, Sides.Sell):
self._entry_order = self._pending = self.SellMarket(self.Volume)
def CreateClone(self):
return well_martin_strategy()