VR Moving Distance 策略
该 StockSharp 策略复刻了 MetaTrader 5 的 VR-Moving 智能交易程序。策略监控一条可配置的均线,当价格偏离均线达到设定点差时触发交易。它可以通过放大追加订单的基础手数来加仓趋势,并在只持有一笔仓位时使用简单的止盈逻辑。
策略概览
- 通过单一的 K 线订阅处理指定品种的数据。
- 计算带有可调周期、平滑方式和价格来源的移动平均线。
- 使用合约的最小价格步长将点差型参数(距离、止盈)转换为价格单位。
- 当价格上穿均线并超过设定距离时加多单;当价格下穿均线并超过设定距离时加空单。
- 反向开仓前先平掉当前净头寸,使策略在净持仓模式下也能运行。
指标与数据
- 单一移动平均指标(支持
Simple、Exponential、Smoothed、Weighted、VolumeWeighted)。 - 所有计算和交易决策均基于配置的
Candle TypeK 线数据流。
入场逻辑
- 每根 K 线收盘后,等待移动平均线完全形成。
- 如果该柱最高价至少高于均线
DistancePips个点,则触发做多信号。 - 如果该柱最低价至少低于均线
DistancePips个点,则触发做空信号。 - 当方向反转时,在新的市价单中附加足够的手数以平掉原有持仓。
加仓与手数控制
- 首笔订单使用参数
BaseVolume指定的手数。 - 同方向的后续订单使用
BaseVolume * VolumeMultiplier手数。 - 策略记录多单的最高进场价和空单的最低进场价,只有当价格再次远离这些极值
DistancePips点时才会继续加仓。
离场逻辑
- 当且仅当存在一笔多单时,在进场价上方
TakeProfitPips点设置止盈,若后续 K 线最高价触及该价位则平仓。 - 空单同理,在进场价下方
TakeProfitPips点设置止盈,若后续最低价触及该价位则平仓。 - 如果已经加仓形成多笔订单,策略会继续持有仓位等待新的加仓信号,本移植版本不执行加权退出。
风险控制说明
- 启动时调用
StartProtection()以接入 StockSharp 标准的保护机制。 - 距离和止盈参数均以点(pip)为单位。对于三位或五位小数报价,策略会将价格步长乘以 10,以匹配 MetaTrader 的点值定义。
- 未设置自动止损,请通过参数或外部风控约束控制风险。
参数列表
- Candle Type – K 线数据类型。
- MA Length – 移动平均周期。
- MA Type – 移动平均的平滑方式。
- Price Source – 计算移动平均所使用的价格字段。
- Distance (pips) – 触发进场所需的最小点差距离。
- Take Profit (pips) – 单笔仓位下的止盈距离。
- Volume Multiplier – 加仓订单的手数倍率。
- Base Volume – 首次下单的基础手数。
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>
/// VR Moving strategy converted from MetaTrader 5 expert advisor.
/// Opens positions when price deviates from a moving average by a configurable distance and scales using a multiplier.
/// </summary>
public class VrMovingDistanceStrategy : Strategy
{
public enum MovingAverageTypes
{
Simple,
Exponential,
Smoothed,
Weighted,
VolumeWeighted
}
public enum CandlePrices
{
Open,
High,
Low,
Close,
Median,
Typical,
Weighted
}
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _maLength;
private readonly StrategyParam<MovingAverageTypes> _maType;
private readonly StrategyParam<CandlePrices> _priceSource;
private readonly StrategyParam<decimal> _distancePips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _volumeMultiplier;
private readonly StrategyParam<decimal> _baseVolume;
private DecimalLengthIndicator _movingAverage = null!;
private decimal _pipSize;
private int _longEntries;
private int _shortEntries;
private decimal _longHighestEntry;
private decimal _shortLowestEntry;
private decimal? _longEntryPrice;
private decimal? _shortEntryPrice;
/// <summary>
/// Candle type used for the moving average and decision logic.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Moving average length.
/// </summary>
public int MaLength
{
get => _maLength.Value;
set => _maLength.Value = value;
}
/// <summary>
/// Moving average smoothing type.
/// </summary>
public MovingAverageTypes MaType
{
get => _maType.Value;
set => _maType.Value = value;
}
/// <summary>
/// Candle price source for the moving average.
/// </summary>
public CandlePrices PriceSource
{
get => _priceSource.Value;
set => _priceSource.Value = value;
}
/// <summary>
/// Distance from the moving average in pips.
/// </summary>
public decimal DistancePips
{
get => _distancePips.Value;
set => _distancePips.Value = value;
}
/// <summary>
/// Take profit distance in pips when a single position is active.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Volume multiplier for additional entries in the same direction.
/// </summary>
public decimal VolumeMultiplier
{
get => _volumeMultiplier.Value;
set => _volumeMultiplier.Value = value;
}
/// <summary>
/// Base order volume.
/// </summary>
public decimal BaseVolume
{
get => _baseVolume.Value;
set => _baseVolume.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public VrMovingDistanceStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for calculations", "General");
_maLength = Param(nameof(MaLength), 60)
.SetGreaterThanZero()
.SetDisplay("MA Length", "Moving average period", "Moving Average")
.SetOptimize(10, 200, 10);
_maType = Param(nameof(MaType), MovingAverageTypes.Exponential)
.SetDisplay("MA Type", "Moving average smoothing method", "Moving Average");
_priceSource = Param(nameof(PriceSource), CandlePrices.Close)
.SetDisplay("Price Source", "Price used for the moving average", "Moving Average");
_distancePips = Param(nameof(DistancePips), 50m)
.SetGreaterThanZero()
.SetDisplay("Distance (pips)", "Offset from the moving average", "Trading")
.SetOptimize(10m, 150m, 10m);
_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Exit distance when only one position is open", "Trading")
.SetOptimize(10m, 150m, 10m);
_volumeMultiplier = Param(nameof(VolumeMultiplier), 1m)
.SetGreaterThanZero()
.SetDisplay("Volume Multiplier", "Multiplier for additional entries", "Trading")
.SetOptimize(1m, 3m, 0.25m);
_baseVolume = Param(nameof(BaseVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Base Volume", "Volume of the initial order", "Trading");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_longEntries = 0;
_shortEntries = 0;
_longHighestEntry = 0m;
_shortLowestEntry = 0m;
_longEntryPrice = null;
_shortEntryPrice = null;
_pipSize = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
UpdatePipSize();
_movingAverage = CreateMovingAverage(MaType, MaLength, PriceSource);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_movingAverage, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _movingAverage);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal maValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_movingAverage.IsFormed)
return;
var distance = DistancePips * _pipSize;
var takeProfit = TakeProfitPips * _pipSize;
var longTrigger = _longEntries == 0 ? maValue + distance : _longHighestEntry + distance;
var shortTrigger = _shortEntries == 0 ? maValue - distance : _shortLowestEntry - distance;
if (takeProfit > 0m)
{
// Close a single long position once the take profit level is reached.
if (_longEntries == 1 && Position > 0 && _longEntryPrice.HasValue)
{
var target = _longEntryPrice.Value + takeProfit;
if (candle.HighPrice >= target)
{
SellMarket(Position);
ResetLongState();
}
}
// Close a single short position once the take profit level is reached.
if (_shortEntries == 1 && Position < 0 && _shortEntryPrice.HasValue)
{
var target = _shortEntryPrice.Value - takeProfit;
if (candle.LowPrice <= target)
{
BuyMarket(Math.Abs(Position));
ResetShortState();
}
}
}
var baseVolume = BaseVolume;
if (baseVolume <= 0m)
return;
// Open or scale a long position when price moves sufficiently above the moving average.
if (candle.HighPrice >= longTrigger)
{
ExecuteLongEntry(longTrigger);
}
// Open or scale a short position when price moves sufficiently below the moving average.
else if (candle.LowPrice <= shortTrigger)
{
ExecuteShortEntry(shortTrigger);
}
}
private void ExecuteLongEntry(decimal triggerPrice)
{
var volume = _longEntries == 0 ? BaseVolume : BaseVolume * VolumeMultiplier;
if (volume <= 0m)
return;
var orderVolume = volume;
// Reverse short exposure before adding new long volume.
if (Position < 0)
{
orderVolume += Math.Abs(Position);
ResetShortState();
}
BuyMarket(orderVolume);
_longEntries++;
_longHighestEntry = _longEntries == 1 ? triggerPrice : Math.Max(_longHighestEntry, triggerPrice);
_longEntryPrice = _longEntries == 1 ? triggerPrice : null;
}
private void ExecuteShortEntry(decimal triggerPrice)
{
var volume = _shortEntries == 0 ? BaseVolume : BaseVolume * VolumeMultiplier;
if (volume <= 0m)
return;
var orderVolume = volume;
// Reverse long exposure before adding new short volume.
if (Position > 0)
{
orderVolume += Position;
ResetLongState();
}
SellMarket(orderVolume);
_shortEntries++;
_shortLowestEntry = _shortEntries == 1 ? triggerPrice : Math.Min(_shortLowestEntry, triggerPrice);
_shortEntryPrice = _shortEntries == 1 ? triggerPrice : null;
}
private void ResetLongState()
{
_longEntries = 0;
_longHighestEntry = 0m;
_longEntryPrice = null;
}
private void ResetShortState()
{
_shortEntries = 0;
_shortLowestEntry = 0m;
_shortEntryPrice = null;
}
private void UpdatePipSize()
{
var step = Security?.PriceStep ?? 1m;
if (step <= 0m)
step = 1m;
var digits = GetDecimalDigits(step);
_pipSize = (digits == 3 || digits == 5) ? step * 10m : step;
}
private static int GetDecimalDigits(decimal value)
{
value = Math.Abs(value);
var digits = 0;
while (value != Math.Truncate(value) && digits < 10)
{
value *= 10m;
digits++;
}
return digits;
}
private static DecimalLengthIndicator CreateMovingAverage(MovingAverageTypes type, int length, CandlePrices priceSource)
{
DecimalLengthIndicator indicator = type switch
{
MovingAverageTypes.Simple => new SimpleMovingAverage(),
MovingAverageTypes.Exponential => new ExponentialMovingAverage(),
MovingAverageTypes.Smoothed => new SmoothedMovingAverage(),
MovingAverageTypes.Weighted => new WeightedMovingAverage(),
_ => new SimpleMovingAverage(),
};
indicator.Length = length;
return indicator;
}
}
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
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class vr_moving_distance_strategy(Strategy):
"""Opens positions when price deviates from MA by distance; scales with multiplier."""
def __init__(self):
super(vr_moving_distance_strategy, self).__init__()
self._ma_length = self.Param("MaLength", 60).SetGreaterThanZero().SetDisplay("MA Length", "Moving average period", "Moving Average")
self._distance = self.Param("DistancePips", 50).SetGreaterThanZero().SetDisplay("Distance", "Offset from MA in pips", "Trading")
self._tp = self.Param("TakeProfitPips", 50).SetNotNegative().SetDisplay("Take Profit", "TP in pips", "Trading")
self._vol_mult = self.Param("VolumeMultiplier", 1).SetGreaterThanZero().SetDisplay("Volume Multiplier", "Multiplier for additional entries", "Trading")
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_moving_distance_strategy, self).OnReseted()
self._long_entries = 0
self._short_entries = 0
self._long_highest = 0
self._short_lowest = 0
self._long_entry_price = None
self._short_entry_price = None
def OnStarted2(self, time):
super(vr_moving_distance_strategy, self).OnStarted2(time)
self._long_entries = 0
self._short_entries = 0
self._long_highest = 0
self._short_lowest = 0
self._long_entry_price = None
self._short_entry_price = None
ma = ExponentialMovingAverage()
ma.Length = self._ma_length.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(ma, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawIndicator(area, ma)
self.DrawOwnTrades(area)
def OnProcess(self, candle, ma_val):
if candle.State != CandleStates.Finished:
return
mv = float(ma_val)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
dist = self._distance.Value
tp = self._tp.Value
long_trigger = mv + dist if self._long_entries == 0 else self._long_highest + dist
short_trigger = mv - dist if self._short_entries == 0 else self._short_lowest - dist
# TP for single long
if tp > 0 and self._long_entries == 1 and self.Position > 0 and self._long_entry_price is not None:
target = self._long_entry_price + tp
if high >= target:
self.SellMarket()
self._long_entries = 0
self._long_highest = 0
self._long_entry_price = None
# TP for single short
if tp > 0 and self._short_entries == 1 and self.Position < 0 and self._short_entry_price is not None:
target = self._short_entry_price - tp
if low <= target:
self.BuyMarket()
self._short_entries = 0
self._short_lowest = 0
self._short_entry_price = None
# Long entry
if high >= long_trigger:
if self.Position < 0:
self.BuyMarket()
self._short_entries = 0
self._short_lowest = 0
self._short_entry_price = None
self.BuyMarket()
self._long_entries += 1
self._long_highest = long_trigger if self._long_entries == 1 else max(self._long_highest, long_trigger)
self._long_entry_price = long_trigger if self._long_entries == 1 else None
elif low <= short_trigger:
if self.Position > 0:
self.SellMarket()
self._long_entries = 0
self._long_highest = 0
self._long_entry_price = None
self.SellMarket()
self._short_entries += 1
self._short_lowest = short_trigger if self._short_entries == 1 else min(self._short_lowest, short_trigger)
self._short_entry_price = short_trigger if self._short_entries == 1 else None
def CreateClone(self):
return vr_moving_distance_strategy()