Skip to main content

OrderFamily

Represents a family of related orders (entry + TP + SL). A bracket order creates one family with three orders: - Entry order (opens position) - Take Profit order (closes at profit) - Stop Loss order (closes at loss) The family tracks the lifecycle of the complete trade.

OrderFamily.java
package com.bookmap.ordermanagement.model;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
* Represents a family of related orders (entry + TP + SL).
*
* A bracket order creates one family with three orders:
* - Entry order (opens position)
* - Take Profit order (closes at profit)
* - Stop Loss order (closes at loss)
*
* The family tracks the lifecycle of the complete trade.
*/
public class OrderFamily {

// Identity
private final String familyId;
private final Direction entryDirection;
private final int size;

// Order references (broker IDs)
private volatile String entryBrokerId;
private volatile String tpBrokerId;
private volatile String slBrokerId;

// State
private volatile FamilyState state;
private volatile CloseReason closeReason;

// Pricing
private volatile double entryPrice;
private volatile double exitPrice;

// Timestamps
private final long createdTime;
private volatile long entryFilledTime;
private volatile long closedTime;

// Callback tracking - prevents duplicate onFamilyClosed callbacks
private volatile boolean closedCallbackFired;

/**
* Creates a new order family.
*
* @param familyId Unique identifier for this family
* @param entryDirection BUY for long entry, SELL for short entry
* @param size Number of contracts
*/
public OrderFamily(String familyId, Direction entryDirection, int size) {
this.familyId = familyId;
this.entryDirection = entryDirection;
this.size = size;
this.state = FamilyState.PENDING;
this.closeReason = null;
this.createdTime = System.currentTimeMillis();
}

// =========================================================================
// Getters
// =========================================================================

public String getFamilyId() {
return familyId;
}

public Direction getEntryDirection() {
return entryDirection;
}

public Direction getExitDirection() {
return entryDirection.opposite();
}

public int getSize() {
return size;
}

public String getEntryBrokerId() {
return entryBrokerId;
}

public String getTpBrokerId() {
return tpBrokerId;
}

public String getSlBrokerId() {
return slBrokerId;
}

public FamilyState getState() {
return state;
}

public CloseReason getCloseReason() {
return closeReason;
}

public double getEntryPrice() {
return entryPrice;
}

public double getExitPrice() {
return exitPrice;
}

public long getCreatedTime() {
return createdTime;
}

public long getEntryFilledTime() {
return entryFilledTime;
}

public long getClosedTime() {
return closedTime;
}

// =========================================================================
// State Queries
// =========================================================================

public boolean isPending() {
return state == FamilyState.PENDING;
}

public boolean isActive() {
return state == FamilyState.ACTIVE;
}

public boolean isClosed() {
return state == FamilyState.CLOSED;
}

/**
* Returns true if the closed callback has already been fired.
* Used to prevent duplicate callbacks.
*/
public boolean isClosedCallbackFired() {
return closedCallbackFired;
}

/**
* Marks that the closed callback has been fired.
* Returns true if this is the first time (callback should proceed),
* false if already fired (callback should be skipped).
*/
public synchronized boolean markClosedCallbackFired() {
if (closedCallbackFired) {
return false; // Already fired, skip
}
closedCallbackFired = true;
return true; // First time, proceed
}

/**
* Returns true if this family represents a long position.
*/
public boolean isLong() {
return entryDirection == Direction.BUY;
}

/**
* Returns true if this family represents a short position.
*/
public boolean isShort() {
return entryDirection == Direction.SELL;
}

/**
* Returns all broker IDs associated with this family.
*/
public List<String> getAllBrokerIds() {
List<String> ids = new ArrayList<>(3);
if (entryBrokerId != null) ids.add(entryBrokerId);
if (tpBrokerId != null) ids.add(tpBrokerId);
if (slBrokerId != null) ids.add(slBrokerId);
return ids;
}

/**
* Determines the order type based on broker ID.
*/
public Optional<OrderType> getOrderType(String brokerId) {
if (brokerId == null) return Optional.empty();
if (brokerId.equals(entryBrokerId)) return Optional.of(OrderType.ENTRY);
if (brokerId.equals(tpBrokerId)) return Optional.of(OrderType.TAKE_PROFIT);
if (brokerId.equals(slBrokerId)) return Optional.of(OrderType.STOP_LOSS);
return Optional.empty();
}

// =========================================================================
// State Updates
// =========================================================================

/**
* Links the entry order broker ID.
*/
public void setEntryBrokerId(String brokerId) {
this.entryBrokerId = brokerId;
}

/**
* Links the take profit order broker ID.
*/
public void setTpBrokerId(String brokerId) {
this.tpBrokerId = brokerId;
}

/**
* Links the stop loss order broker ID.
*/
public void setSlBrokerId(String brokerId) {
this.slBrokerId = brokerId;
}

/**
* Called when entry order fills.
*
* @param price The entry fill price
*/
public void onEntryFilled(double price) {
this.entryPrice = price;
this.entryFilledTime = System.currentTimeMillis();
this.state = FamilyState.ACTIVE;
}

/**
* Called when the family closes (TP or SL fills, or cancelled).
*
* @param reason The reason for closing
* @param exitPrice The exit price (if applicable)
*/
public void onClosed(CloseReason reason, double exitPrice) {
this.closeReason = reason;
this.exitPrice = exitPrice;
this.closedTime = System.currentTimeMillis();
this.state = FamilyState.CLOSED;
}

// =========================================================================
// P&L Calculation
// =========================================================================

/**
* Calculates the profit/loss in price points (not ticks).
*
* @return P&L in price points, positive for profit, negative for loss
*/
public double calculatePnLPoints() {
if (!isClosed() || entryPrice == 0 || exitPrice == 0) {
return 0.0;
}

double pnl = exitPrice - entryPrice;

// For short positions, profit is reversed
if (isShort()) {
pnl = -pnl;
}

return pnl * size;
}

/**
* Calculates P&L in ticks.
*
* @param tickSize The tick size (e.g., 0.25 for ES)
* @return P&L in ticks
*/
public double calculatePnLTicks(double tickSize) {
if (tickSize <= 0) return 0.0;
return calculatePnLPoints() / tickSize;
}

/**
* Calculates P&L in currency.
*
* @param tickSize The tick size
* @param tickValue The dollar value per tick (e.g., $12.50 for ES)
* @return P&L in currency
*/
public double calculatePnLCurrency(double tickSize, double tickValue) {
return calculatePnLTicks(tickSize) * tickValue;
}

/**
* Returns the duration of the trade in milliseconds.
*/
public long getTradeDurationMs() {
if (entryFilledTime == 0) return 0;
long endTime = closedTime > 0 ? closedTime : System.currentTimeMillis();
return endTime - entryFilledTime;
}

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

@Override
public String toString() {
return String.format("Family[%s|%s|%s|entry=%.2f|exit=%.2f]",
familyId,
entryDirection,
state,
entryPrice,
exitPrice);
}

/**
* Returns a detailed multi-line description.
*/
public String toDetailedString() {
return String.format(
"OrderFamily {\n" +
" familyId: %s\n" +
" direction: %s\n" +
" size: %d\n" +
" state: %s\n" +
" entryBrokerId: %s\n" +
" tpBrokerId: %s\n" +
" slBrokerId: %s\n" +
" entryPrice: %.2f\n" +
" exitPrice: %.2f\n" +
" closeReason: %s\n" +
" pnlPoints: %.4f\n" +
"}",
familyId, entryDirection, size, state,
entryBrokerId, tpBrokerId, slBrokerId,
entryPrice, exitPrice, closeReason,
calculatePnLPoints());
}
}