Skip to main content

PositionTracker

Tracks the current position and trade history. Position is derived from execution callbacks, not from order state. This ensures accurate position tracking regardless of API quirks.

PositionTracker.java
package com.bookmap.ordermanagement.core;

import com.bookmap.ordermanagement.model.Direction;
import velox.api.layer1.common.Log;

/**
* Tracks the current position and trade history.
*
* Position is derived from execution callbacks, not from order state.
* This ensures accurate position tracking regardless of API quirks.
*/
public class PositionTracker {

// Configuration
private final String logPrefix;
private final boolean loggingEnabled;

// Current position state
private volatile int currentPosition; // 0 = flat, +1 = long, -1 = short
private volatile double entryPrice;
private volatile long entryTime;
private volatile Direction lastTradeDirection;

// Session P&L
private volatile double sessionPnLTicks;
private final double tickSize;

/**
* Creates a new position tracker.
*
* @param logPrefix Prefix for log messages
* @param loggingEnabled Whether to enable detailed logging
* @param tickSize The tick size for P&L calculations (e.g., 0.25 for ES)
*/
public PositionTracker(String logPrefix, boolean loggingEnabled, double tickSize) {
this.logPrefix = logPrefix;
this.loggingEnabled = loggingEnabled;
this.tickSize = tickSize;
resetInternal();
}

// =========================================================================
// Position Queries
// =========================================================================

/**
* Returns the current position size.
* 0 = flat, positive = long, negative = short
*/
public int getCurrentPosition() {
return currentPosition;
}

/**
* Returns true if position is flat (no open position).
*/
public boolean isFlat() {
return currentPosition == 0;
}

/**
* Returns true if position is long.
*/
public boolean isLong() {
return currentPosition > 0;
}

/**
* Returns true if position is short.
*/
public boolean isShort() {
return currentPosition < 0;
}

/**
* Returns the entry price of current position, or 0 if flat.
*/
public double getEntryPrice() {
return entryPrice;
}

/**
* Returns the entry time of current position, or 0 if flat.
*/
public long getEntryTime() {
return entryTime;
}

/**
* Returns the direction of the last completed trade, or null if none.
*/
public Direction getLastTradeDirection() {
return lastTradeDirection;
}

/**
* Returns the session P&L in ticks.
*/
public double getSessionPnLTicks() {
return sessionPnLTicks;
}

/**
* Returns the session P&L in currency.
*
* @param tickValue Dollar value per tick (e.g., $12.50 for ES)
*/
public double getSessionPnLCurrency(double tickValue) {
return sessionPnLTicks * tickValue;
}

/**
* Returns the tick size.
*/
public double getTickSize() {
return tickSize;
}

// =========================================================================
// Position Updates
// =========================================================================

/**
* Opens a new position.
* Called when an entry order fills.
*
* @param direction BUY for long, SELL for short
* @param size Number of contracts
* @param price Entry price
*/
public void openPosition(Direction direction, int size, double price) {
this.currentPosition = direction.isBuy() ? size : -size;
this.entryPrice = price;
this.entryTime = System.currentTimeMillis();
this.lastTradeDirection = direction;

if (loggingEnabled) {
Log.info(String.format("%s Position OPENED: %s %d @ %.2f",
logPrefix, direction, size, price));
}
}

/**
* Closes the current position.
* Called when an exit order (TP or SL) fills.
*
* @param exitPrice The exit price
* @return The P&L in ticks
*/
public double closePosition(double exitPrice) {
if (isFlat()) {
Log.warn(String.format("%s Attempted to close but already flat", logPrefix));
return 0.0;
}

// Calculate P&L
double pnlPoints = exitPrice - entryPrice;
if (isShort()) {
pnlPoints = -pnlPoints; // Short profits when price goes down
}
double pnlTicks = pnlPoints / tickSize;

// Update session P&L
sessionPnLTicks += pnlTicks;

if (loggingEnabled) {
String posType = isLong() ? "LONG" : "SHORT";
Log.info(String.format("%s Position CLOSED: %s @ %.2f (entry: %.2f) | P&L: %.1f ticks | Session: %.1f ticks",
logPrefix, posType, exitPrice, entryPrice, pnlTicks, sessionPnLTicks));
}

// Reset position state
currentPosition = 0;
entryPrice = 0.0;
entryTime = 0;

return pnlTicks;
}

/**
* Adjusts position for partial fill.
*
* @param direction Direction of the fill
* @param size Size of the fill
* @param price Fill price
*/
public void adjustPosition(Direction direction, int size, double price) {
int adjustment = direction.isBuy() ? size : -size;
int newPosition = currentPosition + adjustment;

if (loggingEnabled) {
Log.info(String.format("%s Position adjusted: %d -> %d (fill: %s %d @ %.2f)",
logPrefix, currentPosition, newPosition, direction, size, price));
}

// Check if this closes the position
if (currentPosition != 0 && newPosition == 0) {
closePosition(price);
} else if (currentPosition == 0 && newPosition != 0) {
// Opening new position
openPosition(direction, size, price);
} else {
// Just updating position
currentPosition = newPosition;
}
}

// =========================================================================
// Reset
// =========================================================================

/**
* Resets all position state.
*/
public void reset() {
resetInternal();

if (loggingEnabled) {
Log.info(String.format("%s Position tracker reset", logPrefix));
}
}

/**
* Internal reset without logging (used by constructor).
*/
private void resetInternal() {
currentPosition = 0;
entryPrice = 0.0;
entryTime = 0;
lastTradeDirection = null;
sessionPnLTicks = 0.0;
}

/**
* Resets session P&L only (keeps position).
*/
public void resetSessionPnL() {
sessionPnLTicks = 0.0;
}

// =========================================================================
// Formatting
// =========================================================================

/**
* Returns a summary string.
*/
public String toSummary() {
if (isFlat()) {
return String.format("FLAT | Session P&L: %.1f ticks", sessionPnLTicks);
}
String posType = isLong() ? "LONG" : "SHORT";
return String.format("%s %d @ %.2f | Session P&L: %.1f ticks",
posType, Math.abs(currentPosition), entryPrice, sessionPnLTicks);
}

@Override
public String toString() {
return toSummary();
}
}