Skip to main content

Bookmap Order Placement Best Practices

Overview​

This document describes the proven order placement pattern for Bookmap trading strategies, specifically addressing the clientId=null problem with bracket orders and ensuring single-position enforcement.

The Problem​

When placing bracket orders (entry + TP + SL) in Bookmap, the following issues occur:

  1. clientId=null on bracket orders: In simulated trading mode, TP and SL orders return clientId=null in OrderInfoUpdate callbacks, making it impossible to correlate them with the entry order using clientId.

  2. Position stacking: Without proper guards, a strategy can fire multiple signals while a position is open, causing overlapping positions.

  3. Same-direction re-entry: After a position closes, the strategy might immediately re-enter in the same direction, causing unintended consecutive trades.

The Solution: OrderManager Pattern​

Core Principles​

  1. Track by brokerId, not clientId: The orderId field (brokerId) is always available, even when clientId is null.

  2. Use status.isActive(): This method returns true for all active states (WORKING, PENDING_SUBMIT, etc.) without needing to enumerate them.

  3. Track unfilled quantity: Allows proper handling of partial fills.

  4. Enforce alternating directions: Prevent same-side re-entry using a simple direction tracker.

Key Data Structures​

/**
* Map of orderId (brokerId) -> unfilled quantity
* - Active orders are in this map
* - Terminal orders (FILLED, CANCELLED, REJECTED) are removed
*/
private final Map<String, Integer> activeOrdersUnfilled = new ConcurrentHashMap<>();

/**
* Simple flag: true if ANY orders are active
* Derived from: !activeOrdersUnfilled.isEmpty()
*/
private volatile boolean isOrderOpen = false;

/**
* Direction of last trade: null=first trade, true=long, false=short
* Only updated when a trade is actually placed
*/
private volatile Boolean lastTradeWasLong = null;

Order Lifecycle​

1. Signal detected Ò†’ check canPlaceOrder(direction)
- isOrderOpen must be false
- direction must be opposite of lastTradeWasLong (or first trade)

2. Place order Ò†’ update lastTradeWasLong

3. onOrderUpdated callback:
- If status.isActive() Ò†’ add to activeOrdersUnfilled
- If terminal status Ò†’ remove from activeOrdersUnfilled
- Update isOrderOpen = !activeOrdersUnfilled.isEmpty()

4. When isOrderOpen transitions false Ò†’ position closed, ready for next signal

Visual Flow​

Ò”ŒÒ”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”
Γ’β€β€š ORDER LIFECYCLE Γ’β€β€š
Ò”œÒ”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€
Γ’β€β€š Γ’β€β€š
Γ’β€β€š SIGNAL DETECTED Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Ò–¼ Γ’β€β€š
Γ’β€β€š Ò”ŒÒ”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò” Γ’β€β€š
Γ’β€β€š Γ’β€β€š canPlaceOrder() Γ’β€β€šΓ’β€β‚¬Γ’β€β‚¬NOÒ”€Ò”€Ò–º Signal blocked Γ’β€β€š
Γ’β€β€š Γ’β€β€Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Β¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Λœ Γ’β€β€š
Γ’β€β€š Γ’β€β€š YES Γ’β€β€š
Γ’β€β€š Ò–¼ Γ’β€β€š
Γ’β€β€š Ò”ŒÒ”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò” Γ’β€β€š
Γ’β€β€š Γ’β€β€š placeBracketOrder() Γ’β€β€š
Γ’β€β€š Γ’β€β€š - Update lastTradeWasLong Γ’β€β€š
Γ’β€β€š Γ’β€β€š - Send order via API Γ’β€β€š
Γ’β€β€š Γ’β€β€Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Β¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Λœ Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Ò–¼ Γ’β€β€š
Γ’β€β€š Ò”ŒÒ”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò” Γ’β€β€š
Γ’β€β€š Γ’β€β€š ORDER CALLBACKS Γ’β€β€š Γ’β€β€š
Γ’β€β€š Ò”œÒ”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š onOrderUpdated(entry, WORKING) Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Ò–¼ Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š activeOrdersUnfilled["0"] = 1 Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š isOrderOpen = true Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Ò–¼ Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š onOrderUpdated(entry, FILLED) Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Ò–¼ Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š remove("0") Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Ò–¼ Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š onOrderUpdated(SL, WORKING) Ò† clientId=null! Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Ò–¼ Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š activeOrdersUnfilled["1"] = 1 (tracked by brokerId) Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Ò–¼ Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š onOrderUpdated(TP, WORKING) Ò† clientId=null! Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Ò–¼ Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š activeOrdersUnfilled["2"] = 1 (tracked by brokerId) Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š isOrderOpen = true (map not empty) Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Ò–¼ Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š [Price hits SL] Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Ò–¼ Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š onOrderUpdated(SL, FILLED) Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Ò–¼ Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š remove("1") Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Ò–¼ Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š onOrderUpdated(TP, CANCELLED) (OCO behavior) Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Ò–¼ Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š remove("2") Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š isOrderOpen = false (map empty!) Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€ΒΌΓ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Λœ Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Ò–¼ Γ’β€β€š
Γ’β€β€š READY FOR OPPOSITE DIRECTION SIGNAL Γ’β€β€š
Γ’β€β€š Γ’β€β€š
Γ’β€β€Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Λœ

Implementation​

Minimal Implementation (Copy-Paste Ready)​

// Instance variables
private final Map<String, Integer> activeOrdersUnfilled = new ConcurrentHashMap<>();
private volatile boolean isOrderOpen = false;
private volatile Boolean lastTradeWasLong = null;

// Check if can place order
private boolean canPlaceOrder(boolean isBuy) {
if (isOrderOpen) return false;
if (lastTradeWasLong == null) return true; // First trade OK
return lastTradeWasLong != isBuy; // Must be opposite direction
}

// Place bracket order
private void placeOrder(boolean isBuy) {
if (!canPlaceOrder(isBuy)) {
Log.info("Order blocked");
return;
}

lastTradeWasLong = isBuy; // Update BEFORE sending

SimpleOrderSendParametersBuilder builder =
new SimpleOrderSendParametersBuilder(alias, isBuy, orderSize);
builder.setDuration(OrderDuration.GTC);
builder.setTakeProfitOffset(takeProfitTicks);
builder.setStopLossOffset(stopLossTicks);

api.sendOrder(builder.build());
}

// Handle order updates
@Override
public void onOrderUpdated(OrderInfoUpdate update) {
String brokerId = update.orderId;

if (update.status.isActive()) {
activeOrdersUnfilled.put(brokerId, update.unfilled);
} else {
activeOrdersUnfilled.remove(brokerId);
}

isOrderOpen = !activeOrdersUnfilled.isEmpty();
}

// Handle executions
@Override
public void onOrderExecuted(ExecutionInfo execution) {
String brokerId = execution.orderId;

Integer unfilled = activeOrdersUnfilled.get(brokerId);
if (unfilled != null) {
int newUnfilled = unfilled - execution.size;
if (newUnfilled <= 0) {
activeOrdersUnfilled.remove(brokerId);
} else {
activeOrdersUnfilled.put(brokerId, newUnfilled);
}
}

isOrderOpen = !activeOrdersUnfilled.isEmpty();
}

// Reset on session change
private void resetOrderState() {
activeOrdersUnfilled.clear();
isOrderOpen = false;
lastTradeWasLong = null;
}

Using OrderManager Utility Class​

public class MyStrategy implements CustomModule, TradeDataListener, OrdersListener {

private OrderManager orderManager;

@Override
public void initialize(String alias, InstrumentInfo info, Api api, InitialState state) {
orderManager = new OrderManager(alias, api)
.withOrderSize(1)
.withTakeProfit(16)
.withStopLoss(8)
.withAlternatingDirections(true)
.withLogging(true)
.withLogPrefix("[MyStrategy]")
.onPositionClosed(reason -> Log.info("Closed: " + reason));
}

@Override
public void onTrade(double price, int size, TradeInfo tradeInfo) {
// Your signal logic here
if (longSignal) {
orderManager.tryPlaceBracketOrder(true);
} else if (shortSignal) {
orderManager.tryPlaceBracketOrder(false);
}
}

// MUST delegate these callbacks!
@Override
public void onOrderUpdated(OrderInfoUpdate update) {
orderManager.onOrderUpdated(update);
}

@Override
public void onOrderExecuted(ExecutionInfo execution) {
orderManager.onOrderExecuted(execution);
}

@Override
public void stop() {
Log.info("Stats: " + orderManager.getStatisticsSummary());
}
}

Verification Checklist​

When testing your strategy, verify these conditions:

CheckExpectedHow to Verify
Max Size1 (or configured size)Trading Statistics Ò†’ Max Size column
No overlapping positionsSequential entry timesTrading Statistics Ò†’ Entry Time column
Alternating directionsBUY/SELL patternTrading Statistics Ò†’ Type column
Bracket orders workClean exitsTP/SL lines visible, trades exit
No orphaned orders0Trading Statistics Ò†’ Orders Cancelled
State resetsTrades after exitMultiple trades in session

Common Pitfalls​

1. Forgetting to delegate callbacks​

// WRONG - OrderManager never sees updates!
@Override
public void onOrderUpdated(OrderInfoUpdate update) {
// Custom logic only
}

// CORRECT - Always delegate first
@Override
public void onOrderUpdated(OrderInfoUpdate update) {
orderManager.onOrderUpdated(update); // MUST call this!
// Then custom logic
}

2. Updating direction tracker on every tick​

// WRONG - Updates constantly, breaks cross detection
if (price > vwap) {
lastTradeWasLong = true; // Updates even without trading!
}

// CORRECT - Only update when actually placing a trade
if (longSignal && canPlaceOrder(true)) {
lastTradeWasLong = true; // Only here!
placeOrder(true);
}

3. Not resetting on session change​

// WRONG - State carries over between sessions
if (!tradeDate.equals(lastSessionDate)) {
vwap.reset();
lastSessionDate = tradeDate;
// Missing: orderManager.reset() or state reset!
}

// CORRECT - Reset everything
if (!tradeDate.equals(lastSessionDate)) {
vwap.reset();
lastSessionDate = tradeDate;
orderManager.reset(); // Reset order state too!
}

4. Using clientId for bracket order tracking​

// WRONG - Will fail because TP/SL have clientId=null
if (update.clientId != null && update.clientId.equals(myTpClientId)) {
// This NEVER matches for bracket orders!
}

// CORRECT - Use orderId (brokerId) which is always available
activeOrdersUnfilled.put(update.orderId, update.unfilled);

API Reference​

Key Bookmap Classes​

ClassPurpose
OrderInfoUpdateOrder state changes (status, filled, unfilled)
ExecutionInfoFill information (price, size, time)
OrderStatusOrder states (WORKING, FILLED, CANCELLED, etc.)
SimpleOrderSendParametersBuilderBuild orders with TP/SL

Key Fields​

FieldAlways AvailableNotes
orderIdÒœ… YesUse this for tracking (brokerId)
clientIdҝŒ Nonull for bracket orders in sim mode
statusÒœ… YesUse status.isActive()
unfilledÒœ… YesTrack for partial fills
doNotIncreaseÒœ… Yestrue for TP/SL orders

Version History​

VersionDateChanges
1.02025-08-19Initial proven pattern

References​

  • VwapAddonRealtime.java - Original pattern source
  • BookmapOrderTrackingSystem.md - API documentation
  • Bookmap Simplified API documentation