UNPKG

hft-js

Version:

High-Frequency Trading in Node.js

164 lines (131 loc) 4.1 kB
/* * bar.ts * * Copyright (c) 2025 Xiongfei Shi * * Author: Xiongfei Shi <xiongfei.shi(a)icloud.com> * License: Apache-2.0 * * https://github.com/shixiongfei/hft.js */ import type { BarData, TapeData, TickData, Writeable } from "./typedef.js"; import type { IBarReceiver, ITickReceiver } from "./interfaces.js"; import { getBarVolume } from "./utils.js"; export type BarInfo = Writeable<BarData>; export class BarGenerator implements ITickReceiver { private readonly receivers: IBarReceiver[]; private readonly symbol: string; private readonly maxVolume: number; private shouldUpdate: number; private bar?: BarInfo; constructor(symbol: string, maxVolume = 0) { this.receivers = []; this.symbol = symbol; this.maxVolume = maxVolume; this.shouldUpdate = 0; } get receiverCount() { return this.receivers.length; } addReceiver(receiver: IBarReceiver) { if (!this.receivers.includes(receiver)) { if (receiver.onUpdateBar) { this.shouldUpdate += 1; } this.receivers.push(receiver); } } removeReceiver(receiver: IBarReceiver) { const index = this.receivers.indexOf(receiver); if (index >= 0) { if (receiver.onUpdateBar) { this.shouldUpdate -= 1; } this.receivers.splice(index, 1); } } onTick(tick: TickData, tape: TapeData) { if (tick.symbol !== this.symbol) { return; } const date = tick.date; const time = this.maxVolume > 0 ? tick.time : Math.floor(tick.time / 100) * 100; if (this.bar) { const isFinished = this.maxVolume > 0 ? this.bar.volume >= this.maxVolume : this.bar.date !== date || this.bar.time !== time; if (isFinished) { const bar = Object.freeze(this.bar); Object.freeze(bar.buyVolumes); Object.freeze(bar.sellVolumes); this.receivers.forEach((receiver) => receiver.onBar(bar)); this.bar = undefined; } } if (tape.volumeDelta === 0) { return; } if (!this.bar) { this.bar = this._createBar(date, time, tick); } this.bar.openInterest = tick.openInterest; this.bar.closePrice = tick.lastPrice; this.bar.highPrice = Math.max(this.bar.highPrice, tick.lastPrice); this.bar.lowPrice = Math.min(this.bar.lowPrice, tick.lastPrice); this.bar.volume += tape.volumeDelta; this.bar.amount += tape.amountDelta; switch (tape.direction) { case "up": this.bar.buyVolumes[tick.lastPrice] = tape.volumeDelta + (this.bar.buyVolumes[tick.lastPrice] ?? 0); this.bar.delta += tape.volumeDelta; break; case "down": this.bar.sellVolumes[tick.lastPrice] = tape.volumeDelta + (this.bar.sellVolumes[tick.lastPrice] ?? 0); this.bar.delta -= tape.volumeDelta; break; } if (tick.lastPrice !== this.bar.poc && tape.direction !== "none") { const tickVP = getBarVolume(this.bar, tick.lastPrice); const pocVP = getBarVolume(this.bar, this.bar.poc); if (tickVP > pocVP) { this.bar.poc = tick.lastPrice; } } if (this.shouldUpdate > 0) { const bar: BarData = Object.freeze({ ...this.bar, buyVolumes: Object.freeze({ ...this.bar.buyVolumes }), sellVolumes: Object.freeze({ ...this.bar.sellVolumes }), }); this.receivers.forEach((receiver) => { if (receiver.onUpdateBar) { receiver.onUpdateBar(bar, tick, tape); } }); } } private _createBar(date: number, time: number, tick: TickData): BarInfo { return { symbol: this.symbol, date: date, time: time, openInterest: tick.openInterest, openPrice: tick.lastPrice, highPrice: tick.lastPrice, lowPrice: tick.lastPrice, closePrice: tick.lastPrice, volume: 0, amount: 0, delta: 0, poc: tick.lastPrice, buyVolumes: {}, sellVolumes: {}, }; } } export const createBarGenerator = (symbol: string, maxVolume = 0) => new BarGenerator(symbol, maxVolume);