The Ensemble Predictor
🦉 AthenaAI is an AI-powered ensemble machine-learning trading bot for PocketOption, built on BinaryOptionsToolsV2 → Get 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):
| Model | Type | Learning mode | Notes |
|---|---|---|---|
| SGD | SGDClassifier(loss="modified_huber", penalty="l2", alpha=1e-4) | Online (partial_fit) | modified_huber loss gives calibrated predict_proba output |
| PA | SGDClassifier(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) |
| NB | GaussianNB() | Online (partial_fit) | Fast probabilistic baseline |
| GBM | GradientBoostingClassifier(n_estimators=200, max_depth=4, learning_rate=0.1, subsample=0.8) | Batch (fit) | Trained on the full accumulated dataset |
| RF | RandomForestClassifier(n_estimators=200, max_depth=8, n_jobs=-1) | Batch (fit) | Trained on the full accumulated dataset |
| MLP (optional) | GPU-trained neural network | Batch | Only 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_everysamples (default 10, seesrc/config.py) callspartial_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_ystore that is never cleared bypartial_fit), and require at least 100 samples before training runs at all. During live trading,src/bot.pyperiodically 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:
- Stacks the buffer into a matrix.
- 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 viaself.__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. - Fits the scaler (first time) or
partial_fits it (thereafter), then trains each online model. - 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).
- 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:
| Signal | Condition | Score |
|---|---|---|
| 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_confidencegate 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
- Feature engine — where the input vector comes from
- Signal gates — what happens to the prediction next
- Feature Lab — why the feature dimension can change
- API reference: src.core.models
💡 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.