Skip to content
EliteChart

Paper trading

An in-memory Broker that simulates fills against the live mid — no backend, useful for demos.

A Broker implementation that lives entirely in memory, simulates fills at the latest tick, and prints the resulting equity curve. Useful for demos, training, and integration tests.

Quick example

code
import type { Broker, BrokerEvent, OrderRequest, OrderResult } from '@elitechart/elitechart';

export function createPaperBroker(): Broker {
  let cash = 10_000;
  let positions = new Map<string, { qty: number; entry: number }>();
  let listeners: Array<(e: BrokerEvent) => void> = [];

  const emit = (e: BrokerEvent) => listeners.forEach((l) => l(e));
  const account = () => ({
    id: 'paper', currency: 'USD',
    balance: cash, equity: cash, margin: 0, freeMargin: cash, marginLevel: null,
  });

  return {
    async getAccount() { return account(); },
    async getPositions() {
      return [...positions.entries()].map(([symbol, p]) => ({
        id: symbol, symbol, side: p.qty > 0 ? 'long' : 'short',
        quantity: Math.abs(p.qty), entryPrice: p.entry,
        stopLoss: null, takeProfit: null,
        unrealizedPnl: 0, openedAt: Date.now(),
      } as const));
    },
    async getOrders() { return []; },
    async placeOrder(req: OrderRequest): Promise<OrderResult> {
      const fill = req.limit ?? req.market ?? 0;
      const cost = fill * req.quantity;
      cash -= cost;
      const existing = positions.get(req.symbol) ?? { qty: 0, entry: 0 };
      const newQty = existing.qty + (req.side === 'long' ? req.quantity : -req.quantity);
      positions.set(req.symbol, { qty: newQty, entry: fill });
      emit({ kind: 'order:fill', order: { id: `o-${Date.now()}`, symbol: req.symbol, side: req.side, quantity: req.quantity, price: fill, status: 'filled' } as never });
      emit({ kind: 'account', account: account() });
      return { id: `o-${Date.now()}` };
    },
    async modifyOrder() { /* no-op */ },
    async cancelOrder() { /* no-op */ },
    async closePosition(id: string) { positions.delete(id); emit({ kind: 'position:close', id }); },
    subscribe(onEvent) {
      listeners.push(onEvent);
      return () => { listeners = listeners.filter((l) => l !== onEvent); };
    },
  };
}

How it works

The broker keeps two state slabs — cash and positions — and emits the same BrokerEvent union your real broker would. Every placeOrder debits cash, mutates the position, and fires a order:fill followed by account. The chart's Trading Panel reflects the change immediately.

For more realistic fills, plug in the latest mid price from your Datafeed.subscribe callback and use it as the simulated fill price.

Variations

Add slippage

code
const fill = (req.market ?? 0) * (1 + (req.side === 'long' ? 0.0005 : -0.0005));

Persist between reloads

Wrap cash and positions in useChartStore.notes JSON or your own localStorage key.

API

The full Broker interface is in concepts/broker-contract.