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 },
});- Why crypto-fast-math?
- Why not
talibortechnicalindicators? - Installation
- Supported platforms
- Quick start
- How to import
- Custom formulas (ExprTk engine)
- Examples
- Ready-made indicators
- API reference
- Formula engine reference
- Recipes
- Performance
- Where native wins and where JS is fine
- Automatic JavaScript / Native dispatch
- Browser support
- TypeScript
- Troubleshooting
- Building from source
- Running the tests
- Publishing to npm (maintainers)
- License
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, nonew FunctionJIT loss- GC pauses kill latency —
{ output: 'typed' }returnsFloat64Arrayvia singlememcpy, zero JS allocationsnpm installmust just work — prebuilt binaries for Win/Linux/macOS/ARM, nonode-gyp, no TA-Lib, no Docker headachesOn 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. |
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:
talibis a build headache;technicalindicatorsis fine for prototypes but chokes on production load (GC pauses, blocked event loop, no custom formulas).crypto-fast-mathgives 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.
npm install crypto-fast-mathThat'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 installautomatically selects the musl build: CI ships acrypto-fast-math.musl.nodenext to the glibc binary inside the sameprebuilds/linux-x64/prebuilds/linux-arm64folder, andnode-gyp-buildpicks 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.
| 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.
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.
const cfm = require('crypto-fast-math');
const sma = cfm.smaSync([100, 102, 101, 105, 107], 3);
// [NaN, NaN, 101, 102.666..., 104.333...]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)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.
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 poolSync 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.
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 holdExprTk 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 |
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.
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 allocationsWhat 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
paramskeys —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.
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');
}
});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`);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[] |
- Each output array has the same length as the input.
- Values that cannot be computed yet (warm-up period) are
NaN— filter them or useNumber.isNaNbefore consuming. - Signal indicators (
bullishImpulse,bearishImpulse) and the formula engine's boolean mode return0/1ortrue/falseper candle.
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 eachDefault (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 |
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.
smaSync(prices, period)— simple moving average.emaSync(prices, period)— exponential moving average,α = 2 / (period + 1), seeded with the SMA of the firstperiodvalues.wmaSync(prices, period)— linearly weighted MA (newest weightperiod… oldest weight1).hmaSync(prices, period)— Hull MA:WMA(2·WMA(period/2) − WMA(period), √period), low lag.
rsiSync(prices, period)— Wilder RSI, values in[0, 100];NaNuntilperioddeltas.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)overkPeriod;%D = SMA(%K, dPeriod).cciSync(high, low, close, period = 20)—(TP − SMA(TP)) / (0.015 · meanAbsoluteDeviation), whereTP = (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.
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 overperiod;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.
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/senkouBare shifted 26 bars forward andchikou(lagging span) 26 bars back; where a shifted point falls outside the data range the value isNaN.
vwapSync(high, low, close, volume)— cumulativeΣ(TP·volume) / Σvolumeusing typical price.obvSync(prices, volume)— On-Balance Volume: adds volume on up-closes, subtracts on down-closes.
medianPriceSync(high, low)—(high + low) / 2per candle.typicalPriceSync(high, low, close)—(high + low + close) / 3per candle.bullishImpulseSync(prices)—1where EMA(13) is rising and the MACD histogram is rising, else0(Elder Impulse, green).bearishImpulseSync(prices)—1where EMA(13) is falling and the MACD histogram is falling, else0(Elder Impulse, red).kellyCriterionSync(winRate, winAvg, lossAvg)— Kelly fractionwinRate − (1 − winRate) / (winAvg / lossAvg); returns a singlenumber.
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[]. |
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| 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, …). |
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.
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',
});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',
});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[]
}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.
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 EMA200const 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 breakoutconst 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 holdBenchmarks 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.
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).Float64Arrayinput is still the fastest path — you skip even that cast.
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.
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.
| 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).
- 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' }+Float64Arrayinput 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.
| 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
⚠️ 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:
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 poolBenchmark (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.
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.
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.
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) | ✅ Prebuild | |
| Alpine (musl) | ✅ Prebuilds | 🟢 Works |
| Windows | ❌ Needs VS Build Tools | ✅ Prebuild |
| macOS ARM64 | ✅ Prebuild | |
| Docker (node:20-slim) | ❌ Missing Python/gcc | ✅ Works |
| GitHub Actions CI | ✅ 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.
| 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 |
If you need... Use this Event loop never freezes under load await cfm.sma(...)— runs on libuv thread poolFormulas from config/DB at runtime cfm.calculateSync({ formula, params })— ExprTk parses onceZero GC pauses on 50+ pairs streaming { output: 'typed' }→Float64Arrayviamemcpynpm installworks everywhere (Win/Linux/macOS/ARM/Docker)Prebuilt binaries, no node-gyp, no TA-LibComplex 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 Float64Arrayis 5–12× fasterBrowser / edge runtime Not supported (WASM planned) Zero dependencies Adds ~2 MB native binary
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 atargetbuffer always runs native — no output conversion to amortise, so a singlememcpyout wins.- Plain
number[]inputs containingnull/undefinedalways 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.
❌ Not supported. C++ Node-API addons cannot run inside a browser sandbox. A WebAssembly build is on the roadmap.
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; ... }| 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 |
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 TypeScriptThe build pipeline (as wired in package.json):
node-gyp configure— generates the platform build files.node scripts/fix-vcxproj.js— Windows only: patches the generated.vcxprojto 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, andbinding.gypcarries the equivalent flags anyway.node-gyp build --release— compilessrc/cpp/*.cppintobuild/Release/addon.node.tsc— compilessrc/*.tsintodist/.
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-gypdoes not auto-detect MSVC from a plain terminal — run the build commands from a Developer Command Prompt for Visual Studio (or callvcvarsall.bat x64first), otherwisenode-gyp configurefails with "Could not find any Visual Studio installation to use".
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.
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:
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>/*.nodeResulting 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.
npm pack --dry-runVerify 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/).
npm login # username + password + OTP (two-factor auth is required to publish)
npm publish # for a scoped name add: --access publicnpm 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 theinstallscript) 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
filesfield is the whitelist; it takes precedence over.gitignore, soprebuilds/anddist/are included even though they are git-ignored.
MIT © Koderkup — see LICENSE.