Esta estrategia es una conversión en C# del asesor experto de MetaTrader Autotrade (edición de barabashkakvn). Mantiene continuamente dos órdenes de entrada por stop simétricas alrededor del precio de mercado actual. Siempre que el mercado permanezca flat y no haya posición abierta, la estrategia actualiza ambas órdenes pendientes. Cuando se ejecuta una orden stop, la posición se monitorea activamente: las salidas se activan cuando la acción del precio se estabiliza o cuando se alcanza un umbral absoluto de ganancia/pérdida. La implementación usa la API de alto nivel de StockSharp según lo requerido por las directrices del proyecto.
Correspondencia con los parámetros originales
Parámetro StockSharp
Parámetro MQL5
Descripción
IndentTicks
InpIndent
Distancia (en pasos de precio) entre el precio actual y las órdenes de entrada stop.
MinProfit
MinProfit
Ganancia flotante mínima (moneda de la cuenta) necesaria para salir durante una fase tranquila del mercado.
ExpirationMinutes
ExpirationMinutes
Tiempo de vida de las órdenes stop pendientes antes de ser canceladas y recreadas.
AbsoluteFixation
AbsoluteFixation
Nivel absoluto de ganancia o pérdida (moneda) que fuerza el cierre de la posición.
StabilizationTicks
InpStabilization
Tamaño máximo del cuerpo de la vela anterior que se trata como zona de consolidación.
OrderVolume
Lots
Volumen utilizado tanto para el buy stop como para el sell stop.
CandleType
Period()
Serie de velas que impulsa la lógica (marco temporal de 1 minuto por defecto).
Todos los parámetros numéricos que representan distancias de precio se convierten de "puntos" a pasos de precio reales mediante el valor Security.PriceStep. Los umbrales basados en ganancias se calculan usando Security.StepPrice, lo que refleja los cálculos de ganancia de MQL que operan en la moneda del depósito.
Lógica de trading
Despliegue de órdenes pendientes
La estrategia reacciona solo a velas terminadas (CandleStates.Finished).
La primera vela se usa para sembrar datos históricos (open/close anterior) e inmediatamente programar órdenes pendientes.
Cuando no hay posición abierta, se limpian las referencias inactivas y:
Se coloca un buy stop en Close + IndentTicks * PriceStep.
Se coloca un sell stop en Close - IndentTicks * PriceStep.
Cada orden pendiente recibe una marca de tiempo de expiración igual a CloseTime + ExpirationMinutes minutos. Cuando se alcanza ese tiempo, la orden se cancela y se recrea en la siguiente vela.
Gestión de posición
Una vez ejecutada cualquiera de las órdenes stop, la orden pendiente contraria se cancela para evitar coberturas no deseadas en el modelo de cuenta basado en netting de StockSharp.
La estrategia guarda el cuerpo de la vela anterior (|Open - Close|) para detectar condiciones tranquilas del mercado.
Para cada vela con una posición abierta:
La ganancia no realizada se estima en moneda usando la diferencia de precio respecto a PositionAvgPrice, escalada por Security.PriceStep y Security.StepPrice.
Si la ganancia supera MinProfity el cuerpo de la vela anterior está por debajo de StabilizationTicks * PriceStep, la posición se cierra a mercado.
Independientemente de la estabilización, si la ganancia o pérdida absoluta supera AbsoluteFixation, la posición también se cierra a mercado.
Cuando la posición vuelve a flat, todas las órdenes pendientes restantes se eliminan.
Comportamientos adicionales
Solo se permite una posición a la vez; los volúmenes de órdenes se netean usando OrderVolume.
Como StockSharp no expone bid/ask durante los backtests de la misma forma que MetaTrader, el precio de cierre de la vela completada se usa como nivel de referencia para nuevas órdenes stop.
La estrategia actualiza automáticamente el valor Volume en caché cuando OrderVolume se ajusta mediante parámetros u optimización.
Notas de implementación y diferencias
Los cálculos de ganancia dependen de Security.PriceStep y Security.StepPrice. Asegúrate de que estos campos estén completos en los metadatos del instrumento; de lo contrario, el valor 1 se usa como fallback.
La versión MQL original permitía cobertura temporal (múltiples órdenes en direcciones opuestas). El port de StockSharp cancela el stop no utilizado inmediatamente después de una ejecución para cumplir con el modelo de netting de la plataforma.
La expiración de órdenes pendientes usa el CloseTime de la vela. Si los datos históricos carecen de marcas de tiempo de cierre, ajusta el feed para proporcionarlas o extiende el código en consecuencia.
La estrategia funciona con cualquier tipo de dato de velas ajustando CandleType. Las velas predeterminadas están basadas en marco temporal (TimeSpan.FromMinutes(1).TimeFrame()).
Recomendaciones de uso
Configura la serie de velas que coincida con el período del gráfico utilizado en MetaTrader.
Establece IndentTicks, StabilizationTicks y los umbrales de ganancia en relación con el tamaño del tick y el valor del tick del instrumento.
Verifica que el portafolio use cobertura o netting según se desee. La estrategia asume netting y cerrará el libro antes de volver a armar las órdenes stop.
Usa los parámetros proporcionados para optimización en StockSharp Designer o Backtester para adaptar el comportamiento a diferentes mercados.
Monitorea la salida del log: el código depende de velas terminadas y disponibilidad del mercado (IsFormedAndOnlineAndAllowTrading()) antes de enviar nuevas órdenes.
Aviso de riesgo
El trading automatizado implica un riesgo sustancial. Realiza backtests exhaustivos, valida los parámetros en datos históricos y confirma los requisitos específicos del broker (como distancias mínimas para órdenes stop) antes de implementar la estrategia en una cuenta real.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Conversion of the MQL Autotrade strategy that places symmetric stop orders around the market.
/// Pending stop entries are refreshed on every candle while no position is open.
/// Positions are closed when the market calms down or when absolute profit/loss thresholds are reached.
/// </summary>
public class AutotradePendingStopsStrategy : Strategy
{
private readonly StrategyParam<int> _indentTicks;
private readonly StrategyParam<decimal> _minProfit;
private readonly StrategyParam<int> _expirationMinutes;
private readonly StrategyParam<decimal> _absoluteFixation;
private readonly StrategyParam<int> _stabilizationTicks;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevOpen;
private decimal _prevClose;
private bool _hasPrevCandle;
private decimal _tickSize = 1m;
private decimal _tickValue = 1m;
/// <summary>
/// Distance in price steps from the current market to the pending stop entries.
/// </summary>
public int IndentTicks
{
get => _indentTicks.Value;
set => _indentTicks.Value = value;
}
/// <summary>
/// Minimal profit in account currency required to exit when price action stabilizes.
/// </summary>
public decimal MinProfit
{
get => _minProfit.Value;
set => _minProfit.Value = value;
}
/// <summary>
/// Lifetime of pending stop orders in minutes.
/// </summary>
public int ExpirationMinutes
{
get => _expirationMinutes.Value;
set => _expirationMinutes.Value = value;
}
/// <summary>
/// Absolute profit or loss that forces the position to close.
/// </summary>
public decimal AbsoluteFixation
{
get => _absoluteFixation.Value;
set => _absoluteFixation.Value = value;
}
/// <summary>
/// Maximum size of the previous candle body that is treated as consolidation.
/// </summary>
public int StabilizationTicks
{
get => _stabilizationTicks.Value;
set => _stabilizationTicks.Value = value;
}
/// <summary>
/// Order volume used for entries.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set
{
_orderVolume.Value = value;
Volume = value;
}
}
/// <summary>
/// Candle type used to drive the strategy logic.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public AutotradePendingStopsStrategy()
{
_indentTicks = Param(nameof(IndentTicks), 200)
.SetGreaterThanZero()
.SetDisplay("Indent Ticks", "Distance in ticks between price and pending stop orders", "Entries");
_minProfit = Param(nameof(MinProfit), 2m)
.SetGreaterThanZero()
.SetDisplay("Min Profit", "Minimum profit to close during low volatility", "Risk");
_expirationMinutes = Param(nameof(ExpirationMinutes), 41)
.SetGreaterThanZero()
.SetDisplay("Order Expiration", "Lifetime of pending stops in minutes", "Entries");
_absoluteFixation = Param(nameof(AbsoluteFixation), 43m)
.SetGreaterThanZero()
.SetDisplay("Absolute Fixation", "Profit or loss in currency that forces exit", "Risk");
_stabilizationTicks = Param(nameof(StabilizationTicks), 25)
.SetGreaterThanZero()
.SetDisplay("Stabilization Ticks", "Maximum candle body considered as flat market", "Exits");
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Default volume for both stop orders", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Time frame that drives order refresh", "General");
Volume = _orderVolume.Value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
// Reset runtime state when the strategy is reloaded.
_prevOpen = 0m;
_prevClose = 0m;
_hasPrevCandle = false;
_entryPrice = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = _orderVolume.Value;
// Cache price step and tick value for fast profit calculations.
_tickSize = Security.PriceStep ?? 1m;
_tickValue = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? _tickSize;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
// Only act on completed candles to stay aligned with the original MQL logic.
if (candle.State != CandleStates.Finished)
return;
if (!_hasPrevCandle)
{
// Store the first candle so that stabilization checks have history.
_prevOpen = candle.OpenPrice;
_prevClose = candle.ClosePrice;
_hasPrevCandle = true;
EnsurePendingOrders(candle);
return;
}
UpdatePendingOrdersLifetime(candle);
if (Position == 0)
{
// Refresh pending orders as soon as the market is flat.
EnsurePendingOrders(candle);
}
else
{
// Manage the active position and close it when required.
ManageOpenPosition(candle);
}
// Keep the previous candle body for stabilization checks on the next bar.
_prevOpen = candle.OpenPrice;
_prevClose = candle.ClosePrice;
}
private decimal _entryPrice;
private void EnsurePendingOrders(ICandleMessage candle)
{
if (!IsFormedAndOnlineAndAllowTrading())
return;
var indent = IndentTicks * _tickSize;
var buyPrice = candle.ClosePrice + indent;
var sellPrice = candle.ClosePrice - indent;
// Simulate stop-order breakout: if high breaches buy level, go long
if (candle.HighPrice >= buyPrice && Position <= 0)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
BuyMarket(OrderVolume);
_entryPrice = buyPrice;
}
// if low breaches sell level, go short
else if (candle.LowPrice <= sellPrice && Position >= 0)
{
if (Position > 0)
SellMarket(Math.Abs(Position));
SellMarket(OrderVolume);
_entryPrice = sellPrice;
}
}
private void UpdatePendingOrdersLifetime(ICandleMessage candle)
{
// No pending orders in simplified version - nothing to expire.
}
private void ManageOpenPosition(ICandleMessage candle)
{
var entryPrice = _entryPrice;
if (entryPrice == 0)
return;
var priceDiff = Position > 0 ? candle.ClosePrice - entryPrice : entryPrice - candle.ClosePrice;
var prevBodySize = Math.Abs(_prevClose - _prevOpen);
// Exit if profitable and market consolidating, or if loss exceeds threshold
var exitByProfit = priceDiff > 0 && prevBodySize < candle.ClosePrice * 0.001m;
var exitByLoss = priceDiff < -candle.ClosePrice * 0.005m;
if (Position > 0 && (exitByProfit || exitByLoss))
{
SellMarket();
}
else if (Position < 0 && (exitByProfit || exitByLoss))
{
BuyMarket();
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class autotrade_pending_stops_strategy(Strategy):
def __init__(self):
super(autotrade_pending_stops_strategy, self).__init__()
self._indent_ticks = self.Param("IndentTicks", 200)
self._min_profit = self.Param("MinProfit", 2.0)
self._expiration_minutes = self.Param("ExpirationMinutes", 41)
self._absolute_fixation = self.Param("AbsoluteFixation", 43.0)
self._stabilization_ticks = self.Param("StabilizationTicks", 25)
self._order_volume = self.Param("OrderVolume", 1.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._prev_open = 0.0
self._prev_close = 0.0
self._has_prev_candle = False
self._tick_size = 1.0
self._entry_price = 0.0
@property
def IndentTicks(self):
return self._indent_ticks.Value
@IndentTicks.setter
def IndentTicks(self, value):
self._indent_ticks.Value = value
@property
def MinProfit(self):
return self._min_profit.Value
@MinProfit.setter
def MinProfit(self, value):
self._min_profit.Value = value
@property
def ExpirationMinutes(self):
return self._expiration_minutes.Value
@ExpirationMinutes.setter
def ExpirationMinutes(self, value):
self._expiration_minutes.Value = value
@property
def AbsoluteFixation(self):
return self._absolute_fixation.Value
@AbsoluteFixation.setter
def AbsoluteFixation(self, value):
self._absolute_fixation.Value = value
@property
def StabilizationTicks(self):
return self._stabilization_ticks.Value
@StabilizationTicks.setter
def StabilizationTicks(self, value):
self._stabilization_ticks.Value = value
@property
def OrderVolume(self):
return self._order_volume.Value
@OrderVolume.setter
def OrderVolume(self, value):
self._order_volume.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(autotrade_pending_stops_strategy, self).OnStarted2(time)
self._prev_open = 0.0
self._prev_close = 0.0
self._has_prev_candle = False
self._entry_price = 0.0
self.Volume = self._order_volume.Value
self._tick_size = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if self._tick_size <= 0.0:
self._tick_size = 1.0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
open_price = float(candle.OpenPrice)
if not self._has_prev_candle:
self._prev_open = open_price
self._prev_close = close
self._has_prev_candle = True
self._ensure_pending_orders(candle)
return
if self.Position == 0:
self._ensure_pending_orders(candle)
else:
self._manage_open_position(candle)
self._prev_open = open_price
self._prev_close = close
def _ensure_pending_orders(self, candle):
if not self.IsFormedAndOnlineAndAllowTrading():
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
order_vol = float(self._order_volume.Value)
indent = int(self._indent_ticks.Value) * self._tick_size
buy_price = close + indent
sell_price = close - indent
pos = float(self.Position)
if high >= buy_price and pos <= 0:
if pos < 0:
self.BuyMarket(abs(pos))
self.BuyMarket(order_vol)
self._entry_price = buy_price
elif low <= sell_price and pos >= 0:
if pos > 0:
self.SellMarket(abs(pos))
self.SellMarket(order_vol)
self._entry_price = sell_price
def _manage_open_position(self, candle):
if self._entry_price == 0.0:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self.Position > 0:
price_diff = close - self._entry_price
else:
price_diff = self._entry_price - close
prev_body_size = abs(self._prev_close - self._prev_open)
exit_by_profit = price_diff > 0.0 and prev_body_size < close * 0.001
exit_by_loss = price_diff < -close * 0.005
if self.Position > 0 and (exit_by_profit or exit_by_loss):
self.SellMarket()
elif self.Position < 0 and (exit_by_profit or exit_by_loss):
self.BuyMarket()
def OnReseted(self):
super(autotrade_pending_stops_strategy, self).OnReseted()
self._prev_open = 0.0
self._prev_close = 0.0
self._has_prev_candle = False
self._tick_size = 1.0
self._entry_price = 0.0
def CreateClone(self):
return autotrade_pending_stops_strategy()