20-Pips-Gegenteil-des-Letzten-N-Stunden-Trends-Strategie
Diese StockSharp-Strategie ist eine High-Level-Portierung des MetaTrader-Experten
«20 Pips Opposite Last N Hour Trend». Sie beobachtet stündliche Kerzen, misst
wie sich der Preis während der vorherigen N Stunden verhalten hat, und öffnet dann eine Position in
der entgegengesetzten Richtung, wenn die konfigurierte Handelsstunde endet. Der Trade wird
mit einem festen 20-Pip-Take-Profit-Ziel und einem stündlichen Timeout verwaltet, während
eine Martingale-artige Volumenstaffel nach aufeinanderfolgenden Verlusten angewendet wird.
Die Implementierung verwendet StockSharp's Kerzenabonnements, das Parametersystem,
und Order-Helfer (BuyMarket, SellMarket), sodass sie unverändert innerhalb von
Designer, API, Runner oder Shell ausgeführt werden kann.
Handelslogik
- Die Strategie abonniert den ausgewählten Kerzentyp (Standard: 1-Stunden-Bars).
- Für jede fertige Kerze speichert sie den Schlusskurs in einem internen Verlauf.
- Wenn eine Kerze mit
OpenTime.Hour == TradingHour abgeschlossen ist und genügend
Verlauf vorhanden ist:
- Vergleichen Sie den Schluss, der
HoursToCheckTrend Bars zurück lag, mit dem
vorherigen Schluss (1 Bar zurück).
- Wenn der Preis in diesem Fenster gefallen ist (bärische Drift) kauft die Strategie;
wenn der Preis gestiegen ist (bullische Drift) verkauft sie. Gleiche Schlüsse überspringen den Trade.
- Nur ein Trade wird pro Tag geöffnet und ausschließlich zur konfigurierten Handelsstunde.
Alle anderen Kerzen werden ausschließlich für die Verwaltung verwendet.
Positionsverwaltung
- Ein 20-Pip-Ziel (angepasst für 3/5-stellige Symbole) wird direkt nach dem
Einstieg berechnet. Wenn eine fertige Kerze zeigt, dass das Hoch/Tief das Ziel berührt hat, wird die
Position auf diesem Level geschlossen.
- Wenn das Ziel nicht während der nächsten Stunde erreicht wird, wird die Position am
Ende der folgenden Kerze geschlossen, um Übernacht-Exposition zu vermeiden.
- Tageszähler werden automatisch zurückgesetzt, wenn ein neuer Handelstag beginnt, damit
das nächste berechtigte Signal in der folgenden Sitzung auslösen kann.
Geldmanagement
Volume setzt die Basisordergröße. MaxVolume begrenzt die resultierende Größe jedes
Martingale-Schritts.
- Nach einem verlierenden Ausstieg erhöht die Strategie die nächste Position um den
entsprechenden Multiplikator: erster Verlust →
FirstMultiplier, zweiter Verlust →
SecondMultiplier, usw. Verlustreihen über fünf Trades hinaus verwenden den fünften
Multiplikator. Jeder profitable oder Breakeven-Schluss setzt die Sequenz zurück.
- Volumenberechnungen basieren auf dem zuletzt ausgeführten Positionspreis, sodass die Gewinn/Verlust-
Erkennung auch ohne vollständige Broker-PnL-Daten deterministisch bleibt.
Parameter
| Parameter |
Standard |
Beschreibung |
MaxPositions |
9 |
Maximale Trades erlaubt pro Tag. Auf 0 setzen zum Deaktivieren. |
Volume |
0.1 |
Basisvolumen für den ersten Trade einer Reihe. |
MaxVolume |
5 |
Hartes Limit für das angepasste Volumen nach Multiplikatoren. |
TakeProfitPips |
20 |
Take-Profit-Abstand in Pips. 0 deaktiviert den TP. |
TradingHour |
7 |
Stunde des Tages (0-23), die für das Öffnen einer Position berechtigt ist. |
HoursToCheckTrend |
24 |
Anzahl der stündlichen Schlüsse zur Messung des vorherigen Trends. |
FirstMultiplier |
2 |
Multiplikator nach dem ersten aufeinanderfolgenden Verlust. |
SecondMultiplier |
4 |
Multiplikator nach dem zweiten aufeinanderfolgenden Verlust. |
ThirdMultiplier |
8 |
Multiplikator nach dem dritten aufeinanderfolgenden Verlust. |
FourthMultiplier |
16 |
Multiplikator nach dem vierten aufeinanderfolgenden Verlust. |
FifthMultiplier |
32 |
Multiplikator ab dem fünften Verlust aufwärts. |
CandleType |
H1 |
Kerzendatentyp für Signalgenerierung und Verwaltung. |
Zusätzliche Hinweise
- Die Pip-Größe wird aus
Security.PriceStep und der Anzahl der Dezimalstellen berechnet, damit
das 20-Pip-Ziel auf 4- und 5-stelligen FX-Symbolen korrekt funktioniert.
StartProtection() wird beim Start der Strategie aufgerufen und aktiviert die eingebauten
StockSharp-Schutzmaßnahmen (Auto-Stop für ungebundene Positionen, Portfolio-Resets).
- Die Logik verwendet nur fertige Kerzen und liest keine Indikatorwerte
direkt, was den Richtlinien aus
AGENTS.md entspricht.
Risikohinweis: Martingale-artiges Positions-Sizing kann zu erheblichen
Drawdowns führen. Testen Sie die Parameter immer an historischen Daten und verwenden Sie umsichtige
Risikolimits, bevor Sie im Live-Trading einsetzen.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy that trades against the last N hours trend with a fixed take profit.
/// </summary>
public class TwentyPipsOppositeLastNHourTrendStrategy : Strategy
{
private readonly StrategyParam<int> _maxPositions;
private readonly StrategyParam<decimal> _maxVolume;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<int> _tradingHour;
private readonly StrategyParam<int> _hoursToCheckTrend;
private readonly StrategyParam<int> _firstMultiplier;
private readonly StrategyParam<int> _secondMultiplier;
private readonly StrategyParam<int> _thirdMultiplier;
private readonly StrategyParam<int> _fourthMultiplier;
private readonly StrategyParam<int> _fifthMultiplier;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _closeHistory = new();
private decimal? _entryPrice;
private decimal? _takeProfitLevel;
private decimal _entryVolume;
private int _positionDirection;
private int _consecutiveLosses;
private DateTime? _currentDay;
private int _tradesToday;
public int MaxPositions
{
get => _maxPositions.Value;
set => _maxPositions.Value = value;
}
public decimal MaxVolume
{
get => _maxVolume.Value;
set => _maxVolume.Value = value;
}
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
public int TradingHour
{
get => _tradingHour.Value;
set => _tradingHour.Value = value;
}
public int HoursToCheckTrend
{
get => _hoursToCheckTrend.Value;
set => _hoursToCheckTrend.Value = value;
}
public int FirstMultiplier
{
get => _firstMultiplier.Value;
set => _firstMultiplier.Value = value;
}
public int SecondMultiplier
{
get => _secondMultiplier.Value;
set => _secondMultiplier.Value = value;
}
public int ThirdMultiplier
{
get => _thirdMultiplier.Value;
set => _thirdMultiplier.Value = value;
}
public int FourthMultiplier
{
get => _fourthMultiplier.Value;
set => _fourthMultiplier.Value = value;
}
public int FifthMultiplier
{
get => _fifthMultiplier.Value;
set => _fifthMultiplier.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public TwentyPipsOppositeLastNHourTrendStrategy()
{
_maxPositions = Param(nameof(MaxPositions), 9)
.SetGreaterThanZero()
.SetDisplay("Max Positions", "Maximum trades per day", "Trading");
_maxVolume = Param(nameof(MaxVolume), 5m)
.SetGreaterThanZero()
.SetDisplay("Max Volume", "Maximum allowed volume", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 20m)
.SetGreaterThanZero()
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Trading");
_tradingHour = Param(nameof(TradingHour), 8)
.SetRange(0, 23)
.SetDisplay("Trading Hour", "Hour (0-23) when entries are allowed", "Timing");
_hoursToCheckTrend = Param(nameof(HoursToCheckTrend), 6)
.SetRange(2, 240)
.SetDisplay("Hours To Check", "Lookback hours for trend calculation", "Signals");
_firstMultiplier = Param(nameof(FirstMultiplier), 2)
.SetGreaterThanZero()
.SetDisplay("First Multiplier", "Multiplier after first loss", "Money Management");
_secondMultiplier = Param(nameof(SecondMultiplier), 4)
.SetGreaterThanZero()
.SetDisplay("Second Multiplier", "Multiplier after second loss", "Money Management");
_thirdMultiplier = Param(nameof(ThirdMultiplier), 8)
.SetGreaterThanZero()
.SetDisplay("Third Multiplier", "Multiplier after third loss", "Money Management");
_fourthMultiplier = Param(nameof(FourthMultiplier), 16)
.SetGreaterThanZero()
.SetDisplay("Fourth Multiplier", "Multiplier after fourth loss", "Money Management");
_fifthMultiplier = Param(nameof(FifthMultiplier), 32)
.SetGreaterThanZero()
.SetDisplay("Fifth Multiplier", "Multiplier after fifth loss", "Money Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe to process", "Market Data");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_closeHistory.Clear();
_entryPrice = null;
_takeProfitLevel = null;
_entryVolume = 0m;
_positionDirection = 0;
_consecutiveLosses = 0;
_currentDay = null;
_tradesToday = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
// no fixed protection needed
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var candleDay = candle.OpenTime.Date;
if (_currentDay != candleDay)
{
_currentDay = candleDay;
_tradesToday = 0;
}
if (_positionDirection != 0)
{
if (_takeProfitLevel is decimal target)
{
// Take profit when the candle range touches the desired level.
var hitTarget = _positionDirection > 0
? candle.HighPrice >= target
: candle.LowPrice <= target;
if (hitTarget)
{
ClosePosition(target);
}
}
if (_positionDirection != 0 && candle.OpenTime.Hour != TradingHour)
{
// Close remaining exposure when the configured session hour has passed.
ClosePosition(candle.ClosePrice);
}
}
if (_positionDirection != 0)
{
UpdateHistory(candle.ClosePrice);
return;
}
if (candle.OpenTime.Hour != TradingHour)
{
UpdateHistory(candle.ClosePrice);
return;
}
if (MaxPositions <= 0 || _tradesToday >= MaxPositions)
{
UpdateHistory(candle.ClosePrice);
return;
}
var requiredHistory = Math.Max(HoursToCheckTrend, 2);
if (_closeHistory.Count < requiredHistory)
{
UpdateHistory(candle.ClosePrice);
return;
}
var referenceClose = _closeHistory[_closeHistory.Count - HoursToCheckTrend];
var previousClose = _closeHistory[_closeHistory.Count - 1];
if (previousClose == referenceClose)
{
UpdateHistory(candle.ClosePrice);
return;
}
// Opposite trend logic: buy after bearish drift, sell after bullish drift.
var goLong = previousClose < referenceClose;
var orderVolume = CalculateOrderVolume();
if (orderVolume <= 0)
{
UpdateHistory(candle.ClosePrice);
return;
}
if (goLong)
{
Volume = orderVolume;
BuyMarket();
_positionDirection = 1;
}
else
{
Volume = orderVolume;
SellMarket();
_positionDirection = -1;
}
_entryPrice = candle.ClosePrice;
_entryVolume = orderVolume;
var distance = GetTakeProfitDistance();
if (distance > 0m)
{
_takeProfitLevel = _positionDirection > 0
? _entryPrice + distance
: _entryPrice - distance;
}
else
{
_takeProfitLevel = null;
}
_tradesToday++;
UpdateHistory(candle.ClosePrice);
}
private void ClosePosition(decimal exitPrice)
{
var direction = _positionDirection;
var entryPrice = _entryPrice;
var volume = Math.Abs(Position);
if (volume <= 0m && _entryVolume > 0m)
{
volume = _entryVolume;
}
if (volume <= 0m)
{
_positionDirection = 0;
_takeProfitLevel = null;
_entryPrice = null;
_entryVolume = 0m;
return;
}
if (direction > 0)
{
SellMarket();
}
else if (direction < 0)
{
BuyMarket();
}
if (entryPrice is decimal price)
{
var isLoss = direction > 0
? exitPrice < price
: exitPrice > price;
_consecutiveLosses = isLoss
? Math.Min(_consecutiveLosses + 1, 5)
: 0;
}
_positionDirection = 0;
_takeProfitLevel = null;
_entryPrice = null;
_entryVolume = 0m;
}
private void UpdateHistory(decimal closePrice)
{
_closeHistory.Add(closePrice);
var maxHistory = Math.Max(HoursToCheckTrend, 2);
if (_closeHistory.Count > maxHistory)
{
_closeHistory.RemoveRange(0, _closeHistory.Count - maxHistory);
}
}
private decimal CalculateOrderVolume()
{
if (Volume <= 0m)
{
return 0m;
}
var multiplier = _consecutiveLosses switch
{
>= 5 => (decimal)FifthMultiplier,
4 => (decimal)FourthMultiplier,
3 => (decimal)ThirdMultiplier,
2 => (decimal)SecondMultiplier,
1 => (decimal)FirstMultiplier,
_ => 1m
};
var desiredVolume = Volume * multiplier;
if (MaxVolume > 0m && desiredVolume > MaxVolume)
{
desiredVolume = MaxVolume;
}
return desiredVolume;
}
private decimal GetTakeProfitDistance()
{
var pipSize = GetPipSize();
return pipSize > 0m
? TakeProfitPips * pipSize
: 0m;
}
private decimal GetPipSize()
{
var priceStep = Security?.PriceStep ?? 0m;
if (priceStep <= 0m)
{
priceStep = 0.0001m;
}
var decimals = Security?.Decimals ?? 0;
if (decimals == 3 || decimals == 5)
{
return priceStep * 10m;
}
return priceStep;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class twenty_pips_opposite_last_n_hour_trend_strategy(Strategy):
"""20 Pips Opposite Last N Hour Trend: counter-trend martingale strategy."""
def __init__(self):
super(twenty_pips_opposite_last_n_hour_trend_strategy, self).__init__()
self._max_positions = self.Param("MaxPositions", 9) \
.SetGreaterThanZero() \
.SetDisplay("Max Positions", "Maximum trades per day", "Trading")
self._max_volume = self.Param("MaxVolume", 5.0) \
.SetGreaterThanZero() \
.SetDisplay("Max Volume", "Maximum allowed volume", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 20.0) \
.SetGreaterThanZero() \
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Trading")
self._trading_hour = self.Param("TradingHour", 8) \
.SetDisplay("Trading Hour", "Hour (0-23) when entries are allowed", "Timing")
self._hours_to_check_trend = self.Param("HoursToCheckTrend", 6) \
.SetDisplay("Hours To Check", "Lookback hours for trend calculation", "Signals")
self._first_multiplier = self.Param("FirstMultiplier", 2) \
.SetGreaterThanZero() \
.SetDisplay("First Multiplier", "Multiplier after first loss", "Money Management")
self._second_multiplier = self.Param("SecondMultiplier", 4) \
.SetGreaterThanZero() \
.SetDisplay("Second Multiplier", "Multiplier after second loss", "Money Management")
self._third_multiplier = self.Param("ThirdMultiplier", 8) \
.SetGreaterThanZero() \
.SetDisplay("Third Multiplier", "Multiplier after third loss", "Money Management")
self._fourth_multiplier = self.Param("FourthMultiplier", 16) \
.SetGreaterThanZero() \
.SetDisplay("Fourth Multiplier", "Multiplier after fourth loss", "Money Management")
self._fifth_multiplier = self.Param("FifthMultiplier", 32) \
.SetGreaterThanZero() \
.SetDisplay("Fifth Multiplier", "Multiplier after fifth loss", "Money Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candle timeframe to process", "Market Data")
self._close_history = []
self._entry_price = None
self._take_profit_level = None
self._entry_volume = 0.0
self._position_direction = 0
self._consecutive_losses = 0
self._current_day = None
self._trades_today = 0
@property
def MaxPositions(self):
return int(self._max_positions.Value)
@property
def MaxVolume(self):
return float(self._max_volume.Value)
@property
def TakeProfitPips(self):
return float(self._take_profit_pips.Value)
@property
def TradingHour(self):
return int(self._trading_hour.Value)
@property
def HoursToCheckTrend(self):
return int(self._hours_to_check_trend.Value)
@property
def FirstMultiplier(self):
return int(self._first_multiplier.Value)
@property
def SecondMultiplier(self):
return int(self._second_multiplier.Value)
@property
def ThirdMultiplier(self):
return int(self._third_multiplier.Value)
@property
def FourthMultiplier(self):
return int(self._fourth_multiplier.Value)
@property
def FifthMultiplier(self):
return int(self._fifth_multiplier.Value)
@property
def CandleType(self):
return self._candle_type.Value
def _get_pip_size(self):
sec = self.Security
if sec is None or sec.PriceStep is None:
return 0.0001
step = float(sec.PriceStep)
if step <= 0:
return 0.0001
decimals = 0
if sec.Decimals is not None:
decimals = int(sec.Decimals)
if decimals == 3 or decimals == 5:
return step * 10.0
return step
def OnStarted2(self, time):
super(twenty_pips_opposite_last_n_hour_trend_strategy, self).OnStarted2(time)
self._close_history = []
self._entry_price = None
self._take_profit_level = None
self._entry_volume = 0.0
self._position_direction = 0
self._consecutive_losses = 0
self._current_day = None
self._trades_today = 0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
candle_day = candle.OpenTime.Date
if self._current_day is None or self._current_day != candle_day:
self._current_day = candle_day
self._trades_today = 0
if self._position_direction != 0:
if self._take_profit_level is not None:
if self._position_direction > 0:
hit_target = float(candle.HighPrice) >= self._take_profit_level
else:
hit_target = float(candle.LowPrice) <= self._take_profit_level
if hit_target:
self._close_position(self._take_profit_level)
if self._position_direction != 0 and candle.OpenTime.Hour != self.TradingHour:
self._close_position(close)
if self._position_direction != 0:
self._update_history(close)
return
if candle.OpenTime.Hour != self.TradingHour:
self._update_history(close)
return
if self.MaxPositions <= 0 or self._trades_today >= self.MaxPositions:
self._update_history(close)
return
required_history = max(self.HoursToCheckTrend, 2)
if len(self._close_history) < required_history:
self._update_history(close)
return
reference_close = self._close_history[len(self._close_history) - self.HoursToCheckTrend]
previous_close = self._close_history[len(self._close_history) - 1]
if previous_close == reference_close:
self._update_history(close)
return
go_long = previous_close < reference_close
order_volume = self._calculate_order_volume()
if order_volume <= 0:
self._update_history(close)
return
if go_long:
self.BuyMarket()
self._position_direction = 1
else:
self.SellMarket()
self._position_direction = -1
self._entry_price = close
self._entry_volume = order_volume
distance = self._get_take_profit_distance()
if distance > 0:
if self._position_direction > 0:
self._take_profit_level = self._entry_price + distance
else:
self._take_profit_level = self._entry_price - distance
else:
self._take_profit_level = None
self._trades_today += 1
self._update_history(close)
def _close_position(self, exit_price):
direction = self._position_direction
entry_price = self._entry_price
volume = abs(self.Position)
if volume <= 0 and self._entry_volume > 0:
volume = self._entry_volume
if volume <= 0:
self._position_direction = 0
self._take_profit_level = None
self._entry_price = None
self._entry_volume = 0.0
return
if direction > 0:
self.SellMarket()
elif direction < 0:
self.BuyMarket()
if entry_price is not None:
if direction > 0:
is_loss = exit_price < entry_price
else:
is_loss = exit_price > entry_price
if is_loss:
self._consecutive_losses = min(self._consecutive_losses + 1, 5)
else:
self._consecutive_losses = 0
self._position_direction = 0
self._take_profit_level = None
self._entry_price = None
self._entry_volume = 0.0
def _update_history(self, close_price):
self._close_history.append(close_price)
max_history = max(self.HoursToCheckTrend, 2)
if len(self._close_history) > max_history:
self._close_history = self._close_history[len(self._close_history) - max_history:]
def _calculate_order_volume(self):
base_vol = self.Volume
if base_vol <= 0:
return 0.0
losses = self._consecutive_losses
if losses >= 5:
multiplier = float(self.FifthMultiplier)
elif losses == 4:
multiplier = float(self.FourthMultiplier)
elif losses == 3:
multiplier = float(self.ThirdMultiplier)
elif losses == 2:
multiplier = float(self.SecondMultiplier)
elif losses == 1:
multiplier = float(self.FirstMultiplier)
else:
multiplier = 1.0
desired_volume = float(base_vol) * multiplier
if self.MaxVolume > 0 and desired_volume > self.MaxVolume:
desired_volume = self.MaxVolume
return desired_volume
def _get_take_profit_distance(self):
pip_size = self._get_pip_size()
if pip_size > 0:
return self.TakeProfitPips * pip_size
return 0.0
def OnReseted(self):
super(twenty_pips_opposite_last_n_hour_trend_strategy, self).OnReseted()
self._close_history = []
self._entry_price = None
self._take_profit_level = None
self._entry_volume = 0.0
self._position_direction = 0
self._consecutive_losses = 0
self._current_day = None
self._trades_today = 0
def CreateClone(self):
return twenty_pips_opposite_last_n_hour_trend_strategy()