Estrategia de revendedor de tendencias (API/3858)
Descripción general
TrendScalperStrategy es una conversión de C# del MetaTrader 4 asesor experto Currencyprofits_01_1.mq4. El robot original es un revendedor liviano que sigue tendencias y combina un filtro cruzado EMA/SMA a corto plazo con entradas de ruptura alrededor de los máximos y mínimos más recientes. El puerto StockSharp mantiene las mismas reglas de decisión al tiempo que adopta la suscripción de velas de alto nivel y el canal de indicadores del marco.
Lógica de trading
- Indicadores
- EMA rápida (predeterminado 6) en precios de cierre.
- Lento SMA (predeterminado 12) en precios de cierre.
- Máximo más alto (ventana 6 predeterminada) y mínimo más bajo (ventana 6 predeterminada) calculados a partir de los máximos y mínimos de las velas.
- Condiciones de entrada
- Largo: el precio avanza hacia la banda baja reciente (
Lowest Low) mientras que el EMA rápido está por encima del SMA lento. La estrategia envía una orden de compra de mercado con el volumen definido por la regla de gestión del dinero.
- Corto: el precio toca la banda alta reciente (
Highest High) mientras que el EMA rápida está por debajo del lento SMA. Una orden de venta de mercado se coloca utilizando el mismo cálculo de volumen.
- El sistema permanece plano mientras una posición está abierta, reflejando el comportamiento de orden única de la versión MQL.
- Condiciones de salida
- Salida larga: cuando en una posición larga abierta el máximo de la vela supera el
Highest High registrado, la posición se cierra en el mercado.
- Salida corta: cuando una posición corta abierta observa que el mínimo de la vela cae a través del
Lowest Low, la posición corta se cubre en el mercado.
- Se adjunta un stop-loss protector administrado por
StartProtection a cada operación cuando StopLossPoints es mayor que cero.
Gestión monetaria
La lógica de tamaño de lote reproduce los tres modos expuestos en el script MQL:
| Modo |
Descripción |
Comportamiento en el puerto |
0 |
Lotes fijos (LotsIfNoMM). |
Devuelve el FixedVolume configurado. |
<0 |
Lotes fraccionarios computados a partir del saldo de la cuenta y el factor de riesgo. |
Calcula ceil(balance * risk / 10000) / 10, con un límite de 100 lotes. |
>0 |
Escalado de lotes completos a partir del equilibrio y el factor de riesgo. |
Utiliza la misma fórmula base, pero el resultado se redondea al siguiente número entero, con un mínimo de 1 lote y un límite de 100. |
El saldo se toma de Portfolio.CurrentValue (regresando a BeginValue). Si el valor de la cartera no está disponible, la estrategia vuelve al volumen fijo, por lo que las órdenes aún se emiten durante las pruebas retrospectivas.
Gestión del riesgo
- Stop-loss: el parámetro
StopLossPoints se expresa en puntos de precio (pips). Durante OnStarted la distancia se multiplica por Security.PriceStep y se pasa a StartProtection, permitiendo a StockSharp mantener la orden de protección.
- Posición única: la lógica aplica
Position == 0 antes de abrir una nueva operación, evitando posiciones superpuestas exactamente como el experto en MT4.
Parámetros
| Nombre |
Predeterminado |
Descripción |
CandleType |
plazo de 15 minutos |
Serie de velas utilizadas para cálculos y señales de indicadores. |
FastLength |
6 |
Período de la EMA rápida. |
SlowLength |
12 |
Periodo de la lentitud SMA. |
BreakoutWindow |
6 |
Número de velas inspeccionadas para detectar el filtro de ruptura más alto/más bajo. |
FixedVolume |
0,1 lotes |
Volumen cuando la administración del dinero está deshabilitada o se requiere respaldo. |
MoneyManagementMode |
0 |
Selecciona entre tamaño de lote fijo, fraccionario o redondeado. |
MoneyManagementRisk |
40 |
Multiplicador de factor de riesgo utilizado en el tamaño de lote basado en el equilibrio. |
StopLossPoints |
50 |
Distancia de stop-loss en puntos de precio (convertida a precio absoluto antes de llamar a StartProtection). |
Notas de implementación
- El encadenamiento de indicadores se basa en el flujo de trabajo de alto nivel
SubscribeCandles().Bind(...); no se requiere almacenamiento en búfer en serie manual.
- Los comentarios en el código se agregaron en inglés para cumplir con las pautas del repositorio.
- No se modificaron pruebas unitarias; El foco de esta conversión es la estrategia y la documentación que la acompaña.
Consejos de uso
- Seleccione un intervalo de vela que coincida con el entorno comercial original (por ejemplo, marcos de tiempo intradiarios cortos para el especulación).
- Asegúrese de que la cartera tenga un
PriceStep válido para que la conversión de stop-loss a precio absoluto funcione correctamente.
- Ajuste
MoneyManagementRisk con cuidado: los valores más altos conducen a posiciones más grandes debido al cálculo ceil(balance * risk / 10000) heredado del experto MQL.
Ventana exacta: los máximos y mínimos proceden de las BreakoutWindow velas anteriores terminadas, excluida la vela actual. Compra: EMA(FastLength) > SMA(SlowLength) y currentLow <= previousWindowLow; venta: tendencia contraria y currentHigh >= previousWindowHigh. El largo se cierra con currentHigh > previousWindowHigh y el corto con currentLow < previousWindowLow. Se exige calentamiento completo. Si StopLossPoints > 0, StartProtection habilita un stop real.
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using System.Linq;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Fast EMA versus slow SMA direction filtered by a sweep of the previous
/// candle-high/low band. Independent natural exits and a native stop protect fills.
/// </summary>
public class TrendScalperStrategy : Strategy
{
private readonly StrategyParam<int> _fastLength;
private readonly StrategyParam<int> _slowLength;
private readonly StrategyParam<int> _breakoutWindow;
private readonly StrategyParam<decimal> _fixedVolume;
private readonly StrategyParam<int> _moneyManagementMode;
private readonly StrategyParam<decimal> _moneyManagementRisk;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<DataType> _candleType;
private readonly Queue<decimal> _fastSeed = new();
private readonly Queue<decimal> _slowCloses = new();
private readonly Queue<decimal> _highs = new();
private readonly Queue<decimal> _lows = new();
private decimal? _fastEma;
private Order _pendingOrder;
public int FastLength { get => _fastLength.Value; set => _fastLength.Value = value; }
public int SlowLength { get => _slowLength.Value; set => _slowLength.Value = value; }
public int BreakoutWindow { get => _breakoutWindow.Value; set => _breakoutWindow.Value = value; }
public decimal FixedVolume { get => _fixedVolume.Value; set => _fixedVolume.Value = value; }
public int MoneyManagementMode { get => _moneyManagementMode.Value; set => _moneyManagementMode.Value = value; }
public decimal MoneyManagementRisk { get => _moneyManagementRisk.Value; set => _moneyManagementRisk.Value = value; }
public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public TrendScalperStrategy()
{
_fastLength = Param(nameof(FastLength), 6).SetGreaterThanZero();
_slowLength = Param(nameof(SlowLength), 12).SetGreaterThanZero();
_breakoutWindow = Param(nameof(BreakoutWindow), 6).SetGreaterThanZero();
_fixedVolume = Param(nameof(FixedVolume), 0.1m).SetGreaterThanZero();
_moneyManagementMode = Param(nameof(MoneyManagementMode), 0);
_moneyManagementRisk = Param(nameof(MoneyManagementRisk), 40m).SetNotNegative();
_stopLossPoints = Param(nameof(StopLossPoints), 50m).SetNotNegative();
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame());
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
protected override void OnReseted()
{
base.OnReseted();
ClearState();
}
private void ClearState()
{
_fastSeed.Clear();
_slowCloses.Clear();
_highs.Clear();
_lows.Clear();
_fastEma = null;
_pendingOrder = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ClearState();
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
throw new InvalidOperationException("Trend Scalper requires a positive PriceStep.");
if (StopLossPoints > 0m)
{
StartProtection(new Unit(), new Unit(StopLossPoints * step, UnitTypes.Absolute),
useMarketOrders: true, isLocalStop: true);
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveQuote).Start();
}
}
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
}
private void ObserveQuote(Level1ChangeMessage message)
{
// A Level1 stream is needed for native stop processing between candle closes.
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var hasBand = _highs.Count == BreakoutWindow;
var bandHigh = hasBand ? _highs.Max() : 0m;
var bandLow = hasBand ? _lows.Min() : 0m;
_highs.Enqueue(candle.HighPrice);
_lows.Enqueue(candle.LowPrice);
while (_highs.Count > BreakoutWindow) _highs.Dequeue();
while (_lows.Count > BreakoutWindow) _lows.Dequeue();
var price = candle.ClosePrice;
if (_fastEma is null)
{
_fastSeed.Enqueue(price);
if (_fastSeed.Count == FastLength)
_fastEma = _fastSeed.Average();
}
else
_fastEma += (price - _fastEma.Value) * 2m / (FastLength + 1);
_slowCloses.Enqueue(price);
while (_slowCloses.Count > SlowLength)
_slowCloses.Dequeue();
if (!hasBand || _fastEma is null || _slowCloses.Count < SlowLength ||
_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
if (Position > 0m && candle.HighPrice > bandHigh)
{
Submit(Sides.Sell, Position, "Trend scalper band exit");
return;
}
if (Position < 0m && candle.LowPrice < bandLow)
{
Submit(Sides.Buy, Math.Abs(Position), "Trend scalper band exit");
return;
}
if (Position != 0m)
return;
var slow = _slowCloses.Average();
if (_fastEma > slow && candle.LowPrice <= bandLow)
Enter(Sides.Buy);
else if (_fastEma < slow && candle.HighPrice >= bandHigh)
Enter(Sides.Sell);
}
private void Enter(Sides side)
{
var volume = CalculateOrderVolume();
if (volume > 0m)
Submit(side, volume, "Trend scalper entry");
}
private decimal CalculateOrderVolume()
{
var volume = FixedVolume;
if (MoneyManagementMode != 0)
{
var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
if (balance > 0m)
{
var fractional = Math.Ceiling(balance * MoneyManagementRisk / 10000m) / 10m;
volume = MoneyManagementMode > 0 ? Math.Max(1m, Math.Ceiling(fractional)) : fractional;
volume = Math.Min(volume, 100m);
}
}
if (Security?.VolumeStep is decimal step && step > 0m)
volume = decimal.Floor(volume / step) * step;
if (Security?.MinVolume is decimal min && min > 0m)
volume = Math.Max(volume, min);
if (Security?.MaxVolume is decimal max && max > 0m)
volume = Math.Min(volume, max);
return volume;
}
private void Submit(Sides side, decimal volume, string comment)
{
_pendingOrder = new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = side,
Volume = volume,
Comment = comment,
};
RegisterOrder(_pendingOrder);
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal
from StockSharp.Messages import (
DataType, CandleStates, Level1Fields, OrderTypes, OrderStates, Sides,
Unit, UnitTypes,
)
from StockSharp.BusinessEntities import Order, Subscription
from StockSharp.Algo.Strategies import Strategy
class trend_scalper_strategy(Strategy):
"""Fast close EMA, slow close SMA, and previous-bar high/low breakout bands."""
def __init__(self):
super(trend_scalper_strategy, self).__init__()
self._fast_length = self.Param("FastLength", 6).SetGreaterThanZero()
self._slow_length = self.Param("SlowLength", 12).SetGreaterThanZero()
self._breakout_window = self.Param("BreakoutWindow", 6).SetGreaterThanZero()
self._fixed_volume = self.Param("FixedVolume", 0.1).SetGreaterThanZero()
self._money_management_mode = self.Param("MoneyManagementMode", 0)
self._money_management_risk = self.Param("MoneyManagementRisk", 40.0).SetNotNegative()
self._stop_loss_points = self.Param("StopLossPoints", 50.0).SetNotNegative()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._clear_state()
@property
def candle_type(self):
return self._candle_type.Value
def _clear_state(self):
self._fast_seed = []
self._slow_closes = []
self._highs = []
self._lows = []
self._fast_ema = None
self._pending_order = None
def OnReseted(self):
super(trend_scalper_strategy, self).OnReseted()
self._clear_state()
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def OnStarted2(self, time):
super(trend_scalper_strategy, self).OnStarted2(time)
self._clear_state()
step = float(self.Security.PriceStep or 0)
if step <= 0:
raise ValueError("Trend Scalper requires a positive PriceStep.")
if self._stop_loss_points.Value > 0:
self.StartProtection(
Unit(), Unit(Decimal(float(self._stop_loss_points.Value) * step), UnitTypes.Absolute),
useMarketOrders=True, isLocalStop=True)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_quote).Start()
self.SubscribeCandles(self.candle_type).Bind(self._process_candle).Start()
def _observe_quote(self, message):
# Keeps native protection informed between completed candles.
pass
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
has_band = len(self._highs) == self._breakout_window.Value
band_high = max(self._highs) if has_band else 0.0
band_low = min(self._lows) if has_band else 0.0
self._highs.append(float(candle.HighPrice))
self._lows.append(float(candle.LowPrice))
if len(self._highs) > self._breakout_window.Value:
self._highs.pop(0)
self._lows.pop(0)
close = float(candle.ClosePrice)
fast_length = self._fast_length.Value
if self._fast_ema is None:
self._fast_seed.append(close)
if len(self._fast_seed) == fast_length:
self._fast_ema = sum(self._fast_seed) / fast_length
else:
self._fast_ema += (close - self._fast_ema) * 2.0 / (fast_length + 1)
self._slow_closes.append(close)
if len(self._slow_closes) > self._slow_length.Value:
self._slow_closes.pop(0)
if not has_band or self._fast_ema is None or len(self._slow_closes) < self._slow_length.Value:
return
if (self._pending_order is not None and
self._pending_order.State not in (OrderStates.Done, OrderStates.Failed)):
return
if self.Position > 0 and float(candle.HighPrice) > band_high:
self._submit(Sides.Sell, float(self.Position), "Trend scalper band exit")
return
if self.Position < 0 and float(candle.LowPrice) < band_low:
self._submit(Sides.Buy, abs(float(self.Position)), "Trend scalper band exit")
return
if self.Position != 0:
return
slow = sum(self._slow_closes) / len(self._slow_closes)
if self._fast_ema > slow and float(candle.LowPrice) <= band_low:
self._enter(Sides.Buy)
elif self._fast_ema < slow and float(candle.HighPrice) >= band_high:
self._enter(Sides.Sell)
def _enter(self, side):
volume = self._calculate_volume()
if volume > 0:
self._submit(side, volume, "Trend scalper entry")
def _calculate_volume(self):
volume = float(self._fixed_volume.Value)
mode = self._money_management_mode.Value
if mode != 0:
balance = (self.Portfolio.CurrentValue or self.Portfolio.BeginValue
if self.Portfolio is not None else 0)
balance = float(balance or 0)
if balance > 0:
fraction = math.ceil(balance * float(self._money_management_risk.Value) / 10000.0) / 10.0
volume = min(100.0, max(1.0, math.ceil(fraction)) if mode > 0 else fraction)
step = float(self.Security.VolumeStep or 0)
if step > 0:
volume = math.floor(volume / step) * step
minimum = float(self.Security.MinVolume or 0)
maximum = float(self.Security.MaxVolume or 0)
if minimum > 0:
volume = max(volume, minimum)
if maximum > 0:
volume = min(volume, maximum)
return volume
def _submit(self, side, volume, comment):
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = Decimal(volume)
order.Comment = comment
self._pending_order = order
self.RegisterOrder(order)
def CreateClone(self):
return trend_scalper_strategy()