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.
193 lines (165 loc) • 6.06 kB
JavaScript
/**
* priceEngine.js
*
* Generates realistic price movements using:
* - Random walk with mean reversion
* - Configurable volatility
* - Trend bias (slight up/down drift)
* - Volume correlation (bigger moves = more volume)
*/
"use strict";
class PriceEngine {
/**
* @param {Object} config
* @param {number} config.startPrice - Initial price
* @param {number} config.volatility - Max % move per tick (e.g. 0.002 = 0.2%)
* @param {number} config.trend - Drift per tick (e.g. 0.0001 up, -0.0001 down)
* @param {number} config.precision - Decimal places (e.g. 2 for stocks, 5 for forex)
* @param {number} config.minPrice - Floor price (default: 10% below start)
* @param {number} config.maxPrice - Ceiling price (default: 10% above start... grows over time)
* @param {Object} config.volume - { min, max } volume range per tick
*/
constructor(config) {
this.startPrice = config.startPrice;
this.volatility = config.volatility ?? 0.002;
this.trend = config.trend ?? 0;
this.precision = config.precision ?? 2;
this.volume = config.volume ?? { min: 100, max: 10000 };
// Price boundaries — mean reversion pulls price back when it strays too far
this.minPrice = config.minPrice ?? config.startPrice * 0.5;
this.maxPrice = config.maxPrice ?? config.startPrice * 1.5;
// Internal state
this.currentPrice = config.startPrice;
this.previousPrice = config.startPrice;
this.high24h = config.startPrice;
this.low24h = config.startPrice;
this.open24h = config.startPrice;
this.totalVolume = 0;
this.tickCount = 0;
}
/**
* Generate next price tick
* @returns {Object} tick data
*/
next() {
this.previousPrice = this.currentPrice;
// ── Price movement algo ──────────────────── //
// 1. Random component — normal-ish distribution
const random = this._randomNormal() * this.volatility;
// 2. Trend component — slight drift
const trendMove = this.trend;
// 3. Mean reversion — pulls price back toward start
// Stronger pull the further price strays
const deviation = (this.currentPrice - this.startPrice) / this.startPrice;
const reversion = -deviation * 0.05; // 5% reversion force
// 4. Combine all forces
const pctChange = random + trendMove + reversion;
// 5. Apply to price
let newPrice = this.currentPrice * (1 + pctChange);
// 6. Enforce boundaries
newPrice = Math.max(this.minPrice, Math.min(this.maxPrice, newPrice));
// 7. Round to precision
newPrice = this._round(newPrice);
this.currentPrice = newPrice;
this.tickCount++;
// ── Volume ──────────────────────────────── //
// Bigger price moves = more volume
const absMoveSize = Math.abs(pctChange) / this.volatility; // 0 to 1
const volume = this._generateVolume(absMoveSize);
this.totalVolume += volume;
// ── 24h stats ───────────────────────────── //
if (newPrice > this.high24h) this.high24h = newPrice;
if (newPrice < this.low24h) this.low24h = newPrice;
// ── Build tick ──────────────────────────── //
const change = this._round(newPrice - this.open24h);
const changePct = this._round(
((newPrice - this.open24h) / this.open24h) * 100,
);
// Spread — realistic bid/ask around price
const spreadPct = this.volatility * 0.5;
const halfSpread = this._round(newPrice * spreadPct);
const bid = this._round(newPrice - halfSpread);
const ask = this._round(newPrice + halfSpread);
return {
price: newPrice,
bid,
ask,
spread: this._round(ask - bid),
volume,
totalVolume: this._round(this.totalVolume),
change,
changePct,
high24h: this.high24h,
low24h: this.low24h,
open24h: this.open24h,
previous: this.previousPrice,
tickCount: this.tickCount,
};
}
/**
* Reset 24h stats — call this at start of new trading day
*/
resetDay() {
this.open24h = this.currentPrice;
this.high24h = this.currentPrice;
this.low24h = this.currentPrice;
this.totalVolume = 0;
}
/**
* Reset everything back to start price
*/
reset() {
this.currentPrice = this.startPrice;
this.previousPrice = this.startPrice;
this.high24h = this.startPrice;
this.low24h = this.startPrice;
this.open24h = this.startPrice;
this.totalVolume = 0;
this.tickCount = 0;
}
/**
* Get current state without advancing price
*/
getState() {
return {
price: this.currentPrice,
high24h: this.high24h,
low24h: this.low24h,
open24h: this.open24h,
totalVolume: this.totalVolume,
tickCount: this.tickCount,
};
}
// ── Private helpers ────────────────────────── //
/**
* Box-Muller transform — generates normal distribution random number
* Much more realistic than Math.random() which is flat distribution
* Most price moves are small, occasional large moves
*/
_randomNormal() {
let u = 0,
v = 0;
while (u === 0) u = Math.random();
while (v === 0) v = Math.random();
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
}
/**
* Generate volume correlated with price move size
* @param {number} intensity - 0 to 1, how big the move was
*/
_generateVolume(intensity) {
const { min, max } = this.volume;
const range = max - min;
// Higher intensity = higher volume
const base = min + range * Math.random();
const boost = range * intensity * 0.5;
return Math.round(base + boost);
}
/**
* Round to configured precision
*/
_round(value) {
return parseFloat(value.toFixed(this.precision));
}
}
module.exports = { PriceEngine };