Trend-Scalper-Strategie (API/3858)
Überblick
Die TrendScalperStrategy ist eine C#-Konvertierung des MetaTrader 4 Expert Advisors Currencyprofits_01_1.mq4. Der ursprüngliche Roboter ist ein leichter Trendfolge-Scalper, der einen kurzfristigen EMA/SMA-Crossover-Filter mit Breakout-Einstiegen rund um die jüngsten Swing-Hochs und -Tiefs kombiniert. Der StockSharp-Port behält die gleichen Entscheidungsregeln bei und umfasst gleichzeitig das High-Level-Kerzenabonnement und die Indikatorpipeline des Frameworks.
Handelslogik
- Indikatoren
- Schneller EMA (Standard 6) bei Schlusskursen.
- Langsames SMA (Standard 12) bei Schlusskursen.
- Höchstes Hoch (Standardfenster 6) und Tiefstes Tief (Standardfenster 6), berechnet aus den Kerzenhochs und -tiefs.
- Eintrittsbedingungen
- Long: Der Preis bewegt sich in das aktuelle Tiefstband (
Lowest Low), während der schnelle EMA über dem langsamen SMA liegt. Die Strategie sendet eine Market-Buy-Order mit dem durch die Money-Management-Regel definierten Volumen.
- Short: Der Preis berührt das jüngste Hochband (
Highest High), während der schnelle EMA unter dem langsamen SMA liegt. Eine Market-Sell-Order wird nach der gleichen Volumenberechnung erteilt.
- Das System bleibt flach, während eine Position offen ist, und spiegelt das Single-Order-Verhalten der MQL-Version wider.
- Exit Conditions
- Long Exit: Wenn bei einer offenen Long-Position das Kerzenhoch den aufgezeichneten
Highest High durchbricht, wird die Position zum Marktwert geschlossen.
- Short-Ausstieg: Wenn eine offene Short-Position beobachtet, dass das Kerzentief durch
Lowest Low fällt, wird der Short zum Marktwert gedeckt.
- Ein von
StartProtection verwalteter schützender Stop-Loss wird jedem Trade beigefügt, wenn StopLossPoints größer als Null ist.
Money-Management
Die Losgrößenlogik reproduziert die drei Modi, die im Skript MQL verfügbar gemacht werden:
| Modus |
Beschreibung |
Verhalten im Hafen |
0 |
Feste Lose (LotsIfNoMM). |
Gibt den konfigurierten FixedVolume zurück. |
<0 |
Bruchteile, die aus dem Kontostand und dem Risikofaktor berechnet werden. |
Berechnet ceil(balance * risk / 10000) / 10, begrenzt auf 100 Lots. |
>0 |
Skalierung des Gesamtpakets aus Gleichgewicht und Risikofaktor. |
Verwendet dieselbe Grundformel, aber das Ergebnis wird auf die nächste ganze Zahl aufgerundet, auf 1 Los begrenzt und auf 100 begrenzt. |
Der Saldo wird von Portfolio.CurrentValue übernommen (und fällt zurück auf BeginValue). Wenn der Portfoliowert nicht verfügbar ist, kehrt die Strategie zum festen Volumen zurück, sodass während der Backtests weiterhin Aufträge ausgegeben werden.
Risikomanagement
- Stop-Loss: Der Parameter
StopLossPoints wird in Preispunkten (Pips) ausgedrückt. Während OnStarted wird die Entfernung mit Security.PriceStep multipliziert und an StartProtection übergeben, sodass StockSharp die Schutzanordnung aufrechterhalten kann.
- Einzelne Position: Die Logik erzwingt
Position == 0, bevor ein neuer Trade eröffnet wird, und verhindert so überlappende Positionen genau wie beim MT4-Experten.
Parameter
| Name |
Standard |
Beschreibung |
CandleType |
15-minütiger Zeitrahmen |
Kerzenserien, die für Indikatorberechnungen und Signale verwendet werden. |
FastLength |
6 |
Periode des schnellen EMA. |
SlowLength |
12 |
Zeitraum der langsamen SMA. |
BreakoutWindow |
6 |
Anzahl der Kerzen, die für den höchsten Hoch-/Tiefst-Tief-Breakout-Filter überprüft wurden. |
FixedVolume |
0,1 Lose |
Volumen, wenn die Geldverwaltung deaktiviert ist oder ein Fallback erforderlich ist. |
MoneyManagementMode |
0 |
Wählt zwischen fester, gebrochener oder gerundeter Losgröße. |
MoneyManagementRisk |
40 |
Risikofaktormultiplikator, der bei der saldobasierten Losgrößenbestimmung verwendet wird. |
StopLossPoints |
50 |
Stop-Loss-Distanz in Preispunkten (vor dem Aufruf von StartProtection in den absoluten Preis umgerechnet). |
Implementierungshinweise
- Die Indikatorverkettung basiert auf dem übergeordneten
SubscribeCandles().Bind(...)-Workflow. Es ist keine manuelle Serienpufferung erforderlich.
- Kommentare im Code wurden in englischer Sprache hinzugefügt, um den Repository-Richtlinien zu entsprechen.
- Es wurden keine Unit-Tests geändert; Im Mittelpunkt dieser Konvertierung steht die Strategie und die dazugehörige Dokumentation.
Nutzungstipps
- Wählen Sie ein Kerzenintervall, das der ursprünglichen Handelsumgebung entspricht (z. B. kurze Intraday-Zeitrahmen für Scalping).
- Stellen Sie sicher, dass das Portfolio über einen gültigen
PriceStep verfügt, damit die Stop-Loss-Umrechnung in den absoluten Preis korrekt funktioniert.
- Passen Sie
MoneyManagementRisk sorgfältig an: Höhere Werte führen aufgrund der vom MQL-Experten übernommenen ceil(balance * risk / 10000)-Berechnung zu größeren Positionen.
Genaues Signalzeitfenster: Hoch und Tief werden aus den vorherigen BreakoutWindow abgeschlossenen Kerzen berechnet, ohne die aktuelle Kerze. Long verlangt EMA(FastLength) > SMA(SlowLength) und currentLow <= previousWindowLow; Short verlangt die Gegenrichtung und currentHigh >= previousWindowHigh. Ein Long endet bei currentHigh > previousWindowHigh, ein Short bei currentLow < previousWindowLow. Ohne vollständig berechnete Indikatoren und Kanal erfolgen keine Trades. Bei StopLossPoints > 0 wird ein echter Stop durch StartProtection aktiviert.
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using System.Linq;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Fast EMA versus slow SMA direction filtered by a sweep of the previous
/// candle-high/low band. Independent natural exits and a native stop protect fills.
/// </summary>
public class TrendScalperStrategy : Strategy
{
private readonly StrategyParam<int> _fastLength;
private readonly StrategyParam<int> _slowLength;
private readonly StrategyParam<int> _breakoutWindow;
private readonly StrategyParam<decimal> _fixedVolume;
private readonly StrategyParam<int> _moneyManagementMode;
private readonly StrategyParam<decimal> _moneyManagementRisk;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<DataType> _candleType;
private readonly Queue<decimal> _fastSeed = new();
private readonly Queue<decimal> _slowCloses = new();
private readonly Queue<decimal> _highs = new();
private readonly Queue<decimal> _lows = new();
private decimal? _fastEma;
private Order _pendingOrder;
public int FastLength { get => _fastLength.Value; set => _fastLength.Value = value; }
public int SlowLength { get => _slowLength.Value; set => _slowLength.Value = value; }
public int BreakoutWindow { get => _breakoutWindow.Value; set => _breakoutWindow.Value = value; }
public decimal FixedVolume { get => _fixedVolume.Value; set => _fixedVolume.Value = value; }
public int MoneyManagementMode { get => _moneyManagementMode.Value; set => _moneyManagementMode.Value = value; }
public decimal MoneyManagementRisk { get => _moneyManagementRisk.Value; set => _moneyManagementRisk.Value = value; }
public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public TrendScalperStrategy()
{
_fastLength = Param(nameof(FastLength), 6).SetGreaterThanZero();
_slowLength = Param(nameof(SlowLength), 12).SetGreaterThanZero();
_breakoutWindow = Param(nameof(BreakoutWindow), 6).SetGreaterThanZero();
_fixedVolume = Param(nameof(FixedVolume), 0.1m).SetGreaterThanZero();
_moneyManagementMode = Param(nameof(MoneyManagementMode), 0);
_moneyManagementRisk = Param(nameof(MoneyManagementRisk), 40m).SetNotNegative();
_stopLossPoints = Param(nameof(StopLossPoints), 50m).SetNotNegative();
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame());
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
protected override void OnReseted()
{
base.OnReseted();
ClearState();
}
private void ClearState()
{
_fastSeed.Clear();
_slowCloses.Clear();
_highs.Clear();
_lows.Clear();
_fastEma = null;
_pendingOrder = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ClearState();
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
throw new InvalidOperationException("Trend Scalper requires a positive PriceStep.");
if (StopLossPoints > 0m)
{
StartProtection(new Unit(), new Unit(StopLossPoints * step, UnitTypes.Absolute),
useMarketOrders: true, isLocalStop: true);
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveQuote).Start();
}
}
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
}
private void ObserveQuote(Level1ChangeMessage message)
{
// A Level1 stream is needed for native stop processing between candle closes.
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var hasBand = _highs.Count == BreakoutWindow;
var bandHigh = hasBand ? _highs.Max() : 0m;
var bandLow = hasBand ? _lows.Min() : 0m;
_highs.Enqueue(candle.HighPrice);
_lows.Enqueue(candle.LowPrice);
while (_highs.Count > BreakoutWindow) _highs.Dequeue();
while (_lows.Count > BreakoutWindow) _lows.Dequeue();
var price = candle.ClosePrice;
if (_fastEma is null)
{
_fastSeed.Enqueue(price);
if (_fastSeed.Count == FastLength)
_fastEma = _fastSeed.Average();
}
else
_fastEma += (price - _fastEma.Value) * 2m / (FastLength + 1);
_slowCloses.Enqueue(price);
while (_slowCloses.Count > SlowLength)
_slowCloses.Dequeue();
if (!hasBand || _fastEma is null || _slowCloses.Count < SlowLength ||
_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
if (Position > 0m && candle.HighPrice > bandHigh)
{
Submit(Sides.Sell, Position, "Trend scalper band exit");
return;
}
if (Position < 0m && candle.LowPrice < bandLow)
{
Submit(Sides.Buy, Math.Abs(Position), "Trend scalper band exit");
return;
}
if (Position != 0m)
return;
var slow = _slowCloses.Average();
if (_fastEma > slow && candle.LowPrice <= bandLow)
Enter(Sides.Buy);
else if (_fastEma < slow && candle.HighPrice >= bandHigh)
Enter(Sides.Sell);
}
private void Enter(Sides side)
{
var volume = CalculateOrderVolume();
if (volume > 0m)
Submit(side, volume, "Trend scalper entry");
}
private decimal CalculateOrderVolume()
{
var volume = FixedVolume;
if (MoneyManagementMode != 0)
{
var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
if (balance > 0m)
{
var fractional = Math.Ceiling(balance * MoneyManagementRisk / 10000m) / 10m;
volume = MoneyManagementMode > 0 ? Math.Max(1m, Math.Ceiling(fractional)) : fractional;
volume = Math.Min(volume, 100m);
}
}
if (Security?.VolumeStep is decimal step && step > 0m)
volume = decimal.Floor(volume / step) * step;
if (Security?.MinVolume is decimal min && min > 0m)
volume = Math.Max(volume, min);
if (Security?.MaxVolume is decimal max && max > 0m)
volume = Math.Min(volume, max);
return volume;
}
private void Submit(Sides side, decimal volume, string comment)
{
_pendingOrder = new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = side,
Volume = volume,
Comment = comment,
};
RegisterOrder(_pendingOrder);
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal
from StockSharp.Messages import (
DataType, CandleStates, Level1Fields, OrderTypes, OrderStates, Sides,
Unit, UnitTypes,
)
from StockSharp.BusinessEntities import Order, Subscription
from StockSharp.Algo.Strategies import Strategy
class trend_scalper_strategy(Strategy):
"""Fast close EMA, slow close SMA, and previous-bar high/low breakout bands."""
def __init__(self):
super(trend_scalper_strategy, self).__init__()
self._fast_length = self.Param("FastLength", 6).SetGreaterThanZero()
self._slow_length = self.Param("SlowLength", 12).SetGreaterThanZero()
self._breakout_window = self.Param("BreakoutWindow", 6).SetGreaterThanZero()
self._fixed_volume = self.Param("FixedVolume", 0.1).SetGreaterThanZero()
self._money_management_mode = self.Param("MoneyManagementMode", 0)
self._money_management_risk = self.Param("MoneyManagementRisk", 40.0).SetNotNegative()
self._stop_loss_points = self.Param("StopLossPoints", 50.0).SetNotNegative()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._clear_state()
@property
def candle_type(self):
return self._candle_type.Value
def _clear_state(self):
self._fast_seed = []
self._slow_closes = []
self._highs = []
self._lows = []
self._fast_ema = None
self._pending_order = None
def OnReseted(self):
super(trend_scalper_strategy, self).OnReseted()
self._clear_state()
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def OnStarted2(self, time):
super(trend_scalper_strategy, self).OnStarted2(time)
self._clear_state()
step = float(self.Security.PriceStep or 0)
if step <= 0:
raise ValueError("Trend Scalper requires a positive PriceStep.")
if self._stop_loss_points.Value > 0:
self.StartProtection(
Unit(), Unit(Decimal(float(self._stop_loss_points.Value) * step), UnitTypes.Absolute),
useMarketOrders=True, isLocalStop=True)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_quote).Start()
self.SubscribeCandles(self.candle_type).Bind(self._process_candle).Start()
def _observe_quote(self, message):
# Keeps native protection informed between completed candles.
pass
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
has_band = len(self._highs) == self._breakout_window.Value
band_high = max(self._highs) if has_band else 0.0
band_low = min(self._lows) if has_band else 0.0
self._highs.append(float(candle.HighPrice))
self._lows.append(float(candle.LowPrice))
if len(self._highs) > self._breakout_window.Value:
self._highs.pop(0)
self._lows.pop(0)
close = float(candle.ClosePrice)
fast_length = self._fast_length.Value
if self._fast_ema is None:
self._fast_seed.append(close)
if len(self._fast_seed) == fast_length:
self._fast_ema = sum(self._fast_seed) / fast_length
else:
self._fast_ema += (close - self._fast_ema) * 2.0 / (fast_length + 1)
self._slow_closes.append(close)
if len(self._slow_closes) > self._slow_length.Value:
self._slow_closes.pop(0)
if not has_band or self._fast_ema is None or len(self._slow_closes) < self._slow_length.Value:
return
if (self._pending_order is not None and
self._pending_order.State not in (OrderStates.Done, OrderStates.Failed)):
return
if self.Position > 0 and float(candle.HighPrice) > band_high:
self._submit(Sides.Sell, float(self.Position), "Trend scalper band exit")
return
if self.Position < 0 and float(candle.LowPrice) < band_low:
self._submit(Sides.Buy, abs(float(self.Position)), "Trend scalper band exit")
return
if self.Position != 0:
return
slow = sum(self._slow_closes) / len(self._slow_closes)
if self._fast_ema > slow and float(candle.LowPrice) <= band_low:
self._enter(Sides.Buy)
elif self._fast_ema < slow and float(candle.HighPrice) >= band_high:
self._enter(Sides.Sell)
def _enter(self, side):
volume = self._calculate_volume()
if volume > 0:
self._submit(side, volume, "Trend scalper entry")
def _calculate_volume(self):
volume = float(self._fixed_volume.Value)
mode = self._money_management_mode.Value
if mode != 0:
balance = (self.Portfolio.CurrentValue or self.Portfolio.BeginValue
if self.Portfolio is not None else 0)
balance = float(balance or 0)
if balance > 0:
fraction = math.ceil(balance * float(self._money_management_risk.Value) / 10000.0) / 10.0
volume = min(100.0, max(1.0, math.ceil(fraction)) if mode > 0 else fraction)
step = float(self.Security.VolumeStep or 0)
if step > 0:
volume = math.floor(volume / step) * step
minimum = float(self.Security.MinVolume or 0)
maximum = float(self.Security.MaxVolume or 0)
if minimum > 0:
volume = max(volume, minimum)
if maximum > 0:
volume = min(volume, maximum)
return volume
def _submit(self, side, volume, comment):
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = Decimal(volume)
order.Comment = comment
self._pending_order = order
self.RegisterOrder(order)
def CreateClone(self):
return trend_scalper_strategy()