Стратегия VR-ZVER v2
VR-ZVER v2 — порт оригинального советника MetaTrader под StockSharp. Логика тройного подтверждения сохранена: для сделки требуется согласие скользящих средних, стохастика и RSI. Пока все активированные фильтры не дадут сигнал в одну сторону, стратегия не откроет позицию.
Логика торговли
- Сигналы рассчитываются только на закрытии свечи. Внутридневные колебания используются лишь для проверки стопов и тейков.
- При включённом фильтре скользящих три экспоненциальные средние (быстрая, медленная, самая медленная) должны располагаться в правильном порядке, подтверждая тренд.
- Стохастик ждёт пересечения %K и %D возле заданных верхнего и нижнего уровней.
- RSI должен выйти из нейтральной зоны: ниже нижнего уровня для покупок и выше верхнего для продаж.
- Сигнал принимается только тогда, когда каждый включённый фильтр голосует в одну сторону. Если кто-то не согласен, сделка пропускается.
- Открывается только одна позиция. Хеджирование и наращивание позиции отсутствуют: после выхода стратегия ждёт нового согласованного сигнала.
Управление позицией
- Тейк-профит и стоп-лосс задаются в пунктах. Начальный стоп устанавливается на две трети от заданного расстояния — как в исходном советнике.
- Уровень безубытка (в пунктах) переносит стоп в точку входа, когда прибыль достигает указанного значения.
- Трейлинг использует расстояние и дополнительный шаг. Шаг не даёт стопу двигаться при каждом небольшом росте и повторяет логику MT5.
- Лонги и шорты управляются симметрично, используя максимумы и минимумы свечи.
Размер позиции
FixedVolumeбольше нуля — фиксированный объём каждой сделки.- Если
FixedVolumeравен нулю, объём рассчитывается поRiskPercent, текущей стоимости портфеля и расстоянию до стопа. Для перевода пунктов в денежный риск берутсяPriceStepиStepPriceинструмента. - Объём округляется с учётом ограничений
VolumeMin,VolumeMaxиVolumeStep. Если рассчитанный объём слишком мал, сделка пропускается.
Параметры
| Параметр | Описание |
|---|---|
CandleType |
Таймфрейм, используемый для сигналов (по умолчанию 15 минут). |
FixedVolume, RiskPercent |
Выбор между фиксированным и риск-ориентированным объёмом. |
StopLossPips, TakeProfitPips |
Базовые защитные расстояния в пунктах. |
TrailingStopPips, TrailingStepPips, BreakevenPips |
Пороговые значения сопровождения позиции. |
AllowLongs, AllowShorts |
Разрешение отдельных направлений. |
UseMovingAverageFilter, FastMaPeriod, SlowMaPeriod, VerySlowMaPeriod |
Фильтр по тройному EMA. |
UseStochastic, StochasticKPeriod, StochasticDPeriod, StochasticSmooth, StochasticUpperLevel, StochasticLowerLevel |
Настройки стохастика. |
UseRsi, RsiPeriod, RsiUpperLevel, RsiLowerLevel |
Зоны подтверждения по RSI. |
Примечания
- Пересчёт пунктов повторяет оригинал: для трёх- и пятизначных котировок шаг цены умножается на десять перед вычислением пунктов.
- Порт под StockSharp использует только рыночные ордера. Функции «замка» и отложенных заявок из версии MetaTrader опущены, чтобы соответствовать высокоуровневому API.
- При подключении к графику автоматически отображаются EMA, стохастик и RSI вместе с сделками.
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>
/// Port of the VR-ZVER v2 expert advisor with triple EMA confirmation and stochastic/RSI filters.
/// </summary>
public class VrZverV2Strategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _fixedVolume;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _trailingStopPips;
private readonly StrategyParam<decimal> _trailingStepPips;
private readonly StrategyParam<decimal> _breakevenPips;
private readonly StrategyParam<bool> _allowLongs;
private readonly StrategyParam<bool> _allowShorts;
private readonly StrategyParam<bool> _useMovingAverageFilter;
private readonly StrategyParam<int> _fastMaPeriod;
private readonly StrategyParam<int> _slowMaPeriod;
private readonly StrategyParam<int> _verySlowMaPeriod;
private readonly StrategyParam<bool> _useStochastic;
private readonly StrategyParam<int> _stochasticKPeriod;
private readonly StrategyParam<int> _stochasticDPeriod;
private readonly StrategyParam<int> _stochasticSmooth;
private readonly StrategyParam<decimal> _stochasticUpperLevel;
private readonly StrategyParam<decimal> _stochasticLowerLevel;
private readonly StrategyParam<bool> _useRsi;
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<decimal> _rsiUpperLevel;
private readonly StrategyParam<decimal> _rsiLowerLevel;
private ExponentialMovingAverage _fastMa;
private ExponentialMovingAverage _slowMa;
private ExponentialMovingAverage _verySlowMa;
private StochasticOscillator _stochastic;
private RelativeStrengthIndex _rsi;
private decimal _pipSize;
private decimal _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
private decimal? _trailingStop;
private bool _breakevenActivated;
public VrZverV2Strategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Time frame for signal generation", "General");
_fixedVolume = Param(nameof(FixedVolume), 1m)
.SetDisplay("Fixed Volume", "Use fixed volume when greater than zero", "Risk");
_riskPercent = Param(nameof(RiskPercent), 10m)
.SetDisplay("Risk %", "Risk percentage used when fixed volume is zero", "Risk");
_stopLossPips = Param(nameof(StopLossPips), 10000m)
.SetDisplay("Stop Loss (pips)", "Full stop distance expressed in pips", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 15000m)
.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk");
_trailingStopPips = Param(nameof(TrailingStopPips), 8000m)
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");
_trailingStepPips = Param(nameof(TrailingStepPips), 3000m)
.SetDisplay("Trailing Step (pips)", "Additional distance before trailing updates", "Risk");
_breakevenPips = Param(nameof(BreakevenPips), 5000m)
.SetDisplay("Breakeven (pips)", "Move stop to entry after this profit", "Risk");
_allowLongs = Param(nameof(AllowLongs), true)
.SetDisplay("Allow Longs", "Permit buy trades", "General");
_allowShorts = Param(nameof(AllowShorts), true)
.SetDisplay("Allow Shorts", "Permit sell trades", "General");
_useMovingAverageFilter = Param(nameof(UseMovingAverageFilter), true)
.SetDisplay("Use MA Filter", "Require triple EMA alignment", "Indicators");
_fastMaPeriod = Param(nameof(FastMaPeriod), 3)
.SetGreaterThanZero()
.SetDisplay("Fast EMA", "Length of the fast EMA", "Indicators");
_slowMaPeriod = Param(nameof(SlowMaPeriod), 5)
.SetGreaterThanZero()
.SetDisplay("Slow EMA", "Length of the slow EMA", "Indicators");
_verySlowMaPeriod = Param(nameof(VerySlowMaPeriod), 7)
.SetGreaterThanZero()
.SetDisplay("Very Slow EMA", "Length of the very slow EMA", "Indicators");
_useStochastic = Param(nameof(UseStochastic), false)
.SetDisplay("Use Stochastic", "Enable stochastic confirmation", "Indicators");
_stochasticKPeriod = Param(nameof(StochasticKPeriod), 42)
.SetGreaterThanZero()
.SetDisplay("Stochastic %K", "Number of periods for %K", "Indicators");
_stochasticDPeriod = Param(nameof(StochasticDPeriod), 5)
.SetGreaterThanZero()
.SetDisplay("Stochastic %D", "Smoothing period for %D", "Indicators");
_stochasticSmooth = Param(nameof(StochasticSmooth), 7)
.SetGreaterThanZero()
.SetDisplay("Stochastic Smooth", "Final smoothing for stochastic", "Indicators");
_stochasticUpperLevel = Param(nameof(StochasticUpperLevel), 60m)
.SetDisplay("Stochastic Upper", "Upper threshold for short signals", "Indicators");
_stochasticLowerLevel = Param(nameof(StochasticLowerLevel), 40m)
.SetDisplay("Stochastic Lower", "Lower threshold for long signals", "Indicators");
_useRsi = Param(nameof(UseRsi), false)
.SetDisplay("Use RSI", "Enable RSI filter", "Indicators");
_rsiPeriod = Param(nameof(RsiPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("RSI Period", "Length of the RSI", "Indicators");
_rsiUpperLevel = Param(nameof(RsiUpperLevel), 60m)
.SetDisplay("RSI Upper", "Upper threshold for short entries", "Indicators");
_rsiLowerLevel = Param(nameof(RsiLowerLevel), 40m)
.SetDisplay("RSI Lower", "Lower threshold for long entries", "Indicators");
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public decimal FixedVolume
{
get => _fixedVolume.Value;
set => _fixedVolume.Value = value;
}
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
public decimal TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
public decimal BreakevenPips
{
get => _breakevenPips.Value;
set => _breakevenPips.Value = value;
}
public bool AllowLongs
{
get => _allowLongs.Value;
set => _allowLongs.Value = value;
}
public bool AllowShorts
{
get => _allowShorts.Value;
set => _allowShorts.Value = value;
}
public bool UseMovingAverageFilter
{
get => _useMovingAverageFilter.Value;
set => _useMovingAverageFilter.Value = value;
}
public int FastMaPeriod
{
get => _fastMaPeriod.Value;
set => _fastMaPeriod.Value = value;
}
public int SlowMaPeriod
{
get => _slowMaPeriod.Value;
set => _slowMaPeriod.Value = value;
}
public int VerySlowMaPeriod
{
get => _verySlowMaPeriod.Value;
set => _verySlowMaPeriod.Value = value;
}
public bool UseStochastic
{
get => _useStochastic.Value;
set => _useStochastic.Value = value;
}
public int StochasticKPeriod
{
get => _stochasticKPeriod.Value;
set => _stochasticKPeriod.Value = value;
}
public int StochasticDPeriod
{
get => _stochasticDPeriod.Value;
set => _stochasticDPeriod.Value = value;
}
public int StochasticSmooth
{
get => _stochasticSmooth.Value;
set => _stochasticSmooth.Value = value;
}
public decimal StochasticUpperLevel
{
get => _stochasticUpperLevel.Value;
set => _stochasticUpperLevel.Value = value;
}
public decimal StochasticLowerLevel
{
get => _stochasticLowerLevel.Value;
set => _stochasticLowerLevel.Value = value;
}
public bool UseRsi
{
get => _useRsi.Value;
set => _useRsi.Value = value;
}
public int RsiPeriod
{
get => _rsiPeriod.Value;
set => _rsiPeriod.Value = value;
}
public decimal RsiUpperLevel
{
get => _rsiUpperLevel.Value;
set => _rsiUpperLevel.Value = value;
}
public decimal RsiLowerLevel
{
get => _rsiLowerLevel.Value;
set => _rsiLowerLevel.Value = value;
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_pipSize = 0m;
_fastMa = null;
_slowMa = null;
_verySlowMa = null;
_stochastic = null;
_rsi = null;
ResetTradeState();
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Prepare pip size once the security is available.
_pipSize = CalculatePipSize();
// Clear any leftover state from previous runs.
ResetTradeState();
// Instantiate indicators with the configured lengths.
_fastMa = new ExponentialMovingAverage { Length = FastMaPeriod };
_slowMa = new ExponentialMovingAverage { Length = SlowMaPeriod };
_verySlowMa = new ExponentialMovingAverage { Length = VerySlowMaPeriod };
if (UseStochastic)
{
_stochastic = new StochasticOscillator();
_stochastic.K.Length = StochasticKPeriod;
_stochastic.D.Length = StochasticDPeriod;
}
if (UseRsi)
{
_rsi = new RelativeStrengthIndex { Length = RsiPeriod };
}
// Subscribe to candle updates and bind the three EMAs.
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_fastMa, _slowMa, _verySlowMa, ProcessCandle)
.Start();
// Draw indicators and trades when a chart area is available.
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _fastMa);
DrawIndicator(area, _slowMa);
DrawIndicator(area, _verySlowMa);
if (_stochastic != null)
DrawIndicator(area, _stochastic);
if (_rsi != null)
DrawIndicator(area, _rsi);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal fastMaValue, decimal slowMaValue, decimal verySlowMaValue)
{
if (candle.State != CandleStates.Finished)
return;
// Process stochastic and RSI manually only when enabled.
IIndicatorValue stochasticValue = null;
if (UseStochastic && _stochastic != null)
stochasticValue = _stochastic.Process(candle);
decimal rsiValue = 50m;
if (UseRsi && _rsi != null)
{
var rsiResult = _rsi.Process(new DecimalIndicatorValue(_rsi, candle.ClosePrice, candle.CloseTime) { IsFinal = true });
rsiValue = rsiResult.IsFormed ? rsiResult.ToDecimal() : 50m;
}
if (UseMovingAverageFilter && (!_fastMa.IsFormed || !_slowMa.IsFormed || !_verySlowMa.IsFormed))
return;
if (UseStochastic && !_stochastic.IsFormed)
return;
if (UseRsi && !_rsi.IsFormed)
return;
// Manage the active position before evaluating new signals.
UpdateRiskManagement(candle);
// Aggregate votes from all enabled filters.
var filters = 0;
var upVotes = 0;
var downVotes = 0;
if (UseMovingAverageFilter)
{
filters++;
if (fastMaValue > slowMaValue && slowMaValue > verySlowMaValue)
upVotes++;
else if (fastMaValue < slowMaValue && slowMaValue < verySlowMaValue)
downVotes++;
}
if (UseStochastic && stochasticValue != null)
{
if (stochasticValue is not IStochasticOscillatorValue stoch)
return;
if (stoch.K is not decimal stochK || stoch.D is not decimal stochD)
return;
filters++;
if (stochD < stochK && StochasticLowerLevel > stochK)
upVotes++;
if (stochD > stochK && StochasticUpperLevel < stochK)
downVotes++;
}
if (UseRsi)
{
filters++;
if (rsiValue < RsiLowerLevel)
upVotes++;
if (rsiValue > RsiUpperLevel)
downVotes++;
}
if (filters == 0)
return;
var longSignal = AllowLongs && upVotes == filters;
var shortSignal = AllowShorts && downVotes == filters;
// Only open a new trade when there is no active position.
if (Position == 0)
{
if (longSignal)
TryEnterLong(candle);
else if (shortSignal)
TryEnterShort(candle);
}
}
private void TryEnterLong(ICandleMessage candle)
{
var volume = CalculateEntryVolume();
if (volume <= 0m)
return;
// Enter a long position at market price.
BuyMarket(volume);
// Store trade prices for later risk management.
_entryPrice = candle.ClosePrice;
_breakevenActivated = false;
_trailingStop = null;
var stopOffset = StopLossPips > 0m ? StopLossPips * _pipSize : 0m;
var takeOffset = TakeProfitPips > 0m ? TakeProfitPips * _pipSize : 0m;
_stopPrice = stopOffset > 0m ? _entryPrice - stopOffset : null;
_takePrice = takeOffset > 0m ? _entryPrice + takeOffset : null;
}
private void TryEnterShort(ICandleMessage candle)
{
var volume = CalculateEntryVolume();
if (volume <= 0m)
return;
// Enter a short position at market price.
SellMarket(volume);
// Store trade prices for later risk management.
_entryPrice = candle.ClosePrice;
_breakevenActivated = false;
_trailingStop = null;
var stopOffset = StopLossPips > 0m ? StopLossPips * _pipSize : 0m;
var takeOffset = TakeProfitPips > 0m ? TakeProfitPips * _pipSize : 0m;
_stopPrice = stopOffset > 0m ? _entryPrice + stopOffset : null;
_takePrice = takeOffset > 0m ? _entryPrice - takeOffset : null;
}
private void UpdateRiskManagement(ICandleMessage candle)
{
// Manage long positions first.
if (Position > 0)
{
HandleBreakevenLong(candle);
HandleTrailingLong(candle);
if (_stopPrice is decimal stop && candle.LowPrice <= stop)
{
SellMarket(Math.Abs(Position));
ResetTradeState();
}
else if (_takePrice is decimal take && candle.HighPrice >= take)
{
SellMarket(Math.Abs(Position));
ResetTradeState();
}
}
// Manage short positions in the same fashion.
else if (Position < 0)
{
HandleBreakevenShort(candle);
HandleTrailingShort(candle);
if (_stopPrice is decimal stop && candle.HighPrice >= stop)
{
BuyMarket(Math.Abs(Position));
ResetTradeState();
}
else if (_takePrice is decimal take && candle.LowPrice <= take)
{
BuyMarket(Math.Abs(Position));
ResetTradeState();
}
}
// Reset helper state when flat.
else
{
ResetTradeState();
}
}
// Move the stop to breakeven for long trades once profit reaches the threshold.
private void HandleBreakevenLong(ICandleMessage candle)
{
if (_breakevenActivated || BreakevenPips <= 0m)
return;
var trigger = _entryPrice + BreakevenPips * _pipSize;
if (candle.HighPrice >= trigger)
{
_breakevenActivated = true;
UpdateLongStop(_entryPrice);
}
}
// Move the stop to breakeven for short trades once profit reaches the threshold.
private void HandleBreakevenShort(ICandleMessage candle)
{
if (_breakevenActivated || BreakevenPips <= 0m)
return;
var trigger = _entryPrice - BreakevenPips * _pipSize;
if (candle.LowPrice <= trigger)
{
_breakevenActivated = true;
UpdateShortStop(_entryPrice);
}
}
// Update trailing logic for long trades using distance and step thresholds.
private void HandleTrailingLong(ICandleMessage candle)
{
if (TrailingStopPips <= 0m)
return;
var distance = TrailingStopPips * _pipSize;
if (distance <= 0m)
return;
var step = TrailingStepPips * _pipSize;
var desiredStop = candle.ClosePrice - distance;
if (_trailingStop is null)
{
var activationPrice = _entryPrice + distance + step;
if (candle.HighPrice >= activationPrice)
{
_trailingStop = desiredStop;
UpdateLongStop(desiredStop);
}
}
else if (desiredStop > _trailingStop.Value + step)
{
_trailingStop = desiredStop;
UpdateLongStop(desiredStop);
}
}
// Update trailing logic for short trades using distance and step thresholds.
private void HandleTrailingShort(ICandleMessage candle)
{
if (TrailingStopPips <= 0m)
return;
var distance = TrailingStopPips * _pipSize;
if (distance <= 0m)
return;
var step = TrailingStepPips * _pipSize;
var desiredStop = candle.ClosePrice + distance;
if (_trailingStop is null)
{
var activationPrice = _entryPrice - distance - step;
if (candle.LowPrice <= activationPrice)
{
_trailingStop = desiredStop;
UpdateShortStop(desiredStop);
}
}
else if (desiredStop < _trailingStop.Value - step)
{
_trailingStop = desiredStop;
UpdateShortStop(desiredStop);
}
}
// Ensure the long stop can only move upward.
private void UpdateLongStop(decimal newLevel)
{
if (_stopPrice is null || newLevel > _stopPrice.Value)
_stopPrice = newLevel;
}
// Ensure the short stop can only move downward.
private void UpdateShortStop(decimal newLevel)
{
if (_stopPrice is null || newLevel < _stopPrice.Value)
_stopPrice = newLevel;
}
// Determine trade size using either fixed volume or risk-based sizing.
private decimal CalculateEntryVolume()
{
if (FixedVolume > 0m)
return AdjustVolume(FixedVolume);
var stopOffset = StopLossPips > 0m ? StopLossPips * _pipSize : 0m;
if (stopOffset <= 0m)
return AdjustVolume(Volume);
var riskVolume = GetRiskVolume(stopOffset);
return AdjustVolume(riskVolume);
}
// Translate the configured risk percentage into lots based on stop distance.
private decimal GetRiskVolume(decimal stopOffset)
{
if (stopOffset <= 0m)
return 0m;
var priceStep = Security?.PriceStep ?? 0m;
var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 0m;
if (priceStep <= 0m || stepPrice <= 0m)
return 0m;
var lossPerUnit = stopOffset / priceStep * stepPrice;
if (lossPerUnit <= 0m)
return 0m;
var equity = Portfolio?.CurrentValue ?? 0m;
if (equity <= 0m)
return 0m;
var riskAmount = equity * RiskPercent / 100m;
if (riskAmount <= 0m)
return 0m;
return riskAmount / lossPerUnit;
}
// Normalize the requested volume to instrument constraints.
private decimal AdjustVolume(decimal volume)
{
if (volume <= 0m)
return 0m;
var security = Security;
if (security == null)
return volume;
var step = security.VolumeStep ?? 0m;
if (step > 0m)
{
var steps = Math.Floor(volume / step);
var adjusted = steps * step;
if (adjusted <= 0m)
adjusted = step;
return adjusted;
}
return volume > 0m ? volume : 0m;
}
// Mimic the MetaTrader pip conversion used in the original script.
private decimal CalculatePipSize()
{
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
return 1m;
// For crypto and large-price instruments, scale pip size
// so that pip-based parameters produce meaningful price offsets.
return step;
}
// Clear cached state values when no position is active.
private void ResetTradeState()
{
_entryPrice = 0m;
_stopPrice = null;
_takePrice = null;
_trailingStop = null;
_breakevenActivated = false;
}
}
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, Decimal
from StockSharp.Messages import DataType, CandleStates, Level1Fields
from StockSharp.Algo.Indicators import ExponentialMovingAverage, RelativeStrengthIndex, StochasticOscillator
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class vr_zver_v2_strategy(Strategy):
"""Port of the VR-ZVER v2 expert advisor with triple EMA confirmation and stochastic/RSI filters."""
def __init__(self):
super(vr_zver_v2_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Time frame for signal generation", "General")
self._fixed_volume = self.Param("FixedVolume", Decimal(1)) \
.SetDisplay("Fixed Volume", "Use fixed volume when greater than zero", "Risk")
self._risk_percent = self.Param("RiskPercent", Decimal(10)) \
.SetDisplay("Risk %", "Risk percentage used when fixed volume is zero", "Risk")
self._stop_loss_pips = self.Param("StopLossPips", Decimal(10000)) \
.SetDisplay("Stop Loss (pips)", "Full stop distance expressed in pips", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", Decimal(15000)) \
.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", Decimal(8000)) \
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk")
self._trailing_step_pips = self.Param("TrailingStepPips", Decimal(3000)) \
.SetDisplay("Trailing Step (pips)", "Additional distance before trailing updates", "Risk")
self._breakeven_pips = self.Param("BreakevenPips", Decimal(5000)) \
.SetDisplay("Breakeven (pips)", "Move stop to entry after this profit", "Risk")
self._allow_longs = self.Param("AllowLongs", True) \
.SetDisplay("Allow Longs", "Permit buy trades", "General")
self._allow_shorts = self.Param("AllowShorts", True) \
.SetDisplay("Allow Shorts", "Permit sell trades", "General")
self._use_ma_filter = self.Param("UseMovingAverageFilter", True) \
.SetDisplay("Use MA Filter", "Require triple EMA alignment", "Indicators")
self._fast_period = self.Param("FastMaPeriod", 3).SetGreaterThanZero() \
.SetDisplay("Fast EMA", "Length of the fast EMA", "Indicators")
self._slow_period = self.Param("SlowMaPeriod", 5).SetGreaterThanZero() \
.SetDisplay("Slow EMA", "Length of the slow EMA", "Indicators")
self._very_slow_period = self.Param("VerySlowMaPeriod", 7).SetGreaterThanZero() \
.SetDisplay("Very Slow EMA", "Length of the very slow EMA", "Indicators")
self._use_stochastic = self.Param("UseStochastic", False) \
.SetDisplay("Use Stochastic", "Enable stochastic confirmation", "Indicators")
self._stoch_k_period = self.Param("StochasticKPeriod", 42).SetGreaterThanZero() \
.SetDisplay("Stochastic %K", "Number of periods for %K", "Indicators")
self._stoch_d_period = self.Param("StochasticDPeriod", 5).SetGreaterThanZero() \
.SetDisplay("Stochastic %D", "Smoothing period for %D", "Indicators")
self._stoch_smooth = self.Param("StochasticSmooth", 7).SetGreaterThanZero() \
.SetDisplay("Stochastic Smooth", "Final smoothing for stochastic", "Indicators")
self._stoch_upper = self.Param("StochasticUpperLevel", Decimal(60)) \
.SetDisplay("Stochastic Upper", "Upper threshold for short signals", "Indicators")
self._stoch_lower = self.Param("StochasticLowerLevel", Decimal(40)) \
.SetDisplay("Stochastic Lower", "Lower threshold for long signals", "Indicators")
self._use_rsi = self.Param("UseRsi", False) \
.SetDisplay("Use RSI", "Enable RSI filter", "Indicators")
self._rsi_period = self.Param("RsiPeriod", 14).SetGreaterThanZero() \
.SetDisplay("RSI Period", "Length of the RSI", "Indicators")
self._rsi_upper = self.Param("RsiUpperLevel", Decimal(60)) \
.SetDisplay("RSI Upper", "Upper threshold for short entries", "Indicators")
self._rsi_lower = self.Param("RsiLowerLevel", Decimal(40)) \
.SetDisplay("RSI Lower", "Lower threshold for long entries", "Indicators")
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(vr_zver_v2_strategy, self).OnReseted()
self._pip_size = Decimal(0)
self._fast_ma = None
self._slow_ma = None
self._very_slow_ma = None
self._stochastic = None
self._rsi = None
self._reset_trade()
def OnStarted2(self, time):
super(vr_zver_v2_strategy, self).OnStarted2(time)
self._pip_size = self._calculate_pip_size()
self._reset_trade()
self._fast_ma = ExponentialMovingAverage()
self._fast_ma.Length = self._fast_period.Value
self._slow_ma = ExponentialMovingAverage()
self._slow_ma.Length = self._slow_period.Value
self._very_slow_ma = ExponentialMovingAverage()
self._very_slow_ma.Length = self._very_slow_period.Value
self._stochastic = None
if self._use_stochastic.Value:
self._stochastic = StochasticOscillator()
self._stochastic.K.Length = self._stoch_k_period.Value
self._stochastic.D.Length = self._stoch_d_period.Value
self._rsi = None
if self._use_rsi.Value:
self._rsi = RelativeStrengthIndex()
self._rsi.Length = self._rsi_period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._fast_ma, self._slow_ma, self._very_slow_ma, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawIndicator(area, self._fast_ma)
self.DrawIndicator(area, self._slow_ma)
self.DrawIndicator(area, self._very_slow_ma)
if self._stochastic is not None:
self.DrawIndicator(area, self._stochastic)
if self._rsi is not None:
self.DrawIndicator(area, self._rsi)
self.DrawOwnTrades(area)
def _process_candle(self, candle, fast_val, slow_val, very_slow_val):
if candle.State != CandleStates.Finished:
return
# Process stochastic and RSI manually only when enabled.
stoch_value = None
if self._use_stochastic.Value and self._stochastic is not None:
stoch_value = self._stochastic.Process(candle)
rsi_val = Decimal(50)
if self._use_rsi.Value and self._rsi is not None:
rsi_result = process_float(self._rsi, candle.ClosePrice, candle.CloseTime, True)
if rsi_result.IsFormed:
rsi_val = rsi_result.ToDecimal()
else:
rsi_val = Decimal(50)
if self._use_ma_filter.Value and (not self._fast_ma.IsFormed or not self._slow_ma.IsFormed or not self._very_slow_ma.IsFormed):
return
if self._use_stochastic.Value and not self._stochastic.IsFormed:
return
if self._use_rsi.Value and not self._rsi.IsFormed:
return
# Manage the active position before evaluating new signals.
self._update_risk_management(candle)
# Aggregate votes from all enabled filters.
filters = 0
up_votes = 0
down_votes = 0
if self._use_ma_filter.Value:
filters += 1
if fast_val > slow_val and slow_val > very_slow_val:
up_votes += 1
elif fast_val < slow_val and slow_val < very_slow_val:
down_votes += 1
if self._use_stochastic.Value and stoch_value is not None:
try:
stoch_k = stoch_value.K
stoch_d = stoch_value.D
if stoch_k is None or stoch_d is None:
return
filters += 1
if stoch_d < stoch_k and self._stoch_lower.Value > stoch_k:
up_votes += 1
if stoch_d > stoch_k and self._stoch_upper.Value < stoch_k:
down_votes += 1
except:
return
if self._use_rsi.Value:
filters += 1
if rsi_val < self._rsi_lower.Value:
up_votes += 1
if rsi_val > self._rsi_upper.Value:
down_votes += 1
if filters == 0:
return
long_signal = self._allow_longs.Value and up_votes == filters
short_signal = self._allow_shorts.Value and down_votes == filters
# Only open a new trade when there is no active position.
if self.Position == 0:
if long_signal:
self._try_enter_long(candle)
elif short_signal:
self._try_enter_short(candle)
def _try_enter_long(self, candle):
volume = self._calculate_entry_volume()
if volume <= 0:
return
self.BuyMarket(volume)
self._entry_price = candle.ClosePrice
self._be_activated = False
self._trail_stop = None
stop_offset = Decimal.Multiply(self._stop_loss_pips.Value, self._pip_size) if self._stop_loss_pips.Value > 0 else Decimal(0)
take_offset = Decimal.Multiply(self._take_profit_pips.Value, self._pip_size) if self._take_profit_pips.Value > 0 else Decimal(0)
self._stop_price = Decimal.Subtract(self._entry_price, stop_offset) if stop_offset > 0 else None
self._take_price = Decimal.Add(self._entry_price, take_offset) if take_offset > 0 else None
def _try_enter_short(self, candle):
volume = self._calculate_entry_volume()
if volume <= 0:
return
self.SellMarket(volume)
self._entry_price = candle.ClosePrice
self._be_activated = False
self._trail_stop = None
stop_offset = Decimal.Multiply(self._stop_loss_pips.Value, self._pip_size) if self._stop_loss_pips.Value > 0 else Decimal(0)
take_offset = Decimal.Multiply(self._take_profit_pips.Value, self._pip_size) if self._take_profit_pips.Value > 0 else Decimal(0)
self._stop_price = Decimal.Add(self._entry_price, stop_offset) if stop_offset > 0 else None
self._take_price = Decimal.Subtract(self._entry_price, take_offset) if take_offset > 0 else None
def _update_risk_management(self, candle):
if self.Position > 0:
self._handle_breakeven_long(candle)
self._handle_trailing_long(candle)
if self._stop_price is not None and candle.LowPrice <= self._stop_price:
self.SellMarket(Math.Abs(self.Position))
self._reset_trade()
elif self._take_price is not None and candle.HighPrice >= self._take_price:
self.SellMarket(Math.Abs(self.Position))
self._reset_trade()
elif self.Position < 0:
self._handle_breakeven_short(candle)
self._handle_trailing_short(candle)
if self._stop_price is not None and candle.HighPrice >= self._stop_price:
self.BuyMarket(Math.Abs(self.Position))
self._reset_trade()
elif self._take_price is not None and candle.LowPrice <= self._take_price:
self.BuyMarket(Math.Abs(self.Position))
self._reset_trade()
else:
self._reset_trade()
def _handle_breakeven_long(self, candle):
if self._be_activated or self._breakeven_pips.Value <= 0:
return
trigger = Decimal.Add(self._entry_price, Decimal.Multiply(self._breakeven_pips.Value, self._pip_size))
if candle.HighPrice >= trigger:
self._be_activated = True
self._update_long_stop(self._entry_price)
def _handle_breakeven_short(self, candle):
if self._be_activated or self._breakeven_pips.Value <= 0:
return
trigger = Decimal.Subtract(self._entry_price, Decimal.Multiply(self._breakeven_pips.Value, self._pip_size))
if candle.LowPrice <= trigger:
self._be_activated = True
self._update_short_stop(self._entry_price)
def _handle_trailing_long(self, candle):
if self._trailing_stop_pips.Value <= 0:
return
distance = Decimal.Multiply(self._trailing_stop_pips.Value, self._pip_size)
if distance <= 0:
return
step = Decimal.Multiply(self._trailing_step_pips.Value, self._pip_size)
desired_stop = Decimal.Subtract(candle.ClosePrice, distance)
if self._trail_stop is None:
activation_price = Decimal.Add(Decimal.Add(self._entry_price, distance), step)
if candle.HighPrice >= activation_price:
self._trail_stop = desired_stop
self._update_long_stop(desired_stop)
elif desired_stop > Decimal.Add(self._trail_stop, step):
self._trail_stop = desired_stop
self._update_long_stop(desired_stop)
def _handle_trailing_short(self, candle):
if self._trailing_stop_pips.Value <= 0:
return
distance = Decimal.Multiply(self._trailing_stop_pips.Value, self._pip_size)
if distance <= 0:
return
step = Decimal.Multiply(self._trailing_step_pips.Value, self._pip_size)
desired_stop = Decimal.Add(candle.ClosePrice, distance)
if self._trail_stop is None:
activation_price = Decimal.Subtract(Decimal.Subtract(self._entry_price, distance), step)
if candle.LowPrice <= activation_price:
self._trail_stop = desired_stop
self._update_short_stop(desired_stop)
elif desired_stop < Decimal.Subtract(self._trail_stop, step):
self._trail_stop = desired_stop
self._update_short_stop(desired_stop)
def _update_long_stop(self, new_level):
if self._stop_price is None or new_level > self._stop_price:
self._stop_price = new_level
def _update_short_stop(self, new_level):
if self._stop_price is None or new_level < self._stop_price:
self._stop_price = new_level
def _calculate_entry_volume(self):
if self._fixed_volume.Value > 0:
return self._adjust_volume(self._fixed_volume.Value)
stop_offset = Decimal.Multiply(self._stop_loss_pips.Value, self._pip_size) if self._stop_loss_pips.Value > 0 else Decimal(0)
if stop_offset <= 0:
return self._adjust_volume(self.Volume)
risk_volume = self._get_risk_volume(stop_offset)
return self._adjust_volume(risk_volume)
def _get_risk_volume(self, stop_offset):
if stop_offset <= 0:
return Decimal(0)
sec = self.Security
price_step = sec.PriceStep if sec is not None and sec.PriceStep is not None else Decimal(0)
step_price = Decimal(0)
try:
sp = self.GetSecurityValue[Decimal](Level1Fields.StepPrice)
if sp is not None:
step_price = sp
except:
step_price = Decimal(0)
if price_step <= 0 or step_price <= 0:
return Decimal(0)
loss_per_unit = Decimal.Multiply(Decimal.Divide(stop_offset, price_step), step_price)
if loss_per_unit <= 0:
return Decimal(0)
portfolio = self.Portfolio
equity = portfolio.CurrentValue if portfolio is not None and portfolio.CurrentValue is not None else Decimal(0)
if equity <= 0:
return Decimal(0)
risk_amount = Decimal.Divide(Decimal.Multiply(equity, self._risk_percent.Value), Decimal(100))
if risk_amount <= 0:
return Decimal(0)
return Decimal.Divide(risk_amount, loss_per_unit)
def _adjust_volume(self, volume):
if volume <= 0:
return Decimal(0)
sec = self.Security
if sec is None:
return volume
step = sec.VolumeStep if sec.VolumeStep is not None else Decimal(0)
if step > 0:
steps = Math.Floor(Decimal.Divide(volume, step))
adjusted = Decimal.Multiply(steps, step)
if adjusted <= 0:
adjusted = step
return adjusted
return volume if volume > 0 else Decimal(0)
def _calculate_pip_size(self):
sec = self.Security
step = sec.PriceStep if sec is not None and sec.PriceStep is not None else Decimal(0)
if step <= 0:
return Decimal(1)
return step
def _reset_trade(self):
self._entry_price = Decimal(0)
self._stop_price = None
self._take_price = None
self._trail_stop = None
self._be_activated = False
def CreateClone(self):
return vr_zver_v2_strategy()