Momentum M15 策略
本策略移植自 MetaTrader 5 顾问 Momentum-M15(原始文件 Momentum-M15.mq5)。它在 15 分钟级别蜡烛上运
行,结合带有水平偏移的移动平均线与按开盘价计算的 Momentum 指标。核心思想是在价格位于偏移平均线相对
侧时反向交易:价格偏低时做多、价格偏高时做空,同时使用跳空过滤器和可选的追踪止损控制风险。
移植要点
- 使用 StockSharp 自带组件实现指标:可配置的移动平均线(默认 Smoothed)以及可以选择价格来源的
Momentum指标。 - MA 的水平偏移通过缓存指标数值并回取
MaShift个已完成柱的值来模拟,无需重写指标算法。 - Momentum 单调性检测保留原始
CheckMO_Up/CheckMO_Down逻辑,仅存储所需数量的最新数值。 - 大幅向上跳空保护 (
GapLevel/GapTimeout) 与 MQL 版本一致,使用Security.PriceStep将点值转换为价 格步长。 - 追踪止损通过监控价位并在触发时市价平仓来完成,对应 MQL 代码中每个新柱修改止损订单的行为。
参数
| 名称 | 说明 | 默认值 |
|---|---|---|
TradeVolume |
每笔交易的数量。 | 0.1 |
CandleType |
主要时间框架。 | 15m |
MaPeriod |
移动平均线周期。 | 26 |
MaShift |
移动平均线向前偏移的柱数。 | 8 |
MaMethod |
移动平均线类型(Simple、Exponential、Smoothed、Weighted)。 |
Smoothed |
MaPrice |
送入移动平均线的价格。 | Low |
MomentumPeriod |
Momentum 指标周期。 | 23 |
MomentumPrice |
Momentum 指标使用的价格。 | Open |
MomentumThreshold |
Momentum 判定基准值。 | 100 |
MomentumShift |
在基准值基础上的偏移量。 | -0.2 |
MomentumOpenLength |
触发进场所需的 Momentum 单调序列长度。 | 6 |
MomentumCloseLength |
触发离场所需的单调序列长度。 | 10 |
GapLevel |
暂停交易的最小正跳空(按价格步长计)。 | 30 |
GapTimeout |
跳空后保持暂停的柱数。 | 100 |
TrailingStop |
追踪止损距离(价格步长,0 为关闭)。 | 0 |
交易逻辑
进场
- 做多 条件:
- 最新 Momentum 小于
MomentumThreshold + MomentumShift。 - 前一根收盘价与当前开盘价都在偏移均线之下。
- Momentum 连续
MomentumOpenLength根保持非上升趋势。
- 最新 Momentum 小于
- 做空 条件:
- 最新 Momentum 大于
MomentumThreshold - MomentumShift。 - 前收与今开均高于偏移均线。
- Momentum 连续
MomentumOpenLength根保持非下降趋势。
- 最新 Momentum 大于
只有在没有持仓且未被跳空过滤器锁定时才会开仓。
离场
- 多头 平仓条件:
- Momentum 连续
MomentumCloseLength根不升,或 - 前一根收盘价跌破偏移均线,或
- 触发追踪止损(当前最低价减去
TrailingStop距离)。
- Momentum 连续
- 空头 平仓条件:
- Momentum 连续
MomentumCloseLength根不降,或 - 前一根收盘价突破偏移均线,或
- 触发追踪止损(当前最高价加上
TrailingStop距离)。
- Momentum 连续
跳空过滤
- 计算当前开盘价与前一收盘价的差值(换算为价格步长)。
- 当差值超过
GapLevel时,将计时器设置为GapTimeout。 - 每根完成的蜡烛将计时器减一,直到归零才允许再次交易。
重要说明
- 策略只处理已完成的蜡烛 (
CandleStates.Finished),因此信号会在下一根柱子开仓/平仓,与原始 EA 在新柱 第一笔成交触发的效果一致。 - MetaTrader 中的“点”通过
Security.PriceStep进行近似转换。如果合约没有正确配置价格步长,则跳空过滤 和追踪止损会自动停用。 - 移动平均线和 Momentum 的价格来源可以独立设置,与原始版本保持一致。
- 策略不会下达止损订单,而是通过市价单实现与
PositionModify类似的止损调整。
使用建议
- 选择目标证券,并确保
CandleType与回测时的时间框架一致(原始脚本为 15 分钟)。 - 根据账户规模设置
TradeVolume。 - 通过调整
MomentumOpenLength/MomentumCloseLength控制 Momentum 序列的严格程度。 - 若希望完全匹配原始“点”距离,可根据交易所的价格步长换算出合适的
TrailingStop和GapLevel。
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;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Momentum based strategy converted from the MetaTrader 5 "Momentum-M15" expert advisor.
/// </summary>
public class MomentumM15Strategy : Strategy
{
/// <summary>
/// Moving average method options aligned with the original expert advisor inputs.
/// </summary>
public enum MovingAverageMethods
{
/// <summary>
/// Simple moving average.
/// </summary>
Simple,
/// <summary>
/// Exponential moving average.
/// </summary>
Exponential,
/// <summary>
/// Smoothed moving average.
/// </summary>
Smoothed,
/// <summary>
/// Weighted moving average.
/// </summary>
Weighted
}
public enum CandlePrices
{
Open,
High,
Low,
Close,
Median,
Typical,
Weighted
}
private readonly StrategyParam<decimal> _volumeParam;
private readonly StrategyParam<DataType> _candleTypeParam;
private readonly StrategyParam<int> _maPeriodParam;
private readonly StrategyParam<int> _maShiftParam;
private readonly StrategyParam<MovingAverageMethods> _maMethodParam;
private readonly StrategyParam<CandlePrices> _maPriceParam;
private readonly StrategyParam<int> _momentumPeriodParam;
private readonly StrategyParam<CandlePrices> _momentumPriceParam;
private readonly StrategyParam<decimal> _momentumThresholdParam;
private readonly StrategyParam<decimal> _momentumShiftParam;
private readonly StrategyParam<int> _momentumOpenLengthParam;
private readonly StrategyParam<int> _momentumCloseLengthParam;
private readonly StrategyParam<int> _gapLevelParam;
private readonly StrategyParam<int> _gapTimeoutParam;
private readonly StrategyParam<decimal> _trailingStopParam;
private IIndicator _ma = null!;
private Momentum _momentum = null!;
private readonly List<decimal> _maHistory = new();
private readonly List<decimal> _momentumHistory = new();
private decimal? _previousClose;
private decimal? _longTrailingStop;
private decimal? _shortTrailingStop;
private int _gapTimer;
/// <summary>
/// Initializes a new instance of <see cref="MomentumM15Strategy"/>.
/// </summary>
public MomentumM15Strategy()
{
_volumeParam = Param(nameof(TradeVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Default order volume", "Trading")
.SetOptimize(0.05m, 0.5m, 0.05m);
_candleTypeParam = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe for calculations", "Common");
_maPeriodParam = Param(nameof(MaPeriod), 26)
.SetGreaterThanZero()
.SetDisplay("MA Period", "Moving average lookback length", "Indicators")
.SetOptimize(10, 60, 5);
_maShiftParam = Param(nameof(MaShift), 8)
.SetNotNegative()
.SetDisplay("MA Shift", "Horizontal shift applied to moving average", "Indicators");
_maMethodParam = Param(nameof(MaMethod), MovingAverageMethods.Smoothed)
.SetDisplay("MA Method", "Type of moving average", "Indicators");
_maPriceParam = Param(nameof(MaPrice), CandlePrices.Low)
.SetDisplay("MA Price", "Price source for moving average", "Indicators");
_momentumPeriodParam = Param(nameof(MomentumPeriod), 23)
.SetGreaterThanZero()
.SetDisplay("Momentum Period", "Momentum indicator lookback", "Indicators")
.SetOptimize(10, 40, 1);
_momentumPriceParam = Param(nameof(MomentumPrice), CandlePrices.Open)
.SetDisplay("Momentum Price", "Price source for momentum", "Indicators");
_momentumThresholdParam = Param(nameof(MomentumThreshold), 100m)
.SetDisplay("Momentum Threshold", "Baseline momentum threshold", "Trading Rules");
_momentumShiftParam = Param(nameof(MomentumShift), -0.2m)
.SetDisplay("Momentum Shift", "Shift applied to momentum threshold", "Trading Rules");
_momentumOpenLengthParam = Param(nameof(MomentumOpenLength), 6)
.SetNotNegative()
.SetDisplay("Momentum Open Length", "Bars required for monotonic momentum on entries", "Trading Rules");
_momentumCloseLengthParam = Param(nameof(MomentumCloseLength), 10)
.SetNotNegative()
.SetDisplay("Momentum Close Length", "Bars required for monotonic momentum on exits", "Trading Rules");
_gapLevelParam = Param(nameof(GapLevel), 30)
.SetNotNegative()
.SetDisplay("Gap Level", "Minimum gap in price steps to pause trading", "Risk Management");
_gapTimeoutParam = Param(nameof(GapTimeout), 100)
.SetNotNegative()
.SetDisplay("Gap Timeout", "Number of bars to skip after a large gap", "Risk Management");
_trailingStopParam = Param(nameof(TrailingStop), 0m)
.SetNotNegative()
.SetDisplay("Trailing Stop", "Trailing stop distance in price steps", "Risk Management");
}
/// <summary>
/// Default trade volume.
/// </summary>
public decimal TradeVolume
{
get => _volumeParam.Value;
set => _volumeParam.Value = value;
}
/// <summary>
/// Candle type for the strategy.
/// </summary>
public DataType CandleType
{
get => _candleTypeParam.Value;
set => _candleTypeParam.Value = value;
}
/// <summary>
/// Moving average period.
/// </summary>
public int MaPeriod
{
get => _maPeriodParam.Value;
set => _maPeriodParam.Value = value;
}
/// <summary>
/// Number of bars to shift the moving average.
/// </summary>
public int MaShift
{
get => _maShiftParam.Value;
set => _maShiftParam.Value = value;
}
/// <summary>
/// Moving average calculation method.
/// </summary>
public MovingAverageMethods MaMethod
{
get => _maMethodParam.Value;
set => _maMethodParam.Value = value;
}
/// <summary>
/// Price source for the moving average.
/// </summary>
public CandlePrices MaPrice
{
get => _maPriceParam.Value;
set => _maPriceParam.Value = value;
}
/// <summary>
/// Momentum indicator lookback period.
/// </summary>
public int MomentumPeriod
{
get => _momentumPeriodParam.Value;
set => _momentumPeriodParam.Value = value;
}
/// <summary>
/// Price source for the momentum indicator.
/// </summary>
public CandlePrices MomentumPrice
{
get => _momentumPriceParam.Value;
set => _momentumPriceParam.Value = value;
}
/// <summary>
/// Baseline momentum threshold.
/// </summary>
public decimal MomentumThreshold
{
get => _momentumThresholdParam.Value;
set => _momentumThresholdParam.Value = value;
}
/// <summary>
/// Shift applied to the momentum threshold.
/// </summary>
public decimal MomentumShift
{
get => _momentumShiftParam.Value;
set => _momentumShiftParam.Value = value;
}
/// <summary>
/// Sequence length for entry momentum validation.
/// </summary>
public int MomentumOpenLength
{
get => _momentumOpenLengthParam.Value;
set => _momentumOpenLengthParam.Value = value;
}
/// <summary>
/// Sequence length for exit momentum validation.
/// </summary>
public int MomentumCloseLength
{
get => _momentumCloseLengthParam.Value;
set => _momentumCloseLengthParam.Value = value;
}
/// <summary>
/// Minimum gap (in price steps) that suspends new entries.
/// </summary>
public int GapLevel
{
get => _gapLevelParam.Value;
set => _gapLevelParam.Value = value;
}
/// <summary>
/// Number of bars to wait after a gap before trading resumes.
/// </summary>
public int GapTimeout
{
get => _gapTimeoutParam.Value;
set => _gapTimeoutParam.Value = value;
}
/// <summary>
/// Trailing stop distance expressed in price steps.
/// </summary>
public decimal TrailingStop
{
get => _trailingStopParam.Value;
set => _trailingStopParam.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_ma = null!;
_momentum = null!;
_maHistory.Clear();
_momentumHistory.Clear();
_previousClose = null;
_longTrailingStop = null;
_shortTrailingStop = null;
_gapTimer = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_ma = CreateMovingAverage(MaMethod, MaPeriod);
_momentum = new Momentum { Length = MomentumPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_ma is null || _momentum is null)
return;
var maValue = ProcessMovingAverage(candle);
var momentumValue = ProcessMomentum(candle);
if (maValue is null || momentumValue is null)
{
_previousClose = candle.ClosePrice;
return;
}
var previousClose = _previousClose;
_previousClose = candle.ClosePrice;
if (previousClose is null)
return;
HandleGapFilter(previousClose.Value, candle.OpenPrice);
if (_gapTimer > 0)
{
_gapTimer--;
if (_gapTimer > 0)
return;
}
if (Position == 0)
{
TryOpenPositions(previousClose.Value, candle.OpenPrice, maValue.Value, momentumValue.Value);
}
else
{
ManageExistingPosition(previousClose.Value, candle, maValue.Value, momentumValue.Value);
}
}
private decimal? ProcessMovingAverage(ICandleMessage candle)
{
var price = GetPrice(candle, MaPrice);
var value = _ma.Process(new DecimalIndicatorValue(_ma, price, candle.OpenTime) { IsFinal = true });
if (value.IsEmpty || !_ma.IsFormed)
return null;
var ma = value.ToDecimal();
_maHistory.Add(ma);
var maxCount = MaShift + 1;
while (_maHistory.Count > maxCount)
_maHistory.RemoveAt(0);
var index = _maHistory.Count - 1 - MaShift;
if (index < 0 || index >= _maHistory.Count)
return null;
return _maHistory[index];
}
private decimal? ProcessMomentum(ICandleMessage candle)
{
var price = GetPrice(candle, MomentumPrice);
var value = _momentum.Process(new DecimalIndicatorValue(_momentum, price, candle.OpenTime) { IsFinal = true });
if (value.IsEmpty || !_momentum.IsFormed)
return null;
var momentum = value.ToDecimal();
_momentumHistory.Add(momentum);
var maxLen = Math.Max(Math.Max(MomentumOpenLength, MomentumCloseLength), 1);
while (_momentumHistory.Count > maxLen)
_momentumHistory.RemoveAt(0);
return momentum;
}
private void HandleGapFilter(decimal previousClose, decimal currentOpen)
{
var priceStep = Security.PriceStep ?? 0m;
if (priceStep <= 0m)
return;
var gap = (currentOpen - previousClose) / priceStep;
if (gap > GapLevel)
_gapTimer = GapTimeout;
}
private void TryOpenPositions(decimal previousClose, decimal currentOpen, decimal maValue, decimal momentumValue)
{
var longMomentumOk = MomentumOpenLength > 0 && IsMomentumDownSequence(MomentumOpenLength);
var shortMomentumOk = MomentumOpenLength > 0 && IsMomentumUpSequence(MomentumOpenLength);
var longCondition = momentumValue < MomentumThreshold + MomentumShift
&& previousClose < maValue
&& currentOpen < maValue
&& longMomentumOk;
var shortCondition = momentumValue > MomentumThreshold - MomentumShift
&& previousClose > maValue
&& currentOpen > maValue
&& shortMomentumOk;
if (longCondition)
{
BuyMarket(TradeVolume);
_longTrailingStop = null;
_shortTrailingStop = null;
}
else if (shortCondition)
{
SellMarket(TradeVolume);
_longTrailingStop = null;
_shortTrailingStop = null;
}
}
private void ManageExistingPosition(decimal previousClose, ICandleMessage candle, decimal maValue, decimal momentumValue)
{
if (Position > 0)
{
var exitMomentum = MomentumCloseLength > 0 && IsMomentumDownSequence(MomentumCloseLength);
var shouldClose = exitMomentum || previousClose < maValue;
if (shouldClose)
{
SellMarket(Position);
_longTrailingStop = null;
return;
}
UpdateLongTrailingStop(candle);
}
else if (Position < 0)
{
var exitMomentum = MomentumCloseLength > 0 && IsMomentumUpSequence(MomentumCloseLength);
var shouldClose = exitMomentum || previousClose > maValue;
if (shouldClose)
{
BuyMarket(Math.Abs(Position));
_shortTrailingStop = null;
return;
}
UpdateShortTrailingStop(candle);
}
}
private void UpdateLongTrailingStop(ICandleMessage candle)
{
if (TrailingStop <= 0m)
return;
var priceStep = Security.PriceStep ?? 0m;
if (priceStep <= 0m)
return;
var distance = TrailingStop * priceStep;
var candidate = candle.LowPrice - distance;
if (_longTrailingStop is null || candidate > _longTrailingStop)
_longTrailingStop = candidate;
if (_longTrailingStop is decimal stop && candle.LowPrice <= stop)
{
SellMarket(Position);
_longTrailingStop = null;
}
}
private void UpdateShortTrailingStop(ICandleMessage candle)
{
if (TrailingStop <= 0m)
return;
var priceStep = Security.PriceStep ?? 0m;
if (priceStep <= 0m)
return;
var distance = TrailingStop * priceStep;
var candidate = candle.HighPrice + distance;
if (_shortTrailingStop is null || candidate < _shortTrailingStop)
_shortTrailingStop = candidate;
if (_shortTrailingStop is decimal stop && candle.HighPrice >= stop)
{
BuyMarket(Math.Abs(Position));
_shortTrailingStop = null;
}
}
private bool IsMomentumDownSequence(int length)
{
if (length <= 0 || _momentumHistory.Count < length)
return false;
var start = _momentumHistory.Count - length;
var previous = _momentumHistory[start];
for (var i = start + 1; i < _momentumHistory.Count; i++)
{
var current = _momentumHistory[i];
if (current > previous)
return false;
previous = current;
}
return true;
}
private bool IsMomentumUpSequence(int length)
{
if (length <= 0 || _momentumHistory.Count < length)
return false;
var start = _momentumHistory.Count - length;
var previous = _momentumHistory[start];
for (var i = start + 1; i < _momentumHistory.Count; i++)
{
var current = _momentumHistory[i];
if (current < previous)
return false;
previous = current;
}
return true;
}
private static decimal GetPrice(ICandleMessage candle, CandlePrices price)
{
return price switch
{
CandlePrices.Open => candle.OpenPrice,
CandlePrices.High => candle.HighPrice,
CandlePrices.Low => candle.LowPrice,
CandlePrices.Close => candle.ClosePrice,
CandlePrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
CandlePrices.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
CandlePrices.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
_ => candle.ClosePrice,
};
}
private static IIndicator CreateMovingAverage(MovingAverageMethods method, int period)
{
return method switch
{
MovingAverageMethods.Simple => new SimpleMovingAverage { Length = period },
MovingAverageMethods.Exponential => new ExponentialMovingAverage { Length = period },
MovingAverageMethods.Smoothed => new SmoothedMovingAverage { Length = period },
MovingAverageMethods.Weighted => new WeightedMovingAverage { Length = period },
_ => new SimpleMovingAverage { Length = period },
};
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Indicators import SmoothedMovingAverage, Momentum
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates
from System import TimeSpan, Math, Decimal
from indicator_extensions import *
class momentum_m15_strategy(Strategy):
def __init__(self):
super(momentum_m15_strategy, self).__init__()
self._trade_volume = self.Param("TradeVolume", 0.1)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._ma_period = self.Param("MaPeriod", 26)
self._ma_shift = self.Param("MaShift", 8)
self._momentum_period = self.Param("MomentumPeriod", 23)
self._momentum_threshold = self.Param("MomentumThreshold", 100.0)
self._momentum_shift = self.Param("MomentumShift", -0.2)
self._momentum_open_length = self.Param("MomentumOpenLength", 6)
self._momentum_close_length = self.Param("MomentumCloseLength", 10)
self._gap_level = self.Param("GapLevel", 30)
self._gap_timeout = self.Param("GapTimeout", 100)
self._trailing_stop = self.Param("TrailingStop", 0.0)
self._ma = None
self._momentum = None
self._ma_history = []
self._momentum_history = []
self._previous_close = None
self._long_trailing_stop = None
self._short_trailing_stop = None
self._gap_timer = 0
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(momentum_m15_strategy, self).OnStarted2(time)
self._ma = SmoothedMovingAverage()
self._ma.Length = self._ma_period.Value
self._momentum = Momentum()
self._momentum.Length = self._momentum_period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._ma is None or self._momentum is None:
return
ma_value = self._process_ma(candle)
momentum_value = self._process_momentum(candle)
if ma_value is None or momentum_value is None:
self._previous_close = float(candle.ClosePrice)
return
previous_close = self._previous_close
self._previous_close = float(candle.ClosePrice)
if previous_close is None:
return
self._handle_gap_filter(previous_close, float(candle.OpenPrice))
if self._gap_timer > 0:
self._gap_timer -= 1
if self._gap_timer > 0:
return
if self.Position == 0:
self._try_open_positions(previous_close, float(candle.OpenPrice), ma_value, momentum_value)
else:
self._manage_existing_position(previous_close, candle, ma_value, momentum_value)
def _process_ma(self, candle):
price = float(candle.LowPrice)
value = process_float(self._ma, Decimal(float(price)), candle.OpenTime, True)
if value.IsEmpty or not self._ma.IsFormed:
return None
ma = float(value.Value)
self._ma_history.append(ma)
max_count = self._ma_shift.Value + 1
while len(self._ma_history) > max_count:
self._ma_history.pop(0)
index = len(self._ma_history) - 1 - self._ma_shift.Value
if index < 0 or index >= len(self._ma_history):
return None
return self._ma_history[index]
def _process_momentum(self, candle):
price = float(candle.OpenPrice)
value = process_float(self._momentum, Decimal(float(price)), candle.OpenTime, True)
if value.IsEmpty or not self._momentum.IsFormed:
return None
mom = float(value.Value)
self._momentum_history.append(mom)
max_len = max(max(self._momentum_open_length.Value, self._momentum_close_length.Value), 1)
while len(self._momentum_history) > max_len:
self._momentum_history.pop(0)
return mom
def _handle_gap_filter(self, previous_close, current_open):
price_step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0
if price_step <= 0:
return
gap = (current_open - previous_close) / price_step
if gap > self._gap_level.Value:
self._gap_timer = self._gap_timeout.Value
def _try_open_positions(self, previous_close, current_open, ma_value, momentum_value):
long_momentum_ok = self._momentum_open_length.Value > 0 and self._is_momentum_down_sequence(self._momentum_open_length.Value)
short_momentum_ok = self._momentum_open_length.Value > 0 and self._is_momentum_up_sequence(self._momentum_open_length.Value)
long_condition = (momentum_value < self._momentum_threshold.Value + self._momentum_shift.Value
and previous_close < ma_value
and current_open < ma_value
and long_momentum_ok)
short_condition = (momentum_value > self._momentum_threshold.Value - self._momentum_shift.Value
and previous_close > ma_value
and current_open > ma_value
and short_momentum_ok)
if long_condition:
vol = self._trade_volume.Value if self._trade_volume.Value > 0 else float(self.Volume)
self.BuyMarket(vol)
self._long_trailing_stop = None
self._short_trailing_stop = None
elif short_condition:
vol = self._trade_volume.Value if self._trade_volume.Value > 0 else float(self.Volume)
self.SellMarket(vol)
self._long_trailing_stop = None
self._short_trailing_stop = None
def _manage_existing_position(self, previous_close, candle, ma_value, momentum_value):
if self.Position > 0:
exit_momentum = self._momentum_close_length.Value > 0 and self._is_momentum_down_sequence(self._momentum_close_length.Value)
should_close = exit_momentum or previous_close < ma_value
if should_close:
self.SellMarket(self.Position)
self._long_trailing_stop = None
return
self._update_long_trailing(candle)
elif self.Position < 0:
exit_momentum = self._momentum_close_length.Value > 0 and self._is_momentum_up_sequence(self._momentum_close_length.Value)
should_close = exit_momentum or previous_close > ma_value
if should_close:
self.BuyMarket(abs(self.Position))
self._short_trailing_stop = None
return
self._update_short_trailing(candle)
def _update_long_trailing(self, candle):
if self._trailing_stop.Value <= 0:
return
price_step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0
if price_step <= 0:
return
distance = self._trailing_stop.Value * price_step
candidate = float(candle.LowPrice) - distance
if self._long_trailing_stop is None or candidate > self._long_trailing_stop:
self._long_trailing_stop = candidate
if self._long_trailing_stop is not None and float(candle.LowPrice) <= self._long_trailing_stop:
self.SellMarket(self.Position)
self._long_trailing_stop = None
def _update_short_trailing(self, candle):
if self._trailing_stop.Value <= 0:
return
price_step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0
if price_step <= 0:
return
distance = self._trailing_stop.Value * price_step
candidate = float(candle.HighPrice) + distance
if self._short_trailing_stop is None or candidate < self._short_trailing_stop:
self._short_trailing_stop = candidate
if self._short_trailing_stop is not None and float(candle.HighPrice) >= self._short_trailing_stop:
self.BuyMarket(abs(self.Position))
self._short_trailing_stop = None
def _is_momentum_down_sequence(self, length):
if length <= 0 or len(self._momentum_history) < length:
return False
start = len(self._momentum_history) - length
previous = self._momentum_history[start]
for i in range(start + 1, len(self._momentum_history)):
current = self._momentum_history[i]
if current > previous:
return False
previous = current
return True
def _is_momentum_up_sequence(self, length):
if length <= 0 or len(self._momentum_history) < length:
return False
start = len(self._momentum_history) - length
previous = self._momentum_history[start]
for i in range(start + 1, len(self._momentum_history)):
current = self._momentum_history[i]
if current < previous:
return False
previous = current
return True
def OnReseted(self):
super(momentum_m15_strategy, self).OnReseted()
self._ma = None
self._momentum = None
self._ma_history = []
self._momentum_history = []
self._previous_close = None
self._long_trailing_stop = None
self._short_trailing_stop = None
self._gap_timer = 0
def CreateClone(self):
return momentum_m15_strategy()