Стратегия VarMovAvg
Общее описание
Стратегия VarMovAvg – это конвертация советника MetaTrader 4 VarMovAvg_v0011. В ней используется адаптивная скользящая средняя VMA для оценки тренда. Система ожидает двухэтапный откат (бар A и бар B по терминологии оригинала) и после его завершения переворачивает позицию. При открытой сделке применяется плавающий стоп на базе скользящей средней, а при появлении противоположного паттерна выполняется немедленный разворот.
Логика работы
- Адаптивная VMA. Индикатор
VariableMovingAverageповторяет формулу MT4:- коэффициент эффективности сравнивает текущую цену закрытия с ценой
AmaPeriodбаров назад и делит на сумму абсолютных колебаний; - сглаживающий множитель интерполируется между быстрым и медленным периодом и возводится в степень
SmoothingPower(параметрGв исходнике).
- коэффициент эффективности сравнивает текущую цену закрытия с ценой
- Обнаружение сигнала (бар A / бар B). Для длинных и коротких сделок ведутся отдельные конечные автоматы:
- бар A: цена уходит от VMA минимум на
SignalPipsBarAпунктов; - бар B: движение продолжается ещё на
SignalPipsBarBпунктов, фиксируя экстремум; - вход: при возврате цены в диапазон
SignalPipsTrade ± EntryPipsDiffвыставляется рыночный ордер (или разворотная сделка).
- бар A: цена уходит от VMA минимум на
- Трейлинг-стоп и разворот.
- Для лонга используется среднее по минимумам, для шорта — по максимумам, с учётом
StopMaShiftиStopPipsDiff. - Пробой свечой уровня стопа закрывает позицию.
- При появлении противоположного паттерна совершается единичная сделка объёмом
|Position| + Volume, что эквивалентно работе советника.
- Для лонга используется среднее по минимумам, для шорта — по максимумам, с учётом
Параметры
| Параметр | Назначение | Соответствие MT4 |
|---|---|---|
AmaPeriod |
Длина окна VMA. | prm.vma.periodAMA |
FastPeriod |
Быстрый сглаживающий период. | prm.vma.nfast |
SlowPeriod |
Медленный сглаживающий период. | prm.vma.nslow |
SmoothingPower |
Степень для адаптивного коэффициента (G). |
prm.vma.G |
SignalPipsBarA |
Минимальное удаление цены для бара A. | prm.sig.pipsBarA |
SignalPipsBarB |
Дополнительное удаление для бара B. | prm.sig.pipsBarB |
SignalPipsTrade |
Смещение входной линии от экстремума бара B. | prm.sig.pipsTrade |
EntryPipsDiff |
Допустимый коридор вокруг входной цены. | prm.entry.diff |
StopPipsDiff |
Отступ от скользящей для трейлинг-стопа. | prm.stop.diff |
StopMaPeriod |
Период стоповой скользящей. | prm.mastop.period |
StopMaShift |
Сдвиг (в барах) стоповой скользящей. | prm.mastop.shift |
StopMaMethod |
Тип скользящей (MODE_SMA/EMA/SMMA/LWMA). |
prm.mastop.method |
CandleType |
Рабочий таймфрейм. | Таймфрейм графика |
Перевод в пункты. Все расстояния в пунктах домножаются на
Security.PriceStep, если шаг цены настроен. При отсутствии шага значения трактуются как абсолютные цены — аналогично запасному варианту советника.
Практические замечания
- Расчёт ведётся на закрытых свечах через
SubscribeCandles; коридор входа имитирует тиковые проверки советника. - Трейлинг-стоп реализован рыночным выходом при пробое экстремума, что эквивалентно постоянному модифицированию стоп-ордера в MT4.
- Для реализации
StopMaShiftиспользуется очередь значений, поэтому сдвиг 0 означает текущую точку, а положительные значения выбирают прошлые бары. - После любой сделки оба автомата сбрасываются в нейтральное состояние — полная аналогия
STATUS_TRADEв MQL.
Быстрый старт
- Добавьте стратегию в StockSharp и выберите инструмент с корректно заданным
PriceStep. - Настройте таймфрейм через
CandleType(советник чаще применялся на M5). - Подберите параметры в пунктах и трейлинг под специфику брокера.
- Запустите стратегию — при формировании паттернов бар A/бар B система будет чередовать длинные и короткие позиции.
Отличия от оригинала
- Переход на расчёт по закрытым свечам: это упрощает реализацию, но благодаря входному коридору момент входа остаётся близок к MT4.
- Стопы реализованы через рыночные сделки вместо стоп-ордеров, что типично для стратегий StockSharp.
- Индикатор VMA переписан на C#, параметр
dKисключён как неиспользуемый в исходнике.
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>
/// Variable Moving Average (VarMovAvg) reversal strategy converted from the MetaTrader expert.
/// Tracks adaptive VMA swings and enters on the Bar A/Bar B breakout pattern.
/// </summary>
public class VarMovAvgStrategy : Strategy
{
/// <summary>
/// MetaTrader moving average methods supported by the stop calculation.
/// </summary>
public enum MovingAverageMethods
{
Simple,
Exponential,
Smoothed,
Weighted
}
private readonly StrategyParam<int> _amaPeriod;
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<decimal> _smoothingPower;
private readonly StrategyParam<decimal> _signalPipsBarA;
private readonly StrategyParam<decimal> _signalPipsBarB;
private readonly StrategyParam<decimal> _signalPipsTrade;
private readonly StrategyParam<decimal> _entryPipsDiff;
private readonly StrategyParam<decimal> _stopPipsDiff;
private readonly StrategyParam<int> _stopMaPeriod;
private readonly StrategyParam<int> _stopMaShift;
private readonly StrategyParam<MovingAverageMethods> _stopMaMethod;
private readonly StrategyParam<DataType> _candleType;
private VariableMovingAverage _vma;
private IIndicator _stopLowMa;
private IIndicator _stopHighMa;
private Queue<decimal> _lowMaValues;
private Queue<decimal> _highMaValues;
private SignalTracker _longSignal;
private SignalTracker _shortSignal;
/// <summary>
/// VMA adaptive window length.
/// </summary>
public int AmaPeriod
{
get => _amaPeriod.Value;
set => _amaPeriod.Value = value;
}
/// <summary>
/// Fast smoothing period used inside the VMA efficiency ratio.
/// </summary>
public int FastPeriod
{
get => _fastPeriod.Value;
set => _fastPeriod.Value = value;
}
/// <summary>
/// Slow smoothing period used inside the VMA efficiency ratio.
/// </summary>
public int SlowPeriod
{
get => _slowPeriod.Value;
set => _slowPeriod.Value = value;
}
/// <summary>
/// Power applied to the smoothing coefficient (MetaTrader parameter G).
/// </summary>
public decimal SmoothingPower
{
get => _smoothingPower.Value;
set => _smoothingPower.Value = value;
}
/// <summary>
/// Distance in pips required for Bar A confirmation.
/// </summary>
public decimal SignalPipsBarA
{
get => _signalPipsBarA.Value;
set => _signalPipsBarA.Value = value;
}
/// <summary>
/// Additional distance in pips required for Bar B confirmation.
/// </summary>
public decimal SignalPipsBarB
{
get => _signalPipsBarB.Value;
set => _signalPipsBarB.Value = value;
}
/// <summary>
/// Offset in pips between the Bar B extreme and the actual entry line.
/// </summary>
public decimal SignalPipsTrade
{
get => _signalPipsTrade.Value;
set => _signalPipsTrade.Value = value;
}
/// <summary>
/// Width in pips accepted when price touches the entry line.
/// </summary>
public decimal EntryPipsDiff
{
get => _entryPipsDiff.Value;
set => _entryPipsDiff.Value = value;
}
/// <summary>
/// Offset in pips applied to the trailing stop moving average.
/// </summary>
public decimal StopPipsDiff
{
get => _stopPipsDiff.Value;
set => _stopPipsDiff.Value = value;
}
/// <summary>
/// Period of the trailing stop moving average.
/// </summary>
public int StopMaPeriod
{
get => _stopMaPeriod.Value;
set => _stopMaPeriod.Value = value;
}
/// <summary>
/// Shift (in bars) applied to the trailing stop moving average.
/// </summary>
public int StopMaShift
{
get => _stopMaShift.Value;
set => _stopMaShift.Value = value;
}
/// <summary>
/// Moving average method used for trailing stop calculation.
/// </summary>
public MovingAverageMethods StopMaMethod
{
get => _stopMaMethod.Value;
set => _stopMaMethod.Value = value;
}
/// <summary>
/// Candle type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes the VarMovAvg strategy.
/// </summary>
public VarMovAvgStrategy()
{
_amaPeriod = Param(nameof(AmaPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("VMA Length", "Adaptive moving average period", "Indicators")
.SetOptimize(20, 120, 10);
_fastPeriod = Param(nameof(FastPeriod), 5)
.SetGreaterThanZero()
.SetDisplay("Fast Period", "Fast smoothing period for VMA", "Indicators")
.SetOptimize(2, 15, 1);
_slowPeriod = Param(nameof(SlowPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Slow Period", "Slow smoothing period for VMA", "Indicators")
.SetOptimize(15, 60, 5);
_smoothingPower = Param(nameof(SmoothingPower), 1m)
.SetGreaterThanZero()
.SetDisplay("Smoothing Power", "Exponent applied to the smoothing coefficient", "Indicators");
_signalPipsBarA = Param(nameof(SignalPipsBarA), 1m)
.SetGreaterThanZero()
.SetDisplay("Bar A Distance", "Pips distance below/above VMA for Bar A", "Signals");
_signalPipsBarB = Param(nameof(SignalPipsBarB), 1m)
.SetGreaterThanZero()
.SetDisplay("Bar B Distance", "Extra pips distance for Bar B confirmation", "Signals");
_signalPipsTrade = Param(nameof(SignalPipsTrade), 10m)
.SetGreaterThanZero()
.SetDisplay("Entry Offset", "Pips offset from Bar B extreme to entry", "Signals");
_entryPipsDiff = Param(nameof(EntryPipsDiff), 500m)
.SetGreaterThanZero()
.SetDisplay("Entry Band", "Accepted pips range around the entry price", "Signals");
_stopPipsDiff = Param(nameof(StopPipsDiff), 34m)
.SetGreaterThanZero()
.SetDisplay("Stop Offset", "Pips offset from the trailing moving average", "Risk");
_stopMaPeriod = Param(nameof(StopMaPeriod), 52)
.SetGreaterThanZero()
.SetDisplay("Stop MA Period", "Period of the trailing moving average", "Risk");
_stopMaShift = Param(nameof(StopMaShift), 0)
.SetNotNegative()
.SetDisplay("Stop MA Shift", "Bars shift applied to the stop moving average", "Risk");
_stopMaMethod = Param(nameof(StopMaMethod), MovingAverageMethods.Exponential)
.SetDisplay("Stop MA Method", "Moving average type used for stops", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Working candle timeframe", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_vma = null;
_stopLowMa = null;
_stopHighMa = null;
_lowMaValues = null;
_highMaValues = null;
_longSignal = null;
_shortSignal = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
StartProtection(null, null);
_vma = new VariableMovingAverage
{
Length = AmaPeriod,
FastPeriod = FastPeriod,
SlowPeriod = SlowPeriod,
SmoothingPower = SmoothingPower
};
_stopLowMa = CreateMovingAverage(StopMaMethod, StopMaPeriod);
_stopHighMa = CreateMovingAverage(StopMaMethod, StopMaPeriod);
_lowMaValues = new Queue<decimal>();
_highMaValues = new Queue<decimal>();
_longSignal = new SignalTracker(true);
_shortSignal = new SignalTracker(false);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _vma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_vma is null || _stopLowMa is null || _stopHighMa is null || _lowMaValues is null || _highMaValues is null || _longSignal is null || _shortSignal is null)
return;
var time = candle.CloseTime;
var vmaResult = _vma.Process(new DecimalIndicatorValue(_vma, candle.ClosePrice, time) { IsFinal = true });
if (vmaResult.IsEmpty) return;
var vmaValue = vmaResult.GetValue<decimal>();
var lowMaResult = _stopLowMa.Process(new DecimalIndicatorValue(_stopLowMa, candle.LowPrice, time) { IsFinal = true });
if (lowMaResult.IsEmpty) return;
var lowMaRaw = lowMaResult.GetValue<decimal>();
var highMaResult = _stopHighMa.Process(new DecimalIndicatorValue(_stopHighMa, candle.HighPrice, time) { IsFinal = true });
if (highMaResult.IsEmpty) return;
var highMaRaw = highMaResult.GetValue<decimal>();
var lowMa = GetShiftedValue(_lowMaValues, lowMaRaw, StopMaShift);
var highMa = GetShiftedValue(_highMaValues, highMaRaw, StopMaShift);
var barADistance = ToPriceDistance(SignalPipsBarA);
var barBDistance = ToPriceDistance(SignalPipsBarB);
var tradeOffset = ToPriceDistance(SignalPipsTrade);
var entryBand = ToPriceDistance(EntryPipsDiff);
var stopOffset = ToPriceDistance(StopPipsDiff);
_longSignal.Update(candle, vmaValue, barADistance, barBDistance, tradeOffset);
_shortSignal.Update(candle, vmaValue, barADistance, barBDistance, tradeOffset);
if (Position == 0)
{
if (Volume > 0 && _longSignal.TryEnter(candle, entryBand))
{
BuyMarket();
AfterEntry();
}
else if (Volume > 0 && _shortSignal.TryEnter(candle, entryBand))
{
SellMarket();
AfterEntry();
}
return;
}
if (Position > 0)
{
if (Volume > 0 && _shortSignal.TryEnter(candle, entryBand))
{
var volumeToSell = Position + Volume;
if (volumeToSell > 0)
SellMarket(volumeToSell);
AfterEntry();
return;
}
var stopPrice = lowMa - stopOffset;
if (stopPrice > 0m && candle.LowPrice <= stopPrice)
{
SellMarket();
AfterExit();
}
}
else
{
if (Volume > 0 && _longSignal.TryEnter(candle, entryBand))
{
var volumeToBuy = Math.Abs(Position) + Volume;
if (volumeToBuy > 0)
BuyMarket(volumeToBuy);
AfterEntry();
return;
}
var stopPrice = highMa + stopOffset;
if (stopPrice > 0m && candle.HighPrice >= stopPrice)
{
BuyMarket();
AfterExit();
}
}
}
private void AfterEntry()
{
_longSignal.Reset();
_shortSignal.Reset();
}
private void AfterExit()
{
_longSignal.Reset();
_shortSignal.Reset();
}
private decimal ToPriceDistance(decimal pips)
{
var step = Security?.PriceStep ?? 0m;
return step > 0m ? pips * step : pips;
}
private static decimal GetShiftedValue(Queue<decimal> buffer, decimal value, int shift)
{
buffer.Enqueue(value);
var maxCount = Math.Max(1, shift + 1);
while (buffer.Count > maxCount)
buffer.Dequeue();
var index = buffer.Count - 1 - Math.Min(shift, buffer.Count - 1);
var current = 0;
foreach (var item in buffer)
{
if (current == index)
return item;
current++;
}
return value;
}
private static IIndicator CreateMovingAverage(MovingAverageMethods method, int length)
{
return method switch
{
MovingAverageMethods.Simple => new SimpleMovingAverage { Length = length },
MovingAverageMethods.Smoothed => new SmoothedMovingAverage { Length = length },
MovingAverageMethods.Weighted => new WeightedMovingAverage { Length = length },
_ => new ExponentialMovingAverage { Length = length }
};
}
private sealed class SignalTracker
{
private enum SignalStates
{
Neutral,
BarA,
BarB
}
private readonly bool _isLong;
private SignalStates _state = SignalStates.Neutral;
private decimal _barAReference;
private decimal _entryPrice;
public SignalTracker(bool isLong)
{
_isLong = isLong;
}
public void Reset()
{
_state = SignalStates.Neutral;
_barAReference = 0m;
_entryPrice = 0m;
}
public void Update(ICandleMessage candle, decimal vma, decimal barAOffset, decimal barBOffset, decimal tradeOffset)
{
var close = candle.ClosePrice;
var high = candle.HighPrice;
var low = candle.LowPrice;
if (_isLong)
{
if (close <= vma - barAOffset)
{
Reset();
return;
}
switch (_state)
{
case SignalStates.Neutral:
if (close >= vma + barAOffset)
{
_state = SignalStates.BarA;
_barAReference = close;
}
break;
case SignalStates.BarA:
if (close <= vma - barAOffset)
{
Reset();
return;
}
if (close >= _barAReference + barBOffset)
{
_state = SignalStates.BarB;
_entryPrice = high + tradeOffset;
}
break;
case SignalStates.BarB:
if (close <= vma - barAOffset)
Reset();
break;
}
}
else
{
if (close >= vma + barAOffset)
{
Reset();
return;
}
switch (_state)
{
case SignalStates.Neutral:
if (close <= vma - barAOffset)
{
_state = SignalStates.BarA;
_barAReference = close;
}
break;
case SignalStates.BarA:
if (close >= vma + barAOffset)
{
Reset();
return;
}
if (close <= _barAReference - barBOffset)
{
_state = SignalStates.BarB;
_entryPrice = low - tradeOffset;
}
break;
case SignalStates.BarB:
if (close >= vma + barAOffset)
Reset();
break;
}
}
}
public bool TryEnter(ICandleMessage candle, decimal entryBand)
{
if (_state != SignalStates.BarB)
return false;
var close = candle.ClosePrice;
if (_isLong)
{
var upper = _entryPrice + entryBand;
if (close >= _entryPrice && close <= upper)
{
Reset();
return true;
}
}
else
{
var lower = _entryPrice - entryBand;
if (close <= _entryPrice && close >= lower)
{
Reset();
return true;
}
}
return false;
}
}
private sealed class VariableMovingAverage : DecimalLengthIndicator
{
private readonly Queue<decimal> _closes = new();
private decimal? _previousAma;
public int FastPeriod { get; set; } = 5;
public int SlowPeriod { get; set; } = 20;
public decimal SmoothingPower { get; set; } = 1m;
protected override IIndicatorValue OnProcess(IIndicatorValue input)
{
var value = input.GetValue<decimal>();
_closes.Enqueue(value);
var required = Math.Max(2, Length + 1);
while (_closes.Count > required)
_closes.Dequeue();
if (_previousAma == null)
_previousAma = value;
var closeCount = _closes.Count;
if (closeCount < 2)
return new DecimalIndicatorValue(this, value, input.Time);
var effectiveLength = Math.Min(Length, closeCount - 1);
var closes = _closes.ToArray();
var newestIndex = closes.Length - 1;
var baseIndex = newestIndex - effectiveLength;
if (baseIndex < 0)
baseIndex = 0;
var newest = closes[newestIndex];
var oldest = closes[baseIndex];
var signal = Math.Abs(newest - oldest);
decimal noise = 0.000000001m;
for (var i = baseIndex; i < newestIndex; i++)
noise += Math.Abs(closes[i + 1] - closes[i]);
var efficiency = noise != 0m ? signal / noise : 0m;
var slowSc = 2m / (SlowPeriod + 1m);
var fastSc = 2m / (FastPeriod + 1m);
var smoothing = slowSc + efficiency * (fastSc - slowSc);
var smoothingFactor = smoothing > 0m ? (decimal)Math.Pow((double)smoothing, (double)SmoothingPower) : 0m;
var amaPrev = _previousAma ?? oldest;
var ama = amaPrev + smoothingFactor * (value - amaPrev);
_previousAma = ama;
IsFormed = closeCount >= required;
return new DecimalIndicatorValue(this, ama, input.Time);
}
public override void Reset()
{
base.Reset();
_closes.Clear();
_previousAma = null;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import (
ExponentialMovingAverage,
SimpleMovingAverage, SmoothedMovingAverage, WeightedMovingAverage
)
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
# MA method constants
MA_SIMPLE = 0
MA_EXPONENTIAL = 1
MA_SMOOTHED = 2
MA_WEIGHTED = 3
# Signal state constants
SIGNAL_NEUTRAL = 0
SIGNAL_BAR_A = 1
SIGNAL_BAR_B = 2
class var_mov_avg_strategy(Strategy):
"""Variable Moving Average reversal strategy. Tracks adaptive VMA swings and
enters on the Bar A/Bar B breakout pattern with MA-based trailing stop."""
def __init__(self):
super(var_mov_avg_strategy, self).__init__()
self._ama_period = self.Param("AmaPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("VMA Length", "Adaptive moving average period", "Indicators")
self._fast_period = self.Param("FastPeriod", 5) \
.SetGreaterThanZero() \
.SetDisplay("Fast Period", "Fast smoothing period for VMA", "Indicators")
self._slow_period = self.Param("SlowPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Slow Period", "Slow smoothing period for VMA", "Indicators")
self._smoothing_power = self.Param("SmoothingPower", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Smoothing Power", "Exponent applied to the smoothing coefficient", "Indicators")
self._signal_pips_bar_a = self.Param("SignalPipsBarA", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Bar A Distance", "Pips distance below/above VMA for Bar A", "Signals")
self._signal_pips_bar_b = self.Param("SignalPipsBarB", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Bar B Distance", "Extra pips distance for Bar B confirmation", "Signals")
self._signal_pips_trade = self.Param("SignalPipsTrade", 10.0) \
.SetGreaterThanZero() \
.SetDisplay("Entry Offset", "Pips offset from Bar B extreme to entry", "Signals")
self._entry_pips_diff = self.Param("EntryPipsDiff", 500.0) \
.SetGreaterThanZero() \
.SetDisplay("Entry Band", "Accepted pips range around the entry price", "Signals")
self._stop_pips_diff = self.Param("StopPipsDiff", 34.0) \
.SetGreaterThanZero() \
.SetDisplay("Stop Offset", "Pips offset from the trailing moving average", "Risk")
self._stop_ma_period = self.Param("StopMaPeriod", 52) \
.SetGreaterThanZero() \
.SetDisplay("Stop MA Period", "Period of the trailing moving average", "Risk")
self._stop_ma_shift = self.Param("StopMaShift", 0) \
.SetDisplay("Stop MA Shift", "Bars shift applied to the stop moving average", "Risk")
self._stop_ma_method = self.Param("StopMaMethod", MA_EXPONENTIAL) \
.SetDisplay("Stop MA Method", "Moving average type used for stops", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Working candle timeframe", "General")
# VMA internal state
self._vma_closes = []
self._vma_prev_ama = None
# Stop MA internal state
self._stop_low_ma = None
self._stop_high_ma = None
self._low_ma_buffer = []
self._high_ma_buffer = []
# Signal trackers
self._long_state = SIGNAL_NEUTRAL
self._long_bar_a_ref = 0.0
self._long_entry_price = 0.0
self._short_state = SIGNAL_NEUTRAL
self._short_bar_a_ref = 0.0
self._short_entry_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 AmaPeriod(self):
return self._ama_period.Value
@property
def FastPeriod(self):
return self._fast_period.Value
@property
def SlowPeriod(self):
return self._slow_period.Value
@property
def SmoothingPower(self):
return self._smoothing_power.Value
@property
def SignalPipsBarA(self):
return self._signal_pips_bar_a.Value
@property
def SignalPipsBarB(self):
return self._signal_pips_bar_b.Value
@property
def SignalPipsTrade(self):
return self._signal_pips_trade.Value
@property
def EntryPipsDiff(self):
return self._entry_pips_diff.Value
@property
def StopPipsDiff(self):
return self._stop_pips_diff.Value
@property
def StopMaPeriod(self):
return self._stop_ma_period.Value
@property
def StopMaShift(self):
return self._stop_ma_shift.Value
@property
def StopMaMethod(self):
return self._stop_ma_method.Value
def OnReseted(self):
super(var_mov_avg_strategy, self).OnReseted()
self._vma_closes = []
self._vma_prev_ama = None
self._stop_low_ma = None
self._stop_high_ma = None
self._low_ma_buffer = []
self._high_ma_buffer = []
self._long_state = SIGNAL_NEUTRAL
self._long_bar_a_ref = 0.0
self._long_entry_price = 0.0
self._short_state = SIGNAL_NEUTRAL
self._short_bar_a_ref = 0.0
self._short_entry_price = 0.0
def OnStarted2(self, time):
super(var_mov_avg_strategy, self).OnStarted2(time)
method = self.StopMaMethod
period = self.StopMaPeriod
self._stop_low_ma = self._create_ma(method, period)
self._stop_high_ma = self._create_ma(method, period)
self._low_ma_buffer = []
self._high_ma_buffer = []
self._vma_closes = []
self._vma_prev_ama = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _create_ma(self, method, period):
if method == MA_SIMPLE:
ma = SimpleMovingAverage()
elif method == MA_SMOOTHED:
ma = SmoothedMovingAverage()
elif method == MA_WEIGHTED:
ma = WeightedMovingAverage()
else:
ma = ExponentialMovingAverage()
ma.Length = period
return ma
def _calculate_vma(self, close_price, time):
"""Custom VMA calculation matching the C# VariableMovingAverage."""
self._vma_closes.append(close_price)
length = self.AmaPeriod
required = max(2, length + 1)
while len(self._vma_closes) > required:
self._vma_closes.pop(0)
if self._vma_prev_ama is None:
self._vma_prev_ama = close_price
close_count = len(self._vma_closes)
if close_count < 2:
return close_price, False
effective_length = min(length, close_count - 1)
newest_index = close_count - 1
base_index = newest_index - effective_length
if base_index < 0:
base_index = 0
newest = self._vma_closes[newest_index]
oldest = self._vma_closes[base_index]
signal = abs(newest - oldest)
noise = 0.000000001
for i in range(base_index, newest_index):
noise += abs(self._vma_closes[i + 1] - self._vma_closes[i])
efficiency = signal / noise if noise != 0 else 0.0
slow_sc = 2.0 / (float(self.SlowPeriod) + 1.0)
fast_sc = 2.0 / (float(self.FastPeriod) + 1.0)
smoothing = slow_sc + efficiency * (fast_sc - slow_sc)
sp = float(self.SmoothingPower)
smoothing_factor = pow(smoothing, sp) if smoothing > 0 else 0.0
ama_prev = self._vma_prev_ama if self._vma_prev_ama is not None else oldest
ama = ama_prev + smoothing_factor * (close_price - ama_prev)
self._vma_prev_ama = ama
is_formed = close_count >= required
return ama, is_formed
def _process_stop_ma(self, ma, value, time):
"""Process a decimal value through the stop MA indicator."""
result = process_float(ma, value, time, True)
if result.IsEmpty:
return None
return float(result)
def _get_shifted_value(self, buffer, value, shift):
"""Get a shifted MA value from the buffer."""
buffer.append(value)
max_count = max(1, shift + 1)
while len(buffer) > max_count:
buffer.pop(0)
index = len(buffer) - 1 - min(shift, len(buffer) - 1)
return buffer[index]
def _to_price_distance(self, pips):
step = self.Security.PriceStep if self.Security is not None else 0.0
if step is None or float(step) <= 0:
return float(pips)
return float(pips) * float(step)
def _update_long_signal(self, candle, vma, bar_a_offset, bar_b_offset, trade_offset):
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
if close <= vma - bar_a_offset:
self._long_state = SIGNAL_NEUTRAL
self._long_bar_a_ref = 0.0
self._long_entry_price = 0.0
return
if self._long_state == SIGNAL_NEUTRAL:
if close >= vma + bar_a_offset:
self._long_state = SIGNAL_BAR_A
self._long_bar_a_ref = close
elif self._long_state == SIGNAL_BAR_A:
if close <= vma - bar_a_offset:
self._long_state = SIGNAL_NEUTRAL
self._long_bar_a_ref = 0.0
self._long_entry_price = 0.0
return
if close >= self._long_bar_a_ref + bar_b_offset:
self._long_state = SIGNAL_BAR_B
self._long_entry_price = high + trade_offset
elif self._long_state == SIGNAL_BAR_B:
if close <= vma - bar_a_offset:
self._long_state = SIGNAL_NEUTRAL
self._long_bar_a_ref = 0.0
self._long_entry_price = 0.0
def _update_short_signal(self, candle, vma, bar_a_offset, bar_b_offset, trade_offset):
close = float(candle.ClosePrice)
low = float(candle.LowPrice)
if close >= vma + bar_a_offset:
self._short_state = SIGNAL_NEUTRAL
self._short_bar_a_ref = 0.0
self._short_entry_price = 0.0
return
if self._short_state == SIGNAL_NEUTRAL:
if close <= vma - bar_a_offset:
self._short_state = SIGNAL_BAR_A
self._short_bar_a_ref = close
elif self._short_state == SIGNAL_BAR_A:
if close >= vma + bar_a_offset:
self._short_state = SIGNAL_NEUTRAL
self._short_bar_a_ref = 0.0
self._short_entry_price = 0.0
return
if close <= self._short_bar_a_ref - bar_b_offset:
self._short_state = SIGNAL_BAR_B
self._short_entry_price = low - trade_offset
elif self._short_state == SIGNAL_BAR_B:
if close >= vma + bar_a_offset:
self._short_state = SIGNAL_NEUTRAL
self._short_bar_a_ref = 0.0
self._short_entry_price = 0.0
def _try_long_enter(self, candle, entry_band):
if self._long_state != SIGNAL_BAR_B:
return False
close = float(candle.ClosePrice)
upper = self._long_entry_price + entry_band
if close >= self._long_entry_price and close <= upper:
self._reset_signals()
return True
return False
def _try_short_enter(self, candle, entry_band):
if self._short_state != SIGNAL_BAR_B:
return False
close = float(candle.ClosePrice)
lower = self._short_entry_price - entry_band
if close <= self._short_entry_price and close >= lower:
self._reset_signals()
return True
return False
def _reset_signals(self):
self._long_state = SIGNAL_NEUTRAL
self._long_bar_a_ref = 0.0
self._long_entry_price = 0.0
self._short_state = SIGNAL_NEUTRAL
self._short_bar_a_ref = 0.0
self._short_entry_price = 0.0
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
low = float(candle.LowPrice)
high = float(candle.HighPrice)
time = candle.CloseTime
# Calculate VMA
vma_value, vma_formed = self._calculate_vma(close, time)
if not vma_formed:
return
# Calculate stop MAs
low_ma_raw = self._process_stop_ma(self._stop_low_ma, low, time)
if low_ma_raw is None:
return
high_ma_raw = self._process_stop_ma(self._stop_high_ma, high, time)
if high_ma_raw is None:
return
shift = self.StopMaShift
low_ma = self._get_shifted_value(self._low_ma_buffer, low_ma_raw, shift)
high_ma = self._get_shifted_value(self._high_ma_buffer, high_ma_raw, shift)
bar_a_distance = self._to_price_distance(self.SignalPipsBarA)
bar_b_distance = self._to_price_distance(self.SignalPipsBarB)
trade_offset = self._to_price_distance(self.SignalPipsTrade)
entry_band = self._to_price_distance(self.EntryPipsDiff)
stop_offset = self._to_price_distance(self.StopPipsDiff)
self._update_long_signal(candle, vma_value, bar_a_distance, bar_b_distance, trade_offset)
self._update_short_signal(candle, vma_value, bar_a_distance, bar_b_distance, trade_offset)
if self.Position == 0:
if self._try_long_enter(candle, entry_band):
self.BuyMarket()
self._reset_signals()
elif self._try_short_enter(candle, entry_band):
self.SellMarket()
self._reset_signals()
return
if self.Position > 0:
if self._try_short_enter(candle, entry_band):
# Reverse: close long and open short
self.SellMarket()
self.SellMarket()
self._reset_signals()
return
stop_price = low_ma - stop_offset
if stop_price > 0 and low <= stop_price:
self.SellMarket()
self._reset_signals()
else:
if self._try_long_enter(candle, entry_band):
# Reverse: close short and open long
self.BuyMarket()
self.BuyMarket()
self._reset_signals()
return
stop_price = high_ma + stop_offset
if stop_price > 0 and high >= stop_price:
self.BuyMarket()
self._reset_signals()
def CreateClone(self):
return var_mov_avg_strategy()