Skip to main content

Brain

Trait Brain 

Source
pub trait Brain:
    Send
    + Sync
    + 'static {
    // Required methods
    fn name(&self) -> &str;
    fn on_event<'life0, 'life1, 'life2, 'async_trait>(
        &'life0 self,
        event: &'life1 MarketDataEvent,
        position: &'life2 Position,
    ) -> Pin<Box<dyn Future<Output = Result<Decision>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait,
             'life2: 'async_trait;

    // Provided methods
    fn owned_symbols(&self) -> Option<Vec<Symbol>> { ... }
    fn on_fill<'life0, 'life1, 'async_trait>(
        &'life0 self,
        _fill: &'life1 Fill,
    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait { ... }
    fn on_position_change<'life0, 'life1, 'life2, 'async_trait>(
        &'life0 self,
        _symbol: &'life1 Symbol,
        _position: &'life2 Position,
    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait,
             'life2: 'async_trait { ... }
    fn health<'life0, 'async_trait>(
        &'life0 self,
    ) -> Pin<Box<dyn Future<Output = BrainHealth> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait { ... }
}
Expand description

The strategic layer of a trading bot.

Implementors receive market events and the current position state, and return a decision on each event. See the module-level docs for the design rationale.

§Threading & mutability

Methods take &self so implementors can be shared across tasks via Arc. Use interior mutability (Mutex, RwLock, atomics) for any state that needs to be updated across calls. This mirrors the pattern in rustrade-supervisor::TradingService.

§Object safety

Brain is object-safe. You can store brains as Box<dyn Brain> or Arc<dyn Brain> and swap between implementations at runtime.

§Example

A minimal brain that goes long when the close is above a fixed threshold and flat otherwise. Note the Mutex<State> pattern for any cross-call state.

use std::sync::Mutex;
use async_trait::async_trait;
use rustrade_core::{
    Brain, BrainHealth, Decision, MarketDataEvent, Position, Result,
};

struct ThresholdBrain {
    threshold: f64,
    state: Mutex<usize>, // events seen
}

#[async_trait]
impl Brain for ThresholdBrain {
    fn name(&self) -> &str { "threshold" }

    async fn on_event(
        &self,
        event: &MarketDataEvent,
        position: &Position,
    ) -> Result<Decision> {
        *self.state.lock().unwrap() += 1;
        let close = match event {
            MarketDataEvent::Candle { candle, .. } => candle.close,
            _ => return Ok(Decision::hold()),
        };
        if close > self.threshold && position.qty <= 0.0 {
            Ok(Decision::buy(1.0))
        } else if close <= self.threshold && position.qty > 0.0 {
            Ok(Decision::close())
        } else {
            Ok(Decision::hold())
        }
    }

    async fn health(&self) -> BrainHealth { BrainHealth::ok() }
}

Required Methods§

Source

fn name(&self) -> &str

Human-readable identifier used in logs and metrics.

Source

fn on_event<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, event: &'life1 MarketDataEvent, position: &'life2 Position, ) -> Pin<Box<dyn Future<Output = Result<Decision>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Core decision point — called on every market event for any symbol this brain cares about.

position is the exchange-reported position for the event’s symbol at the time this call is made. May be Position::FLAT.

Return Decision::hold for “do nothing” — this is always safe. For any recoverable problem (stale data, transient compute error), return Err rather than panicking: the framework logs the error and keeps the service running.

§Panics

Treat a panic here as a hard bug, never as control flow. Each brain runs in its own supervised task, so under panic = "unwind" a panic is contained to that task and sibling brains keep running. But a release build compiled with panic = "abort" (as this workspace does) will abort the entire process on any panic — there is no isolation in that configuration. Return Err for anything you want to survive.

Provided Methods§

Source

fn owned_symbols(&self) -> Option<Vec<Symbol>>

Symbols this brain exclusively owns, or None to see every symbol.

This is the multi-brain arbitration contract:

  • None (default): the brain receives events for all configured symbols and is responsible for its own filtering. Multiple None brains may run together — the framework does not guard against them acting on the same symbol, so they must coordinate themselves.
  • Some(symbols): the framework routes only those symbols’ events to this brain (others are skipped before on_event), and rejects at startup any configuration where two brains claim the same symbol — preventing two strategies from fighting over one position. A brain that owns its symbols needn’t filter internally.

Must be cheap and deterministic — it’s read once at startup for the overlap check and cached per execution loop.

Source

fn on_fill<'life0, 'life1, 'async_trait>( &'life0 self, _fill: &'life1 Fill, ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Called after the exchange confirms a fill. Informational only — returning an error does not unwind the fill.

Default implementation is a no-op.

Source

fn on_position_change<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _symbol: &'life1 Symbol, _position: &'life2 Position, ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Called whenever the exchange reports a position change from any source (our fills, external actions, liquidations, funding). Informational only.

Default implementation is a no-op.

Source

fn health<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = BrainHealth> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Report current brain health for the supervisor’s /health endpoint.

Default implementation returns “healthy” — override to surface indicator warm-up state, model staleness, memory pressure, etc.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§