Skip to main content

Broker

Trait Broker 

Source
pub trait Broker: MaybeSend + 'static {
    // Required methods
    fn connect(&self, events: Events<AccountEvent>);
    fn place(&self, order: OrderRequest, reply: Reply<OrderId>);
    fn cancel(&self, id: OrderId, reply: Reply<()>);

    // Provided methods
    fn modify(&self, id: OrderId, order: OrderRequest, reply: Reply<()>) { ... }
    fn preview(&self, order: OrderRequest, reply: Reply<OrderPreview>) { ... }
    fn rules(&self, symbol: &str, reply: Reply<TradingRules>) { ... }
    fn history(&self, request: HistoryRequest, reply: Reply<HistoryPage>) { ... }
}
Expand description

One trading account at the host’s broker, real or simulated. The library holds no balances or positions: it shows what the broker pushes and places the orders the user confirms. Paper trading is an implementation of this trait. One value is one account; several accounts are several values.

Methods are called on the UI thread and must return at once. Talk to the broker elsewhere and answer or push from any thread, runtime or browser task.

Every price and quantity pushed must be finite. Quantities are positive, except a position’s, which is signed, and an order’s filled quantity, which runs from 0 to the requested quantity; the terminal skips events that break this. Prices are in the symbol’s price units and quantities in its trading units.

A paper account that trades one symbol and fills market orders at a fixed price:

use chrono::Utc;
use egui_charts::data::{FeedError, Reply};
use egui_charts::model::{
    Account, Capabilities, Execution, Order, OrderId, OrderRequest, OrderSide, OrderStatus, OrderType, Position,
};
use egui_charts::services::{AccountEvent, Broker, Events};
use std::sync::Mutex;

const SYMBOL: &str = "AAPL";
const PRICE: f64 = 100.0;

#[derive(Default)]
struct Paper {
    events: Mutex<Option<Events<AccountEvent>>>,
    /// Orders placed so far, and the net quantity held.
    state: Mutex<(u64, f64)>,
}

impl Broker for Paper {
    fn connect(&self, events: Events<AccountEvent>) {
        let mut capabilities = Capabilities::default();
        capabilities.market = true;
        events.push(AccountEvent::Account(Account::new("USD", 10_000.0, capabilities)));
        // Market orders fill at once, so no order is working; a position is pushed when one is
        // open.
        let held = self.state.lock().unwrap().1;
        if held != 0.0 {
            events.push(AccountEvent::Position(Position::new(SYMBOL, held, PRICE)));
        }
        *self.events.lock().unwrap() = Some(events);
    }

    fn place(&self, order: OrderRequest, reply: Reply<OrderId>) {
        if order.order_type != OrderType::Market || order.symbol != SYMBOL {
            return reply.send(Err(FeedError::NotSupported));
        }
        let (id, held) = {
            let mut state = self.state.lock().unwrap();
            state.0 += 1;
            state.1 += match order.side {
                OrderSide::Buy => order.quantity,
                OrderSide::Sell => -order.quantity,
            };
            (OrderId(format!("paper-{}", state.0)), state.1)
        };
        reply.send(Ok(id.clone()));

        let Some(events) = self.events.lock().unwrap().clone() else {
            return;
        };
        let execution = Execution::new(id.clone(), Utc::now(), order.side, PRICE, order.quantity);
        let position = Position::new(order.symbol.clone(), held, PRICE);
        let mut filled = Order::new(id, order, OrderStatus::Filled);
        filled.filled = filled.request.quantity;
        events.push(AccountEvent::Order(filled));
        events.push(AccountEvent::Execution(execution));
        events.push(AccountEvent::Position(position));
    }

    fn cancel(&self, id: OrderId, reply: Reply<()>) {
        reply.send(Err(FeedError::Other(format!("order {id} is already filled"))));
    }
}

Required Methods§

Source

fn connect(&self, events: Events<AccountEvent>)

Pushes the account (balance, currency, capabilities) first, then the working orders and open positions, then orders, positions, executions and connection status as they change. A broker that keeps history pushes it after the open state: the finished orders, the balance changes and the journal, oldest first, which the account panel lists. The terminal calls it when it starts and again, with a new stream, after the stream lagged; each stream starts from nothing, so push the whole current state again. The old stream is inactive from then on: push into the newest one only.

The terminal announces, with a notification and the execution sound for fills, a working limit or stop order it has not seen on the stream, a rejected order and each execution, when they are pushed after connect returned while the broker is live. What is pushed before connect returns, or while the broker reports that it is not live, restates the account and is not announced: push the current state then, before returning or between FeedStatus::Connecting and FeedStatus::Live.

A stream starts as connected. While the connection to the broker is not live, push AccountEvent::Status with FeedStatus::Connecting or FeedStatus::Disconnected, on each new stream too, and FeedStatus::Live once it is live again; until then the account panel and the order ticket mark the broker and the terminal reports it as not connected. A broker that never pushes a status counts as connected. Meanwhile the terminal sends no order, cancellation or change: it reports the ones the user confirms as refused, and a position order confirmed earlier that still waits for its trading rules too, and sends none of them later. It asks for rules and history only while the broker is live, and once it is live again it asks again for those that were on their way when it stopped being live, or that failed meanwhile or with a recoverable error.

Source

fn place(&self, order: OrderRequest, reply: Reply<OrderId>)

Sends order to the broker. The terminal calls this only after the user confirmed the order, or the host performed the place order command itself, so do not ask again. Answer with the broker’s id once the order is accepted, or with a FeedError the terminal shows. The order’s AccountEvent::Order may arrive before or after that answer.

Source

fn cancel(&self, id: OrderId, reply: Reply<()>)

Cancels the working order id. The terminal calls this only after the user confirmed, so do not ask again. Answer once the broker accepted the cancellation, or with a FeedError the terminal shows; the order’s OrderStatus::Cancelled update may arrive before or after that answer.

Provided Methods§

Source

fn modify(&self, id: OrderId, order: OrderRequest, reply: Reply<()>)

Replaces the working order id with order, which differs in its prices or brackets, after the user dragged one of its lines, added or removed a take profit or stop loss, and confirmed. Called only for an account whose Capabilities::modify is set. Answer once the broker accepted the change; push the changed order as an AccountEvent::Order. By default every change is refused as FeedError::NotSupported.

Source

fn preview(&self, order: OrderRequest, reply: Reply<OrderPreview>)

Estimates what order would cost without placing it. Called only for an account whose Capabilities::preview is set, when the user asks to preview an order; the terminal places the previewed order only after the user confirms it. By default every preview is refused as FeedError::NotSupported.

Source

fn rules(&self, symbol: &str, reply: Reply<TradingRules>)

Answers how symbol trades: its quantity step, minimum and maximum quantity, point value and times in force. By default the answer is FeedError::NotSupported, and the order ticket assumes TradingRules::new.

Source

fn history(&self, request: HistoryRequest, reply: Reply<HistoryPage>)

Answers one page of finished history older than what connect pushed, for the kind and range of request. By default the answer is FeedError::NotSupported, and only the pushed history exists.

A page holds AccountEvent::Order with a finished status, AccountEvent::Execution, AccountEvent::Balance or AccountEvent::Journal events, as the request’s HistoryKind says, oldest first, and never working orders or positions: those come only from the pushed stream. The first page of a request holds the newest entries of its range and each page after it, asked for with the next cursor, the entries before the previous one. Entries that were pushed live may appear again in a page: an order is matched by its id and other events by value, so repeat them unchanged. Answer for the account connect last served; a consumer drops an answer that arrives after the account changed.

The account panel asks once the account arrived, while the broker is live, and the user scrolls to the end of a history: first for the range that ends one second after the oldest entry it lists (for an order, when it closed), so that entries sharing that time are not skipped, then for each next until it is None or the history lists 1,000 entries. After an error, which it shows unless it is NotSupported, it asks for that history again only after the next connect; after a recoverable error or one that arrives while the broker reports that it is not live, it asks again once the broker reports that it is live again.

Dyn Compatibility§

This trait is dyn compatible.

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

Implementors§