TwentyPipsOnceADayEstrategia
Puerto del experto MetaTrader 20pipsOnceADayOppositeLastNHourTrend implementado con la API de alto nivel de StockSharp. La estrategia se negocia una vez por hora configurada y abre una posición contraria contra la deriva de las últimas N velas horarias. El tamaño de la posición sigue una escalera de martingala que aumenta el lote sólo cuando una operación reciente terminó con una pérdida. La implementación también impone un cronograma de negociación diario, protección de seguimiento opcional y un período de tenencia máximo.
Lógica de trading
- La estrategia se suscribe a velas horarias (configurables a través de
CandleType). - Cuando una vela se cierra y la siguiente hora coincide con
TradingHour, la estrategia evalúa la dirección:- Compare el precio de cierre de la última hora completa con el precio de cierre de hace
HoursToCheckTrendhoras. - Si el mercado cayó durante ese intervalo, abra una posición larga (desvanezca la tendencia bajista).
- Si el mercado subió, abra una posición corta.
- Compare el precio de cierre de la última hora completa con el precio de cierre de hace
- Sólo una posición puede estar activa a la vez (controlada por
MaxOrders). - Cada operación hereda una toma de ganancias fija y un stop-loss/trailing stop opcional, ambos expresados en pips en relación con el tamaño del pip del instrumento.
- Si la posición permanece abierta por más de
OrderMaxAgeSecondso la siguiente hora está fuera de la sesión permitida definida porTradingDayHours, la estrategia cierra la operación a la fuerza.
Gestión monetaria
FixedVolumedefine el lote base. Configúrelo en0para derivar el lote del valor de la cartera usandoRiskPercent. El tamaño basado en el riesgo refleja la lógica EA original:(portfolio value * RiskPercent) / 1000.- Una vez calculado el lote base, se sujeta tanto por los límites
VolumeMin/VolumeMax/VolumeStepdel instrumento como por los límitesMinVolume/MaxVolumedefinidos por el usuario. - Una escalera martingala aumenta el siguiente lote sólo si la respectiva operación histórica cerró con pérdidas:
FirstMultiplierse aplica cuando se perdió la operación más reciente.SecondMultiplierse aplica cuando la última operación ganó pero la anterior perdió.- La cadena continúa hasta
FifthMultiplier, coincidiendo con la escalada original de cinco pasos.
Parámetros
| Parámetro | Descripción |
|---|---|
FixedVolume |
Volumen de negociación fijo. Utilice 0 para habilitar el dimensionamiento basado en el riesgo. |
MinVolume / MaxVolume |
Límites superior e inferior aplicados después del dimensionamiento. |
RiskPercent |
Porcentaje de cartera convertido en volumen cuando FixedVolume es igual a cero. |
MaxOrders |
Número máximo de posiciones abiertas simultáneamente (por defecto 1). |
TradingHour |
Hora del día (0-23) en la que pueden comenzar nuevas operaciones. |
TradingDayHours |
Horas o rangos separados por comas (por ejemplo, 0-7,13-22) que siguen siendo elegibles para puestos vacantes. Cuando la siguiente hora está fuera de este conjunto, la estrategia sale. |
HoursToCheckTrend |
Mirada retrospectiva en velas horarias utilizadas para la comparación contraria. |
OrderMaxAgeSeconds |
Tiempo máximo de espera en segundos antes de forzar una salida. |
FirstMultiplier … FifthMultiplier |
Martingale multiplicadores asignados a las pérdidas encontradas en las últimas cinco operaciones cerradas. |
StopLossPips |
Distancia inicial de stop loss en pips. Establezca en 0 para desactivar. |
TrailingStopPips |
Distancia del trailing stop en pips. Establezca en 0 para desactivar. |
TakeProfitPips |
Tomar distancia de ganancias en pips. |
CandleType |
Tipo de vela utilizado para la generación de señales (el valor predeterminado es un período de tiempo de 1 hora). |
Controles de Riesgos y Salidas
- Take Profit / Stop Loss: Configurado a través de
TakeProfitPipsyStopLossPipscon conversión automática a unidades de precio de instrumento. - Parada dinámica: si está habilitado, la parada se rastrea una vez que la operación gana más que el número de pips configurado.
- Salida de tiempo de espera: Las posiciones anteriores a
OrderMaxAgeSecondsse cierran al precio de cierre de vela actual. - Filtro de sesión: Las posiciones se cierran cuando la próxima hora no está incluida en
TradingDayHours.
Notas de uso
- La estrategia funciona con cualquier instrumento que proporcione velas horarias y un
PriceStepválido. Cuando el instrumento utiliza pips fraccionarios (3 o 5 decimales), el tamaño del pip se ajusta automáticamente. - Para replicar el comportamiento de MetaTrader, ejecute la estrategia en un solo instrumento con
CandleTypeconfigurado en un período de tiempo por hora y mantenga elTradingDayHourspredeterminado (0-23) para permitir el comercio durante todo el día. - La escalera martingala supone como máximo cinco oficios históricos relevantes. Restablecer la estrategia borra este historial.
- Debido a que la estrategia opera al inicio de la hora configurada utilizando datos de velas cerradas, los llenados se producen al precio disponible cuando comienza la nueva hora.
Archivos
CS/TwentyPipsOnceADayStrategy.cs– implementación principal de C#.README.md– Documentación en inglés (este archivo).README_zh.md– Documentación china.README_ru.md– Documentación rusa.
Los puertos de Python se omiten intencionalmente para esta conversión.
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;
using System.Globalization;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Port of the MetaTrader expert "20pipsOnceADayOppositeLastNHourTrend".
/// Trades once per configured hour against the drift of the last N hourly candles and applies martingale style sizing.
/// Includes daily session control, optional trailing protection, and automatic position aging.
/// </summary>
public class TwentyPipsOnceADayStrategy : Strategy
{
private readonly StrategyParam<decimal> _fixedVolume;
private readonly StrategyParam<decimal> _minVolume;
private readonly StrategyParam<decimal> _maxVolume;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<int> _maxOrders;
private readonly StrategyParam<int> _tradingHour;
private readonly StrategyParam<string> _tradingDayHours;
private readonly StrategyParam<int> _hoursToCheckTrend;
private readonly StrategyParam<int> _orderMaxAgeSeconds;
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<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _trailingStopPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _closeHistory = new();
private readonly List<bool> _recentLosses = new(5);
private readonly HashSet<int> _allowedHours = new();
private SimpleMovingAverage _sma;
private DateTime? _lastTradeBarTime;
private DateTime? _entryTime;
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal? _takeProfitPrice;
private decimal _entryVolume;
private int _positionDirection;
private decimal _pipSize;
/// <summary>
/// Initializes a new instance of <see cref="TwentyPipsOnceADayStrategy"/>.
/// </summary>
public TwentyPipsOnceADayStrategy()
{
_fixedVolume = Param(nameof(FixedVolume), 0.1m)
.SetDisplay("Fixed Volume", "Fixed trading volume (set to 0 to use risk based sizing)", "Risk");
_minVolume = Param(nameof(MinVolume), 0.1m)
.SetDisplay("Min Volume", "Lower volume bound applied after sizing", "Risk");
_maxVolume = Param(nameof(MaxVolume), 5m)
.SetDisplay("Max Volume", "Upper volume bound applied after sizing", "Risk");
_riskPercent = Param(nameof(RiskPercent), 5m)
.SetDisplay("Risk Percent", "Percentage of portfolio value converted into volume when fixed size is disabled", "Risk");
_maxOrders = Param(nameof(MaxOrders), 1)
.SetGreaterThanZero()
.SetDisplay("Max Orders", "Maximum number of simultaneously open positions", "Trading");
_tradingHour = Param(nameof(TradingHour), 7)
.SetRange(0, 23)
.SetDisplay("Trading Hour", "Hour of day (0-23) when the strategy evaluates signals", "Schedule");
_tradingDayHours = Param(nameof(TradingDayHours), "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23")
.SetDisplay("Trading Day Hours", "Comma separated list or ranges of allowed session hours", "Schedule");
_hoursToCheckTrend = Param(nameof(HoursToCheckTrend), 30)
.SetGreaterThanZero()
.SetDisplay("Hours To Check", "Number of historical hourly closes used for the contrarian check", "Signals");
_orderMaxAgeSeconds = Param(nameof(OrderMaxAgeSeconds), 75600)
.SetGreaterThanZero()
.SetDisplay("Max Position Age (s)", "Maximum holding time in seconds before forcing an exit", "Risk");
_firstMultiplier = Param(nameof(FirstMultiplier), 4)
.SetGreaterThanZero()
.SetDisplay("First Multiplier", "Multiplier applied after the most recent loss", "Money Management");
_secondMultiplier = Param(nameof(SecondMultiplier), 2)
.SetGreaterThanZero()
.SetDisplay("Second Multiplier", "Multiplier applied when the last win was preceded by a loss", "Money Management");
_thirdMultiplier = Param(nameof(ThirdMultiplier), 5)
.SetGreaterThanZero()
.SetDisplay("Third Multiplier", "Multiplier applied when the third latest trade was a loss", "Money Management");
_fourthMultiplier = Param(nameof(FourthMultiplier), 5)
.SetGreaterThanZero()
.SetDisplay("Fourth Multiplier", "Multiplier applied when the fourth latest trade was a loss", "Money Management");
_fifthMultiplier = Param(nameof(FifthMultiplier), 1)
.SetGreaterThanZero()
.SetDisplay("Fifth Multiplier", "Multiplier applied when the fifth latest trade was a loss", "Money Management");
_stopLossPips = Param(nameof(StopLossPips), 50m)
.SetDisplay("Stop Loss (pips)", "Stop loss distance expressed in pips", "Risk");
_trailingStopPips = Param(nameof(TrailingStopPips), 0m)
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance expressed in pips", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 10m)
.SetDisplay("Take Profit (pips)", "Take profit distance expressed in pips", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for signal calculations", "Market Data");
}
/// <summary>
/// Fixed trading volume. Set to zero to enable risk based sizing.
/// </summary>
public decimal FixedVolume
{
get => _fixedVolume.Value;
set => _fixedVolume.Value = value;
}
/// <summary>
/// Minimum allowed trading volume.
/// </summary>
public decimal MinVolume
{
get => _minVolume.Value;
set => _minVolume.Value = value;
}
/// <summary>
/// Maximum allowed trading volume.
/// </summary>
public decimal MaxVolume
{
get => _maxVolume.Value;
set => _maxVolume.Value = value;
}
/// <summary>
/// Portfolio percentage converted into volume when <see cref="FixedVolume"/> equals zero.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Maximum number of simultaneously open positions.
/// </summary>
public int MaxOrders
{
get => _maxOrders.Value;
set => _maxOrders.Value = value;
}
/// <summary>
/// Hour of day when new positions may be opened.
/// </summary>
public int TradingHour
{
get => _tradingHour.Value;
set => _tradingHour.Value = value;
}
/// <summary>
/// Comma separated list or ranges of allowed trading hours.
/// </summary>
public string TradingDayHours
{
get => _tradingDayHours.Value;
set
{
_tradingDayHours.Value = value;
UpdateTradingHours();
}
}
/// <summary>
/// Lookback depth measured in hourly candles.
/// </summary>
public int HoursToCheckTrend
{
get => _hoursToCheckTrend.Value;
set => _hoursToCheckTrend.Value = value;
}
/// <summary>
/// Maximum holding time before a position is forcefully closed.
/// </summary>
public int OrderMaxAgeSeconds
{
get => _orderMaxAgeSeconds.Value;
set => _orderMaxAgeSeconds.Value = value;
}
/// <summary>
/// Multiplier used after the latest loss.
/// </summary>
public int FirstMultiplier
{
get => _firstMultiplier.Value;
set => _firstMultiplier.Value = value;
}
/// <summary>
/// Multiplier used when only the second latest trade was a loss.
/// </summary>
public int SecondMultiplier
{
get => _secondMultiplier.Value;
set => _secondMultiplier.Value = value;
}
/// <summary>
/// Multiplier used when the third latest trade was a loss.
/// </summary>
public int ThirdMultiplier
{
get => _thirdMultiplier.Value;
set => _thirdMultiplier.Value = value;
}
/// <summary>
/// Multiplier used when the fourth latest trade was a loss.
/// </summary>
public int FourthMultiplier
{
get => _fourthMultiplier.Value;
set => _fourthMultiplier.Value = value;
}
/// <summary>
/// Multiplier used when the fifth latest trade was a loss.
/// </summary>
public int FifthMultiplier
{
get => _fifthMultiplier.Value;
set => _fifthMultiplier.Value = value;
}
/// <summary>
/// Stop loss distance in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Trailing stop distance in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Take profit distance in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Candle type used to process signals.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_closeHistory.Clear();
_recentLosses.Clear();
_lastTradeBarTime = null;
_entryTime = null;
_entryPrice = null;
_stopPrice = null;
_takeProfitPrice = null;
_entryVolume = 0m;
_positionDirection = 0;
_pipSize = 0m;
UpdateTradingHours();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = CalculatePipSize();
UpdateTradingHours();
_sma = new SimpleMovingAverage { Length = 2 };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(_sma, ProcessCandle).Start();
StartProtection(null, null);
}
private void ProcessCandle(ICandleMessage candle, decimal smaValue)
{
if (candle.State != CandleStates.Finished)
return;
AddCloseToHistory(candle.ClosePrice);
if (_positionDirection != 0)
{
ManageOpenPosition(candle);
if (_positionDirection != 0)
{
EnforceSessionLimits(candle);
}
}
TryOpenPosition(candle);
}
private void AddCloseToHistory(decimal closePrice)
{
if (HoursToCheckTrend <= 0)
return;
_closeHistory.Insert(0, closePrice);
var required = Math.Max(HoursToCheckTrend, 5);
if (_closeHistory.Count > required)
{
_closeHistory.RemoveRange(required, _closeHistory.Count - required);
}
}
private void ManageOpenPosition(ICandleMessage candle)
{
if (_positionDirection == 0 || _entryPrice is not decimal entryPrice)
return;
var direction = _positionDirection;
var closePrice = candle.ClosePrice;
var stopDistance = StopLossPips * _pipSize;
if (_stopPrice is null && stopDistance > 0m)
{
_stopPrice = direction > 0
? entryPrice - stopDistance
: entryPrice + stopDistance;
}
var trailingDistance = TrailingStopPips * _pipSize;
if (trailingDistance > 0m)
{
if (direction > 0)
{
var profit = closePrice - entryPrice;
if (profit > trailingDistance)
{
var candidate = closePrice - trailingDistance;
if (_stopPrice is null || candidate > _stopPrice.Value)
{
_stopPrice = candidate;
}
}
}
else
{
var profit = entryPrice - closePrice;
if (profit > trailingDistance)
{
var candidate = closePrice + trailingDistance;
if (_stopPrice is null || candidate < _stopPrice.Value)
{
_stopPrice = candidate;
}
}
}
}
if (_takeProfitPrice is decimal target)
{
var hitTarget = direction > 0
? candle.HighPrice >= target
: candle.LowPrice <= target;
if (hitTarget)
{
ExitPosition(target);
return;
}
}
if (_stopPrice is decimal stopLevel)
{
var hitStop = direction > 0
? candle.LowPrice <= stopLevel
: candle.HighPrice >= stopLevel;
if (hitStop)
{
ExitPosition(stopLevel);
return;
}
}
if (OrderMaxAgeSeconds > 0 && _entryTime is DateTime entryTime)
{
var age = candle.CloseTime - entryTime;
if (age.TotalSeconds >= OrderMaxAgeSeconds)
{
ExitPosition(candle.ClosePrice);
}
}
}
private void EnforceSessionLimits(ICandleMessage candle)
{
if (_positionDirection == 0)
return;
var nextHour = candle.CloseTime.Hour;
if (!IsHourAllowed(nextHour))
{
ExitPosition(candle.ClosePrice);
}
}
private void TryOpenPosition(ICandleMessage candle)
{
if (MaxOrders <= 0 || _positionDirection != 0)
return;
var nextHour = candle.CloseTime.Hour;
if (nextHour != TradingHour || !IsHourAllowed(nextHour))
return;
if (_lastTradeBarTime.HasValue && _lastTradeBarTime.Value == candle.CloseTime)
return;
if (_closeHistory.Count < HoursToCheckTrend)
return;
var lastClose = _closeHistory[0];
var index = HoursToCheckTrend - 1;
if (index < 0 || index >= _closeHistory.Count)
return;
var referenceClose = _closeHistory[index];
if (lastClose == referenceClose)
return;
var goLong = referenceClose > lastClose;
var volume = CalculateOrderVolume();
if (volume <= 0m)
return;
var entryPrice = candle.ClosePrice;
if (goLong)
{
BuyMarket(volume);
_positionDirection = 1;
}
else
{
SellMarket(volume);
_positionDirection = -1;
}
_entryPrice = entryPrice;
_entryTime = candle.CloseTime;
_entryVolume = volume;
_lastTradeBarTime = candle.CloseTime;
var stopDistance = StopLossPips * _pipSize;
_stopPrice = stopDistance > 0m
? _positionDirection > 0
? entryPrice - stopDistance
: entryPrice + stopDistance
: null;
var takeDistance = TakeProfitPips * _pipSize;
_takeProfitPrice = takeDistance > 0m
? _positionDirection > 0
? entryPrice + takeDistance
: entryPrice - takeDistance
: null;
}
private void ExitPosition(decimal exitPrice)
{
var direction = _positionDirection;
if (direction == 0)
return;
var volume = Math.Abs(Position);
if (volume <= 0m)
{
volume = Math.Abs(_entryVolume);
}
if (volume <= 0m)
{
ResetPositionState();
return;
}
if (direction > 0)
{
SellMarket(volume);
}
else
{
BuyMarket(volume);
}
if (_entryPrice is decimal entryPrice)
{
var isLoss = direction > 0
? exitPrice < entryPrice
: exitPrice > entryPrice;
RegisterTradeResult(isLoss);
}
else
{
ResetPositionState();
}
}
private void RegisterTradeResult(bool isLoss)
{
_recentLosses.Insert(0, isLoss);
if (_recentLosses.Count > 5)
{
_recentLosses.RemoveRange(5, _recentLosses.Count - 5);
}
ResetPositionState();
}
private void ResetPositionState()
{
_positionDirection = 0;
_entryPrice = null;
_entryTime = null;
_entryVolume = 0m;
_stopPrice = null;
_takeProfitPrice = null;
}
private decimal CalculateOrderVolume()
{
var baseVolume = FixedVolume;
if (baseVolume <= 0m)
{
baseVolume = CalculateRiskVolume();
}
if (baseVolume <= 0m)
return 0m;
var multiplier = GetMultiplierFromHistory();
var desired = AlignVolume(baseVolume * multiplier);
return desired;
}
private decimal CalculateRiskVolume()
{
if (RiskPercent <= 0m)
return MinVolume > 0m ? MinVolume : 0m;
var portfolio = Portfolio;
var balance = portfolio?.CurrentValue ?? portfolio?.BeginValue ?? 0m;
if (balance <= 0m)
return MinVolume > 0m ? MinVolume : 0m;
var raw = balance * RiskPercent / 1000m;
return raw;
}
private decimal GetMultiplierFromHistory()
{
for (var index = 0; index < _recentLosses.Count && index < 5; index++)
{
if (!_recentLosses[index])
continue;
return index switch
{
0 => FirstMultiplier,
1 => SecondMultiplier,
2 => ThirdMultiplier,
3 => FourthMultiplier,
4 => FifthMultiplier,
_ => 1m,
};
}
return 1m;
}
private decimal AlignVolume(decimal volume)
{
var security = Security;
if (security != null)
{
var min = security.MinVolume ?? 0m;
var max = security.MaxVolume ?? decimal.MaxValue;
var step = security.VolumeStep ?? 0m;
if (step > 0m)
{
volume = Math.Round(volume / step) * step;
}
if (min > 0m && volume < min)
volume = min;
if (max > 0m && volume > max)
volume = max;
}
if (MinVolume > 0m && volume < MinVolume)
volume = MinVolume;
if (MaxVolume > 0m && volume > MaxVolume)
volume = MaxVolume;
return volume;
}
private decimal CalculatePipSize()
{
var security = Security;
if (security == null)
return 0.0001m;
var step = security.PriceStep ?? 0.0001m;
var decimals = security.Decimals;
if ((decimals == 3 || decimals == 5) && step > 0m)
{
return step * 10m;
}
return step > 0m ? step : 0.0001m;
}
private bool IsHourAllowed(int hour)
{
if (_allowedHours.Count == 0)
return true;
return _allowedHours.Contains(hour);
}
private void UpdateTradingHours()
{
_allowedHours.Clear();
var raw = _tradingDayHours.Value;
if (raw.IsEmptyOrWhiteSpace())
{
FillFullDay();
return;
}
var parts = raw.Split(',', StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts)
{
var trimmed = part.Trim();
if (trimmed.Length == 0)
continue;
if (trimmed.Contains('-', StringComparison.Ordinal))
{
var rangeParts = trimmed.Split('-', StringSplitOptions.RemoveEmptyEntries);
if (rangeParts.Length != 2)
continue;
if (TryParseHour(rangeParts[0], out var start) && TryParseHour(rangeParts[1], out var end))
{
if (end < start)
{
(end, start) = (start, end);
}
for (var hour = start; hour <= end; hour++)
{
_allowedHours.Add(hour);
}
}
}
else if (TryParseHour(trimmed, out var value))
{
_allowedHours.Add(value);
}
}
if (_allowedHours.Count == 0)
{
FillFullDay();
}
}
private static bool TryParseHour(string text, out int hour)
{
if (int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out hour))
{
if (hour >= 0 && hour <= 23)
return true;
}
hour = 0;
return false;
}
private void FillFullDay()
{
_allowedHours.Clear();
for (var hour = 0; hour < 24; hour++)
{
_allowedHours.Add(hour);
}
}
}
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
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import SimpleMovingAverage
class twenty_pips_once_a_day_strategy(Strategy):
def __init__(self):
super(twenty_pips_once_a_day_strategy, self).__init__()
self._fixed_volume = self.Param("FixedVolume", 0.1) \
.SetDisplay("Fixed Volume", "Fixed trading volume (set to 0 to use risk based sizing)", "Risk")
self._min_volume = self.Param("MinVolume", 0.1) \
.SetDisplay("Min Volume", "Lower volume bound applied after sizing", "Risk")
self._max_volume = self.Param("MaxVolume", 5.0) \
.SetDisplay("Max Volume", "Upper volume bound applied after sizing", "Risk")
self._risk_percent = self.Param("RiskPercent", 5.0) \
.SetDisplay("Risk Percent", "Percentage of portfolio value converted into volume", "Risk")
self._max_orders = self.Param("MaxOrders", 1) \
.SetDisplay("Max Orders", "Maximum number of simultaneously open positions", "Trading")
self._trading_hour = self.Param("TradingHour", 7) \
.SetDisplay("Trading Hour", "Hour of day when the strategy evaluates signals", "Schedule")
self._trading_day_hours = self.Param("TradingDayHours", "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23") \
.SetDisplay("Trading Day Hours", "Comma separated list of allowed session hours", "Schedule")
self._hours_to_check_trend = self.Param("HoursToCheckTrend", 30) \
.SetDisplay("Hours To Check", "Number of historical hourly closes used for the contrarian check", "Signals")
self._order_max_age_seconds = self.Param("OrderMaxAgeSeconds", 75600) \
.SetDisplay("Max Position Age (s)", "Maximum holding time in seconds before forcing an exit", "Risk")
self._first_multiplier = self.Param("FirstMultiplier", 4) \
.SetDisplay("First Multiplier", "Multiplier applied after the most recent loss", "Money Management")
self._second_multiplier = self.Param("SecondMultiplier", 2) \
.SetDisplay("Second Multiplier", "Multiplier applied when the last win was preceded by a loss", "Money Management")
self._third_multiplier = self.Param("ThirdMultiplier", 5) \
.SetDisplay("Third Multiplier", "Multiplier applied when the third latest trade was a loss", "Money Management")
self._fourth_multiplier = self.Param("FourthMultiplier", 5) \
.SetDisplay("Fourth Multiplier", "Multiplier applied when the fourth latest trade was a loss", "Money Management")
self._fifth_multiplier = self.Param("FifthMultiplier", 1) \
.SetDisplay("Fifth Multiplier", "Multiplier applied when the fifth latest trade was a loss", "Money Management")
self._stop_loss_pips = self.Param("StopLossPips", 50.0) \
.SetDisplay("Stop Loss (pips)", "Stop loss distance expressed in pips", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", 0.0) \
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance expressed in pips", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 10.0) \
.SetDisplay("Take Profit (pips)", "Take profit distance expressed in pips", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Timeframe used for signal calculations", "Market Data")
self._close_history = []
self._recent_losses = []
self._allowed_hours = set()
self._sma = None
self._last_trade_bar_time = None
self._entry_time = None
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
self._entry_volume = 0.0
self._position_direction = 0
self._pip_size = 0.0
@property
def FixedVolume(self):
return self._fixed_volume.Value
@property
def MinVolume(self):
return self._min_volume.Value
@property
def MaxVolume(self):
return self._max_volume.Value
@property
def RiskPercent(self):
return self._risk_percent.Value
@property
def MaxOrders(self):
return self._max_orders.Value
@property
def TradingHour(self):
return self._trading_hour.Value
@property
def TradingDayHours(self):
return self._trading_day_hours.Value
@property
def HoursToCheckTrend(self):
return self._hours_to_check_trend.Value
@property
def OrderMaxAgeSeconds(self):
return self._order_max_age_seconds.Value
@property
def FirstMultiplier(self):
return self._first_multiplier.Value
@property
def SecondMultiplier(self):
return self._second_multiplier.Value
@property
def ThirdMultiplier(self):
return self._third_multiplier.Value
@property
def FourthMultiplier(self):
return self._fourth_multiplier.Value
@property
def FifthMultiplier(self):
return self._fifth_multiplier.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def TrailingStopPips(self):
return self._trailing_stop_pips.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def CandleType(self):
return self._candle_type.Value
def _calculate_pip_size(self):
if self.Security is None:
return 0.0001
ps = self.Security.PriceStep
step = float(ps) if ps is not None else 0.0001
decimals = self.Security.Decimals if self.Security.Decimals is not None else 0
if (decimals == 3 or decimals == 5) and step > 0:
return step * 10.0
return step if step > 0 else 0.0001
def _update_trading_hours(self):
self._allowed_hours = set()
raw = str(self.TradingDayHours) if self.TradingDayHours is not None else ""
if raw.strip() == "":
for h in range(24):
self._allowed_hours.add(h)
return
parts = raw.split(",")
for part in parts:
trimmed = part.strip()
if len(trimmed) == 0:
continue
if "-" in trimmed:
range_parts = trimmed.split("-")
if len(range_parts) != 2:
continue
try:
start = int(range_parts[0].strip())
end = int(range_parts[1].strip())
except ValueError:
continue
if start < 0 or start > 23 or end < 0 or end > 23:
continue
if end < start:
start, end = end, start
for h in range(start, end + 1):
self._allowed_hours.add(h)
else:
try:
val = int(trimmed)
except ValueError:
continue
if 0 <= val <= 23:
self._allowed_hours.add(val)
if len(self._allowed_hours) == 0:
for h in range(24):
self._allowed_hours.add(h)
def _is_hour_allowed(self, hour):
if len(self._allowed_hours) == 0:
return True
return hour in self._allowed_hours
def OnStarted2(self, time):
super(twenty_pips_once_a_day_strategy, self).OnStarted2(time)
self._pip_size = self._calculate_pip_size()
self._update_trading_hours()
self._sma = SimpleMovingAverage()
self._sma.Length = 2
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._sma, self.ProcessCandle).Start()
def ProcessCandle(self, candle, sma_value):
if candle.State != CandleStates.Finished:
return
self._add_close_to_history(float(candle.ClosePrice))
if self._position_direction != 0:
self._manage_open_position(candle)
if self._position_direction != 0:
self._enforce_session_limits(candle)
self._try_open_position(candle)
def _add_close_to_history(self, close_price):
hours = self.HoursToCheckTrend
if hours <= 0:
return
self._close_history.insert(0, close_price)
required = max(hours, 5)
while len(self._close_history) > required:
self._close_history.pop()
def _manage_open_position(self, candle):
if self._position_direction == 0 or self._entry_price is None:
return
direction = self._position_direction
close_price = float(candle.ClosePrice)
entry_price = self._entry_price
pip = self._pip_size
stop_distance = float(self.StopLossPips) * pip
if self._stop_price is None and stop_distance > 0:
if direction > 0:
self._stop_price = entry_price - stop_distance
else:
self._stop_price = entry_price + stop_distance
trailing_distance = float(self.TrailingStopPips) * pip
if trailing_distance > 0:
if direction > 0:
profit = close_price - entry_price
if profit > trailing_distance:
candidate = close_price - trailing_distance
if self._stop_price is None or candidate > self._stop_price:
self._stop_price = candidate
else:
profit = entry_price - close_price
if profit > trailing_distance:
candidate = close_price + trailing_distance
if self._stop_price is None or candidate < self._stop_price:
self._stop_price = candidate
if self._take_profit_price is not None:
if direction > 0:
hit_target = float(candle.HighPrice) >= self._take_profit_price
else:
hit_target = float(candle.LowPrice) <= self._take_profit_price
if hit_target:
self._exit_position(self._take_profit_price)
return
if self._stop_price is not None:
if direction > 0:
hit_stop = float(candle.LowPrice) <= self._stop_price
else:
hit_stop = float(candle.HighPrice) >= self._stop_price
if hit_stop:
self._exit_position(self._stop_price)
return
max_age = self.OrderMaxAgeSeconds
if max_age > 0 and self._entry_time is not None:
age = candle.CloseTime - self._entry_time
if age.TotalSeconds >= max_age:
self._exit_position(close_price)
def _enforce_session_limits(self, candle):
if self._position_direction == 0:
return
next_hour = candle.CloseTime.Hour
if not self._is_hour_allowed(next_hour):
self._exit_position(float(candle.ClosePrice))
def _try_open_position(self, candle):
if self.MaxOrders <= 0 or self._position_direction != 0:
return
next_hour = candle.CloseTime.Hour
if next_hour != self.TradingHour or not self._is_hour_allowed(next_hour):
return
if self._last_trade_bar_time is not None and self._last_trade_bar_time == candle.CloseTime:
return
hours_check = self.HoursToCheckTrend
if len(self._close_history) < hours_check:
return
last_close = self._close_history[0]
index = hours_check - 1
if index < 0 or index >= len(self._close_history):
return
reference_close = self._close_history[index]
if last_close == reference_close:
return
go_long = reference_close > last_close
volume = self._calculate_order_volume()
if volume <= 0:
return
entry_price = float(candle.ClosePrice)
pip = self._pip_size
if go_long:
self.BuyMarket(volume)
self._position_direction = 1
else:
self.SellMarket(volume)
self._position_direction = -1
self._entry_price = entry_price
self._entry_time = candle.CloseTime
self._entry_volume = volume
self._last_trade_bar_time = candle.CloseTime
stop_distance = float(self.StopLossPips) * pip
if stop_distance > 0:
if self._position_direction > 0:
self._stop_price = entry_price - stop_distance
else:
self._stop_price = entry_price + stop_distance
else:
self._stop_price = None
take_distance = float(self.TakeProfitPips) * pip
if take_distance > 0:
if self._position_direction > 0:
self._take_profit_price = entry_price + take_distance
else:
self._take_profit_price = entry_price - take_distance
else:
self._take_profit_price = None
def _exit_position(self, exit_price):
direction = self._position_direction
if direction == 0:
return
volume = Math.Abs(self.Position)
if volume <= 0:
volume = Math.Abs(self._entry_volume)
if volume <= 0:
self._reset_position_state()
return
if direction > 0:
self.SellMarket(volume)
else:
self.BuyMarket(volume)
if self._entry_price is not None:
if direction > 0:
is_loss = exit_price < self._entry_price
else:
is_loss = exit_price > self._entry_price
self._register_trade_result(is_loss)
else:
self._reset_position_state()
def _register_trade_result(self, is_loss):
self._recent_losses.insert(0, is_loss)
while len(self._recent_losses) > 5:
self._recent_losses.pop()
self._reset_position_state()
def _reset_position_state(self):
self._position_direction = 0
self._entry_price = None
self._entry_time = None
self._entry_volume = 0.0
self._stop_price = None
self._take_profit_price = None
def _calculate_order_volume(self):
base_volume = float(self.FixedVolume)
if base_volume <= 0:
base_volume = self._calculate_risk_volume()
if base_volume <= 0:
return 0.0
multiplier = self._get_multiplier_from_history()
desired = self._align_volume(base_volume * multiplier)
return desired
def _calculate_risk_volume(self):
risk_pct = float(self.RiskPercent)
min_vol = float(self.MinVolume)
if risk_pct <= 0:
return min_vol if min_vol > 0 else 0.0
balance = 0.0
if self.Portfolio is not None:
cv = self.Portfolio.CurrentValue
if cv is not None and float(cv) > 0:
balance = float(cv)
elif self.Portfolio.BeginValue is not None:
balance = float(self.Portfolio.BeginValue)
if balance <= 0:
return min_vol if min_vol > 0 else 0.0
raw = balance * risk_pct / 1000.0
return raw
def _get_multiplier_from_history(self):
multipliers = [
float(self.FirstMultiplier),
float(self.SecondMultiplier),
float(self.ThirdMultiplier),
float(self.FourthMultiplier),
float(self.FifthMultiplier),
]
for index in range(min(len(self._recent_losses), 5)):
if not self._recent_losses[index]:
continue
if index < len(multipliers):
return multipliers[index]
return 1.0
return 1.0
def _align_volume(self, volume):
if self.Security is not None:
min_sec = self.Security.MinVolume
max_sec = self.Security.MaxVolume
step_sec = self.Security.VolumeStep
min_val = float(min_sec) if min_sec is not None else 0.0
max_val = float(max_sec) if max_sec is not None else 0.0
step = float(step_sec) if step_sec is not None else 0.0
if step > 0:
volume = round(volume / step) * step
if min_val > 0 and volume < min_val:
volume = min_val
if max_val > 0 and volume > max_val:
volume = max_val
min_vol = float(self.MinVolume)
max_vol = float(self.MaxVolume)
if min_vol > 0 and volume < min_vol:
volume = min_vol
if max_vol > 0 and volume > max_vol:
volume = max_vol
return volume
def OnReseted(self):
super(twenty_pips_once_a_day_strategy, self).OnReseted()
self._close_history = []
self._recent_losses = []
self._allowed_hours = set()
self._sma = None
self._last_trade_bar_time = None
self._entry_time = None
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
self._entry_volume = 0.0
self._position_direction = 0
self._pip_size = 0.0
self._update_trading_hours()
def CreateClone(self):
return twenty_pips_once_a_day_strategy()