NASDAQ 100 Peak Hours 策略
该策略仅在交易日的前两小时和最后一小时交易纳斯达克100指数。使用 EMA 趋势确认、RSI、ATR 与 VWAP 过滤,并结合基于 ATR 的移动止损和保本止损。
详情
- 入场条件:
- 多头:价格高于快 EMA,快 EMA 高于慢 EMA,二者均上升,RSI 高于 50,价格高于 VWAP,且处于峰值时段。
- 空头:相反条件。
- 多空方向:多空皆可。
- 出场条件:
- ATR 移动止损或保本止损。
- 持仓超过设定的K线数量或 EMA 趋势反转。
- 止损:基于 ATR 的移动止损并可转为保本。
- 默认参数:
Long EMA= 21Short EMA= 9RSI= 14ATR= 14Trail ATR Mult= 1.5Initial SL Mult= 0.5Break-even ATR Mult= 1.5Time Exit Bars= 20
- 过滤器:
- 分类:日内
- 方向:双向
- 指标:EMA, RSI, ATR, VWAP
- 止损:移动止损
- 复杂度:高级
- 时间框架:任意
- 季节性:无
- 神经网络:无
- 背离:无
- 风险等级:中等
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// NASDAQ 100 peak hours strategy.
/// Trades only in the first two hours and the last hour of the cash session. A long opens when the close is above the short EMA,
/// the short EMA is above the long EMA, both EMAs are rising, RSI is above 50 and the close is above the session VWAP; a short uses
/// the opposite conditions. The position is protected by an initial ATR stop that moves to break-even and then trails by an ATR
/// multiple, and it is closed after TimeExitBars bars or when the EMA trend reverses.
/// </summary>
public class Nasdaq100PeakHoursStrategy : Strategy
{
// Regular NASDAQ session (09:30-16:00 New York) expressed in UTC.
private static readonly TimeSpan _sessionStart = new(13, 30, 0);
private static readonly TimeSpan _sessionEnd = new(20, 0, 0);
private readonly StrategyParam<int> _longEmaLength;
private readonly StrategyParam<int> _shortEmaLength;
private readonly StrategyParam<int> _rsiLength;
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<decimal> _trailAtrMult;
private readonly StrategyParam<decimal> _initialSlMult;
private readonly StrategyParam<decimal> _breakEvenAtrMult;
private readonly StrategyParam<int> _timeExitBars;
private readonly StrategyParam<DataType> _candleType;
private decimal? _prevShort;
private decimal? _prevLong;
private DateTime _vwapDate;
private decimal _vwapPriceVolume;
private decimal _vwapVolume;
private decimal _entryPrice;
private decimal _entryAtr;
private decimal _stopPrice;
private decimal _bestPrice;
private int _barsInPosition;
/// <summary>
/// Long EMA period.
/// </summary>
public int LongEmaLength
{
get => _longEmaLength.Value;
set => _longEmaLength.Value = value;
}
/// <summary>
/// Short EMA period.
/// </summary>
public int ShortEmaLength
{
get => _shortEmaLength.Value;
set => _shortEmaLength.Value = value;
}
/// <summary>
/// RSI period.
/// </summary>
public int RsiLength
{
get => _rsiLength.Value;
set => _rsiLength.Value = value;
}
/// <summary>
/// ATR period.
/// </summary>
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
/// <summary>
/// ATR multiple of the trailing stop.
/// </summary>
public decimal TrailAtrMult
{
get => _trailAtrMult.Value;
set => _trailAtrMult.Value = value;
}
/// <summary>
/// ATR multiple of the initial stop.
/// </summary>
public decimal InitialSlMult
{
get => _initialSlMult.Value;
set => _initialSlMult.Value = value;
}
/// <summary>
/// ATR profit that moves the stop to break-even.
/// </summary>
public decimal BreakEvenAtrMult
{
get => _breakEvenAtrMult.Value;
set => _breakEvenAtrMult.Value = value;
}
/// <summary>
/// Bars after which an open position is closed.
/// </summary>
public int TimeExitBars
{
get => _timeExitBars.Value;
set => _timeExitBars.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public Nasdaq100PeakHoursStrategy()
{
_longEmaLength = Param(nameof(LongEmaLength), 21)
.SetGreaterThanZero()
.SetDisplay("Long EMA", "Long EMA period", "Indicators");
_shortEmaLength = Param(nameof(ShortEmaLength), 9)
.SetGreaterThanZero()
.SetDisplay("Short EMA", "Short EMA period", "Indicators");
_rsiLength = Param(nameof(RsiLength), 14)
.SetGreaterThanZero()
.SetDisplay("RSI", "RSI period", "Indicators");
_atrLength = Param(nameof(AtrLength), 14)
.SetGreaterThanZero()
.SetDisplay("ATR", "ATR period", "Indicators");
_trailAtrMult = Param(nameof(TrailAtrMult), 1.5m)
.SetNotNegative()
.SetDisplay("Trail ATR Mult", "ATR multiple of the trailing stop", "Risk");
_initialSlMult = Param(nameof(InitialSlMult), 0.5m)
.SetNotNegative()
.SetDisplay("Initial SL Mult", "ATR multiple of the initial stop", "Risk");
_breakEvenAtrMult = Param(nameof(BreakEvenAtrMult), 1.5m)
.SetNotNegative()
.SetDisplay("Break-even ATR Mult", "ATR profit that moves the stop to break-even", "Risk");
_timeExitBars = Param(nameof(TimeExitBars), 20)
.SetGreaterThanZero()
.SetDisplay("Time Exit Bars", "Bars after which an open position is closed", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
private void ResetState()
{
_prevShort = null;
_prevLong = null;
_vwapDate = default;
_vwapPriceVolume = 0m;
_vwapVolume = 0m;
_entryPrice = 0m;
_entryAtr = 0m;
_stopPrice = 0m;
_bestPrice = 0m;
_barsInPosition = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
var shortEma = new ExponentialMovingAverage { Length = ShortEmaLength };
var longEma = new ExponentialMovingAverage { Length = LongEmaLength };
var rsi = new RelativeStrengthIndex { Length = RsiLength };
var atr = new AverageTrueRange { Length = AtrLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(shortEma, longEma, rsi, atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, shortEma);
DrawIndicator(area, longEma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal shortValue, decimal longValue, decimal rsiValue, decimal atrValue)
{
if (candle.State != CandleStates.Finished)
return;
// Session VWAP restarts every UTC day.
var date = candle.OpenTime.Date;
if (date != _vwapDate)
{
_vwapDate = date;
_vwapPriceVolume = 0m;
_vwapVolume = 0m;
}
var typical = (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m;
_vwapPriceVolume += typical * candle.TotalVolume;
_vwapVolume += candle.TotalVolume;
var prevShort = _prevShort;
var prevLong = _prevLong;
_prevShort = shortValue;
_prevLong = longValue;
if (prevShort is not decimal lastShort || prevLong is not decimal lastLong)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (ManagePosition(candle, shortValue, longValue, atrValue))
return;
if (_vwapVolume <= 0 || atrValue <= 0 || !IsPeakHour(candle.OpenTime.TimeOfDay))
return;
var close = candle.ClosePrice;
var vwap = _vwapPriceVolume / _vwapVolume;
var rising = shortValue > lastShort && longValue > lastLong;
var falling = shortValue < lastShort && longValue < lastLong;
if (close > shortValue && shortValue > longValue && rising && rsiValue > 50 && close > vwap && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
OnEntry(close, atrValue, true);
}
else if (close < shortValue && shortValue < longValue && falling && rsiValue < 50 && close < vwap && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
OnEntry(close, atrValue, false);
}
}
private static bool IsPeakHour(TimeSpan time)
{
var openingWindow = time >= _sessionStart && time < _sessionStart + TimeSpan.FromHours(2);
var closingWindow = time >= _sessionEnd - TimeSpan.FromHours(1) && time < _sessionEnd;
return openingWindow || closingWindow;
}
private void OnEntry(decimal price, decimal atr, bool isLong)
{
_entryPrice = price;
_entryAtr = atr;
_bestPrice = price;
_barsInPosition = 0;
_stopPrice = isLong ? price - atr * InitialSlMult : price + atr * InitialSlMult;
}
private bool ManagePosition(ICandleMessage candle, decimal shortValue, decimal longValue, decimal atr)
{
if (Position == 0)
return false;
_barsInPosition++;
if (Position > 0)
{
if (candle.LowPrice <= _stopPrice || _barsInPosition >= TimeExitBars || shortValue < longValue)
{
SellMarket(Position);
return true;
}
_bestPrice = Math.Max(_bestPrice, candle.HighPrice);
if (_bestPrice - _entryPrice >= _entryAtr * BreakEvenAtrMult)
_stopPrice = Math.Max(_stopPrice, _entryPrice);
_stopPrice = Math.Max(_stopPrice, _bestPrice - atr * TrailAtrMult);
}
else
{
if (candle.HighPrice >= _stopPrice || _barsInPosition >= TimeExitBars || shortValue > longValue)
{
BuyMarket(-Position);
return true;
}
_bestPrice = Math.Min(_bestPrice, candle.LowPrice);
if (_entryPrice - _bestPrice >= _entryAtr * BreakEvenAtrMult)
_stopPrice = Math.Min(_stopPrice, _entryPrice);
_stopPrice = Math.Min(_stopPrice, _bestPrice + atr * TrailAtrMult);
}
return false;
}
}
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 ExponentialMovingAverage, RelativeStrengthIndex, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
# Regular NASDAQ session (09:30-16:00 New York) expressed in UTC minutes.
SESSION_START = 13 * 60 + 30
SESSION_END = 20 * 60
class nasdaq_100_peak_hours_strategy(Strategy):
"""
NASDAQ 100 peak hours strategy.
Trades only in the first two hours and the last hour of the cash session. A long opens when the close is above the short EMA,
the short EMA is above the long EMA, both EMAs are rising, RSI is above 50 and the close is above the session VWAP; a short uses
the opposite conditions. The position is protected by an initial ATR stop that moves to break-even and then trails by an ATR
multiple, and it is closed after TimeExitBars bars or when the EMA trend reverses.
"""
def __init__(self):
super(nasdaq_100_peak_hours_strategy, self).__init__()
self._long_ema_length = self.Param("LongEmaLength", 21).SetGreaterThanZero().SetDisplay("Long EMA", "Long EMA period", "Indicators")
self._short_ema_length = self.Param("ShortEmaLength", 9).SetGreaterThanZero().SetDisplay("Short EMA", "Short EMA period", "Indicators")
self._rsi_length = self.Param("RsiLength", 14).SetGreaterThanZero().SetDisplay("RSI", "RSI period", "Indicators")
self._atr_length = self.Param("AtrLength", 14).SetGreaterThanZero().SetDisplay("ATR", "ATR period", "Indicators")
self._trail_atr_mult = self.Param("TrailAtrMult", 1.5).SetNotNegative().SetDisplay("Trail ATR Mult", "ATR multiple of the trailing stop", "Risk")
self._initial_sl_mult = self.Param("InitialSlMult", 0.5).SetNotNegative().SetDisplay("Initial SL Mult", "ATR multiple of the initial stop", "Risk")
self._break_even_atr_mult = self.Param("BreakEvenAtrMult", 1.5).SetNotNegative().SetDisplay("Break-even ATR Mult", "ATR profit that moves the stop to break-even", "Risk")
self._time_exit_bars = self.Param("TimeExitBars", 20).SetGreaterThanZero().SetDisplay("Time Exit Bars", "Bars after which an open position is closed", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._prev_short = None
self._prev_long = None
self._vwap_date = None
self._vwap_price_volume = 0.0
self._vwap_volume = 0.0
self._entry_price = 0.0
self._entry_atr = 0.0
self._stop_price = 0.0
self._best_price = 0.0
self._bars_in_position = 0
def OnReseted(self):
super(nasdaq_100_peak_hours_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(nasdaq_100_peak_hours_strategy, self).OnStarted2(time)
self._reset_state()
short_ema = ExponentialMovingAverage()
short_ema.Length = self._short_ema_length.Value
long_ema = ExponentialMovingAverage()
long_ema.Length = self._long_ema_length.Value
rsi = RelativeStrengthIndex()
rsi.Length = self._rsi_length.Value
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(short_ema, long_ema, rsi, atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, short_ema)
self.DrawIndicator(area, long_ema)
self.DrawOwnTrades(area)
def _process_candle(self, candle, short_value, long_value, rsi_value, atr_value):
if candle.State != CandleStates.Finished:
return
# Session VWAP restarts every UTC day.
open_time = candle.OpenTime
date = open_time.Date
if self._vwap_date is None or date != self._vwap_date:
self._vwap_date = date
self._vwap_price_volume = 0.0
self._vwap_volume = 0.0
volume = float(candle.TotalVolume)
typical = (float(candle.HighPrice) + float(candle.LowPrice) + float(candle.ClosePrice)) / 3.0
self._vwap_price_volume += typical * volume
self._vwap_volume += volume
short_ema = float(short_value)
long_ema = float(long_value)
last_short = self._prev_short
last_long = self._prev_long
self._prev_short = short_ema
self._prev_long = long_ema
if last_short is None or last_long is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
atr = float(atr_value)
if self._manage_position(candle, short_ema, long_ema, atr):
return
if self._vwap_volume <= 0 or atr <= 0 or not self._is_peak_hour(open_time):
return
close = float(candle.ClosePrice)
rsi = float(rsi_value)
vwap = self._vwap_price_volume / self._vwap_volume
rising = short_ema > last_short and long_ema > last_long
falling = short_ema < last_short and long_ema < last_long
if close > short_ema and short_ema > long_ema and rising and rsi > 50 and close > vwap and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
self._on_entry(close, atr, True)
elif close < short_ema and short_ema < long_ema and falling and rsi < 50 and close < vwap and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
self._on_entry(close, atr, False)
def _is_peak_hour(self, open_time):
minutes = open_time.Hour * 60 + open_time.Minute
opening_window = SESSION_START <= minutes < SESSION_START + 120
closing_window = SESSION_END - 60 <= minutes < SESSION_END
return opening_window or closing_window
def _on_entry(self, price, atr, is_long):
self._entry_price = price
self._entry_atr = atr
self._best_price = price
self._bars_in_position = 0
offset = atr * float(self._initial_sl_mult.Value)
self._stop_price = price - offset if is_long else price + offset
def _manage_position(self, candle, short_ema, long_ema, atr):
if self.Position == 0:
return False
self._bars_in_position += 1
high = float(candle.HighPrice)
low = float(candle.LowPrice)
trail = atr * float(self._trail_atr_mult.Value)
break_even = self._entry_atr * float(self._break_even_atr_mult.Value)
time_exit = self._bars_in_position >= self._time_exit_bars.Value
if self.Position > 0:
if low <= self._stop_price or time_exit or short_ema < long_ema:
self.SellMarket(self.Position)
return True
self._best_price = max(self._best_price, high)
if self._best_price - self._entry_price >= break_even:
self._stop_price = max(self._stop_price, self._entry_price)
self._stop_price = max(self._stop_price, self._best_price - trail)
else:
if high >= self._stop_price or time_exit or short_ema > long_ema:
self.BuyMarket(-self.Position)
return True
self._best_price = min(self._best_price, low)
if self._entry_price - self._best_price >= break_even:
self._stop_price = min(self._stop_price, self._entry_price)
self._stop_price = min(self._stop_price, self._best_price + trail)
return False
def CreateClone(self):
return nasdaq_100_peak_hours_strategy()