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)
| Parameter | Type | Meaning |
|---|---|---|
cfg | BotConfig | All 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).
- Banner β logs version/asset/expiry options/model list/confidence band/hours/feature counts.
- Brain load β reads
PO_RETRAINdirectly from the environment (not fromBotConfig). If"1", logsπ Force retrain requested β ignoring saved brain.and deletescfg.brain_pathif it exists. Otherwise calls_load_brain(). - If the brain loaded, dataset training is skipped. Otherwise:
_reload_from_journal()then, ifcfg.dataset_pathis set,_load_dataset(). Both are wrapped in try/except that logs and continues. - Connect β
PocketOptionAsync(ssid=cfg.ssid),await asyncio.sleep(3)for the websocket handshake, then logsConnected! Balance: $X.XX. An invalid SSID typically surfaces here as an exception from the client. - 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). Callsclient.get_candles(asset, timeframe, effective_warmup * timeframe)β the third argument is seconds of history, not a candle count. Each raw candle goes through_parse_candleinto the deque. - Sets
_running = Trueandawait asyncio.gather(...)on the three loops.start()returns only when all three finish (normally after a fatal stream error orstop()).
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:
- Warmup:
len(candles) < max(warmup_candles, 60). - Max concurrent trades:
len(pending_trades) >= max_concurrent_trades. - Wait between trades:
now β _last_trade_time < min_wait_between_trades. - Daily loss limit:
money_mgr.can_trade()isFalse. - Cooldown:
now < _cooldown_until. - Trading hours: current UTC hour not in
cfg.trading_hours(skipped if the tuple is empty). - Adaptive extra cooldown:
adaptive.get_extra_cooldown()seconds since the last trade. - Features:
features_engine.compute(candles, feature_window);Noneβ skip. Result is passed throughnp.nan_to_num. - Regime:
regime_detector.detect(candles, regime_window); skip ifVOLATILEandskip_volatile_regime. - Prediction:
ensemble.predict(features)β(direction, confidence). - Confidence floor:
confidence < min_confidenceβ skip and clear_signal_history. - Confidence cap (v6):
confidence > max_confidenceβ skip as likely-overfit noise, clear_signal_history. - Low-range candle (v6): last candleβs
high β low < 0.00005(~0.5 pips on EURUSD, hard-coded) β skip. - Indicator alignment (if
require_indicator_alignment):_check_indicator_alignment()must pass. - Signal confirmation:
_check_signal_ready()must pass (see below); on pass,_signal_historyis cleared. - Consecutive-loss cooldown: if
perf.consec_losses >= max_consec_losses, set_cooldown_until = now + cooldown_seconds, log a warning, reset the streak, skip. - 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.
- 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 + 2seconds. - Calls
await client.check_win(tid). The result may be a dict (readsresultorstatus) or a string; if the parsed value isnβtwin/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 withperf.summary()and the expiry stats. - Online learning (win/loss only, and only if
_featuresis attached): label =_direction_inton a win,1 β _direction_inton a loss;ensemble.add_sample(); everycfg.retrain_everysamples βensemble.partial_fit(). Additionally (v6), whenlen(ensemble._batch_X) >= 200and divisible by 100, the batch buffers are trimmed to the last 500 samples andtrain_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_winare logged at DEBUG and retried; a trade older thanexpiry * 5seconds 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.