在 GitHub 上查看

VR Overturn 策略

概述

  • 将 MetaTrader 的 “VR---Overturn” 专家顾问迁移到 StockSharp 的高级 API。
  • 任意时刻只保持一张仓位,前一笔平仓后立即评估下一次进场。
  • 适合希望自动反转并采用马丁或反马丁加仓方式的交易者。

交易流程

  1. 首次建仓:按照 FirstPositionDirection 参数指定的方向,用 BaseVolume 作为基础手数开出第一笔订单。
  2. 止损与止盈:根据 StopLossPipsTakeProfitPips 自动附加保护性订单。程序会读取品种的最小报价步长,将点值转换为绝对价格偏移(对于 3 位和 5 位小数的品种会自动乘以 10,与原始策略一致)。
  3. 平仓记录:当仓位被保护性订单平仓时,策略会保存:
    • 平仓方向(多或空);
    • 实际成交量;
    • 实现盈亏(开仓价与平仓价的差值)。
  4. 下一次进场:根据记录的结果决定下一笔交易的方向与手数。
    • 盈利后保持原方向,亏损后反向。
    • 马丁模式在亏损后将手数乘以 LotMultiplier,盈利后回到基础手数。
    • 反马丁模式在盈利后放大手数,亏损后回到基础手数。
  5. 手数校正:下单前将计算出的数量裁剪到交易所允许的最小手数步长。

参数

参数 说明 默认值
FirstPositionDirection 第一笔交易的方向(Buy/Sell)。 Buy
Mode 手数管理模式:Martingale(亏损后放大)或 AntiMartingale(盈利后放大)。 Martingale
BaseVolume 初始仓位手数,也是重置时的手数。 0.1
StopLossPips 止损距离(点)。 30
TakeProfitPips 止盈距离(点)。 90
LotMultiplier 扩仓时使用的乘数(马丁在亏损后使用,反马丁在盈利后使用)。 1.6

风险管理

  • 通过 StartProtection 为每次进场附加止损与止盈。
  • 止损与止盈距离会根据点值转换为绝对价格偏移。
  • 没有额外的追踪功能,风险完全由保护性订单与反向规则控制。

使用注意

  • 策略不依赖任何蜡烛或指标,仅在 OnOwnTradeReceived 中响应成交确认。
  • 如果保护单被部分成交,会累计剩余仓位,直到全部平仓后才允许下一次进场。
  • StockSharp 的成交对象不包含佣金和隔夜利息,因此盈亏判断仅基于价格差。如手续费较高,可适当增加止损距离或乘数。
  • 使用前请确认标的提供价格步长与手数步长信息,以保证正确的点值与手数计算。
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;
	}
}