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();
}
}