src.bot

πŸ¦‰ 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

Source: src/bot.py. AITradingBot is the orchestrator: it wires every other component together, connects to PocketOption via BinaryOptionsToolsV2.pocketoption.PocketOptionAsync, and runs three concurrent asyncio loops β€” candle streaming, trade decisions, and result checking. If BinaryOptionsToolsV2 is not installed, the module prints ERROR: BinaryOptionsToolsV2 not installed. and calls sys.exit(1) at import time.

Lifecycle overview

AITradingBot(cfg)                       # construct components
  └─ await start()
       β”œβ”€ banner logging
       β”œβ”€ PO_RETRAIN=1 ? delete brain : _load_brain()
       β”œβ”€ if no brain: _reload_from_journal() β†’ _load_dataset(cfg.dataset_path)
       β”œβ”€ connect PocketOptionAsync(ssid), sleep 3s, log balance
       β”œβ”€ fetch warmup candles (get_candles)
       └─ asyncio.gather(
            _candle_stream(),     # loop 1: keep self.candles fresh
            _trade_loop(),        # loop 2: gates β†’ predict β†’ place trades
            _result_checker(),    # loop 3: resolve trades β†’ learn
          )
await stop()                            # _running=False, _save_brain(), final stats log

The three loops share state through self.candles, self.pending_trades, and the component objects; because they run on one event loop there is no locking.

__init__(self, cfg: BotConfig)

ParameterTypeMeaning
cfgBotConfigAll settings

Creates: candles β€” deque(maxlen=cfg.lookback) of Candle; EnsemblePredictor; FeatureEngine; FeatureLab (review_interval=50); RegimeDetector; MoneyManager; TradeJournal (cfg.db_path β€” opens/creates the SQLite file immediately); PerformanceTracker; AdaptiveStrategy (review_interval=25, min_samples=15); ExpirySelector (cfg.expiry_options). Plus internals: pending_trades: dict[str, TradeRecord], _cooldown_until, _samples_since_fit, _running, _signal_history: list[(candle_ts, Direction, confidence)], _last_trade_time.

async start(self)

Main entry point (blocking until the loops die).

  1. Banner β€” logs version/asset/expiry options/model list/confidence band/hours/feature counts.
  2. Brain load β€” reads PO_RETRAIN directly from the environment (not from BotConfig). If "1", logs πŸ”„ Force retrain requested β€” ignoring saved brain. and deletes cfg.brain_path if it exists. Otherwise calls _load_brain().
  3. If the brain loaded, dataset training is skipped. Otherwise: _reload_from_journal() then, if cfg.dataset_path is set, _load_dataset(). Both are wrapped in try/except that logs and continues.
  4. Connect β€” PocketOptionAsync(ssid=cfg.ssid), await asyncio.sleep(3) for the websocket handshake, then logs Connected! Balance: $X.XX. An invalid SSID typically surfaces here as an exception from the client.
  5. Warmup candles β€” enforces effective_warmup = max(cfg.warmup_candles, 60) (features need β‰₯ 50 candles for SMA50; a warning is logged if the config was lower). Calls client.get_candles(asset, timeframe, effective_warmup * timeframe) β€” the third argument is seconds of history, not a candle count. Each raw candle goes through _parse_candle into the deque.
  6. Sets _running = True and await asyncio.gather(...) on the three loops. start() returns only when all three finish (normally after a fatal stream error or stop()).

Loop 1 β€” async _candle_stream(self)

Keeps self.candles current. Prefers client.subscribe_symbol_time_aligned(asset, timedelta(seconds=timeframe)), which delivers completed candles on interval boundaries β€” a v6 fix so features are computed on closed candles rather than noisy in-progress ticks. Falls back to subscribe_symbol(asset, timedelta(...)); if neither method exists, logs an error and sets _running = False (killing the other loops on their next check).

For each streamed candle: parse with _parse_candle; append if its timestamp is newer than the last candle; if the timestamp equals the last one, replace the last candle (same candle updating). Any exception logs Candle stream error: <e> and stops the bot (_running = False) β€” there is no automatic reconnect.

Loop 2 β€” async _trade_loop(self)

The decision loop. Sleeps 2 s at start, then iterates every cfg.poll_interval seconds (default 2.0). A diagnostic β€œwhy am I not trading” line (⏸ <reason>) is logged at most every 30 s. Gates run in this exact order; any failure continues:

  1. Warmup: len(candles) < max(warmup_candles, 60).
  2. Max concurrent trades: len(pending_trades) >= max_concurrent_trades.
  3. Wait between trades: now βˆ’ _last_trade_time < min_wait_between_trades.
  4. Daily loss limit: money_mgr.can_trade() is False.
  5. Cooldown: now < _cooldown_until.
  6. Trading hours: current UTC hour not in cfg.trading_hours (skipped if the tuple is empty).
  7. Adaptive extra cooldown: adaptive.get_extra_cooldown() seconds since the last trade.
  8. Features: features_engine.compute(candles, feature_window); None β†’ skip. Result is passed through np.nan_to_num.
  9. Regime: regime_detector.detect(candles, regime_window); skip if VOLATILE and skip_volatile_regime.
  10. Prediction: ensemble.predict(features) β†’ (direction, confidence).
  11. Confidence floor: confidence < min_confidence β†’ skip and clear _signal_history.
  12. Confidence cap (v6): confidence > max_confidence β†’ skip as likely-overfit noise, clear _signal_history.
  13. Low-range candle (v6): last candle’s high βˆ’ low < 0.00005 (~0.5 pips on EURUSD, hard-coded) β†’ skip.
  14. Indicator alignment (if require_indicator_alignment): _check_indicator_alignment() must pass.
  15. Signal confirmation: _check_signal_ready() must pass (see below); on pass, _signal_history is cleared.
  16. Consecutive-loss cooldown: if perf.consec_losses >= max_consec_losses, set _cooldown_until = now + cooldown_seconds, log a warning, reset the streak, skip.
  17. Multi-timeframe confirmation (v6): with β‰₯ 100 candles, samples every 5th candle to a pseudo-5-minute series; computes SMA5 and SMA20 of the sampled closes; a CALL against a 5-min downtrend (or PUT against an uptrend) is skipped.
  18. Adaptive gate: adaptive.should_trade(direction, regime, confidence, utc_hour, min_confidence); refusal logs 🧠 Adaptive skip: <reason>.

If everything passes: stake = money_mgr.compute_stake(confidence, perf.win_rate, payout=0.85); chosen_expiry = expiry_selector.select(regime, features, confidence); log the β–Ά TRADE … line; call client.buy(asset, stake, expiry) for CALL or client.sell(...) for PUT (each returns (trade_id, _)).

Side effects after placement: builds a TradeRecord, stores it in pending_trades, journal.save_trade(record) (DB write β€” features not yet attached, so the pending row has features = NULL), updates _last_trade_time, then attaches record._features (ndarray), record._direction_int (1=CALL/0=PUT), and record.features_json.

Any exception in an iteration is logged (Trade loop error: …, with traceback) followed by a 5 s sleep β€” the loop itself survives.

Loop 3 β€” async _result_checker(self)

Every 3 s, scans pending_trades:

  • Skips a trade until elapsed >= rec.expiry + 2 seconds.
  • Calls await client.check_win(tid). The result may be a dict (reads result or status) or a string; if the parsed value isn’t win/loss/draw, it substring-searches the raw text for "win", "loss"/"lose", "draw" β€” otherwise leaves the trade pending for the next pass.
  • Computes profit with hard-coded payout 0.85: win β†’ +stake*0.85, loss β†’ βˆ’stake, draw β†’ 0.0. Updates the record (result, profit, exit_time).
  • Side effects per resolved trade: perf.record(), money_mgr.record(), journal.save_trade(rec) (row overwrite, now with features), adaptive.record_trade(...) (with the entry hour in UTC), expiry_selector.record_result(...), and a βœ…/❌/βž– result log line with perf.summary() and the expiry stats.
  • Online learning (win/loss only, and only if _features is attached): label = _direction_int on a win, 1 βˆ’ _direction_int on a loss; ensemble.add_sample(); every cfg.retrain_every samples β†’ ensemble.partial_fit(). Additionally (v6), when len(ensemble._batch_X) >= 200 and divisible by 100, the batch buffers are trimmed to the last 500 samples and train_batch_models() re-runs, logging πŸ”„ Batch models retrained on recent N samples β€” this keeps GBM/RF from going stale.
  • Every 10 resolved trades (perf.total % 10 == 0): journal.save_snapshot(...) and _save_brain() (auto-persistence).
  • Errors from check_win are logged at DEBUG and retried; a trade older than expiry * 5 seconds is abandoned with a warning and removed.

Private helpers

_load_dataset(self, path: str)

Pre-trains the ensemble from a historical CSV before going live (used only when no saved brain exists). Parses either HistData semicolon format (no headers, YYYYMMDD HHMMSS;O;H;L;C[;V]) or headered CSV (comma or semicolon; time column time/timestamp/date/Date, either YYYY-MM-DD HH:MM:SS or a numeric epoch; OHLC with lower/uppercase names; volume from tick_volume/volume/Volume/real_volume). Malformed rows are silently skipped.

Then: requires β‰₯ 50 candles (else warns and returns); caps at the most recent 500,000; label horizon = max(1, default_expiry / timeframe) candles; for each index builds features from the trailing lookback candles and labels 1 if the close label_horizon candles ahead is higher, 0 if lower, skipping flat closes. Requires β‰₯ 20 samples. Splits 70/30 chronologically: feeds the 70% to the ensemble in 5,000-sample chunks (partial_fit per chunk), trains the batch models, then walk-forward tests on the 30% (if > 100 samples) while continuing online learning during the test, logging the honest win rate against a breakeven computed from a 92% payout (1/(1+0.92) β‰ˆ 52.1%). Finally logs the sample counts and calls _save_brain(). This near-duplicates src.training.dataset/trainer β€” prefer train.py for serious training (it is parallelized and supports the GPU MLP).

_reload_from_journal(self)

Loads completed trades via journal.load_completed_trades(). For each: decodes features_json; skips vectors whose length differs from the first (most-recent-schema) trade’s length; label = direction int if the trade won, inverted if it lost; ensemble.add_sample(). Also feeds FeatureLab when the vector length equals the current 57-dim schema. Then partial_fit(), restores perf.wins/perf.losses, and replays trades into adaptive.record_trade(...). Note: the journal query only returns direction/result/features, so the adaptive replay’s regime/confidence/hour always fall back to "ranging"/0.6/12 (see journal.md). Corrupted entries are skipped silently.

_check_indicator_alignment(self, features, direction) -> bool

2-of-3 vote using features[14] (as RSI), features[12] (MACD histogram), features[10] (SMA cross). For CALL the votes are RSI < 70, MACD hist > 0, SMA cross > 0; for PUT: RSI > 30, MACD hist < 0, SMA cross < 0. Returns True when β‰₯ 2 agree; logs a DEBUG line otherwise. (Index 14 is actually the rsi_zone flag in the current feature layout β€” raw RSI is index 13; the code is documented as-is.)

_check_signal_ready(self, direction, confidence, candle_ts) -> bool

Requires cfg.signal_confirmations consecutive candles to agree on direction. Records at most one signal per unique candle timestamp (a repeat timestamp returns False immediately). History is trimmed to signal_confirmations * 3 entries. Passes when the last N recorded signals all match the current direction; logs βœ… Signal CONFIRMED: … with the average confidence. With the default signal_confirmations = 1 it passes on the first new candle.

_save_brain(self) / _load_brain(self) -> bool

_save_brain: ensemble.save_brain(cfg.brain_path) plus a pickle of expiry_selector.save_state() to brain_path with .pkl β†’ _expiry.pkl (e.g. athena_brain_expiry.pkl). All failures caught and logged as warnings. _load_brain: ensemble.load_brain(), then loads the expiry pickle if present; returns whether the main brain loaded.

_parse_candle(raw) -> Candle (static)

Tolerant parser for the PocketOption feed: dict (keys time/timestamp, open, high, low, close, volume), list/tuple (positional [ts, o, h, l, c, (v)]), or attribute-style object. Missing fields default to 0.

async stop(self)

Sets _running = False (the loops exit on their next iteration), saves the brain and expiry state, and logs Bot stopped. Final stats: <perf.summary()>. Called by main.py on Ctrl+C. It does not close the websocket client or the SQLite connection explicitly.

Usage

import asyncio
from src.config import BotConfig
from src.bot import AITradingBot

bot = AITradingBot(BotConfig(ssid="your-session-id"))
try:
    asyncio.run(bot.start())
except KeyboardInterrupt:
    asyncio.run(bot.stop())

Risk note

This class places real trades. Test with a demo-account SSID and conservative stakes first β€” binary options trading can lose money, and past performance does not guarantee future results.

See also

πŸ’¬ Unsure how this interacts with the rest of the configuration? Ask in the Chipa Discord β€” or prototype strategies no-code with ChipaEditor.