La Estrategia Fluctuante es un port de StockSharp del expert advisor de MetaTrader "Fluctuate". Reproduce el comportamiento similar a una cuadrícula del original usando la API de alto nivel: una suscripción de velas impulsa todas las decisiones, las entradas al mercado se realizan con BuyMarket / SellMarket, y las órdenes de recuperación se colocan con stop orders. La exposición larga y corta se rastrean por separado para imitar la contabilidad de posición estilo hedging usada en MetaTrader, mientras que la posición real de StockSharp permanece neta.
Idea central
Cada vez que cierra una nueva vela, la estrategia compara los últimos dos precios de cierre. Un cierre más alto abre una compra de mercado, un cierre más bajo abre una venta de mercado. Si ambos cierres son iguales la barra se ignora.
Cada posición ejecutada recibe un stop-loss y take-profit fijos (expresados en pips). La estrategia también registra el precio de ejecución exacto y el volumen neto añadido por el trade.
Después de una entrada, se activa una stop order opuesta a StepPips de distancia del último relleno (más un pequeño buffer de spread). Su volumen se deriva del trade anterior y el LotCoefficient, opcionalmente usando la exposición acumulada cuando MultiplyLotCoefficient = true.
Cuando la stop order se activa, cancela la orden pendiente anterior, actualiza las estadísticas de exposición interna e inmediatamente programa una nueva stop order de recuperación en la otra dirección. Esto reproduce el bucle de promediado/martingala presente en la implementación MQL.
La protección de trailing eleva (o baja) el stop una vez que el precio se mueve al menos TrailingStopPips + TrailingStepPips a favor de la posición. Esto emula el EA original que requería un buffer de beneficio adicional antes de apretar el stop.
Flujo de trading
Detección de señales. El feed de velas se suscribe vía SubscribeCandles. Solo se procesan velas terminadas. La estrategia se niega a operar fuera de la ventana de tiempo [StartHour, EndHour) o cuando se activa el guardián de capital.
Dimensionamiento inicial de posición. Dependiendo de PositionSizingMode, el primer trade en una secuencia usa un lote fijo (FixedVolume) o un lote basado en riesgo (RiskPercent). En modo de riesgo, el riesgo permitido (porcentaje del capital actual) se divide por la pérdida monetaria que ocurriría si se activa el stop-loss. El paso de precio y el precio de paso se usan para convertir pips a moneda.
Contabilidad de exposición. Acumuladores separados rastrean el volumen largo y corto, el precio promedio y el precio extremo alcanzado desde la entrada. Esto permite que la estrategia mantenga ambos lados "abiertos" internamente aunque StockSharp use netting.
Órdenes de recuperación. Después de cada ejecución, el algoritmo calcula el volumen de la siguiente stop order:
Cuando MultiplyLotCoefficient = false, el nuevo volumen equivale a LastVolume × LotCoefficient.
Cuando true, la exposición absoluta total se multiplica por LotCoefficient.
El volumen se normaliza a las restricciones del intercambio (paso, volumen mínimo y máximo) y se rechaza cuando excedería MaxTotalVolume o el número de posiciones activas más órdenes excedería MaxPositions.
Objetivo de beneficio y guardián de capital. La PnL no realizada agregada se calcula traduciendo diferencias de precio a moneda usando PriceStep/StepPrice. Si alcanza ProfitTarget, todas las posiciones se cierran y las órdenes pendientes se cancelan. El trading también se suspende cuando el capital cae por debajo de MinEquityPercent del saldo inicial.
Lógica de trailing. Para posiciones largas, se registra el precio más alto visto desde la entrada. Una vez que supera el precio de entrada en TrailingStopPips + TrailingStepPips, se establece un trailing stop TrailingStopPips detrás del máximo. Las posiciones cortas aplican la regla simétrica con el precio más bajo. Las actualizaciones de trailing anulan el stop-loss fijo.
Detalles de gestión del riesgo
Stop / take profit. Ambos son opcionales (establecer el valor en pips a cero para deshabilitar). Se recalculan para la exposición larga o corta agregada cada vez que un nuevo trade añade volumen.
Máx. posiciones. Cuenta el número de lados abiertos (largo + corto) más la stop order de recuperación activa. Cuando se alcanza el límite, la estrategia se niega a enviar nuevas stop orders.
Volumen total máximo. Limita la suma del volumen abierto absoluto y el volumen de la orden de recuperación activa.
CloseAllAtStart. Interruptor de seguridad opcional para aplanar el libro antes de que la estrategia empiece a operar.
Parámetros
Nombre
Descripción
Predeterminado
CandleType
Marco temporal principal usado para la detección de señales.
Marco temporal de 1 minuto
StopLossPips
Distancia entre el precio de entrada y el stop-loss (pips). 0 desactiva el stop.
50
TakeProfitPips
Distancia entre el precio de entrada y el take-profit (pips). 0 desactiva el take-profit.
50
TrailingStopPips
Distancia del trailing stop (pips). Requiere TrailingStepPips > 0.
5
TrailingStepPips
Beneficio adicional necesario antes de que avance el trailing stop (pips).
5
StepPips
Distancia entre el último relleno y el stop de recuperación opuesto (pips).
30
LotCoefficient
Multiplicador aplicado al volumen previo (o exposición total).
2.0
MultiplyLotCoefficient
Cuando true, el volumen de la nueva orden se calcula desde la exposición total en lugar del último trade.
false
MaxPositions
Número máximo de lados abiertos simultáneos más la orden pendiente activa.
9
MaxTotalVolume
Tope para la suma del volumen abierto y el volumen de la orden de recuperación.
50
ProfitTarget
Beneficio no realizado (en moneda de cuenta) que desencadena una salida completa. 0 desactiva el objetivo.
50
MinEquityPercent
Porcentaje mínimo de capital (vs. saldo inicial) requerido para seguir operando. Por debajo de este umbral solo se permiten salidas.
30
CloseAllAtStart
Cerrar todas las posiciones y cancelar órdenes cuando la estrategia inicia.
false
StartHour
Hora de inicio de la ventana de trading (inclusiva, tiempo del intercambio).
10
EndHour
Hora de fin de la ventana de trading (exclusiva, tiempo del intercambio).
20
PositionSizingMode
FixedVolume para lotes estáticos, RiskPercent para dimensionamiento por porcentaje de capital.
FixedVolume
VolumeOrRisk
Tamaño de lote fijo (cuando FixedVolume) o porcentaje de riesgo (cuando RiskPercent).
1.0
Notas de implementación
Los precios de la stop order usan una aproximación de spread mínima (PriceStep cuando está disponible) porque MetaTrader requería que la orden estuviera fuera del nivel de congelación. Ajustar StepPips si el spread real es más amplio.
La estrategia cancela cualquier orden de recuperación restante cada vez que se ejecuta un nuevo trade. Esto coincide con el EA original que eliminaba todas las órdenes pendientes después de una ejecución.
Dado que los portafolios de StockSharp están neteados, la exposición con hedge se simula internamente. La posición real del bróker siempre reflejará la cantidad neta.
El dimensionamiento de posición basado en riesgo requiere valores válidos de PriceStep y StepPrice de la descripción del instrumento.
Consejos de uso
Seleccionar un tipo de vela apropiado que coincida con el marco temporal de prueba del EA original (típicamente M5 o M15) para mejor fidelidad.
Verificar los límites de volumen del intercambio: si el volumen de recuperación normalizado se vuelve cero, la estrategia dejará de agregar nuevas piernas.
Cuando PositionSizingMode = RiskPercent, asegurarse de que el portafolio contenga información de capital actualizada; de lo contrario la estrategia recurre al tamaño de lote fijo.
Combinar con StrategyProtection incorporado de StockSharp (habilitado vía StartProtection()) para agregar salvaguardias adicionales a nivel de cuenta si es necesario.
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>
/// Fluctuate strategy using EMA crossover with stop-loss and take-profit.
/// Buys when fast EMA crosses above slow EMA, sells on reverse cross.
/// </summary>
public class FluctuateStrategy : Strategy
{
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private ExponentialMovingAverage _fast;
private ExponentialMovingAverage _slow;
private decimal _prevFast;
private decimal _prevSlow;
private decimal _entryPrice;
private int _cooldown;
/// <summary>
/// Fast EMA period.
/// </summary>
public int FastPeriod
{
get => _fastPeriod.Value;
set => _fastPeriod.Value = value;
}
/// <summary>
/// Slow EMA period.
/// </summary>
public int SlowPeriod
{
get => _slowPeriod.Value;
set => _slowPeriod.Value = value;
}
/// <summary>
/// Stop-loss distance in price steps.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance in price steps.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Initialize <see cref="FluctuateStrategy"/>.
/// </summary>
public FluctuateStrategy()
{
_fastPeriod = Param(nameof(FastPeriod), 50)
.SetGreaterThanZero()
.SetDisplay("Fast Period", "Fast EMA period", "Indicator");
_slowPeriod = Param(nameof(SlowPeriod), 200)
.SetGreaterThanZero()
.SetDisplay("Slow Period", "Slow EMA period", "Indicator");
_stopLossPoints = Param(nameof(StopLossPoints), 200)
.SetNotNegative()
.SetDisplay("Stop Loss", "Stop-loss distance in price steps", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 400)
.SetNotNegative()
.SetDisplay("Take Profit", "Take-profit distance in price steps", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fast = null;
_slow = null;
_prevFast = 0;
_prevSlow = 0;
_entryPrice = 0;
_cooldown = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fast = new ExponentialMovingAverage { Length = FastPeriod };
_slow = new ExponentialMovingAverage { Length = SlowPeriod };
var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
subscription.Bind(_fast, _slow, ProcessCandle);
subscription.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_fast.IsFormed || !_slow.IsFormed)
{
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
if (_cooldown > 0)
{
_cooldown--;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
var close = candle.ClosePrice;
var step = Security?.PriceStep ?? 1m;
// Check SL/TP
if (Position > 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step)
{
SellMarket();
_entryPrice = 0;
_cooldown = 50;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step)
{
SellMarket();
_entryPrice = 0;
_cooldown = 50;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
}
else if (Position < 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step)
{
BuyMarket();
_entryPrice = 0;
_cooldown = 50;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step)
{
BuyMarket();
_entryPrice = 0;
_cooldown = 50;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
}
// EMA crossover buy signal
if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
_entryPrice = close;
_cooldown = 50;
}
// EMA crossover sell signal
else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
_entryPrice = close;
_cooldown = 50;
}
_prevFast = fastValue;
_prevSlow = slowValue;
}
}
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
from StockSharp.Algo.Strategies import Strategy
class fluctuate_strategy(Strategy):
def __init__(self):
super(fluctuate_strategy, self).__init__()
self._fast_period = self.Param("FastPeriod", 50) \
.SetDisplay("Fast Period", "Fast EMA period", "Indicator")
self._slow_period = self.Param("SlowPeriod", 200) \
.SetDisplay("Slow Period", "Slow EMA period", "Indicator")
self._stop_loss_points = self.Param("StopLossPoints", 200) \
.SetDisplay("Stop Loss", "Stop-loss distance in price steps", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 400) \
.SetDisplay("Take Profit", "Take-profit distance in price steps", "Risk")
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
@property
def fast_period(self):
return self._fast_period.Value
@property
def slow_period(self):
return self._slow_period.Value
@property
def stop_loss_points(self):
return self._stop_loss_points.Value
@property
def take_profit_points(self):
return self._take_profit_points.Value
def OnReseted(self):
super(fluctuate_strategy, self).OnReseted()
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(fluctuate_strategy, self).OnStarted2(time)
self._fast = ExponentialMovingAverage()
self._fast.Length = self.fast_period
self._slow = ExponentialMovingAverage()
self._slow.Length = self.slow_period
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(5)))
subscription.Bind(self._fast, self._slow, self._process_candle)
subscription.Start()
def _process_candle(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_value)
slow_val = float(slow_value)
if not self._fast.IsFormed or not self._slow.IsFormed:
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self._cooldown > 0:
self._cooldown -= 1
self._prev_fast = fast_val
self._prev_slow = slow_val
return
close = float(candle.ClosePrice)
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
# Check SL/TP
if self.Position > 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close <= self._entry_price - self.stop_loss_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close >= self._entry_price + self.take_profit_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
return
elif self.Position < 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close >= self._entry_price + self.stop_loss_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close <= self._entry_price - self.take_profit_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
return
# EMA crossover buy signal
if self._prev_fast <= self._prev_slow and fast_val > slow_val and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._entry_price = close
self._cooldown = 50
# EMA crossover sell signal
elif self._prev_fast >= self._prev_slow and fast_val < slow_val and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._entry_price = close
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
def CreateClone(self):
return fluctuate_strategy()