Skip to main content

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​

:::caution Use at Your Own Risk

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 too
if (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;
},
};
};
  1. InternalChartDataProvider Integration:

    • observeSymbolChanged() - Initializes position storage when switching instruments
    • observeSymbolCandleUpdated() - Updates P&L in real-time as prices change
  2. Reactive Effects with RxJS:

    • Creates position storage for each symbol
    • Continuously recalculates P&L based on current market price vs. entry price
  3. Protection Orders Management:

    • linkProtectionOrderToPosition() - Links Stop Loss/Take Profit orders to positions
    • unlinkProtectionOrderFromPosition() - Removes links when protection orders are deleted
    • When closing a position, automatically deletes associated protection orders via the order provider
  4. 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 positionAddedSubject so the order provider can track executed orders
  5. 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:

  1. Closing the position in your trading system
  2. Canceling any associated protection orders (Stop Loss/Take Profit)
  3. 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 position
  • unlinkProtectionOrderFromPosition() - 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
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.

Parameters
symbol: string
dataCallback: (positions: Position[]) => void
Returns
() => void
PositionProvider.closePosition
PositionProvider.closePosition(symbol: string, id: string, position: Position): Promise<void>

Closes position

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.closePositionWithOcoOrders
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]

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]>