Estratégia Exodus
A estratégia EXODUS combina um oscilador de momentum ponderado por volume (VWMO) com o Average Directional Index (ADX) para identificar movimentos direcionais fortes.
Detalhes
- Critérios de entrada
- Comprado:
VWMO > VwmoThresholdeADX > AdxThreshold. - Vendido:
VWMO < -VwmoThresholdeADX > AdxThreshold.
- Comprado:
- Critérios de saída
- O momentum cruza zero ou um sinal oposto aparece.
- Indicadores
- Average True Range
- Average Directional Index
- Simple Moving Average
- Parâmetros
VwmoMomentum,VwmoVolume,VwmoSmooth,VwmoThresholdAtrLength,AtrMultiplier,TpMultiplierAdxLength,AdxThresholdVolumeCandleType
Fórmula VWMO e valores padrão: VwmoMomentum=10, VwmoVolume=20, VwmoSmooth=3, VwmoThreshold=25, AdxLength=14, AdxThreshold=25, AtrLength=14, AtrMultiplier=2, TpMultiplier=3, CandleType=1 minuto. Em cada candle fechado i, d_i=Close_i−Close_(i−VwmoMomentum). VWMO bruto =100 × Σ(d_i×Volume_i)/Σ(|d_i|×Volume_i) nos últimos VwmoVolume candles (zero para denominador zero), suavizado por SMA de VwmoSmooth. É impulso com sinal ponderado por volume (−100..100), não cruzamento EMA. Compra para VWMO > +VwmoThreshold && ADX > AdxThreshold; venda a descoberto para VWMO < −VwmoThreshold && ADX > AdxThreshold. Sinal contrário reverte a posição inteira numa ordem de mercado; sem sinal oposto, o cruzamento do VWMO pelo zero encerra a posição. Stop nativo Wilder ATR(AtrLength) × AtrMultiplier, alvo Stop × TpMultiplier, monitorando bid/ask Level1 entre candles. Sem ordens duplicadas ativas ou futuro.
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()