在 GitHub 上查看

多重套利策略

概述

多重套利策略 是对 MetaTrader "Multi_arbitration 1.000" 专家顾问的 StockSharp 版本移植。原始脚本持续评估已经持有的多头和空头仓位,在浮动收益较弱的一侧加仓,并在达到总体盈利目标后一次性平掉全部仓位。本 C# 实现保留了核心决策逻辑,同时针对 StockSharp 的净额持仓模型和高级策略 API 做了适配。

策略的主要行为如下:

  • 在收到第一根收盘完成的 K 线后立即建立初始多头头寸。
  • 比较当前持仓方向与相反方向的未实现盈亏,以决定是否需要反向开仓。
  • 当达到设定的盈利目标或持仓压力超过限定阈值时,立即清空仓位。
  • 全程仅使用市价单(BuyMarket / SellMarket),以追求执行速度与实现简洁。

交易逻辑

  1. 初始下单 – 第一根收盘完成的 K 线会触发按设定交易量买入的市价单,复现原始 EA 启动时立即建仓的行为。
  2. 收益比较 – 每根已完成的 K 线都会计算当前方向的浮动盈亏:
    • 多头盈亏 = (收盘价 - 入场价) * 交易量
    • 空头盈亏 = (入场价 - 收盘价) * 交易量
  3. 方向选择 – 如果相反方向的表现优于当前持仓,则策略发送足够大的市价单,先覆盖现有持仓,再在新的方向上开出净头寸;当没有任何持仓时,默认开多,与原始 EA 的逻辑保持一致。
  4. 仓位限制保护 – 可配置的 MaxOpenPositions 参数对应 MetaTrader 中对 LimitOrders() 的检查。当多空合计仓位达到该限制且策略处于盈利状态时,会立即清空持仓以避免过度杠杆。
  5. 盈利目标平仓 – 当账户盈亏(包含已实现与未实现)超过 ProfitForClose 阈值后,策略立即平仓,模拟原始脚本中的 Equity - Balance 判断。

参数

名称 说明 默认值
TradeVolume 每次市价单使用的交易量,对应原始 EA 中的最小手数。 1
ProfitForClose 超过该盈亏阈值后将全部平仓。 300
MaxOpenPositions 允许的最大同时持仓数量,达到后会强制清仓,相当于 limit - 15 15
CandleType 用于驱动策略决策的 K 线类型,默认是 1 分钟。 1 分钟 K 线

实现说明

  • StockSharp 采用净额持仓模型,同一时刻只能持有单一净方向。策略在需要反向时,通过放大市价单数量同时完成平仓与开仓。
  • 调用 StartProtection() 以继承框架的自动风险控制能力(例如在策略停止时自动平掉残余仓位)。
  • 关键状态变量(_entryPrice_currentSide_initialOrderPlaced)在 OnReseted 中全部重置,方便多次回测或重启。
  • 策略仅在 收盘完成的 K 线 上运作,避免在未完成的蜡烛上重复计算盈亏。

使用建议

  • 根据标的合约大小或最小交易单位调整 TradeVolume 参数。
  • ProfitForClose 应与账户盈亏所使用的货币单位保持一致(例如外汇账户通常以 USD 表示)。
  • 根据可接受的杠杆水平调节 MaxOpenPositions,值越小越保守。
  • 策略启动后会立即买入一笔多单,建议在允许多头入场的市场条件下启动。

与 MetaTrader 版本的差异

  • MetaTrader 支持同时持有多头与空头(对冲模式),而本移植在净额模式下运行,每次仅保留一个净方向,但仍会比较两个方向的浮动收益。
  • 终端交易权限、撮合模式、魔术号等平台相关设置由 StockSharp 的 StartProtection()、K 线订阅等机制替代。
  • 原 MQL 文件中的文本提示与终端 Comment() 输出未在此版本中复现,如需运行日志可使用 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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Multi-direction arbitration strategy adapted from MetaTrader logic.
/// </summary>
public class MultiArbitrationStrategy : Strategy
{
	private readonly StrategyParam<decimal> _profitForClose;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _maxOpenPositions;
	private readonly StrategyParam<DataType> _candleType;

	private bool _initialOrderPlaced;
	private decimal _entryPrice;
	private Sides? _currentSide;

	/// <summary>
	/// Target profit that triggers a full position exit.
	/// </summary>
	public decimal ProfitForClose
	{
		get => _profitForClose.Value;
		set => _profitForClose.Value = value;
	}

	/// <summary>
	/// Volume used when sending market orders.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Maximum simultaneous positions allowed before forcing a flatten.
	/// </summary>
	public int MaxOpenPositions
	{
		get => _maxOpenPositions.Value;
		set => _maxOpenPositions.Value = value;
	}

	/// <summary>
	/// Candle type used for synchronization and decision making.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="MultiArbitrationStrategy"/> class.
	/// </summary>
	public MultiArbitrationStrategy()
	{
		_profitForClose = Param(nameof(ProfitForClose), 300m)
			.SetDisplay("Profit Threshold", "Profit required before flattening all positions.", "Risk");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Volume used when opening new positions.", "Trading");

		_maxOpenPositions = Param(nameof(MaxOpenPositions), 15)
			.SetGreaterThanZero()
			.SetDisplay("Max Open Positions", "Maximum simultaneous positions allowed before closing everything.", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle type used to synchronize trading decisions.", "Data");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_initialOrderPlaced = false;
		_entryPrice = 0m;
		_currentSide = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_initialOrderPlaced)
		{
			OpenLong(candle);
			_initialOrderPlaced = true;
		}

		var longCount = _currentSide == Sides.Buy ? 1 : 0;
		var shortCount = _currentSide == Sides.Sell ? 1 : 0;

		var longProfit = _currentSide == Sides.Buy ? (candle.ClosePrice - _entryPrice) * Volume : 0m;
		var shortProfit = _currentSide == Sides.Sell ? (_entryPrice - candle.ClosePrice) * Volume : 0m;

		if (longCount + shortCount < MaxOpenPositions)
		{
			if (longProfit < shortProfit && _currentSide != Sides.Buy)
			{
				OpenLong(candle);
			}
			else if (shortProfit < longProfit && _currentSide != Sides.Sell)
			{
				OpenShort(candle);
			}
			else if (longProfit == 0m && shortProfit == 0m && Position == 0 && _currentSide is null)
			{
				OpenLong(candle);
			}
		}
		else if (PnL > 0m && Position != 0)
		{
			FlattenPosition(candle);
		}

		if (PnL > ProfitForClose && Position != 0)
		{
			FlattenPosition(candle);
		}
	}

	private void OpenLong(ICandleMessage candle)
	{
		if (Position > 0)
		{
			// Already holding a long position, so only refresh the entry reference.
			_entryPrice = candle.ClosePrice;
			_currentSide = Sides.Buy;
			return;
		}

		BuyMarket();
		_entryPrice = candle.ClosePrice;
		_currentSide = Sides.Buy;
	}

	private void OpenShort(ICandleMessage candle)
	{
		if (Position < 0)
		{
			// Already holding a short position, so only refresh the entry reference.
			_entryPrice = candle.ClosePrice;
			_currentSide = Sides.Sell;
			return;
		}

		SellMarket();
		_entryPrice = candle.ClosePrice;
		_currentSide = Sides.Sell;
	}

	private void FlattenPosition(ICandleMessage candle)
	{
		if (_currentSide is null)
			return;

		if (Position > 0)
		{
			SellMarket();
		}
		else if (Position < 0)
		{
			BuyMarket();
		}

		_currentSide = null;
		_entryPrice = 0m;
	}
}