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 Case | Example |
|---|---|
| OHLC Bars | 30-second candles from trade data |
| Custom Signals | Order block detection, sweep markers |
| Aggregated Metrics | VWAP, cumulative delta, volume profiles |
| Session Analytics | POC, 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β
- Implements
Serializable: Events may be persisted - Implements
Cloneable: Events must be cloneable - Has
getTime(): Returns event timestamp in nanoseconds - 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β
| Method | Called When | Purpose |
|---|---|---|
getInitialValue(t) | Starting a new interval | Creates empty aggregation container |
aggregateAggregationWithValue() | Processing individual events | Adds one event to running aggregation |
aggregateAggregationWithAggregation() | Merging time intervals | Combines 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β
| Parameter | Description |
|---|---|
strategyClass | The class that owns this generator (used for namespacing) |
generatorName | Unique identifier within the strategy (e.g., "Bars", "Signals") |
isAdd | true to register, false to unregister |
shouldReceiveHistory | If true, generator processes historical data on load |
shouldReceiveBackfilledData | If true, processes data backfilled after subscription |
generator | Your StrategyUpdateGenerator implementation |
info | Array 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β
- StrategyUpdateGenerator - Generator interface
- CustomGeneratedEvent - Event interface
- CustomGeneratedEventAliased - Aliased event wrapper
- CustomEventAggregatble - Aggregation rules interface
- GeneratedEventInfo - Event registration info
- Layer1ApiUserMessageAddStrategyUpdateGenerator - Registration message
- DataStructureInterface - Data retrieval interface
- ChartVisualizationGuide - Rendering the generated events