Insights ledger

Oransel Research Strategy Engineering

Algorithmic Mean Reversion Part 2: Automating Execution and Risk Management

An educational NinjaTrader 8 strategy prototype for close-confirmed mean-reversion entries, stop handling, and realistic validation assumptions.

Part 1 defined an indicator that marks an outside-band close accompanied by above-average volume. This note turns that event into a NinjaTrader 8 Strategy prototype so its order assumptions can be inspected and tested. The example is educational and unvalidated; it is not a complete trading system and does not demonstrate profitability.

What changes from Part 1

A strategy must define when an order is submitted, how position state is handled, and which conditions request an exit. This version deliberately changes the volume test from Part 1's Volume > 1.0 × SMA(Volume, 10) to the stricter illustrative threshold Volume > 1.5 × SMA(Volume, 10). That difference is a research parameter, not evidence that 1.5 is superior.

With Calculate.OnBarClose, a long entry request is submitted after a bar closes below the lower band with sufficient volume; a short request is submitted after a bar closes above the upper band. EnterLong and EnterShort are NinjaTrader managed-order methods. They are not a promise of a fill at the next bar's open. Historical standard-resolution processing commonly models a market request at the next available event, while live timing and price depend on the connection, market, latency, and liquidity.

Exit and stop definitions

The middle Bollinger line is evaluated once per completed bar. A long requests an exit when Close[0] >= Middle[0]; a short requests an exit when Close[0] <= Middle[0]. This is a close-confirmed mean-cross exit, not a working limit order at the moving mean and not an intrabar touch detector. The eventual fill may differ from the middle-band value.

The code also calls SetStopLoss(CalculationMode.Ticks, 40) under its default. Forty ticks is an order offset whose currency value depends on the instrument. It does not cap realized loss: gaps, slippage, rejection, disconnection, market state, or adapter behavior can produce a larger loss. Whether an order is held locally, by an adapter, at a broker, or at an exchange depends on the order type and trading connection.

There is intentionally no fixed profit-target parameter. The earlier draft declared ProfitTargetTicks but never called SetProfitTarget, so the setting had no effect. Removing it keeps the public configuration aligned with the implemented close-based exit.

NinjaScript strategy prototype

#region Using declarations
using System;
using System.ComponentModel.DataAnnotations;
using NinjaTrader.Cbi;
using NinjaTrader.NinjaScript;
using NinjaTrader.NinjaScript.Indicators;
#endregion

namespace NinjaTrader.NinjaScript.Strategies.OranselIndustries
{
    public class VolatilityReversalStrategy : Strategy
    {
        private Bollinger _bb;
        private SMA _volAvg;

        protected override void OnStateChange()
        {
            if (State == State.SetDefaults)
            {
                Description = @"Educational outside-band mean-reversion strategy prototype.";
                Name = "VolatilityReversalStrategy";
                Calculate = Calculate.OnBarClose;
                EntriesPerDirection = 1;
                EntryHandling = EntryHandling.AllEntries;
                IsExitOnSessionCloseStrategy = true;
                ExitOnSessionCloseSeconds = 30;
                IsFillLimitOnTouch = false;
                MaximumBarsLookBack = MaximumBarsLookBack.TwoHundredFiftySix;
                OrderFillResolution = OrderFillResolution.Standard;
                Slippage = 0;
                StartBehavior = StartBehavior.WaitUntilFlat;
                TimeInForce = TimeInForce.Gtc;
                TraceOrders = false;
                RealtimeErrorHandling = RealtimeErrorHandling.StopCancelClose;
                StopTargetHandling = StopTargetHandling.PerEntryExecution;
                BarsRequiredToTrade = 20;
                IsInstantiatedOnEachOptimizationIteration = true;

                BBPeriod = 20;
                StdDev = 2.0;
                VolPeriod = 10;
                VolMultiplier = 1.5;
                StopLossTicks = 40;
            }
            else if (State == State.Configure)
            {
                SetStopLoss(CalculationMode.Ticks, StopLossTicks);
            }
            else if (State == State.DataLoaded)
            {
                _bb = Bollinger(StdDev, BBPeriod);
                _volAvg = SMA(Volume, VolPeriod);
                AddChartIndicator(_bb);
            }
        }

        protected override void OnBarUpdate()
        {
            if (CurrentBar < Math.Max(BBPeriod, VolPeriod))
                return;

            bool closesAboveBand = Close[0] > _bb.Upper[0];
            bool closesBelowBand = Close[0] < _bb.Lower[0];
            bool hasThresholdVolume = Volume[0] > _volAvg[0] * VolMultiplier;

            if (Position.MarketPosition == MarketPosition.Flat)
            {
                if (closesAboveBand && hasThresholdVolume)
                    EnterShort(Convert.ToInt32(DefaultQuantity), "MeanRevShort");
                else if (closesBelowBand && hasThresholdVolume)
                    EnterLong(Convert.ToInt32(DefaultQuantity), "MeanRevLong");
            }

            // These are close-confirmed exit requests, not intrabar touches.
            if (Position.MarketPosition == MarketPosition.Long
                && Close[0] >= _bb.Middle[0])
            {
                ExitLong("ExitLongAtMean", "MeanRevLong");
            }
            else if (Position.MarketPosition == MarketPosition.Short
                && Close[0] <= _bb.Middle[0])
            {
                ExitShort("ExitShortAtMean", "MeanRevShort");
            }
        }

        #region Properties
        [NinjaScriptProperty]
        [Range(1, int.MaxValue)]
        [Display(Name = "BB Period", Description = "Bollinger Band lookback", Order = 1, GroupName = "Signal")]
        public int BBPeriod { get; set; }

        [NinjaScriptProperty]
        [Range(0.1, double.MaxValue)]
        [Display(Name = "Standard Deviation", Description = "Band multiplier", Order = 2, GroupName = "Signal")]
        public double StdDev { get; set; }

        [NinjaScriptProperty]
        [Range(1, int.MaxValue)]
        [Display(Name = "Volume Period", Description = "Volume SMA lookback", Order = 3, GroupName = "Signal")]
        public int VolPeriod { get; set; }

        [NinjaScriptProperty]
        [Range(0.1, double.MaxValue)]
        [Display(Name = "Volume Multiplier", Description = "Volume threshold multiplier", Order = 4, GroupName = "Signal")]
        public double VolMultiplier { get; set; }

        [NinjaScriptProperty]
        [Range(1, int.MaxValue)]
        [Display(Name = "Stop Loss (Ticks)", Description = "Stop offset in ticks", Order = 1, GroupName = "Risk")]
        public int StopLossTicks { get; set; }
        #endregion
    }
}

Complete default assumption ledger

AssumptionDefaultConsequence
Signal evaluationOn bar closeNo unfinished-bar entry signal; intrabar path is unavailable
Bollinger settings20 bars, 2.0 standard deviationsInstrument- and bar-series-dependent threshold
Volume settings10 bars, 1.5 multiplierCurrent volume is compared with an SMA that includes the current bar
Position policyOne entry per direction; enter only while flatNo scaling or simultaneous long and short positions
Entry requestManaged market entryFill time and price are not guaranteed
Mean exitClose crosses current middle bandNo intrabar touch handling and no fixed target order
Stop40 ticksOffset is instrument dependent and realized loss can exceed it
Fill modelStandard resolutionHistorical intrabar sequence is simplified
Slippage0 in the sourceCosts are understated until the researcher changes this setting
CommissionNot set by the scriptMust be configured in the Strategy Analyzer or account template
Session closeExit 30 seconds before closeDepends on the selected trading-hours template
Time in forceGTCWorking-order behavior depends on connection and venue
StartupWait until flat; 20 bars requiredExisting positions are not adopted by this example

The zero-slippage setting is visible so the prototype's baseline is reproducible, not because zero is realistic. Before analysis, set instrument-specific slippage and commission assumptions and inspect how the chosen historical fill resolution treats entries, stops, and session-close exits.

Regime-filter hypothesis

Outside-band fades can behave differently during directional regimes. One testable extension is to block new entries when ADX(14) >= 25. The period 14 and threshold 25 are illustrative, unvalidated choices and the code above does not implement them. A useful comparison reports the base strategy and filtered strategy over the same data, including how many trades the filter removes. Other thresholds should be treated as multiple tested hypotheses, not searched until a favorable result appears.

Validation and the reality gap

A historical report should state instrument, contract roll method where relevant, bar type, bar interval, trading-hours template, sample dates, bid/ask or trade-data availability, fill resolution, slippage, commissions, and whether optimization occurred. Report trade count, exposure, drawdown, adverse and favorable excursion, turnover, and sensitivity to neighboring parameter values. A result based on one market or one selected period is not evidence of generalization.

Use chronological in-sample and out-of-sample segments, then reserve a final untouched evaluation period. Walk-forward analysis can organize repeated estimation and evaluation windows, but it does not remove data leakage, selection bias, regime change, or live execution risk. Paper trading can reveal operational defects while still differing from live queues and fills.

Scope and limitations

This prototype has no portfolio limits, daily loss limit, spread or liquidity check, stale-data detection, reconnect reconciliation, order-rejection recovery beyond the configured platform behavior, telemetry, or independent risk process. It evaluates only the primary bar series. Those omissions matter before any live deployment.

The code's purpose is narrower: make the entry threshold, close-based mean exit, and stop offset explicit enough to test. A later system can add observability and operational controls only after the signal and execution assumptions have been evaluated independently.