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>
/// Dealers Trade v7.51 strategy ported from MetaTrader 4 implementation.
/// Builds directional bias from classic pivot and floating pivot levels
/// and scales into the bias when price retraces by a fixed pip distance.
/// Applies martingale-style position sizing with configurable stop-loss,
/// take-profit, and trailing-stop management.
/// </summary>
public class DealersTradeV751RivotStrategy : Strategy
{
private readonly StrategyParam<int> _maxTrades;
private readonly StrategyParam<decimal> _pipDistance;
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<decimal> _trailingStop;
private readonly StrategyParam<decimal> _volumeMultiplier;
private readonly StrategyParam<decimal> _maxVolume;
private readonly StrategyParam<decimal> _gapThreshold;
private readonly StrategyParam<DataType> _candleType;
private ICandleMessage _previousCandle;
private decimal _pivotLevel;
private decimal _floatingPivot;
private decimal _gapInPips;
private decimal _lastEntryPrice;
private decimal _averageEntryPrice;
private decimal? _trailingStopLevel;
private int _direction; // -1 short, 0 neutral, 1 long
private int _entriesInSeries;
/// <summary>
/// Maximum number of entries allowed in one scaling series.
/// </summary>
public int MaxTrades
{
get => _maxTrades.Value;
set => _maxTrades.Value = value;
}
/// <summary>
/// Distance in pips between martingale entries.
/// </summary>
public decimal PipDistance
{
get => _pipDistance.Value;
set => _pipDistance.Value = value;
}
/// <summary>
/// Take-profit distance in pips.
/// </summary>
public decimal TakeProfit
{
get => _takeProfit.Value;
set => _takeProfit.Value = value;
}
/// <summary>
/// Stop-loss distance in pips.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Trailing-stop distance in pips.
/// </summary>
public decimal TrailingStop
{
get => _trailingStop.Value;
set => _trailingStop.Value = value;
}
/// <summary>
/// Multiplier applied to volume for each additional entry.
/// </summary>
public decimal VolumeMultiplier
{
get => _volumeMultiplier.Value;
set => _volumeMultiplier.Value = value;
}
/// <summary>
/// Maximum allowed volume for a single entry.
/// </summary>
public decimal MaxVolume
{
get => _maxVolume.Value;
set => _maxVolume.Value = value;
}
/// <summary>
/// Minimum pivot gap in pips required to activate the bias.
/// </summary>
public decimal GapThreshold
{
get => _gapThreshold.Value;
set => _gapThreshold.Value = value;
}
/// <summary>
/// Type of candles used for pivot calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public DealersTradeV751RivotStrategy()
{
_maxTrades = Param(nameof(MaxTrades), 2)
.SetGreaterThanZero()
.SetDisplay("Max Trades", "Maximum number of martingale entries", "Position Sizing")
.SetOptimize(1, 10, 1);
_pipDistance = Param(nameof(PipDistance), 10m)
.SetGreaterThanZero()
.SetDisplay("Pip Distance", "Distance between averaged entries in pips", "Position Sizing")
.SetOptimize(2m, 15m, 1m);
_takeProfit = Param(nameof(TakeProfit), 15m)
.SetGreaterThanZero()
.SetDisplay("Take Profit", "Take-profit distance in pips", "Risk Management")
.SetOptimize(5m, 50m, 5m);
_stopLoss = Param(nameof(StopLoss), 90m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss", "Stop-loss distance in pips", "Risk Management")
.SetOptimize(30m, 200m, 10m);
_trailingStop = Param(nameof(TrailingStop), 15m)
.SetGreaterThanZero()
.SetDisplay("Trailing Stop", "Trailing-stop distance in pips", "Risk Management")
.SetOptimize(5m, 40m, 5m);
_volumeMultiplier = Param(nameof(VolumeMultiplier), 1.5m)
.SetGreaterThanZero()
.SetDisplay("Volume Multiplier", "Multiplier applied after each new entry", "Position Sizing")
.SetOptimize(1.1m, 3m, 0.1m);
_maxVolume = Param(nameof(MaxVolume), 5m)
.SetGreaterThanZero()
.SetDisplay("Max Volume", "Upper limit for single-entry volume", "Position Sizing");
_gapThreshold = Param(nameof(GapThreshold), 15m)
.SetGreaterThanZero()
.SetDisplay("Gap Threshold", "Minimal pivot gap required to enable trading", "Signal")
.SetOptimize(3m, 15m, 1m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles used for pivot calculations", "Signal");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetSeries();
_previousCandle = null;
_pivotLevel = 0m;
_floatingPivot = 0m;
_gapInPips = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
/// <inheritdoc />
protected override void OnPositionReceived(Position position)
{
base.OnPositionReceived(position);
if (Position == 0m)
{
// Reset martingale state once the position is closed externally.
ResetSeries();
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_previousCandle == null)
{
_previousCandle = candle;
return;
}
UpdatePivots(candle);
if (Position == 0m && _entriesInSeries > 0)
{
// Force reset when no exposure remains but scaling data still exists.
ResetSeries();
}
if (_entriesInSeries > 0)
{
ManageRisk(candle.ClosePrice);
}
if (_entriesInSeries >= MaxTrades)
{
_previousCandle = candle;
return;
}
if (_direction == 0)
{
EvaluateDirection(candle);
}
TryEnter(candle);
_previousCandle = candle;
}
private void UpdatePivots(ICandleMessage candle)
{
var step = GetPriceStep();
_pivotLevel = (_previousCandle!.HighPrice + _previousCandle.LowPrice + _previousCandle.ClosePrice + candle.OpenPrice) / 4m;
_floatingPivot = (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m;
_gapInPips = step == 0m ? 0m : Math.Abs(_pivotLevel - _floatingPivot) / step;
}
private void EvaluateDirection(ICandleMessage candle)
{
var price = candle.ClosePrice;
if (price > _pivotLevel && price > _floatingPivot && _gapInPips >= GapThreshold)
{
_direction = 1;
LogInfo($"Bias switched to long. Pivot={_pivotLevel:F5}, Floating={_floatingPivot:F5}, Gap={_gapInPips:F2} pips.");
}
else if (price < _pivotLevel && price < _floatingPivot && _gapInPips >= GapThreshold)
{
_direction = -1;
LogInfo($"Bias switched to short. Pivot={_pivotLevel:F5}, Floating={_floatingPivot:F5}, Gap={_gapInPips:F2} pips.");
}
}
private void TryEnter(ICandleMessage candle)
{
if (_direction == 0)
return;
var price = candle.ClosePrice;
var step = GetPriceStep();
var distance = PipDistance * step;
if (_direction > 0)
{
if (_entriesInSeries == 0 || (_lastEntryPrice - price) >= distance)
{
EnterLong(price);
}
}
else
{
if (_entriesInSeries == 0 || (price - _lastEntryPrice) >= distance)
{
EnterShort(price);
}
}
}
private void EnterLong(decimal price)
{
var volume = CalculateNextVolume();
_lastEntryPrice = price;
_averageEntryPrice = UpdateAveragePrice(price, volume, true);
_entriesInSeries++;
LogInfo($"Opening long entry #{_entriesInSeries} at {price:F5} with volume {volume}.");
BuyMarket(volume);
}
private void EnterShort(decimal price)
{
var volume = CalculateNextVolume();
_lastEntryPrice = price;
_averageEntryPrice = UpdateAveragePrice(price, volume, false);
_entriesInSeries++;
LogInfo($"Opening short entry #{_entriesInSeries} at {price:F5} with volume {volume}.");
SellMarket(volume);
}
private decimal CalculateNextVolume()
{
var volume = Volume;
for (var i = 0; i < _entriesInSeries; i++)
{
volume *= VolumeMultiplier;
if (volume >= MaxVolume)
{
volume = MaxVolume;
break;
}
}
var volumeStep = Security?.VolumeStep ?? 0.01m;
if (volumeStep > 0m)
{
volume = Math.Ceiling(volume / volumeStep) * volumeStep;
}
return volume;
}
private decimal UpdateAveragePrice(decimal price, decimal volume, bool isLong)
{
var existingVolume = Math.Abs(Position);
var side = isLong ? 1m : -1m;
if (existingVolume <= 0m)
{
return price;
}
var totalVolume = existingVolume + volume;
var weightedAverage = ((_averageEntryPrice * existingVolume * side) + (price * volume)) / totalVolume;
return Math.Abs(weightedAverage);
}
private void ManageRisk(decimal price)
{
if (_entriesInSeries == 0)
{
_trailingStopLevel = null;
return;
}
var step = GetPriceStep();
var stopDistance = StopLoss * step;
var takeDistance = TakeProfit * step;
var trailingDistance = TrailingStop * step;
if (_direction > 0)
{
var lossLevel = _averageEntryPrice - stopDistance;
var profitLevel = _averageEntryPrice + takeDistance;
if (price <= lossLevel)
{
LogInfo($"Long stop-loss triggered at {price:F5}. Average entry {_averageEntryPrice:F5}.");
SellMarket(Math.Abs(Position));
ResetSeries();
return;
}
if (price >= profitLevel)
{
LogInfo($"Long take-profit triggered at {price:F5}. Average entry {_averageEntryPrice:F5}.");
SellMarket(Math.Abs(Position));
ResetSeries();
return;
}
if (TrailingStop > 0m)
{
var candidate = price - trailingDistance;
if (_trailingStopLevel == null || candidate > _trailingStopLevel)
{
_trailingStopLevel = candidate;
}
if (_trailingStopLevel != null && price <= _trailingStopLevel)
{
LogInfo($"Long trailing stop activated at {price:F5}.");
SellMarket(Math.Abs(Position));
ResetSeries();
}
}
}
else if (_direction < 0)
{
var lossLevel = _averageEntryPrice + stopDistance;
var profitLevel = _averageEntryPrice - takeDistance;
if (price >= lossLevel)
{
LogInfo($"Short stop-loss triggered at {price:F5}. Average entry {_averageEntryPrice:F5}.");
BuyMarket(Math.Abs(Position));
ResetSeries();
return;
}
if (price <= profitLevel)
{
LogInfo($"Short take-profit triggered at {price:F5}. Average entry {_averageEntryPrice:F5}.");
BuyMarket(Math.Abs(Position));
ResetSeries();
return;
}
if (TrailingStop > 0m)
{
var candidate = price + trailingDistance;
if (_trailingStopLevel == null || candidate < _trailingStopLevel)
{
_trailingStopLevel = candidate;
}
if (_trailingStopLevel != null && price >= _trailingStopLevel)
{
LogInfo($"Short trailing stop activated at {price:F5}.");
BuyMarket(Math.Abs(Position));
ResetSeries();
}
}
}
}
private decimal GetPriceStep()
{
var step = Security?.PriceStep ?? 0m;
if (step == 0m)
{
// Fallback to four decimal places when instrument metadata is unknown.
step = 0.0001m;
}
return step;
}
private void ResetSeries()
{
_direction = 0;
_entriesInSeries = 0;
_lastEntryPrice = 0m;
_averageEntryPrice = 0m;
_trailingStopLevel = null;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class dealers_trade_v751_rivot_strategy(Strategy):
"""
Dealers Trade v7.51 strategy ported from MetaTrader 4.
Builds directional bias from classic pivot and floating pivot levels,
scales into the bias when price retraces by a fixed pip distance.
Applies martingale-style position sizing with SL/TP and trailing stop.
"""
def __init__(self):
super(dealers_trade_v751_rivot_strategy, self).__init__()
self._max_trades = self.Param("MaxTrades", 2) \
.SetDisplay("Max Trades", "Maximum number of martingale entries", "Position Sizing")
self._pip_distance = self.Param("PipDistance", 10.0) \
.SetDisplay("Pip Distance", "Distance between averaged entries in pips", "Position Sizing")
self._take_profit = self.Param("TakeProfit", 15.0) \
.SetDisplay("Take Profit", "Take-profit distance in pips", "Risk Management")
self._stop_loss = self.Param("StopLoss", 90.0) \
.SetDisplay("Stop Loss", "Stop-loss distance in pips", "Risk Management")
self._trailing_stop = self.Param("TrailingStop", 15.0) \
.SetDisplay("Trailing Stop", "Trailing-stop distance in pips", "Risk Management")
self._volume_multiplier = self.Param("VolumeMultiplier", 1.5) \
.SetDisplay("Volume Multiplier", "Multiplier applied after each new entry", "Position Sizing")
self._max_volume = self.Param("MaxVolume", 5.0) \
.SetDisplay("Max Volume", "Upper limit for single-entry volume", "Position Sizing")
self._gap_threshold = self.Param("GapThreshold", 15.0) \
.SetDisplay("Gap Threshold", "Minimal pivot gap required to enable trading", "Signal")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles used for pivot calculations", "Signal")
self._previous_candle = None
self._pivot_level = 0.0
self._floating_pivot = 0.0
self._gap_in_pips = 0.0
self._last_entry_price = 0.0
self._average_entry_price = 0.0
self._trailing_stop_level = None
self._direction = 0
self._entries_in_series = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(dealers_trade_v751_rivot_strategy, self).OnReseted()
self._reset_series()
self._previous_candle = None
self._pivot_level = 0.0
self._floating_pivot = 0.0
self._gap_in_pips = 0.0
def OnStarted2(self, time):
super(dealers_trade_v751_rivot_strategy, self).OnStarted2(time)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def OnPositionReceived(self, position):
super(dealers_trade_v751_rivot_strategy, self).OnPositionReceived(position)
if self.Position == 0:
self._reset_series()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._previous_candle is None:
self._previous_candle = candle
return
self._update_pivots(candle)
if self.Position == 0 and self._entries_in_series > 0:
self._reset_series()
if self._entries_in_series > 0:
self._manage_risk(float(candle.ClosePrice))
if self._entries_in_series >= self._max_trades.Value:
self._previous_candle = candle
return
if self._direction == 0:
self._evaluate_direction(candle)
self._try_enter(candle)
self._previous_candle = candle
def _update_pivots(self, candle):
step = self._get_price_step()
self._pivot_level = (float(self._previous_candle.HighPrice) + float(self._previous_candle.LowPrice) +
float(self._previous_candle.ClosePrice) + float(candle.OpenPrice)) / 4.0
self._floating_pivot = (float(candle.HighPrice) + float(candle.LowPrice) + float(candle.ClosePrice)) / 3.0
self._gap_in_pips = abs(self._pivot_level - self._floating_pivot) / step if step != 0 else 0.0
def _evaluate_direction(self, candle):
price = float(candle.ClosePrice)
if price > self._pivot_level and price > self._floating_pivot and self._gap_in_pips >= self._gap_threshold.Value:
self._direction = 1
elif price < self._pivot_level and price < self._floating_pivot and self._gap_in_pips >= self._gap_threshold.Value:
self._direction = -1
def _try_enter(self, candle):
if self._direction == 0:
return
price = float(candle.ClosePrice)
step = self._get_price_step()
distance = self._pip_distance.Value * step
if self._direction > 0:
if self._entries_in_series == 0 or (self._last_entry_price - price) >= distance:
self._enter_long(price)
else:
if self._entries_in_series == 0 or (price - self._last_entry_price) >= distance:
self._enter_short(price)
def _enter_long(self, price):
self._last_entry_price = price
existing_volume = abs(float(self.Position))
if existing_volume <= 0:
self._average_entry_price = price
else:
total = existing_volume + 1.0
self._average_entry_price = abs((self._average_entry_price * existing_volume + price) / total)
self._entries_in_series += 1
self.BuyMarket()
def _enter_short(self, price):
self._last_entry_price = price
existing_volume = abs(float(self.Position))
if existing_volume <= 0:
self._average_entry_price = price
else:
total = existing_volume + 1.0
self._average_entry_price = abs((self._average_entry_price * existing_volume * -1.0 + price) / total)
self._entries_in_series += 1
self.SellMarket()
def _manage_risk(self, price):
if self._entries_in_series == 0:
self._trailing_stop_level = None
return
step = self._get_price_step()
stop_distance = self._stop_loss.Value * step
take_distance = self._take_profit.Value * step
trailing_distance = self._trailing_stop.Value * step
if self._direction > 0:
loss_level = self._average_entry_price - stop_distance
profit_level = self._average_entry_price + take_distance
if price <= loss_level:
self.SellMarket()
self._reset_series()
return
if price >= profit_level:
self.SellMarket()
self._reset_series()
return
if self._trailing_stop.Value > 0:
candidate = price - trailing_distance
if self._trailing_stop_level is None or candidate > self._trailing_stop_level:
self._trailing_stop_level = candidate
if self._trailing_stop_level is not None and price <= self._trailing_stop_level:
self.SellMarket()
self._reset_series()
elif self._direction < 0:
loss_level = self._average_entry_price + stop_distance
profit_level = self._average_entry_price - take_distance
if price >= loss_level:
self.BuyMarket()
self._reset_series()
return
if price <= profit_level:
self.BuyMarket()
self._reset_series()
return
if self._trailing_stop.Value > 0:
candidate = price + trailing_distance
if self._trailing_stop_level is None or candidate < self._trailing_stop_level:
self._trailing_stop_level = candidate
if self._trailing_stop_level is not None and price >= self._trailing_stop_level:
self.BuyMarket()
self._reset_series()
def _get_price_step(self):
step = 0.0001
if self.Security is not None and self.Security.PriceStep is not None:
step = float(self.Security.PriceStep)
if step <= 0:
step = 0.0001
return step
def _reset_series(self):
self._direction = 0
self._entries_in_series = 0
self._last_entry_price = 0.0
self._average_entry_price = 0.0
self._trailing_stop_level = None
def CreateClone(self):
return dealers_trade_v751_rivot_strategy()