在 GitHub 上查看

Lucky 策略

Lucky 策略是一种突破型剥头皮系统,专注于最优买价和卖价之间的快速跳动。当卖价(Ask)相对上一笔报价上跳超过设定的点数时做多;当买价(Bid)下跌超过同样的点数时做空。只要头寸转为盈利,或价格回撤超过保护阈值,仓位会立即平仓。

数据与执行

  • 数据来源:需要 Level 1 行情,以持续获取最优买卖价。
  • 订单类型:所有进出场均使用市价单,以保证对报价突变的快速响应。
  • 持仓模式:原策略针对对冲账户设计,但在净头寸账户中同样适用,会自动累计净头寸。

参数

  • Shift points – 触发交易所需的最小点数跳动。数值越大越能过滤噪音,数值越小反应越敏感。
  • Limit points – 容许的最大不利波动(点数)。达到该阈值时将强制平仓,计算时会结合合约的最小报价步长。
  • Reverse mode – 反向开仓模式。启用后,卖价上跳触发做空,买价下跌触发做多。

交易逻辑

  1. 初始化
    • 根据合约报价步长,把点数参数转换为实际价格距离。
    • 订阅 Level 1 数据,并清空上一笔 bid/ask 缓存。
  2. 入场规则
    • Ask 相对上一笔 Ask 上涨超过阈值时买入(反向模式下改为卖出)。
    • Bid 相对上一笔 Bid 下跌超过阈值时卖出(反向模式下改为买入)。
  3. 头寸规模
    • 默认使用策略自身的 Volume 作为下单数量。
    • 如果能获取到组合资产价值,会模仿 MetaTrader 的做法:按照 FreeMargin / 10,000 估算手数,并保留一位小数,使资金规模更大的账户自动放大下单量。
  4. 离场规则
    • 多头在 Bid 超过平均开仓价时立即平仓;若 Ask 跌破开仓价并达到 Limit points,也会止损离场。
    • 空头在 Ask 跌破开仓价时获利平仓;若 Bid 向上突破开仓价且超过 Limit points,则止损离场。

使用建议

  • 最适合流动性充足且报价跳动明显的外汇或指数差价合约。
  • 实盘前建议额外配置组合级别的风控,例如总权益回撤止损。
  • 开启 Reverse mode 可以快速将策略转变为反向做单模式,无需修改其他参数。
  • 策略会对每一次满足条件的报价更新作出响应,如行情噪音较大,可适当增大 shift 参数或过滤数据频率。
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Breakout strategy that reacts to fast price shifts (candle-to-candle high/low jumps)
/// and closes trades on profit target or adverse move (stop loss).
/// </summary>
public class LuckyStrategy : Strategy
{
	private readonly StrategyParam<decimal> _shiftPct;
	private readonly StrategyParam<decimal> _profitPct;
	private readonly StrategyParam<decimal> _stopPct;
	private readonly StrategyParam<bool> _reverse;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _entryPrice;
	private decimal? _previousHigh;
	private decimal? _previousLow;
	private bool _isReady;

	/// <summary>
	/// Minimum percentage shift in high/low to trigger entry.
	/// </summary>
	public decimal ShiftPct
	{
		get => _shiftPct.Value;
		set => _shiftPct.Value = value;
	}

	/// <summary>
	/// Profit target as percentage of entry price.
	/// </summary>
	public decimal ProfitPct
	{
		get => _profitPct.Value;
		set => _profitPct.Value = value;
	}

	/// <summary>
	/// Stop loss as percentage of entry price.
	/// </summary>
	public decimal StopPct
	{
		get => _stopPct.Value;
		set => _stopPct.Value = value;
	}

	/// <summary>
	/// Switch to invert the trading direction.
	/// </summary>
	public bool Reverse
	{
		get => _reverse.Value;
		set => _reverse.Value = value;
	}

	/// <summary>
	/// Candle type and timeframe.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes the strategy parameters.
	/// </summary>
	public LuckyStrategy()
	{
		_shiftPct = Param(nameof(ShiftPct), 1.5m)
			.SetDisplay("Shift %", "Minimum percentage shift in high/low to trigger entry", "Trading")
			.SetOptimize(0.5m, 3.0m, 0.5m);

		_profitPct = Param(nameof(ProfitPct), 2.0m)
			.SetDisplay("Profit %", "Profit target as percentage of entry price", "Risk management")
			.SetOptimize(1.0m, 5.0m, 0.5m);

		_stopPct = Param(nameof(StopPct), 3.0m)
			.SetDisplay("Stop %", "Stop loss as percentage of entry price", "Risk management")
			.SetOptimize(1.0m, 5.0m, 0.5m);

		_reverse = Param(nameof(Reverse), false)
			.SetDisplay("Reverse mode", "Invert the direction of new trades", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle type", "Candle timeframe", "General");
	}

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

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

		_previousHigh = null;
		_previousLow = null;
		_entryPrice = 0m;
		_isReady = false;
	}

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

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

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

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

		if (!_isReady)
		{
			_previousHigh = high;
			_previousLow = low;
			_isReady = true;
			return;
		}

		// Try to close existing position first
		TryClosePosition(close);

		// Only open new positions if flat
		if (Position == 0 && _previousHigh.HasValue && _previousLow.HasValue)
		{
			var prevH = _previousHigh.Value;
			var prevL = _previousLow.Value;

			// Check for upward breakout: high moved up sharply relative to previous high
			if (prevH > 0m && (high - prevH) / prevH * 100m >= ShiftPct)
			{
				if (Reverse)
					OpenShort(close);
				else
					OpenLong(close);
			}
			// Check for downward breakdown: low moved down sharply relative to previous low
			else if (prevL > 0m && (prevL - low) / prevL * 100m >= ShiftPct)
			{
				if (Reverse)
					OpenLong(close);
				else
					OpenShort(close);
			}
		}

		_previousHigh = high;
		_previousLow = low;
	}

	private void OpenLong(decimal price)
	{
		BuyMarket(Volume);
		_entryPrice = price;
	}

	private void OpenShort(decimal price)
	{
		SellMarket(Volume);
		_entryPrice = price;
	}

	private void TryClosePosition(decimal currentPrice)
	{
		if (Position == 0 || _entryPrice <= 0m)
			return;

		if (Position > 0)
		{
			var pctChange = (currentPrice - _entryPrice) / _entryPrice * 100m;

			if (pctChange >= ProfitPct || pctChange <= -StopPct)
				SellMarket(Position);
		}
		else if (Position < 0)
		{
			var pctChange = (_entryPrice - currentPrice) / _entryPrice * 100m;

			if (pctChange >= ProfitPct || pctChange <= -StopPct)
				BuyMarket(Math.Abs(Position));
		}
	}
}