@jarenjs/core 0.9.2 → 0.34.0

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.
Files changed (121) hide show
  1. package/ARCHITECTURE.md +229 -26
  2. package/README.md +64 -10
  3. package/dist/types/array.d.ts +13 -0
  4. package/dist/types/cache.d.ts +166 -0
  5. package/dist/types/chunk.d.ts +102 -0
  6. package/dist/types/color.d.ts +64 -0
  7. package/dist/types/convert/convert.d.ts +33 -0
  8. package/dist/types/convert/currency.d.ts +47 -0
  9. package/dist/types/convert/index.d.ts +9 -0
  10. package/dist/types/convert/registry.d.ts +55 -0
  11. package/dist/types/dates/civil.d.ts +146 -0
  12. package/dist/types/dates/duration.d.ts +83 -0
  13. package/dist/types/dates/format.d.ts +59 -0
  14. package/dist/types/dates/index.d.ts +4 -0
  15. package/dist/types/{dates.d.ts → dates/rfc3339.d.ts} +57 -3
  16. package/dist/types/errors.d.ts +77 -0
  17. package/dist/types/finance/amortization.d.ts +51 -0
  18. package/dist/types/finance/bond.d.ts +45 -0
  19. package/dist/types/finance/cashflow.d.ts +44 -0
  20. package/dist/types/finance/depreciation.d.ts +26 -0
  21. package/dist/types/finance/index.d.ts +16 -0
  22. package/dist/types/finance/indicators.d.ts +76 -0
  23. package/dist/types/finance/interest.d.ts +44 -0
  24. package/dist/types/finance/returns.d.ts +43 -0
  25. package/dist/types/finance/tvm.d.ts +50 -0
  26. package/dist/types/geo/angle.d.ts +4 -0
  27. package/dist/types/geo/bbox.d.ts +32 -0
  28. package/dist/types/geo/distance.d.ts +67 -0
  29. package/dist/types/geo/geohash.d.ts +63 -0
  30. package/dist/types/geo/geojson.d.ts +108 -0
  31. package/dist/types/geo/index-tree.d.ts +52 -0
  32. package/dist/types/geo/index.d.ts +11 -0
  33. package/dist/types/geo/mercator.d.ts +57 -0
  34. package/dist/types/geo/predicates.d.ts +44 -0
  35. package/dist/types/geo/ring.d.ts +80 -0
  36. package/dist/types/geo/simplify.d.ts +31 -0
  37. package/dist/types/geo/valid.d.ts +24 -0
  38. package/dist/types/geo/wkt.d.ts +16 -0
  39. package/dist/types/math/float64.d.ts +126 -3
  40. package/dist/types/math/format.d.ts +58 -0
  41. package/dist/types/math/index.d.ts +5 -0
  42. package/dist/types/math/mat4.d.ts +60 -0
  43. package/dist/types/math/project.d.ts +54 -0
  44. package/dist/types/math/solve.d.ts +67 -0
  45. package/dist/types/math/word.d.ts +82 -0
  46. package/dist/types/message.d.ts +52 -0
  47. package/dist/types/object.d.ts +151 -0
  48. package/dist/types/scan.d.ts +30 -0
  49. package/dist/types/schema.d.ts +24 -0
  50. package/dist/types/string.d.ts +96 -0
  51. package/dist/types/text/base64.d.ts +6 -4
  52. package/dist/types/text/email.d.ts +0 -1
  53. package/dist/types/text/host.d.ts +39 -8
  54. package/dist/types/text/i18n.d.ts +13 -1
  55. package/dist/types/text/index.d.ts +1 -0
  56. package/dist/types/text/misc.d.ts +15 -1
  57. package/dist/types/text/punycode.d.ts +45 -85
  58. package/docs/CONVERT.md +45 -0
  59. package/docs/DATES.md +78 -0
  60. package/docs/FINANCE.md +59 -0
  61. package/docs/GEO.md +114 -0
  62. package/docs/MATH.md +75 -0
  63. package/package.json +57 -4
  64. package/src/array.js +16 -0
  65. package/src/cache.js +206 -0
  66. package/src/chunk.js +159 -0
  67. package/src/color.js +125 -0
  68. package/src/convert/convert.js +59 -0
  69. package/src/convert/currency.js +74 -0
  70. package/src/convert/index.js +11 -0
  71. package/src/convert/registry.js +213 -0
  72. package/src/dates/civil.js +360 -0
  73. package/src/dates/duration.js +225 -0
  74. package/src/dates/format.js +238 -0
  75. package/src/dates/index.js +30 -0
  76. package/src/dates/rfc3339.js +621 -0
  77. package/src/errors.js +102 -0
  78. package/src/finance/amortization.js +50 -0
  79. package/src/finance/bond.js +100 -0
  80. package/src/finance/cashflow.js +122 -0
  81. package/src/finance/depreciation.js +54 -0
  82. package/src/finance/index.js +27 -0
  83. package/src/finance/indicators.js +207 -0
  84. package/src/finance/interest.js +63 -0
  85. package/src/finance/returns.js +90 -0
  86. package/src/finance/tvm.js +94 -0
  87. package/src/function.js +0 -2
  88. package/src/geo/angle.js +12 -0
  89. package/src/geo/bbox.js +80 -0
  90. package/src/geo/distance.js +155 -0
  91. package/src/geo/geohash.js +224 -0
  92. package/src/geo/geojson.js +361 -0
  93. package/src/geo/index-tree.js +294 -0
  94. package/src/geo/index.js +52 -0
  95. package/src/geo/mercator.js +124 -0
  96. package/src/geo/predicates.js +313 -0
  97. package/src/geo/ring.js +182 -0
  98. package/src/geo/simplify.js +130 -0
  99. package/src/geo/valid.js +142 -0
  100. package/src/geo/wkt.js +262 -0
  101. package/src/math/float64.js +228 -11
  102. package/src/math/format.js +157 -0
  103. package/src/math/index.js +5 -0
  104. package/src/math/mat4.js +131 -0
  105. package/src/math/project.js +49 -0
  106. package/src/math/solve.js +112 -0
  107. package/src/math/word.js +177 -0
  108. package/src/message.js +119 -0
  109. package/src/object.js +329 -0
  110. package/src/scan.js +42 -0
  111. package/src/schema.js +37 -0
  112. package/src/string.js +189 -0
  113. package/src/text/base64.js +15 -52
  114. package/src/text/email.js +0 -5
  115. package/src/text/host.js +515 -140
  116. package/src/text/i18n.js +13 -6
  117. package/src/text/identifiers.js +1 -1
  118. package/src/text/index.js +1 -0
  119. package/src/text/misc.js +81 -5
  120. package/src/text/punycode.js +235 -323
  121. 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
+ }