Order Provider
OrderProvider connects trading orders with DXcharts application, enabling to display and manage orders directly on the chart.
Here's how orders appear on the chart:
What it does
The Order Provider allows you to:
- Display pending orders on the chart
- Create new orders from the chart UI
- Update existing order parameters (price, quantity, etc.)
- Delete orders
- Track executed orders
- Support OCO (One Cancels Other) orders
- Create protection orders (Stop Loss / Take Profit)
Implementation Examples
Simple Mock Provider
This is a minimal stub implementation that satisfies the interface but doesn't store any data:
import { OrderProvider } from '@dx-private/dxchart5-react/dist/providers/trading/order.provider';export const createMockOrderProvider = (): OrderProvider => {return {createOrder() {return Promise.resolve('');},createOcoOrders() {return Promise.resolve(['', '']);},deleteOrder() {return Promise.resolve();},observeExecutedOrders() {return () => {};},observeOrders() {return () => {};},updateOrder() {return Promise.resolve();},};};
Demo Provider
This implementation is used in our demo application but is not production-ready. It relies on InternalChartDataProvider and other internal APIs that are not part of the public API and may change without notice. Use this as a reference to understand how a complete implementation works, but consider building your own provider using your backend API for production use.
This is the full implementation used in our demo app. It includes:
- Automatic order execution simulation
- Integration with real-time chart data
- Protection orders management
- Executed orders tracking
- RxJS-based reactive architecture
Important: This provider has a circular dependency with the position provider. You must initialize them in a specific order (see initialization section below).
import { Instrument } from '@dx-private/dxchart5-react/dist/chart/model/instrument.model';import { merge, ReplaySubject, Subject } from 'rxjs';import { delay, filter, share, tap, switchMap, take, withLatestFrom } from 'rxjs/operators';import { toCandles } from '@dx-private/dxchart5-react/dist/chart/model/chart.model';import { OrderProvider } from '@dx-private/dxchart5-react/dist/providers/trading/order.provider';import { createIdGenerator, nextId } from '@dx-private/dxchart5-react/dist/utils/id-generator.utils';import { getOrderPrice, isProtection } from '@dx-private/dxchart5-react/dist/chart/model/trading/trading.model';import { Order, OrderId, OrderWithId } from '@dx-private/dxchart5-react/dist/chart/model/trading/order.model';import { record, string } from 'fp-ts';import { pipe } from 'fp-ts/function';import { InternalChartDataProvider } from '@dx-private/dxchart5-react/dist/providers/chart-data-provider';import { ExecutedOrder } from '@dx-private/dxchart5-modules/dist/executed-orders/model/executed-orders.model';import { InternalPositionProvider } from './demo-trading-positions-provider';export interface InternalOrdersProvider extends OrderProvider {deleteOrder(symbol: string, order: OrderWithId): Promise<void>;deleteOrder(symbol: string, id: string, withProtections?: boolean): Promise<void>;}export interface TradingOrderProviderConfig {/*** Timestamp delay in milliseconds for executed orders.* Use this if your data provider has a delay, 0 by default*/executedOrdersTimestampDelay?: number;/*** Simulated market order execution delay in milliseconds.* This adds a delay before a market order is converted to a position, 1000ms by default*/marketOrderExecutionDelay?: number;}export const createTradingOrderProvider = (chartDataProviderProxy: InternalChartDataProvider,positionsProvider: InternalPositionProvider,config?: TradingOrderProviderConfig,): InternalOrdersProvider => {const timestampDelay = config?.executedOrdersTimestampDelay ?? 0;const marketOrderDelay = config?.marketOrderExecutionDelay ?? 1000;const generatorId = createIdGenerator('ordersOMS__');const orders: Record<Instrument['symbol'], Record<OrderWithId['id'], OrderWithId>> = {};const currentOrdersBySymbol: Record<Instrument['symbol'], ReplaySubject<OrderWithId[]>> = {};const ordersBySymbol = (symbol: string): Array<OrderWithId> =>pipe(orders[symbol],record.collect(string.Ord)((_, v) => v),orders => orders.filter(order => order !== null && order !== undefined),);const executedOrders: Record<Instrument['symbol'], Record<ExecutedOrder['id'], ExecutedOrder>> = {};const currentExecutedOrdersBySymbol: Record<Instrument['symbol'], ReplaySubject<ExecutedOrder[]>> = {};const executedOrdersBySymbol = (symbol: string): Array<ExecutedOrder> => {if (!executedOrders[symbol]) {return [];}return pipe(executedOrders[symbol],record.collect(string.Ord)((_, v) => v),);};const newOrderWithSymbol = new Subject<[Instrument['symbol'], OrderWithId]>();const deleteOrder = async (symbol: string,arg: OrderWithId | string,withProtections: boolean = true,): Promise<void> => {const id = typeof arg === 'string' ? arg : arg.id;if (orders[symbol]) {const orderForDelete = orders[symbol][id];if (!orderForDelete) {return Promise.resolve();}// delete protection order's id from "original"if (isProtection(orderForDelete)) {const originalOrder = orders[symbol][orderForDelete.originalItemId];// if originalOrder does not exist, it means that original item is positionif (!originalOrder) {positionsProvider.unlinkProtectionOrderFromPosition(symbol,orderForDelete.originalItemId,orderForDelete.type,);}// if originalOrder exists, delete protecction's linkif (originalOrder && !isProtection(originalOrder) && originalOrder.protectionOrderIds) {originalOrder.protectionOrderIds[orderForDelete.type === 'sl' ? 0 : 1] = undefined;originalOrder.protectionOrderIds.every(id => id === undefined) &&delete originalOrder.protectionOrderIds;}} else {// delete all protection orders if order is "original"orderForDelete.protectionOrderIds?.forEach((protectionOrderId: string | undefined) => {withProtections && protectionOrderId && delete orders[symbol][`${protectionOrderId}`];});}delete orders[symbol][`${id}`];triggerOrders(symbol);return Promise.resolve();}return Promise.resolve();};const createOrder = (order: Order): OrderWithId => {const id = nextId(generatorId);const newOrder: OrderWithId = {...order,id: `${id}`,};return newOrder;};const createOcoOrders = (symbol: string,parentOrderId: OrderId,orders: [Order | undefined, Order | undefined],parentOrder?: OrderWithId,): Promise<[string, string]> => {console.log('create oco', symbol, parentOrderId, orders, 'parent order data:', parentOrder);return Promise.resolve(['', '']);};const triggerOrders = (symbol: string) => {if (!currentOrdersBySymbol[symbol]) {currentOrdersBySymbol[symbol] = new ReplaySubject<Array<OrderWithId>>();}currentOrdersBySymbol[symbol].next(ordersBySymbol(symbol));};const triggerExecutedOrders = (symbol: string) => {if (!currentExecutedOrdersBySymbol[symbol]) {currentExecutedOrdersBySymbol[symbol] = new ReplaySubject<Array<ExecutedOrder>>();}currentExecutedOrdersBySymbol[symbol].next(executedOrdersBySymbol(symbol));};// determines how an order should be executed based on current market price and order type/sideconst shouldExecuteOrder = (order: OrderWithId, orderPrice: number, marketPrice: number): boolean => {switch (order.orderType) {case 'stop':return order.side === 'buy' ? marketPrice >= orderPrice : marketPrice <= orderPrice;case 'limit':return order.side === 'buy' ? marketPrice <= orderPrice : marketPrice >= orderPrice;// market order type is proceeded in createPositionAfterMarketOrderEffectcase 'market':default:return false;}};// determines how a protection order should be executed based on current market price and order type/sideconst shouldExecuteProtectionOrder = (order: OrderWithId, orderPrice: number, marketPrice: number): boolean => {switch (order.type) {case 'tp':return order.side === 'buy' ? marketPrice <= orderPrice : marketPrice >= orderPrice;case 'sl':return order.side === 'buy' ? marketPrice >= orderPrice : marketPrice <= orderPrice;default:return false;}};const updateOrdersOnInstrumentChangeEffect = chartDataProviderProxy.observeSymbolChanged().pipe(tap(symbol => {if (!orders[symbol]) {orders[symbol] = {};}triggerOrders(symbol);}),);const updateExecutedOrdersOnInstrumentChangeEffect = pipe(chartDataProviderProxy.observeHistoryDataUpdated(),withLatestFrom(chartDataProviderProxy.observeSymbolChanged()),tap(([_, symbol]) => {if (!orders[symbol]) {orders[symbol] = {};}triggerExecutedOrders(symbol);}),);const createPositionAfterMarketOrderEffect = newOrderWithSymbol.asObservable().pipe(filter(([, order]) => order.orderType === 'market'),delay(marketOrderDelay),switchMap(([symbol, order]) =>chartDataProviderProxy.observeSymbolCandleUpdated().pipe(take(1),tap(symbolCandle => {const lastCandle = toCandles(symbolCandle.candle);deleteOrder(symbol, order.id, false);positionsProvider.addPosition(symbol, order, lastCandle.close || 0);}),),),);const processOrdersEffect = chartDataProviderProxy.observeSymbolCandleUpdated().pipe(tap(symbolCandle => {const lastCandle = toCandles(symbolCandle.candle);const symbol = symbolCandle.symbol;if (lastCandle) {const currentSymbolOrders = ordersBySymbol(symbol);currentSymbolOrders.filter(order => order && order.type === 'original').forEach(order => {const price = getOrderPrice(order, lastCandle.close);if (shouldExecuteOrder(order, price, lastCandle.close)) {const protectionOrderIds = order.type === 'original' ? order.protectionOrderIds : undefined;const slId = protectionOrderIds?.[0];const tpId = protectionOrderIds?.[1];const sl = slId && orders[symbol] ? orders[symbol][slId] : undefined;const tp = tpId && orders[symbol] ? orders[symbol][tpId] : undefined;deleteOrder(symbol, order.id, false).then(() => {const newOriginalId = positionsProvider.addPosition(symbol, order, lastCandle.close);if (newOriginalId) {if (sl && isProtection(sl)) {sl.originalItemId = newOriginalId;positionsProvider.linkProtectionOrderToPosition(symbol,sl.id,newOriginalId,'sl',);}if (tp && isProtection(tp)) {tp.originalItemId = newOriginalId;positionsProvider.linkProtectionOrderToPosition(symbol,tp.id,newOriginalId,'tp',);}}triggerOrders(symbol);});}});currentSymbolOrders.filter(order => order && isProtection(order)).forEach(order => {const price = getOrderPrice(order, lastCandle.close);if (shouldExecuteProtectionOrder(order, price, lastCandle.close)) {const originalOrderExists = currentSymbolOrders.find(o => o?.id === order.originalItemId);if (!originalOrderExists && !orders[symbol]?.[order.originalItemId]) {deleteOrder(symbol, order.id, true).then(() => {positionsProvider.closePosition(symbol, order.originalItemId);triggerOrders(symbol);});}}});}}),);const addExecutedOrderEffect = pipe(pipe(positionsProvider.positionAddedSubject,tap(data => {Object.entries(data).map(([symbol, position]) => {const executedOrder: ExecutedOrder = {id: position.id,price: position.price,quantity: position.quantity,side: position.side,timestamp: Date.now() - timestampDelay,};if (!executedOrders[symbol]) {executedOrders[symbol] = {};}executedOrders[symbol][position.id] = executedOrder;triggerExecutedOrders(symbol);});}),),);const effects = pipe(merge(updateOrdersOnInstrumentChangeEffect,createPositionAfterMarketOrderEffect,processOrdersEffect,updateExecutedOrdersOnInstrumentChangeEffect,addExecutedOrderEffect,),share(),);effects.subscribe();return {createOrder(symbol: string, order: Order): Promise<OrderId> {if (orders[symbol]) {const newOrder = createOrder(order);orders[symbol][newOrder.id] = newOrder;if (isProtection(newOrder)) {const originalOrder = orders[symbol][newOrder.originalItemId];// if originalOrder does not exist, it means that original item is positionif (!originalOrder) {positionsProvider.linkProtectionOrderToPosition(symbol,newOrder.id,newOrder.originalItemId,newOrder.type,);}// if originalOrder exist, link new protection order to protectionOrderIdsif (originalOrder && !isProtection(originalOrder)) {if (originalOrder.protectionOrderIds) {originalOrder.protectionOrderIds[newOrder.type === 'sl' ? 0 : 1] = newOrder.id;} else {originalOrder.protectionOrderIds =newOrder.type === 'sl' ? [newOrder.id, undefined] : [undefined, newOrder.id];}}}triggerOrders(symbol);newOrderWithSymbol.next([symbol, newOrder]);return Promise.resolve(`${newOrder.id}`);}return Promise.reject(new Error(`No storage for ${symbol}`));},createOcoOrders,deleteOrder,updateOrder(symbol: string, order: OrderWithId): Promise<void> {if (orders[symbol][order.id]) {orders[symbol][order.id] = order;}triggerOrders(symbol);return Promise.resolve();},observeOrders(symbol: string, callback: (data: OrderWithId[]) => void): () => void {if (!currentOrdersBySymbol[symbol]) {currentOrdersBySymbol[symbol] = new ReplaySubject<Array<OrderWithId>>();}const subscription = currentOrdersBySymbol[symbol].subscribe(callback);return () => {subscription.unsubscribe();};},observeExecutedOrders(symbol: string, callback: (data: ExecutedOrder[]) => void): () => void {if (!currentExecutedOrdersBySymbol[symbol]) {currentExecutedOrdersBySymbol[symbol] = new ReplaySubject<Array<ExecutedOrder>>();}const subscription = currentExecutedOrdersBySymbol[symbol].subscribe(callback);return () => {subscription.unsubscribe();};},};};
-
InternalChartDataProvider Integration:
observeSymbolChanged()- Tracks when the user switches instrumentsobserveSymbolCandleUpdated()- Monitors real-time price updates for order executionobserveHistoryDataUpdated()- Detects when historical data loads
-
Reactive Effects with RxJS:
- Initializes order storage when switching symbols
- Updates executed orders display when historical data loads
- Auto-executes market orders after a configurable delay
- Checks limit/stop orders against real-time prices and auto-executes when conditions are met
- Creates executed order records when positions are opened
-
Protection Orders Management:
- Automatically links/unlinks Stop Loss and Take Profit orders to parent orders or positions
- Transfers protection orders when an order converts to a position
- Handles cleanup when protection orders or parent items are deleted
-
Circular Dependency Resolution:
- The order provider needs the position provider (to create positions from executed orders)
- The position provider needs the order provider (to delete protection orders when positions close)
- This is resolved through careful initialization order (see next section)
How to use
Using the Simple Mock Provider
import { ChartReactApp } from '@dx-private/dxchart5-react/dist/chart/chart-react-app';
import { CREATE_MOCK_PROVIDERS } from '@dx-private/dxchart5-react-mock-providers';
// Create the simple mock providers
const orderProvider = createMockOrderProvider();
const positionProvider = createMockPositionProvider();
// Use them in your chart
<ChartReactApp
dependencies={{
...CREATE_MOCK_PROVIDERS(),
orderProvider,
positionProvider,
}}
config={{
trading: { enabled: true }
}}
/>
Using the Demo Provider (with Circular Dependency Resolution)
IMPORTANT: The demo order and position providers have a circular dependency. You must initialize them in this specific order:
import { ChartReactApp } from '@dx-private/dxchart5-react/dist/chart/chart-react-app';
import { CREATE_MOCK_PROVIDERS } from '@dx-private/dxchart5-react-mock-providers';
import { createChartDataProviderProxy } from '@dx-private/dxchart5-react/dist/providers/chart-data-provider';
// Step 1: Create chart data provider
const chartDataProvider = createChartDataProviderProxy(yourActualDataProvider);
// Step 2: Create a wrapper function that will return the order provider (resolves circular dependency)
const getOrdersProvider = () => Promise.resolve(orderProvider);
// Step 3: Create position provider FIRST (pass the wrapper function)
const positionProvider = createTradingPositionProvider(
chartDataProvider,
getOrdersProvider // Promise-based lazy reference to order provider
);
// Step 4: Create order provider SECOND (pass actual position provider instance)
const orderProvider = createTradingOrderProvider(
chartDataProvider,
positionProvider, // Direct reference to position provider
{
executedOrdersTimestampDelay: 0, // No delay for real-time data
marketOrderExecutionDelay: 1000 // 1 second delay for market orders
}
);
// Step 5: Use in your chart
<ChartReactApp
dependencies={{
...CREATE_MOCK_PROVIDERS(),
chartDataProvider,
orderProvider,
positionProvider,
}}
config={{
trading: { enabled: true }
}}
/>
React Hook Example:
import { useMemo } from 'react';
function MyChartComponent() {
const chartDataProvider = useMemo(
() => createChartDataProviderProxy(yourActualDataProvider),
[]
);
// Position provider first (with lazy order provider reference)
const positionProvider = useMemo(
() => createTradingPositionProvider(
chartDataProvider,
() => Promise.resolve(orderProvider)
),
[chartDataProvider]
);
// Order provider second (with direct position provider reference)
const orderProvider = useMemo(
() => createTradingOrderProvider(
chartDataProvider,
positionProvider,
{ executedOrdersTimestampDelay: 0 }
),
[chartDataProvider, positionProvider]
);
return (
<ChartReactApp
dependencies={{
...CREATE_MOCK_PROVIDERS(),
chartDataProvider,
orderProvider,
positionProvider,
}}
config={{
trading: { enabled: true }
}}
/>
);
}
Why this order matters:
- Position provider needs to call
orderProvider.deleteOrder()when closing positions with protection orders - Order provider needs to call
positionProvider.addPosition()when orders are executed - By passing a Promise-returning function to the position provider, we delay the resolution until runtime
- By the time the position provider needs to call the order provider, it has already been created
Key methods
observeOrders- Subscribe to order updates. The chart will call this method to receive the current list of orders and updates.observeExecutedOrders- Subscribe to executed order updates. Used to track when orders are filled.createOrder- Called when a user creates a new order from the chart UI.updateOrder- Called when a user modifies an existing order (e.g., drags the order line to a new price).deleteOrder- Called when a user deletes an order.createOcoOrders- Create two linked OCO orders simultaneously.
Order Execution
When an order is executed, you should:
- Add the order ID to the executed orders set
- Remove the order from the active orders list
- Notify subscribers of both changes
// Example: Mark an order as executed
const executeOrder = (symbol: string, orderId: string) => {
// Add to executed orders
if (!executedOrdersMap.has(symbol)) {
executedOrdersMap.set(symbol, new Set());
}
executedOrdersMap.get(symbol)!.add(orderId);
// Remove from active orders
getOrdersForSymbol(symbol).delete(orderId);
// Notify both types of subscribers
notifySubscribers(symbol);
notifyExecutedSubscribers(symbol);
};
Protection Orders
Orders support protection orders (Stop Loss and Take Profit). When a user creates a protection order from the chart UI, the createOrder method will be called with a ProtectionOrder object that includes:
originalItemId- Links the protection order to its parent ordertype- Either'sl'(Stop Loss) or'tp'(Take Profit)
The parent order should maintain a protectionOrderIds array to track its protection orders:
// Example: Creating an order with protection orders
const originalOrder: OrderWithId = {
id: 'order_123',
type: 'original',
side: 'buy',
orderType: 'limit',
limitPrice: 150.00,
quantity: 100,
protectionOrderIds: ['sl_order_124', 'tp_order_125'], // [SL, TP]
};
const stopLossOrder: ProtectionOrder = {
id: 'sl_order_124',
type: 'sl',
originalItemId: 'order_123',
side: 'sell',
orderType: 'stop',
stopPrice: 145.00,
quantity: 100,
};
const takeProfitOrder: ProtectionOrder = {
id: 'tp_order_125',
type: 'tp',
originalItemId: 'order_123',
side: 'sell',
orderType: 'limit',
limitPrice: 155.00,
quantity: 100,
};
Important: When an original order or protection order is deleted, remember to:
- Remove the connection from the parent order's
protectionOrderIdsarray - Delete all associated protection orders when the original order is deleted
See Trading for more details on protection orders and OCO orders.
API reference
OrderProvider
Manages trading orders on the chart
- OrderProvider.observeOrders
- Parameters
- symbol: string
- dataCallback: (orders: OrderWithId[]) => void
- Returns
- () => void
OrderProvider.observeOrders(symbol: string, dataCallback: (orders: OrderWithId[]) => void): () => void
Observes the order lines updates. We expect to be the full list of orders - no partial updates, for now the full list will be replaced.
- OrderProvider.observeExecutedOrders
- Parameters
- symbol: string
- dataCallback: (orders: ExecutedOrder[]) => void
- Returns
- () => void
OrderProvider.observeExecutedOrders(symbol: string, dataCallback: (orders: ExecutedOrder[]) => void): () => void
Observes the executed orders. We expect to be the full list of executed orders - no partial updates, for now the full list will be replaced.
- OrderProvider.createOrder
- Parameters
- symbol: string
- order: Order
- Returns
- Promise<string>
OrderProvider.createOrder(symbol: string, order: Order): Promise<string>
Creates new order We expect creating "id" for order inside this method If you create protection order, please don't forget to link it to original order by "protectionOrderIds" field
- OrderProvider.createOcoOrders
- Parameters
- symbol: string
- - current instrument's symbol
- parentOrderId: string
- - id of the order from which OCO orders will be created
- orders: [Order, Order]
- - array of two orders, which will be created as OCO orders
- parentOrder: OrderWithId
- - optional full parent order object with all order data
- Returns
- Promise<[string, string]>
OrderProvider.createOcoOrders(symbol: string, parentOrderId: string, orders: [Order, Order], parentOrder: OrderWithId): Promise<[string, string]>
Creates new OCO orders We expect creating "ids" for the orders inside this method TODO: right now OCO orders is not fully supported by chart, so instead of orders will be passed [undefined, undefined]
- OrderProvider.updateOrder
- Parameters
- symbol: string
- order: OrderWithId
- previousOrder: OrderWithId
- - optional previous order object before update (for tracking changes)
- Returns
- Promise<void>
OrderProvider.updateOrder(symbol: string, order: OrderWithId, previousOrder: OrderWithId): Promise<void>
Updates single order
- OrderProvider.deleteOrder
- Parameters
- symbol: string
- order: OrderWithId
- Returns
- Promise<void>
OrderProvider.deleteOrder(symbol: string, order: OrderWithId): Promise<void>
Deletes single order Please, take a look at "protectionOrdersIds" field: if order with type "original" order was deleted, protection orders should be deleted too.