trade-data-generator
Version:
Generate realistic real-time market data for trading UIs — equity, forex and crypto. Emits tick, candle, orderbook and market status events via EventEmitter.
238 lines (209 loc) • 6.48 kB
JavaScript
/**
* candleManager.js
*
* Manages OHLCV candlestick data per symbol.
*
* Features:
* - Multiple configurable intervals (1s, 1m, 5m, 15m, 1h, 4h, 1d)
* - Accurate OHLCV — open is always first tick of interval
* - Emits completed candle when interval closes
* - Keeps rolling history (configurable max candles)
* - No double-append bug from old code
*/
"use strict";
const INTERVALS = {
"1s": 1000,
"1m": 60 * 1000,
"5m": 5 * 60 * 1000,
"15m": 15 * 60 * 1000,
"1h": 60 * 60 * 1000,
"4h": 4 * 60 * 60 * 1000,
"1d": 24 * 60 * 60 * 1000,
};
class CandleManager {
/**
* @param {Object} config
* @param {string|string[]} config.intervals - e.g. '1m' or ['1m','5m','1h']
* @param {number} config.precision - Decimal places for price
* @param {number} config.maxCandles - Max candles to keep per interval (default 1000)
* @param {Function} config.onClose - Callback when candle closes: (candle) => {}
*/
constructor(config = {}) {
this.precision = config.precision ?? 2;
this.maxCandles = config.maxCandles ?? 1000;
this.onClose = config.onClose ?? null;
// Normalize intervals to array
const requested = config.intervals
? Array.isArray(config.intervals)
? config.intervals
: [config.intervals]
: ["1m"];
// Validate intervals
this.intervals = requested.filter((i) => {
if (!INTERVALS[i]) {
console.warn(`[CandleManager] Unknown interval "${i}" — skipped.`);
return false;
}
return true;
});
// Per-interval state
// { '1m': { current: Candle|null, history: Candle[] }, ... }
this._state = {};
this.intervals.forEach((interval) => {
this._state[interval] = {
current: null,
history: [],
intervalMs: INTERVALS[interval],
};
});
}
/**
* Feed a new price tick into the candle manager
* Call this on every price update
*
* @param {number} price - Current price
* @param {number} volume - Volume for this tick
* @param {number} timestamp - Unix ms timestamp (default: Date.now())
* @returns {Object} { updated: Object, closed: Object[] }
* updated → current in-progress candles per interval
* closed → candles that just completed this tick
*/
tick(price, volume, timestamp = Date.now()) {
const updated = {};
const closed = [];
this.intervals.forEach((interval) => {
const state = this._state[interval];
const intervalMs = state.intervalMs;
// Calculate which interval bucket this timestamp belongs to
const bucketStart = Math.floor(timestamp / intervalMs) * intervalMs;
const bucketEnd = bucketStart + intervalMs;
if (!state.current) {
// No open candle — open a new one
state.current = this._openCandle(
interval,
price,
volume,
bucketStart,
bucketEnd,
);
} else if (timestamp >= state.current.closeTime) {
// Current candle has expired — close it and open new one
const closed_candle = this._closeCandle(state, interval);
closed.push(closed_candle);
// Fire callback if provided
if (this.onClose) this.onClose(closed_candle);
// Open new candle
state.current = this._openCandle(
interval,
price,
volume,
bucketStart,
bucketEnd,
);
} else {
// Update existing candle
this._updateCandle(state.current, price, volume);
}
updated[interval] = { ...state.current };
});
return { updated, closed };
}
/**
* Get completed candle history for an interval
* @param {string} interval - e.g. '1m'
* @param {number} limit - Max candles to return (default: all)
* @returns {Array} candles oldest → newest
*/
getHistory(interval, limit) {
const state = this._state[interval];
if (!state) {
console.warn(`[CandleManager] Unknown interval "${interval}"`);
return [];
}
const history = state.history;
return limit ? history.slice(-limit) : [...history];
}
/**
* Get current in-progress candle for an interval
* @param {string} interval
* @returns {Object|null}
*/
getCurrent(interval) {
const state = this._state[interval];
if (!state) return null;
return state.current ? { ...state.current } : null;
}
/**
* Get all history for all intervals
* @returns {Object} { '1m': [...], '5m': [...] }
*/
getAllHistory() {
const result = {};
this.intervals.forEach((interval) => {
result[interval] = this.getHistory(interval);
});
return result;
}
/**
* Reset all candle state — call at start of new trading day
*/
reset() {
this.intervals.forEach((interval) => {
this._state[interval].current = null;
this._state[interval].history = [];
});
}
// ── Private helpers ──────────────────────── //
/**
* Open a new candle
*/
_openCandle(interval, price, volume, openTime, closeTime) {
return {
interval,
openTime,
closeTime,
open: this._round(price),
high: this._round(price),
low: this._round(price),
close: this._round(price),
volume: volume,
ticks: 1, // number of price updates in this candle
closed: false,
};
}
/**
* Update an open candle with new price and volume
*/
_updateCandle(candle, price, volume) {
const rounded = this._round(price);
if (rounded > candle.high) candle.high = rounded;
if (rounded < candle.low) candle.low = rounded;
candle.close = rounded;
candle.volume += volume; // numeric addition — no string bug
candle.ticks += 1;
}
/**
* Close current candle and add to history
*/
_closeCandle(state, interval) {
const candle = state.current;
candle.closed = true;
candle.close = this._round(candle.close);
candle.volume = this._round(candle.volume);
// Add to history
state.history.push(candle);
// Trim history if over max
if (state.history.length > this.maxCandles) {
state.history.shift();
}
state.current = null;
return candle;
}
/**
* Round to configured precision
*/
_round(value) {
return parseFloat(value.toFixed(this.precision));
}
}
module.exports = { CandleManager, INTERVALS };