Exodus戦略
EXODUS 戦略は出来高加重モメンタムオシレーター(VWMO)とAverage Directional Index(ADX)を組み合わせて、強い方向性のある動きを検出します。
詳細
- エントリー条件
- ロング:
VWMO > VwmoThresholdかつADX > AdxThreshold。 - ショート:
VWMO < -VwmoThresholdかつADX > AdxThreshold。
- ロング:
- エグジット条件
- モメンタムがゼロを横切るか、反対のシグナルが現れる。
- インジケーター
- Average True Range
- Average Directional Index
- Simple Moving Average
- パラメーター
VwmoMomentum,VwmoVolume,VwmoSmooth,VwmoThresholdAtrLength,AtrMultiplier,TpMultiplierAdxLength,AdxThresholdVolumeCandleType
VWMOの式と初期値: VwmoMomentum=10、VwmoVolume=20、VwmoSmooth=3、VwmoThreshold=25、AdxLength=14、AdxThreshold=25、AtrLength=14、AtrMultiplier=2、TpMultiplier=3、CandleType=1分。確定足iについてd_i=Close_i−Close_(i−VwmoMomentum)、直近VwmoVolume本の生VWMOは100 × Σ(d_i×Volume_i)/Σ(|d_i|×Volume_i)(分母0なら0)。VwmoSmooth本のSMAで平滑化。これは−100..100の符号付き出来高加重モメンタムでありEMAクロスではありません。ロングはVWMO > +VwmoThreshold && ADX > AdxThreshold、ショートはVWMO < −VwmoThreshold && ADX > AdxThreshold。逆シグナルで成行1注文により全量反転。逆シグナルがなければVWMOのゼロクロスで決済。ネイティブストップ幅=Wilder ATR(AtrLength) × AtrMultiplier、利確幅=ストップ幅 × TpMultiplier、確定足の間もLevel1 bid/askで監視します。未完了注文がある間の重複注文や未来参照はありません。
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>
/// Volume-weighted directional momentum filtered by ADX.
/// Reverses on an opposite signal, exits on a momentum zero cross,
/// and protects filled positions by frozen ATR-based native distances.
/// </summary>
public class ExodusStrategy : Strategy
{
private readonly StrategyParam<int> _vwmoMomentum;
private readonly StrategyParam<int> _vwmoVolume;
private readonly StrategyParam<int> _vwmoSmooth;
private readonly StrategyParam<decimal> _vwmoThreshold;
private readonly StrategyParam<int> _adxLength;
private readonly StrategyParam<decimal> _adxThreshold;
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<decimal> _tpMultiplier;
private readonly StrategyParam<DataType> _candleType;
private readonly List<(decimal close, decimal volume)> _history = new();
private readonly Queue<decimal> _oscillator = new();
private decimal? _previousVwmo;
private decimal? _previousClose;
private decimal _atr;
private int _atrSamples;
private Order _pendingOrder;
private Unit _stopDistance;
private Unit _targetDistance;
private bool _protectionStarted;
private AverageDirectionalIndex _adx;
public int VwmoMomentum { get => _vwmoMomentum.Value; set => _vwmoMomentum.Value = value; }
public int VwmoVolume { get => _vwmoVolume.Value; set => _vwmoVolume.Value = value; }
public int VwmoSmooth { get => _vwmoSmooth.Value; set => _vwmoSmooth.Value = value; }
public decimal VwmoThreshold { get => _vwmoThreshold.Value; set => _vwmoThreshold.Value = value; }
public int AdxLength { get => _adxLength.Value; set => _adxLength.Value = value; }
public decimal AdxThreshold { get => _adxThreshold.Value; set => _adxThreshold.Value = value; }
public int AtrLength { get => _atrLength.Value; set => _atrLength.Value = value; }
public decimal AtrMultiplier { get => _atrMultiplier.Value; set => _atrMultiplier.Value = value; }
public decimal TpMultiplier { get => _tpMultiplier.Value; set => _tpMultiplier.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public ExodusStrategy()
{
_vwmoMomentum = Param(nameof(VwmoMomentum), 10).SetGreaterThanZero().SetDisplay("VWMO Momentum", "Momentum difference lag, in candles.", "VWMO");
_vwmoVolume = Param(nameof(VwmoVolume), 20).SetGreaterThanZero().SetDisplay("VWMO Volume", "Window for volume-weighted momentum.", "VWMO");
_vwmoSmooth = Param(nameof(VwmoSmooth), 3).SetGreaterThanZero().SetDisplay("VWMO Smooth", "SMA period for normalized oscillator.", "VWMO");
_vwmoThreshold = Param(nameof(VwmoThreshold), 25m).SetNotNegative().SetDisplay("VWMO Threshold", "Absolute normalized oscillator entry threshold.", "VWMO");
_adxLength = Param(nameof(AdxLength), 14).SetGreaterThanZero().SetDisplay("ADX Length", "Average Directional Index period.", "Strength");
_adxThreshold = Param(nameof(AdxThreshold), 25m).SetNotNegative().SetDisplay("ADX Threshold", "Minimum trend strength for entry.", "Strength");
_atrLength = Param(nameof(AtrLength), 14).SetGreaterThanZero().SetDisplay("ATR Length", "Wilder ATR period for frozen protection.", "Protection");
_atrMultiplier = Param(nameof(AtrMultiplier), 2m).SetGreaterThanZero().SetDisplay("ATR Multiplier", "Stop-loss distance as an ATR multiple.", "Protection");
_tpMultiplier = Param(nameof(TpMultiplier), 3m).SetGreaterThanZero().SetDisplay("TP Multiplier", "Take-profit distance as an additional stop multiple.", "Protection");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame()).SetDisplay("Candle Type", "Source candles.", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
protected override void OnReseted()
{
base.OnReseted();
_history.Clear();
_oscillator.Clear();
_previousVwmo = null;
_previousClose = null;
_atr = 0m;
_atrSamples = 0;
_pendingOrder = null;
_protectionStarted = false;
_stopDistance = null;
_targetDistance = null;
_adx = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_adx = new AverageDirectionalIndex { Length = AdxLength };
_stopDistance = new Unit(0m, UnitTypes.Absolute);
_targetDistance = new Unit(0m, UnitTypes.Absolute);
OrderRegistering += order => _pendingOrder = order;
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.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _adx);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// Native protection is evaluated on the quote stream between signal candles.
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var adxResult = (AverageDirectionalIndexValue)_adx.Process(candle);
var trueRange = _previousClose is decimal previous
? Math.Max(candle.HighPrice - candle.LowPrice,
Math.Max(Math.Abs(candle.HighPrice - previous), Math.Abs(candle.LowPrice - previous)))
: candle.HighPrice - candle.LowPrice;
_atrSamples++;
var atrCount = Math.Min(_atrSamples, AtrLength);
_atr = (_atr * (atrCount - 1) + trueRange) / atrCount;
_previousClose = candle.ClosePrice;
_history.Add((candle.ClosePrice, candle.TotalVolume));
var needed = VwmoMomentum + VwmoVolume;
if (_history.Count > needed) _history.RemoveAt(0);
if (_history.Count < needed || !adxResult.IsFormed || adxResult.MovingAverage is not decimal strength)
return;
var numerator = 0m;
var denominator = 0m;
for (var i = _history.Count - VwmoVolume; i < _history.Count; i++)
{
var weightedDelta = (_history[i].close - _history[i - VwmoMomentum].close) * _history[i].volume;
numerator += weightedDelta;
denominator += Math.Abs(weightedDelta);
}
_oscillator.Enqueue(denominator > 0m ? numerator * 100m / denominator : 0m);
while (_oscillator.Count > VwmoSmooth) _oscillator.Dequeue();
if (_oscillator.Count < VwmoSmooth)
return;
var vwmo = _oscillator.Average();
var pendingActive = _pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed);
if (!pendingActive)
{
var strong = strength > AdxThreshold;
var signalLong = strong && vwmo > VwmoThreshold;
var signalShort = strong && vwmo < -VwmoThreshold;
var zeroExit = Position > 0m && _previousVwmo is decimal longMomentum && longMomentum > 0m && vwmo <= 0m
|| Position < 0m && _previousVwmo is decimal shortMomentum && shortMomentum < 0m && vwmo >= 0m;
if (signalLong && Position <= 0m)
{
ConfigureProtection(_atr);
BuyMarket(Volume + Math.Abs(Position));
}
else if (signalShort && Position >= 0m)
{
ConfigureProtection(_atr);
SellMarket(Volume + Math.Abs(Position));
}
else if (zeroExit && Position != 0m)
{
if (Position > 0m) SellMarket(Position);
else BuyMarket(Math.Abs(Position));
}
}
_previousVwmo = vwmo;
}
private void ConfigureProtection(decimal atr)
{
if (atr <= 0m)
throw new InvalidOperationException("ATR must be positive before placing a protected trade.");
_stopDistance.Value = atr * AtrMultiplier;
_targetDistance.Value = _stopDistance.Value * TpMultiplier;
if (!_protectionStarted)
{
StartProtection(_targetDistance, _stopDistance, useMarketOrders: true, isLocalStop: true);
_protectionStarted = true;
}
}
}
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 Decimal, TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, OrderStates, Unit, UnitTypes, Level1Fields
from StockSharp.Algo.Indicators import AverageDirectionalIndex, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Subscription
class exodus_strategy(Strategy):
"""Normalized volume-weighted momentum with ADX filtering and native ATR protection."""
def __init__(self):
super(exodus_strategy, self).__init__()
self._vwmo_momentum = self.Param("VwmoMomentum", 10).SetGreaterThanZero() \
.SetDisplay("VWMO Momentum", "Lag for price momentum.", "VWMO")
self._vwmo_volume = self.Param("VwmoVolume", 20).SetGreaterThanZero() \
.SetDisplay("VWMO Volume", "Volume-weighted momentum window.", "VWMO")
self._vwmo_smooth = self.Param("VwmoSmooth", 3).SetGreaterThanZero() \
.SetDisplay("VWMO Smooth", "SMA smoothing period of normalized oscillator.", "VWMO")
self._vwmo_threshold = self.Param("VwmoThreshold", 25.0).SetNotNegative() \
.SetDisplay("VWMO Threshold", "Absolute normalized oscillator threshold.", "VWMO")
self._adx_length = self.Param("AdxLength", 14).SetGreaterThanZero() \
.SetDisplay("ADX Length", "Average Directional Index period.", "Strength")
self._adx_threshold = self.Param("AdxThreshold", 25.0).SetNotNegative() \
.SetDisplay("ADX Threshold", "Minimum trend strength.", "Strength")
self._atr_length = self.Param("AtrLength", 14).SetGreaterThanZero() \
.SetDisplay("ATR Length", "Wilder ATR period for entry protection.", "Protection")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0).SetGreaterThanZero() \
.SetDisplay("ATR Multiplier", "Initial stop as a multiple of ATR.", "Protection")
self._tp_multiplier = self.Param("TpMultiplier", 3.0).SetGreaterThanZero() \
.SetDisplay("TP Multiplier", "Take-profit distance as a stop multiple.", "Protection")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))) \
.SetDisplay("Candle Type", "Candle timeframe.", "General")
self._history = []
self._oscillator = []
self._previous_vwmo = None
self._previous_close = None
self._atr = 0.0
self._atr_samples = 0
self._pending_order = None
self._protection_started = False
self._stop_distance = None
self._target_distance = None
def GetWorkingSecurities(self):
return [(self.Security, self._candle_type.Value), (self.Security, DataType.Level1)]
def OnReseted(self):
super(exodus_strategy, self).OnReseted()
self._history = []
self._oscillator = []
self._previous_vwmo = None
self._previous_close = None
self._atr = 0.0
self._atr_samples = 0
self._pending_order = None
self._protection_started = False
self._stop_distance = None
self._target_distance = None
def OnStarted2(self, time):
super(exodus_strategy, self).OnStarted2(time)
self._adx = AverageDirectionalIndex()
self._adx.Length = int(self._adx_length.Value)
self._stop_distance = Unit(Decimal.Zero, UnitTypes.Absolute)
self._target_distance = Unit(Decimal.Zero, UnitTypes.Absolute)
self.OrderRegistering += self._on_order_registering
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.Value)
subscription.Bind(self.on_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._adx)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# Native protection is evaluated on incoming bid/ask updates between bars.
pass
def _on_order_registering(self, order):
self._pending_order = order
def _configure_protection(self, atr):
if atr <= 0.0:
raise ValueError("A positive ATR is required for protected entries")
stop = atr * float(self._atr_multiplier.Value)
target = stop * float(self._tp_multiplier.Value)
self._stop_distance.Value = Decimal(stop)
self._target_distance.Value = Decimal(target)
if not self._protection_started:
self.StartProtection(self._target_distance, self._stop_distance,
useMarketOrders=True, isLocalStop=True)
self._protection_started = True
def on_candle(self, candle):
if candle.State != CandleStates.Finished:
return
input_value = CandleIndicatorValue(self._adx, candle)
input_value.IsFinal = True
value = self._adx.Process(input_value)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
volume = float(candle.TotalVolume)
true_range = (max(high - low, abs(high - self._previous_close),
abs(low - self._previous_close))
if self._previous_close is not None else high - low)
self._atr_samples += 1
atr_count = min(self._atr_samples, int(self._atr_length.Value))
self._atr = (self._atr * (atr_count - 1) + true_range) / atr_count
self._previous_close = close
momentum_period = int(self._vwmo_momentum.Value)
volume_period = int(self._vwmo_volume.Value)
smooth = int(self._vwmo_smooth.Value)
need = momentum_period + volume_period
self._history.append((close, volume))
if len(self._history) > need:
self._history.pop(0)
strength = value.MovingAverage
if len(self._history) < need or not value.IsFormed or strength is None:
return
numerator = 0.0
denominator = 0.0
for i in range(len(self._history) - volume_period, len(self._history)):
weighted_delta = (self._history[i][0] - self._history[i - momentum_period][0]) * self._history[i][1]
numerator += weighted_delta
denominator += abs(weighted_delta)
self._oscillator.append(100.0 * numerator / denominator if denominator > 0.0 else 0.0)
if len(self._oscillator) > smooth:
self._oscillator.pop(0)
if len(self._oscillator) < smooth:
return
oscillator = sum(self._oscillator) / smooth
active = self._pending_order is not None and self._pending_order.State not in (
OrderStates.Done, OrderStates.Failed)
if not active:
strong = float(strength) > float(self._adx_threshold.Value)
long_signal = strong and oscillator > float(self._vwmo_threshold.Value)
short_signal = strong and oscillator < -float(self._vwmo_threshold.Value)
position = self.Position
zero_exit = (position > 0 and self._previous_vwmo is not None
and self._previous_vwmo > 0 and oscillator <= 0) or (
position < 0 and self._previous_vwmo is not None
and self._previous_vwmo < 0 and oscillator >= 0)
if long_signal and position <= 0:
self._configure_protection(self._atr)
self.BuyMarket(self.Volume + Math.Abs(position))
elif short_signal and position >= 0:
self._configure_protection(self._atr)
self.SellMarket(self.Volume + Math.Abs(position))
elif zero_exit and position != 0:
if position > 0:
self.SellMarket(position)
else:
self.BuyMarket(Math.Abs(position))
self._previous_vwmo = oscillator
def CreateClone(self):
return exodus_strategy()