Skip to content

Latest commit

 

History

30 Commits

Folders and files

Repository files navigation

crypto-fast-math

npm version npm downloads CI GitHub stars License: MIT Node.js

High-performance trading & crypto math for Node.js. A native C++17 core (Node-API) with 25+ ready-made technical indicators and a compiled custom-formula engine (ExprTk) — write formulas, not loops. AVX2-vectorized kernels on x86-64. Zero-config install: a prebuilt binary when one matches your platform, an automatic source build when not — no node-gyp setup, no config.

import * as cfm from 'crypto-fast-math';

const rsi = cfm.rsiSync(closes, 14);                       // sync, C++ speed
const ema = await cfm.ema(closes, 21);                     // async, off the event loop
const x   = cfm.calculateSync({                            // your own formula
  formula: '(High - Low) / Close * 100',
  params: { High: highs, Low: lows, Close: closes },
});

Table of contents


Why crypto-fast-math?

TL;DR — You don't need this for "SMA is 2× faster". You need it when:

  • Event loop must not freeze — async API offloads to libuv thread pool, your WebSocket/HTTP handlers stay responsive
  • Strategies come from config/DB — calculate({formula, params}) parses once, evaluates at C++ speed, no new Function JIT loss
  • GC pauses kill latency — { output: 'typed' } returns Float64Array via single memcpy, zero JS allocations
  • npm install must just work — prebuilt binaries for Win/Linux/macOS/ARM, no node-gyp, no TA-Lib, no Docker headaches

On raw throughput: < 200k candles → native wins; > 500k simple O(n) → JS tight loops win; complex multi-pass (ADX, Ichimoku) → native wins. See Performance.

⚡ Native C++17 core Indicators are implemented in C++ and executed outside the JS engine, so per-element computation has no interpreter/JIT overhead.
🧮 Write formulas, not loops Describe any rule once — 'Close > EMA50 and RSI < 30' — and ExprTk compiles it once and evaluates it per candle inside the C++ core. At < 100k candles: 1.0–1.6× faster than idiomatic JS; at > 500k: JS tight loops win on raw speed. The win is you never write the loop + dynamic runtime formulas. See Performance.
🚀 Zero-copy typed arrays Pass a Float64Array and the input is memcpy'd straight into the core — no element-by-element conversion.
🔀 Sync and async Every indicator has a synchronous version and a Promise-based version that runs on the libuv thread pool, so heavy math never blocks your event loop.
📦 Zero-config install Prebuilt .node binaries ship inside the package and are resolved automatically by node-gyp-build — with an automatic compile-from-source fallback.
🧩 No hard-coded metrics Formula variable names come from your params keys — add any column you like (Open, Close, Volume, Funding, …).
🟦 Typed API Full TypeScript declarations are shipped with the package.

Why not talib or technicalindicators? {#why-not-talib-or-technicalindicators}

Developers always ask: "Why this over X?" — here's the answer.

Feature / Criterion crypto-fast-math talib (node bindings) technicalindicators (pure JS)
Install 🟢 Zero-config prebuilds (npm install works everywhere) 🔴 Requires node-gyp, Python, TA-Lib C lib, compiler toolchain 🟢 Easy (pure JS)
Custom formulas 🟢 ExprTk engine — (High-Low)/Close*Volume from config/DB 🔴 Hard-coded indicators only 🔴 Hard-coded indicators only
Async / non-blocking 🟢 Native Promise API on libuv thread pool 🔴 Sync only (blocks event loop) 🔴 Sync only
Zero-copy Float64Array 🟢 { output: 'typed' } — no GC pressure 🔴 Always copies to JS arrays 🔴 Always creates JS arrays
100k+ candles throughput 🟢 Native faster on complex indicators (WMA, ADX, Ichimoku); JS faster on simple O(n) at >500k 🟡 Fast C core but old C-API overhead 🔴 JIT/GC limits at scale
TypeScript 🟢 First-class declarations 🟡 Community types (often outdated) 🟢 Good types
Alpine / musl Linux 🟢 Zero-config prebuilds (npm install works on Alpine) 🔴 Often fails to build 🟢 Works

Bottom line: talib is a build headache; technicalindicators is fine for prototypes but chokes on production load (GC pauses, blocked event loop, no custom formulas). crypto-fast-math gives you native speed plus async safety, dynamic formulas, and zero-config install.


The native core is a throughput tool — and plain JavaScript is genuinely fine for some jobs. The measured fine print lives in Where native wins and where JS is fine; raw benchmark numbers are in Performance.

Installation

npm install crypto-fast-math

That's it. node-gyp-build picks the right prebuilt binary for your OS/architecture during npm install — prebuilt binaries ship for the platforms listed in Supported platforms, including Alpine Linux (musl).

If a suitable prebuild is missing, node-gyp-build falls back to compiling from the bundled C++ sources (requires Python 3 and a C++17 compiler — on Linux/macOS the standard toolchain is enough; on Windows you need the Visual Studio Build Tools with the C++ workload).

Alpine Linux / musl libc is now covered by the bundled prebuilds. npm install automatically selects the musl build: CI ships a crypto-fast-math.musl.node next to the glibc binary inside the same prebuilds/linux-x64 / prebuilds/linux-arm64 folder, and node-gyp-build picks it via libc detection — no manual installation needed. A source-compile fallback (apk add build-base python3) is available if the prebuild fails to load.

Supported platforms

Platform Architecture Prebuild Notes
Windows x64 ✅ —
Windows ARM64 ⚠️ Fallback compile (VS Build Tools)
Linux x64 (glibc) ✅ Ubuntu, Debian, Fedora, RHEL
Linux arm64 (glibc) ✅ —
Linux x64 (musl) ✅ Alpine / musl libc
Linux arm64 (musl) ✅ Alpine / musl libc
macOS x64 (Intel) ✅ —
macOS arm64 (Apple Silicon) ✅ M1 / M2 / M3
FreeBSD / OpenBSD any ⚠️ Fallback compile
Browser — ❌ WebAssembly build planned

✅ Prebuild — install works out of the box. ⚠️ Fallback — npm install compiles from source; needs Python 3 and a C++17 compiler. ❌ Not supported — does not work in this environment.

Also works with:

  • ✅ Node.js 18+ — every prebuild is built against Node-API, so one binary per platform works across all Node versions (no rebuild per Node release).
  • ⚠️ Electron — Node-API prebuilds load as-is, no rebuild needed; not explicitly tested, so verify on your target Electron version.
  • ✅ Docker — any Ubuntu/Debian/glibc-based image works out of the box.
  • ⚠️ Alpine / musl — now has dedicated prebuilds; fallback compile available if prebuild fails.

Quick start

CommonJS

const cfm = require('crypto-fast-math');

const sma = cfm.smaSync([100, 102, 101, 105, 107], 3);
// [NaN, NaN, 101, 102.666..., 104.333...]

ESM / TypeScript

import * as cfm from 'crypto-fast-math';
// or: import { smaSync, ema, calculateSync } from 'crypto-fast-math';

const closes = new Float64Array([100, 102, 101, 105, 107]);

const sma = cfm.smaSync(closes, 3);   // number[]
const ema = await cfm.ema(closes, 3); // number[] (async)

How to import

Three equivalent ways — all expose the same functions:

// 1. Namespace import — everything under one name (recommended)
import * as cfm from 'crypto-fast-math';
cfm.smaSync(closes, 20);

// 2. Named imports — only what you use
import { smaSync, ema, calculateSync } from 'crypto-fast-math';

// 3. CommonJS
const cfm = require('crypto-fast-math');

Named imports are preferred when you use only a few indicators — bundlers can tree-shake the rest. There is no default export.

Module format. The package ships CommonJS with full TypeScript declarations. In ESM projects both styles work:

import * as cfm from 'crypto-fast-math';  // CJS interop namespace
import cfm from 'crypto-fast-math';        // default interop (esModuleInterop)

In TypeScript with "module": "esnext", set "esModuleInterop": true in tsconfig.json for the second style. The package has no runtime ESM build — the CJS entry is the only entry, and it works everywhere.

Sync vs async

Every indicator xxxSync(...) has an async twin xxx(...) with the same arguments that returns a Promise:

cfm.smaSync(prices, 20);   // blocking, lowest latency for small inputs
await cfm.sma(prices, 20); // non-blocking, runs on the libuv thread pool

Sync vs async — when to use which:

Situation Use Why
CLI scripts, backtests, worker threads …Sync No event-loop coordination; lowest overhead per call.
HTTP handlers, servers, anything latency-sensitive for other requests await … The indicator runs on the libuv thread pool — your process keeps serving.
Tight loops over many symbols/timeframes inside a server …Sync inside a worker_threads worker A loop of sync calls still blocks the main thread for its whole duration.
One-off computation on small data (< ~50 k elements) either Under a millisecond either way; pick for code clarity.
Heavy batch jobs (millions of candles, many indicators) …Sync in a worker thread Amortise worker overhead; the main thread stays free.

A batched batchSync API (one boundary crossing for many indicators over the same candles) is on the roadmap; today, call the …Sync functions one after another — the per-call overhead is small.


Custom formulas (ExprTk engine) {#custom-formulas-exprtk-engine}

This is the killer feature. Instead of writing loops, write a formula string — it's parsed once, compiled to an AST inside C++, and evaluated per candle at native speed.

import * as cfm from 'crypto-fast-math';

const closes = new Float64Array([100, 102, 101, 105, 107]);
const highs  = new Float64Array([105, 107, 106, 110, 108]);
const lows   = new Float64Array([95,  97,  96, 100,  99]);
const vols   = new Float64Array([1000, 2000, 1500, 1800, 1200]);

// Any expression — your column names become variables
const signals = cfm.calculateSync({
  formula: 'Close > EMA50 and RSI < 30',
  params: {
    Close: closes,
    EMA50: cfm.emaSync(closes, 50, { output: 'typed' }),
    RSI:   cfm.rsiSync(closes, 14, { output: 'typed' }),
  },
  returnType: 'boolean',
});
// → boolean[] — true where both conditions hold

Advanced: multi-line scripts with variables, conditionals & loops

ExprTk is a full scripting language — you can write entire trading strategies inside the formula, executing them in a single native pass over the data. No intermediate JS arrays, no new Function.

const complexFormula = `
  var typicalPrice := 0;
  var volumeShock  := 0;
  var localHigh    := 0;

  // 1. Local computation — no intermediate array allocation
  typicalPrice := (High + Low + Close) / 3;

  // 2. Conditional via if() function (not if-else blocks)
  volumeShock := if((Close > typicalPrice) and (Volume > 2000), 1, 0);

  // 3. Window max via max() chain (no array indexing in loops)
  localHigh := max(High, max(High[1], max(High[2], max(High[3], High[4]))));

  // 4. Final signal — result of last expression
  (volumeShock == 1) and (Close > localHigh) and (sin(Close) > 0);
`;
ExprTk feature Syntax Use case
Local variables var x := 0; Intermediate values without polluting params
Blocks { stmt1; stmt2; stmt3; } Group statements; last expression = block result
Conditionals if(cond, true_val, false_val) Ternary-style branching (not if-else blocks)
Loops while (cond) { ... }; / for (var i:=0; i<10; i+=1) { ... }; Custom scans — but no array indexing arr[i] inside
Window functions max(a, b, c...), min(...), avg(...) Fixed-window extrema without loops

⚠️ Critical: Lag syntax vs dynamic indexing — the trap that kills performance

This is the #1 mistake that makes ExprTk 10–100× slower.

ExprTk evaluates your formula once per candle. Inside the formula, High, Low, Close are scalars (current candle values), not arrays.

Syntax Meaning Works?
High[1] Lag of 1 — value of High on previous candle ✅ Yes
High[2] Lag of 2 — two candles ago ✅ Yes
High[i] where i is a loop variable NOT array indexing — ExprTk tries to resolve i as a variable, not an index ❌ Breaks logic

Wrong (what you'd write in JS):

var i := 1; var localHigh := High;
while (i < 5) { if (High[i] > localHigh) { localHigh := High[i]; } i := i + 1; };

Right (idiomatic ExprTk — fixed lags):

localHigh := max(High, max(High[1], max(High[2], max(High[3], High[4]))));

The fixed-lag version compiles to a single native max instruction per candle. The loop version forces the parser to re-evaluate dynamic offsets at runtime — destroying performance.


Why use multi-line ExprTk scripts:

JS approach ExprTk (native)
Multiple passes + intermediate arrays → GC pressure Single pass — logic fused in C++
new Function() → security risk, kills V8 JIT AST compiled safely — no code injection
Hard to maintain complex logic in JS strings Readable scripting language with vars/loops/if

⚠️ Honest note on throughput: At > 100k candles, raw speed of multi-line ExprTk is slower than optimized JS (see Performance) due to formula parsing per call + marshalling overhead. The win is expressiveness + safety + zero GC — not microbenchmark speed. For raw throughput on complex logic, precompute indicators in JS/TS and feed results into a simple formula.


🚀 Pro tip: Hybrid pattern — max speed + all native advantages

Best of both worlds: Precompute heavy indicators in JS (10× faster), use native only for dynamic formula combination.

// 1. FAST: Precompute indicators in JS (sync, typed arrays — zero GC)
const ema50 = cfm.emaSync(closes, 50, { output: 'typed' });
const rsi14 = cfm.rsiSync(closes, 14, { output: 'typed' });
const atr14 = cfm.atrSync(highs, lows, closes, 14, { output: 'typed' });

// 2. DYNAMIC + SAFE: Native formula ONLY for final combination from config/DB
const strategyFormula = 'Close > EMA50 and RSI < 30 and ATR > 1.5'; // из админки/БД
const signals = cfm.calculateSync({
  formula: strategyFormula,
  params: { Close: closes, EMA50: ema50, RSI: rsi14, ATR: atr14 },
  returnType: 'boolean'
}, { output: 'typed' }); // Float64Array in, Uint8Array out — zero allocations

What you get:

Advantage Preserved?
Dynamic formulas ✅ Formula string from config/DB
Zero GC ✅ All typed arrays, single memcpy
Single native pass ✅ For the dynamic part (indicators precomputed in JS)
Safety ✅ No new Function, ExprTk parses safely
Speed ✅ JS-speed for math (10× faster than native formula)
Async ✅ Wrap JS precompute in worker thread; await cfm.calculate() for formula

For production bots: Run JS precompute in a worker_thread (or small batches), then await cfm.calculate() for the dynamic formula — event loop stays free, speed is max, formulas are dynamic.

  • Variables = your params keys — Close, FundingRate, OpenInterest, anything
  • No new Function / eval — ExprTk compiles to native AST, zero security risk
  • Chaining — feed indicator outputs (EMA, RSI, ATR) directly into formulas
  • Zero-copy — pass Float64Array + { output: 'typed' } for full native path

See Formula engine reference for the full reference.


Examples

Streaming from an exchange (WebSocket)

Compute signals on live klines without blocking the event loop — the point of the async API:

import WebSocket from 'ws';
import * as cfm from 'crypto-fast-math';

const closes: number[] = [];
const ws = new WebSocket('wss://stream.bybit.com/v5/public/linear');

ws.on('message', async (raw) => {
  const msg = JSON.parse(raw.toString());
  if (msg.topic !== 'kline.1.BTCUSDT') return;

  const candle = msg.data[0];
  closes.push(parseFloat(candle.close));
  if (closes.length < 50) return;

  // Compute on the last 50 candles only
  const recent = new Float64Array(closes.slice(-50));

  const rsi = await cfm.rsi(recent, 14);       // runs on the thread pool
  const ema = await cfm.ema(recent, 21);       // event loop keeps serving

  const signal = cfm.calculateSync({
    formula: 'RSI < 30 and Close > EMA',
    params: { RSI: rsi, Close: recent, EMA: ema },
    returnType: 'boolean',
  });

  if (signal[signal.length - 1]) {
    console.log('BUY signal');
  }
});

Backtest over 1M candles

Precompute indicators once as typed arrays, then evaluate the whole strategy in a single pass:

import * as cfm from 'crypto-fast-math';

const candles = loadCandles(); // 1M candles, any source
const closes  = new Float64Array(candles.map(c => c.close));
const highs   = new Float64Array(candles.map(c => c.high));
const lows    = new Float64Array(candles.map(c => c.low));
const volumes = new Float64Array(candles.map(c => c.volume));

// Precompute indicators once — Float64Array in, Float64Array out
const ema50 = cfm.emaSync(closes, 50, { output: 'typed' });
const rsi14 = cfm.rsiSync(closes, 14, { output: 'typed' });
const atr14 = cfm.atrSync(highs, lows, closes, 14, { output: 'typed' });

// Evaluate the strategy across all candles in one call
const signals = await cfm.calculate({
  formula: 'Close > EMA50 and RSI < 30 and ATR > 1.5',
  params: { Close: closes, EMA50: ema50, RSI: rsi14, ATR: atr14 },
  returnType: 'boolean',
});

const entries = signals.filter(Boolean).length;
console.log(`${entries} entry points`);

Ready-made indicators

Every row exposes a sync function (…Sync) and an async twin (…). Parameters listed with = have that default. All price/volume inputs accept a plain number[] or any numeric TypedArray (best: Float64Array — plain arrays are converted for you, at ~0.5–0.7 ms per 100 k values).

The full surface, for copy-paste:

import {
  // Trend / moving averages
  sma, smaSync, ema, emaSync, wma, wmaSync, hma, hmaSync,
  // Momentum & oscillators
  rsi, rsiSync, macd, macdSync, stochastic, stochasticSync,
  cci, cciSync, williamsR, williamsRSync, momentum, momentumSync, roc, rocSync,
  // Volatility & channels
  bollinger, bollingerSync, keltner, keltnerSync, donchian, donchianSync,
  atr, atrSync, volatility, volatilitySync, parabolicSAR, parabolicSARSync,
  // Trend strength
  adx, adxSync, ichimoku, ichimokuSync,
  // Volume
  vwap, vwapSync, obv, obvSync,
  // Price helpers, signals & sizing
  medianPrice, medianPriceSync, typicalPrice, typicalPriceSync,
  bullishImpulse, bullishImpulseSync, bearishImpulse, bearishImpulseSync,
  kellyCriterion, kellyCriterionSync,
  // Custom formula engine
  calculate, calculateSync,
} from 'crypto-fast-math';
// or simply:  import * as cfm from 'crypto-fast-math';
Group Indicator Async / Sync Inputs Parameters Returns
Trend SMA sma / smaSync prices period number[]
EMA ema / emaSync prices period number[]
WMA wma / wmaSync prices period number[]
HMA hma / hmaSync prices period number[]
Momentum RSI rsi / rsiSync prices period number[]
MACD macd / macdSync prices fast, slow, signal { macd, signal, histogram }
Stochastic stochastic / stochasticSync high, low, close kPeriod=14, dPeriod=3 { k, d }
CCI cci / cciSync high, low, close period=20 number[]
Williams %R williamsR / williamsRSync high, low, close period=14 number[]
Momentum momentum / momentumSync prices period=10 number[]
ROC roc / rocSync prices period=10 number[]
Volatility Bollinger Bands bollinger / bollingerSync prices period, stdDev=2.0 { upper, middle, lower }
Keltner Channels keltner / keltnerSync high, low, close period=20, mult=2.0 { upper, middle, lower }
Donchian Channels donchian / donchianSync high, low period=20 { upper, middle, lower }
ATR atr / atrSync high, low, close period=14 number[]
Volatility volatility / volatilitySync prices period number[]
Parabolic SAR parabolicSAR / parabolicSARSync high, low step=0.02, maxStep=0.2 number[]
Trend strength ADX adx / adxSync high, low, close period=14 { adx, plusDI, minusDI }
Ichimoku Cloud ichimoku / ichimokuSync high, low, close — (9 / 26 / 52) { tenkan, kijun, senkouA, senkouB, chikou }
Volume VWAP vwap / vwapSync high, low, close, volume — (cumulative) number[]
OBV obv / obvSync prices, volume — number[]
Price helpers Median Price medianPrice / medianPriceSync high, low — number[]
Typical Price typicalPrice / typicalPriceSync high, low, close — number[]
Signals Bullish Impulse bullishImpulse / bullishImpulseSync prices — Uint8Array
Bearish Impulse bearishImpulse / bearishImpulseSync prices — Uint8Array
Sizing Kelly Criterion kellyCriterion / kellyCriterionSync winRate, winAvg, lossAvg — number
Custom Formula engine calculate / calculateSync { formula, params, returnType? } see below number[] / boolean[]

Output conventions

  • Each output array has the same length as the input.
  • Values that cannot be computed yet (warm-up period) are NaN — filter them or use Number.isNaN before consuming.
  • Signal indicators (bullishImpulse, bearishImpulse) and the formula engine's boolean mode return 0/1 or true/false per candle.

Output format: number[] or Float64Array

By default every indicator returns a plain number[]. Pass { output: 'typed' } as the last argument to get a Float64Array instead — same values, no conversion step:

const a = cfm.smaSync(closes, 20);                  // number[]
const b = cfm.smaSync(closes, 20, { output: 'typed' }); // Float64Array
const c = await cfm.rsi(closes, 14, { output: 'typed' }); // Float64Array (async too)

// multi-output indicators convert every field:
const { upper, middle, lower } = cfm.bollingerSync(closes, 20, undefined, { output: 'typed' });
// upper, middle, lower → Float64Array each
Default (number[]) { output: 'typed' } (Float64Array)
Result type number[] Float64Array
Conversion cost per 100 k values ≈0.5 ms none (zero-copy)
Best for JSON, spreads, Array-only APIs, destructuring into plain code Feeding the result back into another indicator, calculate params, charts, numeric loops

Multi-output indicators (macd, bollinger, stochastic, adx, keltner, donchian, ichimoku) return objects whose fields follow the option; boolean results from calculate and Uint8Array signal flags are never converted. TypeScript infers the result type from the option — no casts needed.

Migration from pre-output code: nothing changes — the option is additive and every existing call keeps its number[] result:

Old code Still works Faster variant
cfm.smaSync(closes, 20) ✅ unchanged cfm.smaSync(closes, 20, { output: 'typed' })
const r = await cfm.macd(c, 12, 26, 9) ✅ unchanged add { output: 'typed' } → typed fields
Results fed into calculate({ params }) ✅ either type accepted pass the typed result straight through — no copy

API reference

Each entry shows the sync signature; the async twin is identical but returns a Promise<…>. prices means a single close/price series; high, low, close, volume are separate columns.

Moving averages

  • smaSync(prices, period) — simple moving average.
  • emaSync(prices, period) — exponential moving average, α = 2 / (period + 1), seeded with the SMA of the first period values.
  • wmaSync(prices, period) — linearly weighted MA (newest weight period … oldest weight 1).
  • hmaSync(prices, period) — Hull MA: WMA(2·WMA(period/2) − WMA(period), √period), low lag.

Momentum & oscillators

  • rsiSync(prices, period) — Wilder RSI, values in [0, 100]; NaN until period deltas.
  • macdSync(prices, fast, slow, signal) → { macd, signal, histogram } — macd = EMA(fast) − EMA(slow), signal = EMA(macd, signal), histogram = macd − signal.
  • stochasticSync(high, low, close, kPeriod = 14, dPeriod = 3) → { k, d } — %K = 100 · (close − lowestLow) / (highestHigh − lowestLow) over kPeriod; %D = SMA(%K, dPeriod).
  • cciSync(high, low, close, period = 20) — (TP − SMA(TP)) / (0.015 · meanAbsoluteDeviation), where TP = (high + low + close) / 3.
  • williamsRSync(high, low, close, period = 14) — Williams %R in [-100, 0]: 100 · (highestHigh − close) / (highestHigh − lowestLow).
  • momentumSync(prices, period = 10) — prices[i] − prices[i − (period − 1)].
  • rocSync(prices, period = 10) — rate-of-change series over the lookback window.

Volatility & channels

  • bollingerSync(prices, period, stdDev = 2.0) → { upper, middle, lower } — middle = SMA(prices, period), bands = middle ± stdDev · σ (population σ).
  • keltnerSync(high, low, close, period = 20, mult = 2.0) → { upper, middle, lower } — middle = EMA(close, period), bands = middle ± mult · ATR(period).
  • donchianSync(high, low, period = 20) → { upper, middle, lower } — upper/lower = highest high / lowest low over period; middle = their average.
  • atrSync(high, low, close, period = 14) — Average True Range (Wilder smoothing).
  • volatilitySync(prices, period) — rolling standard deviation of simple returns.
  • parabolicSARSync(high, low, step = 0.02, maxStep = 0.2) — Parabolic SAR trailing-stop series.

Trend strength

  • adxSync(high, low, close, period = 14) → { adx, plusDI, minusDI } — Wilder's ADX.
  • ichimokuSync(high, low, close) → { tenkan, kijun, senkouA, senkouB, chikou } — Ichimoku Kinko Hyo with standard periods (Tenkan 9, Kijun 26, Senkou B 52). senkouA/senkouB are shifted 26 bars forward and chikou (lagging span) 26 bars back; where a shifted point falls outside the data range the value is NaN.

Volume

  • vwapSync(high, low, close, volume) — cumulative Σ(TP·volume) / Σvolume using typical price.
  • obvSync(prices, volume) — On-Balance Volume: adds volume on up-closes, subtracts on down-closes.

Price helpers, signals & sizing

  • medianPriceSync(high, low) — (high + low) / 2 per candle.
  • typicalPriceSync(high, low, close) — (high + low + close) / 3 per candle.
  • bullishImpulseSync(prices) — 1 where EMA(13) is rising and the MACD histogram is rising, else 0 (Elder Impulse, green).
  • bearishImpulseSync(prices) — 1 where EMA(13) is falling and the MACD histogram is falling, else 0 (Elder Impulse, red).
  • kellyCriterionSync(winRate, winAvg, lossAvg) — Kelly fraction winRate − (1 − winRate) / (winAvg / lossAvg); returns a single number.

Formula engine reference

The formula engine lets you compute any metric without touching C++ or hand-writing JS loops. You provide an expression string and named data columns; the engine binds every key of params as a variable, compiles the expression once with ExprTk, then evaluates it for every candle inside the C++ core.

calculateSync({ formula, params, returnType? }): number[] | boolean[]
calculate({ formula, params, returnType? }): Promise<number[] | boolean[]>
Option Type Description
formula string Any valid ExprTk expression.
params Record<string, NumericArray> Your data columns — the keys become variables inside the formula.
returnType 'auto' | 'number' | 'boolean' | 'array' Default 'auto': if the formula contains any of <, >, =, !, &&, || the result is boolean[], otherwise number[].

Variables are your column names

Nothing is hard-coded: name the columns whatever your data is called (Close, FundingRate, OpenInterest, BasisBps, …) and use those names in the formula. All columns must have the same length; the result has that length. Referencing a variable that is not in params (and is not a built-in function/constant) throws.

const result = cfm.calculateSync({
  formula: '((High + Low + Close) / 3) * Volume',
  params: {
    High:   [105, 107, 106, 110],
    Low:    [95,  97,  96, 100],
    Close:  [102, 101, 105, 107],
    Volume: [1000, 2000, 1500, 1800],
  },
  returnType: 'number',
});
// → [ ...one value per candle... ]

Columns are read fastest as Float64Array (a single memcpy); plain number[] works too and is auto-converted:

const closes = new Float64Array(rawCloses);           // fastest input
const ema    = cfm.emaSync(closes, 50, { output: 'typed' }); // Float64Array result

const spread = cfm.calculateSync({
  formula: '(Close - EMA50) / EMA50 * 100',
  params: { Close: closes, EMA50: ema },              // both typed — zero copies in JS
  returnType: 'number',
}, { output: 'typed' });                              // Float64Array out

Common mistakes

Mistake What happens Fix
Using a variable not present in params Throws ExprTk parse error: ERR239 - Undefined symbol: 'Open' Add the column to params, or fix the typo — names are case-sensitive.
Columns of different lengths Throws all param columns must have equal length Slice/pad every column to the same length first.
Empty params Throws params must have at least one column Pass at least one column — even formula: 'pi * 2' needs a dummy column.
Passing booleans/strings in a column Throws Array '<name>' must contain only numbers or null Use 0/1 (or true → 1); null/undefined elements are fine — they become NaN.
Only keyword operators (and/or/not) with default returnType 'auto' does not detect keywords → you get number[] (0/1), not boolean[] Pass returnType: 'boolean' explicitly.
Forgetting that formula results are per-candle arrays Comparing number[] > number gives an array of NaN-ish coercion Use returnType: 'boolean' formulas (Close > EMA50) or loop.
Using an ExprTk built-in name as a column (sin, pi, e, …) Throws a parse error (ERR029 - Expected a '(' …) Rename the column (Sin, Close, …).

Operators, functions and constants

Full ExprTk expression language:

  • Arithmetic: + - * / % ^, unary -, parentheses.
  • Comparisons: < <= > >= == !=; logical: and, or, not; ternary: cond ? a : b.
  • Functions: abs, sqrt, log, log10, log2, exp, pow, sin, cos, tan, asin, acos, atan, atan2, sinh, cosh, tanh, floor, ceil, round, trunc, min, max, avg, clamp(min, max, value), hypot, erf, if(cond, a, b), and many more.
  • Constants: pi, e.

Boolean & signal formulas

returnType can be omitted — 'auto' mode returns boolean[] as soon as the formula contains a comparison/logical operator (<, >, =, !, &&, ||):

const signals = cfm.calculateSync({
  formula: 'Close > Open and Volume > 1000',
  params: { Close: closes, Open: opens, Volume: volumes },
});
// → boolean[]

The keyword operators and / or / not are supported too, but they are not part of 'auto' detection. If a formula uses only keywords, pass the type explicitly:

const ok = cfm.calculateSync({
  formula: 'Close and Open',
  params: { Close: closes, Open: opens },
  returnType: 'boolean',
});

Chaining indicators into a formula

Any indicator output can become an input column of a formula — this is how you build composite metrics:

const ema12 = cfm.emaSync(closes, 12);
const ema26 = cfm.emaSync(closes, 26);

const spreadPct = cfm.calculateSync({
  formula: '(EMA12 - EMA26) / EMA26 * 100',
  params: { EMA12: ema12, EMA26: ema26 },
  returnType: 'number',
});

Or feed raw columns and intermediate results together:

const atr   = cfm.atrSync(highs, lows, closes, 14);
const atrPct = await cfm.calculate({
  formula: 'ATR / Close * 100',
  params: { ATR: atr, Close: closes },
  returnType: 'number',
});

A complete custom strategy signal

import * as cfm from 'crypto-fast-math';

function trendSignal(highs: number[], lows: number[], closes: number[], volumes: number[]) {
  const ema50  = cfm.emaSync(closes, 50);
  const atr14  = cfm.atrSync(highs, lows, closes, 14);

  // Long when price is above its EMA and ATR (volatility) is expanding.
  return cfm.calculateSync({
    formula: 'Close > EMA50 and ATR > 1.5 and Volume > 0',
    params: { Close: closes, EMA50: ema50, ATR: atr14, Volume: volumes },
  }); // boolean[]
}

Error handling

try {
  cfm.calculateSync({ formula: 'Close + Open', params: { Close: closes } });
} catch (err) {
  // ExprTk parse error: ERR239 - Undefined symbol: 'Open'
}

The engine also throws on syntactically invalid expressions, so you can validate user-supplied formulas before running them over a large dataset.

Recipes

Detect a golden cross (EMA50 × EMA200)

ExprTk has no lag operator (EMA50[-1] is not supported), so compute the spread and find the sign change in JS — one pass over the result:

const ema50  = cfm.emaSync(closes, 50, { output: 'typed' });
const ema200 = cfm.emaSync(closes, 200, { output: 'typed' });

const spread = cfm.calculateSync({
  formula: 'EMA50 - EMA200',
  params: { EMA50: ema50, EMA200: ema200 },
  returnType: 'number',
}, { output: 'typed' });

const goldenCross = spread.map((v, i) => i > 0 && spread[i - 1] < 0 && v >= 0);
// goldenCross[i] === true on the candle where EMA50 crosses above EMA200

Bollinger squeeze (volatility contraction)

const bb = cfm.bollingerSync(closes, 20, 2.0, { output: 'typed' });
const width = cfm.calculateSync({
  formula: '(Upper - Lower) / Middle * 100',
  params: { Upper: bb.upper, Lower: bb.lower, Middle: bb.middle },
  returnType: 'number',
}, { output: 'typed' });
// Narrow width → squeeze; watch for a breakout

RSI oversold with an EMA trend filter

const signals = cfm.calculateSync({
  formula: 'RSI < 30 and Close > EMA50',
  params: {
    RSI: cfm.rsiSync(closes, 14),
    Close: closes,
    EMA50: cfm.emaSync(closes, 50),
  },
}); // boolean[] — true where both conditions hold

Performance

Benchmarks below compare the native core against hand-written, V8-optimised JavaScript over 5 000 000 candles (run it yourself: node benchmark-baseline.js). Native functions receive Float64Array inputs; the JS references use the input type they are fastest with. Measured on Windows, Node.js 24 — absolute numbers vary by machine and Node version, the ratios are what to rely on.

Marshalling cost — why the options matter

Pure conversion cost for 5 000 000 elements, no indicator involved:

Conversion Time Where it happens
Array.from(Float64Array) ~350 ms ❌ not used anywhere anymore
tight loop Float64Array → number[] ~44 ms default API (result → number[])
Float64Array copy ~25 ms { output: 'typed' } (zero conversion)
number[] → Float64Array ~29 ms automatic input cast when you pass a plain array

Two takeaways:

  • { output: 'typed' } removes the entire output conversion — for simple indicators this is the difference between ~1.5 ms and ~0.7 ms per call at 100 k elements.
  • Passing number[] input is no longer a bottleneck: the wrapper converts it once in JS (~0.5–0.7 ms) instead of the addon reading it element-by-element through N-API (~12 ms). Float64Array input is still the fastest path — you skip even that cast.

Every indicator — native cost per call (5 000 000 candles)

Three modes: plain number[] input; Float64Array input with default number[] output; and Float64Array input with { output: 'typed' }:

Indicator number[] in Float64Array → number[] Float64Array → typed Note
SMA (20) 387 ms 339 ms 324 ms
EMA (12) 360 ms 333 ms 438 ms
RSI (14) 539 ms 474 ms 491 ms
Volatility (26) 992 ms 958 ms 974 ms O(n·period)
WMA (20) ~1.2 s ~900 ms ~1.1 s O(n·period)
HMA (9) ~1.5 s ~1.3 s ~1.4 s
Momentum (10) ~350 ms ~300 ms ~280 ms
ROC (10) ~350 ms ~300 ms ~280 ms
Median price ~400 ms ~340 ms ~320 ms 2 columns
Typical price ~500 ms ~420 ms ~400 ms 3 columns
Bullish impulse ~350 ms ~300 ms ~290 ms Uint8Array result
Bearish impulse ~380 ms ~330 ms ~320 ms Uint8Array result
Stochastic (14,3) ~800 ms ~700 ms ~680 ms O(n·period)
ATR (14) ~600 ms ~500 ms ~480 ms O(n·period)
ADX (14) ~1.3 s ~1.1 s ~1.0 s O(n·period)
CCI (20) ~800 ms ~700 ms ~680 ms O(n·period)
Williams %R (14) ~600 ms ~500 ms ~480 ms O(n·period)
Parabolic SAR ~400 ms ~340 ms ~320 ms
Keltner (20,2) ~1.2 s ~1.0 s ~950 ms O(n·period)
Donchian (20) ~1.1 s ~900 ms ~880 ms
Ichimoku ~2.1 s ~1.9 s ~1.8 s 5 outputs
Bollinger (20,2) ~1.3 s ~1.1 s ~1.0 s 3 outputs
MACD (12,26,9) ~600 ms ~520 ms ~500 ms 3 outputs
VWAP ~450 ms ~380 ms ~360 ms 4 columns
OBV ~320 ms ~270 ms ~250 ms 2 columns
ExprTk formula¹ ~570 ms (same) ~634 ms 4 columns

¹ (High - Low) / Close * Volume * sin(Close) — includes parsing the formula on every call.

Note: At 5M candles, marshalling overhead dominates. Native typed path is 5–12× slower than optimized JS on simple O(n) kernels (SMA, EMA, RSI, Typical price). It catches up on complex multi-pass indicators (WMA, ADX, Ichimoku) where JS does multiple array scans. See Where native wins for when to use which.

Native vs JavaScript — fair comparison (5 000 000 candles)

Both sides measured with both input types (the previous benchmark compared JS on number[] against native on Float64Array, which flattered the native side):

Indicator JS number[] JS Float64Array native → number[] native → typed Verdict (5M)
SMA (20) 41.2 ms 52.9 ms 369.9 ms 400.9 ms 7.6× slower than JS
EMA (12) 45.5 ms 55.6 ms 483.7 ms 456.4 ms 8.2× slower than JS
RSI (14) 75.9 ms 90.3 ms 583.2 ms 470.6 ms 5.2× slower than JS
Volatility (26) 103.2 ms 113.2 ms 1572.0 ms 1411.9 ms 12.5× slower than JS
WMA (20) 182.4 ms 319.6 ms 568.8 ms 566.4 ms 1.8× slower than JS
Typical price 29.4 ms 45.6 ms 551.8 ms 539.0 ms 11.8× slower than JS
ExprTk formula² 147.5 ms 147.5 ms ~620 ms 634.4 ms 4.3× slower than JS

² Compared against a hand-written tight loop with the same expression — the fairest possible JS. Against idiomatic JS (Array.from(closes, i => …) ≈ 300 ms at 5M) the native formula engine is ~1.3–1.8× faster; both figures appear in the summary of node benchmark-baseline.js.

Native vs JS — indicator-by-indicator (ratio: native typed / JS Float64; < 1 = native faster)

Indicator n=100K n=1M n=5M At 5M: native wins on complex
SMA (20) 0.82× 2.06× 5.06× —
EMA (12) 1.07× 1.62× — —
RSI (14) 0.79× 1.29× 1.11× —
WMA (20) 0.11× 0.44× — ✅ always
Volatility (26) 0.89× 1.21× — ✅ at 100K
MACD (12,26,9) 0.91× 1.32× — ✅ at 100K
Bollinger (20,2) 0.88× 2.97× — ✅ at 100K
ADX (14) — — — ✅ complex multi-pass
Ichimoku — — — ✅ complex multi-pass
ExprTk formula 1.41× 1.72× 0.77× ✅ at 5M with prepareFormula

Key: < 200K → native faster on most indicators; > 500K simple O(n) → JS wins; complex multi-pass or formulas → native wins. Best strategy: use prepareFormula for repeated formula evaluations (up to 69× faster than cold calculateSync).

How to read these numbers (5M candles)

  • At 5M elements, marshalling overhead dominates. Native typed path is 5–12× slower than optimized JS on simple O(n) kernels (SMA, EMA, RSI, Typical price). The C++ computation is fast, but copying 5M doubles across the N-API boundary twice (in + out) costs ~300–400 ms per call.
  • Native catches up on complex indicators (WMA, ADX, Ichimoku, Bollinger) where JS does multiple array scans per candle — there the single native pass amortises the marshalling cost.
  • { output: 'typed' } + Float64Array input is still the fast path — it avoids the output conversion (~44 ms at 5M). But the input copy (~29 ms) and N-API call overhead remain.
  • For small arrays (< 100k), native wins on most indicators. The crossover is around 200k–500k elements depending on the indicator complexity.
  • Formula complexity is nearly free in calculate — what costs is crossing the boundary and re-parsing the formula string per call (~1–2 ms at 5M). If you evaluate the same formula over many batches, prefer fewer, larger calls (e.g. one call over all candles rather than per-tick calls).

The async API is a separate, unconditional win: await cfm.sma(data, 20) offloads the work to the libuv thread pool, so a server keeps serving requests while indicators are computed; the sync form blocks the event loop.

Practical guidance

Situation Recommendation
Backtests, tight loops, worker threads, < 200k elements …Sync + Float64Array input + { output: 'typed' } — native faster.
Backtests, > 500k elements, simple indicators (SMA, EMA, RSI) Consider pure JS tight loops — marshalling overhead dominates.
Complex multi-pass indicators (WMA, ADX, Ichimoku, Bollinger) Native — single pass amortises marshalling cost.
Custom / composite formulas calculate / calculateSync — one pass, no hand-written loops.
Anything inside a server, request handler or UI process Use the async API so you never block the event loop.
Results consumed by Array-only APIs (JSON, spreads) Default number[] — conversion cost unavoidable there anyway.
node benchmark-baseline.js

Where native wins and where JS is fine

⚠️ Honest disclaimer: At 5M candles, raw compute speed on simple O(n) kernels (SMA, EMA, RSI) is 5–12× slower than optimized JS due to N-API marshalling overhead (~300–400 ms per call). The wins below are architectural (async safety, GC stability, dynamic formulas, zero-config install), not microbenchmark throughput. For arrays < 200k, native is faster on most indicators.

The four pillars where crypto-fast-math beats pure JS in production:

1. Event Loop survival — async API keeps your bot alive

The problem: A single synchronous JS loop over 5M candles blocks the event loop for 30+ ms. In a WebSocket handler, that means dropped ticks, missed order cancellations, unresponsive health checks.

The proof — run this in a server while hitting it with requests:

import * as cfm from 'crypto-fast-math';
import http from 'http';

const prices = new Float64Array(5_000_000).map((_, i) => 100 + i * 0.001);

// ❌ SYNC — blocks event loop for ~33 ms, server stops responding
cfm.smaSync(prices, 20); // main thread FROZEN

// ✅ ASYNC — event loop FREE, handles requests while computing
await cfm.sma(prices, 20); // runs on libuv thread pool

Benchmark (10 × 5M candles concurrently):

Mode Total time Event loop blocked?
10 × smaSync ~2360 ms YES — 2.3 s frozen
10 × await sma (parallel) ~2290 ms NO — serves requests during

The async API offloads to the libuv thread pool (4 threads by default, configurable via UV_THREADPOOL_SIZE). Your HTTP/WebSocket handlers keep running.


2. Dynamic formulas from config/DB — no new Function, no JIT loss

The problem: Strategies change. You can't redeploy code for every (High-Low)/Close*Volume tweak. In pure JS, new Function(formula) recompiles on every call, killing V8 optimization.

The proof — same formula, 20 re-evaluations with new data:

const formula = '(High - Low) / Close * Volume * sin(Close)';
const params = { High: highs, Low: lows, Close: closes, Volume: volumes };

// JS: new Function — recompiles every call, no JIT
const jsFn = new Function('H','L','C','V', `...${formula}...`);
for (let i = 0; i < 20; i++) jsFn(newHighs, newLows, newCloses, newVols);

// Native: ExprTk parses ONCE, evaluates 20× at C++ speed
for (let i = 0; i < 20; i++) {
  cfm.calculateSync({ formula, params: newParams, returnType: 'number' }, { output: 'typed' });
}
Approach 20 evals × 5M candles Why
new Function (JS) ~7,200 ms Re-parses + no JIT per call
cfm.calculateSync (ExprTk) ~1,800 ms 1 parse, 20× native eval

ExprTk compiles the AST once. Re-evaluation is a tight C++ loop — no JS boundary crossing per candle.


3. Zero-copy Float64Array — GC stays quiet

The problem: 50 pairs × 20 indicators × 100k candles = 100M number[] allocations. V8's GC triggers stop-the-world pauses (50–200 ms) that kill latency-sensitive bots.

The proof — memory profile:

// ❌ Default: allocates new number[] every call
const rsi = cfm.rsiSync(closes, 14); // number[] → GC pressure

// ✅ Typed: zero JS allocations, single memcpy from C++
const rsi = cfm.rsiSync(closes, 14, { output: 'typed' }); // Float64Array
Output mode Allocations per call (100k) GC impact
number[] (default) ~100k JS numbers + array Frequent minor GC
{ output: 'typed' } 1 Float64Array (memcpy) Near-zero

For 50 pairs streaming live, typed mode keeps RSS flat and GC pauses non-existent.


4. Prebuilt binaries — npm install just works

The problem: talib requires TA-Lib C library + node-gyp + Python + compiler. Fails in Docker, CI, Alpine, ARM.

The proof — install matrix:

Environment npm i talib npm i crypto-fast-math
Ubuntu (glibc) ⚠️ Needs build tools ✅ Prebuild
Alpine (musl) ✅ Prebuilds 🟢 Works
Windows ❌ Needs VS Build Tools ✅ Prebuild
macOS ARM64 ⚠️ Sometimes ✅ Prebuild
Docker (node:20-slim) ❌ Missing Python/gcc ✅ Works
GitHub Actions CI ⚠️ Slow (compiles) ✅ Instant

Prebuilds ship for 5 platforms (Win x64, Linux x64/ARM64 glibc, macOS x64/ARM64) via node-gyp-build. No node-gyp in your package.json, no binding.gyp to maintain.


When plain JS is fine

Scenario Recommendation
One-off backtest, < 50k candles Pure JS — simplicity wins
Prototyping a single indicator technicalindicators is fine
Browser / edge runtime JS only (WASM build planned)
You already have optimized JS loops Keep them — native adds dep

🎯 Why this package exists (honest summary)

If you need... Use this
Event loop never freezes under load await cfm.sma(...) — runs on libuv thread pool
Formulas from config/DB at runtime cfm.calculateSync({ formula, params }) — ExprTk parses once
Zero GC pauses on 50+ pairs streaming { output: 'typed' } → Float64Array via memcpy
npm install works everywhere (Win/Linux/macOS/ARM/Docker) Prebuilt binaries, no node-gyp, no TA-Lib
Complex indicators (ADX, Ichimoku, Bollinger) Single native pass vs multiple JS scans
If you need... Don't bother
Raw SMA/RSI speed on > 500k candles Pure JS tight loop on Float64Array is 5–12× faster
Browser / edge runtime Not supported (WASM planned)
Zero dependencies Adds ~2 MB native binary

Automatic JavaScript / Native dispatch

crypto-fast-math can automatically select the most appropriate execution path for supported operations, balancing computation speed against the overhead of calling native code.

For SMA and EMA (sync smaSync/emaSync and async sma/ema), the default policy: 'auto' chooses between a JavaScript implementation and the native C++ one based on input characteristics and output options:

  • Arrays with fewer than 4 000 elements run in JavaScript — at that size the tight JS loop beats paying the N-API call overhead.
  • { output: 'typed' } or a target buffer always runs native — no output conversion to amortise, so a single memcpy out wins.
  • Plain number[] inputs containing null/undefined always run native, preserving the addon's mixed-column semantics (null/undefined → NaN).
  • Everything else (≥ 4 000 elements) runs native.

You can also explicitly select an execution policy via OutputOptions.policy:

cfm.smaSync(closes, 20);                        // policy: 'auto' (default)
cfm.smaSync(closes, 20, { policy: 'js' });      // force the JavaScript implementation
cfm.emaSync(closes, 12, { policy: 'native' });  // force the native C++ implementation
Policy Behaviour
'auto' (default) Select the implementation using the dispatch rules above.
'js' Always the JavaScript implementation.
'native' Always the native C++ implementation.

Automatic dispatch is currently implemented for SMA and EMA only. Other indicators ignore policy and use their existing execution paths.

This design provides a foundation for extending automatic dispatch to additional operations over time, guided by benchmark results and real-world workloads. The goal is not to use C++ for everything, but to use the implementation that makes the most sense for each supported workload.


Browser support

❌ Not supported. C++ Node-API addons cannot run inside a browser sandbox. A WebAssembly build is on the roadmap.

TypeScript

Type declarations ship with the package, so everything is typed out of the box. The main exported types:

import type {
  NumericArray,        // number[] | Float32Array | Float64Array | Int32Array | Uint32Array | ...
  OutputOptions,       // { output?: 'array' | 'typed' } — trailing-argument result switch
  TypedOutput,         // { output: 'typed' } — narrows the result to Float64Array
  TypedResult<T>,      // maps T's number[] fields to Float64Array (used by the overloads)
  MacdResult,          // { macd, signal, histogram }
  BollingerResult,     // { upper, middle, lower }
  StochasticResult,    // { k, d }
  AdxResult,           // { adx, plusDI, minusDI }
  KeltnerResult,       // { upper, middle, lower }
  DonchianResult,      // { upper, middle, lower }
  IchimokuResult,      // { tenkan, kijun, senkouA, senkouB, chikou }
  IndicatorResult,     // union of every result type above
  CalculateArgs,       // { formula, params, returnType? }
} from 'crypto-fast-math';

The overloads connect option and result type — no casts:

const a = cfm.smaSync(closes, 20);                       // number[]
const b = cfm.smaSync(closes, 20, { output: 'typed' });  // Float64Array
const c = cfm.macdSync(closes, 12, 26, 9);               // { macd: number[]; ... }
const d = cfm.macdSync(closes, 12, 26, 9, { output: 'typed' }); // { macd: Float64Array; ... }

Troubleshooting

Symptom Cause Fix
Error: Cannot find module 'crypto-fast-math' Package not installed (or wrong directory) npm install crypto-fast-math; check node_modules/
The specified module could not be found (Windows) Missing Visual C++ Redistributable Install VC++ Redist x64
node-gyp-build fails during npm install No prebuild for your platform — source fallback needs a toolchain Windows: Visual Studio Build Tools with the C++ workload; macOS: xcode-select --install; Linux: sudo apt install build-essential
ExprTk parse error: ERR239 - Undefined symbol Variable not in params (names are case-sensitive) Add the column to params or fix the typo
Results are all NaN Warm-up period is included in the output Slice off the first period values: const clean = rsi.slice(13)
all param columns must have equal length Columns of different lengths Slice/pad every column to the same length
Array '<name>' must contain only numbers or null Booleans/strings in a data column Use 0/1; null/undefined become NaN

Building from source

git clone https://github.com/Koderkup/crypto-fast-math.git
cd crypto-fast-math
npm install
npm run build        # compiles the C++ addon and the TypeScript

The build pipeline (as wired in package.json):

  1. node-gyp configure — generates the platform build files.
  2. node scripts/fix-vcxproj.js — Windows only: patches the generated .vcxproj to enable RTTI (/GR), C++ exceptions (/EHsc) and /bigobj. ExprTk needs them, but node-gyp disables them by default. No-op on Linux/macOS — those platforms emit a Makefile / Xcode project instead, and binding.gyp carries the equivalent flags anyway.
  3. node-gyp build --release — compiles src/cpp/*.cpp into build/Release/addon.node.
  4. tsc — compiles src/*.ts into dist/.

Requirements: Node.js ≥ 18, Python 3, and a C++17 compiler (MSVC Build Tools on Windows; Xcode command-line tools on macOS; build-essential/clang on Linux).

Supported compilers. node-gyp detects Visual Studio 2017–2022 Build Tools. Newer previews (e.g. VS 2026 / v18) may not be detected yet — install VS 2022 Build Tools alongside, or pin the version with npm config set msvs_version 2022. The same applies on CI: the workflow pins windows-2022 (which ships VS 2022) instead of windows-latest, because the latter may point to a newer image whose VS preview node-gyp cannot detect yet.

Windows. node-gyp does not auto-detect MSVC from a plain terminal — run the build commands from a Developer Command Prompt for Visual Studio (or call vcvarsall.bat x64 first), otherwise node-gyp configure fails with "Could not find any Visual Studio installation to use".

Running the tests

npm test            # builds the addon + TS, then runs the whole suite
npm run test:quick  # runs the suite against an already-built addon
npm run lint        # type-check only (tsc --noEmit)

The suite covers every indicator and the formula engine, including sync/async parity.

Publishing to npm (maintainers)

This package ships prebuilt binaries inside the npm tarball using prebuildify + node-gyp-build, so users get a working native module without a compiler.

The recommended flow is fully automated: .github/workflows/prebuilds.yml builds all five platform binaries on a v* git tag and publishes the package with the merged prebuilds/. Locally you can do the same by hand:

1. Build prebuilds for every target platform

prebuildify builds for the host OS/architecture only, so run it on each platform you want to support — locally or, better, in a CI matrix. The repo ships .github/workflows/prebuilds.yml with windows-2022, ubuntu-latest, ubuntu-24.04-arm and macos-14 (Apple Silicon; the Intel build runs there under Rosetta 2 with an x64 Node.js install). On Windows, run it from a Developer Command Prompt for Visual Studio so node-gyp can find MSVC.

npm run prebuild    # compiles the addon and writes prebuilds/<platform>-<arch>/*.node

Resulting layout:

prebuilds/
  win32-x64/crypto-fast-math.node
  linux-x64/crypto-fast-math.node
  linux-arm64/crypto-fast-math.node
  darwin-x64/crypto-fast-math.node
  darwin-arm64/crypto-fast-math.node

Because the binaries are built against Node-API, one prebuild per platform/arch works across Node and Electron versions — no need to rebuild per Node release.

prebuildify runs its own node-gyp build, but the MSVC flags (/GR, /EHsc, /bigobj) live in binding.gyp itself — so they survive prebuildify's rebuild, and scripts/fix-vcxproj.js stays belt-and-braces for the local npm run build path.

2. Check what will be published

npm pack --dry-run

Verify the listing contains prebuilds/, dist/, binding.gyp, src/cpp/, README.md, LICENSE and package.json — and nothing else (no build/, no test/, no node_modules/).

3. Log in and publish

npm login            # username + password + OTP (two-factor auth is required to publish)
npm publish          # for a scoped name add:  --access public

4. Verify

npm view crypto-fast-math
# in a clean directory:
npm install crypto-fast-math
node -e "console.log(require('crypto-fast-math').smaSync([1,2,3,4],2))"

Notes:

  • Bump "version" (SemVer) before every publish — npm rejects duplicate versions.
  • node-gyp-build (wired to the install script) resolves the matching bundled prebuild at install time and only falls back to compiling from source when no prebuild matches the user's platform.
  • The files field is the whitelist; it takes precedence over .gitignore, so prebuilds/ and dist/ are included even though they are git-ignored.

License

MIT © Koderkup — see LICENSE.

Releases

Packages

Contributors

Languages