Estratégia Amstell Gerenciador de Grid
Port de alto nível do expert MetaTrader "exp_Amstell-SL" que executa um grid de médias bidirecional. A estratégia rastreia o preço de execução mais recente em cada lado e emite ordens de mercado adicionais quando o preço se afasta o suficiente, enquanto liquida o lote aberto assim que uma distância fixa de take-profit ou stop-loss é atingida. A implementação usa as assinaturas de candles e os helpers de ordens de alto nível do StockSharp, portanto pode ser conectada a qualquer ambiente que forneça dados de candles para um único instrumento.
A lógica traduzida está ligeiramente adaptada para o modelo de portfólio líquido do StockSharp: os grids comprado e vendido ainda são gerenciados separadamente, mas não são mantidos ao mesmo tempo. O grid comprado está ativo enquanto a posição líquida é não negativa, e o grid vendido assume o controle somente após toda a exposição comprada ter sido zerada.
Como funciona
Fluxo de dados e execução
- Assina o
CandleType configurado (padrão: candles de período de 1 minuto) e processa apenas candles finalizados.
- Calcula offsets baseados em pips a partir do
PriceStep do instrumento. Se o passo tem 3 ou 5 casas decimais, é multiplicado por 10 para imitar o ajuste de pip de 3/5 dígitos do MetaTrader.
- Todas as operações são colocadas através dos helpers
BuyMarket/SellMarket; nenhuma ordem pendente é usada.
Gestão do lado comprado
- Abre a primeira posição comprada (
OrderVolume) assim que não há exposição comprada existente e a estratégia não está no processo de fechar vendidos.
- Rastreia o preço de execução comprado mais recente e o preço de entrada ponderado por volume para o lote comprado ativo.
- Coloca ordens compradas adicionais de tamanho
OrderVolume sempre que o preço de fechamento caiu pelo menos BuyDistancePips (convertidos em unidades de preço) abaixo da última execução comprada.
Gestão do lado vendido
- Assim que o lote comprado está completamente fechado e a posição líquida é não positiva, a estratégia permite entradas vendidas.
- Coloca a ordem vendida inicial quando não há exposição vendida; vendidos adicionais são abertos depois que o preço sobe
BuyDistancePips * SellDistanceMultiplier acima da execução vendida anterior.
- Mantém o preço de execução vendido mais recente e o preço de entrada ponderado por volume para o lote vendido ativo.
Regras de saída
- Para cada direção, calcula o lucro não realizado relativo à execução média.
- Fecha todo o lote comprado com uma venda a mercado quando o lucro atinge
TakeProfitPips pips ou o drawdown atinge StopLossPips pips.
- Fecha todo o lote vendido com uma compra a mercado quando o lucro atinge
TakeProfitPips pips ou o movimento adverso atinge StopLossPips pips.
- Após a liquidação, todos os preços e volumes em cache são redefinidos para que um novo grid possa começar no próximo candle.
Diferenças em relação ao expert MQL original
- A versão StockSharp opera em fechamentos de candles em vez de ticks individuais.
- Os grids comprado e vendido são executados sequencialmente em vez de simultaneamente, correspondendo ao modo de netting padrão do StockSharp.
- Todas as distâncias de proteção são verificadas contra o preço de entrada médio em vez de cada ticket individualmente, o que reflete o comportamento da posição líquida agregada.
Parâmetros
| Parâmetro |
Padrão |
Faixa de otimização |
Descrição |
OrderVolume |
0.01 |
0.01 – 0.10 (passo 0.01) |
Quantidade enviada com cada ordem de grid. Deve ser positiva. |
TakeProfitPips |
30 |
10 – 150 (passo 10) |
Alvo de lucro para o lote ativo expresso em pips. |
StopLossPips |
30 |
10 – 150 (passo 10) |
Movimento adverso máximo antes de abandonar o lote. |
BuyDistancePips |
10 |
5 – 60 (passo 5) |
Queda mínima da última execução comprada para adicionar outra compra. Deve ser menor que TP e SL. |
SellDistanceMultiplier |
10 |
2 – 15 (passo 1) |
Multiplicador aplicado à distância comprada ao espaçar entradas vendidas. |
CandleType |
Período de 1 minuto |
— |
Série de candles usada para geração de sinais. |
Notas de implementação
BuyDistancePips deve ser estritamente menor que TakeProfitPips e StopLossPips; a estratégia lança uma exceção na inicialização caso contrário, reproduzindo a validação do MetaTrader.
- O tamanho do pip é derivado do
PriceStep do instrumento. Ajuste os parâmetros se o instrumento usar um tamanho de tick não padrão.
- Todo o estado interno é limpo em
OnReseted, permitindo reiniciar a estratégia sem dados residuais do grid.
- Nenhuma personalização de cor ou registro manual de indicadores é usado, correspondendo às diretrizes de API de alto nível neste repositório.
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()