Skip to main content

Architecture Overview

OctoBot is a modular, async crypto trading bot. The codebase is a Python monorepo under packages/, built with Pants. Every strategy, evaluator, exchange connector, and trading mode lives as a tentacle — a plugin that sits on top of the framework without modifying it. This separation is the central design decision: the core packages define contracts; tentacles fulfill them.

Package layers

The stack has six layers. Each package depends only on packages in the layers below it.

commons and async_channel are the foundation — neither has any internal dependency. Commons provides configuration, databases, DSL interpreter, and shared utilities. Async_channel is the typed message bus: channels connect producers to consumers through async queues. Synchronized mode removes async tasks and drives execution deterministically, which is what makes backtesting possible.

tentacles_manager, backtesting, and trading_backend form the next layer. Tentacles_manager handles plugin discovery, installation, and configuration. Backtesting provides the time-driven simulation engine. Trading_backend handles exchange-specific broker ID injection and API key permission validation.

trading and evaluators are the core framework. Trading owns exchange managers, order lifecycle, portfolio accounting, and the trading mode abstraction. Evaluators own the Matrix (the in-memory signal tree) and the factory that instantiates evaluator classes across symbol/time-frame combinations.

services sits above trading and backtesting. It integrates external interfaces — web dashboard, Telegram, notification dispatch, AI model backends.

agents, flow, and sync are higher-level packages. Agents orchestrates LLM-powered teams through services. Flow is the stateless automation runner that uses trading for exchange operations. Sync provides multi-instance coordination over HTTPS.

node depends on flow and wraps it in a durable scheduler with crash recovery.

tentacles sits at the top alongside the CLI. It contains no framework code — only concrete implementations that subclass what the framework defines.

The tentacle plugin system

A tentacle is a directory in the tentacles/ tree with a Python module, a metadata.json descriptor, and an optional config/ subdirectory. The descriptor names the Python classes it exports and declares a minimum compatible version. tentacles_manager discovers these directories at startup by scanning for metadata.json files up to three levels deep — no registry, no explicit registration call.

The __init__.py files throughout the tentacles/ tree are generated by tentacles_manager, not written by hand. Each calls check_tentacle_version() on import: if the tentacle's declared version is below the minimum, the import is silently skipped. A broken tentacle cannot crash OctoBot at startup.

This boundary matters because it keeps strategy code out of the core. A new trading mode, evaluator, or exchange connector is a new directory in tentacles/, not a patch to octobot_trading or octobot_evaluators. The framework packages stay stable across very different trading strategies.

Configuration follows the same separation. Every tentacle class has a reference config inside its own config/ directory — the factory default, never modified at runtime — and an optional profile-specific copy written to the active profile's specific_config/ folder. At runtime, the profile copy wins if present; otherwise the reference is used.

The async channel backbone

All runtime data flow is channel-based. A Channel subclass names a data type and declares its producer and consumer classes. Producers enqueue; consumers dequeue and call a registered callback. Consumers can be filtered — a TA evaluator subscribes to EvaluatorsChannel filtered to its specific symbol and time frame, so it only receives relevant triggers.

Channels start paused and resume automatically when a consumer with a non-optional priority level registers. This prevents upstream processing when nothing meaningful is listening.

MatrixChannel is the most consequential channel: every time an evaluator finishes and calls evaluation_completed(), it publishes to MatrixChannel. Strategy evaluators and trading mode producers both subscribe here to be notified when new signal data is available.

The evaluation to trading pipeline

 ╔══════════════════════════════════════════════════════════════╗
║ Exchange (WebSocket / REST) ║
╚════════════════════════════╤═════════════════════════════════╝

┌──────────▼──────────┐
│ ExchangeManager │
│ candles · tickers │
│ order book · fees │
└──────────┬──────────┘
│ EvaluatorsChannel
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌───────────┐ ┌─────────────┐ ┌───────────┐
│ TA │ │ RealTime │ │ Social │
│ Evaluator │ │ Evaluator │ │ Evaluator │
└─────┬─────┘ └──────┬──────┘ └─────┬─────┘
│ eval_note │ eval_note │ eval_note
└───────────────┐ │ ┌───────────────┘
▼ ▼ ▼
┌──────────────────┐
│ Matrix │
│ (signal tree) │
└────────┬─────────┘
│ MatrixChannel
┌────────▼─────────┐
│ Strategy │
│ Evaluator │
└────────┬─────────┘
│ TradingModeChannel
┌───────────┴───────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Producer │ │ Consumer │
│ what/when │─────►│ how/execute │
│ to trade │ │ on exchange │
└──────────────┘ └──────┬───────┘

┌──────▼───────┐
│ Orders │
│ Portfolio │
└──────────────┘

Exchange data arrives from REST polling or WebSocket and populates per-time-frame circular buffers (three thousand candles each). On each closed candle, EvaluatorsChannel triggers the relevant TA evaluators. Each evaluator writes its eval_note (a float in [-1, 1]) to the Matrix and broadcasts on MatrixChannel. A StrategyEvaluator subscribes to MatrixChannel and aggregates signals from all evaluators for its configured time frames — but only after verifying that every contributing TA evaluator's Matrix timestamp is fresh enough relative to exchange time. The strategy posts its own note, which trading mode producers pick up to decide what orders to create.

The producer/consumer split inside a trading mode is deliberate: the producer decides what to trade based on signals; the consumer decides how to execute it against the exchange. Multiple trading modes can share one exchange and operate against isolated sub-portfolios.

Configuration and profiles

Configuration has two layers merged at runtime:

  • config/config.json — exchange credentials and per-installation settings. Never travels with a profile.
  • user/profiles/<name>/profile.json — everything that defines a strategy: active tentacles, evaluator parameters, trading mode config. Safe to share or commit.

The active profile also contains tentacles_config.json (which tentacle classes are active) and specific_config/ (per-class parameter overrides). Profiles can be marked auto_update to poll an origin URL on a configurable interval, which is how managed strategy updates are distributed. A profile update triggers a graceful bot restart.

update_config_fields applies dot-path updates in-place without reloading from disk, so the web UI can save small changes without unnecessary churn.

Deployment modes

Standalone bot — the default. octobot/octobot.py starts four producers (ExchangeProducer, EvaluatorProducer, ServiceFeedProducer, InterfaceProducer) on a single asyncio loop. Everything described in the pipeline section runs here.

Node (master/consumer)octobot_node is a standalone FastAPI service backed by DBOS, a workflow engine that persists every step to SQLite or PostgreSQL. A node accepts automation tasks over its REST/WebSocket API and hands them to octobot_flow for execution. An instance can act as master (schedules tasks), consumer (executes them), or both. Multi-node deployments share a PostgreSQL database; SQLite is single-node only. Task payloads support end-to-end encryption via a hybrid RSA/AES-GCM/ECDSA scheme with directional key separation.

Serverless flowoctobot_flow is a stateless execution engine for individual automations. An AutomationState object is passed in, a DAG of DSL actions runs, and the updated state is returned. No memory is retained between invocations, which makes it safe to run as a serverless function. The flow engine is what the node invokes per task iteration.

Syncoctobot_sync lets multiple OctoBot instances share configurations, signals, and account data over HTTPS. Every request is authenticated via an EIP-191 EVM wallet signature. Access control is data-driven through on-chain ownership resolution. Primary servers use S3-compatible object storage; replica servers mirror a subset locally.