5-Minuten-Umschläge
Die 5Mins Envelopes-Strategie reproduziert den MetaTrader-Experten, der Fünf-Minuten-Kerzen um einen linear gewichteten Umschlag mit gleitendem Durchschnitt handelt. Es sucht nach Preisspitzen, die weit über die Bänder hinausgehen, und bewegt sich dann in die Richtung der Mean-Reversion. Ein Spread-Filter, ein statischer Stop-Loss, ein optionaler Take-Profit und ein Trailing-Stop spiegeln das ursprüngliche Geldmanagement wider.
Handelslogik
- Indikator: Linear Weighted Moving Average (LWMA), berechnet auf Basis des Medianpreises (Hoch+Tief)/2 mit einer Periode von 3.
- Hüllkurvenbreite: 0,05 % Abweichung vom LWMA-Wert (oberes und unteres Band).
- Signalerkennung (ausgewertet anhand der zuvor abgeschlossenen Kerze und des aktuellen Gebots):
- Long: Das vorherige Kerzentief bleibt mehr als
DistancePointsunter dem unteren Band und das aktuelle Gebot liegt ebenfalls außerhalb dieser Distanz. - Short: Das vorherige Kerzenhoch bleibt mehr als
DistancePointsüber dem oberen Band und das aktuelle Gebot liegt ebenfalls außerhalb dieser Distanz.
- Long: Das vorherige Kerzentief bleibt mehr als
- Filter:
- Immer nur eine Position (bei neuen Einträgen muss die aktuelle Position flach sein).
- Wenn
MaxSpreadPointsgrößer als Null ist, muss die Geld-Brief-Spanne unter diesem Schwellenwert bleiben, bevor eine neue Bestellung übermittelt wird.
Risikomanagement
- Ordervolumen: Der Parameter
TradeVolumesteuert die Marktordergröße. - Stop-Loss:
StopLossPointswird unter Verwendung der Tick-Größe des Instruments in einen absoluten Preisabstand umgerechnet. - Take-Profit: Optional
TakeProfitPoints; Zum Deaktivieren auf Null setzen. - Trailing Stop: Optional
TrailingStopPoints; Zum Deaktivieren auf Null setzen. - Schutz: Der
StartProtection-Helfer wendet alle Exits mit Marktaufträgen an und entspricht dem MetaTrader-Verhalten.
Parameter
TradeVolume = 1mDistancePoints = 140EnvelopePeriod = 3EnvelopeDeviationPercent = 0.05mStopLossPoints = 250TakeProfitPoints = 0TrailingStopPoints = 120MaxSpreadPoints = 25CandleType = TimeFrame(5 minutes)
Schlagworte
- Kategorie: Mean Reversion
- Richtung: Beide
- Indikatoren: WeightedMovingAverage
- Stopps: Ja (fest + nachlaufend)
- Zeitrahmen: Intraday (M5)
- Komplexität: Anfänger
- Risikostufe: Mittel
- Saisonalität: Nein
- Neuronale Netze: Nein
- Divergenz: Nein
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Mean reversion from LWMA envelopes of median candle price, using the last finished candle and live bid.
/// </summary>
public class FiveMinsEnvelopesStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<decimal> _distancePoints;
private readonly StrategyParam<int> _envelopePeriod;
private readonly StrategyParam<decimal> _envelopeDeviationPercent;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<decimal> _maxSpreadPoints;
private readonly List<decimal> _medianPrices = [];
private decimal? _previousLow;
private decimal? _previousHigh;
private decimal? _previousLowerBand;
private decimal? _previousUpperBand;
private decimal? _bestBid;
private decimal? _bestAsk;
private decimal? _entryPrice;
private decimal? _trailingStop;
private bool _entryRequested;
private bool _exitRequested;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public decimal TradeVolume { get => _tradeVolume.Value; set => _tradeVolume.Value = value; }
public decimal DistancePoints { get => _distancePoints.Value; set => _distancePoints.Value = value; }
public int EnvelopePeriod { get => _envelopePeriod.Value; set => _envelopePeriod.Value = value; }
public decimal EnvelopeDeviationPercent { get => _envelopeDeviationPercent.Value; set => _envelopeDeviationPercent.Value = value; }
public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
public decimal TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }
public decimal TrailingStopPoints { get => _trailingStopPoints.Value; set => _trailingStopPoints.Value = value; }
public decimal MaxSpreadPoints { get => _maxSpreadPoints.Value; set => _maxSpreadPoints.Value = value; }
public FiveMinsEnvelopesStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame());
_tradeVolume = Param(nameof(TradeVolume), 1m).SetGreaterThanZero();
_distancePoints = Param(nameof(DistancePoints), 140m).SetNotNegative();
_envelopePeriod = Param(nameof(EnvelopePeriod), 3).SetGreaterThanZero();
_envelopeDeviationPercent = Param(nameof(EnvelopeDeviationPercent), 0.05m).SetNotNegative();
_stopLossPoints = Param(nameof(StopLossPoints), 250m).SetNotNegative();
_takeProfitPoints = Param(nameof(TakeProfitPoints), 0m).SetNotNegative();
_trailingStopPoints = Param(nameof(TrailingStopPoints), 120m).SetNotNegative();
_maxSpreadPoints = Param(nameof(MaxSpreadPoints), 25m).SetNotNegative();
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetRuntime();
}
private void ResetRuntime()
{
_medianPrices.Clear();
_previousLow = null;
_previousHigh = null;
_previousLowerBand = null;
_previousUpperBand = null;
_bestBid = null;
_bestAsk = null;
_entryPrice = null;
_trailingStop = null;
_entryRequested = false;
_exitRequested = false;
}
private decimal Point()
{
var point = Security?.PriceStep ?? 0m;
if (point <= 0m)
throw new InvalidOperationException("FiveMinsEnvelopesStrategy needs positive Security.PriceStep.");
return point;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetRuntime();
var point = Point();
var takeProfit = TakeProfitPoints > 0m ? new Unit(TakeProfitPoints * point, UnitTypes.Absolute) : new Unit();
var stopLoss = StopLossPoints > 0m ? new Unit(StopLossPoints * point, UnitTypes.Absolute) : new Unit();
StartProtection(takeProfit, stopLoss, useMarketOrders: true, isLocalStop: true);
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ProcessLevel1).Start();
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_medianPrices.Add((candle.HighPrice + candle.LowPrice) / 2m);
while (_medianPrices.Count > EnvelopePeriod)
_medianPrices.RemoveAt(0);
if (_medianPrices.Count < EnvelopePeriod)
return;
var weighted = 0m;
var totalWeights = EnvelopePeriod * (EnvelopePeriod + 1) / 2m;
for (var i = 0; i < _medianPrices.Count; i++)
weighted += _medianPrices[i] * (i + 1);
var middle = weighted / totalWeights;
_previousLow = candle.LowPrice;
_previousHigh = candle.HighPrice;
_previousLowerBand = middle * (1m - EnvelopeDeviationPercent / 100m);
_previousUpperBand = middle * (1m + EnvelopeDeviationPercent / 100m);
}
private void ProcessLevel1(Level1ChangeMessage message)
{
if (message.TryGetDecimal(Level1Fields.BestBidPrice) is decimal bid && bid > 0m)
_bestBid = bid;
if (message.TryGetDecimal(Level1Fields.BestAskPrice) is decimal ask && ask > 0m)
_bestAsk = ask;
if (_bestBid is not decimal currentBid || _bestAsk is not decimal currentAsk || currentAsk < currentBid)
return;
if (Position != 0m)
{
ApplyTrailing();
return;
}
if (_entryRequested || _exitRequested || _previousLow is null || _previousHigh is null ||
_previousLowerBand is null || _previousUpperBand is null || TradeVolume <= 0m)
return;
var point = Point();
if (MaxSpreadPoints > 0m && currentAsk - currentBid >= MaxSpreadPoints * point)
return;
var distance = DistancePoints * point;
if (_previousLow.Value < _previousLowerBand.Value - distance &&
currentBid < _previousLowerBand.Value - distance)
{
_entryRequested = true;
BuyMarket(TradeVolume);
}
else if (_previousHigh.Value > _previousUpperBand.Value + distance &&
currentBid > _previousUpperBand.Value + distance)
{
_entryRequested = true;
SellMarket(TradeVolume);
}
}
private void ApplyTrailing()
{
if (_exitRequested || _entryPrice is null || TrailingStopPoints <= 0m)
return;
var price = Position > 0m ? _bestBid : _bestAsk;
if (price is null)
return;
var gap = TrailingStopPoints * Point();
if (Position > 0m)
{
if (price.Value >= _entryPrice.Value + gap)
{
var candidate = price.Value - gap;
if (_trailingStop is null || candidate > _trailingStop)
_trailingStop = candidate;
}
if (_trailingStop is decimal stop && price.Value <= stop)
RequestExit();
}
else
{
if (price.Value <= _entryPrice.Value - gap)
{
var candidate = price.Value + gap;
if (_trailingStop is null || candidate < _trailingStop)
_trailingStop = candidate;
}
if (_trailingStop is decimal stop && price.Value >= stop)
RequestExit();
}
}
private void RequestExit()
{
_exitRequested = true;
ClosePosition();
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (Position == 0m)
{
_entryPrice = null;
_trailingStop = null;
_entryRequested = false;
_exitRequested = false;
}
else if (_entryPrice is null)
{
_entryPrice = trade.Trade.Price;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates, Level1Fields, Unit, UnitTypes
from StockSharp.BusinessEntities import Subscription
from StockSharp.Algo.Strategies import Strategy
class five_mins_envelopes_strategy(Strategy):
def __init__(self):
super(five_mins_envelopes_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._trade_volume = self.Param("TradeVolume", 1.0)
self._distance_points = self.Param("DistancePoints", 140.0)
self._envelope_period = self.Param("EnvelopePeriod", 3)
self._envelope_deviation_percent = self.Param("EnvelopeDeviationPercent", 0.05)
self._stop_loss_points = self.Param("StopLossPoints", 250.0)
self._take_profit_points = self.Param("TakeProfitPoints", 0.0)
self._trailing_stop_points = self.Param("TrailingStopPoints", 120.0)
self._max_spread_points = self.Param("MaxSpreadPoints", 25.0)
self._reset_runtime()
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def TradeVolume(self):
return self._trade_volume.Value
@TradeVolume.setter
def TradeVolume(self, value):
self._trade_volume.Value = value
@property
def DistancePoints(self):
return self._distance_points.Value
@DistancePoints.setter
def DistancePoints(self, value):
self._distance_points.Value = value
@property
def EnvelopePeriod(self):
return self._envelope_period.Value
@EnvelopePeriod.setter
def EnvelopePeriod(self, value):
self._envelope_period.Value = value
@property
def EnvelopeDeviationPercent(self):
return self._envelope_deviation_percent.Value
@EnvelopeDeviationPercent.setter
def EnvelopeDeviationPercent(self, value):
self._envelope_deviation_percent.Value = value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@StopLossPoints.setter
def StopLossPoints(self, value):
self._stop_loss_points.Value = value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@TakeProfitPoints.setter
def TakeProfitPoints(self, value):
self._take_profit_points.Value = value
@property
def TrailingStopPoints(self):
return self._trailing_stop_points.Value
@TrailingStopPoints.setter
def TrailingStopPoints(self, value):
self._trailing_stop_points.Value = value
@property
def MaxSpreadPoints(self):
return self._max_spread_points.Value
@MaxSpreadPoints.setter
def MaxSpreadPoints(self, value):
self._max_spread_points.Value = value
def _reset_runtime(self):
self._median_prices = []
self._previous_low = None
self._previous_high = None
self._previous_lower_band = None
self._previous_upper_band = None
self._best_bid = None
self._best_ask = None
self._entry_price = None
self._trailing_stop = None
self._entry_requested = False
self._exit_requested = False
def OnReseted(self):
super(five_mins_envelopes_strategy, self).OnReseted()
self._reset_runtime()
def _point(self):
if self.Security is None or self.Security.PriceStep is None:
raise ValueError("Positive Security.PriceStep is required.")
point = float(self.Security.PriceStep)
if point <= 0:
raise ValueError("Positive Security.PriceStep is required.")
return point
def OnStarted2(self, time):
super(five_mins_envelopes_strategy, self).OnStarted2(time)
self._reset_runtime()
point = self._point()
stop = Unit(Decimal(self.StopLossPoints * point), UnitTypes.Absolute) if self.StopLossPoints > 0 else Unit()
take = Unit(Decimal(self.TakeProfitPoints * point), UnitTypes.Absolute) if self.TakeProfitPoints > 0 else Unit()
self.StartProtection(take, stop, useMarketOrders=True, isLocalStop=True)
self.SubscribeCandles(self.CandleType).Bind(self._process_candle).Start()
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._process_level1).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._median_prices.append((float(candle.HighPrice) + float(candle.LowPrice)) / 2.0)
while len(self._median_prices) > self.EnvelopePeriod:
self._median_prices.pop(0)
if len(self._median_prices) < self.EnvelopePeriod:
return
weights = list(range(1, self.EnvelopePeriod + 1))
middle = sum(value * weight for value, weight in zip(self._median_prices, weights)) / sum(weights)
deviation = float(self.EnvelopeDeviationPercent) / 100.0
self._previous_low = float(candle.LowPrice)
self._previous_high = float(candle.HighPrice)
self._previous_lower_band = middle * (1.0 - deviation)
self._previous_upper_band = middle * (1.0 + deviation)
def _process_level1(self, message):
bid = message.Changes[Level1Fields.BestBidPrice] if message.Changes.ContainsKey(Level1Fields.BestBidPrice) else None
ask = message.Changes[Level1Fields.BestAskPrice] if message.Changes.ContainsKey(Level1Fields.BestAskPrice) else None
if bid is not None and bid > 0:
self._best_bid = float(bid)
if ask is not None and ask > 0:
self._best_ask = float(ask)
current_bid = self._best_bid
current_ask = self._best_ask
if current_bid is None or current_ask is None or current_ask < current_bid:
return
if self.Position != 0:
self._apply_trailing()
return
if (self._entry_requested or self._exit_requested or self._previous_low is None
or self._previous_high is None or self._previous_lower_band is None
or self._previous_upper_band is None or self.TradeVolume <= 0):
return
point = self._point()
if self.MaxSpreadPoints > 0 and current_ask - current_bid >= self.MaxSpreadPoints * point:
return
distance = float(self.DistancePoints) * point
volume = Decimal(float(self.TradeVolume))
if (self._previous_low < self._previous_lower_band - distance
and current_bid < self._previous_lower_band - distance):
self._entry_requested = True
self.BuyMarket(volume)
elif (self._previous_high > self._previous_upper_band + distance
and current_bid > self._previous_upper_band + distance):
self._entry_requested = True
self.SellMarket(volume)
def _apply_trailing(self):
if self._exit_requested or self._entry_price is None or self.TrailingStopPoints <= 0:
return
price = self._best_bid if self.Position > 0 else self._best_ask
if price is None:
return
gap = float(self.TrailingStopPoints) * self._point()
if self.Position > 0:
if price >= self._entry_price + gap:
candidate = price - gap
if self._trailing_stop is None or candidate > self._trailing_stop:
self._trailing_stop = candidate
if self._trailing_stop is not None and price <= self._trailing_stop:
self._request_exit()
else:
if price <= self._entry_price - gap:
candidate = price + gap
if self._trailing_stop is None or candidate < self._trailing_stop:
self._trailing_stop = candidate
if self._trailing_stop is not None and price >= self._trailing_stop:
self._request_exit()
def _request_exit(self):
self._exit_requested = True
self.ClosePosition()
def OnOwnTradeReceived(self, trade):
super(five_mins_envelopes_strategy, self).OnOwnTradeReceived(trade)
if self.Position == 0:
self._entry_price = None
self._trailing_stop = None
self._entry_requested = False
self._exit_requested = False
elif self._entry_price is None:
self._entry_price = float(trade.Trade.TradePrice)
def CreateClone(self):
return five_mins_envelopes_strategy()