Estrategia de Cobertura de Doble Lote por Pasos
Descripción General
La Estrategia de Cobertura de Doble Lote por Pasos es un port en C# de los asesores expertos de MetaTrader 5 "x1 lot from high to low" y "x1 lot from low to high" (carpeta MQL/19543). Los robots originales abren inmediatamente una cesta cubierta de posiciones de compra y venta, ciclan el volumen de la orden después de cada nueva entrada, y cierran toda la cesta una vez que se alcanza un objetivo de beneficio fijo. Esta implementación reproduce ese comportamiento sobre la API de alto nivel de StockSharp mientras expone parámetros limpios y gestión detallada del estado.
Están disponibles dos modos de operación:
- HighToLow – comienza con el multiplicador de lote máximo, abre la primera cesta cubierta con el volumen más grande, y luego disminuye al siguiente paso de lote después de las primeras entradas.
- LowToHigh – comienza con el paso de lote mínimo, aumenta el tamaño del lote después de cada nueva entrada hasta que se alcanza el multiplicador configurado, y luego continúa operando a ese tamaño.
La estrategia mantiene ambas patas de compra y venta activas simultáneamente, gestiona los niveles de stop-loss y take-profit por pata, y monitorea el capital del portafolio para imponer un objetivo de beneficio a nivel de cesta.
Lógica de Trading
- Cuando no existen posiciones, la estrategia abre ambas una orden de mercado larga y corta usando el tamaño de lote actual.
- Si exactamente una pata está activa (por ejemplo, el lado opuesto fue detenido), la pata faltante se reabre en mercado con el tamaño de lote actual.
- Después de cada entrada exitosa, el tamaño de lote se actualiza dependiendo del modo seleccionado (
HighToLow o LowToHigh).
- Las salidas de protección por pata se evalúan en cada tick de operación entrante:
- Una pata larga se cierra si el precio alcanza su stop-loss (
StopLossPips por debajo de la entrada larga promedio) o su take-profit (TakeProfitPips por encima de la entrada promedio).
- Una pata corta se cierra si el precio alcanza su stop-loss (
StopLossPips por encima de la entrada corta promedio) o su take-profit (TakeProfitPips por debajo de la entrada promedio).
- Una vez que la ganancia de capital del portafolio supera
MinProfit, la estrategia cierra todas las posiciones restantes y restablece el estado del lote al tamaño de inicio del modo.
- La lógica de seguridad cierra la cesta y restablece todo si se detecta inesperadamente más de una posición de compra o venta.
Todas las órdenes se envían a través de los helpers de alto nivel BuyMarket y SellMarket. La estrategia rastrea los rellenos con OnOwnTradeReceived, mantiene la exposición agregada por pata, y previene órdenes duplicadas mientras las entradas o salidas aún están pendientes.
Parámetros
| Parámetro |
Descripción |
LotMultiplier |
Multiplicador de lote máximo expresado en pasos de volumen mínimos (por defecto 10). |
StopLossPips |
Distancia de stop-loss en pips para cada pata (por defecto 50). Establecer en 0 para deshabilitar. |
TakeProfitPips |
Distancia de take-profit en pips para cada pata (por defecto 150). Establecer en 0 para deshabilitar. |
MinProfit |
Objetivo de beneficio de la cesta en moneda de la cuenta. Una vez que la ganancia de capital supera este valor, todas las posiciones se cierran (por defecto 27). |
ScalingMode |
Comportamiento del paso de lote. HighToLow refleja el EA "x1 lot from high to low", LowToHigh refleja "x1 lot from low to high". |
La estrategia deriva automáticamente el paso de volumen mínimo de Security.VolumeStep y calcula el valor de pip usando el paso de precio del instrumento (con el ajuste forex tradicional de 4/5 dígitos).
Reinicio y Ciclo de Volumen
- HighToLow – abre la primera cesta con el volumen más alto (
VolumeStep * LotMultiplier). Después de cualquier entrada, el volumen interno se reduce en un paso. Cuando se alcanza el objetivo de beneficio de la cesta, el volumen se restablece a 0 para que el siguiente ciclo comience desde el máximo nuevamente.
- LowToHigh – comienza desde el paso de lote mínimo. Después de cada entrada, el lote se aumenta en un paso hasta que se alcanza el techo del multiplicador. Cuando se alcanza el objetivo de beneficio de la cesta, el volumen se restablece al paso mínimo.
Notas de Uso
- La estrategia se suscribe a operaciones de tick (
DataType.Ticks) porque los bots MetaTrader originales se ejecutan en eventos de tick. Configure el proveedor de historia o el conector en vivo en consecuencia.
- Las verificaciones de stop-loss y take-profit ocurren dentro del algoritmo, por lo que no se registran órdenes de protección adicionales en el exchange.
- Dado que ambas patas se abren en mercado, la estrategia funciona mejor en brokers que soportan posiciones cubiertas y spreads pequeños. En venues de netting, seguirá funcionando pero las patas se compensarán efectivamente hasta que una de ellas sea cerrada por la lógica interna.
- Los parámetros predeterminados copian la configuración MQL original. Ajústelos cuidadosamente: cubrir altos volúmenes puede generar drawdowns significativos antes de que se alcance el objetivo de beneficio de la cesta.
Correspondencia con la Lógica MQL Original
| Variable MetaTrader |
Propiedad C# / Comportamiento |
InpLots |
LotMultiplier con manejo automático de paso de volumen. |
InpStopLoss & InpTakeProfit |
StopLossPips y TakeProfitPips con conversión de pip basada en PriceStep. |
InpMinProfit |
MinProfit y la verificación de capital del portafolio. |
LotCheck |
Helper LotCheck que aplica el paso mínimo y el volumen máximo. |
CalculatePositions |
Seguimiento interno de exposición larga/corta a través de OnOwnTradeReceived. |
CloseAllPositions() |
Método CloseAllPositions con coordinación de órdenes pendientes y reinicio de estado. |
Consideraciones de Gestión de Riesgo
La estrategia mantiene intencionalmente posiciones largas y cortas abiertas, lo que causa exposición continua a costos de spread y tasas de swap. Antes de ejecutar con capital real:
- Validar el comportamiento en el emulador de StockSharp o en trading en papel.
- Asegurarse de que su broker soporte la cobertura; de lo contrario, las patas larga/corta serán neteadas inmediatamente.
- Ajustar los valores de stop-loss, take-profit y objetivo de beneficio a la volatilidad del instrumento.
- Monitorear el uso del margen, porque las patas largas/cortas simultáneas duplican la exposición nominal.
Archivos
CS/DualLotStepHedgeStrategy.cs – implementación de estrategia StockSharp con comentarios en línea extensos.
README_ru.md – traducción rusa con instrucciones detalladas.
README_zh.md – traducción china con instrucciones detalladas.
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>
/// Recreates the "x1 lot from high to low" and "x1 lot from low to high" MetaTrader robots.
/// Opens hedged long/short positions with adjustable lot cycling and closes the basket once
/// a profit target is achieved.
/// </summary>
public class DualLotStepHedgeStrategy : Strategy
{
private readonly StrategyParam<int> _lotMultiplier;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _minProfit;
private readonly StrategyParam<LotScalingModes> _scalingMode;
private decimal _volumeStep;
private decimal _maxVolume;
private decimal _currentVolume;
private decimal _pipValue;
private decimal _initialEquity;
private decimal _longVolume;
private decimal _shortVolume;
private decimal _longAveragePrice;
private decimal _shortAveragePrice;
private bool _longEntryInProgress;
private bool _shortEntryInProgress;
private bool _longExitInProgress;
private bool _shortExitInProgress;
private decimal _pendingLongEntryVolume;
private decimal _pendingShortEntryVolume;
private decimal _pendingLongExitVolume;
private decimal _pendingShortExitVolume;
private bool _resetRequested;
/// <summary>
/// Defines the lot stepping mode that matches the original MetaTrader experts.
/// </summary>
public enum LotScalingModes
{
/// <summary>
/// Start with the maximum lot multiplier and drop to the next step after the first cycle.
/// </summary>
HighToLow,
/// <summary>
/// Start with the minimum lot step and grow until the configured multiplier is reached.
/// </summary>
LowToHigh,
}
/// <summary>
/// Maximum lot multiplier expressed in minimal volume steps.
/// </summary>
public int LotMultiplier
{
get => _lotMultiplier.Value;
set => _lotMultiplier.Value = value;
}
/// <summary>
/// Stop loss distance in pips from the average entry price of the leg.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit distance in pips from the average entry price of the leg.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Basket profit target in account currency.
/// </summary>
public decimal MinProfit
{
get => _minProfit.Value;
set => _minProfit.Value = value;
}
/// <summary>
/// Selected lot stepping mode.
/// </summary>
public LotScalingModes ScalingMode
{
get => _scalingMode.Value;
set => _scalingMode.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="DualLotStepHedgeStrategy"/>.
/// </summary>
public DualLotStepHedgeStrategy()
{
_lotMultiplier = Param(nameof(LotMultiplier), 10)
.SetGreaterThanZero()
.SetDisplay("Lot Multiplier", "Maximum lot multiplier over the minimal step", "Trading")
.SetOptimize(1, 20, 1);
_stopLossPips = Param(nameof(StopLossPips), 50m)
.SetDisplay("Stop Loss (pips)", "Stop loss distance for each leg", "Risk")
.SetOptimize(10m, 200m, 10m);
_takeProfitPips = Param(nameof(TakeProfitPips), 150m)
.SetDisplay("Take Profit (pips)", "Take profit distance for each leg", "Risk")
.SetOptimize(20m, 400m, 20m);
_minProfit = Param(nameof(MinProfit), 27m)
.SetDisplay("Basket Profit", "Target profit in account currency", "Trading")
.SetOptimize(5m, 200m, 5m);
_scalingMode = Param(nameof(ScalingMode), LotScalingModes.HighToLow)
.SetDisplay("Scaling Mode", "How the lot size evolves after entries", "Trading");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, DataType.Ticks)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_volumeStep = 0m;
_maxVolume = 0m;
_currentVolume = 0m;
_pipValue = 0m;
_initialEquity = 0m;
_longVolume = 0m;
_shortVolume = 0m;
_longAveragePrice = 0m;
_shortAveragePrice = 0m;
_longEntryInProgress = false;
_shortEntryInProgress = false;
_longExitInProgress = false;
_shortExitInProgress = false;
_pendingLongEntryVolume = 0m;
_pendingShortEntryVolume = 0m;
_pendingLongExitVolume = 0m;
_pendingShortExitVolume = 0m;
_resetRequested = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_volumeStep = Security.VolumeStep ?? 0m;
if (_volumeStep <= 0m)
_volumeStep = 1m;
_maxVolume = LotCheck(_volumeStep * LotMultiplier);
if (_maxVolume <= 0m)
_maxVolume = _volumeStep;
_currentVolume = ScalingMode == LotScalingModes.HighToLow ? _maxVolume : _volumeStep;
_pipValue = CalculatePipValue();
var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
subscription.Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var price = candle.ClosePrice;
if (_volumeStep <= 0m)
return;
if (_initialEquity <= 0m)
_initialEquity = Portfolio.CurrentValue ?? 0m;
CheckProtectiveLevels(price);
if (_longExitInProgress || _shortExitInProgress)
return;
if (CheckProfitTarget())
return;
ResetCurrentVolumeIfNeeded();
var buyCount = _longVolume > 0m ? 1 : 0;
var sellCount = _shortVolume > 0m ? 1 : 0;
if (buyCount > 1 || sellCount > 1)
{
CloseAllPositions();
return;
}
if (_longEntryInProgress || _shortEntryInProgress)
return;
if (buyCount == 0 && sellCount == 0)
{
TryOpenHedge();
}
else if (buyCount == 1 && sellCount == 0)
{
OpenShortIfNeeded();
}
else if (buyCount == 0 && sellCount == 1)
{
OpenLongIfNeeded();
}
}
private bool CheckProfitTarget()
{
if (_initialEquity <= 0m || MinProfit <= 0m)
return false;
var currentEquity = Portfolio.CurrentValue ?? 0m;
if (currentEquity - _initialEquity >= MinProfit)
{
CloseAllPositions();
return true;
}
return false;
}
private void TryOpenHedge()
{
if (_longEntryInProgress || _shortEntryInProgress)
return;
var volume = LotCheck(_currentVolume);
if (volume <= 0m)
return;
var buyOk = ExecuteBuy(volume, true);
var sellOk = ExecuteSell(volume, true);
if (buyOk && sellOk)
AdjustVolumeAfterEntry();
}
private void OpenLongIfNeeded()
{
if (_longEntryInProgress)
return;
var volume = LotCheck(_currentVolume);
if (volume <= 0m)
return;
if (ExecuteBuy(volume, true))
AdjustVolumeAfterEntry();
}
private void OpenShortIfNeeded()
{
if (_shortEntryInProgress)
return;
var volume = LotCheck(_currentVolume);
if (volume <= 0m)
return;
if (ExecuteSell(volume, true))
AdjustVolumeAfterEntry();
}
private void AdjustVolumeAfterEntry()
{
if (ScalingMode == LotScalingModes.HighToLow)
{
_currentVolume = LotCheck(_currentVolume - _volumeStep);
}
else
{
_currentVolume = LotCheck(_currentVolume + _volumeStep);
}
}
private void CloseAllPositions()
{
if (_longVolume <= 0m && _shortVolume <= 0m && !_longExitInProgress && !_shortExitInProgress)
{
_resetRequested = true;
ApplyResetIfFlat();
return;
}
if (_longVolume > 0m && !_longExitInProgress)
{
if (ExecuteSell(_longVolume, false))
_resetRequested = true;
}
if (_shortVolume > 0m && !_shortExitInProgress)
{
if (ExecuteBuy(_shortVolume, false))
_resetRequested = true;
}
}
private void CloseLong()
{
if (_longVolume <= 0m || _longExitInProgress)
return;
ExecuteSell(_longVolume, false);
}
private void CloseShort()
{
if (_shortVolume <= 0m || _shortExitInProgress)
return;
ExecuteBuy(_shortVolume, false);
}
private bool ExecuteBuy(decimal volume, bool openingLong)
{
if (volume <= 0m)
return false;
var order = BuyMarket(volume);
if (order == null)
return false;
if (openingLong)
{
_longEntryInProgress = true;
_pendingLongEntryVolume += volume;
}
else
{
_shortExitInProgress = true;
_pendingShortExitVolume += volume;
}
return true;
}
private bool ExecuteSell(decimal volume, bool openingShort)
{
if (volume <= 0m)
return false;
var order = SellMarket(volume);
if (order == null)
return false;
if (openingShort)
{
_shortEntryInProgress = true;
_pendingShortEntryVolume += volume;
}
else
{
_longExitInProgress = true;
_pendingLongExitVolume += volume;
}
return true;
}
private void CheckProtectiveLevels(decimal price)
{
if (_pipValue <= 0m)
return;
if (_longVolume > 0m && !_longExitInProgress)
{
var stop = StopLossPips > 0m ? _longAveragePrice - StopLossPips * _pipValue : decimal.MinValue;
var take = TakeProfitPips > 0m ? _longAveragePrice + TakeProfitPips * _pipValue : decimal.MaxValue;
if (StopLossPips > 0m && price <= stop)
{
CloseLong();
return;
}
if (TakeProfitPips > 0m && price >= take)
{
CloseLong();
return;
}
}
if (_shortVolume > 0m && !_shortExitInProgress)
{
var stop = StopLossPips > 0m ? _shortAveragePrice + StopLossPips * _pipValue : decimal.MaxValue;
var take = TakeProfitPips > 0m ? _shortAveragePrice - TakeProfitPips * _pipValue : decimal.MinValue;
if (StopLossPips > 0m && price >= stop)
{
CloseShort();
return;
}
if (TakeProfitPips > 0m && price <= take)
{
CloseShort();
}
}
}
private void ResetCurrentVolumeIfNeeded()
{
if (ScalingMode == LotScalingModes.HighToLow)
{
if (_currentVolume < _volumeStep)
_currentVolume = _maxVolume;
}
else
{
if (_currentVolume < _volumeStep)
_currentVolume = _volumeStep;
else if (_currentVolume > _maxVolume)
_currentVolume = _volumeStep;
}
}
private decimal LotCheck(decimal volume)
{
if (volume <= 0m)
return 0m;
var step = _volumeStep;
if (step <= 0m)
return 0m;
var ratio = Math.Floor(volume / step);
var normalized = ratio * step;
if (normalized < step)
normalized = 0m;
if (normalized > _maxVolume)
normalized = _maxVolume;
return normalized;
}
private decimal CalculatePipValue()
{
var step = Security.PriceStep ?? 0m;
if (step <= 0m)
return 1m;
double stepDouble;
try
{
stepDouble = Convert.ToDouble(step);
}
catch
{
return step;
}
if (stepDouble <= 0d)
return step;
var decimals = (int)Math.Round(-Math.Log10(stepDouble));
if (decimals == 3 || decimals == 5)
return step * 10m;
return step;
}
private void ApplyResetIfFlat()
{
if (!_resetRequested)
return;
if (_longVolume > 0m || _shortVolume > 0m)
return;
if (_longExitInProgress || _shortExitInProgress)
return;
if (_pendingLongEntryVolume > 0m || _pendingShortEntryVolume > 0m)
return;
_resetRequested = false;
_initialEquity = 0m;
if (ScalingMode == LotScalingModes.HighToLow)
{
_currentVolume = 0m;
}
else
{
_currentVolume = _volumeStep;
}
}
private void ApplyLongOpen(decimal volume, decimal price)
{
if (volume <= 0m)
return;
var total = _longVolume + volume;
_longAveragePrice = _longVolume <= 0m
? price
: (_longAveragePrice * _longVolume + price * volume) / total;
_longVolume = total;
}
private void ApplyShortOpen(decimal volume, decimal price)
{
if (volume <= 0m)
return;
var total = _shortVolume + volume;
_shortAveragePrice = _shortVolume <= 0m
? price
: (_shortAveragePrice * _shortVolume + price * volume) / total;
_shortVolume = total;
}
private void ApplyLongClose(decimal volume)
{
if (volume <= 0m || _longVolume <= 0m)
return;
var closed = Math.Min(_longVolume, volume);
_longVolume -= closed;
if (_longVolume <= 0m)
{
_longVolume = 0m;
_longAveragePrice = 0m;
}
}
private void ApplyShortClose(decimal volume)
{
if (volume <= 0m || _shortVolume <= 0m)
return;
var closed = Math.Min(_shortVolume, volume);
_shortVolume -= closed;
if (_shortVolume <= 0m)
{
_shortVolume = 0m;
_shortAveragePrice = 0m;
}
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade.Order.Security != Security)
return;
var volume = trade.Trade.Volume;
if (volume <= 0m)
return;
var price = trade.Trade.Price;
if (trade.Order.Side == Sides.Buy)
{
ProcessBuyTrade(volume, price);
}
else if (trade.Order.Side == Sides.Sell)
{
ProcessSellTrade(volume, price);
}
ApplyResetIfFlat();
}
private void ProcessBuyTrade(decimal volume, decimal price)
{
var remaining = volume;
if (_pendingShortExitVolume > 0m)
{
var closing = Math.Min(_pendingShortExitVolume, remaining);
ApplyShortClose(closing);
_pendingShortExitVolume -= closing;
remaining -= closing;
if (_pendingShortExitVolume <= 0m)
_shortExitInProgress = false;
}
if (remaining <= 0m)
return;
if (_pendingLongEntryVolume > 0m)
{
var opening = Math.Min(_pendingLongEntryVolume, remaining);
ApplyLongOpen(opening, price);
_pendingLongEntryVolume -= opening;
remaining -= opening;
if (_pendingLongEntryVolume <= 0m)
_longEntryInProgress = false;
}
if (remaining > 0m)
ApplyLongOpen(remaining, price);
}
private void ProcessSellTrade(decimal volume, decimal price)
{
var remaining = volume;
if (_pendingLongExitVolume > 0m)
{
var closing = Math.Min(_pendingLongExitVolume, remaining);
ApplyLongClose(closing);
_pendingLongExitVolume -= closing;
remaining -= closing;
if (_pendingLongExitVolume <= 0m)
_longExitInProgress = false;
}
if (remaining <= 0m)
return;
if (_pendingShortEntryVolume > 0m)
{
var opening = Math.Min(_pendingShortEntryVolume, remaining);
ApplyShortOpen(opening, price);
_pendingShortEntryVolume -= opening;
remaining -= opening;
if (_pendingShortEntryVolume <= 0m)
_shortEntryInProgress = false;
}
if (remaining > 0m)
ApplyShortOpen(remaining, price);
}
}
import clr
import math
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, Sides
from StockSharp.Algo.Strategies import Strategy
class dual_lot_step_hedge_strategy(Strategy):
def __init__(self):
super(dual_lot_step_hedge_strategy, self).__init__()
self._lot_multiplier = self.Param("LotMultiplier", 10)
self._stop_loss_pips = self.Param("StopLossPips", 50.0)
self._take_profit_pips = self.Param("TakeProfitPips", 150.0)
self._min_profit = self.Param("MinProfit", 27.0)
self._scaling_mode = self.Param("ScalingMode", 0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._volume_step = 0.0
self._max_volume = 0.0
self._current_volume = 0.0
self._pip_value = 0.0
self._initial_equity = 0.0
self._long_volume = 0.0
self._short_volume = 0.0
self._long_avg_price = 0.0
self._short_avg_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def LotMultiplier(self):
return self._lot_multiplier.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def MinProfit(self):
return self._min_profit.Value
@property
def ScalingMode(self):
return self._scaling_mode.Value
def OnStarted2(self, time):
super(dual_lot_step_hedge_strategy, self).OnStarted2(time)
sec = self.Security
vs = float(sec.VolumeStep) if sec is not None and sec.VolumeStep is not None else 0.0
if vs <= 0:
vs = 1.0
self._volume_step = vs
self._max_volume = self._lot_check(vs * self.LotMultiplier)
if self._max_volume <= 0:
self._max_volume = vs
self._current_volume = self._max_volume if self.ScalingMode == 0 else vs
self._pip_value = self._calculate_pip_value()
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
price = float(candle.ClosePrice)
if self._volume_step <= 0:
return
if self._initial_equity <= 0:
pf = self.Portfolio
if pf is not None and pf.CurrentValue is not None:
self._initial_equity = float(pf.CurrentValue)
self._check_protective_levels(price)
if self._check_profit_target():
return
self._reset_current_volume_if_needed()
buy_count = 1 if self._long_volume > 0 else 0
sell_count = 1 if self._short_volume > 0 else 0
if buy_count > 1 or sell_count > 1:
self._close_all()
return
if buy_count == 0 and sell_count == 0:
self._try_open_hedge(price)
elif buy_count == 1 and sell_count == 0:
self._open_short_if_needed(price)
elif buy_count == 0 and sell_count == 1:
self._open_long_if_needed(price)
def _check_profit_target(self):
if self._initial_equity <= 0 or self.MinProfit <= 0:
return False
pf = self.Portfolio
if pf is None or pf.CurrentValue is None:
return False
current = float(pf.CurrentValue)
if current - self._initial_equity >= self.MinProfit:
self._close_all()
return True
return False
def _try_open_hedge(self, price):
vol = self._lot_check(self._current_volume)
if vol <= 0:
return
self.BuyMarket()
self._apply_long_open(vol, price)
self.SellMarket()
self._apply_short_open(vol, price)
self._adjust_volume_after_entry()
def _open_long_if_needed(self, price):
vol = self._lot_check(self._current_volume)
if vol <= 0:
return
self.BuyMarket()
self._apply_long_open(vol, price)
self._adjust_volume_after_entry()
def _open_short_if_needed(self, price):
vol = self._lot_check(self._current_volume)
if vol <= 0:
return
self.SellMarket()
self._apply_short_open(vol, price)
self._adjust_volume_after_entry()
def _adjust_volume_after_entry(self):
if self.ScalingMode == 0:
self._current_volume = self._lot_check(self._current_volume - self._volume_step)
else:
self._current_volume = self._lot_check(self._current_volume + self._volume_step)
def _close_all(self):
if self._long_volume > 0:
self.SellMarket()
if self._short_volume > 0:
self.BuyMarket()
self._long_volume = 0.0
self._short_volume = 0.0
self._long_avg_price = 0.0
self._short_avg_price = 0.0
self._initial_equity = 0.0
if self.ScalingMode == 0:
self._current_volume = 0.0
else:
self._current_volume = self._volume_step
def _check_protective_levels(self, price):
if self._pip_value <= 0:
return
if self._long_volume > 0:
if self.StopLossPips > 0 and price <= self._long_avg_price - self.StopLossPips * self._pip_value:
self.SellMarket()
self._long_volume = 0.0
self._long_avg_price = 0.0
return
if self.TakeProfitPips > 0 and price >= self._long_avg_price + self.TakeProfitPips * self._pip_value:
self.SellMarket()
self._long_volume = 0.0
self._long_avg_price = 0.0
return
if self._short_volume > 0:
if self.StopLossPips > 0 and price >= self._short_avg_price + self.StopLossPips * self._pip_value:
self.BuyMarket()
self._short_volume = 0.0
self._short_avg_price = 0.0
return
if self.TakeProfitPips > 0 and price <= self._short_avg_price - self.TakeProfitPips * self._pip_value:
self.BuyMarket()
self._short_volume = 0.0
self._short_avg_price = 0.0
return
def _reset_current_volume_if_needed(self):
if self.ScalingMode == 0:
if self._current_volume < self._volume_step:
self._current_volume = self._max_volume
else:
if self._current_volume < self._volume_step:
self._current_volume = self._volume_step
elif self._current_volume > self._max_volume:
self._current_volume = self._volume_step
def _apply_long_open(self, volume, price):
if volume <= 0:
return
total = self._long_volume + volume
if self._long_volume <= 0:
self._long_avg_price = price
else:
self._long_avg_price = (self._long_avg_price * self._long_volume + price * volume) / total
self._long_volume = total
def _apply_short_open(self, volume, price):
if volume <= 0:
return
total = self._short_volume + volume
if self._short_volume <= 0:
self._short_avg_price = price
else:
self._short_avg_price = (self._short_avg_price * self._short_volume + price * volume) / total
self._short_volume = total
def _lot_check(self, volume):
if volume <= 0:
return 0.0
step = self._volume_step
if step <= 0:
return 0.0
ratio = math.floor(volume / step)
normalized = ratio * step
if normalized < step:
normalized = 0.0
if normalized > self._max_volume:
normalized = self._max_volume
return normalized
def _calculate_pip_value(self):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 0.0
if step <= 0:
return 1.0
decimals = sec.Decimals if sec is not None and sec.Decimals is not None else 0
if decimals == 3 or decimals == 5:
return step * 10.0
return step
def OnReseted(self):
super(dual_lot_step_hedge_strategy, self).OnReseted()
self._volume_step = 0.0
self._max_volume = 0.0
self._current_volume = 0.0
self._pip_value = 0.0
self._initial_equity = 0.0
self._long_volume = 0.0
self._short_volume = 0.0
self._long_avg_price = 0.0
self._short_avg_price = 0.0
def CreateClone(self):
return dual_lot_step_hedge_strategy()