四柱动量反转策略
四柱动量反转策略在选定时间窗口内,当收盘价连续 BuyThreshold 根K线低于 Lookback 根之前的收盘价时做多。一旦价格突破前一根K线的最高价,仓位被平掉。
细节
- 入场: 在时间窗口内,连续
BuyThreshold根收盘价低于Lookback根之前的收盘价。 - 出场: 收盘价高于前一根K线最高价。
- 止损: 无。
- 默认值:
BuyThreshold= 4Lookback= 4StartTime= 2014-01-01EndTime= 2099-01-01
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Four Bar Momentum Reversal Strategy.
/// Enters long after consecutive closes below the close from N bars ago.
/// Exits on breakout above previous high.
/// Uses EMA as trend filter.
/// </summary>
public class FourBarMomentumReversalStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _buyThreshold;
private readonly StrategyParam<int> _lookback;
private readonly StrategyParam<int> _emaLength;
private readonly StrategyParam<int> _cooldownBars;
private ExponentialMovingAverage _ema;
private readonly List<decimal> _closes = new();
private int _belowCount;
private decimal _prevHigh;
private int _cooldownRemaining;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int BuyThreshold { get => _buyThreshold.Value; set => _buyThreshold.Value = value; }
public int Lookback { get => _lookback.Value; set => _lookback.Value = value; }
public int EmaLength { get => _emaLength.Value; set => _emaLength.Value = value; }
public int CooldownBars { get => _cooldownBars.Value; set => _cooldownBars.Value = value; }
public FourBarMomentumReversalStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_buyThreshold = Param(nameof(BuyThreshold), 3)
.SetGreaterThanZero()
.SetDisplay("Buy Threshold", "Consecutive closes below reference to trigger buy", "Strategy");
_lookback = Param(nameof(Lookback), 4)
.SetGreaterThanZero()
.SetDisplay("Lookback", "Number of bars to compare", "Strategy");
_emaLength = Param(nameof(EmaLength), 20)
.SetGreaterThanZero()
.SetDisplay("EMA Length", "EMA trend filter period", "Indicators");
_cooldownBars = Param(nameof(CooldownBars), 15)
.SetDisplay("Cooldown Bars", "Bars between trades", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_ema = null;
_closes.Clear();
_belowCount = 0;
_prevHigh = 0;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_ema = new ExponentialMovingAverage { Length = EmaLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_ema, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ema);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal emaVal)
{
if (candle.State != CandleStates.Finished)
return;
if (!_ema.IsFormed)
{
_closes.Add(candle.ClosePrice);
_prevHigh = candle.HighPrice;
return;
}
var close = candle.ClosePrice;
// Track past closes for lookback comparison
if (_closes.Count >= Lookback)
{
var pastClose = _closes[_closes.Count - Lookback];
if (close < pastClose)
_belowCount++;
else
_belowCount = 0;
}
_closes.Add(close);
// Keep list from growing too large
if (_closes.Count > Lookback + 10)
_closes.RemoveAt(0);
if (!IsFormedAndOnlineAndAllowTrading())
{
_prevHigh = candle.HighPrice;
return;
}
if (_cooldownRemaining > 0)
{
_cooldownRemaining--;
_prevHigh = candle.HighPrice;
return;
}
// Buy: consecutive closes below reference + price below EMA (reversal from weakness)
if (_belowCount >= BuyThreshold && Position <= 0)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
BuyMarket(Volume);
_cooldownRemaining = CooldownBars;
}
// Exit long: breakout above previous high
else if (Position > 0 && close > _prevHigh)
{
SellMarket(Math.Abs(Position));
_cooldownRemaining = CooldownBars;
}
// Short: consecutive closes above reference (overbought reversal)
else if (_belowCount == 0 && _closes.Count > Lookback)
{
var pastClose = _closes[_closes.Count - 1 - Lookback];
var aboveCount = 0;
for (int i = _closes.Count - 1; i >= Math.Max(0, _closes.Count - BuyThreshold); i--)
{
if (_closes[i] > pastClose)
aboveCount++;
else
break;
}
if (aboveCount >= BuyThreshold && Position >= 0 && close > emaVal)
{
if (Position > 0)
SellMarket(Math.Abs(Position));
SellMarket(Volume);
_cooldownRemaining = CooldownBars;
}
}
_prevHigh = candle.HighPrice;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class four_bar_momentum_reversal_strategy(Strategy):
"""Four Bar Momentum Reversal Strategy."""
def __init__(self):
super(four_bar_momentum_reversal_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._buy_threshold = self.Param("BuyThreshold", 3) \
.SetDisplay("Buy Threshold", "Consecutive closes below reference to trigger buy", "Strategy")
self._lookback = self.Param("Lookback", 4) \
.SetDisplay("Lookback", "Number of bars to compare", "Strategy")
self._ema_length = self.Param("EmaLength", 20) \
.SetDisplay("EMA Length", "EMA trend filter period", "Indicators")
self._cooldown_bars = self.Param("CooldownBars", 15) \
.SetDisplay("Cooldown Bars", "Bars between trades", "Risk")
self._ema = None
self._closes = []
self._below_count = 0
self._prev_high = 0.0
self._cooldown_remaining = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(four_bar_momentum_reversal_strategy, self).OnReseted()
self._ema = None
self._closes = []
self._below_count = 0
self._prev_high = 0.0
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(four_bar_momentum_reversal_strategy, self).OnStarted2(time)
self._ema = ExponentialMovingAverage()
self._ema.Length = int(self._ema_length.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._ema, self._on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._ema)
self.DrawOwnTrades(area)
def _on_process(self, candle, ema_val):
if candle.State != CandleStates.Finished:
return
if not self._ema.IsFormed:
self._closes.append(float(candle.ClosePrice))
self._prev_high = float(candle.HighPrice)
return
close = float(candle.ClosePrice)
lookback = int(self._lookback.Value)
# Track past closes for lookback comparison
if len(self._closes) >= lookback:
past_close = self._closes[len(self._closes) - lookback]
if close < past_close:
self._below_count += 1
else:
self._below_count = 0
self._closes.append(close)
if len(self._closes) > lookback + 10:
self._closes.pop(0)
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_high = float(candle.HighPrice)
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
self._prev_high = float(candle.HighPrice)
return
buy_threshold = int(self._buy_threshold.Value)
cooldown = int(self._cooldown_bars.Value)
ema_v = float(ema_val)
# Buy: consecutive closes below reference
if self._below_count >= buy_threshold and self.Position <= 0:
if self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
self.BuyMarket(self.Volume)
self._cooldown_remaining = cooldown
# Exit long: breakout above previous high
elif self.Position > 0 and close > self._prev_high:
self.SellMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown
# Short: consecutive closes above reference (overbought reversal)
elif self._below_count == 0 and len(self._closes) > lookback:
past_close = self._closes[len(self._closes) - 1 - lookback]
above_count = 0
start = len(self._closes) - 1
end = max(0, len(self._closes) - buy_threshold)
for i in range(start, end - 1, -1):
if self._closes[i] > past_close:
above_count += 1
else:
break
if above_count >= buy_threshold and self.Position >= 0 and close > ema_v:
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
self.SellMarket(self.Volume)
self._cooldown_remaining = cooldown
self._prev_high = float(candle.HighPrice)
def CreateClone(self):
return four_bar_momentum_reversal_strategy()