多品种利润平仓策略
概述
多品种利润平仓策略 重现了原始的 MetaTrader 脚本:持续监控一组货币对,在组合浮动利润达到目标或总浮亏超过容忍范围时立即平掉所有仓位。该转换基于 StockSharp 的高级 API,实现了利润跟踪、持仓最小时间控制以及跨多个标的的集中平仓。
策略流程
- 从逗号分隔的
WatchedSymbols参数解析需要监控的标的,若留空则退回到主Security。 - 为每个标的订阅所选的 K 线类型(默认 1 分钟)。每根收盘的 K 线都会触发一次组合利润检查。
- 对于每个标的,策略保存:
Positions[i].PnL提供的当前浮动盈亏。- 首次持仓出现的时间,用于满足
MinAgeSeconds的最小持仓时长。
- 累计全部标的的净利润后执行判断:
- 净利润达到
ProfitTarget时,调用BuyMarket/SellMarket平掉所有达到最小时长的仓位。 - 净利润跌破
-MaxLoss时,视为风险控制,同样执行集中平仓。
- 净利润达到
- 日志输出会列出每个标的的盈亏及组合总盈亏,方便对比原脚本的
Comment信息。
参数
| 参数 | 说明 | 默认值 |
|---|---|---|
WatchedSymbols |
需要监控的证券 ID 列表(逗号分隔)。为空时使用主 Security。 |
"GBPUSD,USDCAD,USDCHF,USDSEK" |
ProfitTarget |
触发平仓的组合净利润(账户货币)。 | 60 |
MaxLoss |
触发保护性平仓的最大可承受亏损(账户货币)。 | 60 |
Slippage |
与原脚本一致的滑点设置。由于使用市价单平仓,该参数仅作信息展示。 | 10 |
MinAgeSeconds |
允许平仓前的最小持仓秒数。 | 60 |
CandleType |
用于定期检查的 K 线类型(默认 1 分钟)。 | 1 minute |
注意事项
- 策略直接使用 StockSharp
Positions集合提供的盈亏数据,无需额外的成交历史。 - 如果策略启动时已经有持仓,则以启动时刻作为首次观察时间,需等待
MinAgeSeconds后才会被强制平仓。 - 所有退出均通过市价单完成,以确保尽快离场;
Slippage参数主要用于保留原脚本配置。 - 在日志中可看到每个标的的盈亏以及组合净值,便于实时监控。
使用要求
- 需要一个可解析
WatchedSymbols中标的的SecurityProvider或连接器。 - 请确保每个标的的下单数量设置合理,使得平仓市价单可以完全对冲当前仓位。
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>
/// Closes the current position when floating PnL reaches a profit target or maximum loss.
/// Simplified from the multi-pair closer utility to work with a single security.
/// </summary>
public class MultiPairCloserStrategy : Strategy
{
private readonly StrategyParam<decimal> _profitTarget;
private readonly StrategyParam<decimal> _maxLoss;
private readonly StrategyParam<int> _minAgeSeconds;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _smaPeriod;
private SimpleMovingAverage _sma;
private decimal _entryPrice;
private DateTimeOffset? _entryTime;
/// <summary>
/// Profit target in price units.
/// </summary>
public decimal ProfitTarget
{
get => _profitTarget.Value;
set => _profitTarget.Value = value;
}
/// <summary>
/// Maximum tolerated loss in price units.
/// </summary>
public decimal MaxLoss
{
get => _maxLoss.Value;
set => _maxLoss.Value = value;
}
/// <summary>
/// Minimum age of an open position in seconds before exit is permitted.
/// </summary>
public int MinAgeSeconds
{
get => _minAgeSeconds.Value;
set => _minAgeSeconds.Value = value;
}
/// <summary>
/// Candle type for price monitoring.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// SMA period for entry signals.
/// </summary>
public int SmaPeriod
{
get => _smaPeriod.Value;
set => _smaPeriod.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public MultiPairCloserStrategy()
{
_profitTarget = Param(nameof(ProfitTarget), 5m)
.SetNotNegative()
.SetDisplay("Profit Target", "Close position when floating profit reaches this value", "Risk Management");
_maxLoss = Param(nameof(MaxLoss), 10m)
.SetNotNegative()
.SetDisplay("Maximum Loss", "Close position when floating loss reaches this value", "Risk Management");
_minAgeSeconds = Param(nameof(MinAgeSeconds), 60)
.SetNotNegative()
.SetDisplay("Min Age (s)", "Minimum holding time before exit is allowed", "Execution");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Candle series for monitoring", "General");
_smaPeriod = Param(nameof(SmaPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("SMA Period", "Moving average period for entry signal", "Indicators");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_sma = null;
_entryPrice = 0m;
_entryTime = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_sma = new SimpleMovingAverage { Length = SmaPeriod };
SubscribeCandles(CandleType)
.Bind(_sma, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal smaValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormed)
return;
var price = candle.ClosePrice;
var time = candle.CloseTime;
// Check exit conditions for open position
if (Position != 0 && _entryPrice > 0m)
{
var pnl = Position > 0
? price - _entryPrice
: _entryPrice - price;
var canClose = MinAgeSeconds <= 0 ||
(_entryTime.HasValue && (time - _entryTime.Value).TotalSeconds >= MinAgeSeconds);
if (canClose)
{
if ((ProfitTarget > 0m && pnl >= ProfitTarget) ||
(MaxLoss > 0m && pnl <= -MaxLoss))
{
if (Position > 0)
SellMarket(Math.Abs(Position));
else
BuyMarket(Math.Abs(Position));
_entryPrice = 0m;
_entryTime = null;
return;
}
}
}
// Entry logic: trend following with SMA
if (Position == 0)
{
if (price > smaValue)
{
BuyMarket();
_entryPrice = price;
_entryTime = time;
}
else if (price < smaValue)
{
SellMarket();
_entryPrice = price;
_entryTime = time;
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates
from System import TimeSpan, Math
class multi_pair_closer_strategy(Strategy):
def __init__(self):
super(multi_pair_closer_strategy, self).__init__()
self._profit_target = self.Param("ProfitTarget", 5.0)
self._max_loss = self.Param("MaxLoss", 10.0)
self._min_age_seconds = self.Param("MinAgeSeconds", 60)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30)))
self._sma_period = self.Param("SmaPeriod", 20)
self._sma = None
self._entry_price = 0.0
self._entry_time = None
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(multi_pair_closer_strategy, self).OnStarted2(time)
self._sma = SimpleMovingAverage()
self._sma.Length = self._sma_period.Value
self.SubscribeCandles(self.CandleType).Bind(self._sma, self._process_candle).Start()
def _process_candle(self, candle, sma_val):
if candle.State != CandleStates.Finished:
return
if not self.IsFormed:
return
price = float(candle.ClosePrice)
time = candle.CloseTime
sma_value = float(sma_val)
if self.Position != 0 and self._entry_price > 0:
if self.Position > 0:
pnl = price - self._entry_price
else:
pnl = self._entry_price - price
can_close = self._min_age_seconds.Value <= 0 or (
self._entry_time is not None and (time - self._entry_time).TotalSeconds >= self._min_age_seconds.Value)
if can_close:
if (self._profit_target.Value > 0 and pnl >= self._profit_target.Value) or \
(self._max_loss.Value > 0 and pnl <= -self._max_loss.Value):
if self.Position > 0:
self.SellMarket(abs(self.Position))
else:
self.BuyMarket(abs(self.Position))
self._entry_price = 0.0
self._entry_time = None
return
if self.Position == 0:
if price > sma_value:
self.BuyMarket()
self._entry_price = price
self._entry_time = time
elif price < sma_value:
self.SellMarket()
self._entry_price = price
self._entry_time = time
def OnReseted(self):
super(multi_pair_closer_strategy, self).OnReseted()
self._sma = None
self._entry_price = 0.0
self._entry_time = None
def CreateClone(self):
return multi_pair_closer_strategy()