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:
-
clientId=null on bracket orders: In simulated trading mode, TP and SL orders return
clientId=nullinOrderInfoUpdatecallbacks, making it impossible to correlate them with the entry order using clientId. -
Position stacking: Without proper guards, a strategy can fire multiple signals while a position is open, causing overlapping positions.
-
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β
-
Track by brokerId, not clientId: The
orderIdfield (brokerId) is always available, even whenclientIdis null. -
Use
status.isActive(): This method returns true for all active states (WORKING, PENDING_SUBMIT, etc.) without needing to enumerate them. -
Track unfilled quantity: Allows proper handling of partial fills.
-
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:
| Check | Expected | How to Verify |
|---|---|---|
| Max Size | 1 (or configured size) | Trading Statistics Γ’β β Max Size column |
| No overlapping positions | Sequential entry times | Trading Statistics Γ’β β Entry Time column |
| Alternating directions | BUY/SELL pattern | Trading Statistics Γ’β β Type column |
| Bracket orders work | Clean exits | TP/SL lines visible, trades exit |
| No orphaned orders | 0 | Trading Statistics Γ’β β Orders Cancelled |
| State resets | Trades after exit | Multiple 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β
| Class | Purpose |
|---|---|
OrderInfoUpdate | Order state changes (status, filled, unfilled) |
ExecutionInfo | Fill information (price, size, time) |
OrderStatus | Order states (WORKING, FILLED, CANCELLED, etc.) |
SimpleOrderSendParametersBuilder | Build orders with TP/SL |
Key Fieldsβ
| Field | Always Available | Notes |
|---|---|---|
orderId | Γ’Εβ¦ Yes | Use this for tracking (brokerId) |
clientId | Γ’ΒΕ No | null for bracket orders in sim mode |
status | Γ’Εβ¦ Yes | Use status.isActive() |
unfilled | Γ’Εβ¦ Yes | Track for partial fills |
doNotIncrease | Γ’Εβ¦ Yes | true for TP/SL orders |
Version Historyβ
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2025-08-19 | Initial proven pattern |
Referencesβ
VwapAddonRealtime.java- Original pattern sourceBookmapOrderTrackingSystem.md- API documentation- Bookmap Simplified API documentation