DocumentationOrder Engine & Ladder

On-chart trading

The trade tier (openalgo-charts/trade) adds a full trade-management layer: working order lines, a position marker with live P&L, OCO brackets, an order state machine with validation, and a depth-of-market ladder. The OrderEngine write path talks to any broker through a small OrderFeed interface (place / modify / cancel) - the OpenAlgo adapter (OpenAlgoTradeFeed) implements it.

Two trade interfaces exist and are easy to confuse. The trade tier’s OrderFeed (place / modify / cancel, from openalgo-charts/trade) is the broker write path used by OrderEngine, and is what OpenAlgoTradeFeed implements. The base export TradeFeed (placeOrder / modifyOrder / cancelOrder + subscribeOrders / subscribePositions) is a higher-level shape - implement OrderFeed for the engine.

Order & target lines

Order, stop, and target levels are drawn as labelled price lines you can drag. The visual layer is just price lines; here are entry / stop / target levels on a live chart:

const chart = lib.createChart(el);
const bars = lib.generateBars(1700000000, 140, 3600);
chart.addSeries('candlestick').setData(bars);

const last = bars[bars.length - 1].close;
// Broker-style segmented pill groups on the line - [badge][qty][label][x] -
// draggable, with the x segment routing as a cancel/exit click.
chart.addPriceLine({ price: last, color: '#3b82f6', lineWidth: 2, id: 'entry',
extentFromRight: 0.30, badge: 'BUY', qty: 10, leftLabel: 'LIMIT', closeButton: true, cursor: 'ns-resize' });
chart.addPriceLine({ price: last * 0.98, color: '#ef5350', lineWidth: 1, dashed: true, id: 'sl',
extentFromRight: 0.30, badge: 'SL', qty: 10, closeButton: true, cursor: 'ns-resize' });
chart.addPriceLine({ price: last * 1.03, color: '#26a69a', lineWidth: 1, dashed: true, id: 'tp',
extentFromRight: 0.30, badge: 'TP', qty: 10, closeButton: true, cursor: 'ns-resize' });

chart.timeScale.fitContent(bars.length);
return chart;
live
Rendering live chart…
Entry, stop-loss and target as segmented, draggable pill groups - solid badge, boxed qty, and an integrated x to cancel/exit.

Inline Buy/Sell buttons

BuySellButtons is a TradingView-style trade panel drawn inside the plot — a SELL button, a quantity chip, and a BUY button, docked to a corner and fixed while the chart pans/zooms. It draws on the overlay canvas (so it lands in screenshots) and routes clicks through chart.subscribeClick.

import { BuySellButtons } from 'openalgo-charts';
 
const panel = new BuySellButtons({ id: 'trade', position: 'top-left', qty: 1 });
chart.addPrimitive(panel);
 
panel.setMark(ltp);      // one price on both buttons (no bid/ask)
// panel.setPrices(bid, ask);  // or a spread
panel.setQty(lots);      // centre chip
 
chart.subscribeClick((id) => {
  if (id === 'trade:buy')  eng.placeMarket(symbol, 'BUY', qty);
  if (id === 'trade:sell') eng.placeMarket(symbol, 'SELL', qty);
  if (id === 'trade:qty')  focusQtyInput();   // your qty control
});

Options: position (top-left default, any corner or center), margin, qty, buyColor / sellColor (default to the theme), buyLabel / sellLabel, and showPrices. Update at runtime with setPrices / setMark, setQty, and setColors.

The order engine

OrderEngine is the logic layer - it validates, routes, and tracks orders, with an arm/confirm gate and analyzer (sandbox) mode:

import { OrderEngine } from 'openalgo-charts/trade';
 
const eng = new OrderEngine({
  feed,
  constraints: { tickSize: 0.05, priceBand, freezeQty },
  armed: false,
  gate: (req) => confirm(`Place ${req.side} ${req.qty}?`),
  mode: 'analyzer',
});
 
await eng.placeMarket('RELIANCE', 'BUY', 10);   // one-click market order

The engine enforces tick-size snapping, price-band / freeze-quantity validation, client-token idempotency, OCO (one fill cancels the peer), and a reconnect → stale transition.

Drag-to-modify

Wire chart drags to the engine. Modify on release, not on every pixel, so you do not spam the broker:

chart.subscribeDrag(
  (id, price) => eng.requestModify(clientId(id), price),  // preview while dragging
  (id, price) => eng.commitModify(clientId(id)),          // commit on release
);

Interaction feedback (hover, drag, ghost)

The chart renders the interaction states itself — no app code needed:

  • Cursor hints. Hovering a draggable line shows ns-resize; the cancel button and DOM-ladder rows show pointer. A hover event (chart.on('hover', ({ id }) => ...)) fires on primitive enter/leave for app-side affordances (tooltips, panels).
  • Hover. A draggable order line thickens and its pill brightens; the cancel × fills solid.
  • Dragging. The grabbed line gets a soft emphasis halo, and a drag ghost — a dimmed line at the pre-drag price — marks where the order was, so the trader always sees what a release will change.
  • Pills. Order lines carry a broker-style segmented group — [badge][qty][label][✕] — a solid colored badge (BUY, SL, TP), boxed qty and info segments, and an integrated cancel . Pending (un-acked) orders draw dimmed; partial fills show progress (3/10). Badge text auto-contrasts with its fill, so custom colors stay legible on any theme.

chart.trading (the data-driven layer) wires the ghost automatically. If you drive PriceLines yourself via chart.subscribeDrag, set it explicitly:

chart.subscribeDrag(
  (id, price) => {
    const line = lines.get(id);
    if (dragFrom.get(id) === undefined) {
      dragFrom.set(id, line.price);
      line.setDragGhost(line.price);   // show the pre-drag reference
    }
    line.setPrice(price);
  },
  (id, price) => {
    lines.get(id).setDragGhost(null);  // clear on release
    dragFrom.delete(id);
    commitToBroker(id, price);
  },
);

Depth-of-market ladder

DomLadder is an IPrimitive that renders a virtualized price ladder with a size heatmap, degrading gracefully when only top-of-book is available. Add it with chart.addPrimitive and push a fresh book snapshot with setDepth on each tick.

import { DomLadder } from 'openalgo-charts/trade';
 
const ladder = new DomLadder({ tickSize: 0.05, groupBy: 1, maxRows: 60 });
chart.addPrimitive(ladder);      // draws as a right-side depth strip
ladder.setDepth(marketDepth);    // call again on every book update
 
// A row hit-tests as `ladder-<side>:<price>` (e.g. 'ladder-bid:100.05').
// Route clicks with subscribeClick to place an order at that price:
chart.subscribeClick((id) => {
  if (!id.startsWith('ladder-')) return;
  const [side, price] = id.slice('ladder-'.length).split(':');
  eng.placeLimit(sym, side === 'bid' ? 'buy' : 'sell', qty, Number(price));
});

Ladder helpers

The three pure functions and the options constant are exported for custom rendering, aggregation, or testing without instantiating DomLadder.

import {
  ladderCapability, buildRows, visibleRows,
  DEFAULT_DOM_LADDER_OPTIONS,
} from 'openalgo-charts/trade';
import type { LadderTier, LadderRow, DomLadderOptions } from 'openalgo-charts/trade';
 
// Detect what the live payload supports.
const tier: LadderTier = ladderCapability(depth); // -> 'none' | 'compact' | 'deep'
 
// Merge bids + asks into price rows sorted high -> low price.
// groupBy > 1 buckets every N ticks into one row for deep books.
const rows: LadderRow[] = buildRows(depth, 0.05, /* groupBy */ 1);
 
// Keep only the rows visible inside the plot, capped to maxRows nearest centre.
const subset = visibleRows(rows, priceToY, plotHeight, rowHeight, maxRows);

DEFAULT_DOM_LADDER_OPTIONS is { tickSize: 0.05, width: 96, groupBy: 1, maxRows: 60, rowHeight: 14 }.

P&L and risk math

Five pure functions over Position data, called on every LTP tick.

import {
  unrealizedPnl, unrealizedPnlPercent, breakeven,
  riskReward, bracketValid,
} from 'openalgo-charts/trade';
import type { Position } from 'openalgo-charts/trade';
 
const pos: Position = { symbol: 'RELIANCE', netQty: 10, avgPrice: 2900 };
 
unrealizedPnl(pos, 2950)        // -> 500    (ltp - avgPrice) * netQty
unrealizedPnlPercent(pos, 2950) // -> 1.72   signed % of entry notional
breakeven(pos, 0.5)             // -> 2900.5  avgPrice + dir * chargesPerUnit
                                //            chargesPerUnit defaults to 0
 
riskReward(2900, 2850, 3000)    // -> 2      reward / risk
riskReward(2900, 2900, 3000)    // -> null   risk is zero
 
bracketValid('BUY', 2900, 2850, 3000)  // -> true   SL < entry < TP
bracketValid('BUY', 2900, 2950, 3000)  // -> false  SL is above entry

Signatures:

function unrealizedPnl(position: Position, ltp: number): number
function unrealizedPnlPercent(position: Position, ltp: number): number
function breakeven(position: Position, chargesPerUnit?: number): number
function riskReward(entry: number, stop: number, target: number): number | null
function bracketValid(side: 'BUY' | 'SELL', entry: number, stop: number, target: number): boolean

Order status guard

isWorking returns true for pending | working | partial and false for terminal states. It is the canonical filter for “still live in the book”.

import { isWorking } from 'openalgo-charts/trade';
 
const active = orders.filter(isWorking);

Order state machine

The client-side state machine tracks each order’s lifecycle without optimistic guesses. It is pure and stateless: pass the current ClientOrderState and an OrderEvent, receive the next state. transition returns the same state if the event is not valid for the current state, so spurious events are ignored.

import { transition, canTransition, isTerminal } from 'openalgo-charts/trade';
import type { ClientOrderState, OrderEvent } from 'openalgo-charts/trade';
 
let state: ClientOrderState = 'pending_place';
 
canTransition(state, 'ack')        // -> true
state = transition(state, 'ack')  // -> 'working'
 
state = transition(state, 'submitCancel') // -> 'cancel_pending'
state = transition(state, 'cancelled')    // -> 'cancelled'
isTerminal(state)                          // -> true

State flow:

pending_place  --ack----------> working
               --reject-------> rejected

working        --partialFill--> partial
               --fill---------> filled
               --submitModify-> modify_pending --ack/reject--> working
               --submitCancel-> cancel_pending --cancelled---> cancelled
                                               --reject------> working

any non-terminal --reconnectAbsent--> stale

Terminal states (filled, cancelled, rejected, stale) accept no further events.

Validation

validateOrder runs entirely client-side before the order reaches the broker. It snaps the price to the nearest tick, checks the price band, and enforces the exchange freeze-quantity limit.

import { validateOrder, withinPriceBand } from 'openalgo-charts/trade';
import type { OrderConstraints, PriceBand, ValidationResult } from 'openalgo-charts/trade';
 
const band: PriceBand = { lower: 2700, upper: 3100 };
const c: OrderConstraints = { tickSize: 0.05, priceBand: band, freezeQty: 1800 };
 
withinPriceBand(2950, band) // -> true
withinPriceBand(3200, band) // -> false
 
const r: ValidationResult = validateOrder(2950.03, 100, c);
// r.ok    -> true
// r.price -> 2950.05  (snapped to nearest tick)
 
validateOrder(3200, 100, c)
// { ok: false, reason: 'price 3200 outside band 2700-3100' }
 
validateOrder(2950, 2000, c)
// { ok: false, reason: 'quantity 2000 exceeds freeze limit 1800' }

ValidationResult always has .ok. When false it also has .reason; when true it also has .price (the tick-snapped value).

TradeController and TradeHost

TradeController reconciles order and position snapshots into on-chart primitives. Call reconcile on every book update or reconnect; it is idempotent and correctly handles new, changed, and vanished orders.

import { TradeController } from 'openalgo-charts/trade';
import type { TradeHost } from 'openalgo-charts/trade';
 
// chart must implement addPrimitive / removePrimitive.
const controller = new TradeController(chart);
 
controller.reconcile(orders, positions); // call after every snapshot
controller.onLtp('RELIANCE', 2950);      // call on every LTP tick
 
// Introspection helpers for tests:
controller.orderLineCount() // -> number of active WorkingOrderLine primitives
controller.positionCount()  // -> number of active PositionMarker primitives
controller.bracketCount()   // -> number of active BracketGroup primitives

TradeHost is a two-method interface; any object with addPrimitive and removePrimitive qualifies. IPrimitive is exported from the base package:

// import type { IPrimitive } from 'openalgo-charts';
interface TradeHost {
  addPrimitive(p: IPrimitive): void;
  removePrimitive(p: IPrimitive): void;
}

Low-level visual primitives

WorkingOrderLine, PositionMarker, and BracketGroup are the canvas primitives TradeController manages internally. Use them directly only when building a custom controller or a headless rendering pipeline.

WorkingOrderLine

A horizontal dashed line at the order price, coloured by side (theme buy / sell), with a segmented pill group — [SIDE][qty][TYPE price ±LTP-distance][✕] — and a compact price tag on the axis. The hit-test returns order:<id> with an ns-resize cursor within 4 px of the line (or anywhere on the pill group), and order:<id>::close with a pointer cursor on the ✕ segment.

import { WorkingOrderLine } from 'openalgo-charts/trade';
 
const line = new WorkingOrderLine(order);
chart.addPrimitive(line);
 
line.update(updatedOrder); // call after a new book snapshot
line.setLtp(2950);         // refresh the distance-to-LTP label

PositionMarker

A solid line at the average entry price with a shaded P&L band to LTP and a label showing direction, net quantity, P&L amount, and P&L percent, coloured by profit/loss sign. Hit-test returns position:<symbol>.

import { PositionMarker } from 'openalgo-charts/trade';
 
const marker = new PositionMarker(position);
chart.addPrimitive(marker);
 
marker.update(updatedPosition);
marker.setLtp(2950);

BracketGroup

SL and TP dashed lines with shaded risk (red) and reward (green) zones drawn behind the candles (zOrder: 'bottom'), plus an R:R label near the entry. The hit-tests return bracket-sl:<symbol> and bracket-tp:<symbol> with ns-resize cursors.

import { BracketGroup } from 'openalgo-charts/trade';
import type { BracketState } from 'openalgo-charts/trade';
 
const bracket = new BracketGroup({
  symbol: 'RELIANCE', side: 'BUY',
  entry: 2900, stop: 2850, target: 3000,
});
chart.addPrimitive(bracket);
 
bracket.update({ ...state, target: 3050 }); // adjust TP

FakeBroker

FakeBroker is a deterministic in-memory broker that implements OrderFeed. Use it to exercise OrderEngine and TradeController without a network connection.

import { FakeBroker, OrderEngine, TradeController } from 'openalgo-charts/trade';
 
const broker = new FakeBroker();
const engine = new OrderEngine({ feed: broker, constraints: { tickSize: 0.05 } });
const controller = new TradeController(chart);
 
broker.onBook((orders, positions) => controller.reconcile(orders, positions));
broker.onLtp((symbol, ltp) => controller.onLtp(symbol, ltp));
 
// Seed an initial snapshot:
broker.setBook(
  [{ id: 'O1', symbol: 'RELIANCE', side: 'BUY', type: 'LIMIT',
     qty: 10, filledQty: 0, price: 2900, status: 'working' }],
  [{ symbol: 'RELIANCE', netQty: 10, avgPrice: 2900 }],
);
 
broker.emitLtp('RELIANCE', 2950); // drives P&L display
 
broker.fill('O1');                 // simulate a fill; removes order and notifies
broker.rejectNextPlace = 'outside price band'; // next place() will throw
 
// Static helper -- generate a synthetic depth book around an LTP:
const depth = FakeBroker.makeDepth(2950, 5, 0.05); // 5 levels, 0.05 tick
broker.emitDepth('RELIANCE', depth);

Live order and position feed

OpenAlgoTradeFeed, mapOrder, and mapPosition are exported from the base package (openalgo-charts), not from the trade tier. OpenAlgoTradeConfig is documented in Types.

import { OpenAlgoTradeFeed, mapOrder, mapPosition } from 'openalgo-charts';
import { OrderEngine } from 'openalgo-charts/trade';
 
const feed = new OpenAlgoTradeFeed({
  baseUrl: 'http://127.0.0.1:5000',
  apiKey: 'your-api-key',
  strategy: 'my-strategy',  // groups orders in OpenAlgo; default 'openalgo-charts'
  defaultProduct: 'MIS',    // 'CNC' | 'NRML' | 'MIS'
});
 
const engine = new OrderEngine({ feed, constraints: { tickSize: 0.05 } });
 
// Fetch and reconcile the current book on connect or reconnect:
const [orders, positions] = await Promise.all([
  feed.getOrders(),
  feed.getPositions(),
]);
controller.reconcile(orders, positions);

mapOrder and mapPosition convert raw OpenAlgo REST payloads to Order and Position. They are used internally by getOrders and getPositions; import them when you need to map raw rows from your own fetch calls:

const order    = mapOrder(rawRow);    // coerces string fields, maps order_status
const position = mapPosition(rawRow); // maps quantity (signed net) + average_price

OpenAlgoTradeFeed caches each order’s full context on place() and getOrders() so that a later modify() can supply the complete payload that OpenAlgo’s modify endpoint requires.

⚠️

The trade path is broker-live. Test against OpenAlgo analyzer mode first, and verify the exact endpoints/fields against your running OpenAlgo build before trading real money. See Live Data.