在 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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import Momentum
from StockSharp.Algo.Strategies import Strategy
class next_bar_momentum_strategy(Strategy):
def __init__(self):
super(next_bar_momentum_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe", "General")
self._momentum_period = self.Param("MomentumPeriod", 10) \
.SetDisplay("Momentum Period", "Rate of change lookback", "Indicators")
self._prev_mom = None
@property
def CandleType(self):
return self._candle_type.Value
@property
def MomentumPeriod(self):
return self._momentum_period.Value
def OnReseted(self):
super(next_bar_momentum_strategy, self).OnReseted()
self._prev_mom = None
def OnStarted2(self, time):
super(next_bar_momentum_strategy, self).OnStarted2(time)
self._prev_mom = None
momentum = Momentum()
momentum.Length = self.MomentumPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(momentum, self._on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
ind_area = self.CreateChartArea()
if ind_area is not None:
self.DrawIndicator(ind_area, momentum)
def _on_process(self, candle, mom_value):
if candle.State != CandleStates.Finished:
return
mv = float(mom_value)
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_mom = mv
return
if self._prev_mom is None:
self._prev_mom = mv
return
# Momentum crosses above 100
if self._prev_mom <= 100.0 and mv > 100.0 and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
# Momentum crosses below 100
elif self._prev_mom >= 100.0 and mv < 100.0 and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._prev_mom = mv
def CreateClone(self):
return next_bar_momentum_strategy()