Skip to main content

Price Conversion

This guide covers price conversion in Bookmap add-ons, explaining the relationship between Level 1 (raw) prices and human-readable prices, and how to use the pips value correctly.

Price Formats in Bookmapโ€‹

Bookmap uses two internal price representations:

FormatDescriptionExample (ES futures)
Level 1 (Raw)Internal normalized representation160790
Human-ReadableActual price traders see4019.75

The Pips Valueโ€‹

The pips value from InstrumentInfo represents the minimum price increment for an instrument.

Obtaining Pipsโ€‹

private double pips;

@Override
public void initialize(String alias, InstrumentInfo info, Api api, InitialState initialState) {
this.pips = info.pips;
}

Conversion Formulasโ€‹

ConversionFormulaUse Case
Raw รขโ€ โ€™ Human-Readableprice * pipsUI display, logging, volume profile keys
Human-Readable รขโ€ โ€™ Rawprice / pipsindicator.addPoint(), chart plotting

Critical Rule for Indicatorsโ€‹

indicator.addPoint(value) expects Level 1 format (raw price), NOT human-readable price.

This is the most common source of bugs where indicator lines appear off-chart or at incorrect levels.

Wrong Approachโ€‹

@Override
public void onTrade(double price, int size, TradeInfo tradeInfo) {
double displayPrice = price * pips; // Human-readable: 6086.50

// WRONG - indicator will plot at wrong level (off-screen)
myIndicator.addPoint(displayPrice);
}

Correct Approachโ€‹

@Override
public void onTrade(double price, int size, TradeInfo tradeInfo) {
double rawPrice = price; // Level 1 format
double displayPrice = price * pips; // Human-readable for UI

// CORRECT - use raw price for indicator
myIndicator.addPoint(rawPrice);
}

Common Scenariosโ€‹

Scenario: Trade Price to Indicatorโ€‹

The price parameter in onTrade() is already in Level 1 format.

@Override
public void onTrade(double price, int size, TradeInfo tradeInfo) {
// price is already Level 1 - use directly for indicators
tradeIndicator.addPoint(price);

// Convert for display purposes
double displayPrice = price * pips;
Log.info("Trade at: " + displayPrice);
}

Scenario: Order Price to Indicatorโ€‹

Order prices from OrderInfoUpdate and ExecutionInfo need conversion.

@Override
public void onOrderUpdated(OrderInfoUpdate orderInfoUpdate) {
// limitPrice is raw format, convert for indicator
double levelForIndicator = orderInfoUpdate.limitPrice / pips;
lastOrderLimitPrice.addPoint(levelForIndicator);
}

@Override
public void onOrderExecuted(ExecutionInfo executionInfo) {
// execution price needs same conversion
double levelForIndicator = executionInfo.price / pips;
lastExecutionPrice.addPoint(levelForIndicator);
}

Scenario: Calculated Metrics to Indicatorโ€‹

When you calculate metrics (POC, High, Low) using human-readable prices, convert back for indicators.

// Store metrics in human-readable format for UI
private double pocPrice = 6086.50;
private double highPrice = 6090.25;

private void updateIndicators() {
// Convert back to Level 1 for indicator plotting
if (!Double.isNaN(pocPrice)) {
pocIndicator.addPoint(pocPrice / pips);
}
if (!Double.isNaN(highPrice)) {
highIndicator.addPoint(highPrice / pips);
}
}

Scenario: BBO Level Comparisonโ€‹

BBO prices from onBbo() are already level numbers.

@Override
public void onBbo(int bidPrice, int bidSize, int askPrice, int askSize) {
// bidPrice and askPrice are level numbers

for (OrderInfo order : activeLimitOrders.values()) {
// Convert order's raw price to level for comparison
double orderLevel = order.limitPrice / pips;

boolean nearBid = bidPrice - orderLevel <= CANCEL_DISTANCE;
boolean nearAsk = orderLevel - askPrice <= CANCEL_DISTANCE;
}
}

Complete Exampleโ€‹

@Layer1SimpleAttachable
@Layer1StrategyName("Price Level Demo")
@Layer1ApiVersion(Layer1ApiVersionValue.VERSION2)
public class PriceLevelDemo implements CustomModule, TradeDataListener {

private double pips;
private Indicator highIndicator;
private Indicator lowIndicator;

// Store as human-readable for display
private double highPrice = Double.MIN_VALUE;
private double lowPrice = Double.MAX_VALUE;

@Override
public void initialize(String alias, InstrumentInfo info, Api api, InitialState initialState) {
this.pips = info.pips;

highIndicator = api.registerIndicator("High", GraphType.PRIMARY);
lowIndicator = api.registerIndicator("Low", GraphType.PRIMARY);
}

@Override
public void onTrade(double price, int size, TradeInfo tradeInfo) {
double displayPrice = price * pips; // Human-readable

// Track high/low in human-readable format
if (displayPrice > highPrice) {
highPrice = displayPrice;
}
if (displayPrice < lowPrice) {
lowPrice = displayPrice;
}

// Convert BACK to Level 1 for indicators
highIndicator.addPoint(highPrice / pips);
lowIndicator.addPoint(lowPrice / pips);
}

@Override
public void stop() {}
}

Dual Storage Patternโ€‹

For complex add-ons, store both formats:

// For indicator plotting (Level 1)
private double highRaw = Double.NaN;

// For UI display (human-readable)
private double highDisplay = Double.NaN;

private void updateHigh(double rawPrice) {
double displayPrice = rawPrice * pips;

if (Double.isNaN(highDisplay) || displayPrice > highDisplay) {
highDisplay = displayPrice;
highRaw = rawPrice;
}
}

private void updateIndicators() {
if (!Double.isNaN(highRaw)) {
highIndicator.addPoint(highRaw);
}
}

Helper Methodsโ€‹

Centralize conversions for clarity:

private double toDisplayPrice(double rawPrice) {
return rawPrice * pips;
}

private double toRawPrice(double displayPrice) {
return displayPrice / pips;
}

Common Pitfallsโ€‹

Pitfall: Indicator Lines Off-Chartโ€‹

Symptom: Indicators are enabled but no lines visible on chart

Cause: Passing human-readable price to addPoint() instead of Level 1

Solution: Divide human-readable price by pips before calling addPoint()

Pitfall: Volume Profile Precisionโ€‹

Symptom: Price levels not matching exactly

Cause: Using raw prices for volume profile map keys

Solution: Use human-readable prices for volume profile keys to maintain decimal precision

// Use human-readable for better precision in maps
volumeProfile.merge(displayPrice, (long) size, Long::sum);

See Alsoโ€‹