Estrategia de 20 Pips Opuesta a la Tendencia de las Últimas N Horas
Esta estrategia de StockSharp es un puerto de alto nivel del Asesor Experto MetaTrader
«20 Pips Opposite Last N Hour Trend». Observa velas horarias, mide
cómo se comportó el precio durante las N horas anteriores, y luego abre una posición en
la dirección opuesta cuando finaliza la hora de trading configurada. La operación se
gestiona usando un objetivo fijo de take-profit de 20 pips y un tiempo de espera horario, mientras
se aplica una escala de volumen estilo martingala después de pérdidas consecutivas.
La implementación usa las suscripciones de velas de StockSharp, el sistema de parámetros,
y los helpers de órdenes (BuyMarket, SellMarket) para que pueda ejecutarse sin cambios dentro de
Designer, API, Runner o Shell.
Lógica de trading
- La estrategia se suscribe al tipo de vela seleccionado (predeterminado: barras de 1 hora).
- Para cada vela terminada mantiene el precio de cierre dentro de un historial interno.
- Cuando una vela con
OpenTime.Hour == TradingHour se completa y hay suficiente
historial disponible:
- Compare el cierre que ocurrió hace
HoursToCheckTrend barras con el
cierre anterior (1 barra atrás).
- Si el precio disminuyó en esa ventana (deriva bajista) la estrategia compra;
si el precio aumentó (deriva alcista) vende. Cierres iguales omiten el trading.
- Solo se abre una operación por día y exclusivamente en la hora de trading configurada.
Todas las demás velas se usan puramente para gestión.
Gestión de posición
- Un objetivo de 20 pips (ajustado para símbolos de 3/5 dígitos) se calcula justo después de la
entrada. Cuando cualquier vela terminada muestra que el máximo/mínimo tocó el objetivo, la
posición se cierra en ese nivel.
- Si el objetivo no se alcanza durante la siguiente hora, la posición se cierra al
final de la siguiente vela para evitar exposición nocturna.
- Los contadores diarios se reinician automáticamente cuando comienza un nuevo día de trading, para que
la próxima señal elegible pueda dispararse en la siguiente sesión.
Gestión de dinero
Volume establece el tamaño base de la orden. MaxVolume limita el tamaño resultante de cualquier
paso de martingala.
- Después de una salida perdedora la estrategia aumenta la siguiente posición por el
multiplicador apropiado: primera pérdida →
FirstMultiplier, segunda pérdida →
SecondMultiplier, etc. Las rachas perdedoras más allá de cinco operaciones reutilizan el quinto
multiplicador. Cualquier cierre rentable o en punto de equilibrio reinicia la secuencia.
- Los cálculos de volumen dependen del último precio de posición ejecutado, por lo que la detección de ganancia/pérdida
permanece determinista incluso sin datos completos de PnL del broker.
Parámetros
| Parámetro |
Predeterminado |
Descripción |
MaxPositions |
9 |
Máximo de operaciones permitidas por día. Establezca en 0 para deshabilitar el trading. |
Volume |
0.1 |
Volumen base para la primera operación de una racha. |
MaxVolume |
5 |
Límite máximo para el volumen ajustado después de multiplicadores. |
TakeProfitPips |
20 |
Distancia de take-profit en pips. 0 deshabilita el TP. |
TradingHour |
7 |
Hora del día (0-23) habilitada para abrir una posición. |
HoursToCheckTrend |
24 |
Número de cierres horarios usados para medir la tendencia previa. |
FirstMultiplier |
2 |
Multiplicador aplicado después de la primera pérdida consecutiva. |
SecondMultiplier |
4 |
Multiplicador aplicado después de la segunda pérdida consecutiva. |
ThirdMultiplier |
8 |
Multiplicador aplicado después de la tercera pérdida consecutiva. |
FourthMultiplier |
16 |
Multiplicador aplicado después de la cuarta pérdida consecutiva. |
FifthMultiplier |
32 |
Multiplicador aplicado desde la quinta pérdida en adelante. |
CandleType |
H1 |
Tipo de datos de vela usado para la generación de señales y gestión. |
Notas adicionales
- El tamaño del pip se calcula a partir de
Security.PriceStep y el número de decimales para que
el objetivo de 20 pips funcione correctamente en símbolos FX de 4 y 5 dígitos.
StartProtection() se invoca cuando la estrategia comienza, habilitando las protecciones integradas
de StockSharp (stop automático para posiciones sin límite, reinicios de cartera).
- La lógica usa solo velas terminadas y nunca lee valores de indicadores
directamente, cumpliendo con las directrices de
AGENTS.md.
Descargo de responsabilidad de riesgo: El dimensionamiento de posición estilo martingala puede llevar a
drawdowns sustanciales. Siempre pruebe los parámetros en datos históricos y use límites de riesgo prudentes
antes de implementar en trading en vivo.
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>
/// Strategy that trades against the last N hours trend with a fixed take profit.
/// </summary>
public class TwentyPipsOppositeLastNHourTrendStrategy : Strategy
{
private readonly StrategyParam<int> _maxPositions;
private readonly StrategyParam<decimal> _maxVolume;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<int> _tradingHour;
private readonly StrategyParam<int> _hoursToCheckTrend;
private readonly StrategyParam<int> _firstMultiplier;
private readonly StrategyParam<int> _secondMultiplier;
private readonly StrategyParam<int> _thirdMultiplier;
private readonly StrategyParam<int> _fourthMultiplier;
private readonly StrategyParam<int> _fifthMultiplier;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _closeHistory = new();
private decimal? _entryPrice;
private decimal? _takeProfitLevel;
private decimal _entryVolume;
private int _positionDirection;
private int _consecutiveLosses;
private DateTime? _currentDay;
private int _tradesToday;
public int MaxPositions
{
get => _maxPositions.Value;
set => _maxPositions.Value = value;
}
public decimal MaxVolume
{
get => _maxVolume.Value;
set => _maxVolume.Value = value;
}
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
public int TradingHour
{
get => _tradingHour.Value;
set => _tradingHour.Value = value;
}
public int HoursToCheckTrend
{
get => _hoursToCheckTrend.Value;
set => _hoursToCheckTrend.Value = value;
}
public int FirstMultiplier
{
get => _firstMultiplier.Value;
set => _firstMultiplier.Value = value;
}
public int SecondMultiplier
{
get => _secondMultiplier.Value;
set => _secondMultiplier.Value = value;
}
public int ThirdMultiplier
{
get => _thirdMultiplier.Value;
set => _thirdMultiplier.Value = value;
}
public int FourthMultiplier
{
get => _fourthMultiplier.Value;
set => _fourthMultiplier.Value = value;
}
public int FifthMultiplier
{
get => _fifthMultiplier.Value;
set => _fifthMultiplier.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public TwentyPipsOppositeLastNHourTrendStrategy()
{
_maxPositions = Param(nameof(MaxPositions), 9)
.SetGreaterThanZero()
.SetDisplay("Max Positions", "Maximum trades per day", "Trading");
_maxVolume = Param(nameof(MaxVolume), 5m)
.SetGreaterThanZero()
.SetDisplay("Max Volume", "Maximum allowed volume", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 20m)
.SetGreaterThanZero()
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Trading");
_tradingHour = Param(nameof(TradingHour), 8)
.SetRange(0, 23)
.SetDisplay("Trading Hour", "Hour (0-23) when entries are allowed", "Timing");
_hoursToCheckTrend = Param(nameof(HoursToCheckTrend), 6)
.SetRange(2, 240)
.SetDisplay("Hours To Check", "Lookback hours for trend calculation", "Signals");
_firstMultiplier = Param(nameof(FirstMultiplier), 2)
.SetGreaterThanZero()
.SetDisplay("First Multiplier", "Multiplier after first loss", "Money Management");
_secondMultiplier = Param(nameof(SecondMultiplier), 4)
.SetGreaterThanZero()
.SetDisplay("Second Multiplier", "Multiplier after second loss", "Money Management");
_thirdMultiplier = Param(nameof(ThirdMultiplier), 8)
.SetGreaterThanZero()
.SetDisplay("Third Multiplier", "Multiplier after third loss", "Money Management");
_fourthMultiplier = Param(nameof(FourthMultiplier), 16)
.SetGreaterThanZero()
.SetDisplay("Fourth Multiplier", "Multiplier after fourth loss", "Money Management");
_fifthMultiplier = Param(nameof(FifthMultiplier), 32)
.SetGreaterThanZero()
.SetDisplay("Fifth Multiplier", "Multiplier after fifth loss", "Money Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe to process", "Market Data");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_closeHistory.Clear();
_entryPrice = null;
_takeProfitLevel = null;
_entryVolume = 0m;
_positionDirection = 0;
_consecutiveLosses = 0;
_currentDay = null;
_tradesToday = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
// no fixed protection needed
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var candleDay = candle.OpenTime.Date;
if (_currentDay != candleDay)
{
_currentDay = candleDay;
_tradesToday = 0;
}
if (_positionDirection != 0)
{
if (_takeProfitLevel is decimal target)
{
// Take profit when the candle range touches the desired level.
var hitTarget = _positionDirection > 0
? candle.HighPrice >= target
: candle.LowPrice <= target;
if (hitTarget)
{
ClosePosition(target);
}
}
if (_positionDirection != 0 && candle.OpenTime.Hour != TradingHour)
{
// Close remaining exposure when the configured session hour has passed.
ClosePosition(candle.ClosePrice);
}
}
if (_positionDirection != 0)
{
UpdateHistory(candle.ClosePrice);
return;
}
if (candle.OpenTime.Hour != TradingHour)
{
UpdateHistory(candle.ClosePrice);
return;
}
if (MaxPositions <= 0 || _tradesToday >= MaxPositions)
{
UpdateHistory(candle.ClosePrice);
return;
}
var requiredHistory = Math.Max(HoursToCheckTrend, 2);
if (_closeHistory.Count < requiredHistory)
{
UpdateHistory(candle.ClosePrice);
return;
}
var referenceClose = _closeHistory[_closeHistory.Count - HoursToCheckTrend];
var previousClose = _closeHistory[_closeHistory.Count - 1];
if (previousClose == referenceClose)
{
UpdateHistory(candle.ClosePrice);
return;
}
// Opposite trend logic: buy after bearish drift, sell after bullish drift.
var goLong = previousClose < referenceClose;
var orderVolume = CalculateOrderVolume();
if (orderVolume <= 0)
{
UpdateHistory(candle.ClosePrice);
return;
}
if (goLong)
{
Volume = orderVolume;
BuyMarket();
_positionDirection = 1;
}
else
{
Volume = orderVolume;
SellMarket();
_positionDirection = -1;
}
_entryPrice = candle.ClosePrice;
_entryVolume = orderVolume;
var distance = GetTakeProfitDistance();
if (distance > 0m)
{
_takeProfitLevel = _positionDirection > 0
? _entryPrice + distance
: _entryPrice - distance;
}
else
{
_takeProfitLevel = null;
}
_tradesToday++;
UpdateHistory(candle.ClosePrice);
}
private void ClosePosition(decimal exitPrice)
{
var direction = _positionDirection;
var entryPrice = _entryPrice;
var volume = Math.Abs(Position);
if (volume <= 0m && _entryVolume > 0m)
{
volume = _entryVolume;
}
if (volume <= 0m)
{
_positionDirection = 0;
_takeProfitLevel = null;
_entryPrice = null;
_entryVolume = 0m;
return;
}
if (direction > 0)
{
SellMarket();
}
else if (direction < 0)
{
BuyMarket();
}
if (entryPrice is decimal price)
{
var isLoss = direction > 0
? exitPrice < price
: exitPrice > price;
_consecutiveLosses = isLoss
? Math.Min(_consecutiveLosses + 1, 5)
: 0;
}
_positionDirection = 0;
_takeProfitLevel = null;
_entryPrice = null;
_entryVolume = 0m;
}
private void UpdateHistory(decimal closePrice)
{
_closeHistory.Add(closePrice);
var maxHistory = Math.Max(HoursToCheckTrend, 2);
if (_closeHistory.Count > maxHistory)
{
_closeHistory.RemoveRange(0, _closeHistory.Count - maxHistory);
}
}
private decimal CalculateOrderVolume()
{
if (Volume <= 0m)
{
return 0m;
}
var multiplier = _consecutiveLosses switch
{
>= 5 => (decimal)FifthMultiplier,
4 => (decimal)FourthMultiplier,
3 => (decimal)ThirdMultiplier,
2 => (decimal)SecondMultiplier,
1 => (decimal)FirstMultiplier,
_ => 1m
};
var desiredVolume = Volume * multiplier;
if (MaxVolume > 0m && desiredVolume > MaxVolume)
{
desiredVolume = MaxVolume;
}
return desiredVolume;
}
private decimal GetTakeProfitDistance()
{
var pipSize = GetPipSize();
return pipSize > 0m
? TakeProfitPips * pipSize
: 0m;
}
private decimal GetPipSize()
{
var priceStep = Security?.PriceStep ?? 0m;
if (priceStep <= 0m)
{
priceStep = 0.0001m;
}
var decimals = Security?.Decimals ?? 0;
if (decimals == 3 || decimals == 5)
{
return priceStep * 10m;
}
return priceStep;
}
}
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.Strategies import Strategy
class twenty_pips_opposite_last_n_hour_trend_strategy(Strategy):
"""20 Pips Opposite Last N Hour Trend: counter-trend martingale strategy."""
def __init__(self):
super(twenty_pips_opposite_last_n_hour_trend_strategy, self).__init__()
self._max_positions = self.Param("MaxPositions", 9) \
.SetGreaterThanZero() \
.SetDisplay("Max Positions", "Maximum trades per day", "Trading")
self._max_volume = self.Param("MaxVolume", 5.0) \
.SetGreaterThanZero() \
.SetDisplay("Max Volume", "Maximum allowed volume", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 20.0) \
.SetGreaterThanZero() \
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Trading")
self._trading_hour = self.Param("TradingHour", 8) \
.SetDisplay("Trading Hour", "Hour (0-23) when entries are allowed", "Timing")
self._hours_to_check_trend = self.Param("HoursToCheckTrend", 6) \
.SetDisplay("Hours To Check", "Lookback hours for trend calculation", "Signals")
self._first_multiplier = self.Param("FirstMultiplier", 2) \
.SetGreaterThanZero() \
.SetDisplay("First Multiplier", "Multiplier after first loss", "Money Management")
self._second_multiplier = self.Param("SecondMultiplier", 4) \
.SetGreaterThanZero() \
.SetDisplay("Second Multiplier", "Multiplier after second loss", "Money Management")
self._third_multiplier = self.Param("ThirdMultiplier", 8) \
.SetGreaterThanZero() \
.SetDisplay("Third Multiplier", "Multiplier after third loss", "Money Management")
self._fourth_multiplier = self.Param("FourthMultiplier", 16) \
.SetGreaterThanZero() \
.SetDisplay("Fourth Multiplier", "Multiplier after fourth loss", "Money Management")
self._fifth_multiplier = self.Param("FifthMultiplier", 32) \
.SetGreaterThanZero() \
.SetDisplay("Fifth Multiplier", "Multiplier after fifth loss", "Money Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candle timeframe to process", "Market Data")
self._close_history = []
self._entry_price = None
self._take_profit_level = None
self._entry_volume = 0.0
self._position_direction = 0
self._consecutive_losses = 0
self._current_day = None
self._trades_today = 0
@property
def MaxPositions(self):
return int(self._max_positions.Value)
@property
def MaxVolume(self):
return float(self._max_volume.Value)
@property
def TakeProfitPips(self):
return float(self._take_profit_pips.Value)
@property
def TradingHour(self):
return int(self._trading_hour.Value)
@property
def HoursToCheckTrend(self):
return int(self._hours_to_check_trend.Value)
@property
def FirstMultiplier(self):
return int(self._first_multiplier.Value)
@property
def SecondMultiplier(self):
return int(self._second_multiplier.Value)
@property
def ThirdMultiplier(self):
return int(self._third_multiplier.Value)
@property
def FourthMultiplier(self):
return int(self._fourth_multiplier.Value)
@property
def FifthMultiplier(self):
return int(self._fifth_multiplier.Value)
@property
def CandleType(self):
return self._candle_type.Value
def _get_pip_size(self):
sec = self.Security
if sec is None or sec.PriceStep is None:
return 0.0001
step = float(sec.PriceStep)
if step <= 0:
return 0.0001
decimals = 0
if sec.Decimals is not None:
decimals = int(sec.Decimals)
if decimals == 3 or decimals == 5:
return step * 10.0
return step
def OnStarted2(self, time):
super(twenty_pips_opposite_last_n_hour_trend_strategy, self).OnStarted2(time)
self._close_history = []
self._entry_price = None
self._take_profit_level = None
self._entry_volume = 0.0
self._position_direction = 0
self._consecutive_losses = 0
self._current_day = None
self._trades_today = 0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
candle_day = candle.OpenTime.Date
if self._current_day is None or self._current_day != candle_day:
self._current_day = candle_day
self._trades_today = 0
if self._position_direction != 0:
if self._take_profit_level is not None:
if self._position_direction > 0:
hit_target = float(candle.HighPrice) >= self._take_profit_level
else:
hit_target = float(candle.LowPrice) <= self._take_profit_level
if hit_target:
self._close_position(self._take_profit_level)
if self._position_direction != 0 and candle.OpenTime.Hour != self.TradingHour:
self._close_position(close)
if self._position_direction != 0:
self._update_history(close)
return
if candle.OpenTime.Hour != self.TradingHour:
self._update_history(close)
return
if self.MaxPositions <= 0 or self._trades_today >= self.MaxPositions:
self._update_history(close)
return
required_history = max(self.HoursToCheckTrend, 2)
if len(self._close_history) < required_history:
self._update_history(close)
return
reference_close = self._close_history[len(self._close_history) - self.HoursToCheckTrend]
previous_close = self._close_history[len(self._close_history) - 1]
if previous_close == reference_close:
self._update_history(close)
return
go_long = previous_close < reference_close
order_volume = self._calculate_order_volume()
if order_volume <= 0:
self._update_history(close)
return
if go_long:
self.BuyMarket()
self._position_direction = 1
else:
self.SellMarket()
self._position_direction = -1
self._entry_price = close
self._entry_volume = order_volume
distance = self._get_take_profit_distance()
if distance > 0:
if self._position_direction > 0:
self._take_profit_level = self._entry_price + distance
else:
self._take_profit_level = self._entry_price - distance
else:
self._take_profit_level = None
self._trades_today += 1
self._update_history(close)
def _close_position(self, exit_price):
direction = self._position_direction
entry_price = self._entry_price
volume = abs(self.Position)
if volume <= 0 and self._entry_volume > 0:
volume = self._entry_volume
if volume <= 0:
self._position_direction = 0
self._take_profit_level = None
self._entry_price = None
self._entry_volume = 0.0
return
if direction > 0:
self.SellMarket()
elif direction < 0:
self.BuyMarket()
if entry_price is not None:
if direction > 0:
is_loss = exit_price < entry_price
else:
is_loss = exit_price > entry_price
if is_loss:
self._consecutive_losses = min(self._consecutive_losses + 1, 5)
else:
self._consecutive_losses = 0
self._position_direction = 0
self._take_profit_level = None
self._entry_price = None
self._entry_volume = 0.0
def _update_history(self, close_price):
self._close_history.append(close_price)
max_history = max(self.HoursToCheckTrend, 2)
if len(self._close_history) > max_history:
self._close_history = self._close_history[len(self._close_history) - max_history:]
def _calculate_order_volume(self):
base_vol = self.Volume
if base_vol <= 0:
return 0.0
losses = self._consecutive_losses
if losses >= 5:
multiplier = float(self.FifthMultiplier)
elif losses == 4:
multiplier = float(self.FourthMultiplier)
elif losses == 3:
multiplier = float(self.ThirdMultiplier)
elif losses == 2:
multiplier = float(self.SecondMultiplier)
elif losses == 1:
multiplier = float(self.FirstMultiplier)
else:
multiplier = 1.0
desired_volume = float(base_vol) * multiplier
if self.MaxVolume > 0 and desired_volume > self.MaxVolume:
desired_volume = self.MaxVolume
return desired_volume
def _get_take_profit_distance(self):
pip_size = self._get_pip_size()
if pip_size > 0:
return self.TakeProfitPips * pip_size
return 0.0
def OnReseted(self):
super(twenty_pips_opposite_last_n_hour_trend_strategy, self).OnReseted()
self._close_history = []
self._entry_price = None
self._take_profit_level = None
self._entry_volume = 0.0
self._position_direction = 0
self._consecutive_losses = 0
self._current_day = None
self._trades_today = 0
def CreateClone(self):
return twenty_pips_opposite_last_n_hour_trend_strategy()