High-Level-Port des MetaTrader-Experten "exp_Amstell-SL", der ein bidirektionales Averaging-Grid betreibt. Die Strategie verfolgt den zuletzt ausgeführten Preis auf jeder Seite und gibt zusätzliche Marktaufträge aus, wenn der Preis weit genug driftet, während der offene Batch liquidiert wird, sobald eine feste Take-Profit- oder Stop-Loss-Distanz erreicht ist. Die Implementierung verwendet StockSharps Kerzensubskriptionen und High-Level-Order-Helfer, sodass sie in jede Umgebung eingesteckt werden kann, die Kerzendaten für ein einzelnes Wertpapier bereitstellt.
Die übersetzte Logik ist leicht an StockSharps Netto-Portfolio-Modell angepasst: Long- und Short-Grids werden weiterhin separat verwaltet, aber nicht gleichzeitig gehalten. Das Long-Grid ist aktiv, solange die Nettoposition nicht negativ ist, und das Short-Grid übernimmt erst, nachdem das gesamte Long-Engagement abgebaut wurde.
Wie es funktioniert
Marktdaten und Ausführungsfluss
Abonniert den konfigurierten CandleType (Standard: 1-Minuten-Zeitrahmen-Kerzen) und verarbeitet nur abgeschlossene Kerzen.
Berechnet Pip-basierte Offsets vom PriceStep des Wertpapiers. Wenn der Schritt 3 oder 5 Dezimalstellen hat, wird er mit 10 multipliziert, um die MetaTrader 3/5-Digit-Pip-Anpassung nachzuahmen.
Alle Trades werden durch BuyMarket/SellMarket-Helfer platziert; es werden keine ausstehenden Aufträge verwendet.
Long-Seiten-Management
Öffnet die erste Long-Position (OrderVolume), sobald kein bestehendes Long-Engagement vorhanden ist und die Strategie nicht dabei ist, Shorts zu schließen.
Verfolgt den zuletzt ausgeführten Long-Preis und den volumengewichteten durchschnittlichen Einstandspreis für den aktiven Long-Batch.
Platziert zusätzliche Long-Aufträge der Größe OrderVolume, wenn der Schlusskurs um mindestens BuyDistancePips (in Preiseinheiten umgerechnet) unter dem letzten Long-Fill gefallen ist.
Short-Seiten-Management
Sobald der Long-Batch vollständig geschlossen ist und die Nettoposition nicht positiv ist, erlaubt die Strategie Short-Einstiege.
Platziert die anfängliche Short-Order, wenn keine Short-Exposition vorhanden ist; weitere Shorts werden eröffnet, nachdem der Preis um BuyDistancePips * SellDistanceMultiplier über den vorherigen Short-Fill gestiegen ist.
Verwaltet den zuletzt ausgeführten Short-Preis und den volumengewichteten durchschnittlichen Einstandspreis für den aktiven Short-Batch.
Ausstiegsregeln
Berechnet für jede Richtung den nicht realisierten Gewinn relativ zum durchschnittlichen Fill.
Schließt den gesamten Long-Batch mit einem Marktverkauf, wenn der Gewinn TakeProfitPips Pips erreicht oder der Drawdown StopLossPips Pips erreicht.
Schließt den gesamten Short-Batch mit einem Marktkauf, wenn der Gewinn TakeProfitPips Pips erreicht oder die adverse Bewegung StopLossPips Pips erreicht.
Nach der Liquidation werden alle zwischengespeicherten Preise und Volumen zurückgesetzt, damit ein neues Grid mit der nächsten Kerze beginnen kann.
Unterschiede zum originalen MQL-Experten
Die StockSharp-Version arbeitet auf Kerzenabschlüssen anstatt auf einzelnen Ticks.
Long- und Short-Grids werden sequentiell statt gleichzeitig ausgeführt, was dem Standard-Netting-Modus von StockSharp entspricht.
Alle Schutzabstände werden gegen den gemittelten Einstandspreis statt gegen jedes einzelne Ticket geprüft, was das aggregierte Netto-Positionsverhalten widerspiegelt.
Parameter
Parameter
Standard
Optimierungsbereich
Beschreibung
OrderVolume
0.01
0.01 – 0.10 (Schritt 0.01)
Mit jeder Grid-Order eingereichte Menge. Muss positiv sein.
TakeProfitPips
30
10 – 150 (Schritt 10)
Gewinnziel für den aktiven Batch in Pips.
StopLossPips
30
10 – 150 (Schritt 10)
Maximale adverse Bewegung vor dem Aufgeben des Batches.
BuyDistancePips
10
5 – 60 (Schritt 5)
Minimaler Rückgang vom letzten Long-Fill zum Hinzufügen eines weiteren Kaufs. Muss kleiner als TP und SL sein.
SellDistanceMultiplier
10
2 – 15 (Schritt 1)
Multiplikator für die Long-Distanz beim Abstands-Management von Short-Einstiegen.
CandleType
1-Minuten-Zeitrahmen
—
Kerzenserie für die Signalgenerierung.
Implementierungshinweise
BuyDistancePips muss strikt kleiner als TakeProfitPips und StopLossPips sein; die Strategie wirft ansonsten beim Start eine Ausnahme und reproduziert damit die MetaTrader-Validierung.
Die Pip-Größe wird vom PriceStep des Wertpapiers abgeleitet. Passen Sie die Parameter an, wenn das Instrument eine nicht standardmäßige Tick-Größe verwendet.
Der gesamte interne Zustand wird in OnReseted gelöscht, sodass die Strategie ohne verbleibende Grid-Daten neu gestartet werden kann.
Keine Farbanpassung oder manuelle Indikatorregistrierung wird verwendet, was den High-Level-API-Richtlinien in diesem Repository entspricht.
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>
/// Amstell averaging grid strategy that opens new entries when price drifts away
/// from the last fill and closes exposure once profit or loss thresholds are reached.
/// </summary>
public class AmstellGridManagerStrategy : Strategy
{
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _buyDistancePips;
private readonly StrategyParam<decimal> _sellDistanceMultiplier;
private readonly StrategyParam<DataType> _candleType;
private decimal _longVolume;
private decimal _shortVolume;
private decimal? _averageLongPrice;
private decimal? _averageShortPrice;
private decimal? _lastBuyPrice;
private decimal? _lastSellPrice;
private decimal _pipValue;
private decimal _takeProfitOffset;
private decimal _stopLossOffset;
private decimal _buyDistanceOffset;
private decimal _sellDistanceOffset;
private bool _closingLong;
private bool _closingShort;
/// <summary>
/// Quantity per market order.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Profit target in pips for each grid leg.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Maximum tolerated loss in pips for each grid leg.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Price distance in pips required to add another long position.
/// </summary>
public int BuyDistancePips
{
get => _buyDistancePips.Value;
set => _buyDistancePips.Value = value;
}
/// <summary>
/// Multiplier applied to the long distance when stacking short entries.
/// </summary>
public decimal SellDistanceMultiplier
{
get => _sellDistanceMultiplier.Value;
set => _sellDistanceMultiplier.Value = value;
}
/// <summary>
/// Candle data type used for decision making.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes the strategy parameters.
/// </summary>
public AmstellGridManagerStrategy()
{
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Quantity submitted with each grid order", "Trading")
.SetOptimize(0.01m, 0.1m, 0.01m);
_takeProfitPips = Param(nameof(TakeProfitPips), 30)
.SetGreaterThanZero()
.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk")
.SetOptimize(10, 150, 10);
_stopLossPips = Param(nameof(StopLossPips), 30)
.SetGreaterThanZero()
.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk")
.SetOptimize(10, 150, 10);
_buyDistancePips = Param(nameof(BuyDistancePips), 10)
.SetGreaterThanZero()
.SetDisplay("Buy Distance (pips)", "Distance before adding another long", "Entries")
.SetOptimize(5, 60, 5);
_sellDistanceMultiplier = Param(nameof(SellDistanceMultiplier), 10m)
.SetGreaterThanZero()
.SetDisplay("Sell Distance Multiplier", "Multiplier applied to long distance when adding shorts", "Entries")
.SetOptimize(2m, 15m, 1m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Time frame for processing", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_longVolume = 0m;
_shortVolume = 0m;
_averageLongPrice = null;
_averageShortPrice = null;
_lastBuyPrice = null;
_lastSellPrice = null;
_pipValue = 0m;
_takeProfitOffset = 0m;
_stopLossOffset = 0m;
_buyDistanceOffset = 0m;
_sellDistanceOffset = 0m;
_closingLong = false;
_closingShort = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (BuyDistancePips >= TakeProfitPips || BuyDistancePips >= StopLossPips)
throw new InvalidOperationException("Buy distance must be less than take profit and stop loss distances.");
UpdatePriceOffsets();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
// no indicators bound via .Bind()
var close = candle.ClosePrice;
if (!_closingLong && _longVolume > 0m && _averageLongPrice is decimal longAvg)
{
var profit = close - longAvg;
if (profit >= _takeProfitOffset || -profit >= _stopLossOffset)
{
SellMarket();
_closingLong = true;
return;
}
}
if (!_closingShort && _shortVolume > 0m && _averageShortPrice is decimal shortAvg)
{
var profit = shortAvg - close;
if (profit >= _takeProfitOffset || -profit >= _stopLossOffset)
{
BuyMarket();
_closingShort = true;
return;
}
}
var openedLong = false;
if (!_closingLong && Position >= 0m)
{
if (_longVolume <= 0m)
{
BuyMarket();
openedLong = true;
}
else if (_lastBuyPrice is decimal lastBuy && lastBuy - close >= _buyDistanceOffset)
{
BuyMarket();
openedLong = true;
}
}
if (openedLong)
return;
if (!_closingShort && Position <= 0m)
{
if (_shortVolume <= 0m)
{
SellMarket();
}
else if (_lastSellPrice is decimal lastSell && close - lastSell >= _sellDistanceOffset)
{
SellMarket();
}
}
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade.Order == null)
return;
var tradeVolume = trade.Trade.Volume;
var price = trade.Trade.Price;
if (trade.Order.Side == Sides.Buy)
{
if (_shortVolume > 0m)
{
var closingVolume = Math.Min(tradeVolume, _shortVolume);
_shortVolume -= closingVolume;
tradeVolume -= closingVolume;
if (_shortVolume <= 0m)
{
_shortVolume = 0m;
_averageShortPrice = null;
_lastSellPrice = null;
}
}
if (tradeVolume > 0m)
{
var newVolume = _longVolume + tradeVolume;
var totalCost = (_averageLongPrice ?? 0m) * _longVolume + price * tradeVolume;
_longVolume = newVolume;
_averageLongPrice = totalCost / newVolume;
_lastBuyPrice = price;
_closingLong = false;
}
}
else if (trade.Order.Side == Sides.Sell)
{
if (_longVolume > 0m)
{
var closingVolume = Math.Min(tradeVolume, _longVolume);
_longVolume -= closingVolume;
tradeVolume -= closingVolume;
if (_longVolume <= 0m)
{
_longVolume = 0m;
_averageLongPrice = null;
_lastBuyPrice = null;
}
}
if (tradeVolume > 0m)
{
var newVolume = _shortVolume + tradeVolume;
var totalCost = (_averageShortPrice ?? 0m) * _shortVolume + price * tradeVolume;
_shortVolume = newVolume;
_averageShortPrice = totalCost / newVolume;
_lastSellPrice = price;
_closingShort = false;
}
}
if (_longVolume <= 0m && Position <= 0m)
_closingLong = false;
if (_shortVolume <= 0m && Position >= 0m)
_closingShort = false;
if (Position == 0m)
{
_longVolume = 0m;
_shortVolume = 0m;
_averageLongPrice = null;
_averageShortPrice = null;
_lastBuyPrice = null;
_lastSellPrice = null;
_closingLong = false;
_closingShort = false;
}
}
private void UpdatePriceOffsets()
{
var step = Security?.PriceStep ?? 1m;
if (step <= 0m)
step = 1m;
var decimals = GetDecimalPlaces(step);
_pipValue = decimals == 3 || decimals == 5 ? step * 10m : step;
_takeProfitOffset = TakeProfitPips * _pipValue;
_stopLossOffset = StopLossPips * _pipValue;
_buyDistanceOffset = BuyDistancePips * _pipValue;
_sellDistanceOffset = _buyDistanceOffset * SellDistanceMultiplier;
}
private static int GetDecimalPlaces(decimal value)
{
if (value == 0m)
return 0;
var bits = decimal.GetBits(value);
return (bits[3] >> 16) & 0x7F;
}
}
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 amstell_grid_manager_strategy(Strategy):
"""Amstell averaging grid strategy with TP/SL and distance-based grid entries."""
def __init__(self):
super(amstell_grid_manager_strategy, self).__init__()
self._order_volume = self.Param("OrderVolume", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Order Volume", "Quantity submitted with each grid order", "Trading")
self._take_profit_pips = self.Param("TakeProfitPips", 30) \
.SetGreaterThanZero() \
.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk")
self._stop_loss_pips = self.Param("StopLossPips", 30) \
.SetGreaterThanZero() \
.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk")
self._buy_distance_pips = self.Param("BuyDistancePips", 10) \
.SetGreaterThanZero() \
.SetDisplay("Buy Distance (pips)", "Distance before adding another long", "Entries")
self._sell_distance_mult = self.Param("SellDistanceMultiplier", 10.0) \
.SetGreaterThanZero() \
.SetDisplay("Sell Distance Multiplier", "Multiplier applied to long distance for shorts", "Entries")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Time frame for processing", "General")
self._long_volume = 0.0
self._short_volume = 0.0
self._avg_long_price = None
self._avg_short_price = None
self._last_buy_price = None
self._last_sell_price = None
self._pip_value = 0.0
self._tp_offset = 0.0
self._sl_offset = 0.0
self._buy_dist_offset = 0.0
self._sell_dist_offset = 0.0
self._closing_long = False
self._closing_short = False
@property
def OrderVolume(self):
return self._order_volume.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def BuyDistancePips(self):
return self._buy_distance_pips.Value
@property
def SellDistanceMultiplier(self):
return self._sell_distance_mult.Value
@property
def CandleType(self):
return self._candle_type.Value
def _calc_pip_value(self):
sec = self.Security
if sec is None or sec.PriceStep is None or float(sec.PriceStep) <= 0:
return 1.0
step = float(sec.PriceStep)
decimals = sec.Decimals if sec.Decimals is not None else 0
if decimals == 3 or decimals == 5:
return step * 10.0
return step
def OnStarted2(self, time):
super(amstell_grid_manager_strategy, self).OnStarted2(time)
self.Volume = self.OrderVolume
self._pip_value = self._calc_pip_value()
self._tp_offset = self.TakeProfitPips * self._pip_value
self._sl_offset = self.StopLossPips * self._pip_value
self._buy_dist_offset = self.BuyDistancePips * self._pip_value
self._sell_dist_offset = self._buy_dist_offset * float(self.SellDistanceMultiplier)
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)
# Check long TP/SL
if not self._closing_long and self._long_volume > 0 and self._avg_long_price is not None:
profit = close - self._avg_long_price
if profit >= self._tp_offset or -profit >= self._sl_offset:
self.SellMarket()
self._closing_long = True
return
# Check short TP/SL
if not self._closing_short and self._short_volume > 0 and self._avg_short_price is not None:
profit = self._avg_short_price - close
if profit >= self._tp_offset or -profit >= self._sl_offset:
self.BuyMarket()
self._closing_short = True
return
opened_long = False
# Grid long entries
if not self._closing_long and self.Position >= 0:
if self._long_volume <= 0:
self.BuyMarket()
self._record_buy(close)
opened_long = True
elif self._last_buy_price is not None and self._last_buy_price - close >= self._buy_dist_offset:
self.BuyMarket()
self._record_buy(close)
opened_long = True
if opened_long:
return
# Grid short entries
if not self._closing_short and self.Position <= 0:
if self._short_volume <= 0:
self.SellMarket()
self._record_sell(close)
elif self._last_sell_price is not None and close - self._last_sell_price >= self._sell_dist_offset:
self.SellMarket()
self._record_sell(close)
def _record_buy(self, price):
vol = float(self.Volume) if self.Volume > 0 else 1.0
new_vol = self._long_volume + vol
total_cost = (self._avg_long_price if self._avg_long_price is not None else 0.0) * self._long_volume + price * vol
self._long_volume = new_vol
self._avg_long_price = total_cost / new_vol if new_vol > 0 else price
self._last_buy_price = price
self._closing_long = False
def _record_sell(self, price):
vol = float(self.Volume) if self.Volume > 0 else 1.0
new_vol = self._short_volume + vol
total_cost = (self._avg_short_price if self._avg_short_price is not None else 0.0) * self._short_volume + price * vol
self._short_volume = new_vol
self._avg_short_price = total_cost / new_vol if new_vol > 0 else price
self._last_sell_price = price
self._closing_short = False
def _check_position_sync(self):
if self.Position == 0:
self._long_volume = 0.0
self._short_volume = 0.0
self._avg_long_price = None
self._avg_short_price = None
self._last_buy_price = None
self._last_sell_price = None
self._closing_long = False
self._closing_short = False
def OnReseted(self):
super(amstell_grid_manager_strategy, self).OnReseted()
self._long_volume = 0.0
self._short_volume = 0.0
self._avg_long_price = None
self._avg_short_price = None
self._last_buy_price = None
self._last_sell_price = None
self._pip_value = 0.0
self._tp_offset = 0.0
self._sl_offset = 0.0
self._buy_dist_offset = 0.0
self._sell_dist_offset = 0.0
self._closing_long = False
self._closing_short = False
def CreateClone(self):
return amstell_grid_manager_strategy()