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

igcp-aforro

v2026.527.0

Published

TypeScript library and CLI for simulating Portuguese IGCP Aforro Séries A, B, C, D, E, and F Treasury Certificates: deterministic quarterly compounding with IGCP-aligned monthly base rates (Série A/B hybrid TBA path; Euribor 3M for Séries C–F) and permane

Readme

igcp-aforro

npm version CI License: MIT Node ≥ 20 Types: TypeScript Docs

Deterministic, decimal-safe TypeScript library and CLI for simulating Portuguese IGCP Aforro Séries A, B, C, D, E, and F Treasury Certificates. Drop-in for JS/TS apps; ships with a CLI and a static rates.json artifact for non-JS consumers.


About

Certificados de Aforro are Portuguese state-issued retail savings instruments. This library covers Série A (legacy perpetual certificates subscribed through 30 Jun 1986; EUR 0.34916 nominal per certificate unit; historical base-rate waterfall — see Série A research), Série B (subscriptions from 1 Jul 1986 to 25 Jan 2008, no contractual maturity; base rate 0.60×TBA from 20-day Euribor 3M and 12M moving averages — see methodology), Série C (subscriptions from 26 Jan 2008 to 31 Jan 2015, 10-year maturity; base rate 0.85×E3 ± 0.25% on the rounded Euribor mean, with a March 2009 formula flip — see docs), Série D (subscriptions open from 1 Feb 2015 to 31 Oct 2017, 10-year maturity), Série E (subscriptions open from 1 Nov 2017 to 1 Jun 2023, 10-year maturity), and Série F (subscriptions open from 1 Jun 2023 onwards, 15-year maturity). Their remuneration is the sum of:

  • a monthly base rate: for Séries C, D, E, and F, from the 10-business-day average of Euribor 3M struck on the antepenultimate TARGET2 business day of the previous month, then rounded to 3 decimals (banker's). Série F clamps the result to [0%, 2.5%]; Séries D and E add a +1pp spread to the rounded mean and clamp to [0%, 3.5%]; Série C scales the rounded mean by 0.85 and adds −0.25 or +0.25 percentage points depending on the calendar month (Portarias 73-A/2008 and 230-A/2009), then clamps at 0% minimum. Séries A and B use 0.60×TBA (Portaria 73-B/2008) with TBA per DL 11/1999; Série A months before Jul 1986 read bundled administrative monthly rates instead, and intermediate eras use archived TBA / Lisbor feeds as documented in the methodology and serie-a-research.md;
  • a permanence-premium tier that depends on how many contract years have elapsed since subscription (different tier tables per series);
  • with quarterly capitalization and 28% IRS withholding applied at each capitalization.

This package reproduces that math end-to-end, with all monetary fields returned as decimal strings (via big.js with banker's rounding) so results survive JSON.stringify and cross-language boundaries without floating-point drift.

igcp-aforro is not affiliated with IGCP and does not constitute financial advice. See Methodology and legal notice.

Features

  • Pure calculator — no network, no state, no globals. Euribor 3M and 12M datasets are bundled (12M is required for Série A/B TBA in the Euribor era); Lisbor daily expansion and TBA history JSON cover earlier A/B windows.
  • Decimal-safe — every money/rate field is a big.js-quantized decimal string, banker's-rounded at each cent.
  • aforro.net parity — booked values are derived from the same per-unit quote cadence aforro.net displays: quote rounded to 5 decimals each quarter, then round(units × unitFaceValueEur × quote, 2) (Série A: unitFaceValueEur = 0.34916; other series: 1).
  • Validated inputs — Zod-checked at the public boundary; the library throws on out-of-window subscriptions, invalid units, or impossible as-of dates.
  • Cohort-aware rate lookup — resolve the annual rate that applies to a given subscription on a given quarter, with the base + premium components surfaced for auditability.
  • CLI includedaforro simulate | redeem | portfolio | current | rates | cohort with stable --json output for scripting.
  • Static rates.json — every monthly base rate and every cohort × quarter annual rate for Séries A, B, C, D, E, and F, precomputed and published with the docs site for Python / Java / Excel users.
  • Golden-tested — Série F monthly rates from IGCP press releases; Série B base rates cross-checked against IGCP-published tables; Série A waterfall tiers locked in tests/baseRate.test.ts; Série C monthly rates locked to computeBaseRate() on the bundled Bundesbank Euribor series (aligned to the Diário da República formulas); Séries D and E validated against the technical sheet E3+1% behaviour.
  • TypeScript-first — full .d.ts typings, ESM + CJS dual bundles, Node ≥ 20.

Installation

As a library:

pnpm add igcp-aforro
# or
npm install igcp-aforro
# or
yarn add igcp-aforro

As a global CLI:

pnpm add -g igcp-aforro
aforro --help

igcp-aforro ships ESM and CJS bundles plus full .d.ts typings.

Quickstart

Library

import { simulate, Series } from 'igcp-aforro';

const result = simulate({
  series: Series.F,
  subscriptionDate: '2024-03-15',
  units: 1000,
  asOfDate: '2026-04-19',
  includeSchedule: true,
});

console.log(result.currentValueNet);   // e.g. "1078.42"
console.log(result.currentUnitQuote);  // e.g. "1.07842"
console.log(result.totalInterestNet);  // e.g. "78.42"
console.log(result.matured);           // false
console.log(result.schedule?.length);  // 8 quarters since subscription

Simulate a portfolio of cohorts

import { simulatePortfolio, Series } from 'igcp-aforro';

const portfolio = simulatePortfolio({
  asOfDate: '2026-04-19',
  subscriptions: [
    { series: Series.F, subscriptionDate: '2024-03-15', units: 1000 },
    { series: Series.E, subscriptionDate: '2018-01-15', units: 2500 },
    { series: Series.D, subscriptionDate: '2017-10-01', units: 1500 },
  ],
});

console.log(portfolio.totalValueNet);
console.log(portfolio.totalInterestNet);
console.log(portfolio.bySeries);

All money and rate fields come back as decimal strings (e.g. "1078.42", "0.02750"). Feed them into Big (or your own decimal library) on the consumer side; never coerce them with Number() if you care about precision.

The throwing APIs (simulate, simulatePortfolio, simulateRedemption, getCurrentRate, getRateForCohort, getRateTable) use Zod .parse() and propagate domain errors as exceptions. If you prefer a single discriminated result (for example to map validation to HTTP 400 without a ZodError catch block), use the safe* variants — safeSimulate, safeSimulatePortfolio, safeSimulateRedemption, safeGetCurrentRate, safeGetRateForCohort, safeGetRateTable — which return { ok: true, value } or { ok: false, kind: 'validation' | 'runtime', ... } (SafeResult).

CLI

aforro simulate --subscribed 2024-03-15 --units 1000 --schedule
aforro redeem --subscribed 2024-03-15 --units 1000 --redeem-on 2026-04-19
aforro portfolio --input ./portfolio.json --json
aforro portfolio --cohort F,2024-03-15,1000 --cohort E,2018-01-15,2500 --as-of 2026-04-19 --json
aforro current
aforro rates --from 2023-06 --to 2026-04
aforro rates --from 2023-06 --to 2026-04 --csv > rates.csv
aforro cohort --subscribed 2024-03 --as-of 2026-04
aforro cohort --subscribed 2024-03 --as-of 2026-04 --csv > cohort.csv

For portfolio, read JSON from a file with --input ./file.json, or from stdin with --input=- (the = is required; bare --input - is not parsed as a path).

Every command accepts --json for machine-readable output. The rates and cohort commands also accept --csv (comma-separated, header row, RFC 4180 field quoting); do not combine --csv with --json.

aforro simulate --subscribed 2024-03-15 --units 1000 --json | jq .currentValueNet
aforro portfolio --input=- --json < portfolio.json | jq .totalValueNet

Usage examples

Look up rates without running a full simulation

import { getCurrentRate, getRateForCohort, getRateTable } from 'igcp-aforro';

getCurrentRate({ series: 'F' });
// → { series: 'F', month: '2026-04', fixingDate: '2026-03-27', basePct: '2.500' }

getRateForCohort({
  series: 'F',
  subscriptionDate: '2024-03-15',
  asOfDate: '2026-04-19',
});
// → {
//     series: 'F',
//     subscriptionDate: '2024-03-15',
//     asOfDate: '2026-04-19',
//     quarterStartDate: '2026-03-15',
//     quarterEndDate: '2026-06-15',
//     quarterIndex: 8,
//     yearsSinceSubscription: 2,
//     baseRatePct: '2.500',
//     premiumTier: { fromYear: 2, toYear: 5, ratePct: '0.25' },
//     annualRatePct: '2.750',
//   }

getRateTable({ series: 'F', fromMonth: '2023-06', toMonth: '2026-04' });
// → MonthlyBaseRate[]

Inspect the per-quarter capitalization schedule

import { simulate, Series } from 'igcp-aforro';

const { schedule } = simulate({
  series: Series.F,
  subscriptionDate: '2024-03-15',
  units: 1000,
  asOfDate: '2026-04-19',
  includeSchedule: true,
});

for (const row of schedule ?? []) {
  console.log(
    row.quarterEndDate,
    row.annualRate,         // "0.02750"
    row.interestGross,      // "6.88"
    row.irsWithheld,        // "1.93"
    row.interestNet,        // "4.95"
    row.balanceAfter,       // "1004.95"
    row.unitQuoteAfter,     // "1.00495"
    row.premiumTier.ratePct // "0.25"
  );
}

Override the IRS withholding rate

The default is the 28% Portuguese personal-income-tax rate on interest. Override it for non-resident scenarios or sensitivity analysis:

simulate({
  series: 'F',
  subscriptionDate: '2024-03-15',
  units: 1000,
  irsRate: 0.10,
});

Project an "if I redeemed today" value

accruedSinceLastCapitalization is informational (gross accrued mid-quarter) and is not paid by IGCP on an early redemption date. For payable value, use simulateRedemption():

import { simulateRedemption } from 'igcp-aforro';

const redemption = simulateRedemption({
  series: 'F',
  subscriptionDate: '2024-03-15',
  units: 1000,
  redemptionDate: '2026-04-19',
});

console.log(redemption.redemptionValue); // payable amount at redemption date
console.log(redemption.forfeitedAccruedGross); // accrued slice forfeited on redemption

If you still need an accrued-based projection from simulate(), subtract IRS manually:

import Big from 'big.js';
import { simulate } from 'igcp-aforro';

const r = simulate({
  series: 'F',
  subscriptionDate: '2024-03-15',
  units: 1000,
  asOfDate: '2026-04-19',
});

const accruedNet = Big(r.accruedSinceLastCapitalization)
  .times(Big(1).minus(r.irsRate));
const projectedNet = Big(r.currentValueNet).plus(accruedNet);

API reference

import {
  simulate,
  simulatePortfolio,
  simulateRedemption,
  getCurrentRate,
  getRateForCohort,
  getRateTable,
  Series,
  getSeries,
  listSeries,
  VERSION,
} from 'igcp-aforro';

import type {
  SeriesCode,
  SeriesMetadata,
  PremiumTier,
  RateEntry,
  ScheduleRow,
  SimulateInput,
  PortfolioSubscription,
  SimulatePortfolioInput,
  PortfolioSeriesBreakdown,
  PortfolioResult,
  SimulateResult,
  RedemptionInput,
  RedemptionResult,
  CohortRateInput,
  CohortRateResult,
  CurrentRateInput,
  RateTableInput,
  MonthlyBaseRate,
  IsoDate,
  IsoMonth,
} from 'igcp-aforro';

| Export | Kind | What it does | | --- | --- | --- | | simulate(input) | function | Quarterly-compounding simulator. Returns SimulateResult (optionally with schedule). | | getCurrentRate(input?) | function | IGCP-published monthly base rate for the current (or given) month, plus its fixingDate. | | getRateForCohort(input) | function | Composite annual rate for a cohort × quarter, with base + premium components surfaced. | | getRateTable(input) | function | Monthly base rates between fromMonth and toMonth (inclusive). | | Series | enum-like | Series.D, Series.E, Series.F — the series code constants. | | getSeries(code) | function | Returns the static SeriesMetadata for a series. | | listSeries() | function | Returns the list of series codes the library supports. | | VERSION | string | Library CalVer (YYYY.MMDD.PATCH). |

Full type signatures and TSDoc are generated into the docs site at https://igcp-aforro.primor.me/api/.

HTTP API

A hosted read-only JSON API runs the same safe* entry points over HTTPS (no npm install). Use it when you need parameterized simulations from Python, Excel, or curl; use rates.json when you only need precomputed rate tables.

curl -sS -X POST 'https://api.igcp-aforro.primor.me/v1/simulate' \
  -H 'content-type: application/json' \
  -d '{
    "series": "F",
    "subscriptionDate": "2024-03-15",
    "units": 1000,
    "includeSchedule": true
  }'

Responses use { "ok": true, "value": … } or { "ok": false, "kind": "validation" | "runtime", … } (HTTP 400 / 422). Every response includes X-IGCP-Aforro-Disclaimer. For AI assistants, the monorepo ships an stdio MCP server (igcp-aforro-mcp); see CONTRIBUTING.

Static rates.json for non-JS users

Python, Java, Excel, and spreadsheet users can skip the npm package entirely and consume a precomputed JSON snapshot of every monthly base rate and every cohort-anchored annual rate.

The file is regenerated after every release and after every Euribor / IGCP base-rate refresh PR is merged.

Top-level shape:

{
  "schemaVersion": 1,
  "generatedAt": "2026-04-20T08:00:00Z",
  "libraryVersion": "2026.420.0",
  "euriborSourceMeta": {
    "lastRefreshedAt": "2026-04-19T07:42:11Z",
    "source": "Deutsche Bundesbank time-series API",
    "sourceUrl": "https://api.statistiken.bundesbank.de/rest/download/BBIG1/...",
    "seriesId": "BBIG1.D.D0.EUR.MMKT.EURIBOR.M03.BID._Z"
  },
  "series": {
    "D": { "metadata": { "...": "..." }, "monthlyBaseRates": [], "cohortRates": [] },
    "E": { "metadata": { "...": "..." }, "monthlyBaseRates": [], "cohortRates": [] },
    "F": {
      "metadata": { "...": "..." },
      "monthlyBaseRates": [
        { "month": "2024-03", "fixingDate": "2024-02-27", "basePct": "3.892" }
      ],
      "cohortRates": [
        {
          "subscribed": "2024-03",
          "subscriptionDate": "2024-03-01",
          "quarterIndex": 8,
          "quarterStartDate": "2026-03-01",
          "quarterEndDate": "2026-06-01",
          "yearsSinceSubscription": 2,
          "basePct": "2.500",
          "premiumTierYearsRange": "2-5",
          "premiumPct": "0.25",
          "annualRatePct": "2.750"
        }
      ]
    }
  }
}

Minimal Python compounder using only rates.json:

import json, urllib.request
from decimal import Decimal, ROUND_HALF_EVEN

data = json.load(urllib.request.urlopen('https://igcp-aforro.primor.me/rates.json'))
rows = [r for r in data['series']['F']['cohortRates'] if r['subscribed'] == '2024-03']

units = Decimal('1000')
unit_quote = Decimal('1')
irs = Decimal('0.28')

for r in rows:
    annual = Decimal(r['annualRatePct']) / Decimal('100')
    quarterly = annual / 4
    gross = (units * unit_quote * quarterly).quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)
    withheld = (gross * irs).quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)
    net_per_unit = unit_quote * quarterly * (Decimal('1') - irs)
    unit_quote = (unit_quote + net_per_unit).quantize(Decimal('0.00001'), rounding=ROUND_HALF_EVEN)

print((units * unit_quote).quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN))

The full schema, day-of-month caveat, and field-by-field documentation live at https://igcp-aforro.primor.me/rates-json/.

Methodology and legal notice

Methodology

The library reproduces the IGCP technical sheets for Certificados de Aforro Série D (Portaria n.º 17-B/2015, closed by Portaria n.º 329-A/2017), Série E (Portaria n.º 329-A/2017, closed by Portaria n.º 149-A/2023), and Série F (Portaria n.º 149-A/2023). In summary:

  1. The monthly base rate for month M is the arithmetic mean of the Euribor 3M fixings over the 10 TARGET2 business days ending at the antepenultimate business day of month M-1, rounded to 3 decimals (banker's rounding). For Série F the rounded mean is clamped to [0%, 2.5%]. For Séries D and E a +1pp spread is added to the rounded mean (E3 + 1%) and the result is clamped to [0%, 3.5%].
  2. The annual rate for a cohort × quarter is baseRate(quarterStartMonth) + premium(contractYear), where premium follows the IGCP-published tier table:
    • Série F — year 1: 0.00%, years 2–5: +0.25%, 6–9: +0.50%, 10–11: +1.00%, 12–13: +1.50%, 14–15: +1.75%.
    • Séries D and E — year 1: 0.00%, years 2–5: +0.50%, 6–10: +1.00%.
  3. Quarterly capitalization: the booked net state is a per-unit quote, starting at 1.00000. Each quarter computes grossPerUnit = unitQuote × annualRate / 4, applies IRS to get netPerUnit, then stores the next quote rounded to the series' quote precision (5 decimals for Séries D, E, and F). The headline booked value is currentValueNet = round(units × currentUnitQuote, 2), matching aforro.net to the cent for booked certificates regardless of holding size. Gross interest and IRS withholding are booked separately in real EUR at the holding level each quarter: interestGross = round(units × previousUnitQuote × annualRate / 4, 2), irsWithheld = round(interestGross × 28%, 2), and interestNet = interestGross − irsWithheld. Those cent-rounded quarterly rows reconcile exactly with the headline totalInterest* fields.
  4. Quarter anchoring: quarters start on the subscription's day-of-month, shifted by 3-month multiples. When the day doesn't exist in the target month (e.g. subscription on 31 Jan → next quarter would land on 31 Apr), the date rolls forward to the first day of the following month per the IGCP spec.
  5. Validations (per series, read from SeriesMetadata):
    • Série D — subscriptions in [2015-02-01, 2017-10-31] (closed to new subscriptions); units in [100, 250000]; matures at subscriptionDate + 10 years.
    • Série F — subscriptions on or after 2023-06-01; units in [100, 100000]; matures at subscriptionDate + 15 years.
    • Série E — subscriptions in [2017-11-01, 2023-06-01] (closed to new subscriptions); units in [100, 250000]; matures at subscriptionDate + 10 years.
    • In all cases asOfDate must be on or after subscriptionDate. Past maturity, the simulation stops and reports matured: true.

The Portuguese-language methodology page maps every rule above to the source file that implements it: https://igcp-aforro.primor.me/methodology/.

Legal notice

This is an independent open-source project. It is not affiliated with, endorsed by, or sponsored by:

The package bundles daily EURIBOR® 3-month fixings sourced from the Deutsche Bundesbank time-series API (series BBIG1), which redistributes EMMI EURIBOR® data under non-commercial terms. EURIBOR® is a registered trademark of EMMI. Users that intend to use this library — or its bundled rates — for commercial purposes should review EMMI's terms of use and obtain any licence EMMI requires for their use case. Redistribution of the bundled fixings outside of this package may also require a separate EMMI licence.

Output produced by simulate() is a calculator-quality estimate, not an official IGCP statement. In any case of divergence between this library's output and IGCP's published values or your account statement, the IGCP-published values prevail. Nothing in this library or its documentation constitutes financial, legal, or tax advice.

Development

pnpm install
pnpm build
pnpm test
pnpm lint
pnpm typecheck

Refresh the bundled Euribor dataset (developer-only; the cron in .github/workflows/data-refresh.yml does this automatically):

pnpm fetch:euribor
pnpm fetch:igcp-base-rates

Maintainers can run the live IGCP parity harness for targeted smoke checks or a full local sweep; see the compare runbook in CONTRIBUTING.md for examples and failure triage.

License

MIT © igcp-aforro contributors.

EURIBOR® is a registered trademark of EMMI and is used here for descriptive purposes only.