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

IndexNameFormula / descriptionRange / typical
0bodyclose[-1] - open[-1] β€” signed body of the current candleΒ± a few pips (Β±0.0005)
1rangehigh[-1] - low[-1] β€” current candle’s full rangeβ‰₯ 0, typically 0.0001–0.001
2mom_1close[-1] - close[-2] β€” 1-bar momentumΒ± a few pips
3mom_5close[-1] - close[-5] (0 if fewer than 5 closes)Β± tens of pips

Returns

rets = diff(closes) / closes[:-1] (per-bar fractional returns).

IndexNameFormula / descriptionRange / typical
4ret_lastLatest return rets[-1]β‰ˆ Β±0.001 on 1-min FX
5ret_meanmean(rets[-window:]) β€” mean return over the windowβ‰ˆ Β±0.0002
6volatilitystd(rets[-window:]) β€” return standard deviationβ‰₯ 0, ~0.0001–0.001 (also read by the expiry selector)

Moving averages and MACD

IndexNameFormula / descriptionRange / typical
7price_sma5close[-1] - SMA(5)Β± pips
8price_sma10close[-1] - SMA(10)Β± pips
9price_sma20close[-1] - SMA(window)Β± pips
10ma_crossSMA(5) - SMA(20) β€” fast/slow crossover; sign = short-term trend. Used by the indicator-alignment gate and the fallback predictorΒ± pips
11macd_lineEMA(12) - EMA(26) (scalar EMAs of closes)Β± pips
12macd_histMACD histogram: macd_line_array[-1] - EMA(macd_line_array, 9). Used by the indicator-alignment gateΒ± fractions of a pip

Oscillators

IndexNameFormula / descriptionRange / typical
13rsi14-period RSI (simple-mean gains/losses over the last 15 closes); 50.0 if insufficient data, 100.0 if zero losses0–100
14rsi_zone+1.0 if RSI > 70, βˆ’1.0 if RSI < 30, else 0.0{βˆ’1, 0, +1}
15bb_widthBollinger width (upper βˆ’ lower) / mid with mid = SMA(window), bands at Β±2 std of closesβ‰₯ 0, ~0.0005–0.005
16bb_posPosition within bands (close βˆ’ lower) / (upper βˆ’ lower)~0–1 (can slightly overshoot)
17atr14-period ATR (mean true range)β‰₯ 0, ~0.0001–0.001 (read by expiry selector)
18range_vs_atr(high[-1] βˆ’ low[-1]) / atr β€” is the current candle unusually large?β‰₯ 0, ~0.5–3
19stoch_kStochastic %K over 14 periods: 100Β·(close βˆ’ low_min)/(high_max βˆ’ low_min)0–100
20stoch_d%D β€” mean of the last 3 %K values0–100
21stoch_diff%K βˆ’ %Dβ‰ˆ Β±100, usually Β±20
22cci20-period Commodity Channel Index on typical price (H+L+C)/3, scaled by 0.015 Β· MADunbounded, usually Β±200
23williams_r14-period Williams %R: βˆ’100Β·(HH βˆ’ close)/(HH βˆ’ LL)βˆ’100 to 0
24adxSimplified ADX: DX from smoothed +DM/βˆ’DM over ATR β€” a single DX reading, not a smoothed ADX average0–100 (read by expiry selector)

Volume

If all volumes are zero (common for FX quote feeds), these default to 1.0 and 0.0.

IndexNameFormula / descriptionRange / typical
25rel_volumevol[-1] / mean(vol[-window:])β‰₯ 0, ~1.0
26vol_stdstd(vol[-window:]) / mean(vol[-window:]) (coefficient of variation)β‰₯ 0

Candle patterns

IndexNameFormula / descriptionRange / typical
27doji1.0 if body/range < 0.1 (indecision candle), else 0.0{0, 1}
28hammer+1.0 hammer (lower wick > 2Γ— body, small upper wick); βˆ’1.0 inverted hammer; else 0.0{βˆ’1, 0, +1}
29engulfing+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:].

IndexNameFormula / descriptionRange / typical
30ret_p2525th percentile of windowed returns≀ 0 usually
31ret_p7575th percentile of windowed returnsβ‰₯ 0 usually
32skewSample skewness mean(((rβˆ’ΞΌ)/Οƒ)Β³)unbounded, usually Β±2
33kurtExcess kurtosis mean(((rβˆ’ΞΌ)/Οƒ)⁴) βˆ’ 3unbounded, usually βˆ’2 to +10
34fractal_dimHiguchi 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

IndexNameFormula / descriptionRange / typical
35streakSigned count of consecutive up (+) or down (βˆ’) closes ending at the latest candle, capped at Β±10, terminated by a flat closeβˆ’10 to +10
36time_sin_hsin(2Ο€ Β· hour/24) with fractional hour, UTCβˆ’1 to 1
37time_cos_hcos(2Ο€ Β· hour/24)βˆ’1 to 1
38time_sin_dsin(2Ο€ Β· weekday/7) (Monday = 0)βˆ’1 to 1
39time_cos_dcos(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

IndexNameFormula / descriptionRange / typical
40shooting_star1.0 if upper wick > 2Γ— body and lower wick < body (and body > 0), else 0.0{0, 1}
41bearish_engulfing1.0 if previous candle bullish, current bearish, and current body engulfs previous (open[-1] > close[-2] and close[-1] < open[-2]){0, 1}
55candle_wick_ratio(range βˆ’ body) / body β€” total wick length relative to bodyβ‰₯ 0, large for dojis
56body_vs_avgcurrent `body

Long trend

IndexNameFormula / descriptionRange / typical
42sma50_distclose[-1] βˆ’ SMA(50); 0.0 if fewer than 50 closesΒ± tens of pips
43sma50_above+1.0 if close above SMA(50), else βˆ’1.0; 0.0 if insufficient data{βˆ’1, 0, +1}
44momentum_ratioSMA(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:

IndexNameFormula / descriptionRange / typical
45fib_dist_236(price βˆ’ (high βˆ’ 0.236Β·range)) / range~βˆ’1 to +1
46fib_dist_382(price βˆ’ (high βˆ’ 0.382Β·range)) / range~βˆ’1 to +1
47fib_dist_500(price βˆ’ (high βˆ’ 0.500Β·range)) / range~βˆ’1 to +1
48fib_dist_618(price βˆ’ (high βˆ’ 0.618Β·range)) / range~βˆ’1 to +1

Support / resistance and ratios

IndexNameFormula / descriptionRange / typical
49support_dist(price βˆ’ min(lows[-window:])) / support β€” fractional distance above windowed lowβ‰₯ 0, tiny (~0.0005)
50resist_dist(max(highs[-window:]) βˆ’ price) / price β€” fractional distance below windowed highβ‰₯ 0, tiny
51hl_ratiohigh[-1] / low[-1]~1.0 + a few Γ—10⁻⁴
52oc_ratioopen[-1] / close[-1]~1.0 Β± a few Γ—10⁻⁴

Divergence and volume spike

IndexNameFormula / descriptionRange / typical
53rsi_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}
54volume_spikevol[-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

πŸ’‘ 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.