Стратегия Martini Martingale
Данная стратегия реализует хеджирующий мартингейл. Она выставляет стоп-заявки с обеих сторон от текущей цены и удваивает размер позиции в противоположном направлении при движении рынка против открытой позиции на заданный шаг. Все сделки закрываются, когда накопленная прибыль превышает целевое значение.
Детали
- Вход:
- Устанавливаются buy stop выше и sell stop ниже рынка на расстояние
Step. - При исполнении одной из заявок противоположная отменяется.
- Устанавливаются buy stop выше и sell stop ниже рынка на расстояние
- Управление позицией:
- Отслеживается цена последней исполненной заявки.
- Если цена движется против позиции на
Step * orderCount, отправляется рыночная заявка в противоположную сторону с двойным объемом.
- Выход:
- Все позиции закрываются при достижении прибыли
ProfitClose.
- Все позиции закрываются при достижении прибыли
- Длинные/короткие: Да, обе стороны.
- Стопы: Стоп-заявки используются только для входа; стоп-лосс не применяется.
- Индикаторы: Нет.
- Фильтры: Нет.
Параметры
Step– шаг цены в абсолютных единицах.ProfitClose– прибыль для закрытия всех позиций.InitialVolume– начальный объем первой сделки.CandleType– тип свечей для обновления цены.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.MatchingEngine;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Martingale grid using real conditional entries linked by client-side cancellation.
/// </summary>
public class MartiniMartingaleStrategy : Strategy
{
private readonly StrategyParam<decimal> _step;
private readonly StrategyParam<decimal> _profitClose;
private readonly StrategyParam<decimal> _initialVolume;
private readonly StrategyParam<DataType> _candleType;
private Order _buyStopOrder;
private Order _sellStopOrder;
private Order _martingaleOrder;
private long? _countedOrderId;
private decimal _lastExecutionPrice;
private decimal _lastLegVolume;
private int _orderCount;
private bool _closingCycle;
private decimal _cyclePnlBase;
public decimal Step { get => _step.Value; set => _step.Value = value; }
public decimal ProfitClose { get => _profitClose.Value; set => _profitClose.Value = value; }
public decimal InitialVolume { get => _initialVolume.Value; set => _initialVolume.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
private bool IsMartingaleWorking => _martingaleOrder is { State: not (OrderStates.Done or OrderStates.Failed) };
public MartiniMartingaleStrategy()
{
_step = Param(nameof(Step), 10m).SetGreaterThanZero();
_profitClose = Param(nameof(ProfitClose), 10m).SetGreaterThanZero();
_initialVolume = Param(nameof(InitialVolume), 0.1m).SetGreaterThanZero();
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame());
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
ResetCycleState();
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetCycleState();
// An OHLC-only replay cannot order the two stop activations and a client cancellation.
// Build the signal bars from real trades so conditional entries use the same tick clock.
var subscription = new Subscription(CandleType, Security);
subscription.MarketData.BuildMode = MarketDataBuildModes.Build;
subscription.MarketData.BuildFrom = DataType.Ticks;
SubscribeCandles(subscription).Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (Position == 0m)
{
if (!_closingCycle && _buyStopOrder is null && _sellStopOrder is null)
PlaceInitialStops(candle.ClosePrice);
return;
}
if (!_closingCycle && PnL - _cyclePnlBase >= ProfitClose)
{
_closingCycle = true;
CancelInitialStops();
CloseNetPosition();
return;
}
if (_closingCycle || IsMartingaleWorking || _lastLegVolume <= 0m || _orderCount <= 0)
return;
var adverseDistance = Step * _orderCount;
if (Position > 0m && candle.LowPrice <= _lastExecutionPrice - adverseDistance)
PlaceMartingale(Sides.Sell, _lastLegVolume * 2m);
else if (Position < 0m && candle.HighPrice >= _lastExecutionPrice + adverseDistance)
PlaceMartingale(Sides.Buy, _lastLegVolume * 2m);
}
private void PlaceInitialStops(decimal center)
{
_cyclePnlBase = PnL;
_buyStopOrder = CreateStopOrder(Sides.Buy, center + Step, InitialVolume, "Martini initial buy stop");
_sellStopOrder = CreateStopOrder(Sides.Sell, center - Step, InitialVolume, "Martini initial sell stop");
RegisterOrder(_buyStopOrder);
RegisterOrder(_sellStopOrder);
}
private Order CreateStopOrder(Sides side, decimal activationPrice, decimal volume, string comment)
=> new()
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Conditional,
Condition = new StopOrderCondition { ActivationPrice = activationPrice },
Side = side,
Volume = volume,
Comment = comment,
};
private void PlaceMartingale(Sides side, decimal volume)
{
volume = NormalizeVolume(volume);
if (volume <= 0m)
return;
_martingaleOrder = side == Sides.Buy ? BuyMarket(volume) : SellMarket(volume);
}
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade?.Order is null || trade.Trade is null)
return;
if (_closingCycle)
{
if (Position == 0m)
{
_closingCycle = false;
ResetCycleState(keepPnlBase: false);
}
return;
}
if (trade.Order.Type == OrderTypes.Conditional)
{
if (trade.Order.Side == Sides.Buy)
CancelIfActive(_sellStopOrder);
else
CancelIfActive(_buyStopOrder);
_buyStopOrder = null;
_sellStopOrder = null;
}
_lastExecutionPrice = trade.Trade.Price;
// A leg filled in several trades is still one order of its full volume.
if (_countedOrderId != trade.Order.TransactionId)
{
_countedOrderId = trade.Order.TransactionId;
_lastLegVolume = trade.Order.Volume;
_orderCount++;
}
}
protected override void OnOrderChanged(Order order)
{
base.OnOrderChanged(order);
if (order is null || order.State != OrderStates.Done)
return;
if (_buyStopOrder is not null && order.TransactionId == _buyStopOrder.TransactionId && order.Balance == order.Volume)
_buyStopOrder = null;
if (_sellStopOrder is not null && order.TransactionId == _sellStopOrder.TransactionId && order.Balance == order.Volume)
_sellStopOrder = null;
}
private void CancelInitialStops()
{
CancelIfActive(_buyStopOrder);
CancelIfActive(_sellStopOrder);
_buyStopOrder = null;
_sellStopOrder = null;
}
private void CancelIfActive(Order order)
{
if (order is { State: OrderStates.Active })
CancelOrder(order);
}
private void CloseNetPosition()
{
if (Position > 0m)
SellMarket(Math.Abs(Position));
else if (Position < 0m)
BuyMarket(Math.Abs(Position));
}
private decimal NormalizeVolume(decimal volume)
{
if (Security?.MaxVolume is decimal max && max > 0m)
volume = Math.Min(volume, max);
if (Security?.MinVolume is decimal min && min > 0m)
volume = Math.Max(volume, min);
if (Security?.VolumeStep is decimal step && step > 0m)
volume = Math.Floor(volume / step) * step;
return volume;
}
private void ResetCycleState(bool keepPnlBase = false)
{
_buyStopOrder = null;
_sellStopOrder = null;
_martingaleOrder = null;
_countedOrderId = null;
_lastExecutionPrice = 0m;
_lastLegVolume = 0m;
_orderCount = 0;
_closingCycle = false;
if (!keepPnlBase)
_cyclePnlBase = PnL;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.MatchingEngine")
from System import TimeSpan, Math, Decimal
from StockSharp.Messages import DataType, CandleStates, Sides, OrderTypes, OrderStates, MarketDataBuildModes
from StockSharp.BusinessEntities import Order, Subscription
from StockSharp.MatchingEngine import StopOrderCondition
from StockSharp.Algo.Strategies import Strategy
class martini_martingale_strategy(Strategy):
def __init__(self):
super(martini_martingale_strategy, self).__init__()
self._step = self.Param("Step", 10.0).SetGreaterThanZero()
self._profit_close = self.Param("ProfitClose", 10.0).SetGreaterThanZero()
self._initial_volume = self.Param("InitialVolume", 0.1).SetGreaterThanZero()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._buy_stop_order = None
self._sell_stop_order = None
self._martingale_order = None
self._counted_order_id = None
self._last_execution_price = Decimal.Zero
self._last_leg_volume = Decimal.Zero
self._order_count = 0
self._closing_cycle = False
self._cycle_pnl_base = Decimal.Zero
def GetWorkingSecurities(self):
return [(self.Security, self._candle_type.Value)]
def OnReseted(self):
super(martini_martingale_strategy, self).OnReseted()
self._reset_cycle_state()
def OnStarted2(self, time):
super(martini_martingale_strategy, self).OnStarted2(time)
self._reset_cycle_state()
# Real trades preserve the intrabar ordering of stop fills and client cancellations.
subscription = Subscription(self._candle_type.Value, self.Security)
subscription.MarketData.BuildMode = MarketDataBuildModes.Build
subscription.MarketData.BuildFrom = DataType.Ticks
self.SubscribeCandles(subscription).Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self.Position == 0:
if not self._closing_cycle and self._buy_stop_order is None and self._sell_stop_order is None:
self._place_initial_stops(float(candle.ClosePrice))
return
if not self._closing_cycle and self.PnL - self._cycle_pnl_base >= Decimal(self._profit_close.Value):
self._closing_cycle = True
self._cancel_initial_stops()
self._close_net_position()
return
if self._closing_cycle or self._is_martingale_working() or self._last_leg_volume <= 0 or self._order_count <= 0:
return
adverse = Decimal(self._step.Value) * Decimal(self._order_count)
if self.Position > 0 and candle.LowPrice <= self._last_execution_price - adverse:
self._place_martingale(Sides.Sell, self._last_leg_volume * Decimal(2))
elif self.Position < 0 and candle.HighPrice >= self._last_execution_price + adverse:
self._place_martingale(Sides.Buy, self._last_leg_volume * Decimal(2))
def _is_martingale_working(self):
order = self._martingale_order
return order is not None and order.State != OrderStates.Done and order.State != OrderStates.Failed
def _place_initial_stops(self, center):
self._cycle_pnl_base = self.PnL
self._buy_stop_order = self._create_stop(Sides.Buy, center + float(self._step.Value), float(self._initial_volume.Value), "Martini initial buy stop")
self._sell_stop_order = self._create_stop(Sides.Sell, center - float(self._step.Value), float(self._initial_volume.Value), "Martini initial sell stop")
self.RegisterOrder(self._buy_stop_order)
self.RegisterOrder(self._sell_stop_order)
def _create_stop(self, side, activation, volume, comment):
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Conditional
condition = StopOrderCondition()
condition.ActivationPrice = activation
order.Condition = condition
order.Side = side
order.Volume = volume
order.Comment = comment
return order
def _place_martingale(self, side, volume):
volume = self._normalize_volume(volume)
if volume <= 0:
return
if side == Sides.Buy:
self._martingale_order = self.BuyMarket(volume)
else:
self._martingale_order = self.SellMarket(volume)
def OnOwnTradeReceived(self, trade):
super(martini_martingale_strategy, self).OnOwnTradeReceived(trade)
if trade is None or trade.Order is None or trade.Trade is None:
return
if self._closing_cycle:
if self.Position == 0:
self._reset_cycle_state()
return
if trade.Order.Type == OrderTypes.Conditional:
if trade.Order.Side == Sides.Buy:
self._cancel_if_active(self._sell_stop_order)
else:
self._cancel_if_active(self._buy_stop_order)
self._buy_stop_order = None
self._sell_stop_order = None
self._last_execution_price = trade.Trade.TradePrice
# A leg filled in several trades is still one order of its full volume.
if self._counted_order_id != trade.Order.TransactionId:
self._counted_order_id = trade.Order.TransactionId
self._last_leg_volume = trade.Order.Volume
self._order_count += 1
def OnOrderChanged(self, order):
super(martini_martingale_strategy, self).OnOrderChanged(order)
if order is None or order.State != OrderStates.Done:
return
if self._buy_stop_order is not None and order.TransactionId == self._buy_stop_order.TransactionId and order.Balance == order.Volume:
self._buy_stop_order = None
if self._sell_stop_order is not None and order.TransactionId == self._sell_stop_order.TransactionId and order.Balance == order.Volume:
self._sell_stop_order = None
def _cancel_initial_stops(self):
self._cancel_if_active(self._buy_stop_order)
self._cancel_if_active(self._sell_stop_order)
self._buy_stop_order = None
self._sell_stop_order = None
def _cancel_if_active(self, order):
if order is not None and order.State == OrderStates.Active:
self.CancelOrder(order)
def _close_net_position(self):
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
elif self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
def _normalize_volume(self, volume):
security = self.Security
if security is not None:
if security.MaxVolume is not None and security.MaxVolume > 0:
volume = Math.Min(volume, security.MaxVolume)
if security.MinVolume is not None and security.MinVolume > 0:
volume = Math.Max(volume, security.MinVolume)
if security.VolumeStep is not None and security.VolumeStep > 0:
step = security.VolumeStep
volume = Math.Floor(volume / step) * step
return volume
def _reset_cycle_state(self):
self._buy_stop_order = None
self._sell_stop_order = None
self._martingale_order = None
self._counted_order_id = None
self._last_execution_price = Decimal.Zero
self._last_leg_volume = Decimal.Zero
self._order_count = 0
self._closing_cycle = False
self._cycle_pnl_base = self.PnL
def CreateClone(self):
return martini_martingale_strategy()