在 GitHub 上查看

自动止损与止盈策略

该实用型策略会在指定品种的每一笔持仓上自动挂入止损和止盈保护单,重现 MetaTrader "AutoSet SL TP" 智能交易顾问的行为。策略持续监控当前持仓列表,在提交保护单之前先验证券商对最小距离的限制。

策略本身不会开仓,而是观察由人工或其他系统生成的仓位数量、方向和成交价。一旦检测到新的多头或空头仓位,算法就会以 MetaTrader 风格的“点(pip)”为单位计算期望的止损、止盈距离,根据交易所提供的 Freeze/Stop 限制调整实际价格,然后提交相应的止损或止盈委托。当仓位完全平仓后,这些保护单会自动撤销。

工作流程

  1. 订阅 Level1 数据,获取最优买卖价以及券商可能提供的 StopLevelFreezeLevel 字段。
  2. 读取合约的价格步长和小数位数,将输入的点数转换为实际价格距离。对于三位或五位报价,会自动乘以 10,确保与 MetaTrader 的点值定义一致。
  3. 当收到行情更新或个人成交通知时:
    • 如果当前没有仓位,或仓位方向不符合过滤条件(仅多、仅空或双向),则忽略此次事件。
    • 计算市场价格与保护单之间的最小允许距离。若券商未提供 Freeze/Stop 信息,则退回到“3 倍价差 × 1.1”的安全缓冲。
    • 基于当前买价(多头)或卖价(空头)推导止损、止盈价格,并按照合约价格步长进行标准化。
    • 以仓位的实际数量创建或重新注册止损/止盈委托。只有在目标价格或数量发生变化时才会更新委托,从而减少修改次数。
  4. 当仓位归零时,所有尚未成交的保护单都会被撤销;若仓位方向与过滤条件不符,保护单也会立即取消。

依靠这种外部成交驱动的方式,策略可以与手动交易、交易面板或其他自动化入场系统无缝结合,始终为持仓提供一致的风险防护。

参数

  • StopLossPips – 距离当前价格的止损点数,单位为 MetaTrader 点。设置为 0 表示不挂止损。默认值:50
  • TakeProfitPips – 距离当前价格的止盈点数,单位为 MetaTrader 点。设置为 0 表示不挂止盈。默认值:140
  • DirectionFilter – 指定需要保护的仓位方向:
    • Buy – 仅管理多头仓位。
    • Sell – 仅管理空头仓位。
    • BuySell – 同时管理多头与空头(与原始 EA 的默认行为一致)。

使用提示

  • 保护单的数量等于仓位的绝对数量。若券商对最小或最大手数有限制,策略会在提交前将数量调整到最近的合法值。
  • 策略通过 ReRegisterOrder 调整已有保护单,尽量复用原有订单编号,减少频繁撤单带来的额外风险。
  • 采用“价差 × 3 × 1.1”的后备距离,确保在缺少显式 Freeze/Stop 数据时也不会触碰交易所隐藏的风控阈值。
  • 策略可在持仓前或持仓后启动。若在启动时已经存在仓位,只要收到第一笔行情更新,就会立即补上保护单。
  • 对于三位或五位报价的品种,MetaTrader 的“点”与交易所的价格步长不同。策略会与原版 EA 一样对步长进行放大,保证参数与 MT5 中的设置完全对应。

与 MetaTrader 版本的差异

  • StockSharp 版本使用显式的止损和限价委托来实现保护逻辑,而不是修改仓位上的止损/止盈属性,使得订单簿中的行为更加透明。
  • 通过 Level1 数据重建券商的限制。如果数据源使用了不同的字段名称,策略会自动在 Level1Fields 枚举中查找匹配项。
  • 按仓库规范,代码注释和日志全部采用英文;为了便于使用者阅读,文档提供了俄语和中文翻译。
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

public class AutoSetStopLossTakeProfitStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _slow;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
	public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
	public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }

	public AutoSetStopLossTakeProfitStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 14).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
		_slowPeriod = Param(nameof(SlowPeriod), 50).SetGreaterThanZero().SetDisplay("Slow Period", "Slow EMA period", "Indicator");
		_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_fast = null; _slow = null;
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_fast = new ExponentialMovingAverage { Length = FastPeriod };
		_slow = new ExponentialMovingAverage { Length = SlowPeriod };
		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished) return;
		if (!_fast.IsFormed || !_slow.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
		if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}

		if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
		{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 100; }
		else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
		{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 100; }

		_prevFast = fastValue; _prevSlow = slowValue;
	}
}