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;
using StockSharp.Algo;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// VR Overturn strategy that alternates between martingale and anti-martingale sizing rules.
/// It opens a single position at a time and reverses direction after losses while
/// optionally increasing size after wins depending on the selected mode.
/// </summary>
public class VrOverturnStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _volumeEpsilon;
public enum InitialDirections
{
/// <summary>
/// Start with a long position.
/// </summary>
Buy,
/// <summary>
/// Start with a short position.
/// </summary>
Sell
}
public enum TradeModes
{
/// <summary>
/// Increase size after losses and reset after wins.
/// </summary>
Martingale,
/// <summary>
/// Increase size after wins and reset after losses.
/// </summary>
AntiMartingale
}
private readonly StrategyParam<InitialDirections> _initialDirection;
private readonly StrategyParam<TradeModes> _tradeMode;
private readonly StrategyParam<decimal> _baseVolume;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<decimal> _lotMultiplier;
private decimal _pipSize;
private Sides? _pendingEntrySide;
private Sides? _activeSide;
private decimal _entryPrice;
private decimal _openedVolume;
private decimal _closedVolume;
private decimal _realizedPnL;
private decimal _lastClosedVolume;
private decimal _lastClosedProfit;
private Sides? _lastClosedSide;
private bool _hasClosedHistory;
/// <summary>
/// Initializes a new instance of the <see cref="VrOverturnStrategy"/> class.
/// </summary>
public VrOverturnStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle type for data feed", "General");
_volumeEpsilon = Param(nameof(VolumeEpsilon), 1e-6m)
.SetGreaterThanZero()
.SetDisplay("Volume Epsilon", "Minimum volume threshold to treat position as flat", "Risk");
_initialDirection = Param(nameof(FirstPositionDirection), InitialDirections.Buy)
.SetDisplay("Initial Direction", "Direction of the very first trade", "Trading");
_tradeMode = Param(nameof(Mode), TradeModes.Martingale)
.SetDisplay("Trading Mode", "Choose martingale or anti-martingale sizing", "Trading");
_baseVolume = Param(nameof(BaseVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Base Volume", "Initial order size", "Risk")
;
_stopLossPips = Param(nameof(StopLossPips), 300)
.SetGreaterThanZero()
.SetDisplay("Stop Loss (pips)", "Distance to stop loss in pips", "Risk")
;
_takeProfitPips = Param(nameof(TakeProfitPips), 900)
.SetGreaterThanZero()
.SetDisplay("Take Profit (pips)", "Distance to take profit in pips", "Risk")
;
_lotMultiplier = Param(nameof(LotMultiplier), 1.6m)
.SetGreaterThanZero()
.SetDisplay("Lot Multiplier", "Multiplier applied after losses or wins", "Risk")
;
}
/// <summary>
/// Candle type for data feed.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Volume tolerance used to consider a position fully closed.
/// </summary>
public decimal VolumeEpsilon
{
get => _volumeEpsilon.Value;
set => _volumeEpsilon.Value = value;
}
/// <summary>
/// Direction of the very first position.
/// </summary>
public InitialDirections FirstPositionDirection
{
get => _initialDirection.Value;
set => _initialDirection.Value = value;
}
/// <summary>
/// Selected sizing regime.
/// </summary>
public TradeModes Mode
{
get => _tradeMode.Value;
set => _tradeMode.Value = value;
}
/// <summary>
/// Base contract volume used for new sequences.
/// </summary>
public decimal BaseVolume
{
get => _baseVolume.Value;
set => _baseVolume.Value = value;
}
/// <summary>
/// Stop loss distance expressed in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit distance expressed in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Multiplier applied after wins or losses depending on the selected mode.
/// </summary>
public decimal LotMultiplier
{
get => _lotMultiplier.Value;
set => _lotMultiplier.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pipSize = 0m;
_pendingEntrySide = null;
_activeSide = null;
_entryPrice = 0m;
_openedVolume = 0m;
_closedVolume = 0m;
_realizedPnL = 0m;
_lastClosedVolume = 0m;
_lastClosedProfit = 0m;
_lastClosedSide = null;
_hasClosedHistory = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = CalculatePipSize();
var stopDistance = StopLossPips * _pipSize;
var takeDistance = TakeProfitPips * _pipSize;
// Subscribe to candles so the backtest emulator has price data.
var subscription = SubscribeCandles(CandleType);
subscription.Bind(OnCandle).Start();
StartProtection(
takeProfit: new Unit(takeDistance, UnitTypes.Absolute),
stopLoss: new Unit(stopDistance, UnitTypes.Absolute),
useMarketOrders: true);
}
private void OnCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
// Try to open a position if flat (handles both initial and post-exit entries).
if (Position == 0 && !_pendingEntrySide.HasValue)
TryOpenNextPosition();
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
if (trade?.Order == null)
return;
var side = trade.Order.Side;
var volume = trade.Trade.Volume;
var price = trade.Trade.Price;
if (volume <= 0m)
return;
if (_pendingEntrySide.HasValue && side == _pendingEntrySide.Value)
{
RegisterEntry(price, volume, side);
_pendingEntrySide = null;
return;
}
if (_activeSide == null)
return;
if (side == _activeSide)
{
RegisterEntry(price, volume, side);
return;
}
RegisterExit(price, volume);
}
private void RegisterEntry(decimal price, decimal volume, Sides side)
{
var previousVolume = _openedVolume;
_openedVolume += volume;
_entryPrice = previousVolume <= 0m
? price
: (_entryPrice * previousVolume + price * volume) / _openedVolume;
_activeSide = side;
}
private void RegisterExit(decimal price, decimal volume)
{
_closedVolume += volume;
var profit = _activeSide == Sides.Buy
? (price - _entryPrice) * volume
: (_entryPrice - price) * volume;
_realizedPnL += profit;
if (_closedVolume + VolumeEpsilon < _openedVolume)
return;
var closedVolume = _openedVolume;
_lastClosedSide = _activeSide;
_lastClosedVolume = closedVolume;
_lastClosedProfit = _realizedPnL;
_hasClosedHistory = true;
_activeSide = null;
_openedVolume = 0m;
_closedVolume = 0m;
_realizedPnL = 0m;
_entryPrice = 0m;
TryOpenNextPosition();
}
private void TryOpenNextPosition()
{
// Strategy has no indicators to check formation on.
if (Position != 0 || _pendingEntrySide.HasValue)
return;
var baseVolume = AdjustVolume(BaseVolume);
if (baseVolume <= 0m)
return;
Sides nextSide;
decimal orderVolume;
if (!_hasClosedHistory)
{
nextSide = FirstPositionDirection == InitialDirections.Buy ? Sides.Buy : Sides.Sell;
orderVolume = baseVolume;
}
else if (_lastClosedSide.HasValue)
{
var referenceVolume = _lastClosedVolume > 0m ? _lastClosedVolume : baseVolume;
if (_lastClosedProfit > 0m && Mode == TradeModes.Martingale)
referenceVolume = baseVolume;
if (_lastClosedProfit < 0m && Mode == TradeModes.AntiMartingale)
referenceVolume = baseVolume;
if (_lastClosedSide == Sides.Buy)
{
if (_lastClosedProfit > 0m)
{
nextSide = Sides.Buy;
orderVolume = referenceVolume * GetWinningMultiplier();
}
else if (_lastClosedProfit < 0m)
{
nextSide = Sides.Sell;
orderVolume = referenceVolume * GetLosingMultiplier();
}
else
{
return;
}
}
else
{
if (_lastClosedProfit > 0m)
{
nextSide = Sides.Sell;
orderVolume = referenceVolume * GetWinningMultiplier();
}
else if (_lastClosedProfit < 0m)
{
nextSide = Sides.Buy;
orderVolume = referenceVolume * GetLosingMultiplier();
}
else
{
return;
}
}
}
else
{
nextSide = FirstPositionDirection == InitialDirections.Buy ? Sides.Buy : Sides.Sell;
orderVolume = baseVolume;
}
orderVolume = AdjustVolume(orderVolume);
if (orderVolume <= 0m)
return;
_pendingEntrySide = nextSide;
if (nextSide == Sides.Buy)
BuyMarket();
else
SellMarket();
}
private decimal GetWinningMultiplier()
{
return Mode == TradeModes.Martingale ? 1m : LotMultiplier;
}
private decimal GetLosingMultiplier()
{
return Mode == TradeModes.Martingale ? LotMultiplier : 1m;
}
private decimal CalculatePipSize()
{
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
return 1m;
var temp = step;
var digits = 0;
while (temp < 1m && digits < 10)
{
temp *= 10m;
digits++;
}
return digits == 3 || digits == 5 ? step * 10m : step;
}
private decimal AdjustVolume(decimal volume)
{
if (volume <= 0m)
return 0m;
var step = Security?.VolumeStep ?? 0m;
if (step > 0m)
{
var stepsCount = Math.Floor(volume / step);
volume = stepsCount * step;
}
return volume;
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class vr_overturn_strategy(Strategy):
"""Martingale/anti-martingale reversal: alternates direction after losses with StartProtection SL/TP."""
def __init__(self):
super(vr_overturn_strategy, self).__init__()
self._sl = self.Param("StopLossPips", 300).SetGreaterThanZero().SetDisplay("Stop Loss", "SL in pips", "Risk")
self._tp = self.Param("TakeProfitPips", 900).SetGreaterThanZero().SetDisplay("Take Profit", "TP in pips", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))).SetDisplay("Candle Type", "Timeframe", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(vr_overturn_strategy, self).OnReseted()
self._entered = False
self._is_long = True
def OnStarted2(self, time):
super(vr_overturn_strategy, self).OnStarted2(time)
self._entered = False
self._is_long = True
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
sl = self._sl.Value
tp = self._tp.Value
self.StartProtection(
takeProfit=Unit(float(tp), UnitTypes.Absolute) if tp > 0 else None,
stopLoss=Unit(float(sl), UnitTypes.Absolute) if sl > 0 else None,
useMarketOrders=True
)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
if self.Position == 0 and not self._entered:
# Initial entry
self.BuyMarket()
self._is_long = True
self._entered = True
elif self.Position == 0 and self._entered:
# Re-enter in opposite direction after exit
if self._is_long:
self.SellMarket()
self._is_long = False
else:
self.BuyMarket()
self._is_long = True
def CreateClone(self):
return vr_overturn_strategy()