npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

crypto-fast-math

v0.1.17

Published

C++-accelerated trading indicators for Node.js — SMA, RSI, MACD, Bollinger, ADX, Ichimoku + custom ExprTk formulas. Async, zero-config prebuilt binaries.

Readme

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.