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, Error>> + Send + 'async_trait>>
where 'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Self: '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<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait,
'life1: 'async_trait,
Self: '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<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Self: 'async_trait { ... }
fn health<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = BrainHealth> + Send + 'async_trait>>
where 'life0: 'async_trait,
Self: '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§
Sourcefn on_event<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
event: &'life1 MarketDataEvent,
position: &'life2 Position,
) -> Pin<Box<dyn Future<Output = Result<Decision, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Self: 'async_trait,
fn on_event<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
event: &'life1 MarketDataEvent,
position: &'life2 Position,
) -> Pin<Box<dyn Future<Output = Result<Decision, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Self: '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§
Sourcefn owned_symbols(&self) -> Option<Vec<Symbol>>
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. MultipleNonebrains 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 beforeon_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.
Sourcefn on_fill<'life0, 'life1, 'async_trait>(
&'life0 self,
_fill: &'life1 Fill,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
Self: 'async_trait,
fn on_fill<'life0, 'life1, 'async_trait>(
&'life0 self,
_fill: &'life1 Fill,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
Self: '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.
Sourcefn on_position_change<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
_symbol: &'life1 Symbol,
_position: &'life2 Position,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Self: '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<(), Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Self: '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.
Sourcefn health<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = BrainHealth> + Send + 'async_trait>>where
'life0: 'async_trait,
Self: 'async_trait,
fn health<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = BrainHealth> + Send + 'async_trait>>where
'life0: 'async_trait,
Self: '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".