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;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 orderThe 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 showpointer. Ahoverevent (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 entrySignatures:
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): booleanOrder 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) // -> trueState 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--> staleTerminal 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 primitivesTradeHost 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 labelPositionMarker
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 TPFakeBroker
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_priceOpenAlgoTradeFeed 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.