Strategie TwentyPipsOnceADayStrategy
Port des MetaTrader-Experten 20pipsOnceADayOppositeLastNHourTrend, implementiert mit dem StockSharp-High-Level-API. Die Strategie handelt einmal pro konfigurierter Stunde und eröffnet eine konträre Position gegen die Drift der letzten N stündlichen Kerzen. Die Positionsgröße folgt einer Martingalleiter, die das Los nur dann erhöht, wenn ein aktueller Handel mit einem Verlust endete. Die Implementierung erzwingt außerdem einen täglichen Handelsplan, einen optionalen Trailing-Schutz und eine maximale Haltedauer.
Handelslogik
- Die Strategie abonniert stündliche Kerzen (konfigurierbar über
CandleType). - Wenn eine Kerze schließt und die nächste Stunde mit
TradingHourübereinstimmt, wertet die Strategie die Richtung aus:- Vergleichen Sie den Schlusskurs der letzten abgeschlossenen Stunde mit dem Schlusskurs vor
HoursToCheckTrendStunden. - If the market fell over that interval, open a long position (fade the bearish drift).
- Wenn der Markt steigt, eröffnen Sie eine Short-Position.
- Vergleichen Sie den Schlusskurs der letzten abgeschlossenen Stunde mit dem Schlusskurs vor
- Es kann jeweils nur eine Position aktiv sein (gesteuert durch
MaxOrders). - Jeder Trade erbt einen festen Take-Profit und einen optionalen Stop-Loss/Trailing-Stop, beide ausgedrückt in Pips im Verhältnis zur Pip-Größe des Instruments.
- Wenn die Position länger als
OrderMaxAgeSecondsoffen bleibt oder die nächste Stunde außerhalb der durchTradingDayHoursdefinierten zulässigen Sitzung liegt, schließt die Strategie den Handel zwangsweise.
Money-Management
FixedVolumedefiniert das Basislos. Setzen Sie es auf0, um das Los aus dem Portfoliowert mitRiskPercentabzuleiten. Die risikobasierte Dimensionierung spiegelt die ursprüngliche EA-Logik wider:(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. - Eine Martingalleiter erhöht das nächste Los nur dann, wenn der jeweilige historische Handel mit Verlust schloss:
FirstMultipliergilt, wenn der letzte Trade verloren ging.SecondMultipliergilt, wenn der letzte Trade gewonnen hat, der vorherige jedoch verloren hat.- Die Kette wird bis
FifthMultiplierfortgesetzt und entspricht der ursprünglichen fünfstufigen Eskalation.
Parameter
| Parameter | Beschreibung |
|---|---|
FixedVolume |
Festes Handelsvolumen. Verwenden Sie 0, um die risikobasierte Größenanpassung zu aktivieren. |
MinVolume / MaxVolume |
Untere und obere Grenzen werden nach der Größenbestimmung angewendet. |
RiskPercent |
Portfolio-Prozentsatz in Volumen umgerechnet, wenn FixedVolume gleich Null ist. |
MaxOrders |
Maximale Anzahl gleichzeitig offener Positionen (Standard 1). |
TradingHour |
Stunde des Tages (0-23), zu der neue Geschäfte beginnen können. |
TradingDayHours |
Durch Kommas getrennte Stunden oder Bereiche (z. B. 0-7,13-22), die weiterhin für offene Positionen in Frage kommen. Wenn die nächste Stunde außerhalb dieser Menge liegt, wird die Strategie beendet. |
HoursToCheckTrend |
Rückblick in stündlichen Kerzen, die für den konträren Vergleich verwendet werden. |
OrderMaxAgeSeconds |
Maximale Haltezeit in Sekunden, bevor ein Ausgang erzwungen wird. |
FirstMultiplier … FifthMultiplier |
Martingale Multiplikatoren, die den in den letzten fünf geschlossenen Geschäften festgestellten Verlusten zugewiesen wurden. |
StopLossPips |
Anfängliche Stop-Loss-Distanz in Pips. Zum Deaktivieren auf 0 setzen. |
TrailingStopPips |
Trailing-Stop-Distanz in Pips. Zum Deaktivieren auf 0 setzen. |
TakeProfitPips |
Nehmen Sie die Gewinnentfernung in Pips. |
CandleType |
Candle type used for signal generation (defaults to 1-hour time frame). |
Risk Controls and Exits
- Take-Profit/Stop-Loss: Konfiguriert über
TakeProfitPipsundStopLossPipsmit automatischer Umrechnung in Instrumentenpreiseinheiten. - Trailing Stop: Wenn aktiviert, wird der Stop nachgezogen, sobald der Trade mehr als die konfigurierte Anzahl an Pips gewinnt.
- Timeout-Exit: Positionen, die älter als
OrderMaxAgeSecondssind, werden zum aktuellen Schlusskurs der Kerze geschlossen. - Sitzungsfilter: Positionen werden geschlossen, wenn die kommende Stunde nicht in
TradingDayHoursenthalten ist.
Nutzungshinweise
- Die Strategie funktioniert mit jedem Instrument, das stündliche Kerzen und einen gültigen
PriceStepbietet. Wenn das Instrument gebrochene Pips (3 oder 5 Dezimalstellen) verwendet, wird die Pip-Größe automatisch angepasst. - Um das Verhalten von MetaTrader zu reproduzieren, führen Sie die Strategie auf einem einzelnen Instrument aus, wobei
CandleTypeauf einen stündlichen Zeitrahmen eingestellt ist, und behalten Sie den StandardwertTradingDayHours(0-23) bei, um den Handel über den ganzen Tag hinweg zu ermöglichen. - Die Martingalleiter geht von höchstens fünf relevanten historischen Berufen aus. Durch das Zurücksetzen der Strategie wird dieser Verlauf gelöscht.
- Da die Strategie zu Beginn der konfigurierten Stunde unter Verwendung geschlossener Kerzendaten handelt, erfolgt die Ausführung zu dem Preis, der zu Beginn der neuen Stunde verfügbar ist.
Dateien
CS/TwentyPipsOnceADayStrategy.cs– Haupt-C#-Implementierung.README.md– Englische Dokumentation (diese Datei).README_zh.md– Chinesische Dokumentation.README_ru.md– Russische Dokumentation.
Auf Python-Ports wird bei dieser Konvertierung bewusst verzichtet.
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()