在 GitHub 上查看
多重套利策略
概述
多重套利策略 是对 MetaTrader "Multi_arbitration 1.000" 专家顾问的 StockSharp 版本移植。原始脚本持续评估已经持有的多头和空头仓位,在浮动收益较弱的一侧加仓,并在达到总体盈利目标后一次性平掉全部仓位。本 C# 实现保留了核心决策逻辑,同时针对 StockSharp 的净额持仓模型和高级策略 API 做了适配。
策略的主要行为如下:
- 在收到第一根收盘完成的 K 线后立即建立初始多头头寸。
- 比较当前持仓方向与相反方向的未实现盈亏,以决定是否需要反向开仓。
- 当达到设定的盈利目标或持仓压力超过限定阈值时,立即清空仓位。
- 全程仅使用市价单(
BuyMarket / SellMarket),以追求执行速度与实现简洁。
交易逻辑
- 初始下单 – 第一根收盘完成的 K 线会触发按设定交易量买入的市价单,复现原始 EA 启动时立即建仓的行为。
- 收益比较 – 每根已完成的 K 线都会计算当前方向的浮动盈亏:
- 多头盈亏 =
(收盘价 - 入场价) * 交易量
- 空头盈亏 =
(入场价 - 收盘价) * 交易量
- 方向选择 – 如果相反方向的表现优于当前持仓,则策略发送足够大的市价单,先覆盖现有持仓,再在新的方向上开出净头寸;当没有任何持仓时,默认开多,与原始 EA 的逻辑保持一致。
- 仓位限制保护 – 可配置的
MaxOpenPositions 参数对应 MetaTrader 中对 LimitOrders() 的检查。当多空合计仓位达到该限制且策略处于盈利状态时,会立即清空持仓以避免过度杠杆。
- 盈利目标平仓 – 当账户盈亏(包含已实现与未实现)超过
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 支持同时持有多头与空头(对冲模式),而本移植在净额模式下运行,每次仅保留一个净方向,但仍会比较两个方向的浮动收益。
- 终端交易权限、撮合模式、魔术号等平台相关设置由 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;
}
}
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.Strategies import Strategy
class multi_arbitration_strategy(Strategy):
"""Multi-direction arbitration strategy with profit-based flattening."""
def __init__(self):
super(multi_arbitration_strategy, self).__init__()
self._profit_for_close = self.Param("ProfitForClose", 300.0) \
.SetDisplay("Profit Threshold", "Profit required before flattening all positions.", "Risk")
self._max_open_positions = self.Param("MaxOpenPositions", 15) \
.SetGreaterThanZero() \
.SetDisplay("Max Open Positions", "Maximum simultaneous positions allowed before closing everything.", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candle type used to synchronize trading decisions.", "Data")
self._initial_order_placed = False
self._entry_price = 0.0
self._current_side = 0 # 0=none, 1=buy, -1=sell
@property
def ProfitForClose(self):
return self._profit_for_close.Value
@property
def MaxOpenPositions(self):
return self._max_open_positions.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(multi_arbitration_strategy, self).OnStarted2(time)
self._initial_order_placed = False
self._entry_price = 0.0
self._current_side = 0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
vol = float(self.Volume) if self.Volume > 0 else 1.0
if not self._initial_order_placed:
self._open_long(close)
self._initial_order_placed = True
long_count = 1 if self._current_side == 1 else 0
short_count = 1 if self._current_side == -1 else 0
long_profit = (close - self._entry_price) * vol if self._current_side == 1 else 0.0
short_profit = (self._entry_price - close) * vol if self._current_side == -1 else 0.0
if long_count + short_count < self.MaxOpenPositions:
if long_profit < short_profit and self._current_side != 1:
self._open_long(close)
elif short_profit < long_profit and self._current_side != -1:
self._open_short(close)
elif long_profit == 0.0 and short_profit == 0.0 and self.Position == 0 and self._current_side == 0:
self._open_long(close)
elif float(self.PnL) > 0.0 and self.Position != 0:
self._flatten(close)
if float(self.PnL) > float(self.ProfitForClose) and self.Position != 0:
self._flatten(close)
def _open_long(self, close):
if self.Position > 0:
self._entry_price = close
self._current_side = 1
return
self.BuyMarket()
self._entry_price = close
self._current_side = 1
def _open_short(self, close):
if self.Position < 0:
self._entry_price = close
self._current_side = -1
return
self.SellMarket()
self._entry_price = close
self._current_side = -1
def _flatten(self, close):
if self._current_side == 0:
return
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
self._current_side = 0
self._entry_price = 0.0
def OnReseted(self):
super(multi_arbitration_strategy, self).OnReseted()
self._initial_order_placed = False
self._entry_price = 0.0
self._current_side = 0
def CreateClone(self):
return multi_arbitration_strategy()