Esta estrategia es un port de alto nivel en StockSharp del asesor experto Aeron JJN Scalper. Observa velas terminadas, identifica situaciones específicas de reversión de dos barras y coloca órdenes stop simuladas en la apertura de la vela opuesta más reciente. Cuando el mercado alcanza el nivel de stop almacenado, la estrategia entra con una orden a mercado, aplica objetivos de riesgo basados en ATR y gestiona la operación con un trailing stop basado en pips.
Ideas clave:
La dirección de la operación se decide por un patrón de reversión alcista/bajista de dos velas.
Los niveles de entrada provienen del precio de apertura de la última vela fuerte en la dirección opuesta.
Un valor ATR(8) medido en la barra de señal establece las distancias de stop-loss y take-profit.
La lógica del trailing stop mueve el nivel de protección una vez que el precio avanza por los offsets de pip configurados.
Los niveles pendientes expiran automáticamente después del número configurado de minutos.
Reglas de trading
Detección de señal
Trabajar solo con velas terminadas del marco temporal configurado (predeterminado: 1 minuto).
Calcular el tamaño del pip a partir del paso de precio del instrumento y multiplicar por 10 para precios de 3 o 5 decimales para imitar el comportamiento de pip de MetaTrader.
Mantener una ventana deslizante de las últimas 120 velas para buscar barras de referencia.
Detectar una configuración larga cuando:
La vela actual cierra por encima de su apertura (alcista), y
La vela anterior es bajista con tamaño de cuerpo mayor que DojiDiff1Pips.
Buscar hacia atrás la última vela bajista cuyo cuerpo supere DojiDiff2Pips; su precio de apertura se convierte en el nivel de buy stop.
Detectar una configuración corta cuando:
La vela actual cierra por debajo de su apertura (bajista), y
La vela anterior es alcista con tamaño de cuerpo mayor que DojiDiff1Pips.
Buscar hacia atrás la última vela alcista cuyo cuerpo supere DojiDiff2Pips; su precio de apertura se convierte en el nivel de sell stop.
Ignorar nuevas configuraciones si ya hay un nivel pendiente en la misma dirección, o si el valor ATR para la vela aún no está disponible.
Gestión de niveles pendientes
El nivel almacenado se trata como una orden stop pendiente. Se descarta si el precio permanece por debajo (largo) o por encima (corto) del disparador hasta que expira el tiempo ResetMinutes.
Cuando el precio toca el nivel en una vela posterior (máximo ≥ nivel de compra o mínimo ≤ nivel de venta), la estrategia envía una orden a mercado dimensionada para cerrar cualquier exposición existente y añadir contratos Volume.
Entrar en una posición larga limpia cualquier nivel corto pendiente y viceversa.
Stop-loss, take-profit y trailing
Al entrar, la estrategia registra el valor ATR(8) de la vela de señal.
Verifica si el precio alcanzó el stop-loss o take-profit y sale con una orden a mercado si se toca.
Aplica trailing cuando el precio se ha movido al menos TrailingStopPips + TrailingStepPips a favor de la posición. El nuevo stop queda TrailingStopPips detrás del último cierre. El stop nunca retrocede.
Si la posición se cierra manualmente, el estado interno se restablece automáticamente.
Parámetros
Parámetro
Valor predeterminado
Descripción
Volume
0.1
Tamaño de posición neta utilizado para entradas; la estrategia añade la posición actual absoluta para cambiar de dirección cuando sea necesario.
TrailingStopPips
5
Distancia base del trailing stop (convertida a unidades de precio).
TrailingStepPips
5
Avance adicional requerido antes de mover el trailing stop nuevamente.
ResetMinutes
10
Tiempo de expiración para un nivel pendiente almacenado (minutos).
DojiDiff1Pips
10
Tamaño mínimo de cuerpo (en pips) para la vela de reversión que precede a la señal.
DojiDiff2Pips
4
Tamaño mínimo de cuerpo (en pips) para la vela usada como nivel de referencia de entrada.
CandleType
Marco temporal de 1 minuto
Tipo de datos de vela usado para cálculos.
Notas de implementación
La estrategia opera puramente en velas terminadas y usa niveles en memoria en lugar de órdenes stop reales; cuando se viola el nivel, se envía inmediatamente una orden a mercado. Esto refleja el comportamiento original del EA dentro de la API de alto nivel de StockSharp.
ATR(8) se calcula con AverageTrueRange y se almacena en caché para que las distancias de stop/objetivo originales permanezcan constantes para cada operación.
La conversión de pips reproduce el ajuste de MetaTrader para cotizaciones de 3 y 5 dígitos. Si el instrumento carece de PriceStep, se usa un paso predeterminado de 1.
Se almacenan hasta 120 velas históricas para replicar el look-back original de CopyRates de 100 barras con algún margen de seguridad.
No se proporciona port en Python para esta estrategia.
Uso
Adjuntar la estrategia al instrumento y portafolio deseados.
Ajustar el marco temporal de velas, los offsets de pip y los filtros basados en ATR para adaptarlos al instrumento.
Iniciar la estrategia; rastreará señales, enviará órdenes a mercado cuando se toquen los niveles de activación y gestionará las salidas automáticamente.
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Port of the Aeron JJN Scalper expert advisor.
/// Detects reversal candle patterns (bullish candle after strong bearish, and vice versa)
/// and enters at breakout of the prior candle's open level.
/// </summary>
public class AeronJjnScalperEaStrategy : Strategy
{
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<decimal> _bodyMinAtr;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private AverageTrueRange _atr;
private decimal _prevOpen;
private decimal _prevClose;
private bool _hasPrev;
private int _cooldown;
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
public decimal BodyMinAtr
{
get => _bodyMinAtr.Value;
set => _bodyMinAtr.Value = value;
}
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public AeronJjnScalperEaStrategy()
{
_atrLength = Param(nameof(AtrLength), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Length", "ATR indicator period", "Indicators");
_bodyMinAtr = Param(nameof(BodyMinAtr), 1.5m)
.SetDisplay("Body Min ATR", "Minimum candle body size as ATR multiple", "Indicators");
_cooldownBars = Param(nameof(CooldownBars), 10)
.SetGreaterThanZero()
.SetDisplay("Cooldown Bars", "Bars to wait between trades", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle Type", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
_hasPrev = false;
_cooldown = 0;
_prevOpen = 0;
_prevClose = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_atr = new AverageTrueRange { Length = AtrLength };
_hasPrev = false;
_cooldown = 0;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(_atr, ProcessCandle).Start();
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent)
);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal atrVal)
{
if (candle.State != CandleStates.Finished)
return;
if (atrVal <= 0)
{
SavePrev(candle);
return;
}
if (_cooldown > 0)
_cooldown--;
if (_hasPrev && _cooldown == 0 && Position == 0)
{
var prevBody = Math.Abs(_prevClose - _prevOpen);
var minBody = atrVal * BodyMinAtr;
// Bullish candle after strong bearish candle => buy
if (candle.ClosePrice > candle.OpenPrice && _prevClose < _prevOpen && prevBody >= minBody)
{
BuyMarket();
_cooldown = CooldownBars;
}
// Bearish candle after strong bullish candle => sell
else if (candle.ClosePrice < candle.OpenPrice && _prevClose > _prevOpen && prevBody >= minBody)
{
SellMarket();
_cooldown = CooldownBars;
}
}
SavePrev(candle);
}
private void SavePrev(ICandleMessage candle)
{
_prevOpen = candle.OpenPrice;
_prevClose = candle.ClosePrice;
_hasPrev = true;
}
}
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.Indicators import AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class aeron_jjn_scalper_ea_strategy(Strategy):
def __init__(self):
super(aeron_jjn_scalper_ea_strategy, self).__init__()
self._atr_length = self.Param("AtrLength", 14)
self._body_min_atr = self.Param("BodyMinAtr", 1.5)
self._cooldown_bars = self.Param("CooldownBars", 10)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._prev_open = 0.0
self._prev_close = 0.0
self._has_prev = False
self._cooldown = 0
@property
def AtrLength(self):
return self._atr_length.Value
@property
def BodyMinAtr(self):
return self._body_min_atr.Value
@property
def CooldownBars(self):
return self._cooldown_bars.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(aeron_jjn_scalper_ea_strategy, self).OnStarted2(time)
self._has_prev = False
self._cooldown = 0
atr = AverageTrueRange()
atr.Length = self.AtrLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(atr, self._process_candle).Start()
self.StartProtection(
takeProfit=Unit(2, UnitTypes.Percent),
stopLoss=Unit(1, UnitTypes.Percent)
)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle, atr_val):
if candle.State != CandleStates.Finished:
return
atr_value = float(atr_val)
if atr_value <= 0:
self._save_prev(candle)
return
if self._cooldown > 0:
self._cooldown -= 1
if self._has_prev and self._cooldown == 0 and float(self.Position) == 0:
prev_body = abs(self._prev_close - self._prev_open)
min_body = atr_value * float(self.BodyMinAtr)
if float(candle.ClosePrice) > float(candle.OpenPrice) and self._prev_close < self._prev_open and prev_body >= min_body:
self.BuyMarket()
self._cooldown = self.CooldownBars
elif float(candle.ClosePrice) < float(candle.OpenPrice) and self._prev_close > self._prev_open and prev_body >= min_body:
self.SellMarket()
self._cooldown = self.CooldownBars
self._save_prev(candle)
def _save_prev(self, candle):
self._prev_open = float(candle.OpenPrice)
self._prev_close = float(candle.ClosePrice)
self._has_prev = True
def OnReseted(self):
super(aeron_jjn_scalper_ea_strategy, self).OnReseted()
self._has_prev = False
self._cooldown = 0
self._prev_open = 0.0
self._prev_close = 0.0
def CreateClone(self):
return aeron_jjn_scalper_ea_strategy()