Estrategia MA Deviation
Estrategia que opera cuando el precio se desvía significativamente de su media móvil
Las pruebas indican un rendimiento anual promedio de aproximadamente 124%. Funciona mejor en el mercado forex.
MA Deviation entra cuando el precio se desvía un porcentaje establecido de su media móvil, anticipando un retorno a la media. La posición se cierra cuando el precio converge de regreso hacia el promedio.
Los umbrales de desviación pueden ampliarse o reducirse según la volatilidad. Usar ATR para el dimensionamiento de posiciones mantiene el riesgo consistente en todos los mercados.
Detalles
- Criterios de entrada: Señales basadas en MA, ATR.
- Largo/Corto: Ambos direcciones.
- Criterios de salida: Señal opuesta o stop.
- Stops: Sí.
- Valores predeterminados:
MAPeriod= 20DeviationPercent= 5mAtrPeriod= 14AtrMultiplier= 2.0mCandleType= TimeSpan.FromMinutes(5)
- Filtros:
- Categoría: Tendencia
- Dirección: Ambos
- Indicadores: MA, ATR
- Stops: Sí
- Complejidad: Básico
- Marco temporal: Intradía (5m)
- Estacionalidad: No
- Redes neuronales: No
- Divergencia: No
- Nivel de riesgo: Medio
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>
/// Strategy that trades when price deviates significantly from its moving average.
/// Opens positions when price deviates by a specified percentage from MA
/// and closes when price returns to MA.
/// </summary>
public class MADeviationStrategy : Strategy
{
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<decimal> _deviationPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<decimal> _riskPercent;
private Order _pendingOrder;
private Unit _stopDistance;
private bool _protectionStarted;
/// <summary>
/// Period for Moving Average calculation.
/// </summary>
public int MAPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Deviation percentage from MA required for entry.
/// </summary>
public decimal DeviationPercent
{
get => _deviationPercent.Value;
set => _deviationPercent.Value = value;
}
/// <summary>
/// Type of candles used for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize the MA Deviation strategy.
/// </summary>
public MADeviationStrategy()
{
_maPeriod = Param(nameof(MAPeriod), 20).SetGreaterThanZero()
.SetDisplay("MA Period", "Period for Moving Average calculation", "Indicators")
.SetOptimize(10, 50, 5);
_deviationPercent = Param(nameof(DeviationPercent), 5m).SetGreaterThanZero()
.SetDisplay("Deviation %", "Deviation percentage from MA required for entry", "Entry")
.SetOptimize(1m, 10m, 1m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_atrPeriod = Param(nameof(AtrPeriod), 14).SetGreaterThanZero()
.SetDisplay("ATR Period", "Wilder ATR lookback for sizing and protection.", "Protection");
_atrMultiplier = Param(nameof(AtrMultiplier), 2m).SetNotNegative()
.SetDisplay("ATR Multiplier", "Frozen entry ATR distance; zero disables stop and ATR sizing.", "Protection");
_riskPercent = Param(nameof(RiskPercent), 1m).SetGreaterThanZero()
.SetDisplay("Risk (%)", "Portfolio value percentage budgeted against the entry ATR stop.", "Protection");
OrderRegistering += order => _pendingOrder = order;
}
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public decimal AtrMultiplier { get => _atrMultiplier.Value; set => _atrMultiplier.Value = value; }
public decimal RiskPercent { get => _riskPercent.Value; set => _riskPercent.Value = value; }
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType), (Security, DataType.Level1)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pendingOrder = null;
_stopDistance = null;
_protectionStarted = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var sma = new SimpleMovingAverage { Length = MAPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
}
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(sma, atr, ProcessCandle, false)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawIndicator(area, atr);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// Native protection runs before the callback, including between finished candles.
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue maValue, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!maValue.IsFormed || !atrValue.IsFormed || !IsFormedAndOnlineAndAllowTrading())
return;
if (_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
var mean = maValue.GetValue<decimal>();
if (mean <= 0m)
return;
var deviation = 100m * (candle.ClosePrice - mean) / mean;
if (Position > 0m && candle.ClosePrice >= mean)
SellMarket(Position);
else if (Position < 0m && candle.ClosePrice <= mean)
BuyMarket(Math.Abs(Position));
else if (Position == 0m && Math.Abs(deviation) > DeviationPercent)
Enter(deviation < 0m ? Sides.Buy : Sides.Sell, candle.ClosePrice, atrValue.GetValue<decimal>());
}
private void Enter(Sides side, decimal close, decimal atr)
{
var distance = atr * AtrMultiplier;
var volume = CalculateVolume(close, distance);
if (volume <= 0m)
return;
_stopDistance ??= new Unit(distance);
_stopDistance.Value = distance;
if (!_protectionStarted && distance > 0m)
{
StartProtection(new Unit(), _stopDistance, useMarketOrders: true, isLocalStop: true);
_protectionStarted = true;
}
RegisterOrder(new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = side,
Volume = volume,
Comment = "MA deviation entry",
});
}
private decimal CalculateVolume(decimal close, decimal distance)
{
if (AtrMultiplier == 0m)
return NormalizeVolume(Volume);
var balance = Portfolio.CurrentValue ?? Portfolio.BeginValue ?? 0m;
var stepPrice = Security.StepPrice ?? 0m;
var priceStep = Security.PriceStep ?? 0m;
var moneyFactor = stepPrice > 0m && priceStep > 0m ? stepPrice / priceStep : Security.Multiplier ?? 1m;
if (balance <= 0m || distance <= 0m || close <= 0m || moneyFactor <= 0m)
return 0m;
var budget = balance * RiskPercent / 100m;
var riskVolume = budget / (distance * moneyFactor);
var cashVolume = balance / (close * moneyFactor);
return NormalizeVolume(Math.Min(riskVolume, cashVolume));
}
private decimal NormalizeVolume(decimal volume)
{
var minimum = Math.Max(0m, Security.MinVolume ?? 0m);
var maximum = Security.MaxVolume is decimal max && max > 0m ? max : decimal.MaxValue;
volume = Math.Min(volume, maximum);
if (Security.VolumeStep is decimal step && step > 0m)
{
minimum = Math.Ceiling(minimum / step) * step;
if (maximum != decimal.MaxValue) maximum = Math.Floor(maximum / step) * step;
volume = Math.Floor(volume / step) * step;
}
// Never round UP through the risk budget to force a minimum-size trade.
return minimum <= maximum && volume >= minimum ? volume : 0m;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal, Math
from StockSharp.Messages import DataType, CandleStates, Unit, OrderStates, OrderTypes, Sides, Level1Fields
from StockSharp.BusinessEntities import Subscription, Order
from StockSharp.Algo.Indicators import SimpleMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class ma_deviation_strategy(Strategy):
"""
MA Deviation strategy.
Trades when price deviates significantly from its moving average.
"""
def __init__(self):
super(ma_deviation_strategy, self).__init__()
self._ma_period = self.Param("MAPeriod", 20).SetGreaterThanZero().SetDisplay("MA Period", "Period for Moving Average calculation", "Indicators")
self._deviation_percent = self.Param("DeviationPercent", 5.0).SetGreaterThanZero().SetDisplay("Deviation %", "Deviation percentage from MA required for entry", "Entry")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._atr_period = self.Param("AtrPeriod", 14).SetGreaterThanZero() \
.SetDisplay("ATR Period", "Wilder ATR lookback for sizing and protection.", "Protection")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0).SetNotNegative() \
.SetDisplay("ATR Multiplier", "Frozen entry ATR distance; zero disables stop and ATR sizing.", "Protection")
self._risk_percent = self.Param("RiskPercent", 1.0).SetGreaterThanZero() \
.SetDisplay("Risk (%)", "Portfolio value percentage budgeted against the entry ATR stop.", "Protection")
self._pending_order = None
self._stop_distance = None
self._protection_started = False
self.OrderRegistering += self._track_pending
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def _track_pending(self, order):
self._pending_order = order
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(ma_deviation_strategy, self).OnReseted()
self._pending_order = None
self._stop_distance = None
self._protection_started = False
def OnStarted2(self, time):
super(ma_deviation_strategy, self).OnStarted2(time)
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_protection_quote).Start()
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(sma, atr, self._process_candle, False).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawIndicator(area, atr)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# Native protection runs before this callback, including between signal bars.
pass
def _process_candle(self, candle, ma_val, atr_val):
if candle.State != CandleStates.Finished:
return
if not ma_val.IsFormed or not atr_val.IsFormed or not self.IsFormedAndOnlineAndAllowTrading():
return
if self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed):
return
mean = ma_val.GetValue[Decimal](None)
if mean <= 0:
return
close = candle.ClosePrice
deviation = Decimal(100) * (close - mean) / mean
if self.Position > 0 and close >= mean:
self.SellMarket(self.Position)
elif self.Position < 0 and close <= mean:
self.BuyMarket(Math.Abs(self.Position))
elif self.Position == 0 and Math.Abs(deviation) > Decimal(self._deviation_percent.Value):
self._enter(Sides.Buy if deviation < 0 else Sides.Sell, close, atr_val.GetValue[Decimal](None))
def _enter(self, side, close, atr):
distance = atr * Decimal(self._atr_multiplier.Value)
volume = self._calculate_volume(close, distance)
if volume <= 0:
return
if self._stop_distance is None:
self._stop_distance = Unit(distance)
self._stop_distance.Value = distance
if not self._protection_started and distance > 0:
self.StartProtection(Unit(), self._stop_distance, useMarketOrders=True, isLocalStop=True)
self._protection_started = True
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = volume
order.Comment = "MA deviation entry"
self.RegisterOrder(order)
def _calculate_volume(self, close, distance):
if self._atr_multiplier.Value == 0:
return self._normalize_volume(self.Volume)
balance = self.Portfolio.CurrentValue
if balance is None:
balance = self.Portfolio.BeginValue
if balance is None:
balance = Decimal.Zero
step_price = self.Security.StepPrice if self.Security.StepPrice is not None else Decimal.Zero
price_step = self.Security.PriceStep if self.Security.PriceStep is not None else Decimal.Zero
money_factor = step_price / price_step if step_price > 0 and price_step > 0 else self.Security.Multiplier
if money_factor is None:
money_factor = Decimal.One
if balance <= 0 or distance <= 0 or close <= 0 or money_factor <= 0:
return Decimal.Zero
budget = balance * Decimal(self._risk_percent.Value) / Decimal(100)
risk_volume = budget / (distance * money_factor)
cash_volume = balance / (close * money_factor)
return self._normalize_volume(Math.Min(risk_volume, cash_volume))
def _normalize_volume(self, volume):
minimum = self.Security.MinVolume if self.Security.MinVolume is not None else Decimal.Zero
minimum = Math.Max(Decimal.Zero, minimum)
maximum = self.Security.MaxVolume
if maximum is None or maximum <= 0:
maximum = Decimal.MaxValue
volume = Math.Min(volume, maximum)
step = self.Security.VolumeStep
if step is not None and step > 0:
minimum = Math.Ceiling(minimum / step) * step
if maximum != Decimal.MaxValue:
maximum = Math.Floor(maximum / step) * step
volume = Math.Floor(volume / step) * step
# Never round up through the risk budget to force a minimum-size trade.
return volume if minimum <= maximum and volume >= minimum else Decimal.Zero
def CreateClone(self):
return ma_deviation_strategy()