Skip to main content

OrderManager

OrderManager - Main facade for the order placement system. This is the primary interface that strategies use to place and manage orders. It coordinates all internal components and handles Bookmap API callbacks. USAGE:

{@code // In strategy initialization: OrderManagerConfig config = OrderManagerConfig.forES() .orderSize(1) .takeProfitTicks(16) .stopLossTicks(8) .cooldownMinutes(2) .logPrefix("[MyStrategy]") .build(); orderManager = new OrderManager(alias, api, config); // In signal detection: if (longSignal) { orderManager.tryPlaceBracketOrder(Direction.BUY); } // MUST delegate callbacks: @Override public void onOrderUpdated(OrderInfoUpdate update) { orderManager.onOrderUpdated(update); } @Override public void onOrderExecuted(ExecutionInfo exec) { orderManager.onOrderExecuted(exec); } }

OrderManager.java
package com.bookmap.ordermanagement.manager;

import com.bookmap.ordermanagement.core.*;
import com.bookmap.ordermanagement.model.*;
import com.bookmap.ordermanagement.util.SessionTimeUtils;

import velox.api.layer1.simplified.Api;
import velox.api.layer1.data.*;
import velox.api.layer1.common.Log;

import java.util.function.Consumer;

/**
* OrderManager - Main facade for the order placement system.
*
* This is the primary interface that strategies use to place and manage orders.
* It coordinates all internal components and handles Bookmap API callbacks.
*
* USAGE:
* <pre>{@code
* // In strategy initialization:
* OrderManagerConfig config = OrderManagerConfig.forES()
* .orderSize(1)
* .takeProfitTicks(16)
* .stopLossTicks(8)
* .cooldownMinutes(2)
* .logPrefix("[MyStrategy]")
* .build();
*
* orderManager = new OrderManager(alias, api, config);
*
* // In signal detection:
* if (longSignal) {
* orderManager.tryPlaceBracketOrder(Direction.BUY);
* }
*
* // MUST delegate callbacks:
* @Override
* public void onOrderUpdated(OrderInfoUpdate update) {
* orderManager.onOrderUpdated(update);
* }
*
* @Override
* public void onOrderExecuted(ExecutionInfo exec) {
* orderManager.onOrderExecuted(exec);
* }
* }</pre>
*/
public class OrderManager {

// =========================================================================
// Dependencies
// =========================================================================

private final String alias;
private final Api api;
private final OrderManagerConfig config;

// =========================================================================
// Internal Components
// =========================================================================

private final OrderRegistry registry;
private final PositionTracker position;
private final GuardSystem guards;
private final TradeStatistics statistics;

// =========================================================================
// State
// =========================================================================

private volatile long lastSessionCheckTime;
private volatile int pendingOrderCount; // Orders sent but not yet acknowledged

// =========================================================================
// Callbacks
// =========================================================================

private Consumer<OrderFamily> onFamilyOpened;
private Consumer<OrderFamily> onFamilyClosed;
private Consumer<OrderRecord> onOrderFilled;
private Consumer<GuardResult> onOrderBlocked;

// =========================================================================
// Constructor
// =========================================================================

/**
* Creates a new OrderManager.
*
* @param alias The instrument alias (e.g., "ESH5.CME")
* @param api The Bookmap API
* @param config Configuration settings
*/
public OrderManager(String alias, Api api, OrderManagerConfig config) {
// Validate inputs
if (alias == null || alias.isEmpty()) {
throw new IllegalArgumentException("alias cannot be null or empty");
}
if (api == null) {
throw new IllegalArgumentException("api cannot be null");
}
if (config == null) {
config = OrderManagerConfig.forES().build();
}
config.validate();

this.alias = alias;
this.api = api;
this.config = config;

// Initialize components
String logPrefix = config.getLogPrefix();
boolean logging = config.isLoggingEnabled();

this.registry = new OrderRegistry(logPrefix, logging);
this.position = new PositionTracker(logPrefix, logging, config.getTickSize());
this.guards = new GuardSystem(logPrefix, logging, config.getCooldownDuration());
this.statistics = new TradeStatistics();

this.lastSessionCheckTime = System.currentTimeMillis();
this.pendingOrderCount = 0;

if (logging) {
Log.info(String.format("%s OrderManager initialized: %s", logPrefix, config));
}
}

/**
* Creates OrderManager with default ES configuration.
*/
public OrderManager(String alias, Api api) {
this(alias, api, OrderManagerConfig.forES().build());
}

// =========================================================================
// Order Placement
// =========================================================================

/**
* Attempts to place a bracket order.
*
* Checks all guards first. If allowed, places entry order with TP and SL.
*
* @param direction BUY for long entry, SELL for short entry
* @return The family ID if order was placed, null if blocked
*/
public String tryPlaceBracketOrder(Direction direction) {
// Check for session reset if enabled
if (config.isAutoSessionReset()) {
checkSessionReset();
}

// Run guard checks
GuardResult guardResult = guards.canPlaceOrder(position, registry);

if (guardResult.isBlocked()) {
statistics.recordBlockedByGuard();

if (onOrderBlocked != null) {
onOrderBlocked.accept(guardResult);
}

return null;
}

// Create family
OrderFamily family = registry.createFamily(direction, config.getOrderSize());

// Build bracket order
SimpleOrderSendParametersBuilder builder = new SimpleOrderSendParametersBuilder(
alias,
direction.isBuy(),
config.getOrderSize()
);

builder.setDuration(OrderDuration.GTC);
builder.setTakeProfitOffset(config.getTakeProfitTicks());
builder.setStopLossOffset(config.getStopLossTicks());

// Track that we're expecting orders
pendingOrderCount = 3; // Entry + TP + SL

// Send order
api.sendOrder(builder.build());

statistics.recordOrderSent();

if (config.isLoggingEnabled()) {
Log.info(String.format("%s Bracket order SENT: %s %d, TP=%d, SL=%d, family=%s",
config.getLogPrefix(),
direction,
config.getOrderSize(),
config.getTakeProfitTicks(),
config.getStopLossTicks(),
family.getFamilyId()));
}

return family.getFamilyId();
}

/**
* Convenience method for placing a long order.
*/
public String tryPlaceLongOrder() {
return tryPlaceBracketOrder(Direction.BUY);
}

/**
* Convenience method for placing a short order.
*/
public String tryPlaceShortOrder() {
return tryPlaceBracketOrder(Direction.SELL);
}

// =========================================================================
// Callback Handlers - MUST BE CALLED BY STRATEGY
// =========================================================================

/**
* Handles order update callbacks from Bookmap.
*
* IMPORTANT: Your strategy MUST call this method!
*
* @param update The order update from Bookmap
*/
public void onOrderUpdated(OrderInfoUpdate update) {
String brokerId = update.orderId;

// Try to find existing order
OrderRecord order = registry.findByBrokerId(brokerId);

if (order == null) {
// New order - must be from our pending bracket
order = handleNewOrder(brokerId, update);
if (order == null) {
// Not our order
return;
}
}

// Update order state
OrderState newState = OrderState.fromBrokerStatus(update.status, update.filled, update.unfilled);

// Skip CANCELLED for entry that was already filled (normal Bookmap behavior)
if (newState == OrderState.CANCELLED && order.isEntry() && order.isFilled()) {
if (config.isLoggingEnabled()) {
Log.info(String.format("%s Ignoring CANCELLED for already-filled entry: %s",
config.getLogPrefix(), brokerId));
}
return;
}

registry.updateOrderState(brokerId, newState);

// Handle state transitions
if (newState.isTerminal()) {
handleTerminalState(order, newState);
}
}

/**
* Handles execution callbacks from Bookmap.
*
* IMPORTANT: Your strategy MUST call this method!
*
* @param execution The execution info from Bookmap
*/
public void onOrderExecuted(ExecutionInfo execution) {
String brokerId = execution.orderId;

// Find the order
OrderRecord order = registry.findByBrokerId(brokerId);
if (order == null) {
if (config.isLoggingEnabled()) {
Log.warn(String.format("%s Execution for unknown order: %s",
config.getLogPrefix(), brokerId));
}
return;
}

// Record the fill
double price = execution.price;
registry.recordFill(brokerId, execution.size, price);
statistics.recordFill();

// Update position based on order type
OrderFamily family = registry.getFamily(order.getFamilyId());
if (family == null) return;

if (order.isEntry()) {
// Entry fill - open position
position.openPosition(family.getEntryDirection(), execution.size, price);
family.onEntryFilled(price);

if (onFamilyOpened != null) {
onFamilyOpened.accept(family);
}
} else {
// Exit fill - close position
position.closePosition(price);

// Determine close reason by comparing exit price to entry price
// (more reliable than order type heuristic since Bookmap order varies)
CloseReason reason;
double entryPrice = family.getEntryPrice();
if (family.isLong()) {
// LONG: exit > entry = profit (TP), exit < entry = loss (SL)
reason = (price > entryPrice) ? CloseReason.TP_HIT : CloseReason.SL_HIT;
} else {
// SHORT: exit < entry = profit (TP), exit > entry = loss (SL)
reason = (price < entryPrice) ? CloseReason.TP_HIT : CloseReason.SL_HIT;
}
family.onClosed(reason, price);

// Trigger family completion from here since we now have the exit price
// The duplicate callback prevention in handleFamilyComplete() ensures
// the callback only fires once, whether triggered from here or onOrderUpdated()
handleFamilyComplete(family.getFamilyId());
}

if (onOrderFilled != null) {
onOrderFilled.accept(order);
}
}

/**
* Handles a new order that we don't have in our registry yet.
*/
private OrderRecord handleNewOrder(String brokerId, OrderInfoUpdate update) {
// Must have an active family expecting orders
OrderFamily family = registry.getActiveFamily();
if (family == null || pendingOrderCount <= 0) {
// Not our order
return null;
}

pendingOrderCount--;

// Determine order type based on direction
Direction orderDirection = Direction.fromBoolean(update.isBuy);
Direction entryDirection = family.getEntryDirection();

com.bookmap.ordermanagement.model.OrderType orderType;
if (orderDirection == entryDirection) {
// Same direction as entry - must be entry order
orderType = com.bookmap.ordermanagement.model.OrderType.ENTRY;
} else {
// Opposite direction - exit order (TP or SL)
// We can't reliably distinguish TP from SL at this point,
// so we use a simple heuristic: first exit = TP, second = SL
if (family.getTpBrokerId() == null) {
orderType = com.bookmap.ordermanagement.model.OrderType.TAKE_PROFIT;
} else {
orderType = com.bookmap.ordermanagement.model.OrderType.STOP_LOSS;
}
}

// Register the order
OrderRecord order = registry.registerOrder(
brokerId,
family.getFamilyId(),
orderType,
orderDirection,
config.getOrderSize()
);

return order;
}

/**
* Handles an order reaching terminal state.
*/
private void handleTerminalState(OrderRecord order, OrderState state) {
switch (state) {
case FILLED -> { /* Fill handling is done in onOrderExecuted */ }
case CANCELLED -> statistics.recordCancellation();
case REJECTED -> {
statistics.recordRejection();
if (config.isLoggingEnabled()) {
Log.warn(String.format("%s Order REJECTED: %s",
config.getLogPrefix(), order.getBrokerId()));
}
}
default -> { /* Other states handled elsewhere */ }
}

// Check if family is complete
String familyId = order.getFamilyId();
if (registry.isFamilyComplete(familyId)) {
handleFamilyComplete(familyId);
}
}

/**
* Handles a family completing (all orders terminal).
*/
private void handleFamilyComplete(String familyId) {
OrderFamily family = registry.getFamily(familyId);
if (family == null) return;

// CRITICAL: Don't fire callback until we have valid exit price
// Bookmap sends onOrderUpdated(FILLED) BEFORE onOrderExecuted(),
// so we must wait for execution to set the exit price
if (family.getExitPrice() == 0 && family.getEntryPrice() > 0) {
// Exit price not set yet - wait for onOrderExecuted() to call us again
if (config.isLoggingEnabled()) {
Log.debug(String.format("%s Deferring family completion - waiting for exit price: %s",
config.getLogPrefix(), familyId));
}
return;
}

// CRITICAL: Prevent duplicate callbacks using atomic flag check
// This method can be called multiple times (once per terminal order)
// but we only want to fire the callback ONCE
if (!family.markClosedCallbackFired()) {
// Callback already fired for this family, skip all processing
return;
}

// Make sure family is marked as closed
if (!family.isClosed()) {
CloseReason reason = registry.determineFamilyCloseReason(familyId);
family.onClosed(reason, family.getExitPrice());
}

// CRITICAL: Ensure position is closed to prevent orphaned positions
if (!position.isFlat()) {
double exitPrice = family.getExitPrice();
if (exitPrice == 0 && family.getEntryPrice() > 0) {
exitPrice = family.getEntryPrice(); // Use entry price if no exit recorded
}
if (config.isLoggingEnabled()) {
Log.info(String.format("%s Force closing position in handleFamilyComplete: exitPrice=%.2f",
config.getLogPrefix(), exitPrice));
}
position.closePosition(exitPrice);
}

// Record statistics
statistics.recordTrade(family, config.getTickSize());

// Start cooldown
guards.startCooldown();

// Clear active family
if (familyId.equals(registry.getActiveFamilyId())) {
registry.clearActiveFamily();
}

if (config.isLoggingEnabled()) {
Log.info(String.format("%s Family COMPLETE: %s | Reason: %s | P&L: %.1f ticks",
config.getLogPrefix(),
familyId,
family.getCloseReason(),
family.calculatePnLTicks(config.getTickSize())));
}

if (onFamilyClosed != null) {
onFamilyClosed.accept(family);
}
}

// =========================================================================
// Session Management
// =========================================================================

/**
* Checks if session should be reset (at 9:30 AM ET).
*/
public void checkSessionReset() {
long now = System.currentTimeMillis();

if (SessionTimeUtils.shouldResetSession(lastSessionCheckTime, now)) {
if (config.isLoggingEnabled()) {
Log.info(String.format("%s ═══════════════════════════════════════",
config.getLogPrefix()));
Log.info(String.format("%s SESSION RESET (9:30 AM ET)",
config.getLogPrefix()));
Log.info(String.format("%s ═══════════════════════════════════════",
config.getLogPrefix()));
}
reset();
}

lastSessionCheckTime = now;
}

/**
* Manually resets all state.
*/
public void reset() {
registry.clear();
position.reset();
guards.reset();
statistics.reset();
pendingOrderCount = 0;

if (config.isLoggingEnabled()) {
Log.info(String.format("%s OrderManager RESET complete", config.getLogPrefix()));
}
}

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

/**
* Checks if guards allow placing a new order.
*/
public GuardResult canPlaceOrder() {
return guards.canPlaceOrder(position, registry);
}

/**
* Returns true if ready to place a new order.
*/
public boolean isReadyForNewOrder() {
return guards.canPlaceOrder(position, registry).isAllowed();
}

/**
* Returns true if position is flat.
*/
public boolean isFlat() {
return position.isFlat();
}

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

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

/**
* Returns the current position size.
*/
public int getPosition() {
return position.getCurrentPosition();
}

/**
* Returns true if there are active orders.
*/
public boolean hasActiveOrders() {
return registry.hasActiveOrders();
}

/**
* Returns true if cooldown is active.
*/
public boolean isCooldownActive() {
return guards.isCooldownActive();
}

/**
* Returns remaining cooldown in seconds.
*/
public int getCooldownRemainingSeconds() {
return guards.getCooldownRemainingSeconds();
}

/**
* Returns the active family, or null if none.
*/
public OrderFamily getActiveFamily() {
return registry.getActiveFamily();
}

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

/**
* Returns the current session P&L in currency.
*/
public double getSessionPnLCurrency() {
return position.getSessionPnLCurrency(config.getTickValue());
}

// =========================================================================
// Component Access
// =========================================================================

/**
* Returns the trade statistics.
*/
public TradeStatistics getStatistics() {
return statistics;
}

/**
* Returns the configuration.
*/
public OrderManagerConfig getConfig() {
return config;
}

/**
* Returns the order registry.
*/
public OrderRegistry getRegistry() {
return registry;
}

/**
* Returns the position tracker.
*/
public PositionTracker getPositionTracker() {
return position;
}

/**
* Returns the guard system.
*/
public GuardSystem getGuardSystem() {
return guards;
}

// =========================================================================
// Callback Registration
// =========================================================================

/**
* Sets callback for when a family (position) is opened.
*/
public OrderManager onFamilyOpened(Consumer<OrderFamily> callback) {
this.onFamilyOpened = callback;
return this;
}

/**
* Sets callback for when a family (position) is closed.
*/
public OrderManager onFamilyClosed(Consumer<OrderFamily> callback) {
this.onFamilyClosed = callback;
return this;
}

/**
* Sets callback for when an order fills.
*/
public OrderManager onOrderFilled(Consumer<OrderRecord> callback) {
this.onOrderFilled = callback;
return this;
}

/**
* Sets callback for when an order is blocked by guards.
*/
public OrderManager onOrderBlocked(Consumer<GuardResult> callback) {
this.onOrderBlocked = callback;
return this;
}

// =========================================================================
// Status and Logging
// =========================================================================

/**
* Returns a status summary string.
*/
public String getStatusSummary() {
StringBuilder sb = new StringBuilder();
sb.append(position.toSummary());
sb.append(" | ");
sb.append(guards.getStatusSummary());
sb.append(" | Active Orders: ");
sb.append(registry.getActiveOrderCount());
return sb.toString();
}

/**
* Logs the current status.
*/
public void logStatus() {
Log.info(String.format("%s STATUS: %s", config.getLogPrefix(), getStatusSummary()));
}

/**
* Logs the statistics summary.
*/
public void logStatistics() {
Log.info(String.format("%s STATS: %s", config.getLogPrefix(), statistics.toSummary()));
}

/**
* Logs a detailed statistics report.
*/
public void logDetailedStatistics() {
for (String line : statistics.toDetailedReport().split("\n")) {
Log.info(config.getLogPrefix() + " " + line);
}
}
}