在 GitHub 上查看

Next Bar Momentum 策略

该策略在最新收盘价远离较旧的参考收盘价时入场,利用价格的动量突破。这一思路来源于 MetaTrader 上的 “Nextbar” 智能交易系统,并保留了原有的风控元素:以点(pip)为单位的止损/止盈、可选的移动止损以及持仓寿命限制。

默认设置针对 15 分钟周期的外汇或指数期货等快速市场,不过只要有标准的蜡烛图数据,任何品种都可以使用。所有订单均以配置好的手数直接市价成交。

交易逻辑

  • 信号识别
    • 当新的蜡烛收盘后,算法会比较上一根蜡烛的收盘价与 SignalBar 根之前的收盘价。
    • 如果上一根收盘价高于较旧收盘价且差值超过 MinDistancePips,生成做多信号。
    • 如果上一根收盘价低于较旧收盘价且差值超过 MinDistancePips,生成做空信号。
    • ReverseSignals 选项用于反向交易,将所有信号方向互换。
  • 下单管理
    • 持仓期间不会开立新单,策略始终只持有一个方向的仓位,与原始 EA 的行为一致。
    • 入场后记录开仓价格,并将点值参数转换为价格单位,提前计算止损和止盈价位;对于 5 位报价品种会自动乘以 10,使 pip 定义与 MetaTrader 保持一致。

离场规则

  • 止损 / 止盈:可选参数,设置为 0 即可关闭。策略依据蜡烛的最高价和最低价判断是否触发离场。
  • 移动止损:当 TrailingStopPips > 0 时启用。一旦浮动盈利超过 TrailingStopPips + TrailingStepPips,止损价会沿着行情朝盈利方向推进,且与价格之间的距离不会缩小。
  • 持仓寿命:仓位在市场中保持 LifetimeBars 根完整蜡烛后,会在下一根开始时强制平仓,复刻原版 EA 的“持仓 N 根蜡烛后退出”规则。

参数说明

  • CandleType – 用于评估信号的蜡烛周期,默认 15 分钟。
  • OrderVolume – 每次市价单的手数/数量。
  • StopLossPips – 入场价到止损位的距离(pip)。
  • TakeProfitPips – 入场价到止盈位的距离(pip)。
  • TrailingStopPips – 移动止损与价格保持的距离,为 0 时关闭移动止损。
  • TrailingStepPips – 每次推进移动止损前所需的额外盈利,移动止损关闭时忽略该参数。
  • SignalBar – 参与比较的蜡烛间隔数,至少为 2,避免引用当前蜡烛。
  • MinDistancePips – 确认信号所需的最小点差。
  • LifetimeBars – 仓位允许持有的完整蜡烛数量,为 0 时表示不限时。
  • ReverseSignals – 启用后将多空信号互换。

实现要点

  • 策略只维护一个简短的收盘价列表来计算信号,不需要庞大的历史缓存。
  • Pip 到价格的转换基于品种的最小报价步长;对于 3 位或 5 位小数的报价,会自动对应传统的 pip 定义。
  • 所有风控均基于完整的蜡烛实现。如需更细粒度的保护,可在平台上结合交易所原生的止损单。
  • 示例未附带自动化测试,上线前请务必使用历史数据和仿真环境充分验证。
using System;
using System.Collections.Generic;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Next Bar Momentum strategy. Compares close with a reference bar for momentum breakout.
/// </summary>
public class NextBarMomentumStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _momentumPeriod;

	private decimal? _prevMom;

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

	public int MomentumPeriod
	{
		get => _momentumPeriod.Value;
		set => _momentumPeriod.Value = value;
	}

	public NextBarMomentumStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe", "General");

		_momentumPeriod = Param(nameof(MomentumPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("Momentum Period", "Rate of change lookback", "Indicators");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_prevMom = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_prevMom = null;

		var momentum = new Momentum { Length = MomentumPeriod };

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

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

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

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_prevMom = momVal;
			return;
		}

		if (_prevMom == null)
		{
			_prevMom = momVal;
			return;
		}

		// Momentum crosses above zero → buy
		if (_prevMom.Value <= 100m && momVal > 100m && Position <= 0)
		{
			if (Position < 0)
				BuyMarket();
			BuyMarket();
		}
		// Momentum crosses below zero → sell
		else if (_prevMom.Value >= 100m && momVal < 100m && Position >= 0)
		{
			if (Position > 0)
				SellMarket();
			SellMarket();
		}

		_prevMom = momVal;
	}
}