Sobres de 5 minutos
La estrategia Sobres de 5 minutos reproduce el experto MetaTrader que intercambia velas de cinco minutos alrededor de una envolvente de media móvil ponderada lineal. Busca picos de precios que se extienden mucho más allá de las bandas y luego entra en la dirección de reversión a la media. Un filtro de diferencial, un stop-loss estático, un take-profit opcional y un trailing stop reflejan la gestión del dinero original.
Lógica de trading
- Indicador: Media Móvil Lineal Ponderada (LWMA) calculada sobre el precio medio (alto+bajo)/2 con un periodo de 3.
- Ancho del sobre: 0,05% de desviación del valor LWMA (bandas superior e inferior).
- Detección de señal (evaluada según la vela completada anteriormente y la oferta actual):
- Largo: el mínimo de la vela anterior se mantiene a más de
DistancePointspor debajo de la banda inferior y la oferta actual también está más allá de esa distancia. - Corto: el máximo de la vela anterior se mantiene a más de
DistancePointspor encima de la banda superior y la oferta actual también está más allá de esa distancia.
- Largo: el mínimo de la vela anterior se mantiene a más de
- Filtros:
- Sólo una posición a la vez (las nuevas entradas requieren que la posición actual sea plana).
- Si
MaxSpreadPointses mayor que cero, el diferencial entre oferta y demanda debe permanecer por debajo de este umbral antes de enviar un nuevo pedido.
Gestión del riesgo
- Volumen de orden: el parámetro
TradeVolumecontrola el tamaño de la orden de mercado. - Stop-loss:
StopLossPointsconvierte a distancia de precio absoluta utilizando el tamaño del tick del instrumento. - Take-profit: Optional
TakeProfitPoints; póngalo en cero para desactivarlo. - Trailing stop: Optional
TrailingStopPoints; póngalo en cero para desactivarlo. - Protección: El asistente
StartProtectionaplica todas las salidas con órdenes de mercado, coincidiendo con el comportamiento de MetaTrader.
Parámetros
TradeVolume = 1mDistancePoints = 140EnvelopePeriod = 3EnvelopeDeviationPercent = 0.05mStopLossPoints = 250TakeProfitPoints = 0TrailingStopPoints = 120MaxSpreadPoints = 25CandleType = TimeFrame(5 minutes)
Etiquetas
- Categoría: Reversión a la media
- Dirección: Ambos
- Indicators: WeightedMovingAverage
- Stops: Yes (fixed + trailing)
- Plazo: Intradiario (M5)
- Complejidad: Principiante
- Nivel de riesgo: medio
- Estacionalidad: No
- Redes neuronales: No
- Divergencia: No
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()