Стратегия Dual Lot Step Hedge
Обзор
Dual Lot Step Hedge — это порт MetaTrader 5 советников «x1 lot from high to low» и «x1 lot from low to high» (каталог MQL/19543) на язык C#. Оригинальные роботы мгновенно открывают хеджевую корзину из покупок и продаж, изменяют объём заявки после каждого нового входа и закрывают корзину при достижении целевой прибыли. Реализация на StockSharp повторяет эту логику с использованием высокоуровневого API и предоставляет понятные параметры и расширенное управление состоянием.
Поддерживаются два режима работы:
- HighToLow — старт с максимального множителя лота, первая корзина открывается на наибольший объём, после чего объём снижается на один шаг.
- LowToHigh — старт с минимального шага лота, объём увеличивается после каждого входа до достижения настроенного множителя, затем фиксируется на этом уровне.
Стратегия одновременно поддерживает обе ноги (лонг и шорт), рассчитывает стоп-лосс/тейк-профит для каждой ноги и отслеживает совокупную прибыль по портфелю, чтобы закрыть корзину при достижении цели.
Логика торговли
- При отсутствии позиций стратегия открывает одновременно рыночные покупки и продажи текущим объёмом.
- Если активна только одна нога (например, противоположная закрылась по стопу), недостающая нога немедленно восстанавливается рыночным ордером с текущим объёмом.
- После каждого успешного входа объём обновляется в зависимости от выбранного режима (
HighToLow или LowToHigh).
- Защитные уровни проверяются на каждом тиковом событии:
- Лонг закрывается при достижении стоп-лосса (
StopLossPips ниже средней цены входа) или тейк-профита (TakeProfitPips выше средней цены входа).
- Шорт закрывается при достижении стоп-лосса (
StopLossPips выше средней цены входа) или тейк-профита (TakeProfitPips ниже средней цены входа).
- При превышении прироста капитала над
MinProfit стратегия закрывает обе ноги и сбрасывает объём к стартовому значению режима.
- Дополнительная проверка закрывает корзину и делает полный сброс, если неожиданно обнаружено более одной позиции в каждом направлении.
Заказы отправляются через методы BuyMarket и SellMarket. Заполнение отслеживается в OnOwnTradeReceived, хранится агрегированная экспозиция по каждой ноге и блокируется дублирование, пока выполняются заявки на вход или выход.
Параметры
| Параметр |
Описание |
LotMultiplier |
Максимальный множитель лота в шагах минимального объёма (по умолчанию 10). |
StopLossPips |
Дистанция стоп-лосса в пипсах для каждой ноги (по умолчанию 50). Значение 0 отключает стоп. |
TakeProfitPips |
Дистанция тейк-профита в пипсах для каждой ноги (по умолчанию 150). Значение 0 отключает тейк. |
MinProfit |
Целевая прибыль корзины в валюте счёта. После превышения этого значения позиции закрываются (по умолчанию 27). |
ScalingMode |
Поведение изменения объёма. HighToLow повторяет советник «x1 lot from high to low», LowToHigh — «x1 lot from low to high». |
Минимальный шаг объёма определяется автоматически из Security.VolumeStep, а стоимость пипса вычисляется по PriceStep с поправкой на 3/5-значные котировки.
Циклическое изменение объёма
- HighToLow — первая корзина открывается максимальным объёмом (
VolumeStep * LotMultiplier). После любого входа объём уменьшается на один шаг. После фиксации прибыли объём сбрасывается в 0, чтобы следующий цикл снова начался с максимума.
- LowToHigh — старт с минимального шага. После каждого входа объём увеличивается на шаг до потолка множителя. После фиксации прибыли объём сбрасывается к минимальному шагу.
Практические рекомендации
- Подписка ведётся на тиковые сделки (
DataType.Ticks), т. к. оригинальные роботы работают по каждому тику. Настройте поставщик истории или коннектор соответствующим образом.
- Стоп-лосс и тейк-профит рассчитываются внутри алгоритма, дополнительные защитные заявки на биржу не выставляются.
- Поскольку обе ноги открываются по рынку, стратегия максимально эффективна у брокеров с поддержкой хеджирования и низким спредом. На неттинговых площадках ноги будут взаимно компенсироваться до тех пор, пока одна не закроется по внутренней логике.
- Значения по умолчанию соответствуют исходным MQL-настройкам. Изменяйте их аккуратно: крупные хеджевые объёмы могут вызвать значительные просадки до достижения целевой прибыли.
Соответствие оригиналу MQL
| Переменная MT5 |
Связь в C# |
InpLots |
Параметр LotMultiplier с автоматикой по шагу объёма. |
InpStopLoss и InpTakeProfit |
Параметры StopLossPips и TakeProfitPips с преобразованием пипсов. |
InpMinProfit |
Параметр MinProfit и проверка прироста капитала. |
LotCheck |
Метод LotCheck, ограничивающий шаг и максимум. |
CalculatePositions |
Учёт лонгов и шортов через OnOwnTradeReceived. |
CloseAllPositions() |
Метод CloseAllPositions с координацией заявок и сбросом состояния. |
Управление рисками
Стратегия специально удерживает противоположные позиции, что приводит к постоянным издержкам на спред и своп. Перед запуском на реальном счёте:
- Протестируйте алгоритм в эмуляторе StockSharp или на демо-счёте.
- Убедитесь, что брокер поддерживает хеджирование. На неттинговых счетах позиции будут схлопываться.
- Настройте стопы, тейки и целевую прибыль согласно волатильности инструмента.
- Контролируйте нагрузку по марже: одновременные лонг и шорт удваивают номинальную позицию.
Файлы
CS/DualLotStepHedgeStrategy.cs — реализация стратегии с подробными комментариями.
README.md — версия описания на английском языке.
README_zh.md — версия описания на китайском языке.
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()