多品种套保调度策略
概述
Multi Hedging Scheduler Strategy 是将 MetaTrader 5 指标 MultiHedg_1.mq5 迁移到 StockSharp 的版本。策略面向支持对冲的账户,可同时管理最多十个交易品种。在可配置的交易时间窗口内按统一方向开仓,并可根据时间或权益百分比阈值一次性平掉所有仓位。
策略不依赖技术指标,而是订阅(默认)一分钟蜡烛,仅把蜡烛的收盘时间当作调度触发器。每根完成的蜡烛会触发:
- 是否需要在交易窗口内开仓;
- 是否达到权益止盈/止损目标;
- 是否进入强制平仓时间窗口。
运行逻辑
- 品种选择:通过
UseSymbolX参数启用最多十个交易品种。对每个启用的符号,策略通过SecurityProvider查找标的并订阅所选CandleType的蜡烛数据。 - 交易窗口:当蜡烛时间进入 [
TradeStartTime,TradeStartTime + TradeDuration) 区间时,若当前方向的仓位尚未建立,策略按TradeDirection方向发送市价单。若账户中存在反向仓位,将追加足够的数量以翻转方向。 - 权益保护:启用
CloseByEquityPercent后,策略会将实时权益与启动时的基准余额比较。当收益超过PercentProfit或回撤超过PercentLoss时,所有受管仓位都会被平掉。 - 时间平仓:启用
UseTimeClose后,当时间进入 [CloseTime,CloseTime + TradeDuration) 区间时,策略会强制平掉全部仓位。 - 日志记录:所有关键事件(开仓、权益保护触发、时间平仓)都会通过
LogInfo记录,便于回溯。
参数
| 参数 | 说明 | 默认值 |
|---|---|---|
TradeDirection |
全部订单的方向(Buy 或 Sell)。 |
Buy |
TradeStartTime |
交易窗口开始时间。 | 19:51 |
TradeDuration |
交易窗口与平仓窗口的持续时间。 | 00:05:00 |
UseTimeClose |
是否启用时间平仓窗口。 | true |
CloseTime |
平仓窗口开始时间。 | 20:50 |
CloseByEquityPercent |
是否按权益百分比执行全局平仓。 | true |
PercentProfit |
达到该收益百分比时平掉全部仓位。 | 1.0 |
PercentLoss |
达到该回撤百分比时平掉全部仓位。 | 55.0 |
CandleType |
作为调度驱动的蜡烛类型。 | 1 分钟 |
UseSymbol0..9 |
是否启用对应品种。 | 0–5 启用,6–9 关闭 |
Symbol0..9 |
每个槽位的标的代码(SecurityProvider.LookupById 使用的 ID)。 |
见下表 |
Volume0..9 |
每个槽位的下单数量。 | 0.1–1.0 |
默认品种配置
| 槽位 | 启用 | 品种 | 数量 |
|---|---|---|---|
| 0 | ✔ | EURUSD | 0.1 |
| 1 | ✔ | GBPUSD | 0.2 |
| 2 | ✔ | GBPJPY | 0.3 |
| 3 | ✔ | EURCAD | 0.4 |
| 4 | ✔ | USDCHF | 0.5 |
| 5 | ✔ | USDJPY | 0.6 |
| 6 | ✖ | USDCHF | 0.7 |
| 7 | ✖ | GBPUSD | 0.8 |
| 8 | ✖ | EURUSD | 0.9 |
| 9 | ✖ | USDJPY | 1.0 |
使用建议
- 如果需要复制 MT5 的对冲行为,请确认账户允许多方向同时持仓。在净额账户中,策略会自动补量以翻转到目标方向。
SymbolX参数必须与数据源中的标的 ID 完全一致(例如EURUSD@FXCM)。- 蜡烛数据仅作为时钟驱动,如需不同的更新频率可修改
CandleType。 - 每次启动策略都会重新记录起始余额,后续的权益阈值比较基于该数值。
- 策略不包含逐单止盈止损,全部退出逻辑由时间窗口和权益百分比共同控制。
转换说明
- 原 MT5 版本在
OnTick中运行;StockSharp 版本改为基于完成的蜡烛触发,从而使用更高层级的事件绑定 API。 - 不再需要
magic number过滤;CloseAllManagedPositions只会遍历在参数中启用的品种。 - 原策略的声音提示和图表注释未迁移,取而代之的是详细的
LogInfo日志。
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>
/// Hedging scheduler strategy that opens positions during a configurable time window
/// and closes when equity targets are reached or a separate exit window arrives.
/// Simplified to single-security from the original multi-symbol version.
/// </summary>
public class MultiHedgingSchedulerStrategy : Strategy
{
private readonly StrategyParam<Sides> _tradeDirection;
private readonly StrategyParam<TimeSpan> _tradeStartTime;
private readonly StrategyParam<TimeSpan> _tradeDuration;
private readonly StrategyParam<bool> _enableTimeClose;
private readonly StrategyParam<TimeSpan> _closeTime;
private readonly StrategyParam<bool> _enableEquityClose;
private readonly StrategyParam<decimal> _profitPercent;
private readonly StrategyParam<decimal> _lossPercent;
private readonly StrategyParam<DataType> _candleType;
private decimal _initialBalance;
private bool _positionOpened;
/// <summary>
/// Trading direction used when opening positions.
/// </summary>
public Sides TradeDirection
{
get => _tradeDirection.Value;
set => _tradeDirection.Value = value;
}
/// <summary>
/// Time of day when the trading window starts.
/// </summary>
public TimeSpan TradeStartTime
{
get => _tradeStartTime.Value;
set => _tradeStartTime.Value = value;
}
/// <summary>
/// Duration of the trading and optional closing windows.
/// </summary>
public TimeSpan TradeDuration
{
get => _tradeDuration.Value;
set => _tradeDuration.Value = value;
}
/// <summary>
/// Enables the separate time based close window.
/// </summary>
public bool UseTimeClose
{
get => _enableTimeClose.Value;
set => _enableTimeClose.Value = value;
}
/// <summary>
/// Time of day when the closing window starts.
/// </summary>
public TimeSpan CloseTime
{
get => _closeTime.Value;
set => _closeTime.Value = value;
}
/// <summary>
/// Enables closing when equity reaches profit or loss thresholds.
/// </summary>
public bool CloseByEquityPercent
{
get => _enableEquityClose.Value;
set => _enableEquityClose.Value = value;
}
/// <summary>
/// Percentage profit target based on starting balance.
/// </summary>
public decimal PercentProfit
{
get => _profitPercent.Value;
set => _profitPercent.Value = value;
}
/// <summary>
/// Percentage loss threshold based on starting balance.
/// </summary>
public decimal PercentLoss
{
get => _lossPercent.Value;
set => _lossPercent.Value = value;
}
/// <summary>
/// Candle series driving the scheduling logic.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes the strategy.
/// </summary>
public MultiHedgingSchedulerStrategy()
{
_tradeDirection = Param(nameof(TradeDirection), Sides.Buy)
.SetDisplay("Trade Direction", "Direction used for opening positions", "General");
_tradeStartTime = Param(nameof(TradeStartTime), new TimeSpan(10, 0, 0))
.SetDisplay("Trade Start", "Time of day to begin opening positions", "Scheduling");
_tradeDuration = Param(nameof(TradeDuration), TimeSpan.FromMinutes(5))
.SetDisplay("Window Length", "Duration of trading and closing windows", "Scheduling");
_enableTimeClose = Param(nameof(UseTimeClose), true)
.SetDisplay("Use Close Window", "Enable time based portfolio closing", "Scheduling");
_closeTime = Param(nameof(CloseTime), new TimeSpan(17, 0, 0))
.SetDisplay("Close Start", "Time of day to start the close window", "Scheduling");
_enableEquityClose = Param(nameof(CloseByEquityPercent), true)
.SetDisplay("Use Equity Targets", "Enable equity based exit", "Risk Management");
_profitPercent = Param(nameof(PercentProfit), 1m)
.SetDisplay("Profit %", "Equity percentage gain to close all positions", "Risk Management");
_lossPercent = Param(nameof(PercentLoss), 55m)
.SetDisplay("Loss %", "Equity percentage loss to close all positions", "Risk Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Candle series driving the scheduler", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_initialBalance = 0m;
_positionOpened = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_initialBalance = Portfolio?.CurrentValue ?? 0m;
SubscribeCandles(CandleType)
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormed)
return;
var timeOfDay = candle.OpenTime.TimeOfDay;
if (CloseByEquityPercent && TryHandleEquityTargets())
return;
if (UseTimeClose && IsWithinWindow(timeOfDay, CloseTime, TradeDuration))
{
if (Position > 0)
SellMarket(Math.Abs(Position));
else if (Position < 0)
BuyMarket(Math.Abs(Position));
_positionOpened = false;
return;
}
var direction = TradeDirection;
if (!IsWithinWindow(timeOfDay, TradeStartTime, TradeDuration))
return;
if (_positionOpened)
return;
var volume = Volume;
if (volume <= 0m)
volume = 1m;
if (direction == Sides.Buy && Position <= 0)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
BuyMarket(volume);
_positionOpened = true;
}
else if (direction == Sides.Sell && Position >= 0)
{
if (Position > 0)
SellMarket(Position);
SellMarket(volume);
_positionOpened = true;
}
}
private bool TryHandleEquityTargets()
{
if (_initialBalance <= 0m)
return false;
var equity = Portfolio?.CurrentValue;
if (equity == null)
return false;
var profitLevel = _initialBalance * (1m + PercentProfit / 100m);
var lossLevel = _initialBalance * (1m - PercentLoss / 100m);
if (equity.Value >= profitLevel || equity.Value <= lossLevel)
{
if (Position > 0)
SellMarket(Math.Abs(Position));
else if (Position < 0)
BuyMarket(Math.Abs(Position));
_positionOpened = false;
return true;
}
return false;
}
private static bool IsWithinWindow(TimeSpan current, TimeSpan start, TimeSpan length)
{
if (length <= TimeSpan.Zero)
return current == start;
var end = start + length;
if (end < TimeSpan.FromDays(1))
return current >= start && current < end;
var overflow = end - TimeSpan.FromDays(1);
return current >= start || current < overflow;
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
SIDE_BUY = 0
SIDE_SELL = 1
class multi_hedging_scheduler_strategy(Strategy):
def __init__(self):
super(multi_hedging_scheduler_strategy, self).__init__()
self._trade_direction = self.Param("TradeDirection", SIDE_BUY)
self._trade_start_hour = self.Param("TradeStartHour", 10)
self._trade_start_minute = self.Param("TradeStartMinute", 0)
self._trade_duration_minutes = self.Param("TradeDurationMinutes", 5)
self._enable_time_close = self.Param("UseTimeClose", True)
self._close_hour = self.Param("CloseHour", 17)
self._close_minute = self.Param("CloseMinute", 0)
self._enable_equity_close = self.Param("CloseByEquityPercent", True)
self._profit_percent = self.Param("PercentProfit", 1.0)
self._loss_percent = self.Param("PercentLoss", 55.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1)))
self._initial_balance = 0.0
self._position_opened = False
@property
def TradeDirection(self):
return self._trade_direction.Value
@TradeDirection.setter
def TradeDirection(self, value):
self._trade_direction.Value = value
@property
def TradeStartHour(self):
return self._trade_start_hour.Value
@TradeStartHour.setter
def TradeStartHour(self, value):
self._trade_start_hour.Value = value
@property
def TradeStartMinute(self):
return self._trade_start_minute.Value
@TradeStartMinute.setter
def TradeStartMinute(self, value):
self._trade_start_minute.Value = value
@property
def TradeDurationMinutes(self):
return self._trade_duration_minutes.Value
@TradeDurationMinutes.setter
def TradeDurationMinutes(self, value):
self._trade_duration_minutes.Value = value
@property
def UseTimeClose(self):
return self._enable_time_close.Value
@UseTimeClose.setter
def UseTimeClose(self, value):
self._enable_time_close.Value = value
@property
def CloseHour(self):
return self._close_hour.Value
@CloseHour.setter
def CloseHour(self, value):
self._close_hour.Value = value
@property
def CloseMinute(self):
return self._close_minute.Value
@CloseMinute.setter
def CloseMinute(self, value):
self._close_minute.Value = value
@property
def CloseByEquityPercent(self):
return self._enable_equity_close.Value
@CloseByEquityPercent.setter
def CloseByEquityPercent(self, value):
self._enable_equity_close.Value = value
@property
def PercentProfit(self):
return self._profit_percent.Value
@PercentProfit.setter
def PercentProfit(self, value):
self._profit_percent.Value = value
@property
def PercentLoss(self):
return self._loss_percent.Value
@PercentLoss.setter
def PercentLoss(self, value):
self._loss_percent.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def _is_within_window(self, current_minutes, start_hour, start_minute, duration_minutes):
start_total = start_hour * 60 + start_minute
end_total = start_total + duration_minutes
if end_total < 1440:
return current_minutes >= start_total and current_minutes < end_total
else:
overflow = end_total - 1440
return current_minutes >= start_total or current_minutes < overflow
def OnStarted2(self, time):
super(multi_hedging_scheduler_strategy, self).OnStarted2(time)
self._initial_balance = 0.0
self._position_opened = False
self.SubscribeCandles(self.CandleType).Bind(self.ProcessCandle).Start()
self.StartProtection(
Unit(2000.0, UnitTypes.Absolute),
Unit(1000.0, UnitTypes.Absolute))
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._initial_balance == 0.0 and self.Portfolio is not None and self.Portfolio.CurrentValue is not None:
self._initial_balance = float(self.Portfolio.CurrentValue)
open_time = candle.OpenTime
hour = open_time.Hour
minute = open_time.Minute
current_minutes = hour * 60 + minute
duration = int(self.TradeDurationMinutes)
if self.CloseByEquityPercent and self._try_handle_equity_targets():
return
if self.UseTimeClose and self._is_within_window(current_minutes, int(self.CloseHour), int(self.CloseMinute), duration):
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
self._position_opened = False
return
if not self._is_within_window(current_minutes, int(self.TradeStartHour), int(self.TradeStartMinute), duration):
return
if self._position_opened:
return
direction = int(self.TradeDirection)
if direction == SIDE_BUY and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._position_opened = True
elif direction == SIDE_SELL and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._position_opened = True
def _try_handle_equity_targets(self):
if self._initial_balance <= 0.0:
return False
if self.Portfolio is None or self.Portfolio.CurrentValue is None:
return False
equity = float(self.Portfolio.CurrentValue)
profit_level = self._initial_balance * (1.0 + float(self.PercentProfit) / 100.0)
loss_level = self._initial_balance * (1.0 - float(self.PercentLoss) / 100.0)
if equity >= profit_level or equity <= loss_level:
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
self._position_opened = False
return True
return False
def OnReseted(self):
super(multi_hedging_scheduler_strategy, self).OnReseted()
self._initial_balance = 0.0
self._position_opened = False
def CreateClone(self):
return multi_hedging_scheduler_strategy()