openalgo-charts 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,680 @@
1
+ /**
2
+ * Trade-layer data model (ARCHITECTURE.md §9). Broker-agnostic shapes the
3
+ * TradeFeed produces; the chart depends only on these, not on OpenAlgo's REST.
4
+ */
5
+ type OrderSide = 'BUY' | 'SELL';
6
+ type OrderType = 'MARKET' | 'LIMIT' | 'SL' | 'SL-M';
7
+ /** Lifecycle states (§9.5). Phase 8 reconciles read-only; Phase 9 drives writes. */
8
+ type OrderStatus = 'pending' | 'working' | 'partial' | 'filled' | 'cancelled' | 'rejected';
9
+ type OrderRole = 'entry' | 'sl' | 'tp';
10
+ interface Order {
11
+ id: string;
12
+ symbol: string;
13
+ side: OrderSide;
14
+ type: OrderType;
15
+ qty: number;
16
+ filledQty: number;
17
+ price: number;
18
+ triggerPrice?: number;
19
+ status: OrderStatus;
20
+ /** Links SL/TP child orders to their position/entry. */
21
+ parentId?: string;
22
+ role?: OrderRole;
23
+ }
24
+ interface Position {
25
+ symbol: string;
26
+ /** Net signed quantity: positive = long, negative = short, 0 = flat. */
27
+ netQty: number;
28
+ avgPrice: number;
29
+ }
30
+ /** A working order is one still live in the book (not terminal). */
31
+ declare function isWorking(o: Order): boolean;
32
+
33
+ /**
34
+ * P&L and risk math (ARCHITECTURE.md §9.1). Pure functions — the hot path on
35
+ * every LTP tick, and the part most worth unit-testing.
36
+ */
37
+
38
+ /** Unrealized P&L for a position at the given last price. */
39
+ declare function unrealizedPnl(position: Position, ltp: number): number;
40
+ /** Unrealized P&L as a percentage of the entry notional. */
41
+ declare function unrealizedPnlPercent(position: Position, ltp: number): number;
42
+ /** Breakeven price (entry ± per-unit charges; charges default 0). */
43
+ declare function breakeven(position: Position, chargesPerUnit?: number): number;
44
+ /** Risk:reward for a bracket relative to entry. Returns null if risk is zero. */
45
+ declare function riskReward(entry: number, stop: number, target: number): number | null;
46
+ /** True if a stop/target is correctly placed for the side (long: SL<entry<TP). */
47
+ declare function bracketValid(side: 'BUY' | 'SELL', entry: number, stop: number, target: number): boolean;
48
+
49
+ interface LogicalRange {
50
+ from: number;
51
+ to: number;
52
+ }
53
+ interface TimeScaleOptions {
54
+ barSpacing: number;
55
+ minBarSpacing: number;
56
+ maxBarSpacing: number;
57
+ /** Empty bars of space kept to the right of the latest bar. */
58
+ rightOffset: number;
59
+ }
60
+ declare class TimeScale {
61
+ private _barSpacing;
62
+ private _rightOffset;
63
+ private readonly _minBarSpacing;
64
+ private readonly _maxBarSpacing;
65
+ private _width;
66
+ private _baseIndex;
67
+ constructor(options?: Partial<TimeScaleOptions>);
68
+ setWidth(width: number): void;
69
+ get width(): number;
70
+ get barSpacing(): number;
71
+ setBarSpacing(value: number): void;
72
+ get rightOffset(): number;
73
+ setRightOffset(value: number): void;
74
+ /** Logical index of the latest bar; the right edge anchors to baseIndex+rightOffset. */
75
+ setBaseIndex(index: number): void;
76
+ private _rightEdgeIndex;
77
+ /** Logical index → x (media px), bar center. */
78
+ indexToX(index: number): number;
79
+ /** x (media px) → fractional logical index. */
80
+ xToIndex(x: number): number;
81
+ /** Currently visible logical index range (fractional, unclamped to data). */
82
+ visibleRange(): LogicalRange;
83
+ /**
84
+ * Pan by a pixel delta. Positive `dx` drags chart content to the right
85
+ * (revealing older bars), matching a natural left-button drag.
86
+ */
87
+ scrollByPixels(dx: number): void;
88
+ /**
89
+ * Zoom around an anchor x (the cursor): change bar spacing by `factor`
90
+ * while keeping whatever logical index sits under `focusX` pinned there.
91
+ * `factor` > 1 zooms in (wider bars).
92
+ */
93
+ zoomAtX(focusX: number, factor: number): void;
94
+ /** Choose bar spacing so `barCount` bars fit the width, anchored at the right edge. */
95
+ fitContent(barCount: number): void;
96
+ }
97
+
98
+ interface PriceRange {
99
+ min: number;
100
+ max: number;
101
+ }
102
+ /**
103
+ * Price-scale mode. `linear` and `logarithmic` are full coordinate transforms;
104
+ * `percentage`/`indexed-to-100` (rebase to a baseline) and overlay scales are
105
+ * not yet implemented — see END_TO_END_AUDIT.md / README known limitations.
106
+ */
107
+ type PriceScaleMode = 'linear' | 'logarithmic';
108
+ interface PriceScaleOptions {
109
+ /** Fraction of pane height kept empty at top/bottom (default 0.1 each). */
110
+ marginTop: number;
111
+ marginBottom: number;
112
+ /** Instrument tick size (minMove), e.g. 0.05. 0 → infer from range. */
113
+ minMove: number;
114
+ /** Linear or logarithmic price↔y mapping. */
115
+ mode: PriceScaleMode;
116
+ /** Flip the axis (price increases downward) — for spread/short views. */
117
+ inverted: boolean;
118
+ }
119
+ declare class PriceScale {
120
+ private _options;
121
+ private _height;
122
+ private _min;
123
+ private _max;
124
+ private _autoScale;
125
+ constructor(options?: Partial<PriceScaleOptions>);
126
+ get options(): PriceScaleOptions;
127
+ setHeight(height: number): void;
128
+ get height(): number;
129
+ setPriceRange(range: PriceRange): void;
130
+ priceRange(): PriceRange;
131
+ /** Whether the range tracks the data (true) or has been set manually (false). */
132
+ get autoScale(): boolean;
133
+ setAutoScale(on: boolean): void;
134
+ /**
135
+ * Manually scale the visible range around its centre. `factor` > 1 widens the
136
+ * range (compress / zoom out), < 1 narrows it (expand / zoom in). Switches the
137
+ * scale to manual mode so autoscale stops overriding it.
138
+ */
139
+ scaleAroundCenter(factor: number): void;
140
+ /**
141
+ * Pan the visible range vertically by `dy` media px (dragging the plot up/down).
142
+ * Works in transformed space so it's correct for log scales, and respects
143
+ * `inverted`. Switches to manual mode so autoscale stops overriding it.
144
+ */
145
+ panByPixels(dy: number): void;
146
+ /** Recompute the visible range from data extremes + configured margins. */
147
+ autoscale(low: number, high: number): void;
148
+ /** Coordinate transform for the active mode (identity for linear, log10 for log). */
149
+ private _t;
150
+ private _tInv;
151
+ /** Price → y (media px). Higher price → smaller y (top of pane), unless inverted. */
152
+ priceToY(price: number): number;
153
+ /** y (media px) → price. */
154
+ yToPrice(y: number): number;
155
+ /** Decimal precision implied by minMove (or the visible range if unset). */
156
+ precision(): number;
157
+ /** Snap a price to the instrument tick size (no-op if minMove is 0). */
158
+ snapToTick(price: number): number;
159
+ /** Format a price for axis/label display. */
160
+ format(price: number): string;
161
+ /** Clamp a y to the pane (used by crosshair/order dragging). */
162
+ clampY(y: number): number;
163
+ }
164
+
165
+ /**
166
+ * Internal time is always **UTC seconds** (integer). Feed adapters convert
167
+ * broker formats (IST strings, epoch ms) to this at the edge; see ARCHITECTURE.md §4.0.
168
+ */
169
+ type UTCSeconds = number;
170
+ /** A single OHLC(V) bar. `volume` is optional (not all feeds carry it). */
171
+ interface Bar {
172
+ time: UTCSeconds;
173
+ open: number;
174
+ high: number;
175
+ low: number;
176
+ close: number;
177
+ volume?: number;
178
+ }
179
+
180
+ /**
181
+ * Shared data layer (ARCHITECTURE.md §4.1). One per chart. Merges all series by
182
+ * time onto a single logical-index space (0..N-1) so price + volume + indicator
183
+ * panes stay aligned, and so non-trading gaps collapse (an absent time simply
184
+ * has no logical index). Per-series rows are addressable by that shared index.
185
+ */
186
+
187
+ type SeriesId = number;
188
+ interface IndexedBar {
189
+ index: number;
190
+ bar: Bar;
191
+ }
192
+ declare class DataLayer {
193
+ private readonly _series;
194
+ private _sortedTimes;
195
+ private readonly _indexByTime;
196
+ private _nextId;
197
+ /** Register a new series; returns its id. */
198
+ createSeries(): SeriesId;
199
+ removeSeries(id: SeriesId): void;
200
+ /** Bulk-load (full replace) one series' data, then re-merge the time axis. */
201
+ setSeriesData(id: SeriesId, bars: readonly Bar[]): void;
202
+ /**
203
+ * Upsert bars into a series by time (used for history paging / backfill /
204
+ * out-of-order corrections — ARCHITECTURE.md §4.2). Existing times are
205
+ * replaced; new times are inserted; the result stays time-sorted.
206
+ *
207
+ * Prepending older bars shifts every existing logical index up by the
208
+ * inserted count — callers preserve the viewport by re-reading `baseIndex`
209
+ * (the invariant `rightEdge − index` is unchanged, so visible bars don't move).
210
+ */
211
+ addBars(id: SeriesId, bars: readonly Bar[]): void;
212
+ /**
213
+ * Apply a single live bar (ARCHITECTURE.md §4.2 hot path). Returns the kind of
214
+ * change so the chart auto-scrolls only on a genuine right-edge append:
215
+ * - `'append'` → newer than the last bar (advances baseIndex)
216
+ * - `'replace'` → same time as the last bar (intra-bar tick) or an existing time
217
+ * - `'insert'` → an older time inserted into history (late / out-of-order)
218
+ */
219
+ update(id: SeriesId, bar: Bar): 'append' | 'replace' | 'insert';
220
+ private _appendTime;
221
+ /** Number of logical indices (distinct time points across all series). */
222
+ get length(): number;
223
+ /** Logical index of the latest real bar (length - 1), or -1 if empty. */
224
+ get baseIndex(): number;
225
+ indexToTime(index: number): number | undefined;
226
+ timeToIndex(time: number): number | undefined;
227
+ /** All bars of a series paired with their shared logical index. */
228
+ indexedBars(id: SeriesId): IndexedBar[];
229
+ /** Bars of a series whose logical index lies within [fromIndex, toIndex]. */
230
+ visibleBars(id: SeriesId, fromIndex: number, toIndex: number): IndexedBar[];
231
+ private _rebuild;
232
+ }
233
+
234
+ /**
235
+ * Chart theme (palette). A single object drives chart chrome (background, grid,
236
+ * axes, crosshair), series defaults (up/down, line, area gradient, last price),
237
+ * and the trade layer (buy/sell, profit/loss). Renderers read theme colors when
238
+ * a per-series style field is absent, so one theme restyles the whole chart.
239
+ */
240
+ interface ChartTheme {
241
+ background: string;
242
+ grid: string;
243
+ axisText: string;
244
+ axisLine: string;
245
+ crosshair: string;
246
+ upColor: string;
247
+ downColor: string;
248
+ wickUpColor: string;
249
+ wickDownColor: string;
250
+ lineColor: string;
251
+ areaTopColor: string;
252
+ areaBottomColor: string;
253
+ baselineTopLine: string;
254
+ baselineTopFill: string;
255
+ baselineBottomLine: string;
256
+ baselineBottomFill: string;
257
+ lastPriceUp: string;
258
+ lastPriceDown: string;
259
+ lastPriceText: string;
260
+ buy: string;
261
+ sell: string;
262
+ profit: string;
263
+ loss: string;
264
+ }
265
+
266
+ /**
267
+ * Primitive / plugin API (ARCHITECTURE.md §8). The extension point that keeps
268
+ * the core small and powers markers, events, indicators, and the trade layer.
269
+ * A primitive draws on a pane, optionally contributes to autoscale, and
270
+ * optionally hit-tests for hover/drag.
271
+ */
272
+
273
+ type ZOrder = 'bottom' | 'normal' | 'top';
274
+ interface PrimitiveRenderContext {
275
+ timeScale: TimeScale;
276
+ priceScale: PriceScale;
277
+ dataLayer: DataLayer;
278
+ plotWidth: number;
279
+ plotHeight: number;
280
+ priceAxisWidth: number;
281
+ dpr: number;
282
+ theme: ChartTheme;
283
+ }
284
+ interface PrimitiveHit {
285
+ externalId: string;
286
+ zOrder: ZOrder;
287
+ /** Pixel distance from the cursor (smaller wins ties before z-order). */
288
+ distance: number;
289
+ cursor?: string;
290
+ }
291
+ /** Injected when a primitive is attached; lets it request a repaint. */
292
+ interface PrimitiveHost {
293
+ requestUpdate(): void;
294
+ }
295
+ interface IPrimitive {
296
+ /** Layer order vs series: 'bottom' (behind), 'normal' (over), 'top' (overlay). */
297
+ zOrder(): ZOrder;
298
+ draw(ctx: CanvasRenderingContext2D, rc: PrimitiveRenderContext): void;
299
+ /** Optional: expand the pane's autoscale range so this primitive isn't clipped. */
300
+ autoscaleInfo?(): {
301
+ min: number;
302
+ max: number;
303
+ } | null;
304
+ /** Optional: topmost hit under (x,y) in media px (relative to the pane plot). */
305
+ hitTest?(x: number, y: number, rc: PrimitiveRenderContext): PrimitiveHit | null;
306
+ attached?(host: PrimitiveHost): void;
307
+ detached?(): void;
308
+ }
309
+
310
+ /**
311
+ * Working-order line (ARCHITECTURE.md §9.1). A horizontal line at the order
312
+ * price, colored by side, labelled with side/qty/distance-from-LTP. Phase 8 is
313
+ * read-only; drag-to-modify and the ✕ cancel hit-zone arrive in Phase 9 (the
314
+ * hit-test already returns the order id + an ns-resize cursor for that).
315
+ */
316
+
317
+ declare class WorkingOrderLine implements IPrimitive {
318
+ private _order;
319
+ private _ltp;
320
+ private _host;
321
+ constructor(order: Order);
322
+ attached(host: PrimitiveHost): void;
323
+ detached(): void;
324
+ zOrder(): ZOrder;
325
+ get order(): Order;
326
+ update(order: Order): void;
327
+ setLtp(ltp: number): void;
328
+ autoscaleInfo(): {
329
+ min: number;
330
+ max: number;
331
+ };
332
+ private _price;
333
+ draw(ctx: CanvasRenderingContext2D, rc: PrimitiveRenderContext): void;
334
+ hitTest(x: number, y: number, rc: PrimitiveRenderContext): PrimitiveHit | null;
335
+ }
336
+
337
+ /**
338
+ * Position marker (ARCHITECTURE.md §9.1). A line at the average entry price with
339
+ * a live unrealized-P&L tag (₹ and %), colored by P&L sign, plus a breakeven
340
+ * reference. Updates cheaply on every LTP tick (overlay-only repaint).
341
+ */
342
+
343
+ declare class PositionMarker implements IPrimitive {
344
+ private _position;
345
+ private _ltp;
346
+ private _host;
347
+ constructor(position: Position);
348
+ attached(host: PrimitiveHost): void;
349
+ detached(): void;
350
+ zOrder(): ZOrder;
351
+ get position(): Position;
352
+ update(position: Position): void;
353
+ setLtp(ltp: number): void;
354
+ autoscaleInfo(): {
355
+ min: number;
356
+ max: number;
357
+ };
358
+ draw(ctx: CanvasRenderingContext2D, rc: PrimitiveRenderContext): void;
359
+ hitTest(x: number, y: number, rc: PrimitiveRenderContext): PrimitiveHit | null;
360
+ }
361
+
362
+ /**
363
+ * Bracket group (ARCHITECTURE.md §9.1). SL + Target lines tied to a position,
364
+ * with shaded risk (red) and reward (green) zones and an R:R label — the core
365
+ * advanced-trade-management visualisation. Phase 8 is read-only; Phase 9 makes
366
+ * the SL/TP lines draggable (OCO modify).
367
+ */
368
+
369
+ interface BracketState {
370
+ symbol: string;
371
+ side: OrderSide;
372
+ entry: number;
373
+ stop: number;
374
+ target: number;
375
+ }
376
+ declare class BracketGroup implements IPrimitive {
377
+ private _state;
378
+ private _host;
379
+ constructor(state: BracketState);
380
+ attached(host: PrimitiveHost): void;
381
+ detached(): void;
382
+ zOrder(): ZOrder;
383
+ get state(): BracketState;
384
+ update(state: BracketState): void;
385
+ autoscaleInfo(): {
386
+ min: number;
387
+ max: number;
388
+ };
389
+ draw(ctx: CanvasRenderingContext2D, rc: PrimitiveRenderContext): void;
390
+ hitTest(x: number, y: number, rc: PrimitiveRenderContext): PrimitiveHit | null;
391
+ }
392
+
393
+ /**
394
+ * Trade controller (ARCHITECTURE.md §9.3) — read-only in Phase 8. The single
395
+ * source of truth: it reconciles order/position book snapshots into on-chart
396
+ * primitives (add/update/remove) and pushes LTP into them for live P&L. On a
397
+ * reconnect, a fresh snapshot is diffed against current primitives, so vanished
398
+ * orders are removed (STALE handling) with no special code path.
399
+ */
400
+
401
+ /** Where the controller attaches/detaches its primitives (the chart implements this). */
402
+ interface TradeHost {
403
+ addPrimitive(p: IPrimitive): void;
404
+ removePrimitive(p: IPrimitive): void;
405
+ }
406
+ declare class TradeController {
407
+ private readonly _host;
408
+ private readonly _orderLines;
409
+ private readonly _markers;
410
+ private readonly _brackets;
411
+ private readonly _ltp;
412
+ constructor(host: TradeHost);
413
+ /** Reconcile a full book snapshot. Idempotent — safe to call on every update or reconnect. */
414
+ reconcile(orders: readonly Order[], positions: readonly Position[]): void;
415
+ private _reconcileOrderLines;
416
+ private _reconcilePositions;
417
+ private _reconcileBrackets;
418
+ /** Push a last price; updates the live P&L / distance on bound primitives. */
419
+ onLtp(symbol: string, ltp: number): void;
420
+ /** Test/introspection helpers. */
421
+ orderLineCount(): number;
422
+ positionCount(): number;
423
+ bracketCount(): number;
424
+ }
425
+
426
+ interface DepthLevel {
427
+ price: number;
428
+ qty: number;
429
+ orders?: number;
430
+ }
431
+ /** Variable-depth book; `bids`/`asks` length = whatever the broker streams (5..200). */
432
+ interface MarketDepth {
433
+ bids: DepthLevel[];
434
+ asks: DepthLevel[];
435
+ ltp: number;
436
+ ltq?: number;
437
+ }
438
+
439
+ /**
440
+ * Order state machine (ARCHITECTURE.md §9.5). Explicit client-side states with
441
+ * a guarded transition table — no optimistic guesswork. Pure and fully testable.
442
+ *
443
+ * pending_place ─ack→ working ─fill→ filled
444
+ * ─reject→ rejected
445
+ * working/partial ─submitModify→ modify_pending ─ack→ working / ─reject→ working
446
+ * working/partial ─submitCancel→ cancel_pending ─cancelled→ cancelled / ─reject→ working
447
+ * any non-terminal ─reconnectAbsent→ stale
448
+ */
449
+ type ClientOrderState = 'pending_place' | 'working' | 'partial' | 'filled' | 'modify_pending' | 'cancel_pending' | 'rejected' | 'cancelled' | 'stale';
450
+ type OrderEvent = 'ack' | 'partialFill' | 'fill' | 'reject' | 'submitModify' | 'submitCancel' | 'cancelled' | 'reconnectAbsent';
451
+ declare function isTerminal(state: ClientOrderState): boolean;
452
+ /** Whether `event` is allowed from `state`. */
453
+ declare function canTransition(state: ClientOrderState, event: OrderEvent): boolean;
454
+ /** Apply `event`; returns the next state, or the same state if the event is invalid. */
455
+ declare function transition(state: ClientOrderState, event: OrderEvent): ClientOrderState;
456
+
457
+ interface PriceBand {
458
+ lower: number;
459
+ upper: number;
460
+ }
461
+ interface ValidationResult {
462
+ ok: boolean;
463
+ reason?: string;
464
+ /** Price after tick-size snapping (when applicable). */
465
+ price?: number;
466
+ }
467
+ interface OrderConstraints {
468
+ tickSize: number;
469
+ priceBand?: PriceBand;
470
+ /** Max quantity per single order (exchange freeze limit). */
471
+ freezeQty?: number;
472
+ }
473
+ /** True if `price` lies within the inclusive band. */
474
+ declare function withinPriceBand(price: number, band: PriceBand): boolean;
475
+ /**
476
+ * Validate a price + qty against constraints. Snaps price to the tick size and
477
+ * returns the snapped value; rejects out-of-band prices and over-freeze qty.
478
+ */
479
+ declare function validateOrder(price: number, qty: number, c: OrderConstraints): ValidationResult;
480
+
481
+ /**
482
+ * Order engine (ARCHITECTURE.md §9.5) — the chart-trading write path. Drives the
483
+ * order state machine with: client-token idempotency, an arm/confirm gate,
484
+ * pre-trade validation, rate-limited drag-modify, OCO linking, and analyzer
485
+ * (sandbox) mode. Network-agnostic: it talks to an injected OrderFeed (the
486
+ * FakeBroker simulates it in tests/demos).
487
+ */
488
+
489
+ interface PlaceRequest {
490
+ symbol: string;
491
+ exchange?: string;
492
+ side: OrderSide;
493
+ type: OrderType;
494
+ qty: number;
495
+ price?: number;
496
+ triggerPrice?: number;
497
+ /** Product: CNC (delivery), NRML (F&O carry), MIS (intraday). Required by OpenAlgo. */
498
+ product?: 'CNC' | 'NRML' | 'MIS';
499
+ /** Idempotency token; a retry with the same token is never double-sent. */
500
+ clientToken?: string;
501
+ }
502
+ interface OrderFeed {
503
+ place(req: PlaceRequest & {
504
+ mode: TradeMode;
505
+ }): Promise<{
506
+ orderId: string;
507
+ }>;
508
+ modify(orderId: string, patch: {
509
+ price?: number;
510
+ triggerPrice?: number;
511
+ qty?: number;
512
+ }): Promise<void>;
513
+ cancel(orderId: string): Promise<void>;
514
+ }
515
+ type TradeMode = 'live' | 'analyzer';
516
+ type GateFn = (req: PlaceRequest) => boolean | Promise<boolean>;
517
+ interface OrderEngineOptions {
518
+ feed: OrderFeed;
519
+ constraints: OrderConstraints;
520
+ mode?: TradeMode;
521
+ /** Armed = fire immediately; otherwise the gate must approve each order. */
522
+ armed?: boolean;
523
+ gate?: GateFn;
524
+ minModifyIntervalMs?: number;
525
+ now?: () => number;
526
+ idGen?: () => string;
527
+ /** Called when a drag-modify price fails validation (so the UI can snap back). */
528
+ onValidationError?: (reason: string) => void;
529
+ }
530
+ interface PlaceResult {
531
+ ok: boolean;
532
+ clientId?: string;
533
+ state?: ClientOrderState;
534
+ reason?: string;
535
+ }
536
+ declare class OrderEngine {
537
+ private readonly _feed;
538
+ private readonly _constraints;
539
+ private readonly _mode;
540
+ private readonly _armed;
541
+ private readonly _gate?;
542
+ private readonly _minModifyMs;
543
+ private readonly _now;
544
+ private readonly _idGen;
545
+ private readonly _onValidationError?;
546
+ private readonly _orders;
547
+ private readonly _byBroker;
548
+ private readonly _sentTokens;
549
+ private readonly _lastModifyAt;
550
+ private readonly _pendingModify;
551
+ private _counter;
552
+ constructor(opts: OrderEngineOptions);
553
+ get mode(): TradeMode;
554
+ state(clientId: string): ClientOrderState | undefined;
555
+ placeOrder(req: PlaceRequest): Promise<PlaceResult>;
556
+ /** One-click market order. */
557
+ placeMarket(symbol: string, side: OrderSide, qty: number): Promise<PlaceResult>;
558
+ /** Link two orders as OCO: when one fills/cancels, the other is cancelled. */
559
+ linkOco(clientIdA: string, clientIdB: string): void;
560
+ /**
561
+ * Rate-limited modify (drag): coalesces to the latest, sends at most every
562
+ * minModifyMs. An invalid price (tick/band/freeze) is NOT enqueued or sent —
563
+ * it surfaces via `onValidationError` so the UI can snap the line back.
564
+ */
565
+ requestModify(clientId: string, price: number): void;
566
+ /** Force-send any pending modify (e.g. on drag end). */
567
+ commitModify(clientId: string): Promise<void>;
568
+ private _flushModify;
569
+ cancelOrder(clientId: string): Promise<void>;
570
+ /** Broker fill event (by broker id). Advances state and triggers OCO. */
571
+ onFill(brokerId: string, full: boolean): void;
572
+ private _cancelOcoPeer;
573
+ /** On reconnect, mark any non-terminal order absent from the fresh book as STALE. */
574
+ onReconnect(presentBrokerIds: ReadonlySet<string>): void;
575
+ }
576
+
577
+ /**
578
+ * Deterministic in-memory broker simulator (ARCHITECTURE.md §11.1). Holds order
579
+ * and position snapshots and notifies subscribers — lets the trade layer be
580
+ * tested and demoed with zero network. Phase 9 extends it with the place/modify/
581
+ * cancel state machine; Phase 8 uses it read-only (seed snapshots + emit LTP).
582
+ */
583
+
584
+ declare class FakeBroker implements OrderFeed {
585
+ private _orders;
586
+ private _positions;
587
+ private readonly _bookListeners;
588
+ private readonly _ltpListeners;
589
+ private readonly _depthListeners;
590
+ private _idCounter;
591
+ /** Set to a reason to make the next place() reject (test hook). */
592
+ rejectNextPlace: string | null;
593
+ onBook(cb: (orders: Order[], positions: Position[]) => void): void;
594
+ onLtp(cb: (symbol: string, ltp: number) => void): void;
595
+ /** Replace the current book snapshot and notify (simulates a poll / WS update / reconnect). */
596
+ setBook(orders: Order[], positions: Position[]): void;
597
+ emitLtp(symbol: string, ltp: number): void;
598
+ onDepth(cb: (symbol: string, depth: MarketDepth) => void): void;
599
+ emitDepth(symbol: string, depth: MarketDepth): void;
600
+ /** Build a deterministic N-level synthetic book around `ltp` (demo/test helper). */
601
+ static makeDepth(ltp: number, levels: number, tickSize?: number): MarketDepth;
602
+ orders(): readonly Order[];
603
+ positions(): readonly Position[];
604
+ place(req: PlaceRequest & {
605
+ mode: TradeMode;
606
+ }): Promise<{
607
+ orderId: string;
608
+ }>;
609
+ modify(orderId: string, patch: {
610
+ price?: number;
611
+ triggerPrice?: number;
612
+ qty?: number;
613
+ }): Promise<void>;
614
+ cancel(orderId: string): Promise<void>;
615
+ /** Simulate a fill for an order (test hook). */
616
+ fill(orderId: string): void;
617
+ private _notify;
618
+ }
619
+
620
+ /**
621
+ * Depth-of-market ladder (ARCHITECTURE.md §9.4). Docked to the right of the
622
+ * plot, price-aligned to the price scale. Depth-agnostic: it reads the level
623
+ * count from the payload (5 / 20 / 30 / 50 / 200) at runtime. Viewport
624
+ * virtualization keeps a deep book at 60 fps; price-bucket aggregation compacts
625
+ * deep books; a size heatmap highlights resting liquidity; and it degrades
626
+ * gracefully to nothing when no depth is available.
627
+ *
628
+ * Pure helpers (capability / aggregation / virtualization) are split out for
629
+ * unit testing; the primitive draws on the top (overlay) canvas so frequent
630
+ * depth updates only repaint the cheap overlay.
631
+ */
632
+
633
+ type LadderTier = 'none' | 'compact' | 'deep';
634
+ /** Capability tier from the live payload — drives graceful degradation. */
635
+ declare function ladderCapability(depth: MarketDepth): LadderTier;
636
+ interface LadderRow {
637
+ price: number;
638
+ bidQty: number;
639
+ askQty: number;
640
+ }
641
+ /**
642
+ * Merge bids + asks into price rows, optionally bucketing every `groupBy` ticks
643
+ * (price-step aggregation for deep books). Returns rows sorted high → low price.
644
+ */
645
+ declare function buildRows(depth: MarketDepth, tickSize: number, groupBy?: number): LadderRow[];
646
+ /**
647
+ * Virtualize: keep only rows whose y is inside the plot (± one row) and cap the
648
+ * count to `maxRows` nearest the vertical centre (around the LTP). This is what
649
+ * keeps a 200-level book cheap and readable.
650
+ */
651
+ declare function visibleRows(rows: readonly LadderRow[], priceToY: (p: number) => number, plotHeight: number, rowHeight: number, maxRows: number): LadderRow[];
652
+ interface DomLadderOptions {
653
+ tickSize: number;
654
+ /** Strip width in media px. */
655
+ width: number;
656
+ /** Group every N ticks into one row (deep-book aggregation). */
657
+ groupBy: number;
658
+ /** Max rows drawn per frame (virtualization cap). */
659
+ maxRows: number;
660
+ rowHeight: number;
661
+ }
662
+ declare const DEFAULT_DOM_LADDER_OPTIONS: DomLadderOptions;
663
+ declare class DomLadder implements IPrimitive {
664
+ private _opts;
665
+ private _depth;
666
+ private _host;
667
+ private _rowHits;
668
+ constructor(options?: Partial<DomLadderOptions>);
669
+ attached(host: PrimitiveHost): void;
670
+ detached(): void;
671
+ zOrder(): ZOrder;
672
+ setDepth(depth: MarketDepth): void;
673
+ tier(): LadderTier;
674
+ draw(ctx: CanvasRenderingContext2D, rc: PrimitiveRenderContext): void;
675
+ hitTest(x: number, y: number, rc: PrimitiveRenderContext): PrimitiveHit | null;
676
+ }
677
+
678
+ declare const TRADE_TIER: "trade";
679
+
680
+ export { BracketGroup, type BracketState, type ClientOrderState, DEFAULT_DOM_LADDER_OPTIONS, DomLadder, type DomLadderOptions, FakeBroker, type GateFn, type LadderRow, type LadderTier, type Order, type OrderConstraints, OrderEngine, type OrderEngineOptions, type OrderEvent, type OrderFeed, type OrderRole, type OrderSide, type OrderStatus, type OrderType, type PlaceRequest, type PlaceResult, type Position, PositionMarker, type PriceBand, TRADE_TIER, TradeController, type TradeHost, type TradeMode, type ValidationResult, WorkingOrderLine, bracketValid, breakeven, buildRows, canTransition, isTerminal, isWorking, ladderCapability, riskReward, transition, unrealizedPnl, unrealizedPnlPercent, validateOrder, visibleRows, withinPriceBand };