ML Engine v.2.0.0.0: Commitment of Traders Features and Adaptive Contract Quantity Forecasting in NinjaTrader 8

In our previous article we introduced ML Engine — an ensemble machine learning filter that takes the directional signal from your existing indicator and decides whether it is worth taking, based on a probability score and a set of quality metrics.

That first version answered one question: “Is this signal good enough to trade?”

ML Engine v.2.0.0.0 answers a second one: “If I trade it — how many contracts should I trade?”

This release adds two major pieces to the engine:

  1. Commitment of Traders (COT) features in the direction ensemble — giving the classifier visibility into institutional and commercial positioning, not just price and order flow.
  2. A full Quantity Forecast module — a separate regression ensemble that learns from your strategy’s closed trades and recommends an adaptive contract size for every entry.

This article focuses on what’s new. If you haven’t read the first article yet, it’s worth doing so first — it covers the directional ensemble, the feature set (VWAP, cumulative delta, ATR, Slope, MACD), and the probability/metrics filtering logic that v2.0.0.0 builds on top of.

What’s New in v.2.0.0.0

  • Direction ensemble now ingests COT data (Noncommercial Net, Commercial Net, Nonreportable Positions Net) as additional normalized features.
  • A new, independent Quantity Forecast module recommends position size per trade.
  • A second model ensemble dedicated to regression (quantity), separate from the classification ensemble used for direction.
  • New auto-weighting and metrics filter logic for the quantity ensemble (R², MAE), mirroring the AUC/F1 filter already used for direction.
  • New strategy API: GetRecommendedContracts() and UpdateStrategySnapshot().

Important — make sure BaseSignal doesn’t fire on every tick

ML Engine treats every non-zero value it reads from BaseSignal as a signal to evaluate (and, internally, a sample to learn from). If your base indicator’s logic outputs a non-zero value continuously — for example on every tick while a condition stays true, instead of only at the moment the condition first becomes true — ML Engine will end up re-evaluating and re-training on what is effectively the same signal over and over, which can freeze the chart or strategy and stall backtests. Write your base signal so it fires only on the triggering event (a cross, a breakout, a state change) and resets back to 0 afterward, the same way the EMA-cross example in this article uses CrossAbove/CrossBelow rather than a running condition.

// ❌ Bad: stays non-zero for as long as the condition holds true.
// ML Engine sees a "new" signal on every bar/tick this is active,
// and keeps re-evaluating / re-learning on the same trade idea.
BaseSignal[0] = EMA1[0] > EMA2[0] ?  1 :
                EMA1[0] < EMA2[0] ? -1 :
                0;

// ✅ Good: fires only once, at the moment of the actual crossover,
// and falls back to 0 on every other bar.
BaseSignal[0] =
    CrossAbove(EMA1, EMA2, 1) ?  1 :
    CrossBelow(EMA1, EMA2, 1) ? -1 :
    0;

The same rule applies to any indicator you feed in as the directional input — order flow triggers, breakout flags, custom signals — as long as the value returns to 0 once the triggering event has passed, ML Engine evaluates each signal exactly once.

Commitment of Traders Joins the Direction Ensemble

Price, order flow and volatility describe what is happening right now. COT data describes who is positioned, and how heavily, in the underlying futures market — a slower-moving but often telling layer of context.

In v2.0.0.0, ML Engine loads the built-in COT indicator and feeds three normalized series into the direction ensemble alongside VWAP, Slope, Cumulative Delta, MACD, ATR and Gap:

  • Noncommercial Net — large speculators’ net positioning;
  • Commercial Net — hedgers’ net positioning;
  • Nonreportable Positions Net — smaller, non-reporting traders’ net positioning.

These don’t replace any existing feature — they’re simply additional inputs the classifiers can learn to weigh. A signal that aligns with building commercial hedging pressure, for example, may statistically behave differently from the same signal appearing against it, and the ensemble is now able to pick up on that.

To make COT data available, enable it once in NinjaTrader’s global settings:

Control Center → Tools → Settings → Market Data → turn on Download COT data at startup.

If a COT value is missing or invalid for a given instrument or bar, ML Engine simply skips the COT inputs for that bar — direction training and MLSignal inference continue uninterrupted using the remaining features. No configuration is required beyond enabling the download; COT is a global on/off, not a per-feature toggle.

Introducing the Quantity Forecast Module

Filtering a signal solves half the problem. The other half — sizing — was previously left entirely to the strategy itself (a fixed DefaultQuantity, or whatever hand-built logic you wrote around it). v2.0.0.0 puts position sizing under the same adaptive, data-driven approach as signal filtering.

A Separate Ensemble, Built for Regression

Direction is a classification problem (long / short / no-trade), so it uses classifiers. Quantity is a regression problem (a continuous “how many contracts” output), so it uses a dedicated set of regression models:

  • Qty LightGbm
  • Qty FastTree
  • Qty FastForest
  • Qty SDCA
  • Qty FastTree Tweedie — well-suited to count-like, non-negative targets such as contract size;
  • Qty OGD (Online Gradient Descent) — adapts continuously as new trades come in;
  • Qty LBFGS Poisson — another regressor tuned for count data.

Each can be toggled independently, and just like the direction ensemble, the quantity ensemble can auto-weight its models based on their out-of-sample R² — models that explain more of the variance in actual outcomes get more say in the final recommendation.

How the Quantity Model Learns

The quantity model doesn’t train on price data directly — it trains on your strategy’s own trading outcomes. Two inputs drive it:

  • the direction model’s confidence/accuracy at the time of each trade;
  • a snapshot of your strategy’s live trade performance, pushed in after every closed position.

Every time a position returns to flat, you push the current SystemPerformance.AllTrades.TradesPerformance snapshot into the engine. Internally, ML Engine compares consecutive snapshots to build labeled samples — effectively learning the relationship between market/model conditions at entry and how well that trade size actually performed. As more closed trades accumulate, the recommendation shifts from a flat default toward output that reflects what has genuinely worked.

This means the module needs a real trading history to become useful — on a fresh chart or strategy instance, it falls back to your default quantity until enough labeled samples exist (governed by Records Before Retrain, the same setting already used for the direction model).

Controlling Position Sizing

The new settings give you the same kind of guardrails you already have around probability filtering:

  • Min Contracts / Max Contracts — hard floor and ceiling, regardless of what the model recommends.
  • Max Qty Step — caps how much size can change from one recommendation to the next, preventing the model from jumping straight from 1 to 10 contracts on a single new sample.
  • Use Qty Auto Weights — lets the ensemble re-weight regressors by R² automatically (recommended default).
  • Use Qty Metrics Filter, with Min Qty R² and Max Qty MAE — if the quantity ensemble’s accuracy drops below your threshold (or its error rises above it), ML Engine stops adjusting size and quietly falls back to your default quantity until quality recovers.

In other words: the model is allowed to scale you up or down, but never outside the box you define, and never while it’s performing poorly.

Using It From Your Strategy

Two new methods tie the module into your NinjaScript strategy:

At entry, ask the engine for a recommended size instead of hardcoding DefaultQuantity:

int qty = ml != null
    ? ml.GetRecommendedContracts(DefaultQuantity)
    : DefaultQuantity;

EnterLong(qty, "Long");

GetRecommendedContracts() already applies Min/Max Contracts, Max Qty Step and the metrics filter internally — it returns defaultQty whenever the forecast is disabled, filtered out, or not yet trained.

After every closed trade, feed the outcome back in so the model keeps learning:

protected override void OnPositionUpdate(
    Position position,
    double averagePrice,
    int quantity,
    MarketPosition marketPosition)
{
    if (position.MarketPosition == MarketPosition.Flat)
    {
        if (UseQuantityForecast && ml != null)
            ml.UpdateStrategySnapshot(SystemPerformance.AllTrades.TradesPerformance);
    }
}

That’s the whole loop: BaseSignalMLSignal (filtered direction) → GetRecommendedContracts() (adaptive size) → trade closes → UpdateStrategySnapshot() feeds the result back in for the next decision.

Direction Filtering and Quantity Forecasting, Working Together

The two modules are independent — you can use direction filtering alone, quantity forecasting alone, or both together — but they’re designed to reinforce each other. A high-confidence signal from a well-performing direction ensemble is exactly the kind of trade where scaling up makes sense; a marginal signal that barely cleared the probability threshold is exactly the kind where staying small (or sticking to the minimum) is the safer call. With both modules active, ML Engine now expresses that logic automatically, instead of leaving it to a fixed lot size or manual judgment.

Where to Go Next

For the complete, up-to-date property reference and a full step-by-step strategy integration (including data series requirements and the complete MLEngine(...) constructor signature), see the official documentation:

ML Engine documentation →

ML Engine is included in the ASF Pack.

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *