ActuarialOS for Excel
Function Reference
Signature, plain-English use, and the method, convention and engine anchor behind every function. The same guide the ribbon's Help dialog opens in Excel.
- Source
- Published verbatim from the add-in’s own
FUNCTIONS.md
The complete guide to the aos_* worksheet functions. Two audiences:
- Using the tool (analysts): each function's signature and a plain-English "use it for" line. Most builders are also on the ActuarialOS ribbon (or the All Formulas dropdown), which write the formula for you with a live preview. As you type
=aos_*(in a cell, in-cell IntelliSense names each argument and describes the one you're on; the ribbon Help → Function Reference opens this guide, searchable. - Understanding the calculations (actuaries): each function's method / convention / anchor — what it computes, the conventions it follows, and which
actuarialEngine.jsvalue it reproduces. Every signedaos_*number is cross-checked to that engine to machine epsilon (the "anchor").
Verify before signing. Functions reproduce the engine exactly, but some embedded DATA is an attested input — see § Verify before signing. The cross-check's current case count and max relative error (the verifier of record) are stated in the add-in's governing doc, Addin/PROJECT.md, and printed by every verifier run.Conventions used throughout
- Default payment convention = MONTHLY, paid in advance (due). A bare
aos_ax(and the rest of the annuity family) values 12 payments/year at the start of each month — the real-world pension convention. To get an annual immediate factor, passfreq=1, timing="immediate"; to get annual due,freq=1, timing="due". (Exception:aos_LumpSum417defaults to annual, engine-anchored.) - Annuity timing (the engine anchor). At
freq=1, timing="immediate",aos_axreproducespvInPayLifeEXACTLY: each payment is made at the END of its period and weighted by survival TO it — the textbook annual annuity-immediate a_x = Σ v^k · ₖp_x. (Before ActOS 2026.06.197 both products weighted it by survival to the period's START, "v·ä" = a_x + A_x, a full extra payment in the year of death; worksheet cells usingtiming="immediate"move at that rebuild.) The exact m-thly kernel reduces to this at frequency 1 / immediate, so monthly/due are true refinements of that convention, not approximations. - Monthly survival uses UDD (uniform distribution of deaths) for intra-year survival.
- Generational mortality.
valYearis the calendar year at the valuation age; attained agex+tis reached invalYear+t. Static projection (aos_Mort) projects a table to one year. - A fractional age rounds HALF-UP. Every function that reads an age as a whole year (
aos_qx,aos_px,aos_axand its commencement age, the joint / J&S / refund / §417(e) forms, the commutation functions,aos_Limit415Adjusted, …) rounds it half-up, the engine'sMath.round: 62.5 reads 63 (ADDIN-C, Scott's ruling 2026-09-29). Before it every one of them butaos_Limit415Adjustedused .NET's default banker's rounding, which read 60.5, 62.5 and 64.5 as 60, 62 and 64 — a different answer from the engine at every x.5 age with an even whole part. The mid-of-year family (aos_axMOY*) is different by design: it FLOORS the age and carries the fraction. These cells are calculators of the age you STATE, not a valuation: the ActuarialOS valuation values an in-pay life (since2026.06.229) and a commencement annuity (since.231) at the exact fractional age, (1 − f)·ä(⌊x⌋) + f·ä(⌊x⌋ + 1), so at 62.5 it reads halfway between ä(62) and ä(63) where a cell reads ä(63). - Discount basis. The interest argument of every annuity accepts a flat rate, a 3-value §430 segment triple, or a spot yield curve (
aos_Curve). On a segment triple every payment's segment is assigned from the valuation date (IRC §430(h)(2)(B)): a DEFERRED annuity (aos_axwith a later commencement, the deferred joint and J&S forms, the MOY deferred and Woolhouse forms, and the MOY stub's half-year tail) discounts a payment t years after a commencement T0 years out atSegmentDF(T0 + t)— ActOSENGINE-M, the engine's own rule. Before it these cells restarted the 5 / 15 / thereafter grid at commencement,SegmentDF(T0) × SegmentDF(t); an immediate annuity (T0 = 0) and every flat-rate cell are unchanged. A spot curve follows the same rule (ADDIN-C, Scott's ruling 2026-09-29): the same deferred cells discount atD(A + T0 + t) / D(A), A =aos_Curve'svaluationOffset, where before it they restarted the curve at commencement. Each segment rate is a SPOT rate (ActOSENGINE-N, IRC §430(h)(2)(B), Reg. §1.430(h)(2)-1(b)):SegmentDF(t)= (1 + s_k)^−t at the one rate of the period t falls in — seg1 for t < 5, seg2 for 5 ≤ t < 20, seg3 for t ≥ 20 (a payment at exactly 5 or 20 takes the LATER rate, Reg. §1.430(a)-1 Example 1). Before it the rates were chained as forward rates, (1 + seg1)^−5 · (1 + seg2)^−(t − 5) · …, which moves every segment cell paid at t > 5. - COLA. A scalar (level) or an
aos_Colaselect-and-ultimate schedule (with an optional cap). - Mortality tables. Use the real published SOA tables for signed work; the four "Combined Healthy" base tables are illustrative (see Verify). Any table arg accepts a named id, an
aos_Blend(...)two-table blend, or anaos_Table(...)custom table from cell ranges. - Refusals (`#AOS: …`). Where the add-in has no stated input for a number it REFUSES — the cell shows an
#AOS: …sentence naming what is missing and the remedy — rather than substituting a default. Every mortality-based function (theaos_qxfamily, every annuity,aos_LumpSum417,aos_Limit415Adjusted) refuses: a read below a table's first age or in a gap (wrap the id withaos_Floorfor survivable pre-eligibility ages), a commencement past a table's last age (every payment would be skipped), an unknown improvement-scale name (never read as MP-2021 — since 2026-09-25), and a scale on a table that declares no base year or already embeds improvement. An unknown table id is#NAME?. Published series refuse a year or month not on file (each row says which). A refusal is TEXT, and SUM / AVERAGE skip text while ISERROR reads it FALSE — a total over refused cells drops them without a sign (ruled 2026-09-25, Q10 (i): the refusal stays text, because its sentence is the point). Test withaos_IsRefused(cell)(TRUE for an#AOS:text; a range answers per cell, so=SUMPRODUCT(--aos_IsRefused(B2:B99))counts what the SUM beside it skipped) before totalling. A present but NON-NUMERIC value in an optional numeric argument (text that is not a number, TRUE/FALSE, an error value) is#VALUE!; numeric text such as"25"reads as the number (Q10 (ii) — it used to read as the argument's default). The same rule holds for the CHOICE and TEXT arguments (2026-09-25, AU1): afreqthat is not a whole number of payments ≥ 1 or one of"monthly"/"m","quarterly"/"q","semiannual"/"s","annual"/"a"; atimingthat is not TRUE/FALSE,"due"(or"annuity-due","begin","advance") or"immediate"(or"arrears","end"); astatus,monthlyBasisorcolaMethodit does not recognise; and a number, boolean or error value where a table id or scale NAME is expected — each is#VALUE!. They used to read as a value the cell never stated: a typo'd freq ("monthyl") as ANNUAL, a freq of 0 as 1, any unrecognised timing (and any number) as immediate, an unrecognised status as Active, a scale of#N/Aas "None". Blank text reads as omitted. - Cell references. Any scalar/text arg can be a cell reference; range args take ranges.
Commutation
| Function | Use it for | Method / anchor |
|---|---|---|
aos_Dx(age, rate, mort, [blend], [method]) | Discounted survivors Dₓ | lₓ built from a radix at the table's youngest defined age (ratio = ActOS survival annuities); blend = an optional second range averaged 50/50. method is aos_Nx12's fractional-age basis (2026-09-25, AP8 / Q7 — it was accepted and not read, and aos_Nx12 always computed Nx − 11/24·Dx while the Commutation dialog defaulted to and emitted "UDD"): `"1124"` (the default when omitted) = Nx − 11/24·Dx, the traditional approximation; `"UDD"` = α(12)·Nx − β(12)·Dx, exact under a uniform distribution of deaths at the column's rate, α(12) = i·d / (i⁽¹²⁾·d⁽¹²⁾), β(12) = (i − i⁽¹²⁾) / (i⁽¹²⁾·d⁽¹²⁾) (β = 0.46651 at 5%, against 11/24 = 0.45833); any other text REFUSES (#AOS: unknown fractional-age method …). A saved `"UDD"` formula moves (it computed 11/24). The dialog now defaults to "1124". aos_Dx/Nx/Cx/Mx do not read it; aos_CommTable's Nx12 column is the "1124" default. Same gap refusal as the columns below. |
aos_Nx(age, rate, mort, …) | Σ Dₓ (annuity-due numerator) | aos_Nx/aos_Dx reproduces the engine annuity-due to machine epsilon. |
aos_Nx12, aos_Cx, aos_Mx | Monthly Nx; Cx / Mx commutation | Standard commutation columns on the same lₓ basis. A mortality range with a GAP inside its span (a blank, text, or a qx outside 0–1 at some age between its first and last) REFUSES for every column — #AOS: the mortality range has no valid qx at age N …, naming the first gapped age (2026-09-24, AI2; it used to return #NUM! from the gap down, and the columns below it were built from a broken lₓ chain). A non-finite value from an extreme rate (near −1) returns #NUM!. |
aos_Mort(table, [ageAdj], [scale], [calYear]) | Build the mortality argument | Static projection to calYear by a band or 2-D generational scale (== genImprovementFactor). |
aos_CommTable(minAge, maxAge, rate, mortality, [blend], [header]) | Spilling commutation table | Spills Age, Dx, Nx, Cx, Mx, Nx12 — one row per integer age over [minAge, maxAge], with an optional header row. Same kernel as aos_Dx/aos_Nx/… (so Nx/Dx reproduces the engine annuity-due); ages outside the table's defined range return #NUM. A range with a gap inside its span REFUSES the whole spill (#AOS: …, naming the first gapped age — 2026-09-24, AI2; it used to spill NaN through every Nx and Mx cell), and no cell ever carries a non-finite number (an extreme rate's overflow shows #NUM! in that cell). A dynamic-array formula — enter once and it spills. |
Mortality & scale
| Function | Use it for | Method / anchor |
|---|---|---|
aos_qx(table, x, [y], [ageAdj], [scale], [year], [gen]) | Mortality rate q | One-year q(x), or (y-x)q(x) = 1 − l(y)/l(x) when y is given. q == genQx. Static or generational (gen). Under a named scale, a calendar year BEFORE the improvement start (the later of the table's published base year and the scale's own — Pri-2012 under MP-2021: 2012; RP-2014: 2014; GAM-83 under Scale AA: 2000) REFUSES (#AOS: the "…" improvement scale was asked for calendar year … improvement on that table starts from …, ActuarialOS 2026.06.194) — it used to price the unimproved base rates; the start year itself is the base rates; scale "None" is inert at any year. |
aos_px(table, x, [y], [ageAdj], [scale], [year], [gen]) | Survival probability p | One-year p(x), or (y-x)p(x) = l(y)/l(x) when y is given. |
aos_lx(table, x, [ageAdj], [scale], [year], [gen]) | Life-table survivors l(x) | Radix-100000 life table from the table's min age, built from genQx. Generational (gen=TRUE) under a named scale: the radix sits at the min age in calendar year year − (x − min age), so a radix year BEFORE the improvement start REFUSES, as aos_qx does. |
aos_Deaths(table, x, [y], [ageAdj], [scale], [year], [gen]) | Life-table deaths d(x) | l(x)·q(x) one-year, or l(x) − l(y) when y is given. (Named aos_Deaths, not aos_dx: Excel is case-insensitive and aos_Dx is the commutation column.) Built on aos_lx, so a generational radix year BEFORE the improvement start REFUSES as it does. |
aos_Scale(scale, age) | Improvement rate at an age | == getImprovementRate (the band scales: MP-2021, MP-2020, None — Scale AA is the published 2-D scale since ActOS wave 2c E5, and refuses here as one). An unknown scale name REFUSES (#AOS: unknown mortality improvement scale …, listing the scales carried — 2026-09-25, AL4; it used to read MP-2021's rates, here and in every band projection a misspelled scale reached). A 2-D generational scale (e.g. MP-2019) also refuses here — its rate varies by year; project on it through aos_qx / aos_ax. |
aos_Blend(A, B, w, [wB]) | A weighted two-table blend id "A|B|w" (or "A|B|w|wB") | Understood by GenQx, so ANY function can use w·A + (1−w)·B. Give wB explicitly for a weighted SUM whose weights need not total 1 (e.g. 0.6045·M + 0.5525·F adjusted-table blends); each component projects on its own sex's scale. The composition is bounded (2026-09-25, AU3 — ruling AR3 (a)): where BOTH components sit at their terminal closure (each reads 1.0 at or after its own last age) the blend reads 1.0, whatever the weights sum to — the 0.6045 + 0.5525 idiom used to compose 1.157 there, so aos_qx printed 1.157, aos_Deaths exceeded l(x) and a GAM-83 aos_lx went negative past 110; any other composed qx above 1 REFUSES (#AOS: mortality id … composes qx = …); a negative weight (or w above 1 in the convex form) REFUSES. Annuity factors move only in the last year of the table, and only where survival to it is not already negligible (the golden AE beneficiary blend, weights summing 0.623, moved −5.7e-7 relative: its closure used to read 0.623, a survivor past the end of both tables). |
aos_Splice(preTable, postTable) | An active→in-pay spliced table id ("pre>post") | The PRE (active / pre-commencement) table up to its last published age, the POST (in-pay) table above it — usable as any function's table argument (both annuity builders emit it; e.g. aos_ax(aos_Splice("Pri-2012 Employee Male", "Pri-2012 WC Retiree Male"), 45, 0.05)). A bare Employee table already auto-extends with its default in-pay partner; aos_Splice picks a DIFFERENT one. Either id unknown → #NAME?. A read below the PRE table's first age still REFUSES (#AOS: … carries no rate at age N — the span refusal; wrap with aos_Floor for survivable pre-eligibility ages). A wrapped PRE (aos_Project / aos_ProjectGen / aos_Floor of a table) cuts at the INNER table's published last age, and a registered custom PRE (aos_RegisterTable) at its last defined age (2026-09-25, AU2 — both used to cut at 120, so a projected Employee PRE read its baked default partner above 80 instead of the chosen POST: a saved splice over a wrapped or custom PRE moves). A blend PRE has no published last age and REFUSES (#AOS: the splice's PRE table … is a blend …) — name a single table as the PRE. |
aos_Floor(table) | Survivable pre-eligibility ages ("FLOOR~…" id) | Wraps any table/blend/splice id so ages BELOW its first defined age get qx = 0 instead of the span refusal (without it, a read below the first age REFUSES — #AOS: … carries no rate at age N, since 2026-09-22) — e.g. a Retiree (50+) table in a conversion-factor grid spanning ages 20+. At/above the floor, identical to the inner id. |
aos_Table(ages, rates, [gender], [baseYear]) | A custom table from cell ranges (inline) | Builds a CUSTOM|… id; reproduces a named table's annuity exactly (incl. scale projection). gender is "M" (default), "F", or `"U"` for a UNISEX table — under a 2-D generational scale a unisex table is improved on the 50/50 blend of the male and female cumulative improvement factors (the IRS unisex construction; "U:0.6" states another male weight). baseYear states the year the supplied qx apply — where improvement is projected FROM (e.g. 2014 for RP-2014 rates), appended to the id as |by=<year>. It is REQUIRED to use the table with an improvement scale: a scale applied to a table that declares no base year is REFUSED, never assumed — omit it only when no scale will be applied (then nothing reads it and the answer is unchanged). |
aos_RegisterTable(name, ages, rates, [gender], [baseYear]) | Name a custom table → reference it by name | Put in a cell; then any function resolves the name, e.g. =aos_qx("MyPlan", 65), and it appears in aos_Tables(). The rate DATA stays in the ranges (reproducible — no hidden machine state). Session-global names; reference the register cell (or recalc) so dependents refresh. baseYear has the same meaning — and the same requirement under an improvement scale — as aos_Table's. |
aos_QxTable(table, [minAge], [maxAge], [scale], [calYear], [header]) | Spilling raw mortality table (verify / compare / view) | Spills Age, qx, lx, tPx — the EXACT qx the engine uses (optionally projected by scale to calYear), for verifying a table against the SOA publication or comparing against another tool. Omit `minAge`/`maxAge` to view the whole table (defaults to the table's full defined age range), e.g. =aos_QxTable("Pri-2012 Male"). qx == genQx; lx is a radix-100000 life table from minAge; tPx is survival from minAge. Dynamic-array spill. The Mortality dialog's "full table" function builds this. |
aos_Project(table, [scale], [baseYear], [targetYear]) | Static-projection table id | Builds a "PROJ~…" id: the table improved by scale from its own published base year to targetYear and FROZEN at that level (the same rate every payment year) — the id reads exactly the targetYear rate. baseYear is optional and, if stated, must BE the table's published base year: any other year REFUSES (#AOS: …, naming both; ADDIN-C, Scott's ruling 2026-09-29 — it used to count from the stated year and replay from the table's, freezing at a year nobody typed, e.g. GAM-83 stated 2012 → 2020 froze at 1991). To measure improvement from a different base year use aos_ProjectGen. A targetYear before the table's base year under a scale REFUSES (it used to freeze at the base year). Use as any annuity/commutation table arg (pass scale="None" there — the projection is baked in). A table that declares no base year (a CUSTOM|… spec with no by=) is refused under any scale — state it on aos_Table / aos_RegisterTable, or use a published table. The id replays at the table's own base year + (targetYear − that base year) = targetYear; where targetYear falls BEFORE the improvement start (the later of the table's base year and the scale's — e.g. GAM-83 under MP-2021 with a targetYear before 2012) every read REFUSES (ActuarialOS 2026.06.194; it used to freeze the unimproved base rates). |
aos_ProjectGen(table, [scale], [baseYear]) | Generational projection re-based to a base year | Builds a "GENBASE~…" id: generational improvement (keeps advancing each future payment year, unlike aos_Project) measured from baseYear instead of the scale's intrinsic 2012 anchor. baseYear = the improvement start (the later of the table's published base year and the scale's own — the default) reproduces the plain generational result (so a bare scale arg); a baseYear BEFORE the improvement start REFUSES at the door (#AOS: …, ActuarialOS 2026.06.194 — the default used to be the table's own base, which for GAM-83 / UP-94 / GATT-83 under an MP scale now precedes the start). Use it to value a table whose rates are as-of a year other than 2012. Pass scale="None" on the consuming function (the scale is baked in). A table that declares no base year is refused under any scale, exactly as in aos_Project. A re-based rate is the plain one divided by the scale's improvement factor from the table's base to baseYear; where that lifts it above 1 it REFUSES (#AOS: mortality id … composes qx = …, 2026-09-25, AU3 — measured reachable only with a far-future baseYear near a table's end — at calendar 2025, GAM-83 / GATT-83 at ages 107–109 with baseYear 2073 or later). |
Annuities — core
| Function | Use it for | Method / anchor |
|---|---|---|
aos_ax(table, age, rate, [valYear], [scale], [cola], [commence], [freq], [timing], [beforeTable]) | PV of a $1/yr single-life annuity | == pvInPayLife. Deferral is engine-anchored (deferral survival × vⁿ × annuity-at-commencement); exact m-thly (1/12) with UDD; beforeTable = before/after-commencement mortality. |
aos_axy(tableA, ageA, tableB, ageB, rate, …) | PV of a joint-life annuity (pays while BOTH alive) | == pvInPayJoint. |
aos_axVector(table, ages, rate, [valYear], [scale], [cola], [commence], [freq], [timing], [beforeTable]) | Vectorized `aos_ax` (spills) | One aos_ax value per age in the ages range, spilling in the range's row/column orientation. Same kernel as aos_ax. Omit commence for an immediate annuity at each age; pass a scalar commence to defer every age to that fixed commencement. |
aos_axTrace(table, age, rate, [valYear], [scale], [cola], [freq], [timing], [header]) | Explain this number (spills) | The term-by-term decomposition of a $1/yr immediate single-life annuity: Age, PayTime, tPx, Discount, COLA, Payment, Term, Cumulative — one row per payment, then a TOTAL row. The Term column sums to aos_ax (mirrors that kernel exactly), so a reviewer can see survival × discount × COLA build the number. For peer review and teaching. |
Annuities — optional forms
| Function | Use it for | Method / anchor |
|---|---|---|
aos_axc(table, age, certainYears, rate, …) | n-year certain & life | Payments in [0,n) guaranteed (weight 1), then life-contingent — via the generalized AxForm kernel. |
aos_axt(table, age, tempYears, rate, …) | n-year temporary life annuity | No payments at/after time n. |
aos_axJS(memberTable, memberAge, survivorTable, survivorAge, jsPct, rate, …) | Contingent J&S ($1 to member, jsPct to survivor) | == ActOS ä_member + jsPct·(ä_survivor − ä_joint). |
aos_axRefund(table, age, refundBase, rate, [valYear], [scale], [cola]) | Modified cash refund life annuity | Life annuity + PV of the at-death shortfall: if total payments < refundBase at death, the lump-sum shortfall is refunded at end of the death year (completed payments counted). refundBase=0 == plain life annuity (anchor). Annual. Mechanical convention; installment variant = `aos_axInstallmentRefund`; contribution-base refund is the user's. |
aos_axInstallmentRefund(table, age, refundBase, rate, [valYear], [scale], [cola], [freq], [timing]) | Installment-refund life annuity | Payments GUARANTEED (continue to a beneficiary) until cumulative reaches refundBase, then life-contingent — a refund-certain annuity = certain-&-life with the recovery period (n = refundBase level; ln(1+cola·refundBase)/ln(1+cola) with COLA). refundBase=0 == plain life. Cash-refund variant: aos_axRefund. |
aos_axSSLevel(table, age, ssAge, rate, …) | SS level-income factor L | L = (deferred-to-ssAge annuity)/(immediate annuity). Pre-ssAge pension = base + SS·L; post-ssAge = that − SS. ssAge==age → L=1 (anchor). |
aos_ConvFactor(memberTable, memberAge, survivorTable, survivorAge, jsPct, rate, …, [popup]) | Single-life → J&S conversion factor | = ä(single life)/ä(J&S). Multiply a single-life benefit by this for the actuarially-equivalent J&S benefit. jsPct=0 → 1 (anchor). popup=TRUE → pop-up J&S (the member reverts to the full single-life benefit if the survivor dies first): factor = ä_joint/(ä_joint + jsPct·(ä_survivor − ä_joint)). Actuarial-equivalence baseline — a plan's PUBLISHED factor table overrides it and is user-supplied. |
aos_ConvFactorForm(table, age, form, param, rate, …) | Single-life → optional-form conversion factor | = ä(single life)/ä(form). form = "certain & life" / "temporary" / "certain only" / "cash refund" / "installment refund"; param = the guarantee/term years or the refund base. Anchors: "certain & life" with param=0 → 1; "temporary" covering the whole life → 1. Cash refund is annual (matches aos_axRefund). Same equivalence baseline — published factors override. |
aos_LumpSum417(commenceAge, commenceYear, rate, [cola], [freq], [maleTable], [femaleTable], [scale], [applicableTable]) | §417(e) minimum lump-sum factor | Per $1/yr of annual life annuity at commencement: an annuity-DUE on the applicable basis (rate = the 3-segment §430 triple for the lookback month, or a curve / flat) with the unisex applicable mortality (50/50 M/F blend projected STATICALLY to commenceYear for every payment — the §417(e)(3) table is prescribed per calendar year, Reg. §1.417(e)-1(d)(2); ActOS 2026.06.204, generational to each payment year through .203 — default Pri-2012, pass the MP scale). Accrued annual benefit × factor = the minimum lump sum. Reproduces the engine's lumpSumLifeFactorAt (engine-anchored). Pick the lookback-month rates via aos_EconMonthlyLookup. |
aos_ERF(retireAge, nra, reductionPct, [subsidized], [service], [unreducedAge], [unreducedService], [unreducedPoints]) | Plan early-retirement reduction factor | reductionPct per year early from NRA (or from an unreduced age when the service/points condition is met — Rule-of-X), floored at 0; subsidized=TRUE or retireAge ≥ NRA → 1. reductionPct omitted or 0 = no reduction (factor 1.0). Reproduces the engine's earlyRetirementFactor. This is the PLAN's stated reduction formula; a plan's tabulated ERF table overrides it and is user-supplied. |
Annuities — mid-of-year (fractional age)
There are two MOY conventions (they agree only at an integer age, where both reduce to pvInPayLife/pvInPayJoint):
| Function | Use it for | Method / anchor |
|---|---|---|
aos_axMOY(table, age, rate, [valYear], [scale], [cola], [colaMethod]) | Single life on the member's anniversary schedule | Annual payments at t = 1−g, 2−g, … from a fractional age a0+g; UDD partial-first-year survival; colaMethod = full or partial-year. Integer age == pvInPayLife. |
aos_axMOYm(table, age, rate, …, [freq], [timing], [commence], [beforeTable], [colaPartial], [monthlyBasis]) | Single life, forward from valuation (monthly / deferred) | Payments at regular 1/m intervals forward from the valuation date; UDD survival from the fractional age. Integer age == aos_ax; integer-age/commence deferral == aos_ax deferred. Level/aos_Cola COLA (no partial-year). monthlyBasis = "Exact" (default, the engine anchor) or "Woolhouse" (2-term ä⁽ᵐ⁾ = ä − (m−1)/2m — ties to legacy valuation tools; at freq=1, immediate it gives ä − 1). |
aos_axyMOY(tableA, ageA, tableB, ageB, rate, …, [colaPartial], [monthlyBasis]) | Joint, forward from valuation | Pays while both survive; one schedule from valuation. Integer ages == aos_axy. monthlyBasis = "Exact" (default) or "Woolhouse" (legacy-tie), as for aos_axMOYm. |
aos_axMOYStatus(table, age, status, rate, …, [monthlyBasis], [commence]) | Valuation 3-status mid-year model | status = Active (commence + value at the mid-year point; attained age = age+0.5), Inactive-Deferred (commence at the middle of the commencement year, value at BOY), or Inactive-InPay (commence + value at BOY). Reproduces a legacy valuation tool's mid-year statuses. Integer-age InPay == aos_axMOYm. |
Monthly basis (`aos_axMOYStatus`). The sub-annual method is selectable: `Woolhouse` (default) uses the 2-term Woolhouse approximation ä⁽ᵐ⁾ = ä − (m−1)/2m — the industry standard (SOA factor tables, most valuation systems) and what legacy tools emit, so in-pay factors tie out; `Exact` sums the m monthly payments under UDD (the add-in's engine-anchored convention), which sits ~0.0055 lower for monthly — that gap is exactly the third Woolhouse term (the (δ+μ) mortality-curvature correction). Pick the basis your plan/peer-review/audit expects. The mid-year first half-year is always valued exactly (the 6 monthly payments), so Active/Inactive-Deferred may differ by ~1e-4 from a legacy tool that approximates the first half-year with a 7/24 + 5/24 closed form — a deliberate, more-exact choice.
COLA & interest helpers
| Function | Use it for | Method / anchor |
|---|---|---|
aos_Cola(selectRates, ultimate, [max]) | A select-and-ultimate COLA schedule | Threaded through all annuity kernels; an ultimate-only schedule equals the level path. |
aos_SegmentDF(t, seg1, seg2, seg3) | §430 three-segment discount factor at t | (1 + seg_k)^−t at the ONE rate of the period t falls in — seg1 on (0,5), seg2 on [5,20), seg3 on [20,∞); a SPOT rate, not chained (ENGINE-N) — == segmentDiscountFactor. |
aos_Curve(years, rates, [compounding], [valuationOffset]) | Build a spot-curve interest spec | For the rate arg of any annuity; == yieldCurve.js (linear interp, flat-extrapolated). compounding = "annual" (default, (1+s)^-t) or "semiannual" ((1+s/2)^-2t, the PPA "half year rates" convention). valuationOffset = years from the curve's as-of date to the attained-age point (default 0 = value at the curve date); >0 discounts on the curve's FORWARD segment, normalized to the attained age — the "PV Factor at Decrement Age" a funding tool (fxAct/Quantify) reports for a deferred/mid-year-commencing benefit. A DEFERRED annuity on a curve (a commence later than the age, or a deferred status) is discounted from the VALUATION date — a payment t years after a commencement n years out at D(A + n + t) / D(A), A = valuationOffset — exactly as a §430 segment triple is (ADDIN-C, Scott's ruling 2026-09-29; before it the add-in restarted the curve at commencement, D(A + n) · D(A + t) / D(A)²). That is a value AS OF the valuation date, survival and discount to commencement included. A funding tool's factor AS OF commencement (per $1 at the decrement age) is instead: value as Inactive-InPay at the commencement age with valuationOffset = years from the valuation date to commencement — and do NOT combine that offset with a deferral as well (the origin would count the deferral twice). |
aos_AdjustCurve(curve, schedMaturities, schedBps) | Apply a maturity-dependent bps spread schedule to a curve spec | curve = a spec string (aos_Curve / aos_HqmCurve / aos_TreasuryCurve / aos_Pbgc4044Curve) or a 2-column [maturity, rate] range; returns an adjusted spec for any annuity's rate arg. The spread is interpolated between schedule maturities and held flat beyond; negative bps allowed; a single-entry schedule is a parallel shift; the spec's compounding/valuationOffset tail is preserved. == rate-engine adjustCurve (golden cross-checked). This is a user-supplied adjustment — label and justify it; it is never a bond-universe percentile selection. |
aos_SpotRate(t, years, rates) | Spot rate at maturity t | == spotRateForYear. |
Economic / statutory / PBGC
| Function | Use it for | Method / anchor |
|---|---|---|
aos_EconAnnualLookup(table, year, [indexRate]) | IRS qualified-plan limits suite | table = "401(a)(17)"/"comp", "415(b)"/"benefit", "415(c)", "402(g)", "414(v)"/"catch-up", "414(q)"/"HCE", "416(i)"/"key", or "wage base". == statutoryLimits.js (2020–2026, IRS cola-table + Notice 2025-67); a year after the table is a projection made only at a STATED `indexRate` — compounded, then floored to the limit's statutory step (engine 2026.06.79 — $5,000 comp/415(b)/HCE/key, $1,000 415(c), $500 402(g)/414(v)); with `indexRate` omitted, an unpublished year REFUSES (#AOS: no IRS … limit is published for YYYY …, naming the years on file — 2026-09-24, N8; it used to default the index to 0 and hold the last limit flat). A stated 0 is a stated flat projection and values, and a negative index projects flat — limits never decrease (Notice 2009-94: a decline in the applicable index cannot reduce the limit; ruled 2026-09-25, P8). Years before the table read its first year. Wage base single-sourced from covered comp; the §230 taxable wage base NEVER projects, whatever index is given — a year after the last published base REFUSES with or without indexRate (42 USC 430(b) indexes the base on the national-average-wage ratio, not a CPI-type rate; ruled 2026-09-25, P2). |
aos_Limit415Adjusted(commenceAge, year, [rate], [maleTable], [femaleTable], [scale]) | §415(b) dollar limit, age-adjusted | The §415(b) DB dollar limit adjusted to actuarial equivalence outside the 62–65 plateau, WITH the deferral (ActOS 2026.06.195, OR2-R1 — Reg. §1.415(b)-1(d)(1), (e)): before 62 L · ä(62) · v^n · ₙp_x / ä(x) (n = 62 − x, reduced); after 65 L · ä(65) / (v^m · ₘp₆₅ · ä(x)) (m = x − 65, increased); L = benefitLimit(year), x = the commencement age rounded half-up, and ä is an annual life annuity-DUE on the §415 basis (default 5% + unisex 50/50 applicable mortality, generational via scale, the survival decrement applied). Through .197 this was L · ä(ref)/ä(commence) — two immediate annuities, no deferral; saved cells outside 62–65 MOVE at the rebuild. The §415(b)(1)(B) 100%-of-compensation prong is its own function, aos_Limit415Comp; the §415(b) limit is the lesser of the two. Ages 62–65 return L unchanged. year must have a published §415(b) limit — this function takes no index, so an unpublished year REFUSES (#AOS: …; 2026-09-24, N8 — it held the last limit flat). The §415(b)(2)(E) "lesser/greater of plan basis vs 5%+applicable" comparison is the user's. An unknown maleTable / femaleTable id returns #NAME? at every age (2026-09-24), like every table-taking function — never a limit priced on a half-missing blend. |
aos_Limit415Comp(high3, [serviceYears]) | §415(b)(1)(B) compensation limit | 100% of the participant's average compensation for the high-3 years, prorated for years of service under 10 (§415(b)(5)(B)) and never below 1/10 (§415(b)(5)(C)): high3 × max(min(serviceYears / 10, 1), 0.1) — the engine's own expression (capBenefit; ADDIN-C, Scott's ruling 2026-09-29). serviceYears omitted = no proration (factor 1), as the engine reads an unstated service. Not age-adjusted. high3 is an input: the average for the high-3 years with each year's pay already capped at §401(a)(17) — the engine caps and averages the pay history itself (high3Comp415); a worksheet function cannot see it. The §415(b) limit is the lesser of this and the dollar limit (aos_Limit415Adjusted). A high3 that is not a positive amount REFUSES (#AOS: …) — the engine reads it as no pay stated and applies no compensation prong, and a $0 limit would be a confident wrong answer; a non-numeric serviceYears is #VALUE!. |
aos_CoveredComp(yearOfBirth, [determinationYear], [rounded]) | IRC §401(l)(5)(E) covered compensation | The 35-year average of the SSA taxable wage bases ending in the year the employee attains social security retirement age (the §415(b)(8) whole-year SSRA: 65 for birth < 1938, 66 for 1938–1954, 67 for 1955+). Wage bases for years after determinationYear are held at the determination-year base; unrounded = floor(35-yr average / 12) × 12. rounded=TRUE → the IRS rounded table (nearest $3,000, capped at the wage base). Cross-checked to the dollar against IRS Rev. Rul. 2026-1 (the SSA wage-base series is validated by the exact match over all 87 birth years — see coveredComp.test.js). determinationYear defaults to the latest published wage base (2026). What the regulation holds, and what it does not (Q9, ruled 2026-09-25): Treas. Reg. §1.401(l)-1(c)(7)(i) assumes the wage base for every year AFTER the determination year, inside the 35-year average, equals the year-D base — it does not say which table an unpublished determination year uses; that is a plan election. So a determination year after the last published wage base REFUSES (it used to be held at the latest table, on that citation — N8's "ruled hold", reversed), like the limits and the PBGC values. A determination year before the first wage base (1937) has no base either and REFUSES (#AOS: no §230 taxable wage base is published for …; 2026-09-25, AL3 — it used to print the text #ERR The given key was not present in the dictionary.). The span: birth years whose 35-year window starts in 1937 or later — a birth year whose 35-year window starts before 1937, the first wage base (births 1905 and earlier under SSRA 65), REFUSES (#AOS: no §230 taxable wage base is published for 1936 …; 2026-09-25, AU4 — ruling AR4 (a): it borrowed the 1937 base for the missing years, a value §1.401(l)-1(c)(7)(i) does not state; the IRS tables start at birth year 1907). 1906 answers 4,320 and 1907 4,488 (2026 determination year). |
aos_EconMonthlyLookup("Segment 1|2|3"|"24-mo Segment …", year, month) | §430 segment rates (spot or 24-mo avg) | The 24-mo average FOR month M is the mean of the 24 months M−24 … M−1 (§430(h)(2)(D)(i), "ending with the month preceding"; through ActOS .211 it read M−23 … M — matches the IRS published table since .212). The average is taken at two decimals of a percent, half-up (the rate of record since ActOS .231; the add-in since AN1, 2026-10-18 — it returned the unrounded mean). Reproduces getSegmentRates/get24MonthAverageRates on every published month — not where a month is missing: ActuarialOS substitutes and flags that case (substituted / monthsMissing), the add-in has no flag channel and refuses. An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; AI1, 2026-09-24, R7's rule carried to this family). A 24-month average needs all 24 months published — a window reaching a month not on file REFUSES, naming the first missing month (it used to average only the months it found; the ActOS side flags the same case monthsMissing). |
aos_SegRate(years, year, month, [basis], [planYear]) | §430 segment rate by duration | The segment is chosen by the time to payment — under 5 yrs = seg 1, 5 to under 20 = seg 2, 20 and over = seg 3 (the boundary aos_SegmentDF applies; through ActOS .205 exactly 5 and 20 read the EARLIER segment) — and returns that segment's rate. basis = spot (default) / 24mo / stabilized / 25yr. Each basis REFUSES what it cannot state (2026-09-24, AI1): an unpublished month (spot), a 24-month window not wholly published (24mo, stabilized), and a plan year with no published 25-year averages (25yr — it used to show an untyped #VALUE! — and stabilized, which used to return the unclamped 24-month average). |
aos_Seg25Avg(planYear, seg) | §430 25-year average segment rate | IRS Table 2A (incl. ARPA 5% floor), decimal; seg 1/2/3. Returns `#N/A` for a plan year the IRS has not published — it no longer holds the nearest year's rate forward. Attested input — verify. |
aos_SegCorridor(planYear, "min"|"max") | MAP-21/ARPA/IIJA stabilization corridor | Applicable min/max fraction: 95%/105% for 2020–2030, then widening 5%/yr to 70%/130% after 2034; 90%/110% pre-2020. (Corrected 2026-08-06 — this previously widened from 2026, i.e. ARPA §9706(a)(1) as enacted, missing IIJA §80602, which extended 95%/105% through 2030. Verified vs IRS Funding Table 1A and Notice 2026-19.) |
aos_SegStabilized(planYear, rateYear, rateMonth, seg) | Stabilized §430 segment rate | The 24-mo average for rateYear/rateMonth clamped to the corridor around the 25-yr average; == stabilizeSegmentRates. The rounded average is compared with the floor / ceiling and the figure taken is at two decimals of a percent, half-up (ActOS .231; the add-in since AN1, 2026-10-18). REFUSES a lookback window not wholly published, and a plan year with no published 25-year averages (2026-09-24, AI1 — it used to return the unclamped 24-month average as though stabilized; the ActOS run path refuses the same case). |
aos_Pbgc4044(year, month, maturity) | PBGC §4044 monthly spot rate | Published month-end curve (series begins 2024-07); reproduces spotRateForYear. An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; R7, 2026-09-24). Attested input — verify. |
aos_Pbgc4044Curve(year, month) | PBGC §4044 curve spec (interest arg) | A curve usable as any annuity's rate arg. An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; R7, 2026-09-24). |
aos_Hqm(year, month, maturity) | Treasury HQM monthly spot curve — spot rate | The Treasury HQM corporate-bond monthly spot curve, the source of the §430 segment rates; published monthly spot (0.5–30, flat beyond), reproduces spotRateForYear. Not the full §430 curve: Reg. §1.430(h)(2)-1(d)(1)(i) runs the §430(h)(2)(D)(ii) full yield curve from 6 months to 100 years; the add-in carries the HQM curve as published, 0.5–30 years (flat beyond 30), and ActuarialOS builds no full-yield-curve election yet. An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; R7, 2026-09-24). Attested input — verify. |
aos_HqmCurve(year, month) | Treasury HQM curve spec (interest arg) | Value an annuity directly on the Treasury HQM monthly spot curve (0.5–30, flat beyond) — usable as any annuity's rate arg. Same curve as aos_FullYieldCurve. An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; R7, 2026-09-24). |
aos_FullYieldCurve(year, month) | Treasury HQM curve spec under its funding name (interest arg) | The HQM curve as a curve spec (== aos_HqmCurve). It is not the §430(h)(2)(D)(ii) full yield curve a plan may elect: Reg. §1.430(h)(2)-1(d)(1)(i) runs the §430(h)(2)(D)(ii) full yield curve from 6 months to 100 years; the add-in carries the HQM curve as published, 0.5–30 years (flat beyond 30), and ActuarialOS builds no full-yield-curve election yet. An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; R7, 2026-09-24). |
aos_HqmTable(year, month, [header]) | HQM spot curve, spilling | Spills Maturity, Spot for every published half-year tenor (0.5–30) — see/plot what the add-in carries (the full §430 curve runs to 100 years). Each Spot == aos_Hqm at that maturity. An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; R7, 2026-09-24). |
aos_Treasury(year, month, maturity, [spreadBps]) | Treasury PAR yield (month-end CMT) | Published Daily Treasury Par Yield Curve Rates, month-end; interpolated, flat beyond 30. PAR basis (semiannual coupon-equivalent), NOT a spot rate — for a corporate spot curve use aos_Hqm. Optional parallel spread in bps ("Treasury + spread"); numeric text such as "25" reads as 25, and non-numeric text, TRUE/FALSE or an error value is #VALUE! (AP2, 2026-09-25 — it read as a 0 spread). An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; AI1, 2026-09-24, R7's rule carried to this family). Attested input — verify vs home.treasury.gov. |
aos_TreasuryCurve(year, month, [spreadBps]) | Treasury par curve spec (interest arg) | The month's par curve (+ spread) as a curve spec. Caution: par yields, not spots — discounting on it is the labeled "Treasury + spread" approximation convention; for spot discounting use aos_HqmCurve. An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; AI1, 2026-09-24, R7's rule carried to this family). |
aos_TreasuryTable(year, month, [header], [spreadBps]) | Treasury par curve, spilling | Spills Maturity, Par yield for every published tenor of the month (tenors vary — a 1.5-mo tenor appears 2025-02). Each row == aos_Treasury at that maturity, spread included. An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; AI1, 2026-09-24, R7's rule carried to this family). |
aos_PbgcPremium(year, "flat"|"vrp"|"cap"|"me") | PBGC premium rates | Single-employer flat $/participant, variable-rate $/\$1,000 UVB, VRP per-participant cap $, or multiemployer flat $/participant. A year not on file REFUSES (#AOS: no PBGC … is published for YYYY, naming the years on file — 2026-09-24, N8; it used to clamp to the nearest year): a premium rate is set for each plan year and is not projectable. Attested input — verify. |
aos_PbgcMaxGuarantee(year, [age]) | PBGC §4022(b) max guarantee | Straight-life max guaranteeable benefit, $/month; age defaults 65, and ages 45–75 apply PBGC's published §4022 age factors (interpolated; 66 = 1.10 … 70 = 1.66 … 75 = 3.04 — 2026-09-25, AP3 / Q8: ages above 65 used to return the age-65 maximum, $7,789.77 at 70 in 2026 where PBGC publishes $12,931.02). An age outside 45–75 REFUSES (#AOS: no … age factor is published for age …) — PBGC prints no row there (it took the age-45 factor below 45, a row PBGC does not print; Scott's ruling Q8). A year not on file REFUSES (#AOS: …, naming the years on file — 2026-09-24, N8; it used to clamp to the nearest year): the maximum guarantee is published for each year and is not projectable. Attested input — verify. |
aos_SsBendPoint(year, 1|2) | Social Security PIA bend points | The first/second dollar bend point in the PIA formula (90%/32%/15%). Backfilled from the source of record (Q6, 2026-09-25 — every cell rebuilt from the committed page by ssDataProvenance.test.js): eligibility years 1979–2026 (SSA OACT bend points). A year outside them REFUSES (#AOS: no Social Security PIA bend point is published for …) — it was held flat at the nearest stored year. Attested input — verify vs SSA. |
aos_SsCola(year) | Social Security COLA (decimal) | The CPI-W–based benefit increase effective that January (SSA's row for the prior year, payable from its December). Backfilled from the source of record (Q6, 2026-09-25 — every cell rebuilt from the committed page by ssDataProvenance.test.js): 1984–2026 (SSA OACT COLA series; the 1975–82 COLAs were payable for June and are not January COLAs). A year outside them REFUSES (#AOS: …) — a future COLA is an assumption the plan states, not a lookup; it was held flat. Attested input — verify vs SSA. |
aos_SsAwi(year) | National Average Wage Index | The SSA AWI ($); ~2-year publication lag. Backfilled from the source of record (Q6, 2026-09-25 — every cell rebuilt from the committed page by ssDataProvenance.test.js): 1951–2024 (SSA OACT AWI series). A year outside them REFUSES (#AOS: …) — a later year's index is a wage-growth assumption; it was held flat. Attested input — verify vs SSA. |
aos_SsNRA(birthYear, [output]) | Social Security full (normal) retirement age | The statutory §416(l) table (not a published series): 65 through 1937, +2 mo/yr to 66 (1943–54), +2 mo/yr to 67 (1960+). output = "years" (default, decimal e.g. 66.8333) | "months" | "ym" ("66y 10m"). SSA treats a Jan-1 birth as the prior year — pass the adjusted year if that applies. |
aos_SsPIA(aime, eligibilityYear, [colaToYear]) | Social Security PIA from AIME | 90/32/15 bend formula on the eligibility-year (age-62) bend points, dime-rounded; optional COLA-forward. Supply AIME. An eligibility year outside the bend-point series, or a `colaToYear` past the last published COLA, REFUSES (#AOS: …; Q6, 2026-09-25) — it held the nearest stored year flat and compounded the last COLA forward, an unstated COLA assumption. Eligibility years 1979–1982 cannot be COLA-forwarded: they answer their own PIA, and any colaToYear after the eligibility year REFUSES — the June 1975–82 COLAs are not January COLAs, so the stored COLA series starts at 1984 (2026-09-25, AT5); 1983 forwards. Attested input — verify vs SSA. |
aos_Cpi(year) | BLS CPI-U annual average | All Urban Consumers, U.S. city average (1982-84=100); for plan COLAs tied to CPI-U. (SS COLA = CPI-W-based: aos_SsCola.) Backfilled from the source of record (Q6, 2026-09-25 — every cell rebuilt from the committed page by ssDataProvenance.test.js): 1913–2025 (BLS series CUUR0000SA0, the M13 annual average). A year outside them REFUSES (#AOS: …) — it was held flat. Attested input — verify vs BLS. |
Dates
| Function | Use it for | Method / anchor |
|---|---|---|
aos_DateAdd(date, years, months, days, [eom], [method]) | Add y/m/d (eom-aware); service adjustment | method shifts an entry date FORWARD by a break, quantized to whole months/years (down/up/near) for credited service. Default "exact". Break-in-service eligibility is the user's. |
aos_DateDiff(start, end, [interval], [rounding]) | Difference in y/m/d, with rounding | "y"/none == ActOS ageAtDate. rounding="component" returns the calendar component; interval "ymd" returns the "Xy Ym Zd" breakdown string. |
Diagnostics
| Function | Use it for | Method / anchor |
|---|---|---|
aos_Version() | Version stamp (audit) | A one-line stamp: the add-in version, the ActOS ENGINE_VERSION these numbers reproduce, the data-library version, and the generation date. Put it next to a signed result. |
aos_License() | License status | The beta key / tier (metadata only — never plan data). During the beta the add-in always runs (fail-open); set your beta key via the ribbon License button. |
aos_Versions() | Full version block (spills) | Spills component, value rows — add-in, engine, data manifest, mortality-table count, generation date. The audit stamp for a signed workbook. |
aos_Attest(table) | Assumptions behind a calc | For a mortality table id: its basis/gender/base year, the data-provenance caveat, and the engine + data + generation stamp — a one-cell attestation to drop beside a signed number. Omit table for the engine/data stamp only. |
aos_Tables([contains]) | Discover table ids (spills) | Lists the available mortality table ids, optionally filtered to those containing a substring. Copy a name into any table argument — the fix for #NAME? from a mis-typed table. |
aos_Scales() | Discover improvement-scale ids (spills) | Lists the scale ids (incl. "None") valid for the scale argument. |
aos_Check(table) | Validate a table id | "OK" if the table exists, else an actionable #AOS message naming the nearest valid table (edit-distance "did you mean…?"). Run it to debug a #NAME? before signing. |
aos_IsRefused(cell) | Is this cell a refusal? (test before totalling) | TRUE when the cell holds an ActuarialOS typed refusal — text starting #AOS: — else FALSE (a number, other text, blank, an Excel error value). A refusal is TEXT by ruling (2026-09-25, Q10 (i) — its sentence is the point), and SUM / AVERAGE skip text while ISERROR reads it FALSE, so a total over refused cells drops them without a sign. A range answers per cell (spills): =SUMPRODUCT(--aos_IsRefused(B2:B99)) counts what a SUM beside it skipped. Never refuses. |
aos_Default(value, fallback) | Reproducible firm default | Returns value when supplied, else fallback. Point every formula at one firm-config cell with a built-in fallback — e.g. aos_Default(Config!$B$1, "Pri-2012 Male"). The resolved value stays visible in the cell; there is no hidden global default state (a deliberate choice — silent machine-level defaults would let the same formula give different numbers, breaking reproducibility). |
Verify before signing
Functions reproduce actuarialEngine.js exactly; the data is the attest surface.
- Mortality tables. Use the real published SOA tables (Pri-2012 / RP-2014 Total-Dataset Employee / Healthy Retiree-or-Annuitant / Disabled; WC/TQ role tables). The four "Combined Healthy" base tables are illustrative/approximate — not for signed work. Confirm any table against the SOA publication.
- Legacy historical tables (for old plan docs / §415·§417 conversions / prior-basis ties):
GAM-83 Male,GAM-83 Female,UP-94 Male,UP-94 Female,GATT-83 Unisex— published SOA annuitant tables imported fromdata/source/historical-tables.xlsx. These are STATIC tables (no built-in projection). NOTE: the engine clamps the lookup age to [18, 120], so although the CSVs carry the full published range (UP-94 from age 1, GAM/GATT from age 5), ages below 18 return the age-18 rate — irrelevant for annuitant/pension use but don't read these tables below 18. - Public-plan tables (Pub-2010 / Pub-2016) for governmental plans:
PubT-/PubS-/PubG-(Teachers / Safety / General){2010,2016}×Employee/Healthy Retiree/Disabled Retiree/Contingent Survivor×Male/Female— the SOA Pub-2010 study, Total Dataset, amount-weighted (e.g.PubG-2010 Healthy Retiree Male). STATIC tables (no built-in projection). Attested published INPUT — verify against the SOA Pub-2010 publication before signed use. (Above-/Below- Median income variants are not loaded.) - Generational improvement scales (2-D, age × calendar year) project qₓ forward. `aos_Scales()` lists every one the add-in carries — ask it rather than trusting a list in a document, which goes stale on the next lawful addition (this line named two of the three SOA scales for a year; MP-2020 has been carried since ActOS
2026.06.39). Each is stored as the forward slice from base year 2012, flat-held at its ultimate column beyond the last published year. Use via thescalearg, e.g.aos_Mort(table, 0, "MP-2019", 2030)oraos_qx(table, x, , , "MP-2019", 2030, TRUE). - A table's closing qₓ of 1.0 is never improved (ActOS2026.06.187, ruling N2): the 1.0 a table states at its last age, and the certain death above it, are returned as 1.0 under EVERY scale, band or generational. Before, the scale multiplied it, so where a scale is not exactly 1 at the closure (the retired Scale AA band's 0.005 at 95+; MP-2021 / MP-2019 at 110..119 on GAM-83 / GATT-83, which end at 110) a life was kept alive past the table's end. A 1.0 in the body of a table (before its last age) is improved like any rate. - The SOA scalesMP-2021,MP-2020andMP-2019come from the SOA rates files — verify against the SOA publication for signed use. - `IRS 2024 Adjusted MP-2021` (ActOS2026.06.155) is a different document and a United States Government work: the IRS's "2024 Adjusted Scale MP-2021 Rates" (August 11, 2023), which 26 CFR §1.430(h)(3)-1(b)(1)(iii)(A) makes the prescribed §430 improvement rates for valuation dates on or after January 1, 2024. It agrees with the SOA'sMP-2021for 2013-2020 and differs in every year from 2021 on, and it carries a 2037+ ultimate the SOA scale does not. Do not treat one as a spelling of the other. (The PLANMP201D 1-D scale was removed with the Pre-82 subsystem 2026-08-26, ActOS2026.06.143.) - PBGC §4044 spot curves are an attested published INPUT — verify against the PBGC ERISA 4044 Yield Curves workbook for the measurement month.
- Statutory limits cover only §401(a)(17) and §415(b); statutory projection past the table is unrounded.
- Optional-form / service conventions (cash refund, SS level-income, conversion factor, DateAdd service-adjustment) are mechanical actuarial-equivalence baselines — a plan's published factors / break-in-service rules override them and are supplied by the user.
See PROJECT.md (§7 limitations, §8 findings) for the full conventions and caveats.
Glossary (concepts behind the functions)
For the new analyst. The same glossary (plus a "Your first annuity" walkthrough) is in the ribbon Help dialog; a full worked workbook is Addin/SAMPLES.md.
- Mortality table — yearly death probabilities (qₓ) by age, e.g.
Pri-2012 Male.aos_Tables()lists the loaded ids. Ids match forgivingly — case, spaces, and hyphens are ignored ("pri2012male"resolves toPri-2012 Male); the exact published id is still what attestations and audit output show. - qₓ / pₓ — qₓ = chance a life age x dies within the year; pₓ = 1 − qₓ (survival to x+1).
- Improvement scale — how mortality falls over calendar time (e.g.
MP-2021);aos_Scales()lists them. Static projects a table to one year and holds it; generational keeps improving qₓ as a person ages through future years. - Annuity (ä) — PV today of $1/yr while a life survives:
aos_ax(single),aos_axy(joint). Due pays at the start of each period, immediate at the end (thetimingarg). - Commutation functions — pre-summed columns (Dₓ, Nₓ, Cₓ, Mₓ) making a PV a ratio (ä = Nₓ/Dₓ);
aos_Dx/aos_Nx/aos_CommTable. - COLA — yearly % the benefit grows in payment (level, or
aos_Colaselect-and-ultimate with a cap). - Deferral / commencement — a benefit starting later than today; the PV discounts and applies survival to the commencement age (the
commencearg). - Discount basis — flat rate, a §430 three-segment set, or a spot curve (
aos_Curve/aos_Pbgc4044Curve); any can be the annuityratearg. - §430 segment rates — three IRS interest rates for payments under 5 years / 5 to under 20 / 20 and over, each applied as a spot rate from the valuation date (
aos_EconMonthlyLookup,aos_SegmentDF). - §417(e) lump sum — minimum lump-sum value = annual benefit × an annuity-due factor on the applicable basis (
aos_LumpSum417). `applicableTable` (2026.06.91) names the PUBLISHED §417(e) applicable mortality table, e.g."PPA Static 2026"— one token for the basis AND its year, the same vocabulary the JS engine uses (lumpSumApplicableTable). When given it REPLACES the male/female blend, because a published static table already embeds improvement to its own year — so passscaleas"None"or omit it: a scale STATED besideapplicableTableis refused with#AOS: applicableTable carries its own basis — remove the scale argument(since 2026-09-05; it used to be discarded silently). OmitapplicableTablefor the computed 50/50 blend. An unpublished year returns#NAME?rather than the nearest year — a static table is fixed by statute, not extrapolated. The same rule holds for everyaos_function: a PPA static table under any improvement scale refuses (#AOS: … already embeds improvement to its own year) rather than compounding a second one. - §415(b) / §401(a)(17) — IRS caps on the annual DB benefit / counted pay (
aos_EconAnnualLookup,aos_Limit415Adjusted, and the §415(b)(1)(B) compensation prongaos_Limit415Comp). - Covered compensation — 35-yr average of SSA wage bases to SSRA, for integrated formulas (
aos_CoveredComp). - J&S — keeps paying a % to the survivor: ä_member + jsPct·(ä_survivor − ä_joint) (
aos_axJS,aos_ConvFactor). - Early-retirement factor (ERF) — reduction for commencing before NRA (
aos_ERF; plan tables override). - Optional form — an alternate payment shape actuarially equivalent to the normal form (
aos_axc,aos_axt,aos_axRefund,aos_axSSLevel). - Engine anchor / golden — every
aos_number reproduces a specific ActuarialOS engine value, checked to machine precision by a generated golden grid. - Provenance / attestation — where a table's numbers come from and how far they're verified;
aos_Atteststamps it (basis + caveat + engine/data version) beside a signed result. - Spill (dynamic array) — a function returning a whole range from one cell (
aos_CommTable,aos_axTrace); legacy Excel needs Ctrl+Shift+Enter.
← ActuarialOS for Excel · Questions: hello@actuarialos.io