Skip to main content

Custom Event Generator Guide

This guide covers creating custom event generators that store and aggregate data in Bookmap's data structures for use in indicators and analysis.

Overview​

What Are Custom Event Generators?​

Custom event generators allow you to:

  • Create your own data types (OHLC bars, signals, metrics)
  • Store them in Bookmap's efficient time-series data structures
  • Aggregate them across time intervals
  • Retrieve them for historical and real-time visualization

Use Cases​

Use CaseExample
OHLC Bars30-second candles from trade data
Custom SignalsOrder block detection, sweep markers
Aggregated MetricsVWAP, cumulative delta, volume profiles
Session AnalyticsPOC, VAH/VAL calculations

Architecture Flow​

Ò”ŒÒ”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”
Γ’β€β€š Event Generator Architecture Γ’β€β€š
Ò”œÒ”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€
Γ’β€β€š Γ’β€β€š
Γ’β€β€š Ò”ŒÒ”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò” Γ’β€β€š
Γ’β€β€š Γ’β€β€š Market Events Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š (trades, depth) Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Β¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Λœ Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Ò–¼ Γ’β€β€š
Γ’β€β€š Ò”ŒÒ”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò” Γ’β€β€š
Γ’β€β€š Γ’β€β€š StrategyUpdateGenerator Γ’β€β€š Γ’β€”β€žΓ’β€β‚¬Γ’β€β‚¬ Processes raw events Γ’β€β€š
Γ’β€β€š Γ’β€β€š - onTrade() Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š - onDepth() Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š - setTime() Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Β¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Λœ Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š consumer.accept(CustomGeneratedEventAliased) Γ’β€β€š
Γ’β€β€š Ò–¼ Γ’β€β€š
Γ’β€β€š Ò”ŒÒ”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò” Γ’β€β€š
Γ’β€β€š Γ’β€β€š CustomGeneratedEvent Γ’β€β€š Γ’β€”β€žΓ’β€β‚¬Γ’β€β‚¬ Your custom event class Γ’β€β€š
Γ’β€β€š Γ’β€β€š - getTime() Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€š - clone() Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Β¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Λœ Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Ò–¼ Γ’β€β€š
Γ’β€β€š Ò”ŒÒ”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò” Γ’β€β€š
Γ’β€β€š Γ’β€β€š DataStructureInterface Γ’β€β€š Γ’β€”β€žΓ’β€β‚¬Γ’β€β‚¬ Stored and aggregated Γ’β€β€š
Γ’β€β€š Γ’β€β€š - get() methods Γ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Β¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Λœ Γ’β€β€š
Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€š Ò–¼ Γ’β€β€š
Γ’β€β€š Ò”ŒÒ”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò” Γ’β€β€š
Γ’β€β€š Γ’β€β€š OnlineCalculatable Γ’β€β€š Γ’β€”β€žΓ’β€β‚¬Γ’β€β‚¬ Retrieved for visualization Γ’β€β€š
Γ’β€β€š Γ’β€β€š - calculateValuesInRangeΓ’β€β€š Γ’β€β€š
Γ’β€β€š Γ’β€β€Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Λœ Γ’β€β€š
Γ’β€β€š Γ’β€β€š
Γ’β€β€Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Λœ

CustomGeneratedEvent Interface​

All custom events must implement CustomGeneratedEvent:

public interface CustomGeneratedEvent extends CloneableSerializable {
long getTime();
Object clone(); // From CloneableSerializable
}

Requirements​

  1. Implements Serializable: Events may be persisted
  2. Implements Cloneable: Events must be cloneable
  3. Has getTime(): Returns event timestamp in nanoseconds
  4. Has clone(): Creates a deep copy of the event

Complete BarEvent Implementation​

From Layer1ApiBarsDemo.java:

private static class BarEvent implements CustomGeneratedEvent, DataCoordinateMarker {
private static final long serialVersionUID = 1L;

/**
* While bar is being accumulated we store open time here,
* then we change it to actual event time.
*/
private long time;

double open;
double low;
double high;
double close;

// transient: not serialized, set at render time
transient int bodyWidthPx;

// Constructor: empty bar
public BarEvent(long time) {
this(time, Double.NaN);
}

// Constructor: bar starting with open price
public BarEvent(long time, double open) {
this(time, open, -1);
}

public BarEvent(long time, double open, int bodyWidthPx) {
this(time, open, open, open, open, bodyWidthPx);
}

// Full constructor
public BarEvent(long time, double open, double low, double high, double close, int bodyWidthPx) {
this.time = time;
this.open = open;
this.low = low;
this.high = high;
this.close = close;
this.bodyWidthPx = bodyWidthPx;
}

// Copy constructor (important!)
public BarEvent(BarEvent other) {
this(other.time, other.open, other.low, other.high, other.close, other.bodyWidthPx);
}

public void setTime(long time) {
this.time = time;
}

@Override
public long getTime() {
return time;
}

@Override
public Object clone() {
return new BarEvent(time, open, low, high, close, bodyWidthPx);
}

@Override
public String toString() {
return "[" + time + ": " + open + "/" + low + "/" + high + "/" + close + "]";
}

/**
* Update bar with a new price tick
*/
public void update(double price) {
if (Double.isNaN(price)) {
return;
}

// If bar was not initialized yet
if (Double.isNaN(open)) {
open = price;
low = price;
high = price;
} else {
low = Math.min(low, price);
high = Math.max(high, price);
}
close = price;
}

/**
* Merge another bar into this one
*/
public void update(BarEvent nextBar) {
// Inefficient but simple - update with each OHLC value
update(nextBar.open);
update(nextBar.low);
update(nextBar.high);
update(nextBar.close);
}

public void setBodyWidthPx(int bodyWidthPx) {
this.bodyWidthPx = bodyWidthPx;
}

// DataCoordinateMarker implementation for visualization
@Override
public double getMinY() { return low; }

@Override
public double getMaxY() { return high; }

@Override
public double getValueY() { return close; }

@Override
public Marker makeMarker(Function<Double, Integer> yDataCoordinateToPixelFunction) {
// See ChartVisualizationGuide.md for full implementation
// ...
}

/**
* Convert from level numbers to display prices for bottom panel
*/
public void applyPips(double pips) {
open *= pips;
low *= pips;
high *= pips;
close *= pips;
}
}

CustomEventAggregatble - Aggregation Rules​

The CustomEventAggregatble interface defines how events combine across time intervals.

Interface Methods​

public interface CustomEventAggregatble {
// Create an empty aggregation for time t
CustomGeneratedEvent getInitialValue(long t);

// Add a single value event to an aggregation
void aggregateAggregationWithValue(CustomGeneratedEvent aggregation, CustomGeneratedEvent value);

// Merge two aggregations together
void aggregateAggregationWithAggregation(CustomGeneratedEvent aggregation1, CustomGeneratedEvent aggregation2);
}

Method Purposes​

MethodCalled WhenPurpose
getInitialValue(t)Starting a new intervalCreates empty aggregation container
aggregateAggregationWithValue()Processing individual eventsAdds one event to running aggregation
aggregateAggregationWithAggregation()Merging time intervalsCombines two aggregations (e.g., two 1-min Ò†’ one 2-min)

Complete BAR_EVENTS_AGGREGATOR​

From Layer1ApiBarsDemo.java:

public static final CustomEventAggregatble BAR_EVENTS_AGGREGATOR = new CustomEventAggregatble() {

@Override
public CustomGeneratedEvent getInitialValue(long t) {
// Create empty bar for this time interval
return new BarEvent(t);
}

@Override
public void aggregateAggregationWithValue(CustomGeneratedEvent aggregation, CustomGeneratedEvent value) {
// Add a single bar event to the aggregation
BarEvent aggregationEvent = (BarEvent) aggregation;
BarEvent valueEvent = (BarEvent) value;
aggregationEvent.update(valueEvent);
}

@Override
public void aggregateAggregationWithAggregation(CustomGeneratedEvent aggregation1,
CustomGeneratedEvent aggregation2) {
// Merge two bar aggregations (e.g., when zooming out)
BarEvent aggregationEvent1 = (BarEvent) aggregation1;
BarEvent aggregationEvent2 = (BarEvent) aggregation2;
aggregationEvent1.update(aggregationEvent2);
}
};

Aggregation Flow Diagram​

Individual Events (1-second bars):
[Bar1] [Bar2] [Bar3] [Bar4] [Bar5] [Bar6]
Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€Γ’β€β‚¬Γ’β€β‚¬Γ’β€Β¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Λœ Γ’β€β€Γ’β€β‚¬Γ’β€β‚¬Γ’β€Β¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Λœ Γ’β€β€Γ’β€β‚¬Γ’β€β‚¬Γ’β€Β¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Λœ
Γ’β€β€š Γ’β€β€š Γ’β€β€š
Ò–¼ Ò–¼ Ò–¼
aggregateAggregationWithAggregation()
Γ’β€β€š Γ’β€β€š Γ’β€β€š
Ò–¼ Ò–¼ Ò–¼
[2-sec] [2-sec] [2-sec]
Γ’β€β€š Γ’β€β€š Γ’β€β€š
Γ’β€β€Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Β¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Λœ Γ’β€β€š
Γ’β€β€š Γ’β€β€š
Ò–¼ Γ’β€β€š
aggregateAggregationWithAggregation()
Γ’β€β€š Γ’β€β€š
Ò–¼ Γ’β€β€š
[4-sec] Γ’β€β€š
Γ’β€β€š Γ’β€β€š
Γ’β€β€Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Β¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€β‚¬Γ’β€Λœ
Γ’β€β€š
Ò–¼
aggregateAggregationWithAggregation()
Γ’β€β€š
Ò–¼
[6-sec]

GeneratedEventInfo Registration​

The GeneratedEventInfo class describes your custom event to Bookmap.

Constructor​

public GeneratedEventInfo(
Class<?> valueClass, // Class of individual events
Class<?> aggregationClass, // Class of aggregated events (same or different)
CustomEventAggregatble aggregator // Aggregation rules
)

For Non-Aggregatable Events​

// Events that don't aggregate (just stored individually)
new GeneratedEventInfo(MySignalEvent.class)

For Aggregatable Events​

// Events that aggregate (like OHLC bars)
new GeneratedEventInfo(
BarEvent.class, // valueClass: individual bars
BarEvent.class, // aggregationClass: aggregated bars (same class)
BAR_EVENTS_AGGREGATOR // aggregator: rules for combining
)

Important Rule​

All events in one generator must be the same type - either ALL aggregatable (with aggregator) or ALL non-aggregatable.

If you need both types, register two separate generators.


StrategyUpdateGenerator Implementation​

The StrategyUpdateGenerator interface processes market events and produces custom events.

Key Methods​

public interface StrategyUpdateGenerator extends 
Layer1ApiDataAdapter, Layer1ApiTradingAdapter,
Layer1ApiInstrumentAdapter, GeneratedUpdateConsumer {

void setTime(long time); // Called periodically (~50ms)
void onTrade(String alias, double price, int size, TradeInfo tradeInfo);
void onDepth(String alias, boolean isBid, int price, int size);
// ... other event handlers
}

Time-Based Bar Completion Pattern​

From Layer1ApiBarsDemo.java:

private static final long CANDLE_INTERVAL_NS = TimeUnit.SECONDS.toNanos(30);

private Layer1ApiUserMessageAddStrategyUpdateGenerator getGeneratorMessage(boolean isAdd) {
return new Layer1ApiUserMessageAddStrategyUpdateGenerator(
Layer1ApiBarsDemo.class, // strategyClass
TREE_NAME, // generatorName (unique within strategy)
isAdd, // isAdd: true to add, false to remove
true, // shouldReceiveHistory
new StrategyUpdateGenerator() {

private Consumer<CustomGeneratedEventAliased> consumer;
private long time = 0;
private Map<String, BarEvent> aliasToLastBar = new HashMap<>();

@Override
public void setGeneratedEventsConsumer(Consumer<CustomGeneratedEventAliased> consumer) {
this.consumer = consumer;
}

@Override
public Consumer<CustomGeneratedEventAliased> getGeneratedEventsConsumer() {
return consumer;
}

@Override
public void setTime(long time) {
this.time = time;

/*
* Publish finished bars. Bookmap calls this method periodically
* even if nothing is happening (~50ms intervals).
*/
long barStartTime = getBarStartTime(time);

for (Entry<String, BarEvent> entry : aliasToLastBar.entrySet()) {
String alias = entry.getKey();
BarEvent bar = entry.getValue();

// If we've moved to a new bar period
if (barStartTime != bar.time) {
// Finalize the old bar
bar.setTime(time);
consumer.accept(new CustomGeneratedEventAliased(bar, alias));

// Start new bar with previous close as seed
bar = new BarEvent(barStartTime, bar.close);
entry.setValue(bar);
}
}
}

@Override
public void onTrade(String alias, double price, int size, TradeInfo tradeInfo) {
BarEvent bar = aliasToLastBar.get(alias);
long barStartTime = getBarStartTime(time);

// Initialize bar if needed
if (bar == null) {
bar = new BarEvent(barStartTime);
aliasToLastBar.put(alias, bar);
}

// Check if we've crossed into a new bar
if (barStartTime != bar.time) {
// Finalize the old bar
bar.setTime(time);
consumer.accept(new CustomGeneratedEventAliased(bar, alias));

// Start new bar
bar = new BarEvent(barStartTime, bar.close);
aliasToLastBar.put(alias, bar);
}

// Update current bar with trade
if (size != 0) {
bar.update(price);
}
}

@Override
public void onInstrumentRemoved(String alias) {
aliasToLastBar.remove(alias);
}

// Empty implementations for unused handlers
@Override public void onDepth(String alias, boolean isBid, int price, int size) {}
@Override public void onMarketMode(String alias, MarketMode marketMode) {}
@Override public void onOrderUpdated(OrderInfoUpdate orderInfoUpdate) {}
@Override public void onOrderExecuted(ExecutionInfo executionInfo) {}
@Override public void onStatus(StatusInfo statusInfo) {}
@Override public void onBalance(BalanceInfo balanceInfo) {}
@Override public void onInstrumentAdded(String alias, InstrumentInfo instrumentInfo) {}
@Override public void onInstrumentNotFound(String symbol, String exchange, String type) {}
@Override public void onInstrumentAlreadySubscribed(String symbol, String exchange, String type) {}
@Override public void onUserMessage(Object data) {}

},
new GeneratedEventInfo[] {
new GeneratedEventInfo(BarEvent.class, BarEvent.class, BAR_EVENTS_AGGREGATOR)
}
);
}

private long getBarStartTime(long time) {
return time - time % CANDLE_INTERVAL_NS;
}

Layer1ApiUserMessageAddStrategyUpdateGenerator​

Registration Message​

new Layer1ApiUserMessageAddStrategyUpdateGenerator(
Class<?> strategyClass, // Your strategy class
String generatorName, // Unique name within strategy
boolean isAdd, // true=add, false=remove
boolean shouldReceiveHistory, // Process historical data?
boolean shouldReceiveBackfilledData, // Process backfill data?
StrategyUpdateGenerator generator, // Your generator implementation
GeneratedEventInfo[] info // Event descriptions
)

Parameters Explained​

ParameterDescription
strategyClassThe class that owns this generator (used for namespacing)
generatorNameUnique identifier within the strategy (e.g., "Bars", "Signals")
isAddtrue to register, false to unregister
shouldReceiveHistoryIf true, generator processes historical data on load
shouldReceiveBackfilledDataIf true, processes data backfilled after subscription
generatorYour StrategyUpdateGenerator implementation
infoArray of GeneratedEventInfo describing your events

Typical Registration Flow​

@Override
public void onUserMessage(Object data) {
if (data instanceof UserMessageLayersChainCreatedTargeted) {
UserMessageLayersChainCreatedTargeted message = (UserMessageLayersChainCreatedTargeted) data;
if (message.targetClass == getClass()) {
// 1. Request DataStructureInterface
provider.sendUserMessage(new Layer1ApiDataInterfaceRequestMessage(
dsi -> this.dataStructureInterface = dsi
));

// 2. Register indicators
addIndicator(INDICATOR_NAME_BARS_MAIN);
addIndicator(INDICATOR_NAME_BARS_BOTTOM);

// 3. Register the generator
provider.sendUserMessage(getGeneratorMessage(true));
}
}
}

@Override
public void finish() {
// 1. Remove indicators
for (String userName : indicatorsFullNameToUserName.values()) {
provider.sendUserMessage(new Layer1ApiUserMessageModifyIndicator(
MyStrategy.class, userName, false
));
}

// 2. Remove the generator
provider.sendUserMessage(getGeneratorMessage(false));
}

Retrieving Custom Events from DataStructureInterface​

For Aggregated Events (Multiple Intervals)​

private static final String TREE_NAME = "Bars";
private static final Class<?>[] INTERESTING_CUSTOM_EVENTS = new Class<?>[] { BarEvent.class };

List<TreeResponseInterval> result = dataStructureInterface.get(
Layer1ApiBarsDemo.class, // strategyClass
TREE_NAME, // generatorName
t0, // start time (nanoseconds)
intervalWidth, // interval duration (nanoseconds)
intervalsNumber, // number of intervals
indicatorAlias, // instrument alias
INTERESTING_CUSTOM_EVENTS // event classes to retrieve
);

// Element 0 is the "snapshot" (aggregation before t0)
// Elements 1 through intervalsNumber are the requested intervals
for (int i = 1; i <= intervalsNumber; i++) {
BarEvent value = getBarEvent(result.get(i));
if (value != null) {
// Process the bar
}
}

Helper Method for Extraction​

private BarEvent getBarEvent(TreeResponseInterval treeResponseInterval) {
// Key is the string value of the event class
Object result = treeResponseInterval.events.get(BarEvent.class.toString());
if (result != null) {
return (BarEvent) result;
} else {
return null;
}
}

CRITICAL: Do Not Modify Returned Events​

Events returned from DataStructureInterface may be cached by Bookmap. Always clone before modifying!

Wrong Approach​

// BAD: Modifying cached event
BarEvent value = getBarEvent(result.get(i));
if (value != null) {
value.setBodyWidthPx(bodyWidth); // DANGER: Modifies cached copy!
listener.provideResponse(value);
}

Correct Approach​

// GOOD: Clone before modifying
BarEvent value = getBarEvent(result.get(i));
if (value != null) {
/*
* IMPORTANT: Don't edit value returned by interface directly.
* It might be cached by Bookmap for performance reasons, so
* you'll often end up with the modified value next time you
* request it, but it isn't going to happen every time, so
* the behavior won't be predictable.
*/
value = new BarEvent(value); // Clone!

value.setBodyWidthPx(bodyWidth);
if (isBottomChart) {
value.applyPips(pips);
}
listener.provideResponse(value);
} else {
listener.provideResponse(Double.NaN);
}

Real-time Updates via OnlineValueCalculatorAdapter​

Intercepting CustomGeneratedEventAliased Messages​

@Override
public OnlineValueCalculatorAdapter createOnlineValueCalculator(
String indicatorName, String indicatorAlias, long time,
Consumer<Object> listener, InvalidateInterface invalidateInterface) {

String userName = indicatorsFullNameToUserName.get(indicatorName);
boolean isBottomChart = userName.equals(INDICATOR_NAME_BARS_BOTTOM);
Double pips = pipsMap.get(indicatorAlias);

return new OnlineValueCalculatorAdapter() {

int bodyWidth = MAX_BODY_WIDTH;

@Override
public void onIntervalWidth(long intervalWidth) {
this.bodyWidth = getBodyWidth(intervalWidth);
}

@Override
public void onUserMessage(Object data) {
if (data instanceof CustomGeneratedEventAliased) {
CustomGeneratedEventAliased aliasedEvent = (CustomGeneratedEventAliased) data;

// Check if this event is for our instrument and type
if (indicatorAlias.equals(aliasedEvent.alias) &&
aliasedEvent.event instanceof BarEvent) {

BarEvent event = (BarEvent) aliasedEvent.event;

/*
* Same idea as in calculateValuesInRange - we don't want
* to mess up the message. We have a chance of changing it
* before or after it's stored inside Bookmap, resulting
* in undefined behavior.
*/
event = new BarEvent(event); // Clone!

event.setBodyWidthPx(bodyWidth);
if (isBottomChart) {
event.applyPips(pips);
}
listener.accept(event);
}
}
}
};
}

Complete Example: 30-Second OHLC Bars​

This example combines all concepts into a working OHLC bar generator:

@Layer1Attachable
@Layer1StrategyName("30-Second Bars Demo")
@Layer1ApiVersion(Layer1ApiVersionValue.VERSION2)
public class OHLCBarsDemo implements
Layer1ApiFinishable,
Layer1ApiAdminAdapter,
Layer1ApiInstrumentListener,
OnlineCalculatable {

// Configuration
private static final String TREE_NAME = "OHLC";
private static final String INDICATOR_NAME = "OHLC Bars";
private static final long CANDLE_INTERVAL_NS = TimeUnit.SECONDS.toNanos(30);
private static final int MAX_BODY_WIDTH = 30;
private static final int MIN_BODY_WIDTH = 1;
private static final Class<?>[] INTERESTING_EVENTS = new Class<?>[] { BarEvent.class };

// State
private Layer1ApiProvider provider;
private DataStructureInterface dataStructureInterface;
private Map<String, Double> pipsMap = new ConcurrentHashMap<>();
private Map<String, String> indicatorsFullNameToUserName = new HashMap<>();

// Aggregator for OHLC bars
public static final CustomEventAggregatble BAR_AGGREGATOR = new CustomEventAggregatble() {
@Override
public CustomGeneratedEvent getInitialValue(long t) {
return new BarEvent(t);
}

@Override
public void aggregateAggregationWithValue(CustomGeneratedEvent agg, CustomGeneratedEvent val) {
((BarEvent) agg).update((BarEvent) val);
}

@Override
public void aggregateAggregationWithAggregation(CustomGeneratedEvent agg1, CustomGeneratedEvent agg2) {
((BarEvent) agg1).update((BarEvent) agg2);
}
};

public OHLCBarsDemo(Layer1ApiProvider provider) {
this.provider = provider;
ListenableHelper.addListeners(provider, this);
}

@Override
public void onUserMessage(Object data) {
if (data instanceof UserMessageLayersChainCreatedTargeted) {
UserMessageLayersChainCreatedTargeted msg = (UserMessageLayersChainCreatedTargeted) data;
if (msg.targetClass == getClass()) {
// Get DataStructureInterface
provider.sendUserMessage(new Layer1ApiDataInterfaceRequestMessage(
dsi -> this.dataStructureInterface = dsi
));

// Register indicator
Layer1ApiUserMessageModifyIndicator indicatorMsg = Layer1ApiUserMessageModifyIndicator
.builder(OHLCBarsDemo.class, INDICATOR_NAME)
.setIsAdd(true)
.setGraphType(GraphType.PRIMARY)
.setOnlineCalculatable(this)
.setIndicatorLineStyle(IndicatorLineStyle.NONE)
.build();

indicatorsFullNameToUserName.put(indicatorMsg.fullName, indicatorMsg.userName);
provider.sendUserMessage(indicatorMsg);

// Register generator
provider.sendUserMessage(createGeneratorMessage(true));
}
}
}

private Layer1ApiUserMessageAddStrategyUpdateGenerator createGeneratorMessage(boolean isAdd) {
return new Layer1ApiUserMessageAddStrategyUpdateGenerator(
OHLCBarsDemo.class,
TREE_NAME,
isAdd,
true, // shouldReceiveHistory
new StrategyUpdateGenerator() {
private Consumer<CustomGeneratedEventAliased> consumer;
private long time = 0;
private Map<String, BarEvent> bars = new HashMap<>();

@Override
public void setGeneratedEventsConsumer(Consumer<CustomGeneratedEventAliased> c) {
this.consumer = c;
}

@Override
public Consumer<CustomGeneratedEventAliased> getGeneratedEventsConsumer() {
return consumer;
}

@Override
public void setTime(long t) {
this.time = t;
long barStart = t - t % CANDLE_INTERVAL_NS;

for (Entry<String, BarEvent> e : bars.entrySet()) {
if (barStart != e.getValue().time) {
e.getValue().setTime(t);
consumer.accept(new CustomGeneratedEventAliased(e.getValue(), e.getKey()));
e.setValue(new BarEvent(barStart, e.getValue().close));
}
}
}

@Override
public void onTrade(String alias, double price, int size, TradeInfo info) {
long barStart = time - time % CANDLE_INTERVAL_NS;
BarEvent bar = bars.get(alias);

if (bar == null) {
bar = new BarEvent(barStart);
bars.put(alias, bar);
}

if (barStart != bar.time) {
bar.setTime(time);
consumer.accept(new CustomGeneratedEventAliased(bar, alias));
bar = new BarEvent(barStart, bar.close);
bars.put(alias, bar);
}

if (size != 0) bar.update(price);
}

@Override public void onInstrumentRemoved(String alias) { bars.remove(alias); }
@Override public void onDepth(String a, boolean b, int p, int s) {}
@Override public void onMarketMode(String a, MarketMode m) {}
@Override public void onOrderUpdated(OrderInfoUpdate o) {}
@Override public void onOrderExecuted(ExecutionInfo e) {}
@Override public void onStatus(StatusInfo s) {}
@Override public void onBalance(BalanceInfo b) {}
@Override public void onInstrumentAdded(String a, InstrumentInfo i) {}
@Override public void onInstrumentNotFound(String s, String e, String t) {}
@Override public void onInstrumentAlreadySubscribed(String s, String e, String t) {}
@Override public void onUserMessage(Object d) {}
},
new GeneratedEventInfo[] {
new GeneratedEventInfo(BarEvent.class, BarEvent.class, BAR_AGGREGATOR)
}
);
}

@Override
public void onInstrumentAdded(String alias, InstrumentInfo info) {
pipsMap.put(alias, info.pips);
}

@Override
public void calculateValuesInRange(String indicatorName, String alias,
long t0, long intervalWidth, int intervalsNumber, CalculatedResultListener listener) {

if (dataStructureInterface == null) {
listener.setCompleted();
return;
}

List<TreeResponseInterval> result = dataStructureInterface.get(
OHLCBarsDemo.class, TREE_NAME, t0, intervalWidth, intervalsNumber,
alias, INTERESTING_EVENTS
);

int bodyWidth = getBodyWidth(intervalWidth);

for (int i = 1; i <= intervalsNumber; i++) {
Object obj = result.get(i).events.get(BarEvent.class.toString());
if (obj != null) {
BarEvent bar = new BarEvent((BarEvent) obj); // Clone!
bar.setBodyWidthPx(bodyWidth);
listener.provideResponse(bar);
} else {
listener.provideResponse(Double.NaN);
}
}

listener.setCompleted();
}

@Override
public OnlineValueCalculatorAdapter createOnlineValueCalculator(
String indicatorName, String alias, long time,
Consumer<Object> listener, InvalidateInterface invalidate) {

return new OnlineValueCalculatorAdapter() {
int bodyWidth = MAX_BODY_WIDTH;

@Override
public void onIntervalWidth(long intervalWidth) {
bodyWidth = getBodyWidth(intervalWidth);
}

@Override
public void onUserMessage(Object data) {
if (data instanceof CustomGeneratedEventAliased) {
CustomGeneratedEventAliased ae = (CustomGeneratedEventAliased) data;
if (alias.equals(ae.alias) && ae.event instanceof BarEvent) {
BarEvent bar = new BarEvent((BarEvent) ae.event); // Clone!
bar.setBodyWidthPx(bodyWidth);
listener.accept(bar);
}
}
}
};
}

private int getBodyWidth(long intervalWidth) {
long w = CANDLE_INTERVAL_NS / intervalWidth;
return (int) Math.max(MIN_BODY_WIDTH, Math.min(MAX_BODY_WIDTH, w));
}

@Override
public void finish() {
for (String name : indicatorsFullNameToUserName.values()) {
provider.sendUserMessage(new Layer1ApiUserMessageModifyIndicator(
OHLCBarsDemo.class, name, false
));
}
provider.sendUserMessage(createGeneratorMessage(false));
}

@Override public void onInstrumentRemoved(String alias) {}
@Override public void onInstrumentNotFound(String s, String e, String t) {}
@Override public void onInstrumentAlreadySubscribed(String s, String e, String t) {}
}

Cross-References​