Estrategia de Cuadrícula Amstell
La Estrategia de Cuadrícula Amstell es un puerto en C# del asesor experto de MetaTrader 5 exp_Amstell.mq5. Crea una cuadrícula simétrica de compra/venta y aplica un take profit virtual a las entradas individuales. La conversión sigue las pautas de la API de alto nivel de StockSharp y reemplaza el manejo de ticks con procesamiento de velas mientras mantiene intacta la idea original.
Cómo funciona
Inicialización
- La estrategia se suscribe al tipo de vela configurado e inicia la protección de posición.
- Se calcula un tamaño de pip ajustado a partir del
PriceStep del instrumento y la precisión decimal. Los símbolos de cinco dígitos y tres dígitos reciben automáticamente un multiplicador de 10x, reflejando la implementación de MT5.
Primera operación
- Cuando los precios de compra y venta registrados están vacíos (lanzamiento inicial), se envía inmediatamente una orden de compra a mercado. Esto arranca la cuadrícula exactamente como el asesor experto original.
Expansión de la cuadrícula
- Se emite una nueva compra cada vez que el precio de cierre actual está al menos
StepPips por debajo del último precio de compra registrado.
- Se emite una nueva venta cada vez que el precio está al menos
StepPips por encima del último precio de venta registrado.
- La estrategia rastrea internamente pilas separadas de largos y cortos para que las órdenes alternadas puedan coexistir incluso en una cuenta de netting. Las órdenes opuestas primero reducen la otra pila antes de agregar nueva exposición, reproduciendo el comportamiento de cobertura de la versión MT5.
Take Profit virtual
- Cada largo abierto se monitorea de forma independiente. Cuando el precio avanza
TakeProfitPips, se envía una venta a mercado solo por el volumen de esa posición.
- Cada corto abierto se trata de manera similar en la dirección opuesta. El take profit es "virtual" porque las posiciones se cierran programáticamente sin usar órdenes TP del lado del broker.
- Después de que una dirección se haya cerrado completamente mientras el lado opuesto aún existe, el precio del último trato correspondiente se limpia para que la siguiente orden en esa dirección pueda dispararse inmediatamente, igual que en el código original.
Seguimiento del estado
- El controlador
OnOwnTradeReceived reconstruye las pilas de largos/cortos a partir de las operaciones ejecutadas, permitiendo manejar llenados parciales y reversiones con elegancia.
- Los últimos precios de compra/venta permanecen en caché cuando ambos lados están planos para que la cuadrícula espere el paso requerido antes de volver a entrar después de un reinicio completo.
Parámetros
| Parámetro |
Predeterminado |
Descripción |
Volume |
0.1 |
Tamaño de orden utilizado para cada orden de mercado en ambas direcciones. |
TakeProfitPips |
50 |
Distancia en pips que debe ganarse antes de que se cierre una posición individual. |
StepPips |
15 |
Brecha en pips entre órdenes de cuadrícula consecutivas de la misma dirección. |
CandleType |
1 Minute |
Fuente de datos de velas usada para aproximar la lógica basada en ticks. |
Todas las configuraciones basadas en pips respetan el paso de precio y precisión del instrumento. Por ejemplo, en EURUSD (5 dígitos) StepPips = 15 corresponde a 0.0015.
Notas prácticas
- La estrategia usa precios de cierre de velas para emular las comparaciones a nivel de tick que se encuentran en el código MT5. Para operaciones de alta frecuencia, reduzca el marco temporal.
- No existe stop-loss por defecto. Como con cualquier enfoque de cuadrícula, las tendencias desbocadas pueden acumular gran exposición. Use volúmenes conservadores y considere la supervisión basada en sesiones.
- Debido a que los take profits se manejan virtualmente, las operaciones cerradas se reflejan inmediatamente en el PnL de la estrategia sin colocar órdenes TP visibles en el broker.
- La implementación deja los últimos precios en caché sin cambios después de que ambos lados se aplanen. Esto preserva el comportamiento original donde la cuadrícula espera el desplazamiento de precio antes de reiniciarse.
Archivos
CS/AmstellGridStrategy.cs – Implementación de la estrategia StockSharp con extensos comentarios en línea.
README.md, README_ru.md, README_zh.md – Documentación completa en inglés, ruso y chino.
Este puerto está listo para mayor personalización (por ejemplo, gestión de dinero, límites de riesgo) directamente dentro del ecosistema StockSharp.
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 strategy that alternates buy and sell entries with a virtual take profit.
/// </summary>
public class AmstellGridStrategy : Strategy
{
private sealed class PositionEntry
{
public PositionEntry(decimal price, decimal volume)
{
Price = price;
Volume = volume;
}
public decimal Price { get; set; }
public decimal Volume { get; set; }
public bool IsClosing { get; set; }
}
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _stepPips;
private readonly StrategyParam<DataType> _candleType;
private readonly List<PositionEntry> _longEntries = new();
private readonly List<PositionEntry> _shortEntries = new();
private decimal? _lastBuyPrice;
private decimal? _lastSellPrice;
private bool _hasInitialOrder;
private decimal _pipSize;
/// <summary>
/// Virtual take profit distance in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Distance between consecutive entries in pips.
/// </summary>
public int StepPips
{
get => _stepPips.Value;
set => _stepPips.Value = value;
}
/// <summary>
/// Candle type used to generate trade decisions.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="AmstellGridStrategy"/> class.
/// </summary>
public AmstellGridStrategy()
{
_takeProfitPips = Param(nameof(TakeProfitPips), 50)
.SetGreaterThanZero()
.SetDisplay("Take Profit (pips)", "Virtual take profit distance", "Risk")
.SetOptimize(10, 150, 10);
_stepPips = Param(nameof(StepPips), 15)
.SetGreaterThanZero()
.SetDisplay("Step (pips)", "Distance between grid entries", "Grid")
.SetOptimize(5, 60, 5);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for signal candles", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_longEntries.Clear();
_shortEntries.Clear();
_lastBuyPrice = null;
_lastSellPrice = null;
_hasInitialOrder = false;
_pipSize = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = CalculatePipSize();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
// Only react to completed candles to emulate stable tick processing.
if (candle.State != CandleStates.Finished)
return;
var price = candle.ClosePrice;
var stepDistance = GetStepDistance();
var takeProfitDistance = GetTakeProfitDistance();
// Bootstrap the grid exactly like the MQL version.
if (!_hasInitialOrder && _lastBuyPrice is null && _lastSellPrice is null)
{
BuyMarket(Volume);
_hasInitialOrder = true;
return;
}
// Check whether the grid should add a new long layer.
if (CanOpenBuy(price, stepDistance))
{
BuyMarket(Volume);
return;
}
// Mirror logic for the short side of the grid.
if (CanOpenSell(price, stepDistance))
{
SellMarket(Volume);
return;
}
// No new entries were placed, so check for virtual take-profit exits.
if (TryClosePositions(price, takeProfitDistance))
return;
}
private bool CanOpenBuy(decimal price, decimal stepDistance)
{
if (Volume <= 0)
return false;
return !_lastBuyPrice.HasValue || _lastBuyPrice.Value - price >= stepDistance;
}
private bool CanOpenSell(decimal price, decimal stepDistance)
{
if (Volume <= 0)
return false;
return !_lastSellPrice.HasValue || price - _lastSellPrice.Value >= stepDistance;
}
private bool TryClosePositions(decimal price, decimal takeProfitDistance)
{
if (takeProfitDistance <= 0)
return false;
// Evaluate longs first because the original EA does the same.
foreach (var entry in _longEntries)
{
if (entry.IsClosing)
continue;
if (price - entry.Price >= takeProfitDistance)
{
// Prevent duplicate closing requests until the trade is processed.
entry.IsClosing = true;
SellMarket(entry.Volume);
return true;
}
}
// Short entries use the symmetrical distance check.
foreach (var entry in _shortEntries)
{
if (entry.IsClosing)
continue;
if (entry.Price - price >= takeProfitDistance)
{
entry.IsClosing = true;
BuyMarket(entry.Volume);
return true;
}
}
return false;
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade?.Order == null || trade.Order.Security != Security)
return;
var volume = trade.Trade.Volume;
// Feed the executed trade into the synthetic short stack first.
if (trade.Order.Side == Sides.Buy)
{
var remainder = ReduceEntries(_shortEntries, volume);
if (remainder > 0)
{
// Remaining volume becomes a new long layer.
_longEntries.Add(new PositionEntry(trade.Trade.Price, remainder));
_lastBuyPrice = trade.Trade.Price;
}
}
else if (trade.Order.Side == Sides.Sell)
{
var remainder = ReduceEntries(_longEntries, volume);
if (remainder > 0)
{
// Remaining volume becomes a new short layer.
_shortEntries.Add(new PositionEntry(trade.Trade.Price, remainder));
_lastSellPrice = trade.Trade.Price;
}
}
// Recalculate helper state after rebuilding the stacks.
UpdateLastPrices();
}
private decimal ReduceEntries(List<PositionEntry> entries, decimal volume)
{
var remaining = volume;
// Consume volume using a FIFO approach just like MT5 positions.
while (remaining > 0 && entries.Count > 0)
{
var entry = entries[0];
var used = Math.Min(entry.Volume, remaining);
entry.Volume -= used;
remaining -= used;
if (entry.Volume <= 0)
{
// Entry fully closed, remove it from the stack.
entries.RemoveAt(0);
}
else
{
// Partial reduction keeps the entry alive; clear closing flag.
entry.IsClosing = false;
}
}
return remaining;
}
private void UpdateLastPrices()
{
// If only shorts remain, unlock the buy grid for immediate reuse.
if (_longEntries.Count == 0 && _shortEntries.Count > 0)
{
_lastBuyPrice = null;
}
// If only longs remain, clear the last sell price to mimic MT5 logic.
if (_shortEntries.Count == 0 && _longEntries.Count > 0)
{
_lastSellPrice = null;
}
// Any surviving entries should be marked as active again.
for (var i = 0; i < _longEntries.Count; i++)
{
_longEntries[i].IsClosing = false;
}
for (var i = 0; i < _shortEntries.Count; i++)
{
_shortEntries[i].IsClosing = false;
}
}
private decimal GetStepDistance()
{
var pip = _pipSize;
if (pip <= 0)
{
// Fallback to the raw price step if the pip size has not been initialized yet.
pip = Security?.PriceStep ?? 1m;
}
return StepPips * pip;
}
private decimal GetTakeProfitDistance()
{
var pip = _pipSize;
if (pip <= 0)
{
// Same fallback logic as the step distance.
pip = Security?.PriceStep ?? 1m;
}
return TakeProfitPips * pip;
}
private decimal CalculatePipSize()
{
var step = Security?.PriceStep ?? 0m;
if (step <= 0)
step = 1m;
return step;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import CandleStates, Sides
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
class amstell_grid_strategy(Strategy):
"""
Grid strategy that alternates buy and sell entries with a virtual take profit.
"""
def __init__(self):
super(amstell_grid_strategy, self).__init__()
self._take_profit_pips = self.Param("TakeProfitPips", 50) \
.SetGreaterThanZero() \
.SetDisplay("Take Profit (pips)", "Virtual take profit distance", "Risk")
self._step_pips = self.Param("StepPips", 15) \
.SetGreaterThanZero() \
.SetDisplay("Step (pips)", "Distance between grid entries", "Grid")
self._candle_type = self.Param("CandleType", tf(240)) \
.SetDisplay("Candle Type", "Timeframe for signal candles", "General")
self._long_entries = []
self._short_entries = []
self._last_buy_price = None
self._last_sell_price = None
self._has_initial_order = False
@property
def TakeProfitPips(self): return self._take_profit_pips.Value
@TakeProfitPips.setter
def TakeProfitPips(self, v): self._take_profit_pips.Value = v
@property
def StepPips(self): return self._step_pips.Value
@StepPips.setter
def StepPips(self, v): self._step_pips.Value = v
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, v): self._candle_type.Value = v
def OnReseted(self):
super(amstell_grid_strategy, self).OnReseted()
self._long_entries = []
self._short_entries = []
self._last_buy_price = None
self._last_sell_price = None
self._has_initial_order = False
def OnStarted2(self, time):
super(amstell_grid_strategy, self).OnStarted2(time)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
price = float(candle.ClosePrice)
step_dist = self.StepPips * 1.0
tp_dist = self.TakeProfitPips * 1.0
if not self._has_initial_order and self._last_buy_price is None and self._last_sell_price is None:
self.BuyMarket(self.Volume)
self._has_initial_order = True
return
# Check grid buy
if self._last_buy_price is None or self._last_buy_price - price >= step_dist:
self.BuyMarket(self.Volume)
self._last_buy_price = price
return
# Check grid sell
if self._last_sell_price is None or price - self._last_sell_price >= step_dist:
self.SellMarket(self.Volume)
self._last_sell_price = price
return
# Check TP for longs
for entry in self._long_entries:
if not entry.get("closing", False) and price - entry["price"] >= tp_dist:
entry["closing"] = True
self.SellMarket(entry["volume"])
return
# Check TP for shorts
for entry in self._short_entries:
if not entry.get("closing", False) and entry["price"] - price >= tp_dist:
entry["closing"] = True
self.BuyMarket(entry["volume"])
return
def OnOwnTradeReceived(self, trade):
super(amstell_grid_strategy, self).OnOwnTradeReceived(trade)
price = float(trade.Trade.Price)
volume = float(trade.Trade.Volume)
side = trade.Order.Side
if side == Sides.Buy:
remainder = self._reduce_entries(self._short_entries, volume)
if remainder > 0:
self._long_entries.append({"price": price, "volume": remainder, "closing": False})
self._last_buy_price = price
elif side == Sides.Sell:
remainder = self._reduce_entries(self._long_entries, volume)
if remainder > 0:
self._short_entries.append({"price": price, "volume": remainder, "closing": False})
self._last_sell_price = price
self._update_last_prices()
def _reduce_entries(self, entries, volume):
remaining = volume
while remaining > 0 and len(entries) > 0:
entry = entries[0]
used = min(entry["volume"], remaining)
entry["volume"] -= used
remaining -= used
if entry["volume"] <= 0:
entries.pop(0)
else:
entry["closing"] = False
return remaining
def _update_last_prices(self):
if len(self._long_entries) == 0 and len(self._short_entries) > 0:
self._last_buy_price = None
if len(self._short_entries) == 0 and len(self._long_entries) > 0:
self._last_sell_price = None
for e in self._long_entries:
e["closing"] = False
for e in self._short_entries:
e["closing"] = False
def CreateClone(self):
"""!! REQUIRED!! Creates a new instance of the strategy."""
return amstell_grid_strategy()