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.
291 lines (241 loc) • 7.66 kB
JavaScript
/**
* simulation.js
*
* Simulation connector — wraps the existing price engine,
* order book and candle manager into the connector interface.
*
* This is the default connector when no source is specified.
* Generates realistic fake market data locally.
* No API keys, no network calls, no dependencies.
*
* source: 'simulation' (default)
*/
'use strict'
const { BaseConnector } = require('./base')
const { PriceEngine } = require('../core/priceEngine')
const { OrderBook } = require('../core/orderBook')
const { CandleManager } = require('../core/candleManager')
const { MarketClock } = require('../core/marketClock')
const { DEFAULTS } = require('../constants/defaults')
class SimulationConnector extends BaseConnector {
/**
* @param {Object} config
* @param {Array} config.pairs - pair configs
* @param {number} config.interval - ms between ticks
* @param {string[]} config.candleIntervals - e.g. ['1m', '5m']
* @param {number} config.depth - order book levels
* @param {number} config.maxCandles - max candle history
* @param {string} config.type - 'crypto'|'equity'|'forex'
* @param {Object} config.marketHours - for equity/forex
*/
constructor(config = {}) {
super(config)
this._name = 'simulation'
this._type = config.type ?? 'crypto'
this._candleIntervals = config.candleIntervals ?? DEFAULTS.candleIntervals
this._depth = config.depth ?? DEFAULTS.depth
this._maxCandles = config.maxCandles ?? DEFAULTS.maxCandles
this._timer = null
this._paused = false
// Per-symbol engines — Map<symbol, { priceEngine, orderBook, candleManager }>
this._symbols = new Map()
// Initialize engines for each pair
config.pairs.forEach((pair) => this._initSymbol(pair))
// Market clock — handles open/close for equity/forex
this._clock = new MarketClock({
type: config.type,
marketHours: config.marketHours ?? DEFAULTS.marketHours[config.type],
onOpen: (info) => this._emitOpen(info),
onClose: (info) => this._emitClose(info),
})
}
// ── Lifecycle ─────────────────────────── //
async connect() {
if (this._running) return
this._running = true
this._paused = false
// Start market clock
this._clock.start()
// Start tick loop
this._timer = setInterval(() => {
this._tick()
}, this.interval)
// First tick immediately
this._tick()
}
async disconnect() {
if (!this._running) return
this._running = false
this._paused = false
if (this._timer) {
clearInterval(this._timer)
this._timer = null
}
this._clock.stop()
}
async subscribe(symbols) {
// Simulation subscribes to all pairs at init
// Nothing to do here — all pairs already running
}
async unsubscribe(symbols) {
// Could pause specific symbols in future
// For now no-op
}
// ── Pause / Resume ────────────────────── //
pause() {
this._paused = true
}
resume() {
this._paused = false
}
// ── Normalization ─────────────────────── //
/**
* Simulation data is already in standard format
* from PriceEngine — just add source field
*/
normalizeTick(raw) {
return {
...raw,
source: 'simulation',
}
}
normalizeCandle(raw) {
return {
...raw,
source: 'simulation',
}
}
normalizeDepth(raw) {
return {
...raw,
source: 'simulation',
}
}
// ── State ─────────────────────────────── //
getState(symbol) {
const entry = this._symbols.get(symbol)
if (!entry) return null
const priceState = entry.priceEngine.getState()
const depth = entry.orderBook.getSnapshot()
const candles = entry.candleManager.getAllHistory()
return {
symbol,
type: this._type,
source: 'simulation',
marketOpen: this._clock.isOpen(),
...priceState,
depth,
candles,
}
}
reset(symbol) {
const entry = this._symbols.get(symbol)
if (!entry) return
entry.priceEngine.reset()
entry.candleManager.reset()
}
resetAll() {
this._symbols.forEach((entry) => {
entry.priceEngine.reset()
entry.candleManager.reset()
})
}
getSymbols() {
return Array.from(this._symbols.keys())
}
getMarketStatus() {
return this._clock.getStatus()
}
// ── Private ───────────────────────────── //
/**
* Initialize engines for one pair
*/
_initSymbol(pair) {
const defaults = DEFAULTS.pair
const priceEngine = new PriceEngine({
startPrice: pair.startPrice,
volatility: pair.volatility ?? defaults.volatility,
trend: pair.trend ?? defaults.trend,
precision: pair.precision ?? defaults.precision,
tickSize: pair.tickSize ?? defaults.tickSize,
minPrice: pair.minPrice,
maxPrice: pair.maxPrice,
volume: pair.volume ?? defaults.volume,
})
const orderBook = new OrderBook({
depth: this._depth,
tickSize: pair.tickSize ?? defaults.tickSize,
precision: pair.precision ?? defaults.precision,
volume: pair.volume ?? defaults.volume,
spreadPct: pair.spreadPct ?? defaults.spreadPct,
})
const candleManager = new CandleManager({
intervals: this._candleIntervals,
precision: pair.precision ?? defaults.precision,
maxCandles: this._maxCandles,
onClose: (candle) => {
this._emitCandle(this.normalizeCandle({
symbol: pair.symbol,
type: this._type,
...candle,
}))
},
})
this._symbols.set(pair.symbol, {
priceEngine,
orderBook,
candleManager,
config: pair,
})
}
/**
* Main tick — advance price, update book, emit events
*/
_tick() {
if (this._paused) return
// Market closed — emit closed event and skip
if (!this._clock.isOpen()) {
this._emitClose(this._clock.getStatus())
return
}
const timestamp = Date.now()
this._symbols.forEach((entry, symbol) => {
try {
// 1. Advance price
const tick = entry.priceEngine.next()
// 2. Update order book
const depth = entry.orderBook.update(tick.price)
// 3. Update candles
entry.candleManager.tick(tick.price, tick.volume, timestamp)
// 4. Emit tick
this._emitTick(this.normalizeTick({
symbol,
type: this._type,
timestamp,
price: tick.price,
bid: tick.bid,
ask: tick.ask,
spread: tick.spread,
volume: tick.volume,
change: tick.change,
changePct: tick.changePct,
high24h: tick.high24h,
low24h: tick.low24h,
open24h: tick.open24h,
previous: tick.previous,
}))
// 5. Emit depth
this._emitDepth(this.normalizeDepth({
symbol,
type: this._type,
timestamp,
bids: depth.bids,
asks: depth.asks,
}))
} catch (err) {
this._emitError(symbol, err)
}
})
}
}
module.exports = { SimulationConnector }