在 GitHub 上查看

Amstell 网格管理策略

本策略是 MetaTrader 专家顾问 "exp_Amstell-SL" 的高级 API 版本,用于运行双向加仓网格。策略分别跟踪最近一次的多头或空头成交价,当价格偏离最新成交价达到设定距离时继续加仓;当累计浮盈或浮亏达到设定的止盈、止损距离时,立即通过市价单平掉当前方向的全部仓位。实现基于 StockSharp 的蜡烛订阅与高层下单接口,因此只要有单品种的蜡烛数据即可运行。

为了适配 StockSharp 默认的净持仓模式,移植版本将多头网格与空头网格顺序执行:只要净持仓为非负则维护多头网格;当多头完全平仓后,才允许启动空头网格。

工作流程

数据与执行

  • 订阅参数 CandleType 指定的蜡烛序列(默认 1 分钟),仅处理收盘后的蜡烛。
  • 根据证券的 PriceStep 计算点值;当步长保留 3 或 5 位小数时乘以 10,复刻 MetaTrader 中 3/5 位报价的点值换算。
  • 所有交易均通过 BuyMarket / SellMarket 市价助手完成,不使用挂单。

多头管理

  • 当没有多头敞口且当前不在平空流程中时,以 OrderVolume 的数量开出初始多单。
  • 持续记录最新多头成交价和当前多头批次的加权平均持仓价。
  • 当收盘价相比最近一次多头成交价至少下跌 BuyDistancePips(换算成价格单位)时,加仓同样数量的多头。

空头管理

  • 只有当多头全部平仓、净持仓不为正时,才允许空头信号生效。
  • 若没有空头敞口则开出首笔空单;之后当价格相对于最近空头成交价上涨 BuyDistancePips * SellDistanceMultiplier 时继续加仓。
  • 同样维护空头的最新成交价与当前批次的加权平均成交价。

平仓规则

  • 对每个方向计算相对于平均价的未实现盈亏。
  • 当多头浮盈达到 TakeProfitPips 点或浮亏达到 StopLossPips 点时,使用市价卖出全部多头仓位。
  • 当空头浮盈达到 TakeProfitPips 点或浮亏达到 StopLossPips 点时,使用市价买入全部空头仓位。
  • 平仓完成后重置所有缓存的价格与仓位信息,等待下一根蜡烛重新启动网格。

与原始 MQL 程序的差异

  • 使用蜡烛收盘价而非逐笔报价触发逻辑。
  • 因净头寸限制,多头与空头网格不能同时持有,而是顺序运行。
  • 止盈止损在批次的加权平均价上评估,而不是针对每一张单独订单。

参数说明

参数 默认值 优化范围 说明
OrderVolume 0.01 0.010.10(步长 0.01 每次网格下单的数量,必须大于零。
TakeProfitPips 30 10150(步长 10 当前批次的止盈距离,单位为点。
StopLossPips 30 10150(步长 10 当前批次允许的最大回撤距离,单位为点。
BuyDistancePips 10 560(步长 5 多头加仓所需的最小回撤距离,必须小于止盈与止损。
SellDistanceMultiplier 10 215(步长 1 空头加仓距离相对于多头距离的倍数。
CandleType 1 分钟时间框架 用于计算信号的蜡烛类型。

实现提示

  • BuyDistancePips 不小于 TakeProfitPipsStopLossPips,策略会在启动时抛出异常,以复制原版参数校验。
  • 点值由 PriceStep 推导,若品种 tick 大小不同需相应调整参数。
  • OnReseted 中清空全部内部状态,可安全重复启动。
  • 实现严格遵循本仓库的高层 API 指南,没有自定义颜色或手动注册指标。
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>
/// Amstell averaging grid strategy that opens new entries when price drifts away
/// from the last fill and closes exposure once profit or loss thresholds are reached.
/// </summary>
public class AmstellGridManagerStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _buyDistancePips;
	private readonly StrategyParam<decimal> _sellDistanceMultiplier;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _longVolume;
	private decimal _shortVolume;
	private decimal? _averageLongPrice;
	private decimal? _averageShortPrice;
	private decimal? _lastBuyPrice;
	private decimal? _lastSellPrice;
	private decimal _pipValue;
	private decimal _takeProfitOffset;
	private decimal _stopLossOffset;
	private decimal _buyDistanceOffset;
	private decimal _sellDistanceOffset;
	private bool _closingLong;
	private bool _closingShort;

	/// <summary>
	/// Quantity per market order.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Profit target in pips for each grid leg.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Maximum tolerated loss in pips for each grid leg.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Price distance in pips required to add another long position.
	/// </summary>
	public int BuyDistancePips
	{
		get => _buyDistancePips.Value;
		set => _buyDistancePips.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the long distance when stacking short entries.
	/// </summary>
	public decimal SellDistanceMultiplier
	{
		get => _sellDistanceMultiplier.Value;
		set => _sellDistanceMultiplier.Value = value;
	}

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

	/// <summary>
	/// Initializes the strategy parameters.
	/// </summary>
	public AmstellGridManagerStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Quantity submitted with each grid order", "Trading")
			
			.SetOptimize(0.01m, 0.1m, 0.01m);

		_takeProfitPips = Param(nameof(TakeProfitPips), 30)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk")
			
			.SetOptimize(10, 150, 10);

		_stopLossPips = Param(nameof(StopLossPips), 30)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk")
			
			.SetOptimize(10, 150, 10);

		_buyDistancePips = Param(nameof(BuyDistancePips), 10)
			.SetGreaterThanZero()
			.SetDisplay("Buy Distance (pips)", "Distance before adding another long", "Entries")
			
			.SetOptimize(5, 60, 5);

		_sellDistanceMultiplier = Param(nameof(SellDistanceMultiplier), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Sell Distance Multiplier", "Multiplier applied to long distance when adding shorts", "Entries")
			
			.SetOptimize(2m, 15m, 1m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for processing", "General");
	}

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

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

		_longVolume = 0m;
		_shortVolume = 0m;
		_averageLongPrice = null;
		_averageShortPrice = null;
		_lastBuyPrice = null;
		_lastSellPrice = null;
		_pipValue = 0m;
		_takeProfitOffset = 0m;
		_stopLossOffset = 0m;
		_buyDistanceOffset = 0m;
		_sellDistanceOffset = 0m;
		_closingLong = false;
		_closingShort = false;
	}

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

		if (BuyDistancePips >= TakeProfitPips || BuyDistancePips >= StopLossPips)
			throw new InvalidOperationException("Buy distance must be less than take profit and stop loss distances.");

		UpdatePriceOffsets();

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

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

		// no indicators bound via .Bind()

		var close = candle.ClosePrice;

		if (!_closingLong && _longVolume > 0m && _averageLongPrice is decimal longAvg)
		{
			var profit = close - longAvg;
			if (profit >= _takeProfitOffset || -profit >= _stopLossOffset)
			{
				SellMarket();
				_closingLong = true;
				return;
			}
		}

		if (!_closingShort && _shortVolume > 0m && _averageShortPrice is decimal shortAvg)
		{
			var profit = shortAvg - close;
			if (profit >= _takeProfitOffset || -profit >= _stopLossOffset)
			{
				BuyMarket();
				_closingShort = true;
				return;
			}
		}

		var openedLong = false;

		if (!_closingLong && Position >= 0m)
		{
			if (_longVolume <= 0m)
			{
				BuyMarket();
				openedLong = true;
			}
			else if (_lastBuyPrice is decimal lastBuy && lastBuy - close >= _buyDistanceOffset)
			{
				BuyMarket();
				openedLong = true;
			}
		}

		if (openedLong)
			return;

		if (!_closingShort && Position <= 0m)
		{
			if (_shortVolume <= 0m)
			{
				SellMarket();
			}
			else if (_lastSellPrice is decimal lastSell && close - lastSell >= _sellDistanceOffset)
			{
				SellMarket();
			}
		}
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade.Order == null)
			return;

		var tradeVolume = trade.Trade.Volume;
		var price = trade.Trade.Price;

		if (trade.Order.Side == Sides.Buy)
		{
			if (_shortVolume > 0m)
			{
				var closingVolume = Math.Min(tradeVolume, _shortVolume);
				_shortVolume -= closingVolume;
				tradeVolume -= closingVolume;
				if (_shortVolume <= 0m)
				{
					_shortVolume = 0m;
					_averageShortPrice = null;
					_lastSellPrice = null;
				}
			}

			if (tradeVolume > 0m)
			{
				var newVolume = _longVolume + tradeVolume;
				var totalCost = (_averageLongPrice ?? 0m) * _longVolume + price * tradeVolume;
				_longVolume = newVolume;
				_averageLongPrice = totalCost / newVolume;
				_lastBuyPrice = price;
				_closingLong = false;
			}
		}
		else if (trade.Order.Side == Sides.Sell)
		{
			if (_longVolume > 0m)
			{
				var closingVolume = Math.Min(tradeVolume, _longVolume);
				_longVolume -= closingVolume;
				tradeVolume -= closingVolume;
				if (_longVolume <= 0m)
				{
					_longVolume = 0m;
					_averageLongPrice = null;
					_lastBuyPrice = null;
				}
			}

			if (tradeVolume > 0m)
			{
				var newVolume = _shortVolume + tradeVolume;
				var totalCost = (_averageShortPrice ?? 0m) * _shortVolume + price * tradeVolume;
				_shortVolume = newVolume;
				_averageShortPrice = totalCost / newVolume;
				_lastSellPrice = price;
				_closingShort = false;
			}
		}

		if (_longVolume <= 0m && Position <= 0m)
			_closingLong = false;

		if (_shortVolume <= 0m && Position >= 0m)
			_closingShort = false;

		if (Position == 0m)
		{
			_longVolume = 0m;
			_shortVolume = 0m;
			_averageLongPrice = null;
			_averageShortPrice = null;
			_lastBuyPrice = null;
			_lastSellPrice = null;
			_closingLong = false;
			_closingShort = false;
		}
	}

	private void UpdatePriceOffsets()
	{
		var step = Security?.PriceStep ?? 1m;
		if (step <= 0m)
			step = 1m;

		var decimals = GetDecimalPlaces(step);
		_pipValue = decimals == 3 || decimals == 5 ? step * 10m : step;

		_takeProfitOffset = TakeProfitPips * _pipValue;
		_stopLossOffset = StopLossPips * _pipValue;
		_buyDistanceOffset = BuyDistancePips * _pipValue;
		_sellDistanceOffset = _buyDistanceOffset * SellDistanceMultiplier;
	}

	private static int GetDecimalPlaces(decimal value)
	{
		if (value == 0m)
			return 0;

		var bits = decimal.GetBits(value);
		return (bits[3] >> 16) & 0x7F;
	}
}