MA Deviation 戦略
価格が移動平均から大きく乖離したときに取引する戦略
テストでは年平均リターン約124%が示されています。外国為替市場で最もパフォーマンスが高くなります。
MA Deviationは、価格が移動平均から設定した割合だけ乖離したときにエントリーし、平均への回帰を期待します。ポジションは価格が平均に向かって収束したときに決済されます。
乖離の閾値はボラティリティに応じて広げたり狭めたりできます。ATRを使ったポジションサイジングにより、市場を問わずリスクを一定に保ちます。
詳細
- エントリー条件: MA、ATRに基づくシグナル。
- ロング/ショート: 両方。
- エグジット条件: 逆シグナルまたはストップ。
- ストップ: はい。
- デフォルト値:
MAPeriod= 20DeviationPercent= 5mAtrPeriod= 14AtrMultiplier= 2.0mCandleType= TimeSpan.FromMinutes(5)
- フィルター:
- カテゴリ: トレンド
- 方向: 両方
- インジケーター: MA, ATR
- ストップ: はい
- 複雑さ: 基本
- 時間軸: イントラデイ (5m)
- 季節性: いいえ
- ニューラルネットワーク: いいえ
- ダイバージェンス: いいえ
- リスクレベル: 中
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy that trades when price deviates significantly from its moving average.
/// Opens positions when price deviates by a specified percentage from MA
/// and closes when price returns to MA.
/// </summary>
public class MADeviationStrategy : Strategy
{
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<decimal> _deviationPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<decimal> _riskPercent;
private Order _pendingOrder;
private Unit _stopDistance;
private bool _protectionStarted;
/// <summary>
/// Period for Moving Average calculation.
/// </summary>
public int MAPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Deviation percentage from MA required for entry.
/// </summary>
public decimal DeviationPercent
{
get => _deviationPercent.Value;
set => _deviationPercent.Value = value;
}
/// <summary>
/// Type of candles used for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize the MA Deviation strategy.
/// </summary>
public MADeviationStrategy()
{
_maPeriod = Param(nameof(MAPeriod), 20).SetGreaterThanZero()
.SetDisplay("MA Period", "Period for Moving Average calculation", "Indicators")
.SetOptimize(10, 50, 5);
_deviationPercent = Param(nameof(DeviationPercent), 5m).SetGreaterThanZero()
.SetDisplay("Deviation %", "Deviation percentage from MA required for entry", "Entry")
.SetOptimize(1m, 10m, 1m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_atrPeriod = Param(nameof(AtrPeriod), 14).SetGreaterThanZero()
.SetDisplay("ATR Period", "Wilder ATR lookback for sizing and protection.", "Protection");
_atrMultiplier = Param(nameof(AtrMultiplier), 2m).SetNotNegative()
.SetDisplay("ATR Multiplier", "Frozen entry ATR distance; zero disables stop and ATR sizing.", "Protection");
_riskPercent = Param(nameof(RiskPercent), 1m).SetGreaterThanZero()
.SetDisplay("Risk (%)", "Portfolio value percentage budgeted against the entry ATR stop.", "Protection");
OrderRegistering += order => _pendingOrder = order;
}
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public decimal AtrMultiplier { get => _atrMultiplier.Value; set => _atrMultiplier.Value = value; }
public decimal RiskPercent { get => _riskPercent.Value; set => _riskPercent.Value = value; }
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType), (Security, DataType.Level1)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pendingOrder = null;
_stopDistance = null;
_protectionStarted = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var sma = new SimpleMovingAverage { Length = MAPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
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 subscription = SubscribeCandles(CandleType);
subscription
.BindEx(sma, atr, ProcessCandle, false)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawIndicator(area, atr);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// Native protection runs before the callback, including between finished candles.
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue maValue, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!maValue.IsFormed || !atrValue.IsFormed || !IsFormedAndOnlineAndAllowTrading())
return;
if (_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
var mean = maValue.GetValue<decimal>();
if (mean <= 0m)
return;
var deviation = 100m * (candle.ClosePrice - mean) / mean;
if (Position > 0m && candle.ClosePrice >= mean)
SellMarket(Position);
else if (Position < 0m && candle.ClosePrice <= mean)
BuyMarket(Math.Abs(Position));
else if (Position == 0m && Math.Abs(deviation) > DeviationPercent)
Enter(deviation < 0m ? Sides.Buy : Sides.Sell, candle.ClosePrice, atrValue.GetValue<decimal>());
}
private void Enter(Sides side, decimal close, decimal atr)
{
var distance = atr * AtrMultiplier;
var volume = CalculateVolume(close, distance);
if (volume <= 0m)
return;
_stopDistance ??= new Unit(distance);
_stopDistance.Value = distance;
if (!_protectionStarted && distance > 0m)
{
StartProtection(new Unit(), _stopDistance, useMarketOrders: true, isLocalStop: true);
_protectionStarted = true;
}
RegisterOrder(new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = side,
Volume = volume,
Comment = "MA deviation entry",
});
}
private decimal CalculateVolume(decimal close, decimal distance)
{
if (AtrMultiplier == 0m)
return NormalizeVolume(Volume);
var balance = Portfolio.CurrentValue ?? Portfolio.BeginValue ?? 0m;
var stepPrice = Security.StepPrice ?? 0m;
var priceStep = Security.PriceStep ?? 0m;
var moneyFactor = stepPrice > 0m && priceStep > 0m ? stepPrice / priceStep : Security.Multiplier ?? 1m;
if (balance <= 0m || distance <= 0m || close <= 0m || moneyFactor <= 0m)
return 0m;
var budget = balance * RiskPercent / 100m;
var riskVolume = budget / (distance * moneyFactor);
var cashVolume = balance / (close * moneyFactor);
return NormalizeVolume(Math.Min(riskVolume, cashVolume));
}
private decimal NormalizeVolume(decimal volume)
{
var minimum = Math.Max(0m, Security.MinVolume ?? 0m);
var maximum = Security.MaxVolume is decimal max && max > 0m ? max : decimal.MaxValue;
volume = Math.Min(volume, maximum);
if (Security.VolumeStep is decimal step && step > 0m)
{
minimum = Math.Ceiling(minimum / step) * step;
if (maximum != decimal.MaxValue) maximum = Math.Floor(maximum / step) * step;
volume = Math.Floor(volume / step) * step;
}
// Never round UP through the risk budget to force a minimum-size trade.
return minimum <= maximum && volume >= minimum ? volume : 0m;
}
}
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, Math
from StockSharp.Messages import DataType, CandleStates, Unit, OrderStates, OrderTypes, Sides, Level1Fields
from StockSharp.BusinessEntities import Subscription, Order
from StockSharp.Algo.Indicators import SimpleMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class ma_deviation_strategy(Strategy):
"""
MA Deviation strategy.
Trades when price deviates significantly from its moving average.
"""
def __init__(self):
super(ma_deviation_strategy, self).__init__()
self._ma_period = self.Param("MAPeriod", 20).SetGreaterThanZero().SetDisplay("MA Period", "Period for Moving Average calculation", "Indicators")
self._deviation_percent = self.Param("DeviationPercent", 5.0).SetGreaterThanZero().SetDisplay("Deviation %", "Deviation percentage from MA required for entry", "Entry")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._atr_period = self.Param("AtrPeriod", 14).SetGreaterThanZero() \
.SetDisplay("ATR Period", "Wilder ATR lookback for sizing and protection.", "Protection")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0).SetNotNegative() \
.SetDisplay("ATR Multiplier", "Frozen entry ATR distance; zero disables stop and ATR sizing.", "Protection")
self._risk_percent = self.Param("RiskPercent", 1.0).SetGreaterThanZero() \
.SetDisplay("Risk (%)", "Portfolio value percentage budgeted against the entry ATR stop.", "Protection")
self._pending_order = None
self._stop_distance = None
self._protection_started = False
self.OrderRegistering += self._track_pending
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def _track_pending(self, order):
self._pending_order = order
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(ma_deviation_strategy, self).OnReseted()
self._pending_order = None
self._stop_distance = None
self._protection_started = False
def OnStarted2(self, time):
super(ma_deviation_strategy, self).OnStarted2(time)
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
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()
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(sma, atr, self._process_candle, False).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawIndicator(area, atr)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# Native protection runs before this callback, including between signal bars.
pass
def _process_candle(self, candle, ma_val, atr_val):
if candle.State != CandleStates.Finished:
return
if not ma_val.IsFormed or not atr_val.IsFormed or not self.IsFormedAndOnlineAndAllowTrading():
return
if self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed):
return
mean = ma_val.GetValue[Decimal](None)
if mean <= 0:
return
close = candle.ClosePrice
deviation = Decimal(100) * (close - mean) / mean
if self.Position > 0 and close >= mean:
self.SellMarket(self.Position)
elif self.Position < 0 and close <= mean:
self.BuyMarket(Math.Abs(self.Position))
elif self.Position == 0 and Math.Abs(deviation) > Decimal(self._deviation_percent.Value):
self._enter(Sides.Buy if deviation < 0 else Sides.Sell, close, atr_val.GetValue[Decimal](None))
def _enter(self, side, close, atr):
distance = atr * Decimal(self._atr_multiplier.Value)
volume = self._calculate_volume(close, distance)
if volume <= 0:
return
if self._stop_distance is None:
self._stop_distance = Unit(distance)
self._stop_distance.Value = distance
if not self._protection_started and distance > 0:
self.StartProtection(Unit(), self._stop_distance, useMarketOrders=True, isLocalStop=True)
self._protection_started = True
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = volume
order.Comment = "MA deviation entry"
self.RegisterOrder(order)
def _calculate_volume(self, close, distance):
if self._atr_multiplier.Value == 0:
return self._normalize_volume(self.Volume)
balance = self.Portfolio.CurrentValue
if balance is None:
balance = self.Portfolio.BeginValue
if balance is None:
balance = Decimal.Zero
step_price = self.Security.StepPrice if self.Security.StepPrice is not None else Decimal.Zero
price_step = self.Security.PriceStep if self.Security.PriceStep is not None else Decimal.Zero
money_factor = step_price / price_step if step_price > 0 and price_step > 0 else self.Security.Multiplier
if money_factor is None:
money_factor = Decimal.One
if balance <= 0 or distance <= 0 or close <= 0 or money_factor <= 0:
return Decimal.Zero
budget = balance * Decimal(self._risk_percent.Value) / Decimal(100)
risk_volume = budget / (distance * money_factor)
cash_volume = balance / (close * money_factor)
return self._normalize_volume(Math.Min(risk_volume, cash_volume))
def _normalize_volume(self, volume):
minimum = self.Security.MinVolume if self.Security.MinVolume is not None else Decimal.Zero
minimum = Math.Max(Decimal.Zero, minimum)
maximum = self.Security.MaxVolume
if maximum is None or maximum <= 0:
maximum = Decimal.MaxValue
volume = Math.Min(volume, maximum)
step = self.Security.VolumeStep
if step is not None and step > 0:
minimum = Math.Ceiling(minimum / step) * step
if maximum != Decimal.MaxValue:
maximum = Math.Floor(maximum / step) * step
volume = Math.Floor(volume / step) * step
# Never round up through the risk budget to force a minimum-size trade.
return volume if minimum <= maximum and volume >= minimum else Decimal.Zero
def CreateClone(self):
return ma_deviation_strategy()