La Estrategia TurnGrid replica el comportamiento del MQL5 Asesor Experto TurnGrid.mq5 original. Construye una cuadrícula de precios simétrica alrededor del precio de mercado actual y alterna entre órdenes largas y cortas cada vez que el precio migra de una celda de la cuadrícula a otra. La estrategia reequilibra continuamente las órdenes abiertas para mantener una exposición tanto alcista como bajista hasta que se alcance el objetivo de renta variable configurado.
La conversión utiliza el nivel alto API de StockSharp: las suscripciones de velas impulsan las actualizaciones de la red, las órdenes de mercado manejan las entradas y salidas, y la gestión de riesgos se expresa a través de parámetros estratégicos. Todos los comentarios han sido traducidos al inglés y los nombres siguen las convenciones StockSharp.
Lógica de trading
Cuando comienza la estrategia, captura el último cierre de vela y crea una cuadrícula que contiene 4 * GridShares niveles. El nivel central se establece en el precio actual, los niveles superiores escalan en 1 + GridDistance y los niveles inferiores escalan en 1 - GridDistance.
Se coloca una orden de compra de mercado inicial en el centro de la cuadrícula. Su volumen se calcula a partir de la parte del presupuesto disponible (Balance / GridShares) y una fórmula de apuesta incremental heredada de la versión MQL.
Cada vela terminada actualiza el índice de la cuadrícula actual según el precio de cierre. Si el índice cambia:
Las posiciones vinculadas a billetes a dos niveles del nuevo índice se cierran (los billetes comprados por debajo del precio se venden, los billetes vendidos por encima se recompran).
Se abren nuevas posiciones para mantener activos los anclajes tanto largos como cortos. Si ninguna de las partes está presente, la estrategia abre la parte con menos posiciones activas para equilibrar la exposición.
Las tarifas se aproximan mediante el parámetro FeeRate. Cada pedido ejecutado contribuye a una tarifa corriente total que se utiliza al evaluar el rendimiento.
Cuando el capital de la cuenta (después de restar la estimación de la tarifa acumulada) excede el saldo inicial en EquityTakeProfit, la estrategia cierra la posición neta y reconstruye la cuadrícula en torno al último precio.
Parámetros
Nombre
Descripción
Predeterminado
GridDistance
Distancia relativa entre niveles de cuadrícula adyacentes.
0.01
GridShares
Número máximo de posiciones de grilla simultáneas que pueden estar activas.
50
EquityTakeProfit
Ganancia porcentual sobre el saldo inicial requerido para restablecer la red.
0.02
FeeRate
Tarifa de transacción estimada por operación, aplicada al volumen ejecutado.
0.0008
CandleType
Serie de velas utilizada para impulsar la estrategia.
1 período de tiempo de minutos
Notas de implementación
La suscripción a velas se maneja a través de SubscribeCandles(CandleType) y la estrategia reacciona solo a las velas terminadas, coincidiendo con la lógica basada en ticks del EA original mientras mantiene la compatibilidad con StockSharp.
El estado de la cuadrícula se almacena en una matriz liviana de estructuras GridLevel que contienen anclajes de precios, indicadores booleanos y volúmenes de tickets para cierres diferidos.
Los tamaños de las órdenes siguen la fórmula de asignación de capital incremental original, con una normalización adicional a través de las configuraciones VolumeStep, VolumeMin y VolumeMax del valor.
Los reinicios basados en acciones esperan a que se cierre la posición neta actual antes de reconstruir la red, lo que garantiza transiciones limpias entre los ciclos comerciales.
Archivos
CS/TurnGridStrategy.cs: implementación en C# de la estrategia utilizando StockSharp construcciones de alto nivel.
README.md – Documentación en inglés (este archivo).
README_zh.md – Documentación en chino simplificado.
README_ru.md – Documentación rusa.
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()