Dealers Trade v7.51 RIVOT(C#)
摘要
Dealers Trade v7.51 最初是 MetaTrader 4 平台上的 Dealers_Trade_v_7.51_RIVOT.mq4 专家顾问,属于典型的基于枢轴的网格马丁策略。本移植版本在 StockSharp 平台上重建了核心思想:利用经典枢轴价位与浮动枢轴价位之间的差异判定方向,只要价格向不利方向回撤到设定的点差,就逐步加仓并放大手数,同时通过止损、止盈与跟踪止损控制风险。
交易逻辑
枢轴体系
- 每根完成的 K 线都会计算两条参考线:
- 经典枢轴
P=(上一根最高价 + 上一根最低价 + 上一根收盘价 + 当前开盘价) / 4; - 浮动枢轴
FLP=(当前最高价 + 当前最低价 + 当前收盘价) / 3。
- 经典枢轴
- 当
P与FLP之间的点差超过GapThreshold时,策略才允许在本根 K 线进行交易。
- 每根完成的 K 线都会计算两条参考线:
方向判断
- 收盘价同时高于两条枢轴且满足点差过滤时,建立 多头 偏向;
- 收盘价同时低于两条枢轴且满足点差过滤时,建立 空头 偏向;
- 在当前加仓序列关闭之前,偏向保持不变。
加仓方式
- 同一时间仅允许存在一个网格序列;
- 第一笔订单在偏向确认后立即执行;
- 只有当价格相对上一次成交向不利方向回撤至少
PipDistance个点时,才会触发下一笔加仓; - 新订单手数按
VolumeMultiplier倍数递增,但不会超过MaxVolume; - 序列中的订单数量受
MaxTrades限制。
风险控制
- 以加权平均开仓价为基准,
StopLoss点的浮动止损触发全仓平仓; - 价格向有利方向运行
TakeProfit点时,触发统一止盈; - 如果
TrailingStop大于零,策略会在盈利扩大的同时移动跟踪止损,锁定部分收益。
- 以加权平均开仓价为基准,
重置条件
- 当仓位被止损、止盈、跟踪止损或手动平仓后,加仓计数与方向状态都会被重置。
参数
| 参数 | 默认值 | 说明 |
|---|---|---|
Volume |
1 | 首笔订单的基础手数。 |
MaxTrades |
5 | 同一序列内最多允许的订单数量。 |
PipDistance |
4 | 触发下一笔加仓所需的最小不利点数。 |
TakeProfit |
15 | 相对平均开仓价的整体止盈距离。 |
StopLoss |
90 | 相对平均开仓价的整体止损距离。 |
TrailingStop |
15 | 跟踪止损与价格之间保持的点差,设为 0 表示关闭。 |
VolumeMultiplier |
1.5 | 每次加仓后手数放大的倍数。 |
MaxVolume |
5 | 单笔订单手数的上限。 |
GapThreshold |
7 | 启动交易所需的最小枢轴点差。 |
CandleType |
15 分钟时间框架 | 计算所用的 K 线类型。 |
所有参数均通过 StrategyParam<T> 声明,可在 StockSharp Designer 或回测环境中直接优化。
使用提示
- 策略完全依赖 K 线数据,无需逐笔报价;请确保数据源能够提供所选时间框架的蜡烛序列。
- StockSharp 默认采用净头寸模型,代码会维护内部的加权平均价来模拟 MT4 的多笔订单效果。
- 如果图表区域可用,程序会绘制
Pivot与FloatingPivot两条参考线以便观察。 - 策略不会在持仓过程中反向开仓,只有在当前序列结束后才会评估新的方向。
与 MQL 版本的差异
- 原始 EA 会在 MT4 图表上绘制标签与文字提示,移植版本仅保留交易逻辑,并以图表线条代替视觉提示。
- 与账户余额、魔术号、品种点值相关的保护逻辑在 StockSharp 中不再需要,因此被删除。
- MT4 中基于
Ask == tp的精确出场在此实现中转换为对 K 线价格的比较。 - 下单与平仓统一使用
BuyMarket/SellMarket,并在 K 线更新时执行风控,不再遍历 MT4 的订单列表。
最佳实践
- 在真实交易前务必进行历史回测或模拟盘测试,并考虑点差和手续费的影响。
- 对于波动性较大的品种,可适当降低
VolumeMultiplier或MaxTrades以控制回撤。 - 可根据需要调整
CandleType(例如 M15、H1 等)以契合原始策略的使用场景。
文件
CS/DealersTradeV751RivotStrategy.cs—— C# 策略实现。README.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>
/// 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()