在 GitHub 上查看

Lucky Shift Limit 策略

Lucky Shift Limit 策略是 MetaTrader 4 专家 Lucky_acnl6p6j89zn91fa.mq4 的完整移植版。策略实时监听最优买价和卖价(Level 1 报价),根据相邻报价之间的“点”(MetaTrader 点值)跳变进行反向交易:卖价向上跳变触发做空,买价向下跳变触发做多。所有持仓都会被持续跟踪,只要出现盈利或者浮亏超过安全阈值,就会立即平仓,与原始 MQ4 逻辑完全一致。

数据与执行要求

  • 行情数据:只需要 Level 1 报价,不依赖蜡烛或深度数据。
  • 委托类型:所有进出场均使用市价单,模拟 MetaTrader 中的即时 OrderSend/OrderClose 调用。
  • 账户模式:兼容对冲和净额账户。净额账户上仓位会累加为单一净头寸,由退出模块统一平仓。
  • 下单手数:默认使用 Strategy.Volume,若能获取组合权益,则按照 AccountFreeMargin/10000 的公式动态计算,与原版 GetLots() 函数一致。

参数

参数 默认值 说明
Shift points 3 连续两次报价之间需要出现的最小点差,达到后触发新的交易。数值越大越能过滤噪声,越小越敏感。
Limit points 18 持仓允许承受的最大不利波动。价格逆行超过该点数时将强制平仓。

参数以 MetaTrader 点为单位,内部会依据标的物的最小报价步长自动换算成绝对价格偏移;优化区间与原始专家保持一致。

交易流程

  1. 初始化
    • 使用 Security.PriceStep 将点值参数转换为实际价格距离。
    • 清空前一笔 Bid/Ask 缓存,并通过高阶 Bind 接口启动 Level 1 订阅。
  2. 入场条件
    • 当前 Ask 相比上一笔 Ask 上涨至少 Shift points 时,策略发送市价卖单(做空以衰减价格尖峰),日志记录触发原因。
    • 当前 Bid 相比上一笔 Bid 下跌至少相同点数时,策略发送市价买单。
    • 信号可连续触发,完全复现 MQ4 版本中允许多笔持仓的行为。
  3. 离场管理
    • 每次报价更新都会调用 TryClosePosition():多头在 Bid 高于均价时立即平仓获利,或者在 Ask 低于开仓价 Limit points 时止损。
    • 空头则在 Ask 低于均价时盈利平仓,或当 Bid 高于开仓价超过限制时止损。
    • 平仓全部通过市价单完成,确保同一报价周期内清掉头寸。
  4. 仓位控制
    • 当可获取账户权益时,按 equity / 10000(四舍五入到 0.1 手)计算下单手数,和 MQ4 的 GetLots() 完全一致。
    • 若权益信息缺失,则回退至策略默认的 Volume 数值。

实现细节

  • 仅使用 StockSharp 的高阶 API:SubscribeLevel1().Bind(ProcessLevel1),无需编写底层事件处理。
  • 只用简单的 nullable 变量保存上一笔报价,不创建自定义集合,满足 AGENTS.md 的限制。
  • 损失阈值会根据品种的价格步长自动调整,支持 5 位报价或更细分辨率的交易品种。
  • 运行过程中如果用户调整参数,策略会在下一次报价时重新计算阈值。
  • 日志会记录每次开仓和平仓的原因,方便回测和实盘排查。

使用建议

  • 最适合应用于流动性高、报价跳动频繁的外汇或指数品种。
  • 如需额外的风险控制,可结合 StartProtection 等账户级保护模块一起使用。
  • 在高噪声行情下可提高 Shift points 以减少过度交易;若想捕捉更细微的跳变,则降低该参数。
  • 该策略本质上是反趋势思路,若想转换为顺势突破,可将触发阈值设得更大或叠加趋势过滤指标。
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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Candle-based reversion strategy that reacts to sudden price jumps (high/low shifts)
/// and enforces a configurable loss cap. Adapted from a Level1 quote-reversion approach
/// to work with candle data for backtesting.
/// </summary>
public class LuckyShiftLimitStrategy : Strategy
{
	private readonly StrategyParam<int> _shiftPoints;
	private readonly StrategyParam<int> _limitPoints;

	private decimal? _previousHigh;
	private decimal? _previousLow;
	private decimal _shiftThreshold;
	private decimal _limitThreshold;
	private decimal _entryPrice;
	private bool _thresholdsReady;
	private int _holdBars;

	/// <summary>
	/// Minimum price shift (as percentage tenths) required to trigger an entry.
	/// </summary>
	public int ShiftPoints
	{
		get => _shiftPoints.Value;
		set => _shiftPoints.Value = value;
	}

	/// <summary>
	/// Maximum adverse excursion (as percentage) tolerated before force-closing losing trades.
	/// </summary>
	public int LimitPoints
	{
		get => _limitPoints.Value;
		set => _limitPoints.Value = value;
	}

	/// <summary>
	/// Initializes the strategy parameters taken from the original MQ4 expert.
	/// </summary>
	public LuckyShiftLimitStrategy()
	{
		_shiftPoints = Param(nameof(ShiftPoints), 3)
			.SetGreaterThanZero()
			.SetDisplay("Shift points", "Minimum price delta between consecutive candles", "Trading")

			.SetOptimize(1, 20, 1);

		_limitPoints = Param(nameof(LimitPoints), 18)
			.SetGreaterThanZero()
			.SetDisplay("Limit points", "Maximum allowed drawdown in percentage", "Risk management")

			.SetOptimize(5, 80, 5);
	}

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

		_previousHigh = null;
		_previousLow = null;
		_shiftThreshold = 0m;
		_limitThreshold = 0m;
		_entryPrice = 0m;
		_thresholdsReady = false;
		_holdBars = 0;
	}

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

		var tf = TimeSpan.FromMinutes(5).TimeFrame();

		SubscribeCandles(tf)
			.Bind(ProcessCandle)
			.Start();
	}

	private void EnsureThresholds(decimal price)
	{
		if (_thresholdsReady)
			return;

		if (price <= 0m)
			return;

		// ShiftPoints=3 -> 0.9% shift threshold, LimitPoints=18 -> 1.8% limit threshold
		_shiftThreshold = price * ShiftPoints * 0.003m;
		_limitThreshold = price * LimitPoints * 0.01m;
		_thresholdsReady = true;
	}

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

		var high = candle.HighPrice;
		var low = candle.LowPrice;
		var close = candle.ClosePrice;

		EnsureThresholds(close);

		if (!_thresholdsReady)
			return;

		// Count hold bars for position management.
		if (Position != 0)
			_holdBars++;

		// Entry logic: detect sudden shifts in high/low between consecutive candles.
		// Only enter when flat.
		if (Position == 0 && _previousHigh is decimal prevHigh && _previousLow is decimal prevLow)
		{
			// High jumped up sharply -> sell on expected reversion
			if (high - prevHigh >= _shiftThreshold)
			{
				SellMarket();
				_entryPrice = close;
				_holdBars = 0;
				LogInfo($"Sell triggered: high shift {high - prevHigh:0.##} >= {_shiftThreshold:0.##}. Price={close:0.#####}");
			}
			// Low dropped sharply -> buy on expected rebound
			else if (prevLow - low >= _shiftThreshold)
			{
				BuyMarket();
				_entryPrice = close;
				_holdBars = 0;
				LogInfo($"Buy triggered: low shift {prevLow - low:0.##} >= {_shiftThreshold:0.##}. Price={close:0.#####}");
			}
		}

		_previousHigh = high;
		_previousLow = low;

		TryClosePosition(close);
	}

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

		if (Position != 0m && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;

		if (Position == 0m)
			_entryPrice = 0m;
	}

	private void TryClosePosition(decimal currentPrice)
	{
		if (Position == 0)
			return;

		var avgPrice = _entryPrice;

		if (avgPrice <= 0m)
			return;

		// Minimum hold of 5 bars before checking exit.
		if (_holdBars < 5)
			return;

		// Use half of shift threshold as profit target.
		var profitTarget = _shiftThreshold * 0.5m;

		if (Position > 0)
		{
			// Close long on profit or loss cap.
			if (currentPrice - avgPrice >= profitTarget)
			{
				SellMarket();
				_holdBars = 0;
				LogInfo($"Closed long in profit. Price={currentPrice:0.#####}");
			}
			else if (_limitThreshold > 0m && avgPrice - currentPrice >= _limitThreshold)
			{
				SellMarket();
				_holdBars = 0;
				LogInfo($"Closed long on loss cap. Price={currentPrice:0.#####}");
			}
		}
		else if (Position < 0)
		{
			// Close short on profit or loss cap.
			if (avgPrice - currentPrice >= profitTarget)
			{
				BuyMarket();
				_holdBars = 0;
				LogInfo($"Closed short in profit. Price={currentPrice:0.#####}");
			}
			else if (_limitThreshold > 0m && currentPrice - avgPrice >= _limitThreshold)
			{
				BuyMarket();
				_holdBars = 0;
				LogInfo($"Closed short on loss cap. Price={currentPrice:0.#####}");
			}
		}
	}
}