The Ensemble Predictor

🦉 AthenaAI is an AI-powered ensemble machine-learning trading bot for PocketOption, built on BinaryOptionsToolsV2Get it on GitLab
💬 Need help? Join the Chipa Discord · ⚡ Go no-code with ChipaEditor

EnsemblePredictor (in src/core/models.py) is the brain of the bot. Every polling cycle it receives the current feature vector from the feature engine and returns two things: a direction (CALL or PUT) and a confidence (a probability). Every downstream gate — minimum/maximum confidence, signal confirmation, the adaptive strategy — operates on this output.

Rather than betting on a single algorithm, AthenaAI votes across five scikit-learn models (plus an optional sixth from offline training):

ModelTypeLearning modeNotes
SGDSGDClassifier(loss="modified_huber", penalty="l2", alpha=1e-4)Online (partial_fit)modified_huber loss gives calibrated predict_proba output
PASGDClassifier(loss="hinge", penalty=None, learning_rate="pa1", eta0=1.0)Online (partial_fit)Passive-Aggressive variant; hinge loss means hard 0/1 votes (no predict_proba)
NBGaussianNB()Online (partial_fit)Fast probabilistic baseline
GBMGradientBoostingClassifier(n_estimators=200, max_depth=4, learning_rate=0.1, subsample=0.8)Batch (fit)Trained on the full accumulated dataset
RFRandomForestClassifier(n_estimators=200, max_depth=8, n_jobs=-1)Batch (fit)Trained on the full accumulated dataset
MLP (optional)GPU-trained neural networkBatchOnly present if the brain file was produced by train.py — see below

All models share a single StandardScaler that is fitted once and then updated incrementally with partial_fit.

Online vs batch models

The two groups solve different problems:

  • Online models (SGD, PA, NB) adapt in real time. After every completed trade the bot buffers the trade’s feature vector and label, and every retrain_every samples (default 10, see src/config.py) calls partial_fit(), which trains the online models on the buffered batch and clears the buffer. This lets the ensemble track short-term shifts in market behavior.
  • Batch models (GBM, RF) cannot learn incrementally. They are trained once via train_batch_models() on all accumulated samples (a separate _batch_X/_batch_y store that is never cleared by partial_fit), and require at least 100 samples before training runs at all. During live trading, src/bot.py periodically retrains them on a sliding window of the most recent 500 samples (triggered when the batch store reaches 200+ samples and its length is a multiple of 100) to prevent stale models from poisoning the vote.

partial_fit buffering

add_sample(features, label) appends the sample to both the online buffer and the batch store. Labels are binary: 1 means “CALL would have won”, 0 means “PUT would have won”. A trade that was predicted CALL and lost is labeled 0 — the outcome, not the prediction, is what is stored.

partial_fit() then:

  1. Stacks the buffer into a matrix.
  2. Dimension-change check — if the number of features no longer matches scaler.n_features_in_ (e.g. the Feature Lab or a code change altered the vector length), the entire predictor is reset via self.__init__() and a warning is logged: Feature dimension changed (X → Y) — resetting. All learned state is discarded; this is deliberate, because scaler and model weights are meaningless for a different feature layout.
  3. Fits the scaler (first time) or partial_fits it (thereafter), then trains each online model.
  4. Accuracy tracking — for each online model it computes accuracy on the just-trained batch and updates an exponential moving average:
m["accuracy_ema"] = 0.9 * m["accuracy_ema"] + 0.1 * acc

Each model starts at accuracy_ema = 0.5. The 0.9/0.1 split means roughly the last 10 batches dominate the weight — a model that goes cold loses influence within a few retrains, without one bad batch destroying it. The update is skipped on the very first fit (there is no meaningful pre-training accuracy).

  1. Clears the online buffer and logs Models updated | acc EMAs: {...}.

Voting and the batch ×0.75 demotion

predict(features) scales the input and collects a probability-of-CALL from every model, weighted by its accuracy_ema:

  • Online models vote with weight accuracy_ema.
  • Batch models vote with weight accuracy_ema * 0.75.

The 0.75 multiplier is a v6 change (the code comment records it was previously 2.0×). Batch models report near-perfect training-set accuracy — GBM and RF measure their accuracy_ema on the same data they were fitted on — so under the old scheme they dominated the vote while producing overfit, sometimes anti-correlated confidence in live trading. Demoting them to 0.75× keeps their pattern knowledge in the mix without letting them outvote the adaptive online learners.

Models without predict_proba (PA with hinge loss) contribute a hard 1.0 or 0.0. Any model that throws during prediction contributes a neutral 0.5.

The final probability is the weighted average:

p = weighted_call / (total_weight + 1e-10)

The [0.22, 0.78] probability clamp

Before converting to a direction, p is clamped:

p = min(p, 0.78)
p = max(p, 0.22)

Per the v6 code comment, live data showed that signals above 78% confidence were overfit noise — they won at roughly 50%, no better than a coin flip. The clamp means the reported confidence can never exceed 78% (p >= 0.5 → CALL with confidence p; otherwise PUT with confidence 1 - p, so PUT confidence also caps at 0.78). This works together with BotConfig.max_confidence (default 0.85) in the signal gate pipeline; with the clamp in place the max-confidence gate rarely fires unless you lower it below 0.78.

Rule-based fallback predictor

If scikit-learn is not installed, the models are not yet fitted, the scaler’s dimension does not match the incoming feature vector, or scaling throws, predict falls back to _fallback_predict — a simple three-indicator score using raw feature values:

SignalConditionScore
RSI zone (index 14)RSI < 30 / RSI > 70+0.3 / −0.3
MACD histogram (index 12)> 0 / < 0+0.2 / −0.2
SMA cross (index 10)> 0 / < 0+0.15 / −0.15

Confidence is 0.5 + min(|score|, 0.45); positive score → CALL, otherwise PUT. Note the fallback reads features[14] for RSI where index 14 is actually rsi_zone in the feature layout (raw RSI is index 13) — it still works because the comparisons against 30/70 treat the −1/0/+1 zone value as “never overbought/oversold”, making the fallback effectively MACD + SMA driven.

Persistence and the optional MLP

save_brain(path) pickles the scaler, all online and batch models, and their accuracy EMAs (default path athena_po_brain.pkl, configurable via PO_BRAIN_PATH). load_brain(path) restores them and rebuilds the batch-model list from whatever the file contains — so a brain produced by the standalone trainer (see Training guide) can carry an extra GPU-trained MLP that then votes as an additional batch model at the same 0.75× weight.

The bot auto-saves the brain every 10 completed trades and on shutdown (src/bot.py), alongside the expiry selector’s <brain>_expiry.pkl companion file. See Persistence.

Tuning

  • retrain_every (default 10) controls how often online models update — see retrain-every.
  • min_confidence / max_confidence gate the ensemble’s output — see PO_MIN_CONF and PO_MAX_CONF.
  • The 0.75× demotion and [0.22, 0.78] clamp are hard-coded in src/core/models.py.

Risk note: a higher walk-forward win rate on historical data does not guarantee future results; binary options trading can lose your entire stake on each trade.

See also

💡 Want to discuss how AthenaAI makes decisions — or design your own logic like this without writing code? Join the Chipa Discord and give ChipaEditor a try.