Position Provider
PositionProvider connects trading positions with DXcharts application, enabling to display and manage positions directly on the chart.
What it does
The Position Provider allows you to:
- Display open positions on the chart
- Show profit/loss for each position
- Close positions from the chart UI
- Close positions with OCO protection orders
- Track position changes in real-time
Implementation Examples
Simple Mock Provider
This is a minimal stub implementation that satisfies the interface but doesn't store any data. Perfect for initial setup or testing:
import { PositionProvider } from '@dx-private/dxchart5-react/dist/providers/trading/position.provider';export const createMockPositionProvider = (): PositionProvider => {return {closePosition() {return Promise.resolve();},closePositionWithOcoOrders() {return Promise.resolve(['', '']);},observePositions() {return () => {};},};};
Example 2: Demo Provider
This implementation is used in our demo application but is not production-ready. It relies on InternalChartDataProvider interface 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 advanced features like:
- Real-time P&L updates based on live market data
- Integration with order provider for protection orders
- Automatic position tracking from executed orders
- RxJS-based reactive architecture
Important: This provider has a circular dependency with the order 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 { share, tap } from 'rxjs/operators';import { createIdGenerator, nextId } from '@dx-private/dxchart5-react/dist/utils/id-generator.utils';import { toCandles } from '@dx-private/dxchart5-react/dist/chart/model/chart.model';import { InternalChartDataProvider } from '@dx-private/dxchart5-react/dist/providers/chart-data-provider';import { pipe } from 'fp-ts/function';import { PositionProvider } from '@dx-private/dxchart5-react/dist/providers/trading/position.provider';import { Position } from '@dx-private/dxchart5-react/dist/chart/model/trading/position.model';import { Order, ProtectionOrderType } from '@dx-private/dxchart5-react/dist/chart/model/trading/order.model';import { InternalOrdersProvider } from './demo-trading-orders-provider';export interface InternalPositionProvider extends PositionProvider {positionAddedSubject: Subject<Record<Instrument['symbol'], Position>>;addPosition(symbol: string, order: Order, instrumentPrice: number): string | undefined;linkProtectionOrderToPosition(symbol: string,protectionOrderId: string,positionId: string,type: ProtectionOrderType,): void;unlinkProtectionOrderFromPosition(symbol: string, positionId: string, type: ProtectionOrderType): void;}export const createTradingPositionProvider = (chartDataProviderProxy: InternalChartDataProvider,getOrdersProvider: () => Promise<InternalOrdersProvider>,): InternalPositionProvider => {const generatorId = createIdGenerator('positionOMS__');const positions: Record<Instrument['symbol'], Record<Position['id'], Position>> = {};const currentPositionsBySymbol: Record<Instrument['symbol'], ReplaySubject<Array<Position>>> = {};const positionAddedSubject = new Subject<Record<Instrument['symbol'], Position>>();const positionsBySymbol = (symbol: string): Array<Position> => {return Object.values(positions[symbol] ?? {}).filter(position => position !== null && position !== undefined);};const triggerPositions = (symbol: string) => {if (!currentPositionsBySymbol[symbol]) {currentPositionsBySymbol[symbol] = new ReplaySubject<Array<Position>>(1);}currentPositionsBySymbol[symbol].next(positionsBySymbol(symbol));};const linkProtectionOrderToPosition = (symbol: string,protectionOrderId: string,positionId: string,type: ProtectionOrderType,) => {if (!positions[symbol]) {positions[symbol] = {};}const position = positions[symbol][positionId];if (position) {if (position.protectionOrderIds) {position.protectionOrderIds[type === 'sl' ? 0 : 1] = protectionOrderId;} else {position.protectionOrderIds =type === 'sl' ? [protectionOrderId, undefined] : [undefined, protectionOrderId];}positions[symbol][positionId] = position;triggerPositions(symbol);}};const unlinkProtectionOrderFromPosition = (symbol: string, positionId: string, type: ProtectionOrderType) => {const position = positions[symbol][positionId];if (position && position.protectionOrderIds) {position.protectionOrderIds[type === 'sl' ? 0 : 1] = undefined;position.protectionOrderIds.every(id => id === undefined) && delete position.protectionOrderIds;}positions[symbol][positionId] = position;triggerPositions(symbol);};const updatePositionsOnInstrumentChangeEffect = chartDataProviderProxy.observeSymbolChanged().pipe(tap(symbol => {if (!positions[symbol]) {positions[symbol] = {};}triggerPositions(symbol);}),);const updatePositionsOnLastCandleEffect = chartDataProviderProxy.observeSymbolCandleUpdated().pipe(tap(symbolCandle => {const lastCandle = toCandles(symbolCandle.candle);const symbol = symbolCandle.symbol;if (lastCandle) {positionsBySymbol(symbol).forEach(p => {if (p) {p.pl = lastCandle.close - p.price;}});triggerPositions(symbol);}}),);const closePositionWithOcoOrders = (symbol: string,positionId: Position['id'],orders: [Order | undefined, Order | undefined],position?: Position,): Promise<[string, string]> => {console.log('close position with oco', symbol, positionId, orders, 'position data:', position);return Promise.resolve(['', '']);};const effects = pipe(merge(updatePositionsOnInstrumentChangeEffect, updatePositionsOnLastCandleEffect), share());effects.subscribe();return {closePositionWithOcoOrders,positionAddedSubject,linkProtectionOrderToPosition,unlinkProtectionOrderFromPosition,async closePosition(symbol: string, id: string): Promise<void> {if (!positions[symbol]) {positions[symbol] = {};}if (positions[symbol][id]) {const protectionOrderIds = positions[symbol][id].protectionOrderIds;// if proctection orders are linked, delete/close them tooif (protectionOrderIds) {const spId = protectionOrderIds[0];const tpId = protectionOrderIds[1];const ordersProvider = await getOrdersProvider();spId && (await ordersProvider.deleteOrder(symbol, spId));tpId && (await ordersProvider.deleteOrder(symbol, tpId));}delete positions[symbol][id];triggerPositions(symbol);} else {console.warn(`Attempted to close non-existent position: ${symbol}`);}return Promise.resolve();},observePositions(symbol: string, callback: (data: Position[]) => void): () => void {if (!currentPositionsBySymbol[symbol]) {currentPositionsBySymbol[symbol] = new ReplaySubject<Array<Position>>(1);}const subscription = currentPositionsBySymbol[symbol].subscribe(callback);return () => {subscription.unsubscribe();};},addPosition(symbol: string, order: Order, instrumentPrice: number) {if (positions[symbol]) {const id = nextId(generatorId);const protectionIdsRest =order.type === 'original' && order.protectionOrderIds? { protectionOrderIds: order.protectionOrderIds }: {};const position: Position = {id: `${id}`,quantity: order.quantity,price: instrumentPrice,side: order.side,pl: instrumentPrice,type: 'original',...protectionIdsRest,};positions[symbol][`${id}`] = position;positionAddedSubject.next({ [symbol]: position });triggerPositions(symbol);return id;}return undefined;},};};
-
InternalChartDataProvider Integration:
observeSymbolChanged()- Initializes position storage when switching instrumentsobserveSymbolCandleUpdated()- Updates P&L in real-time as prices change
-
Reactive Effects with RxJS:
- Creates position storage for each symbol
- Continuously recalculates P&L based on current market price vs. entry price
-
Protection Orders Management:
linkProtectionOrderToPosition()- Links Stop Loss/Take Profit orders to positionsunlinkProtectionOrderFromPosition()- Removes links when protection orders are deleted- When closing a position, automatically deletes associated protection orders via the order provider
-
Position Creation from Orders:
addPosition()- Called by the order provider when orders are executed- Automatically transfers protection order IDs from the executed order to the new position
- Emits to
positionAddedSubjectso the order provider can track executed orders
-
Circular Dependency with Order Provider:
- Position provider needs order provider to delete protection orders when positions close
- Order provider needs position provider to create positions from executed orders
- Resolved using
getOrdersProvider()function that returns a Promise (lazy loading)
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();
<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
observePositions- Subscribe to position updates. The chart will call this method to receive the current list of positions and updates.closePosition- Called when a user closes a position from the chart UI.closePositionWithOcoOrders- Close a position and simultaneously create OCO protection orders.
Position Data
Each position should include:
- Entry price
- Current quantity
- Side (buy/sell)
- Profit/Loss information
- Optional protection order IDs
Positions are displayed as horizontal lines on the chart at the entry price, with P&L information updated in real-time.
Creating positions from executed orders
In the demo provider, the addPosition() method is called by the order provider when orders are executed. Here's how position averaging works if you want to implement it:
// Example: Position averaging logic for the simple mock provider
const createOrUpdatePositionFromOrder = (
symbol: string,
order: OrderWithId,
executionPrice: number
) => {
const positions = getPositionsForSymbol(symbol);
// Check if we already have a position for this symbol and side
const existingPosition = Array.from(positions.values())
.find(p => p.side === order.side);
if (existingPosition) {
// Update existing position (average the entry price)
const totalQuantity = existingPosition.quantity + order.quantity;
const avgPrice = (
(existingPosition.price * existingPosition.quantity) +
(executionPrice * order.quantity)
) / totalQuantity;
existingPosition.quantity = totalQuantity;
existingPosition.price = avgPrice;
positions.set(existingPosition.id, existingPosition);
} else {
// Create new position
const newPosition: Position = {
id: `position_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
type: 'original',
side: order.side,
quantity: order.quantity,
price: executionPrice,
pl: 0, // Will be updated with market price
};
positions.set(newPosition.id, newPosition);
}
notifySubscribers(symbol);
};
Note: The demo provider creates a new position for each executed order (no averaging). If you need position averaging, add the logic above to the addPosition() method.
Protection Orders
Positions can have protection orders (Stop Loss and Take Profit). The position maintains a protectionOrderIds array to track them.
Simple Mock Provider Approach
In the simple mock provider, you'll need to manually handle protection orders:
async closePosition(symbol, id, position) {
const positions = getPositionsForSymbol(symbol);
// If you have a backend API:
await fetch(`https://your-api.com/positions/${id}`, {
method: 'DELETE',
body: JSON.stringify({ symbol, position }),
});
// Update local state
positions.delete(id);
notifySubscribers(symbol);
}
Your backend should handle:
- Closing the position in your trading system
- Canceling any associated protection orders (Stop Loss/Take Profit)
- Returning the updated state
Demo Provider Approach
The demo provider automatically handles protection orders through integration with the order provider:
async closePosition(symbol: string, id: string): Promise<void> {
if (positions[symbol] && positions[symbol][id]) {
const protectionOrderIds = positions[symbol][id].protectionOrderIds;
// Automatically delete associated protection orders
if (protectionOrderIds) {
const slId = protectionOrderIds[0]; // Stop Loss
const tpId = protectionOrderIds[1]; // Take Profit
const ordersProvider = await getOrdersProvider();
if (slId) await ordersProvider.deleteOrder(symbol, slId);
if (tpId) await ordersProvider.deleteOrder(symbol, tpId);
}
delete positions[symbol][id];
triggerPositions(symbol);
}
}
The demo provider also provides helper methods to link/unlink protection orders:
linkProtectionOrderToPosition()- Called when creating a protection order for a positionunlinkProtectionOrderFromPosition()- Called when deleting a protection order
These methods ensure that the protectionOrderIds array stays in sync with the actual orders.
See Trading for more details on protection orders and OCO orders.
API reference
PositionProvider
Manages trading positions on the chart
- PositionProvider.observePositions
- Parameters
- symbol: string
- dataCallback: (positions: Position[]) => void
- Returns
- () => void
PositionProvider.observePositions(symbol: string, dataCallback: (positions: Position[]) => void): () => void
Observes the positions updates. We expect to be the full list of positions - no partial updates, for now the full list will be replaced.
- PositionProvider.closePosition
- Parameters
- symbol: string
- - current instrument's symbol
- id: string
- - id of the position to close
- position: Position
- - optional full position object with all position data
- Returns
- Promise<void>
PositionProvider.closePosition(symbol: string, id: string, position: Position): Promise<void>
Closes position
- PositionProvider.closePositionWithOcoOrders
- Parameters
- symbol: string
- - current instrument's symbol
- parentPositionId: string
- - id of the position from which OCO orders will be created
- orders: [Order, Order]
- - array of two orders, which will be created as OCO orders
- position: Position
- - optional full position object with all position data
- Returns
- Promise<[string, string]>
PositionProvider.closePositionWithOcoOrders(symbol: string, parentPositionId: string, orders: [Order, Order], position: Position): Promise<[string, string]>
Closes position with 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]