Start
/
Strategie-Beispiele
Auf GitHub ansehen
TurnGrid-Strategie
Überblick
Die TurnGrid-Strategie repliziert das Verhalten des ursprünglichen MQL5 Expert Advisors TurnGrid.mq5. Es erstellt ein symmetrisches Preisgitter um den aktuellen Marktpreis und wechselt zwischen Long- und Short-Orders, wenn der Preis von einer Gitterzelle zur anderen wandert. Die Strategie gleicht offene Aufträge kontinuierlich neu aus, um sowohl ein bullisches als auch ein bärisches Engagement aufrechtzuerhalten, bis das konfigurierte Aktienziel erreicht ist.
Bei der Konvertierung wird das übergeordnete API von StockSharp verwendet: Kerzenabonnements steuern die Rasteraktualisierungen, Marktaufträge verwalten Ein- und Ausstiege und das Risikomanagement wird durch Strategieparameter ausgedrückt. Alle Kommentare wurden ins Englische übersetzt und die Benennung folgt den StockSharp-Konventionen.
Handelslogik
Wenn die Strategie startet, erfasst sie den letzten Kerzenschluss und erstellt ein Raster mit 4 * GridShares Niveaus. Die mittlere Ebene wird auf den aktuellen Preis eingestellt, die oberen Ebenen skalieren um 1 + GridDistance und die unteren Ebenen skalieren um 1 - GridDistance.
In der Mitte des Rasters wird eine erste Marktkauforder platziert. Sein Volumen wird aus dem verfügbaren Budgetanteil (Balance / GridShares) und einer inkrementellen Einsatzformel berechnet, die von der MQL-Version übernommen wurde.
Jede fertige Kerze aktualisiert den aktuellen Rasterindex basierend auf dem Schlusskurs. Wenn sich der Index ändert:
Positionen, die mit Tickets verknüpft sind, die zwei Ebenen vom neuen Index entfernt sind, werden geschlossen (Kauftickets unter dem Preis werden verkauft, Verkaufstickets über dem Preis werden zurückgekauft).
Es werden neue Positionen eröffnet, um sowohl Long- als auch Short-Anker auf dem aktiven Niveau zu halten. Wenn keine Seite vorhanden ist, öffnet die Strategie die Seite mit weniger aktiven Positionen, um das Risiko auszugleichen.
Die Gebühren werden über den Parameter FeeRate angenähert. Jede ausgeführte Bestellung trägt zu einer laufenden Gebühr bei, die bei der Bewertung der Leistung verwendet wird.
Wenn das Kontoguthaben (nach Abzug der kumulierten Gebührenschätzung) den Anfangssaldo um EquityTakeProfit übersteigt, schließt die Strategie die Nettoposition und baut das Raster um den neuesten Preis herum neu auf.
Parameter
Name
Beschreibung
Standard
GridDistance
Relativer Abstand zwischen benachbarten Rasterebenen.
0.01
GridShares
Maximale Anzahl gleichzeitiger Rasterpositionen, die aktiv sein können.
50
EquityTakeProfit
Prozentualer Anstieg gegenüber dem Anfangssaldo, der zum Zurücksetzen des Rasters erforderlich ist.
0.02
FeeRate
Geschätzte Transaktionsgebühr pro Trade, angewendet auf das ausgeführte Volumen.
0.0008
CandleType
Kerzenserien dienten als Antrieb für die Strategie.
1 Minuten Zeitrahmen
Implementierungshinweise
Das Kerzenabonnement wird über SubscribeCandles(CandleType) abgewickelt und die Strategie reagiert nur auf fertige Kerzen und entspricht der tickgesteuerten Logik des ursprünglichen EA, während die Kompatibilität mit StockSharp gewahrt bleibt.
Der Rasterstatus wird in einem kompakten Array von GridLevel-Strukturen gespeichert, die Preisanker, boolesche Flags und Ticketvolumina für verzögerte Schließungen enthalten.
Die Auftragsgrößen folgen der ursprünglichen Formel für die inkrementelle Kapitalzuteilung, mit zusätzlicher Normalisierung durch die Wertpapiereinstellungen VolumeStep, VolumeMin und VolumeMax.
Eigenkapitalbasierte Resets warten darauf, dass die aktuelle Nettoposition geschlossen wird, bevor das Raster neu aufgebaut wird, um saubere Übergänge zwischen Handelszyklen sicherzustellen.
Dateien
CS/TurnGridStrategy.cs – C#-Implementierung der Strategie unter Verwendung von StockSharp High-Level-Konstrukten.
README.md – Englische Dokumentation (diese Datei).
README_zh.md – Vereinfachte chinesische Dokumentation.
README_ru.md – Russische Dokumentation.
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>
/// Grid trading strategy that mirrors the TurnGrid Expert Advisor logic from MQL5.
/// </summary>
public class TurnGridStrategy : Strategy
{
private enum TradeDirections
{
Buy,
Sell,
}
private struct GridLevel
{
public decimal Price;
public bool HasBuy;
public bool HasSell;
public decimal BuyVolumeTicket;
public decimal SellVolumeTicket;
}
private readonly StrategyParam<decimal> _gridDistance;
private readonly StrategyParam<int> _gridShares;
private readonly StrategyParam<decimal> _equityTakeProfit;
private readonly StrategyParam<decimal> _feeRate;
private readonly StrategyParam<DataType> _candleType;
private GridLevel[] _grid;
private int _currentIndex;
private decimal _openBudget;
private decimal _openMoneyIncrement;
private int _buyCount;
private int _sellCount;
private decimal _lastPrice;
private decimal _totalFee;
private decimal _initialBalance;
private bool _resetRequested;
private decimal _resetPrice;
public decimal GridDistance
{
get => _gridDistance.Value;
set => _gridDistance.Value = value;
}
public int GridShares
{
get => _gridShares.Value;
set => _gridShares.Value = value;
}
public decimal EquityTakeProfit
{
get => _equityTakeProfit.Value;
set => _equityTakeProfit.Value = value;
}
public decimal FeeRate
{
get => _feeRate.Value;
set => _feeRate.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public TurnGridStrategy()
{
_gridDistance = Param(nameof(GridDistance), 0.01m)
.SetDisplay("Grid Distance", "Relative distance between grid levels", "Grid");
_gridShares = Param(nameof(GridShares), 50)
.SetDisplay("Max Grid Positions", "Maximum number of open grid entries", "Grid");
_equityTakeProfit = Param(nameof(EquityTakeProfit), 0.02m)
.SetDisplay("Equity Take Profit", "Equity growth ratio that triggers a reset", "Risk");
_feeRate = Param(nameof(FeeRate), 0.0008m)
.SetDisplay("Fee Rate", "Estimated transaction fee per trade", "Costs");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Candle type used to drive the grid", "Data");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_grid = null;
_currentIndex = 0;
_openBudget = 0m;
_openMoneyIncrement = 0m;
_buyCount = 0;
_sellCount = 0;
_lastPrice = 0m;
_totalFee = 0m;
_initialBalance = 0m;
_resetRequested = false;
_resetPrice = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_lastPrice = candle.ClosePrice;
if (_resetRequested)
{
if (!TryCloseNetPosition())
return;
InitializeGrid(_resetPrice);
_resetRequested = false;
}
if (_grid == null)
{
InitializeGrid(candle.ClosePrice);
return;
}
if (!UpdateCurrentIndex(candle.ClosePrice))
return;
if (CheckEquityTarget())
{
RequestReset(candle.ClosePrice);
return;
}
CloseReachedPositions();
ManageOpenings();
}
private void InitializeGrid(decimal price)
{
if (price <= 0m)
return;
var shares = Math.Max(1, GridShares);
var size = shares * 4;
_grid = new GridLevel[size];
_currentIndex = shares * 2;
_grid[_currentIndex].Price = price;
for (var i = _currentIndex + 1; i < size; i++)
{
_grid[i].Price = _grid[i - 1].Price * (1m + GridDistance);
}
for (var i = _currentIndex - 1; i >= 0; i--)
{
_grid[i].Price = _grid[i + 1].Price * (1m - GridDistance);
}
_buyCount = 0;
_sellCount = 0;
_totalFee = 0m;
var portfolio = Portfolio;
_initialBalance = portfolio?.CurrentValue ?? portfolio?.CurrentValue ?? _initialBalance;
if (_initialBalance <= 0m)
_initialBalance = shares * price;
_openBudget = _initialBalance / shares;
if (_openBudget <= 0m)
_openBudget = price;
_openMoneyIncrement = CalculateOpenMoneyIncrement();
_lastPrice = price;
TryOpenBuy();
}
private bool UpdateCurrentIndex(decimal price)
{
if (_grid == null)
return false;
var newIndex = _currentIndex;
while (newIndex + 1 < _grid.Length && price >= _grid[newIndex + 1].Price)
newIndex++;
while (newIndex - 1 >= 0 && price <= _grid[newIndex - 1].Price)
newIndex--;
if (newIndex == _currentIndex)
return false;
_currentIndex = newIndex;
return true;
}
private bool CheckEquityTarget()
{
if (_initialBalance <= 0m)
return false;
var portfolio = Portfolio;
var equity = portfolio?.CurrentValue ?? portfolio?.CurrentValue ?? 0m;
if (equity <= 0m)
return false;
return equity - _totalFee > _initialBalance * (1m + EquityTakeProfit);
}
private void RequestReset(decimal price)
{
_resetRequested = true;
_resetPrice = price;
_grid = null;
_buyCount = 0;
_sellCount = 0;
_totalFee = 0m;
TryCloseNetPosition();
}
private bool TryCloseNetPosition()
{
if (Position > 0m)
{
SellMarket(Position);
return false;
}
if (Position < 0m)
{
BuyMarket(Math.Abs(Position));
return false;
}
return true;
}
private void CloseReachedPositions()
{
if (_grid == null)
return;
ref var currentLevel = ref _grid[_currentIndex];
if (currentLevel.BuyVolumeTicket > 0m)
{
SellMarket(currentLevel.BuyVolumeTicket);
_buyCount = Math.Max(0, _buyCount - 1);
currentLevel.BuyVolumeTicket = 0m;
var anchorIndex = _currentIndex - 2;
if (anchorIndex >= 0)
_grid[anchorIndex].HasBuy = false;
}
if (currentLevel.SellVolumeTicket > 0m)
{
BuyMarket(currentLevel.SellVolumeTicket);
_sellCount = Math.Max(0, _sellCount - 1);
currentLevel.SellVolumeTicket = 0m;
var anchorIndex = _currentIndex + 2;
if (_grid != null && anchorIndex < _grid.Length)
_grid[anchorIndex].HasSell = false;
}
}
private void ManageOpenings()
{
if (_grid == null)
return;
ref var level = ref _grid[_currentIndex];
if (level.HasBuy && !level.HasSell)
{
TryOpenSell();
return;
}
if (!level.HasBuy && level.HasSell)
{
TryOpenBuy();
return;
}
if (!level.HasBuy && !level.HasSell)
{
if (_buyCount > _sellCount)
TryOpenSell();
else
TryOpenBuy();
}
}
private void TryOpenBuy()
{
if (_grid == null)
return;
if (_buyCount + _sellCount >= GridShares)
return;
var volume = CalculateVolume(TradeDirections.Buy);
if (volume <= 0m)
return;
BuyMarket(volume);
ref var level = ref _grid[_currentIndex];
level.HasBuy = true;
var targetIndex = _currentIndex + 2;
if (targetIndex < _grid.Length)
_grid[targetIndex].BuyVolumeTicket += volume;
_buyCount++;
}
private void TryOpenSell()
{
if (_grid == null)
return;
if (_buyCount + _sellCount >= GridShares)
return;
var volume = CalculateVolume(TradeDirections.Sell);
if (volume <= 0m)
return;
SellMarket(volume);
ref var level = ref _grid[_currentIndex];
level.HasSell = true;
var targetIndex = _currentIndex - 2;
if (targetIndex >= 0)
_grid[targetIndex].SellVolumeTicket += volume;
_sellCount++;
}
private decimal CalculateVolume(TradeDirections direction)
{
if (_lastPrice <= 0m)
return 0m;
var firstMoney = _openBudget / 10m;
if (firstMoney <= 0m)
firstMoney = _lastPrice;
decimal money;
switch (direction)
{
case TradeDirections.Buy:
money = firstMoney + _buyCount * _openMoneyIncrement;
break;
case TradeDirections.Sell:
money = firstMoney + _sellCount * _openMoneyIncrement;
break;
default:
money = firstMoney;
break;
}
if (money <= 0m)
return 0m;
var volume = money / _lastPrice;
volume = NormalizeVolume(volume);
if (volume <= 0m)
return 0m;
_totalFee += _lastPrice * volume * FeeRate;
LogInfo($"Total Fee = {_totalFee:F2}; Grid = {_buyCount + _sellCount} / {GridShares} ({_buyCount}, {_sellCount})");
return volume;
}
private decimal NormalizeVolume(decimal volume)
{
var security = Security;
if (security == null)
return volume;
var step = security.VolumeStep ?? 0m;
if (step > 0m)
volume = step * Math.Round(volume / step, MidpointRounding.AwayFromZero);
var min = 0m;
if (min > 0m && volume < min)
return 0m;
var max = decimal.MaxValue;
if (volume > max)
volume = max;
return volume;
}
private decimal CalculateOpenMoneyIncrement()
{
var halfShares = GridShares / 2m;
if (halfShares <= 1m)
return 0m;
var numerator = _initialBalance / 2m - halfShares / 10m;
if (numerator <= 0m)
numerator = _initialBalance / 4m;
var denominator = halfShares * (halfShares - 1m) / 2m;
if (denominator <= 0m)
return 0m;
return numerator / denominator;
}
}
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.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class turn_grid_strategy(Strategy):
"""Simplified grid: SMA crossover (10/30) for direction with alternating trades."""
def __init__(self):
super(turn_grid_strategy, self).__init__()
self._grid_dist = self.Param("GridDistance", 0.01).SetDisplay("Grid Distance", "Relative distance between levels", "Grid")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Candle type", "Data")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(turn_grid_strategy, self).OnReseted()
self._prev_fast = 0
self._prev_slow = 0
self._last_trade_price = 0
def OnStarted2(self, time):
super(turn_grid_strategy, self).OnStarted2(time)
self._prev_fast = 0
self._prev_slow = 0
self._last_trade_price = 0
fast = SimpleMovingAverage()
fast.Length = 10
slow = SimpleMovingAverage()
slow.Length = 30
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(fast, slow, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle, fast_val, slow_val):
if candle.State != CandleStates.Finished:
return
fast = float(fast_val)
slow = float(slow_val)
close = float(candle.ClosePrice)
dist = self._grid_dist.Value
if self._prev_fast == 0 or self._prev_slow == 0:
self._prev_fast = fast
self._prev_slow = slow
self._last_trade_price = close
return
# Grid re-entry check
if self._last_trade_price > 0 and dist > 0:
price_move = abs(close - self._last_trade_price) / self._last_trade_price
if price_move < dist:
self._prev_fast = fast
self._prev_slow = slow
return
cross_up = self._prev_fast <= self._prev_slow and fast > slow
cross_down = self._prev_fast >= self._prev_slow and fast < slow
if cross_up and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._last_trade_price = close
elif cross_down and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._last_trade_price = close
self._prev_fast = fast
self._prev_slow = slow
def CreateClone(self):
return turn_grid_strategy()