@jarenjs/core 0.9.2 → 0.34.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +229 -26
- package/README.md +64 -10
- package/dist/types/array.d.ts +13 -0
- package/dist/types/cache.d.ts +166 -0
- package/dist/types/chunk.d.ts +102 -0
- package/dist/types/color.d.ts +64 -0
- package/dist/types/convert/convert.d.ts +33 -0
- package/dist/types/convert/currency.d.ts +47 -0
- package/dist/types/convert/index.d.ts +9 -0
- package/dist/types/convert/registry.d.ts +55 -0
- package/dist/types/dates/civil.d.ts +146 -0
- package/dist/types/dates/duration.d.ts +83 -0
- package/dist/types/dates/format.d.ts +59 -0
- package/dist/types/dates/index.d.ts +4 -0
- package/dist/types/{dates.d.ts → dates/rfc3339.d.ts} +57 -3
- package/dist/types/errors.d.ts +77 -0
- package/dist/types/finance/amortization.d.ts +51 -0
- package/dist/types/finance/bond.d.ts +45 -0
- package/dist/types/finance/cashflow.d.ts +44 -0
- package/dist/types/finance/depreciation.d.ts +26 -0
- package/dist/types/finance/index.d.ts +16 -0
- package/dist/types/finance/indicators.d.ts +76 -0
- package/dist/types/finance/interest.d.ts +44 -0
- package/dist/types/finance/returns.d.ts +43 -0
- package/dist/types/finance/tvm.d.ts +50 -0
- package/dist/types/geo/angle.d.ts +4 -0
- package/dist/types/geo/bbox.d.ts +32 -0
- package/dist/types/geo/distance.d.ts +67 -0
- package/dist/types/geo/geohash.d.ts +63 -0
- package/dist/types/geo/geojson.d.ts +108 -0
- package/dist/types/geo/index-tree.d.ts +52 -0
- package/dist/types/geo/index.d.ts +11 -0
- package/dist/types/geo/mercator.d.ts +57 -0
- package/dist/types/geo/predicates.d.ts +44 -0
- package/dist/types/geo/ring.d.ts +80 -0
- package/dist/types/geo/simplify.d.ts +31 -0
- package/dist/types/geo/valid.d.ts +24 -0
- package/dist/types/geo/wkt.d.ts +16 -0
- package/dist/types/math/float64.d.ts +126 -3
- package/dist/types/math/format.d.ts +58 -0
- package/dist/types/math/index.d.ts +5 -0
- package/dist/types/math/mat4.d.ts +60 -0
- package/dist/types/math/project.d.ts +54 -0
- package/dist/types/math/solve.d.ts +67 -0
- package/dist/types/math/word.d.ts +82 -0
- package/dist/types/message.d.ts +52 -0
- package/dist/types/object.d.ts +151 -0
- package/dist/types/scan.d.ts +30 -0
- package/dist/types/schema.d.ts +24 -0
- package/dist/types/string.d.ts +96 -0
- package/dist/types/text/base64.d.ts +6 -4
- package/dist/types/text/email.d.ts +0 -1
- package/dist/types/text/host.d.ts +39 -8
- package/dist/types/text/i18n.d.ts +13 -1
- package/dist/types/text/index.d.ts +1 -0
- package/dist/types/text/misc.d.ts +15 -1
- package/dist/types/text/punycode.d.ts +45 -85
- package/docs/CONVERT.md +45 -0
- package/docs/DATES.md +78 -0
- package/docs/FINANCE.md +59 -0
- package/docs/GEO.md +114 -0
- package/docs/MATH.md +75 -0
- package/package.json +57 -4
- package/src/array.js +16 -0
- package/src/cache.js +206 -0
- package/src/chunk.js +159 -0
- package/src/color.js +125 -0
- package/src/convert/convert.js +59 -0
- package/src/convert/currency.js +74 -0
- package/src/convert/index.js +11 -0
- package/src/convert/registry.js +213 -0
- package/src/dates/civil.js +360 -0
- package/src/dates/duration.js +225 -0
- package/src/dates/format.js +238 -0
- package/src/dates/index.js +30 -0
- package/src/dates/rfc3339.js +621 -0
- package/src/errors.js +102 -0
- package/src/finance/amortization.js +50 -0
- package/src/finance/bond.js +100 -0
- package/src/finance/cashflow.js +122 -0
- package/src/finance/depreciation.js +54 -0
- package/src/finance/index.js +27 -0
- package/src/finance/indicators.js +207 -0
- package/src/finance/interest.js +63 -0
- package/src/finance/returns.js +90 -0
- package/src/finance/tvm.js +94 -0
- package/src/function.js +0 -2
- package/src/geo/angle.js +12 -0
- package/src/geo/bbox.js +80 -0
- package/src/geo/distance.js +155 -0
- package/src/geo/geohash.js +224 -0
- package/src/geo/geojson.js +361 -0
- package/src/geo/index-tree.js +294 -0
- package/src/geo/index.js +52 -0
- package/src/geo/mercator.js +124 -0
- package/src/geo/predicates.js +313 -0
- package/src/geo/ring.js +182 -0
- package/src/geo/simplify.js +130 -0
- package/src/geo/valid.js +142 -0
- package/src/geo/wkt.js +262 -0
- package/src/math/float64.js +228 -11
- package/src/math/format.js +157 -0
- package/src/math/index.js +5 -0
- package/src/math/mat4.js +131 -0
- package/src/math/project.js +49 -0
- package/src/math/solve.js +112 -0
- package/src/math/word.js +177 -0
- package/src/message.js +119 -0
- package/src/object.js +329 -0
- package/src/scan.js +42 -0
- package/src/schema.js +37 -0
- package/src/string.js +189 -0
- package/src/text/base64.js +15 -52
- package/src/text/email.js +0 -5
- package/src/text/host.js +515 -140
- package/src/text/i18n.js +13 -6
- package/src/text/identifiers.js +1 -1
- package/src/text/index.js +1 -0
- package/src/text/misc.js +81 -5
- package/src/text/punycode.js +235 -323
- package/src/dates.js +0 -371
package/src/errors.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The suite's one coded-error contract. Every engine error that
|
|
4
|
+
* carries a stable diagnosis code derives from {@link CodedError}; the
|
|
5
|
+
* canonical form is the structured triple `{ code, reason, docPath |
|
|
6
|
+
* dataPath }`, and `message` is COMPOSED from it for the channels that
|
|
7
|
+
* have no structured reader (an uncaught throw, a console, a CI
|
|
8
|
+
* transcript, a model reading `{ error: err.message }`).
|
|
9
|
+
*
|
|
10
|
+
* The rendering decision, fixed here so every package agrees:
|
|
11
|
+
*
|
|
12
|
+
* `${code}: ${reason}` — no location
|
|
13
|
+
* `${code}: ${reason} at ${docPath}` — a document location
|
|
14
|
+
* `${code}: ${reason} in data ${dataPath}` — a data location
|
|
15
|
+
* `${code}: ${reason} at ${d} in data ${p}` — both
|
|
16
|
+
*
|
|
17
|
+
* `docPath` points into the offending *document* (a query, a stylesheet,
|
|
18
|
+
* an app document); `dataPath` points into the offending *data* the
|
|
19
|
+
* document was applied to. They are different facts and stay separately
|
|
20
|
+
* readable and separately identifiable — `at` always means document,
|
|
21
|
+
* `in data` always means data. A root pointer (`''`) is a real location
|
|
22
|
+
* and renders as `at ''`; absence is `undefined` and renders nothing.
|
|
23
|
+
* Absence must never be normalized to `''` — the AI repair loop's
|
|
24
|
+
* `docPath ?? instancePath ?? ''` fallback chain relies on `undefined`
|
|
25
|
+
* to fall through, and `''` is a legitimate root pointer.
|
|
26
|
+
*
|
|
27
|
+
* Cause presence uses the `hasOwn` form: the constructor takes an
|
|
28
|
+
* options bag, and `Object.hasOwn(options, 'cause')` decides whether an
|
|
29
|
+
* own `cause` is installed — so a host that threw `undefined` (passed as
|
|
30
|
+
* `{ cause: undefined }`) stays distinguishable from "no cause" (no
|
|
31
|
+
* options, or an options bag without the key). Positional-adapter
|
|
32
|
+
* subclasses that translate an optional trailing `cause` parameter MUST
|
|
33
|
+
* map an `undefined` argument to "no cause", never to
|
|
34
|
+
* `{ cause: undefined }`.
|
|
35
|
+
*
|
|
36
|
+
* Deliberately OUTSIDE this contract: `ValidationError`
|
|
37
|
+
* (keyword-keyed, locale-rendered, has its own `toJSON`), the
|
|
38
|
+
* `LabeledSyntaxError` family (`source`/`position` over a source
|
|
39
|
+
* string), and josl's line/column family (`line`/`column`/`hint`).
|
|
40
|
+
* Different shapes for different consumers — do not "finish the job" by
|
|
41
|
+
* folding them in.
|
|
42
|
+
*
|
|
43
|
+
* Localization: `@jarenjs/locales` keys its catalogs by JSON Schema
|
|
44
|
+
* KEYWORD (`form/minLength`, …), not by error code — coded errors are
|
|
45
|
+
* localizable by nothing today. Stated so nobody assumes otherwise; a
|
|
46
|
+
* code-keyed catalog would be a new locales feature, not a property of
|
|
47
|
+
* this base.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Compose the canonical coded message from the structured fields.
|
|
52
|
+
* @param {string} code - Stable diagnosis code
|
|
53
|
+
* @param {string} reason - The bare human-readable reason
|
|
54
|
+
* @param {string} [docPath] - JSON Pointer into the offending document
|
|
55
|
+
* @param {string} [dataPath] - Pointer/path into the offending data
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
function composeCodedMessage(code, reason, docPath, dataPath) {
|
|
59
|
+
let message = `${code}: ${reason}`;
|
|
60
|
+
if (docPath !== undefined) message += ` at ${docPath === '' ? "''" : docPath}`;
|
|
61
|
+
if (dataPath !== undefined) message += ` in data ${dataPath === '' ? "''" : dataPath}`;
|
|
62
|
+
return message;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Base class for every coded error in the suite. Subclasses are thin
|
|
67
|
+
* per-package adapters that fix the public `name` (passed as a string
|
|
68
|
+
* literal because bundles are minified and a mangled class name must
|
|
69
|
+
* not leak into `error.name`) and translate their historical positional
|
|
70
|
+
* signatures into the location/options form.
|
|
71
|
+
*/
|
|
72
|
+
export class CodedError extends Error {
|
|
73
|
+
/**
|
|
74
|
+
* @param {string} name - The public class name for `error.name`
|
|
75
|
+
* @param {string} code - Stable diagnosis code
|
|
76
|
+
* @param {string} reason - The bare reason; `message` is composed from
|
|
77
|
+
* `code`, `reason` and the location and must not be pre-composed
|
|
78
|
+
* @param {string | { docPath?: string, dataPath?: string }} [location]
|
|
79
|
+
* - A string is a `docPath`; the object form carries either or both
|
|
80
|
+
* @param {{ cause?: unknown }} [options] - `cause` is installed as an
|
|
81
|
+
* own property exactly when the key is present (`hasOwn` form),
|
|
82
|
+
* preserving "host threw `undefined`" as distinct from "no cause"
|
|
83
|
+
*/
|
|
84
|
+
constructor(name, code, reason, location = undefined, options = undefined) {
|
|
85
|
+
const docPath = typeof location === 'string' ? location : location?.docPath;
|
|
86
|
+
const dataPath = typeof location === 'string' ? undefined : location?.dataPath;
|
|
87
|
+
super(composeCodedMessage(code, reason, docPath, dataPath));
|
|
88
|
+
this.name = name;
|
|
89
|
+
this.code = code;
|
|
90
|
+
this.reason = reason;
|
|
91
|
+
this.docPath = docPath;
|
|
92
|
+
if (dataPath !== undefined) this.dataPath = dataPath;
|
|
93
|
+
if (options !== undefined && Object.hasOwn(options, 'cause')) {
|
|
94
|
+
// The native `new Error(msg, { cause })` shape: non-enumerable,
|
|
95
|
+
// writable, configurable — installed by hand so an explicitly
|
|
96
|
+
// `undefined` cause is representable.
|
|
97
|
+
Object.defineProperty(this, 'cause', {
|
|
98
|
+
value: options.cause, writable: true, enumerable: false, configurable: true,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Loan amortization. Given a principal, a periodic
|
|
4
|
+
* rate and a term, produce the period-by-period split of each payment
|
|
5
|
+
* into interest and principal with the running balance.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { pmt as tvmPmt } from './tvm.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} AmortRow
|
|
12
|
+
* @property {number} period 1-based period index
|
|
13
|
+
* @property {number} payment total payment this period
|
|
14
|
+
* @property {number} interest interest portion
|
|
15
|
+
* @property {number} principal principal portion
|
|
16
|
+
* @property {number} balance remaining balance after this payment
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build a full amortization schedule. The payment is derived from the
|
|
21
|
+
* standard annuity formula unless an explicit `payment` is supplied
|
|
22
|
+
* (e.g. a rounded real-world instalment); the final period absorbs any
|
|
23
|
+
* rounding residue so the balance lands exactly on zero.
|
|
24
|
+
*
|
|
25
|
+
* @param {number} principal loan amount (positive)
|
|
26
|
+
* @param {number} rate periodic interest rate
|
|
27
|
+
* @param {number} nper number of periods
|
|
28
|
+
* @param {{ payment?: number, type?: number }} [opts]
|
|
29
|
+
* @returns {AmortRow[]}
|
|
30
|
+
*/
|
|
31
|
+
export function amortizationSchedule(principal, rate, nper, opts = {}) {
|
|
32
|
+
const type = opts.type ?? 0;
|
|
33
|
+
const payment = opts.payment ?? -tvmPmt(rate, nper, principal, 0, type);
|
|
34
|
+
const rows = [];
|
|
35
|
+
let balance = principal;
|
|
36
|
+
for (let period = 1; period <= nper; period++) {
|
|
37
|
+
const interest = balance * rate;
|
|
38
|
+
let principalPart = payment - interest;
|
|
39
|
+
let pay = payment;
|
|
40
|
+
if (period === nper) {
|
|
41
|
+
// clear any residual balance on the last row
|
|
42
|
+
principalPart = balance;
|
|
43
|
+
pay = balance + interest;
|
|
44
|
+
}
|
|
45
|
+
balance -= principalPart;
|
|
46
|
+
if (Math.abs(balance) < 1e-9) balance = 0;
|
|
47
|
+
rows.push({ period, payment: pay, interest, principal: principalPart, balance });
|
|
48
|
+
}
|
|
49
|
+
return rows;
|
|
50
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Bond math: price, yield-to-maturity, Macaulay and
|
|
4
|
+
* modified duration, and convexity. Basic day-count only (level coupons,
|
|
5
|
+
* whole periods to maturity); exotic conventions are out of scope.
|
|
6
|
+
*
|
|
7
|
+
* Rates and the coupon rate are annual; `freq` coupons are paid per year.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { newtonRaphson, bisect } from '../math/solve.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Clean price of a coupon bond.
|
|
14
|
+
* @param {number} face par/redemption value
|
|
15
|
+
* @param {number} couponRate annual coupon rate
|
|
16
|
+
* @param {number} ytm annual yield to maturity
|
|
17
|
+
* @param {number} years years to maturity
|
|
18
|
+
* @param {number} [freq] coupons per year
|
|
19
|
+
* @returns {number}
|
|
20
|
+
*/
|
|
21
|
+
export function bondPrice(face, couponRate, ytm, years, freq = 2) {
|
|
22
|
+
const n = Math.round(years * freq);
|
|
23
|
+
const c = face * couponRate / freq;
|
|
24
|
+
const y = ytm / freq;
|
|
25
|
+
let price = 0;
|
|
26
|
+
for (let t = 1; t <= n; t++) price += c / Math.pow(1 + y, t);
|
|
27
|
+
price += face / Math.pow(1 + y, n);
|
|
28
|
+
return price;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Yield to maturity from a market price (iterative).
|
|
33
|
+
* @param {number} price @param {number} face @param {number} couponRate
|
|
34
|
+
* @param {number} years @param {number} [freq]
|
|
35
|
+
* @returns {number}
|
|
36
|
+
*/
|
|
37
|
+
export function bondYTM(price, face, couponRate, years, freq = 2) {
|
|
38
|
+
const f = (y) => bondPrice(face, couponRate, y, years, freq) - price;
|
|
39
|
+
const df = (y) => {
|
|
40
|
+
const h = 1e-6;
|
|
41
|
+
return (f(y + h) - f(y - h)) / (2 * h);
|
|
42
|
+
};
|
|
43
|
+
const n = newtonRaphson(f, df, couponRate || 0.05, { tol: 1e-9, maxIter: 100 });
|
|
44
|
+
if (n.converged && isFinite(n.root) && n.root > -1) return n.root;
|
|
45
|
+
const b = bisect(f, -0.9999, 5, { tol: 1e-9, maxIter: 300 });
|
|
46
|
+
return b.converged ? b.root : NaN;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Macaulay duration (in years).
|
|
51
|
+
* @param {number} face @param {number} couponRate @param {number} ytm
|
|
52
|
+
* @param {number} years @param {number} [freq]
|
|
53
|
+
* @returns {number}
|
|
54
|
+
*/
|
|
55
|
+
export function macaulayDuration(face, couponRate, ytm, years, freq = 2) {
|
|
56
|
+
const n = Math.round(years * freq);
|
|
57
|
+
const c = face * couponRate / freq;
|
|
58
|
+
const y = ytm / freq;
|
|
59
|
+
let weighted = 0;
|
|
60
|
+
let price = 0;
|
|
61
|
+
for (let t = 1; t <= n; t++) {
|
|
62
|
+
const cf = t === n ? c + face : c;
|
|
63
|
+
const dpv = cf / Math.pow(1 + y, t);
|
|
64
|
+
price += dpv;
|
|
65
|
+
weighted += t * dpv;
|
|
66
|
+
}
|
|
67
|
+
return (weighted / price) / freq;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Modified duration.
|
|
72
|
+
* @param {number} face @param {number} couponRate @param {number} ytm
|
|
73
|
+
* @param {number} years @param {number} [freq]
|
|
74
|
+
* @returns {number}
|
|
75
|
+
*/
|
|
76
|
+
export function modifiedDuration(face, couponRate, ytm, years, freq = 2) {
|
|
77
|
+
const mac = macaulayDuration(face, couponRate, ytm, years, freq);
|
|
78
|
+
return mac / (1 + ytm / freq);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Convexity (in years²).
|
|
83
|
+
* @param {number} face @param {number} couponRate @param {number} ytm
|
|
84
|
+
* @param {number} years @param {number} [freq]
|
|
85
|
+
* @returns {number}
|
|
86
|
+
*/
|
|
87
|
+
export function convexity(face, couponRate, ytm, years, freq = 2) {
|
|
88
|
+
const n = Math.round(years * freq);
|
|
89
|
+
const c = face * couponRate / freq;
|
|
90
|
+
const y = ytm / freq;
|
|
91
|
+
let price = 0;
|
|
92
|
+
let cx = 0;
|
|
93
|
+
for (let t = 1; t <= n; t++) {
|
|
94
|
+
const cf = t === n ? c + face : c;
|
|
95
|
+
const dpv = cf / Math.pow(1 + y, t);
|
|
96
|
+
price += dpv;
|
|
97
|
+
cx += dpv * t * (t + 1);
|
|
98
|
+
}
|
|
99
|
+
return (cx / (price * Math.pow(1 + y, 2))) / (freq * freq);
|
|
100
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Cash-flow analysis: NPV, IRR, MIRR and their
|
|
4
|
+
* date-indexed variants XNPV/XIRR. Built on `math/solve`. Cash flows are
|
|
5
|
+
* arrays with element `t` occurring at period `t` (index 0 = now).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { newtonRaphson, bisect } from '../math/solve.js';
|
|
9
|
+
|
|
10
|
+
const MS_PER_DAY = 86400000;
|
|
11
|
+
const DAYS_PER_YEAR = 365;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Net present value of a period-indexed cash-flow series.
|
|
15
|
+
* @param {number} rate discount rate per period
|
|
16
|
+
* @param {number[]} cashflows cashflows[t] at period t (index 0 = t0)
|
|
17
|
+
* @returns {number}
|
|
18
|
+
*/
|
|
19
|
+
export function npv(rate, cashflows) {
|
|
20
|
+
let acc = 0;
|
|
21
|
+
for (let t = 0; t < cashflows.length; t++) {
|
|
22
|
+
acc += cashflows[t] / Math.pow(1 + rate, t);
|
|
23
|
+
}
|
|
24
|
+
return acc;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Internal rate of return: the rate for which `npv` is zero. Newton with
|
|
29
|
+
* a bisection fallback; returns `NaN` if no rate is found.
|
|
30
|
+
* @param {number[]} cashflows
|
|
31
|
+
* @param {number} [guess]
|
|
32
|
+
* @returns {number}
|
|
33
|
+
*/
|
|
34
|
+
export function irr(cashflows, guess = 0.1) {
|
|
35
|
+
const f = (r) => npv(r, cashflows);
|
|
36
|
+
const df = (r) => {
|
|
37
|
+
let acc = 0;
|
|
38
|
+
for (let t = 1; t < cashflows.length; t++) {
|
|
39
|
+
acc += -t * cashflows[t] / Math.pow(1 + r, t + 1);
|
|
40
|
+
}
|
|
41
|
+
return acc;
|
|
42
|
+
};
|
|
43
|
+
const n = newtonRaphson(f, df, guess, { tol: 1e-9, maxIter: 100 });
|
|
44
|
+
if (n.converged && isFinite(n.root) && n.root > -1) return n.root;
|
|
45
|
+
const b = bisect(f, -0.999999, 1e6, { tol: 1e-9, maxIter: 300 });
|
|
46
|
+
return b.converged ? b.root : NaN;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Modified internal rate of return.
|
|
51
|
+
* @param {number[]} cashflows
|
|
52
|
+
* @param {number} financeRate rate paid on negative flows
|
|
53
|
+
* @param {number} reinvestRate rate earned on positive flows
|
|
54
|
+
* @returns {number}
|
|
55
|
+
*/
|
|
56
|
+
export function mirr(cashflows, financeRate, reinvestRate) {
|
|
57
|
+
const n = cashflows.length - 1;
|
|
58
|
+
let pvNeg = 0;
|
|
59
|
+
let fvPos = 0;
|
|
60
|
+
for (let t = 0; t < cashflows.length; t++) {
|
|
61
|
+
const cf = cashflows[t];
|
|
62
|
+
if (cf < 0) pvNeg += cf / Math.pow(1 + financeRate, t);
|
|
63
|
+
else fvPos += cf * Math.pow(1 + reinvestRate, n - t);
|
|
64
|
+
}
|
|
65
|
+
if (pvNeg === 0 || fvPos === 0) return NaN;
|
|
66
|
+
return Math.pow(-fvPos / pvNeg, 1 / n) - 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Convert an array of dates (Date, ms timestamp, or day number) to year
|
|
71
|
+
* fractions from the first date.
|
|
72
|
+
* @param {Array<Date|number>} dates
|
|
73
|
+
* @returns {number[]}
|
|
74
|
+
*/
|
|
75
|
+
function yearFractions(dates) {
|
|
76
|
+
const toMs = (d) => (d instanceof Date ? d.getTime() : d * (typeof d === 'number' && d < 1e6 ? MS_PER_DAY : 1));
|
|
77
|
+
const t0 = toMs(dates[0]);
|
|
78
|
+
return dates.map((d) => (toMs(d) - t0) / MS_PER_DAY / DAYS_PER_YEAR);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Date-indexed NPV. `dates` may be `Date`s or ms timestamps.
|
|
83
|
+
* @param {number} rate annual discount rate
|
|
84
|
+
* @param {number[]} cashflows
|
|
85
|
+
* @param {Array<Date|number>} dates
|
|
86
|
+
* @returns {number}
|
|
87
|
+
*/
|
|
88
|
+
export function xnpv(rate, cashflows, dates) {
|
|
89
|
+
const yf = yearFractions(dates);
|
|
90
|
+
let acc = 0;
|
|
91
|
+
for (let i = 0; i < cashflows.length; i++) {
|
|
92
|
+
acc += cashflows[i] / Math.pow(1 + rate, yf[i]);
|
|
93
|
+
}
|
|
94
|
+
return acc;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Date-indexed IRR.
|
|
99
|
+
* @param {number[]} cashflows
|
|
100
|
+
* @param {Array<Date|number>} dates
|
|
101
|
+
* @param {number} [guess]
|
|
102
|
+
* @returns {number}
|
|
103
|
+
*/
|
|
104
|
+
export function xirr(cashflows, dates, guess = 0.1) {
|
|
105
|
+
const yf = yearFractions(dates);
|
|
106
|
+
const f = (r) => {
|
|
107
|
+
let acc = 0;
|
|
108
|
+
for (let i = 0; i < cashflows.length; i++) acc += cashflows[i] / Math.pow(1 + r, yf[i]);
|
|
109
|
+
return acc;
|
|
110
|
+
};
|
|
111
|
+
const df = (r) => {
|
|
112
|
+
let acc = 0;
|
|
113
|
+
for (let i = 0; i < cashflows.length; i++) {
|
|
114
|
+
acc += -yf[i] * cashflows[i] / Math.pow(1 + r, yf[i] + 1);
|
|
115
|
+
}
|
|
116
|
+
return acc;
|
|
117
|
+
};
|
|
118
|
+
const n = newtonRaphson(f, df, guess, { tol: 1e-9, maxIter: 100 });
|
|
119
|
+
if (n.converged && isFinite(n.root) && n.root > -1) return n.root;
|
|
120
|
+
const b = bisect(f, -0.999999, 1e6, { tol: 1e-9, maxIter: 300 });
|
|
121
|
+
return b.converged ? b.root : NaN;
|
|
122
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Depreciation schedules: straight-line,
|
|
4
|
+
* declining-balance (double-declining when factor = 2) and
|
|
5
|
+
* sum-of-years-digits. Each returns an array of per-period depreciation
|
|
6
|
+
* amounts of length `life`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Straight-line depreciation: equal charge each period.
|
|
11
|
+
* @param {number} cost @param {number} salvage @param {number} life periods
|
|
12
|
+
* @returns {number[]}
|
|
13
|
+
*/
|
|
14
|
+
export function straightLine(cost, salvage, life) {
|
|
15
|
+
const per = (cost - salvage) / life;
|
|
16
|
+
return Array.from({ length: life }, () => per);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Declining-balance depreciation. `factor` = 2 is double-declining. Never
|
|
21
|
+
* depreciates below salvage; the final periods absorb the remainder.
|
|
22
|
+
* @param {number} cost @param {number} salvage @param {number} life
|
|
23
|
+
* @param {number} [factor]
|
|
24
|
+
* @returns {number[]}
|
|
25
|
+
*/
|
|
26
|
+
export function decliningBalance(cost, salvage, life, factor = 2) {
|
|
27
|
+
const rate = factor / life;
|
|
28
|
+
const out = [];
|
|
29
|
+
let book = cost;
|
|
30
|
+
for (let i = 0; i < life; i++) {
|
|
31
|
+
let dep = book * rate;
|
|
32
|
+
if (book - dep < salvage) dep = book - salvage;
|
|
33
|
+
if (dep < 0) dep = 0;
|
|
34
|
+
out.push(dep);
|
|
35
|
+
book -= dep;
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Sum-of-years-digits depreciation.
|
|
42
|
+
* @param {number} cost @param {number} salvage @param {number} life
|
|
43
|
+
* @returns {number[]}
|
|
44
|
+
*/
|
|
45
|
+
export function sumOfYearsDigits(cost, salvage, life) {
|
|
46
|
+
const syd = (life * (life + 1)) / 2;
|
|
47
|
+
const base = cost - salvage;
|
|
48
|
+
const out = [];
|
|
49
|
+
for (let i = 0; i < life; i++) {
|
|
50
|
+
const remaining = life - i;
|
|
51
|
+
out.push(base * remaining / syd);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file `@jarenjs/core/finance` — the finance & trading formula library.
|
|
4
|
+
* Pure, zero runtime dependencies (imports only `@jarenjs/core/math`).
|
|
5
|
+
* Every export is a plain function over numbers/number-arrays, reusable
|
|
6
|
+
* by any package: a calculator, a trading dashboard or a backtester. The
|
|
7
|
+
* math lives here; orchestration (which variable to solve, forms, display)
|
|
8
|
+
* lives in the consumer.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { pmt, pv, fv, nper, rate } from './tvm.js';
|
|
12
|
+
export { npv, irr, mirr, xnpv, xirr } from './cashflow.js';
|
|
13
|
+
export { amortizationSchedule } from './amortization.js';
|
|
14
|
+
export {
|
|
15
|
+
simpleInterest, compoundAmount, compoundInterest,
|
|
16
|
+
nominalToEffective, effectiveToNominal, continuousCompound,
|
|
17
|
+
} from './interest.js';
|
|
18
|
+
export { straightLine, decliningBalance, sumOfYearsDigits } from './depreciation.js';
|
|
19
|
+
export {
|
|
20
|
+
bondPrice, bondYTM, macaulayDuration, modifiedDuration, convexity,
|
|
21
|
+
} from './bond.js';
|
|
22
|
+
export {
|
|
23
|
+
sma, ema, wma, macd, rsi, bollinger, stochastic, atr, roc,
|
|
24
|
+
} from './indicators.js';
|
|
25
|
+
export {
|
|
26
|
+
cagr, holdingPeriodReturn, returnsOf, volatility, sharpe, maxDrawdown,
|
|
27
|
+
} from './returns.js';
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Technical / trading indicators over a price or OHLC
|
|
4
|
+
* series. Every function returns an array **aligned** to the input (same
|
|
5
|
+
* length), with `null` in warm-up positions where the indicator is not
|
|
6
|
+
* yet defined. Single-pass where the math allows; `Float64Array` inputs
|
|
7
|
+
* are accepted (array-like reads only).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Simple moving average.
|
|
12
|
+
* @param {ArrayLike<number>} values @param {number} period
|
|
13
|
+
* @returns {Array<number|null>}
|
|
14
|
+
*/
|
|
15
|
+
export function sma(values, period) {
|
|
16
|
+
const n = values.length;
|
|
17
|
+
const out = new Array(n).fill(null);
|
|
18
|
+
let sum = 0;
|
|
19
|
+
for (let i = 0; i < n; i++) {
|
|
20
|
+
sum += values[i];
|
|
21
|
+
if (i >= period) sum -= values[i - period];
|
|
22
|
+
if (i >= period - 1) out[i] = sum / period;
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Exponential moving average (SMA-seeded).
|
|
29
|
+
* @param {ArrayLike<number>} values @param {number} period
|
|
30
|
+
* @returns {Array<number|null>}
|
|
31
|
+
*/
|
|
32
|
+
export function ema(values, period) {
|
|
33
|
+
const n = values.length;
|
|
34
|
+
const out = new Array(n).fill(null);
|
|
35
|
+
if (n < period) return out;
|
|
36
|
+
const k = 2 / (period + 1);
|
|
37
|
+
let seed = 0;
|
|
38
|
+
for (let i = 0; i < period; i++) seed += values[i];
|
|
39
|
+
let prev = seed / period;
|
|
40
|
+
out[period - 1] = prev;
|
|
41
|
+
for (let i = period; i < n; i++) {
|
|
42
|
+
prev = values[i] * k + prev * (1 - k);
|
|
43
|
+
out[i] = prev;
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Weighted moving average (linear weights 1..period).
|
|
50
|
+
* @param {ArrayLike<number>} values @param {number} period
|
|
51
|
+
* @returns {Array<number|null>}
|
|
52
|
+
*/
|
|
53
|
+
export function wma(values, period) {
|
|
54
|
+
const n = values.length;
|
|
55
|
+
const out = new Array(n).fill(null);
|
|
56
|
+
const denom = (period * (period + 1)) / 2;
|
|
57
|
+
for (let i = period - 1; i < n; i++) {
|
|
58
|
+
let acc = 0;
|
|
59
|
+
for (let j = 0; j < period; j++) acc += values[i - period + 1 + j] * (j + 1);
|
|
60
|
+
out[i] = acc / denom;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* MACD: line = EMA(fast) − EMA(slow), signal = EMA(line, signalPeriod),
|
|
67
|
+
* histogram = line − signal. Each is an aligned array.
|
|
68
|
+
* @param {ArrayLike<number>} values
|
|
69
|
+
* @param {number} [fast] @param {number} [slow] @param {number} [signalPeriod]
|
|
70
|
+
* @returns {{ macd: Array<number|null>, signal: Array<number|null>, histogram: Array<number|null> }}
|
|
71
|
+
*/
|
|
72
|
+
export function macd(values, fast = 12, slow = 26, signalPeriod = 9) {
|
|
73
|
+
const ef = ema(values, fast);
|
|
74
|
+
const es = ema(values, slow);
|
|
75
|
+
const line = ef.map((v, i) => (v !== null && es[i] !== null ? v - es[i] : null));
|
|
76
|
+
// signal EMA runs over the defined tail of the MACD line
|
|
77
|
+
const defined = line.filter((v) => v !== null);
|
|
78
|
+
const startIdx = line.findIndex((v) => v !== null);
|
|
79
|
+
const sig = ema(defined, signalPeriod);
|
|
80
|
+
const signal = new Array(values.length).fill(null);
|
|
81
|
+
for (let i = 0; i < sig.length; i++) {
|
|
82
|
+
if (sig[i] !== null) signal[startIdx + i] = sig[i];
|
|
83
|
+
}
|
|
84
|
+
const histogram = line.map((v, i) => (v !== null && signal[i] !== null ? v - signal[i] : null));
|
|
85
|
+
return { macd: line, signal, histogram };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Relative Strength Index (Wilder's smoothing).
|
|
90
|
+
* @param {ArrayLike<number>} values @param {number} [period]
|
|
91
|
+
* @returns {Array<number|null>}
|
|
92
|
+
*/
|
|
93
|
+
export function rsi(values, period = 14) {
|
|
94
|
+
const n = values.length;
|
|
95
|
+
const out = new Array(n).fill(null);
|
|
96
|
+
if (n <= period) return out;
|
|
97
|
+
let gain = 0;
|
|
98
|
+
let loss = 0;
|
|
99
|
+
for (let i = 1; i <= period; i++) {
|
|
100
|
+
const ch = values[i] - values[i - 1];
|
|
101
|
+
if (ch >= 0) gain += ch; else loss -= ch;
|
|
102
|
+
}
|
|
103
|
+
let avgGain = gain / period;
|
|
104
|
+
let avgLoss = loss / period;
|
|
105
|
+
out[period] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss);
|
|
106
|
+
for (let i = period + 1; i < n; i++) {
|
|
107
|
+
const ch = values[i] - values[i - 1];
|
|
108
|
+
const g = ch >= 0 ? ch : 0;
|
|
109
|
+
const l = ch < 0 ? -ch : 0;
|
|
110
|
+
avgGain = (avgGain * (period - 1) + g) / period;
|
|
111
|
+
avgLoss = (avgLoss * (period - 1) + l) / period;
|
|
112
|
+
out[i] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss);
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Bollinger Bands (population standard deviation).
|
|
119
|
+
* @param {ArrayLike<number>} values @param {number} [period] @param {number} [k]
|
|
120
|
+
* @returns {{ middle: Array<number|null>, upper: Array<number|null>, lower: Array<number|null> }}
|
|
121
|
+
*/
|
|
122
|
+
export function bollinger(values, period = 20, k = 2) {
|
|
123
|
+
const n = values.length;
|
|
124
|
+
const middle = sma(values, period);
|
|
125
|
+
const upper = new Array(n).fill(null);
|
|
126
|
+
const lower = new Array(n).fill(null);
|
|
127
|
+
for (let i = period - 1; i < n; i++) {
|
|
128
|
+
const mean = middle[i];
|
|
129
|
+
let variance = 0;
|
|
130
|
+
for (let j = i - period + 1; j <= i; j++) variance += (values[j] - mean) ** 2;
|
|
131
|
+
const sd = Math.sqrt(variance / period);
|
|
132
|
+
upper[i] = mean + k * sd;
|
|
133
|
+
lower[i] = mean - k * sd;
|
|
134
|
+
}
|
|
135
|
+
return { middle, upper, lower };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Stochastic oscillator (%K and its %D moving average).
|
|
140
|
+
* @param {ArrayLike<number>} high @param {ArrayLike<number>} low @param {ArrayLike<number>} close
|
|
141
|
+
* @param {number} [kPeriod] @param {number} [dPeriod]
|
|
142
|
+
* @returns {{ k: Array<number|null>, d: Array<number|null> }}
|
|
143
|
+
*/
|
|
144
|
+
export function stochastic(high, low, close, kPeriod = 14, dPeriod = 3) {
|
|
145
|
+
const n = close.length;
|
|
146
|
+
const k = new Array(n).fill(null);
|
|
147
|
+
for (let i = kPeriod - 1; i < n; i++) {
|
|
148
|
+
let hh = -Infinity;
|
|
149
|
+
let ll = Infinity;
|
|
150
|
+
for (let j = i - kPeriod + 1; j <= i; j++) {
|
|
151
|
+
if (high[j] > hh) hh = high[j];
|
|
152
|
+
if (low[j] < ll) ll = low[j];
|
|
153
|
+
}
|
|
154
|
+
k[i] = hh === ll ? 100 : (100 * (close[i] - ll)) / (hh - ll);
|
|
155
|
+
}
|
|
156
|
+
const kDefined = k.filter((v) => v !== null);
|
|
157
|
+
const start = k.findIndex((v) => v !== null);
|
|
158
|
+
const dTail = sma(kDefined, dPeriod);
|
|
159
|
+
const d = new Array(n).fill(null);
|
|
160
|
+
for (let i = 0; i < dTail.length; i++) if (dTail[i] !== null) d[start + i] = dTail[i];
|
|
161
|
+
return { k, d };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Average True Range (Wilder's smoothing).
|
|
166
|
+
* @param {ArrayLike<number>} high @param {ArrayLike<number>} low @param {ArrayLike<number>} close
|
|
167
|
+
* @param {number} [period]
|
|
168
|
+
* @returns {Array<number|null>}
|
|
169
|
+
*/
|
|
170
|
+
export function atr(high, low, close, period = 14) {
|
|
171
|
+
const n = close.length;
|
|
172
|
+
const out = new Array(n).fill(null);
|
|
173
|
+
if (n <= period) return out;
|
|
174
|
+
const tr = new Array(n);
|
|
175
|
+
tr[0] = high[0] - low[0];
|
|
176
|
+
for (let i = 1; i < n; i++) {
|
|
177
|
+
tr[i] = Math.max(
|
|
178
|
+
high[i] - low[i],
|
|
179
|
+
Math.abs(high[i] - close[i - 1]),
|
|
180
|
+
Math.abs(low[i] - close[i - 1]),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
let acc = 0;
|
|
184
|
+
for (let i = 1; i <= period; i++) acc += tr[i];
|
|
185
|
+
let prev = acc / period;
|
|
186
|
+
out[period] = prev;
|
|
187
|
+
for (let i = period + 1; i < n; i++) {
|
|
188
|
+
prev = (prev * (period - 1) + tr[i]) / period;
|
|
189
|
+
out[i] = prev;
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Rate of change (percent) over `period`.
|
|
196
|
+
* @param {ArrayLike<number>} values @param {number} [period]
|
|
197
|
+
* @returns {Array<number|null>}
|
|
198
|
+
*/
|
|
199
|
+
export function roc(values, period = 12) {
|
|
200
|
+
const n = values.length;
|
|
201
|
+
const out = new Array(n).fill(null);
|
|
202
|
+
for (let i = period; i < n; i++) {
|
|
203
|
+
const base = values[i - period];
|
|
204
|
+
out[i] = base === 0 ? null : (100 * (values[i] - base)) / base;
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|