The Feature Engine
π¦ 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
FeatureEngine (in src/core/feature_engine.py) converts a window of candles into the numeric feature vector that feeds the ensemble predictor, the expiry selector, and the indicator-alignment gate in src/bot.py. The vector always has 57 slots: 40 core features (indices 0β39, always active) followed by 17 experimental features (indices 40β56, individually maskable by the Feature Lab).
compute(candles, window=20) takes the botβs candle history (up to BotConfig.lookback, default 200 candles) and the rolling window (BotConfig.feature_window, default 20). All indicators are implemented in-house as static helpers on the class β there is no TA-lib dependency.
The β₯26-candle minimum
if len(candles) < max(window, 26):
return None
compute returns None until at least max(window, 26) candles exist β 26 because the MACDβs slow EMA needs 26 closes. The trade loop treats a None result as a skip (log line βΈ Feature compute returned None). Separately, src/bot.py enforces an effective warmup of at least 60 candles before trading at all, since experimental features such as sma50_dist need 50 candles to produce non-zero values (features that lack enough history return neutral defaults rather than failing).
NaN handling
The engine itself guards divisions with + 1e-10 epsilons, but callers additionally sanitize the output before use:
features = np.nan_to_num(features, nan=0.0, posinf=0.0, neginf=0.0)
This happens both in the live trade loop and in dataset pre-training (src/bot.py), so a pathological candle window can never inject NaN/Inf into the models.
Core features (indices 0β39)
Prices below are raw quote prices (for EURUSD, roughly 1.0β1.2), so absolute-price-difference features are on the order of 0.0001 (one pip = 0.0001).
Price action
| Index | Name | Formula / description | Range / typical |
|---|---|---|---|
| 0 | body | close[-1] - open[-1] β signed body of the current candle | Β± a few pips (Β±0.0005) |
| 1 | range | high[-1] - low[-1] β current candleβs full range | β₯ 0, typically 0.0001β0.001 |
| 2 | mom_1 | close[-1] - close[-2] β 1-bar momentum | Β± a few pips |
| 3 | mom_5 | close[-1] - close[-5] (0 if fewer than 5 closes) | Β± tens of pips |
Returns
rets = diff(closes) / closes[:-1] (per-bar fractional returns).
| Index | Name | Formula / description | Range / typical |
|---|---|---|---|
| 4 | ret_last | Latest return rets[-1] | β Β±0.001 on 1-min FX |
| 5 | ret_mean | mean(rets[-window:]) β mean return over the window | β Β±0.0002 |
| 6 | volatility | std(rets[-window:]) β return standard deviation | β₯ 0, ~0.0001β0.001 (also read by the expiry selector) |
Moving averages and MACD
| Index | Name | Formula / description | Range / typical |
|---|---|---|---|
| 7 | price_sma5 | close[-1] - SMA(5) | Β± pips |
| 8 | price_sma10 | close[-1] - SMA(10) | Β± pips |
| 9 | price_sma20 | close[-1] - SMA(window) | Β± pips |
| 10 | ma_cross | SMA(5) - SMA(20) β fast/slow crossover; sign = short-term trend. Used by the indicator-alignment gate and the fallback predictor | Β± pips |
| 11 | macd_line | EMA(12) - EMA(26) (scalar EMAs of closes) | Β± pips |
| 12 | macd_hist | MACD histogram: macd_line_array[-1] - EMA(macd_line_array, 9). Used by the indicator-alignment gate | Β± fractions of a pip |
Oscillators
| Index | Name | Formula / description | Range / typical |
|---|---|---|---|
| 13 | rsi | 14-period RSI (simple-mean gains/losses over the last 15 closes); 50.0 if insufficient data, 100.0 if zero losses | 0β100 |
| 14 | rsi_zone | +1.0 if RSI > 70, β1.0 if RSI < 30, else 0.0 | {β1, 0, +1} |
| 15 | bb_width | Bollinger width (upper β lower) / mid with mid = SMA(window), bands at Β±2 std of closes | β₯ 0, ~0.0005β0.005 |
| 16 | bb_pos | Position within bands (close β lower) / (upper β lower) | ~0β1 (can slightly overshoot) |
| 17 | atr | 14-period ATR (mean true range) | β₯ 0, ~0.0001β0.001 (read by expiry selector) |
| 18 | range_vs_atr | (high[-1] β low[-1]) / atr β is the current candle unusually large? | β₯ 0, ~0.5β3 |
| 19 | stoch_k | Stochastic %K over 14 periods: 100Β·(close β low_min)/(high_max β low_min) | 0β100 |
| 20 | stoch_d | %D β mean of the last 3 %K values | 0β100 |
| 21 | stoch_diff | %K β %D | β Β±100, usually Β±20 |
| 22 | cci | 20-period Commodity Channel Index on typical price (H+L+C)/3, scaled by 0.015 Β· MAD | unbounded, usually Β±200 |
| 23 | williams_r | 14-period Williams %R: β100Β·(HH β close)/(HH β LL) | β100 to 0 |
| 24 | adx | Simplified ADX: DX from smoothed +DM/βDM over ATR β a single DX reading, not a smoothed ADX average | 0β100 (read by expiry selector) |
Volume
If all volumes are zero (common for FX quote feeds), these default to 1.0 and 0.0.
| Index | Name | Formula / description | Range / typical |
|---|---|---|---|
| 25 | rel_volume | vol[-1] / mean(vol[-window:]) | β₯ 0, ~1.0 |
| 26 | vol_std | std(vol[-window:]) / mean(vol[-window:]) (coefficient of variation) | β₯ 0 |
Candle patterns
| Index | Name | Formula / description | Range / typical |
|---|---|---|---|
| 27 | doji | 1.0 if body/range < 0.1 (indecision candle), else 0.0 | {0, 1} |
| 28 | hammer | +1.0 hammer (lower wick > 2Γ body, small upper wick); β1.0 inverted hammer; else 0.0 | {β1, 0, +1} |
| 29 | engulfing | +1.0 bullish engulfing (bear candle followed by larger bull body); β1.0 bearish engulfing; else 0.0 | {β1, 0, +1} |
Higher-order statistics
Computed on rets[-window:].
| Index | Name | Formula / description | Range / typical |
|---|---|---|---|
| 30 | ret_p25 | 25th percentile of windowed returns | β€ 0 usually |
| 31 | ret_p75 | 75th percentile of windowed returns | β₯ 0 usually |
| 32 | skew | Sample skewness mean(((rβΞΌ)/Ο)Β³) | unbounded, usually Β±2 |
| 33 | kurt | Excess kurtosis mean(((rβΞΌ)/Ο)β΄) β 3 | unbounded, usually β2 to +10 |
| 34 | fractal_dim | Higuchi fractal dimension of the last window closes (kmax=5, log-log slope); 1.5 fallback when the series is too short or degenerate | ~1.0 (trending) to ~2.0 (noisy) |
Streak and time
| Index | Name | Formula / description | Range / typical |
|---|---|---|---|
| 35 | streak | Signed count of consecutive up (+) or down (β) closes ending at the latest candle, capped at Β±10, terminated by a flat close | β10 to +10 |
| 36 | time_sin_h | sin(2Ο Β· hour/24) with fractional hour, UTC | β1 to 1 |
| 37 | time_cos_h | cos(2Ο Β· hour/24) | β1 to 1 |
| 38 | time_sin_d | sin(2Ο Β· weekday/7) (Monday = 0) | β1 to 1 |
| 39 | time_cos_d | cos(2Ο Β· weekday/7) | β1 to 1 |
The sine/cosine pairs encode time cyclically so 23:59 and 00:01 are numerically adjacent.
Experimental features (indices 40β56)
These sit behind the experimental_enabled master switch (default True; when off, all 17 slots are zero-filled to keep the vector length stable) and are the only features the Feature Lab may mask.
Candlestick anatomy
| Index | Name | Formula / description | Range / typical |
|---|---|---|---|
| 40 | shooting_star | 1.0 if upper wick > 2Γ body and lower wick < body (and body > 0), else 0.0 | {0, 1} |
| 41 | bearish_engulfing | 1.0 if previous candle bullish, current bearish, and current body engulfs previous (open[-1] > close[-2] and close[-1] < open[-2]) | {0, 1} |
| 55 | candle_wick_ratio | (range β body) / body β total wick length relative to body | β₯ 0, large for dojis |
| 56 | body_vs_avg | current ` | body |
Long trend
| Index | Name | Formula / description | Range / typical |
|---|---|---|---|
| 42 | sma50_dist | close[-1] β SMA(50); 0.0 if fewer than 50 closes | Β± tens of pips |
| 43 | sma50_above | +1.0 if close above SMA(50), else β1.0; 0.0 if insufficient data | {β1, 0, +1} |
| 44 | momentum_ratio | SMA(14) / SMA(28); 1.0 if fewer than 28 closes | ~1.0 Β± 0.001 |
Fibonacci retracement distances
Over the last 50 candlesβ high/low range (all 0.0 if fewer than 50 candles). Each is the signed distance of the current price from the retracement level, normalized by the range:
| Index | Name | Formula / description | Range / typical |
|---|---|---|---|
| 45 | fib_dist_236 | (price β (high β 0.236Β·range)) / range | ~β1 to +1 |
| 46 | fib_dist_382 | (price β (high β 0.382Β·range)) / range | ~β1 to +1 |
| 47 | fib_dist_500 | (price β (high β 0.500Β·range)) / range | ~β1 to +1 |
| 48 | fib_dist_618 | (price β (high β 0.618Β·range)) / range | ~β1 to +1 |
Support / resistance and ratios
| Index | Name | Formula / description | Range / typical |
|---|---|---|---|
| 49 | support_dist | (price β min(lows[-window:])) / support β fractional distance above windowed low | β₯ 0, tiny (~0.0005) |
| 50 | resist_dist | (max(highs[-window:]) β price) / price β fractional distance below windowed high | β₯ 0, tiny |
| 51 | hl_ratio | high[-1] / low[-1] | ~1.0 + a few Γ10β»β΄ |
| 52 | oc_ratio | open[-1] / close[-1] | ~1.0 Β± a few Γ10β»β΄ |
Divergence and volume spike
| Index | Name | Formula / description | Range / typical |
|---|---|---|---|
| 53 | rsi_divergence | +1.0 if price rose over 14 bars while RSI fell by more than 5 points (bearish divergence); β1.0 for the mirror case; else 0.0. Requires β₯28 closes (recomputes RSI at each of the last 14 bars) | {β1, 0, +1} |
| 54 | volume_spike | vol[-1] / mean(vol[-window:]); 1.0 when volume data is absent β numerically the same as rel_volume (index 25) | β₯ 0, ~1.0 |
The mask mechanism
The engine holds a feature_mask β a length-57 float array of 1.0s at construction. Just before returning, compute multiplies element-wise:
result = np.array(feats, dtype=np.float64)
if len(result) == len(self.feature_mask):
result *= self.feature_mask
return result
A masked feature (mask value 0.0) is therefore zeroed, not removed β the vector length never changes, so the ensembleβs scaler and models keep their dimensions and no reset is triggered. Only the Feature Lab writes to the mask, and only for experimental indices (40+); core features are always active. The mask is in-memory only β it resets to all-ones on restart and is rebuilt as the Feature Lab accumulates trade outcomes again.
See also
- Feature Lab β how experimental features get masked
- Ensemble predictor β the consumer of this vector
- Regime detection β a separate, simpler computation on the same candles
- feature-window config field, lookback
- API reference: src.core.feature-engine
π‘ 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.