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

@calculator53295/backtest-engine

v0.1.0

Published

Pure TypeScript backtest and retirement-simulation engine with zero runtime dependencies: daily TWR/IRR backtesting, Monte Carlo retirement simulation, and a historical cohort engine with nine withdrawal strategies.

Readme

@calculator53295/backtest-engine

A pure-TypeScript, zero-runtime-dependency engine for portfolio backtesting and retirement simulation. Everything ships as plain functions over plain data — no network, no filesystem, no globals — so the same inputs produce the same numbers in a browser, a worker, or Node.

Three surfaces in one package:

  • Daily backtest enginerunBacktest walks a portfolio over the intersection of its tickers' trading days, handling dividends (reinvested or accrued as cash), splits, monthly contributions, and rebalancing. It reports a time-weighted-return index (flows stripped out), a money-weighted IRR, plus drawdown, volatility, annual/monthly/rolling returns, and per-holding breakdowns. A two-asset efficient-frontier helper (twoAssetFrontier, minVarianceIndex, maxSharpeIndex) rounds it out.
  • Monte Carlo retirement simulatorrunHistoricalSequence, runBootstrap, and maxSafeWithdrawal run withdrawal plans over historical or block-bootstrapped real-return sequences and summarize success rate, balance and income percentiles, and pay-cut risk. Everything is in real (inflation-adjusted) dollars.
  • Historical cohort retirement engine (retirement.*) — a bucketed stocks/bonds/cash simulator with glide paths, income and extra-withdrawal streams, fees, and a registry of nine withdrawal strategies (constant dollar, percent-of-portfolio, 1/N, VPW, Guyton-Klinger, CAPE-based, 95%-rule, endowment, Vanguard dynamic — plus legacy adapters). runAllCohorts sweeps every historical start date; solveSwr back-solves the safe withdrawal rate.

A namespaced canonical module (canonical.*) provides a forward-compatible type vocabulary and pure adapters that reconcile the two engine dialects lifted into this repo, so consumers can share one vocabulary as the surfaces converge.

How this engine is tested

This package exists to be trusted by three production apps, so its behavior is pinned by tests at three levels — hand-computed synthetic fixtures for exact correctness, real-market regression bands for realism, and a golden parity fixture that locks results to the app the code was extracted from. The engine itself carries no personal financial data: only synthetic tuples and public market/Shiller data are used as fixtures.

| Fixture class | What it pins | Concrete examples (from the test suite) | | --- | --- | --- | | Hand-computed synthetic fixtures | Exact arithmetic of the daily engine — every value is derived by hand in the test, no tolerance beyond float epsilon. | A flat/linear price path 100 → 110 → 99 on a single asset must yield portfolio values 10 000 → 11 000 → 9 900 and totalReturn = −0.01 (engine/__tests__/backtest.test.ts). A $2 dividend where unadjusted close drops 100 → 99 yields 10 100 reinvested and 10 100 with $200 accrued cash when not. A 2:1 split (100 → 51, real +2%) must neutralize to 10 200. Two-asset 60/40 monthly rebalancing lands on 11 024; TWR strips a $1 000 monthly contribution so the index matches the no-contribution run exactly. | | Real-data regression bands | The engine reproduces known market history within approximate ranges (vendor adjustment methods differ slightly). Runs against gitignored Tiingo data; auto-skips when absent. | SPY 1994–2023 CAGR ∈ (9%, 11%); GFC max drawdown ∈ (−58%, −50%) troughing in 2009-03 with a non-null recovery; 2008 annual return ∈ (−40%, −34%) (≈ −37%); AAPL 2020 4:1 split continuity — no daily move outside (0.85×, 1.15×); a 60/40 SPY/BND mix shows lower volatility and shallower drawdown than pure SPY and clamps its range to BND's 2007 inception (engine/__tests__/realdata.test.ts). | | Golden Shiller cohort parity | The retirement cohort engine matches the app it was ported from, bit-for-bit. | The first 48 rows of retirement-sim's public shiller.csv (1871-01…1874-12) run through runAllCohorts must reproduce captured summary quantiles (real and nominal end balances) to 10 decimal places, with count = 3, successRate = 1, start dates 1871/1872/1873-01, worst cohort 1872-01, best 1871-01, and every year-1 real withdrawal ≈ $4 000 (retirement/__tests__/golden-cohort.test.ts). |

Consumers

One engine, three production apps by the same author — extracting the shared math here means simulation results are identical everywhere the same inputs appear, with no per-app drift:

  • retirement-sim — the cohort retirement engine and withdrawal-strategy registry were ported from it.
  • Fathom stock-analysis suite — the daily backtest and Monte Carlo engines were lifted verbatim from its fixture-guarded src/engine + src/montecarlo.
  • finance-master — shares the same engine.

Quickstart

Daily backtest — runBacktest

import { runBacktest } from "@calculator53295/backtest-engine";

const spy /* : TickerSeries */ = {
  ticker: "SPY",
  records: [
    { date: "2020-01-02", close: 100, adjClose: 100, divCash: 0, splitFactor: 1 },
    { date: "2020-01-03", close: 110, adjClose: 110, divCash: 0, splitFactor: 1 },
    { date: "2020-01-06", close: 99, adjClose: 99, divCash: 0, splitFactor: 1 },
  ],
};

const result = runBacktest(
  [spy],
  { name: "All SPY", allocations: [{ ticker: "SPY", weight: 100 }] },
  { initialAmount: 10_000, monthlyContribution: 0, rebalance: "none", reinvestDividends: true },
);

result.values;         // portfolio value per trading day
result.metrics.cagr;   // + totalReturn, irr, volatility, drawdown, annualReturns…
result.twrIndex;       // flow-stripped time-weighted-return index

Retirement Monte Carlo — runHistoricalSequence / maxSafeWithdrawal

import {
  runHistoricalSequence,
  maxSafeWithdrawal,
  type RealReturnSeries,
  type SimParams,
} from "@calculator53295/backtest-engine";

// Real (inflation-adjusted) monthly total returns, oldest first.
const series: RealReturnSeries = { dates: ["1994-01", /* … */], returns: [0.003, /* … */] };

const params: Omit<SimParams, "withdrawalRate"> = {
  initialBalance: 1_000_000,
  strategy: "fixedReal",   // or "fixedPercent" | "vpw" | "guardrails"
  horizonYears: 30,
  feeRate: 0.001,
};

const sim = runHistoricalSequence(series, { ...params, withdrawalRate: 0.04 });
sim.successRate;              // fraction of historical cohorts that survived
sim.percentiles.p50;          // median balance path, today's dollars

// Or back-solve the highest rate meeting a 95% success target:
const swr = maxSafeWithdrawal(series, params, 0.95);

Historical cohort engine — retirement.simulate

import { retirement } from "@calculator53295/backtest-engine";
// or: import { simulate, runAllCohorts } from "@calculator53295/backtest-engine/retirement";

// [date, spReturn, bondReturn, cashReturn, cpi] → HistoricalRow[]
const rows: retirement.HistoricalRow[] = [
  { date: "1871-01", spReturn: 0.0184, bondReturn: 0.0042, cashReturn: 0.0011, cpi: 12.4641 },
  // …monthly public Shiller / asset-class rows…
];

const trial = retirement.simulate(
  {
    startDate: "1871-01",
    horizonYears: 30,
    stepFreq: "monthly",
    initialBalance: 100_000,
    initialAllocation: { stocks: 0.6, bonds: 0.3, cash: 0.1 },
    strategy: { kind: "constant-dollar", annualAmount: 40_000, inflationAdjusted: true, sourcing: "proportional" },
    rebalance: "yearly",
  },
  rows,
);

trial.endBalanceReal;           // ending balance, today's dollars
trial.ranOutAtPeriod;           // null if the plan survived

// Sweep every start date in the dataset:
const { summary } = retirement.runAllCohorts({ /* config without startDate */ }, rows);
summary.successRate;

The cohort engine takes an injectable random source (RandomSource) for its sampling helpers (e.g. pickRandomStart), so simulations stay deterministic and testable — pass a seeded RNG like the exported mulberry32.

API stability

This package is 0.x: minor releases may include breaking changes to the two flat engine dialects while the surfaces converge. The canonical module is the forward-compatible vocabulary — prefer it for long-lived integrations. The exported VERSION constant tracks the package version.

License

MIT