Skip to content

MultiModePositionTracker

Position tracking with configurable aggregation: net, per-side (hedging), or grouped (per-order).

#include "lrvx/position/multi_mode_position_tracker.h"

Aggregation Modes

Mode Behavior
NET Single position per symbol. BUY adds, SELL deducts. Automatic flip through zero.
PER_SIDE Separate long and short positions. Intent via reduceOnly/closePosition flags or explicit API.
GROUPED Each order creates an individual position. Contingent orders (TP/SL/OCO) grouped by orderTag.

Explicit Intent API

MultiModePositionTracker tracker{1, PositionAggregationMode::PER_SIDE};

tracker.openLong(symbol, price, qty);
tracker.closeLong(symbol, price, qty);
tracker.openShort(symbol, price, qty);
tracker.closeShort(symbol, price, qty);

// With tag for grouped mode:
tracker.openLong(symbol, price, qty, /*tag=*/42);
tracker.closeLong(symbol, price, qty, /*tag=*/42);

Works in all modes. In NET mode, openLong/openShort both aggregate into the net position.

In GROUPED mode the tag scopes the close. A close carrying a tag unwinds the positions in that tag's group, oldest first. A close with no tag, which is the default and what Strategy::emitClosePosition sends, unwinds every open position on the symbol, oldest first. An untagged close used to be a silent no-op: the position stayed open, its realized PnL was lost, and subscribers were still told the position had changed.

Snapshot

Atomic read of all position fields in a single lock acquisition:

auto snap = tracker.snapshot(symbol);
snap.longQty;       // long side quantity
snap.shortQty;      // short side quantity
snap.longAvgEntry;  // long side average entry price
snap.shortAvgEntry; // short side average entry price
snap.realizedPnl;   // accumulated realized PnL
snap.netQty();      // longQty - shortQty
snap.unrealizedPnl(currentPrice);  // mark-to-market

Net entry price

std::optional<Price> getAverageEntryPrice(SymbolId symbol) const override;
lrvx::PositionSnapshot positionSnapshot(SymbolId symbol) const override;

The IPositionManager overrides the strategy context reads. Empty when the symbol is flat. In PER_SIDE mode a book that is long and short at once has no single entry price, so the two sides are blended by quantity the same way the net position is.

positionSnapshot() returns the net position and that blended entry price under one acquisition of the mutex; it is what Strategy::refreshPosition() calls on every tick. Note the two snapshots are different things: snapshot() above is this tracker's own per-side breakdown, while lrvx::PositionSnapshot is the interface's {position, avgEntryPrice} pair.

Position Change Callback

tracker.onPositionChange([](SymbolId sym, const auto& snap) {
    log("Position changed: {} net={}", sym, snap.netQty().toDouble());
});

Fires after a fill that moves the position. A fill that moves nothing, such as a reduce-only order against no position, does not call it; subscribers used to be told the position had changed and handed back the snapshot they already had.

Exchange Integration

Receives fills via IOrderExecutionListener:

// Subscribe to OrderExecutionBus
bus.subscribe(&tracker);

// For multiple trackers, use MultiExecutionListener
MultiExecutionListener multi{0};
multi.addListener(&netTracker);
multi.addListener(&perSideTracker);
bus.subscribe(&multi);

onOrderFilled is called once with the full order quantity for complete fills. onOrderPartiallyFilled is called for each partial fill. Do not call both for the same fill.

Reconciliation

PositionReconciler reconciler;
auto mismatches = tracker.reconcile(reconciler, exchangePositions);

Holds the lock for the entire operation (atomic across all symbols).

Reset

tracker.reset();  // Clear all positions and PnL

Thread Safety

  • All public methods are mutex-protected
  • lockedGroups() returns a proxy that holds the lock:
auto positions = tracker.lockedGroups()->getOpenPositions(symbol);
  • groups() provides raw access without locking (caller must ensure safety)

Cost Basis

Inherits FIFO/LIFO/AVERAGE from PositionTracker. All PnL computed in fixed-point arithmetic (no float conversion).

See Also