Skip to content

Order books, positions, profiles, statistics


Order book

const book = new OrderBook(0.01);  // tick size
book.applySnapshot([50000, 49999], [1.5, 2.0], [50001, 50002], [0.5, 1.0]);
book.bestBid();    // 50000
book.bestAsk();    // 50001
book.mid();
book.spread();
book.getBids(5);   // [[price, qty], ...]
book.getAsks(5);

// L3 (order-level)
const l3 = new L3Book();
l3.addOrder(1, 50000, 1.5, 'buy');
l3.removeOrder(1);
l3.bestBid();

The tick size defaults to 0.01 when the argument is omitted. An explicit value must be positive; new OrderBook(0) throws a RangeError. The smallest representable tick is 1e-8, since prices are fixed-point with a scale of 1e8. The book covers 8192 consecutive ticks centred on the market and re-anchors that window as a delta feed walks the price away from the last snapshot. See The order book's tick window.


Composite book

Aggregates books across multiple exchanges per symbol.

const matrix = new CompositeBook();
matrix.applySnapshot(exchangeId, symbol, bidPrices, bidQtys, askPrices, askQtys, recvNs);
matrix.applyDelta(exchangeId, symbol, bidPrices, bidQtys, askPrices, askQtys, recvNs);
matrix.bestBid(symbol);      // { price, qty } | null
matrix.bestAsk(symbol);      // { price, qty } | null
matrix.hasArbitrage(symbol);
matrix.markStale(exchangeId, symbol);
matrix.checkStaleness(nowNs, thresholdNs);

applySnapshot replaces both sides of that exchange's top-of-book wholesale, including clearing a side that arrives as an empty array. applyDelta only touches the side(s) actually present in the call -- a side passed as an empty array is left exactly as it was, not zeroed. recvNs feeds checkStaleness's staleness clock; pass the actual receive timestamp if you use staleness eviction.


Position tracking

const tracker = new PositionTracker();
tracker.onFill(symbolId, 'buy', 50000, 1.0);
tracker.onFill(symbolId, 'sell', 50100, 1.0);
tracker.position(symbolId);        // 0
tracker.realizedPnl(symbolId);     // 100
tracker.totalRealizedPnl();

// Group tracking
const groups = new PositionGroupTracker();
const pid = groups.openPosition(symbolId, groupId, 'buy', 50000, 1.0);
groups.closePosition(pid, 50500);
groups.totalRealizedPnl();

Profiles

// Volume profile
const vp = new VolumeProfile(0.01);
vp.addTrade(50000, 1.0, true);
vp.poc();
vp.valueAreaHigh();
vp.valueAreaLow();

// Market profile
const mp = new MarketProfile(0.01, 30, 0);
mp.addTrade(Date.now() * 1e6, 50000, 1.0, true);
mp.poc();
mp.initialBalanceHigh();
mp.isPoorHigh();

// Footprint
const fp = new FootprintBar(0.01);
fp.addTrade(50000, 1.0, true);
fp.totalDelta();
fp.totalVolume();

Statistics

lrvx.correlation([1, 2, 3], [1, 2, 3]);
lrvx.profitFactor([100, -50, 200, -30]);
lrvx.winRate([100, -50, 200, -30]);
lrvx.bootstrapCI([1, 2, 3, 4, 5], 0.95, 10000);  // { lower, median, upper }
lrvx.permutationTest([1, 2, 3], [4, 5, 6], 10000); // p-value

Segment operations

Segment directories use the .lrvx extension.

lrvx.validateSegment('/path/to/segment.lrvx');

// Second argument is an OUTPUT FILE PATH, not a directory.
lrvx.mergeSegments('/path/to/input_dir', '/path/to/merged.lrvx');

Extended operations:

Call Description
lrvx.mergeDir(inputDir, outputDir) Merge a directory into an output directory
lrvx.splitSegment(inputPath, outputDir, mode, timeIntervalNs, eventsPerFile) Split one segment into many
lrvx.exportSegment(inputPath, outputPath, format, fromNs, toNs, symbols) Export a range to another format
lrvx.validateSegmentFull(path, verifyCrc, verifyTimestamps) Full validation, optional CRC and timestamp checks
lrvx.validateDataset(dataDir) Validate every segment in a dataset directory
lrvx.recompressSegment(inputPath, outputPath, level) Recompress at a different level
lrvx.extractSymbols(inputPath, outputDir, symbols) Extract selected symbols
lrvx.extractTimeRange(inputPath, outputPath, fromNs, toNs) Extract a time window

Bar aggregation

Each helper takes (timestamps, prices, quantities, sides, param). lrvx.renkoBars can return more bars than there were input trades: a trade that gaps past more than one brick width closes the brick that was forming and synthesizes the bricks in between (see bar types). Every other helper here closes at most one bar per input trade.

Call param
lrvx.timeBars(ts, px, qty, sides, intervalNs) Interval in nanoseconds — 60000000000 is one minute
lrvx.tickBars(ts, px, qty, sides, ticksPerBar) Trades per bar
lrvx.volumeBars(ts, px, qty, sides, volumePerBar) Volume per bar
lrvx.rangeBars(ts, px, qty, sides, rangeSize) High-low range
lrvx.renkoBars(ts, px, qty, sides, brickSize) Brick size
lrvx.heikinBars(ts, px, qty, sides, intervalNs) Interval in nanoseconds

A nanosecond interval may be a Number or a BigInt. Each bar comes back with ts (the bar start, a nanosecond BigInt), open, high, low, close, volume, buyVolume and trades. Only closed bars are returned: a bar still forming when the tape ends is not one of them.


Data reader / writer / recorder

Read trades and book updates from binary log segments, or record live data.

const reader = new DataReader('./data');
// Filtered reader:
//   new DataReader({ dir: './data', fromNs, toNs, symbols })

reader.count;            // total events
reader.summary();        // { firstEventNs, lastEventNs, totalEvents, segmentCount, totalBytes, durationSeconds }
reader.stats();          // { filesRead, eventsRead, tradesRead, bookUpdatesRead, bytesRead, crcErrors }

const trades = reader.readTrades(maxTrades = 0);   // 0 = all
const bbos = reader.readBBO(maxEvents = 0);
const events = reader.readBookUpdates();

// Mid-stream seek: start from a given timestamp
const tradesFrom = reader.readTradesFrom(startTsNs, maxTrades = 0);
const bbosFrom = reader.readBBOFrom(startTsNs, maxEvents = 0);
const eventsFrom = reader.readBookUpdatesFrom(startTsNs);

reader.destroy();

Record shapes:

  • Trade: { exchangeTsNs, recvTsNs, price, qty, tradeId, symbolId, side }
  • BBO: { exchangeTsNs, recvTsNs, seq, symbolId, eventType, bidPrice, bidQty, askPrice, askQty }
  • Book update: { exchangeTsNs, recvTsNs, seq, symbolId, eventType, bids: [{price, qty}, ...], asks: [{price, qty}, ...] }

eventType is 2 for a snapshot, 3 for a delta.

const writer = new DataWriter('./out', maxSegmentMb, exchangeId);
writer.writeTrade(exchangeTsNs, recvTsNs, price, qty, tradeId, symbolId, side);
// Raw int64 book levels — flat BigInt64Array as [price_raw, qty_raw, ...]
writer.writeBook(exchangeTsNs, recvTsNs, seqNs, symbolId, isSnapshot, bidsBuf, asksBuf);
writer.flush();
writer.close();
writer.stats();          // { bytesWritten, eventsWritten, segmentsCreated, tradesWritten }

const hook = new BinaryLogRecorderHook('./out', maxSegmentMb, exchangeId, 'none');
hook.addSymbol(symbolId, name, base, quote, pricePrecision, qtyPrecision);
// attach via the runner's market-data-recorder slot; lifecycle is engine-driven.
hook.destroy();