position-tracer
position-tracer is a tool for dumping Ember positions in CSV format. Given a projection path, it reads the Ember journal and shows how a position changes after each trade or position reset/snapshot message.
Starting from 1.14.257 this tool is available via Ember Monitor (You can right click on any row in Positions panel and run "Download Trace" command).
Like most Ember tools, position-tracer relies on the EMBER_HOME and EMBER_WORK environment variables to locate Ember configuration and journal files.
Ember uses the average cost method for position calculation.
The first output line contains the table header, with the following fields:
- Timestamp — timestamp of the trade, position reset message or position snapshot message
- Action — either
BUY\SELL,RESET(meansPositionResetRequest; zeros position) orSNAPSHOT(meansPositionSnapshot; sets the position to an arbitrary size) - TradeSize — fill size of the trade reported on this line
- TradePrice — fill price of the trade reported on this line
- PositionQty — resulting position size after this trade
- PositionAvgCost — resulting average position cost after this trade
- PositionPnL — resulting profit or loss after this trade
In case of a position reset, TradeSize and TradePrice are omitted.
The tool takes instrument multipliers into account.
Starting from 1.15.29 position tracer supports position calculation for inverse contracts. Average cost of inverse contracts is computed correctly starting from 1.14.276 (see Inverse Contracts).
Usage
/opt/deltix/ember/bin/position-tracer --projectionPath <path> [--limit <n>] [--output <file>] [--skipNotCounted <bool>] [--format table|csv]
Arguments:
--projectionPath(required) — projection path identifying the position to trace. Must contain aSymbolprojection key and must not containRootSymbol. Example:Source[BOT42]/Destination[COINBASED]/Symbol[BTCZ25-M].--limit(optional, default10000) — maximum number of history entries kept in memory. Must be a positive integer.--output(optional) — path to the output file. If omitted, the report is printed to standard output.--skipNotCounted(optional, defaulttrue) — if set tofalse, then position tracer will show all trades in the history which aren't counted toward position (they're marked with*in the Action column).--format(optional, defaulttable) — output format:tablefor the bordered ASCII table shown below, orcsvfor a single comma-separated table (header row followed by one row per history entry, all fields double-quoted).
By default, the instrument's multipliers are loaded from TimeBase. To load it from the journal instead, set the ENV property instruments.from.journal=true.
The example below shows how the position of the BOT42 algorithm on COINBASE derivatives changed for the BTCZ25-M instrument:
/opt/deltix/ember/bin/position-tracer --projectionPath Source[BOT42]/Destination[COINBASED]/Symbol[BTCZ25-M]
2026-05-20 13:55:19.127 INFO [main] Starting loading instrument snapshot from journal
2026-05-20 13:55:19.148 INFO [main] Extracting list of instruments from journal
2026-05-20 13:55:25.860 INFO [main] Loaded 107 instruments from journal
2026-05-20 13:55:25.861 INFO [main] Loaded instrument snapshot with 107 instruments.
=== End of Journal ===
+--------------------------+--------+------------+------------+-------------+-------------------+--------------------+
| Timestamp | Action | TradeSize | TradePrice | PositionQty | PositionAvgCost | PositionPnL |
+--------------------------+--------+------------+------------+-------------+-------------------+--------------------+
| 2024-05-29T08:51:13.438Z | BUY | 0.0001 | 67890.22 | 0.0001 | 67890.22 | 0 |
| 2024-05-29T08:51:22.597Z | SELL | 0.0001 | 67873.99 | 0 | 0 | -0.001623 |
| 2024-05-30T14:18:53.933Z | BUY | 0.0001 | 68677.9 | 0.0001 | 68677.9 | -0.001623 |
| 2024-05-30T14:19:02.535Z | SELL | 0.0001 | 68665.34 | 0 | 0 | -0.002879 |
| 2024-06-05T16:03:16.700Z | BUY | 0.01 | 71684.04 | 0.01 | 71684.04 | -0.002879 |
| 2024-06-05T16:03:25.544Z | SELL | 0.00279051 | 71658.66 | 0.00720949 | 71684.04 | -0.0737021438 |
| 2024-06-05T16:03:25.544Z | SELL | 0.00720949 | 71658.63 | 0 | 0 | -0.2568952847 |
| 2024-06-25T12:29:40.282Z | BUY | 0.00000338 | 61221.32 | 0.00000338 | 61221.32 | -0.2568952847 |
| 2024-06-25T12:29:40.283Z | BUY | 0.03671707 | 61234.87 | 0.03672045 | 61234.86875276583 | -0.2568952847 |
| 2024-06-25T12:29:40.283Z | BUY | 0.09523323 | 61240.06 | 0.13195368 | 61238.6153647727 | -0.2568952847 |
| 2024-06-25T12:29:40.283Z | BUY | 0.02449376 | 61240.07 | 0.15644744 | 61238.84310570694 | -0.2568952847 |
| 2024-06-25T12:29:40.284Z | BUY | 0.04355256 | 61242.3 | 0.2 | 61239.5958886875 | -0.2568952847 |
| 2024-06-25T12:30:36.107Z | BUY | 0.00014595 | 61224.77 | 0.20014595 | 61239.58507738478 | -0.2568952847 |
| 2024-06-25T12:30:36.107Z | BUY | 0.04985405 | 61224.78 | 0.25 | 61236.632705112 | -0.2568952847 |
...
CSV output (--format csv) for the same run looks like this:
"Timestamp","Action","TradeSize","TradePrice","PositionQty","PositionAvgCost","PositionPnL"
"2024-05-29T08:51:13.438Z","BUY","0.0001","67890.22","0.0001","67890.22","0"
"2024-05-29T08:51:22.597Z","SELL","0.0001","67873.99","0","0","-0.001623"
How Ember Computes Positions
Ember maintains positions by processing a stream of journal messages in the order they are written. Three message types directly affect position state:
OrderTradeReportEventmessages (normal trades) update the quantity and average cost using the fill price and size;PositionSnapshotmessages adjust position with user-provided values; andPositionResetRequestmessages zero out the position.
Trade messages appear as BUY or SELL in the output; PositionSnapshot messages manifest as SNAPSHOT, and PositionResetRequest messages manifest as RESET.
Position Manager currently does not handle trade corrections and cancellations (OrderTradeCorrectEvent / OrderTradeCancelEvent). Ask us for workarounds if necessary.
Because multi-level algorithmic orders generate one trade report at every level of the hierarchy, naively counting all of them would inflate the position. Ember avoids this double-counting by applying a projection-path–based filter.
A DMA (Direct Market Access) order is one that routes directly to a market via a trade connector — for example, orders sent to the CME or Binance connectors. An Ember algorithm can also be explicitly tagged as DMA, for instance when it implements a matching engine.
Consider an example: a TWAP execution algorithm routes sliced child orders to a Smart Order Router (SOR), which routes its children to trade connectors for EXCHANGE_A and EXCHANGE_B. When the EXCHANGE_A connector emits a trade event, it is republished as a separate trade message for the SOR order and then again on behalf of TWAP.
If Ember is configured to track per-account positions (using an Account/Symbol projection), only trades from EXCHANGE_A and EXCHANGE_B orders (tagged as DMA) will affect the per-account position.
However, in some cases you may want to track positions per algorithm — for example, per order source or destination. Going back to the example above, imagine tracking positions per execution algorithm using a Destination/Symbol projection. Ember recognizes this and counts every trade (DMA and non-DMA), bucketing each one by the destination of the order it belongs to (TWAP, SOR, EXCHANGE_A, or EXCHANGE_B).
In summary: a trade affects the position either because it belongs to a DMA order (when the projection path has no Source/Destination key), or because its Source/Destination matches the projection path key.
Inverse Contracts
Inverse contracts (instruments with inverseContract flag, for example the XBTUSD perpetual) have position size in quote currency (USD), price in USD per BTC, and P&L in base currency (BTC). Closing a position of size S opened at price E at price X gives:
P&L (BTC) = S * multiplier * (1/E - 1/X)
Average cost. Each fill buys size / price BTC, so the average cost is the total size divided by the total BTC amount (a harmonic mean of fill prices). It is the break-even price of the position. For example:
| Trade | BTC amount | Position size | Average cost |
|---|---|---|---|
| BUY $300 @ 3000 | 0.1 | $300 | 3000 |
| BUY $500 @ 5000 | 0.1 | $800 | 800 / 0.2 = 4000 |
| SELL $800 @ 2000 | $0 | realized P&L: -0.2 BTC |
A simple quantity-weighted average would give 4250 and a wrong P&L. Average cost is shown in USD per BTC, like trade prices. Ember versions before 1.14.276 used the quantity-weighted average for inverse contracts.
Market value and unrealized P&L are in BTC, the same as realized P&L (since 1.14.276; earlier versions reported market value as size × market price):
Market value (BTC) = S * multiplier / market price
Unrealized P&L (BTC) = S * multiplier * (1/average cost - 1/market price)
In the example above, before the SELL and with the market at 2000, the market value is 800 / 2000 = 0.4 BTC and unrealized P&L is 800 / 4000 - 0.4 = -0.2 BTC.
Combining P&L of several instruments. P&L of each instrument position stays in its own currency. MaxLoss and MaxDailyLoss convert P&L of every instrument position to the main currency (USD by default), add it up, and compare the total with the limit:
- P&L of an inverse contract is first converted from BTC to its quote currency (USD) using the current market price of the contract itself (since 1.14.276). For example, -0.2 BTC at market price 2000 is -$400.
- P&L in a quote currency other than the main currency is then converted at the current rate. For example, a loss of 100 EUR on Eurex futures counts as $110 when EUR/USD is 1.1. Ember versions up to 1.14.276 skipped this step and counted it as $100.
- If the market price of an inverse contract with non-zero P&L is unknown, or the rate between the quote currency and the main currency is unknown, total P&L cannot be computed and orders are rejected. This also applies when
maxLossIncludesUnrealizedPnLisfalse.
Current limitations:
- Quanto contracts. Contracts with P&L settled in a third currency (for example, an ETH/USD quanto future settled in BTC) are not supported. Ember computes their P&L as for a regular (linear) contract in the quote currency and ignores the settlement currency.
Position snapshots written by Ember versions before 1.14.276 (including snapshots created by journal compaction) keep the average cost computed by the old formula (quantity-weighted mean). Ember cannot recompute it, because a snapshot does not keep individual fills. This matters only for inverse positions that were open during the upgrade and were built from fills at different prices. Such a position can be corrected by sending a PositionSnapshot with the right average cost (for example, the entry price reported by the exchange); otherwise it becomes correct once the position is closed. Positions rebuilt from all trades in the journal are not affected. Realized P&L accumulated before the upgrade is not recomputed.