在 GitHub 上查看

Lock Strategy 策略 (中文)

Lock Strategy 复刻了 MetaTrader 上常见的“锁单”专家顾问:系统始终保持一多一空的对冲仓位,并不断循环,直到满足锁定利润的条件为止。该算法适用于点值较小、可以使用固定点差止盈的交易品种。

运行流程

  1. 建立初始对冲:一旦有行情数据,策略立即以相同手数买入和卖出。如果两笔订单都成功成交,则下一次对冲所用的手数乘以 LotExponential 系数。
  2. 止盈管理:每条腿记录自己的进场价。当收盘价相对进场价移动了 TakeProfitPips(换算成最小跳动)时,该腿通过市价单平仓;另一条腿继续持有,保持 MQL 版本的锁仓特性。
  3. 重新锁仓:如果当前仅剩一条腿或完全没有持仓,策略会立即再开出一组多空。当没有任何腿时,下一次下单前会把手数重置为 LotSize
  4. 手数控制AdjustVolume 方法会按照交易所规则调整手数——对齐到 VolumeStep,并限制在 MinVolumeMaxVolume 之间;如果结果为零,则取消放大。

利润锁定条件

原始 MQL 脚本比较账户余额与权益:当余额比权益高出 ExcessBalanceOverEquity,并且权益相对上一次锁定时至少增加 MinProfit,策略就会平掉全部仓位。C# 实现通过记录空仓时的权益来模拟余额,一旦触发条件,所有腿都会被平仓,基准权益被更新,然后循环重新从 LotSize 开始。

参数说明

  • LotSize:第一轮锁仓使用的基础手数(默认 0.1m)。
  • TakeProfitPips:每条腿的止盈距离,单位为点(默认 1000 表示不自动平仓)。
  • LotExponential:两条腿都成交后,下一个周期的放大倍数(默认 2m)。
  • ExcessBalanceOverEquity:允许的余额与权益差值阈值,超过后触发锁盈(默认 3000m)。
  • MinProfit:触发全平前所需的额外权益收益(默认 500m)。
  • CandleType:驱动策略逻辑的 K 线周期(默认 1 分钟)。

实现细节

  • 点值根据 Security.PriceStepSecurity.Decimals 自动计算,可兼容 3/5 位的外汇合约以及常见的期货、股票。
  • 策略使用市价单进出场,模拟原专家顾问发送市价单并由券商处理止盈的行为。
  • 系统保存所有打开的腿,因此可以在每个方向叠加多笔仓位,与源代码的行为保持一致。
namespace StockSharp.Samples.Strategies;

using System;

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

/// <summary>
/// Lock strategy (simplified).
/// Uses fast and slow EMA crossover with momentum confirmation.
/// </summary>
public class LockStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	public LockStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Source candles", "General");

		_fastPeriod = Param(nameof(FastPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Fast EMA period", "Indicators");

		_slowPeriod = Param(nameof(SlowPeriod), 30)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Slow EMA period", "Indicators");
	}

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

		var fastEma = new ExponentialMovingAverage { Length = FastPeriod };
		var slowEma = new ExponentialMovingAverage { Length = SlowPeriod };

		decimal prevFast = 0, prevSlow = 0;
		bool hasPrev = false;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(fastEma, slowEma, (ICandleMessage candle, decimal fastValue, decimal slowValue) =>
			{
				if (candle.State != CandleStates.Finished)
					return;

				if (!hasPrev)
				{
					prevFast = fastValue;
					prevSlow = slowValue;
					hasPrev = true;
					return;
				}

				if (!IsFormedAndOnlineAndAllowTrading())
				{
					prevFast = fastValue;
					prevSlow = slowValue;
					return;
				}

				// Buy on golden cross
				if (prevFast <= prevSlow && fastValue > slowValue && Position <= 0)
				{
					BuyMarket();
				}
				// Sell on death cross
				else if (prevFast >= prevSlow && fastValue < slowValue && Position >= 0)
				{
					SellMarket();
				}

				prevFast = fastValue;
				prevSlow = slowValue;
			})
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, fastEma);
			DrawIndicator(area, slowEma);
			DrawOwnTrades(area);
		}
	}
}