TwentyPipsOnceADayStrategy
Port of the MetaTrader expert 20pipsOnceADayOppositeLastNHourTrend implemented with the StockSharp high-level API. The strategy trades once per configured hour and opens a contrarian position against the drift of the last N hourly candles. Position size follows a martingale ladder that increases the lot only when a recent trade ended with a loss. The implementation also enforces a daily trading schedule, optional trailing protection, and a maximum holding period.
Trading Logic
- The strategy subscribes to hourly candles (configurable through
CandleType). - When a candle closes and the next hour matches
TradingHour, the strategy evaluates the direction:- Compare the closing price of the last completed hour with the close
HoursToCheckTrendhours ago. - If the market fell over that interval, open a long position (fade the bearish drift).
- If the market rose, open a short position.
- Compare the closing price of the last completed hour with the close
- Only one position can be active at a time (controlled by
MaxOrders). - Each trade inherits a fixed take profit and optional stop-loss/trailing stop, both expressed in pips relative to the instrument's pip size.
- If the position remains open longer than
OrderMaxAgeSecondsor the next hour is outside the allowed session defined byTradingDayHours, the strategy forcefully closes the trade.
Money Management
FixedVolumedefines the base lot. Set it to0to derive the lot from the portfolio value usingRiskPercent. The risk-based sizing mirrors the original EA logic:(portfolio value * RiskPercent) / 1000.- After the base lot is calculated it is clamped by both the instrument's
VolumeMin/VolumeMax/VolumeStepand the user-definedMinVolume/MaxVolumebounds. - A martingale ladder increases the next lot only if the respective historical trade closed at a loss:
FirstMultiplierapplies when the most recent trade lost.SecondMultiplierapplies when the latest trade won but the previous one lost.- The chain continues up to
FifthMultiplier, matching the original five-step escalation.
Parameters
| Parameter | Description |
|---|---|
FixedVolume |
Fixed trading volume. Use 0 to enable risk-based sizing. |
MinVolume / MaxVolume |
Lower and upper bounds applied after sizing. |
RiskPercent |
Portfolio percentage converted into volume when FixedVolume equals zero. |
MaxOrders |
Maximum number of simultaneously open positions (default 1). |
TradingHour |
Hour of the day (0-23) when new trades may start. |
TradingDayHours |
Comma-separated hours or ranges (e.g. 0-7,13-22) that remain eligible for open positions. When the next hour is outside this set, the strategy exits. |
HoursToCheckTrend |
Lookback in hourly candles used for the contrarian comparison. |
OrderMaxAgeSeconds |
Maximum holding time in seconds before forcing an exit. |
FirstMultiplier … FifthMultiplier |
Martingale multipliers assigned to losses found in the last five closed trades. |
StopLossPips |
Initial stop loss distance in pips. Set to 0 to disable. |
TrailingStopPips |
Trailing stop distance in pips. Set to 0 to disable. |
TakeProfitPips |
Take profit distance in pips. |
CandleType |
Candle type used for signal generation (defaults to 1-hour time frame). |
Risk Controls and Exits
- Take profit / stop loss: Configured through
TakeProfitPipsandStopLossPipswith automatic conversion to instrument price units. - Trailing stop: If enabled, the stop is trailed once the trade gains more than the configured number of pips.
- Time-out exit: Positions older than
OrderMaxAgeSecondsare closed at the current candle close price. - Session filter: Positions are closed when the upcoming hour is not included in
TradingDayHours.
Usage Notes
- The strategy works with any instrument that provides hourly candles and a valid
PriceStep. When the instrument uses fractional pips (3 or 5 decimals), the pip size is automatically adjusted. - To replicate the MetaTrader behaviour, run the strategy on a single instrument with
CandleTypeset to an hourly timeframe and keep the defaultTradingDayHours(0-23) to allow trading throughout the day. - The martingale ladder assumes at most five relevant historical trades. Resetting the strategy clears this history.
- Because the strategy trades at the open of the configured hour using closed candle data, fills occur at the price available when the new hour starts.
Files
CS/TwentyPipsOnceADayStrategy.cs– main C# implementation.README.md– English documentation (this file).README_zh.md– Chinese documentation.README_ru.md– Russian documentation.
Python ports are intentionally omitted for this conversion.
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()