Стратегия MA Volume
MA Volume сочетает фильтр тренда по скользящей средней с всплесками объёма для определения точек входа. Рост объёма при цене выше средней сигнализирует о накоплении; падение объёма ниже средней указывает на распределение.
Тестирование показывает среднегодичную доходность около 136%. Стратегию лучше запускать на фондовом рынке.
Стратегия торгует по направлению скользящей средней, когда объём увеличивается, и выходит, когда объём снижается или средняя разворачивается.
Процентный стоп защищает от резких изменений тренда.
Детали
- Критерий входа: сигнал индикатора
- Длинная/короткая сторона: обе
- Критерий выхода: стоп-лосс или противоположный сигнал
- Стопы: да, процентные
- Значения по умолчанию:
CandleType= 15 минутStopLossPercent= 2MaPeriod= 20VolumePeriod= 20VolumeThreshold= 1.2 Объём расширяется, когда объём свечи больше VolumeThreshold средних объёмов предыдущих VolumePeriod свечей. Без позиции свеча с расширением, закрывшаяся выше растущей SMA, открывает покупку, а закрывшаяся ниже падающей SMA — продажу. Позиция закрывается, когда объём опускается ниже среднего или SMA разворачивается против неё.
- Фильтры:
- Категория: Следование за трендом
- Направление: обе
- Индикаторы: Moving Average, Volume
- Стопы: да
- Сложность: средняя
- Таймфрейм: внутридневной
- Сезонность: нет
- Нейросети: нет
- Дивергенция: нет
- Уровень риска: средний
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// MA Volume strategy.
/// Volume expands when a candle's volume exceeds VolumeThreshold times the average of the previous VolumePeriod candles.
/// While flat, an expanding candle that closes above a rising MaPeriod SMA goes long and one that closes below a falling SMA goes short.
/// The position closes once volume dries up below its average or the SMA turns against it, and a percent stop limits the loss.
/// </summary>
public class MaVolumeStrategy : Strategy
{
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _volumePeriod;
private readonly StrategyParam<decimal> _volumeThreshold;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _volumes = [];
private decimal? _prevMa;
/// <summary>
/// Period of the trend SMA.
/// </summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Previous candles the volume is averaged over.
/// </summary>
public int VolumePeriod
{
get => _volumePeriod.Value;
set => _volumePeriod.Value = value;
}
/// <summary>
/// How many times the average volume an expanding candle must exceed.
/// </summary>
public decimal VolumeThreshold
{
get => _volumeThreshold.Value;
set => _volumeThreshold.Value = value;
}
/// <summary>
/// Stop loss percentage from entry price.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public MaVolumeStrategy()
{
_maPeriod = Param(nameof(MaPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("MA Period", "Period of the trend SMA", "Indicators");
_volumePeriod = Param(nameof(VolumePeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Volume Period", "Previous candles the volume is averaged over", "Indicators");
_volumeThreshold = Param(nameof(VolumeThreshold), 1.2m)
.SetGreaterThanZero()
.SetDisplay("Volume Threshold", "How many times the average volume an expanding candle must exceed", "Indicators");
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetNotNegative()
.SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_volumes.Clear();
_prevMa = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_volumes.Clear();
_prevMa = null;
var sma = new SimpleMovingAverage { Length = MaPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(sma, ProcessCandle)
.Start();
StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);
// The stop has to see prices between candles, not only at their close.
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
}
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// The high-level handler activates native protection before this callback, also between signal bars.
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue smaValue)
{
if (candle.State != CandleStates.Finished)
return;
// Expansion is measured against the candles before this one.
var average = _volumes.Count == VolumePeriod ? _volumes.Average() : (decimal?)null;
_volumes.Add(candle.TotalVolume);
if (_volumes.Count > VolumePeriod)
_volumes.RemoveAt(0);
if (!smaValue.IsFormed)
return;
var ma = smaValue.GetValue<decimal>();
var prevMa = _prevMa;
_prevMa = ma;
if (average is not decimal avgVolume || prevMa is not decimal lastMa)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var close = candle.ClosePrice;
var volume = candle.TotalVolume;
if (Position > 0)
{
if (volume < avgVolume || ma < lastMa)
SellMarket(Position);
}
else if (Position < 0)
{
if (volume < avgVolume || ma > lastMa)
BuyMarket(-Position);
}
else if (volume > avgVolume * VolumeThreshold)
{
if (close > ma && ma > lastMa)
BuyMarket(Volume);
else if (close < ma && ma < lastMa)
SellMarket(Volume);
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes, Level1Fields
from StockSharp.BusinessEntities import Subscription
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class ma_volume_strategy(Strategy):
"""
MA Volume strategy.
Volume expands when a candle's volume exceeds VolumeThreshold times the average of the previous VolumePeriod candles.
While flat, an expanding candle that closes above a rising MaPeriod SMA goes long and one that closes below a falling SMA goes short.
The position closes once volume dries up below its average or the SMA turns against it, and a percent stop limits the loss.
"""
def __init__(self):
super(ma_volume_strategy, self).__init__()
self._ma_period = self.Param("MaPeriod", 20).SetGreaterThanZero().SetDisplay("MA Period", "Period of the trend SMA", "Indicators")
self._volume_period = self.Param("VolumePeriod", 20).SetGreaterThanZero().SetDisplay("Volume Period", "Previous candles the volume is averaged over", "Indicators")
self._volume_threshold = self.Param("VolumeThreshold", 1.2).SetGreaterThanZero().SetDisplay("Volume Threshold", "How many times the average volume an expanding candle must exceed", "Indicators")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0).SetNotNegative().SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._volumes = []
self._prev_ma = None
def OnReseted(self):
super(ma_volume_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(ma_volume_strategy, self).OnStarted2(time)
self._reset_state()
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(sma, self._process_candle).Start()
self.StartProtection(Unit(), Unit(Decimal(self._stop_loss_percent.Value), UnitTypes.Percent), useMarketOrders=True, isLocalStop=True)
# The stop has to see prices between candles, not only at their close.
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_protection_quote).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# The high-level handler activates native protection before the callback, including between bars.
pass
def _process_candle(self, candle, sma_value):
if candle.State != CandleStates.Finished:
return
# Expansion is measured against the candles before this one.
period = self._volume_period.Value
average = None
if len(self._volumes) == period:
total = Decimal(0)
for volume in self._volumes:
total += volume
average = total / Decimal(period)
self._volumes.append(candle.TotalVolume)
if len(self._volumes) > period:
self._volumes.pop(0)
if not sma_value.IsFormed:
return
ma = sma_value.GetValue[Decimal](None)
prev_ma = self._prev_ma
self._prev_ma = ma
if average is None or prev_ma is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
close = candle.ClosePrice
volume = candle.TotalVolume
if self.Position > 0:
if volume < average or ma < prev_ma:
self.SellMarket(self.Position)
elif self.Position < 0:
if volume < average or ma > prev_ma:
self.BuyMarket(-self.Position)
elif volume > average * Decimal(self._volume_threshold.Value):
if close > ma and ma > prev_ma:
self.BuyMarket(self.Volume)
elif close < ma and ma < prev_ma:
self.SellMarket(self.Volume)
def CreateClone(self):
return ma_volume_strategy()