@holoscript/plugin-banking-finance 2.0.1 → 2.0.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/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@holoscript/plugin-banking-finance",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "main": "src/index.ts",
5
5
  "peerDependencies": {
6
- "@holoscript/core": "8.0.6"
6
+ "@holoscript/core": ">=8.0.0"
7
7
  },
8
8
  "license": "MIT",
9
9
  "scripts": {
10
10
  "test": "vitest run --passWithNoTests",
11
11
  "test:coverage": "vitest run --coverage --passWithNoTests"
12
12
  }
13
- }
13
+ }
@@ -26,9 +26,9 @@ describe('analyzeBond', () => {
26
26
  * When coupon rate = yield, price = par: $1000.
27
27
  */
28
28
  const parBond: BondSpec = {
29
- faceValue: 1_000,
30
- couponRate: 0.05,
31
- periods: 20, // 10 years × 2
29
+ faceValue: 1_000,
30
+ couponRate: 0.05,
31
+ periods: 20, // 10 years × 2
32
32
  periodsPerYear: 2,
33
33
  };
34
34
 
@@ -62,8 +62,8 @@ describe('analyzeBond', () => {
62
62
 
63
63
  it('price is inverse function of yield (higher yield → lower price)', () => {
64
64
  const bond: BondSpec = { faceValue: 1000, couponRate: 0.05, periods: 20, periodsPerYear: 2 };
65
- const r4 = analyzeBond(bond, 0.04);
66
- const r6 = analyzeBond(bond, 0.06);
65
+ const r4 = analyzeBond(bond, 0.04);
66
+ const r6 = analyzeBond(bond, 0.06);
67
67
  expect(r4.price).toBeGreaterThan(r6.price);
68
68
  });
69
69
 
@@ -105,7 +105,11 @@ describe('analyzeBond', () => {
105
105
 
106
106
  it('YTM > coupon rate for discount bond', () => {
107
107
  const bond: BondSpec = {
108
- faceValue: 1000, couponRate: 0.04, periods: 20, periodsPerYear: 2, marketPrice: 922.78,
108
+ faceValue: 1000,
109
+ couponRate: 0.04,
110
+ periods: 20,
111
+ periodsPerYear: 2,
112
+ marketPrice: 922.78,
109
113
  };
110
114
  const r = analyzeBond(bond, 0.05);
111
115
  expect(r.ytm).not.toBeNull();
@@ -193,15 +197,13 @@ describe('analyzePortfolio', () => {
193
197
  it('throws for mismatched return series lengths', () => {
194
198
  const bad: PortfolioHolding[] = [
195
199
  { id: 'A', returns: [0.01, 0.02, 0.03], weight: 0.5 },
196
- { id: 'B', returns: [0.01, 0.02], weight: 0.5 },
200
+ { id: 'B', returns: [0.01, 0.02], weight: 0.5 },
197
201
  ];
198
202
  expect(() => analyzePortfolio(bad)).toThrow();
199
203
  });
200
204
 
201
205
  it('throws for fewer than 2 observations', () => {
202
- expect(() =>
203
- analyzePortfolio([{ id: 'A', returns: [0.01], weight: 1.0 }]),
204
- ).toThrow();
206
+ expect(() => analyzePortfolio([{ id: 'A', returns: [0.01], weight: 1.0 }])).toThrow();
205
207
  });
206
208
  });
207
209
 
@@ -213,7 +215,7 @@ describe('blackScholes', () => {
213
215
  * S=40, K=40 (ATM), T=0.5yr, r=0.10, σ=0.40
214
216
  * call ≈ $4.76, put ≈ $2.82 (from original paper, approximate)
215
217
  */
216
- const atm = () => blackScholes(40, 40, 0.5, 0.10, 0.40);
218
+ const atm = () => blackScholes(40, 40, 0.5, 0.1, 0.4);
217
219
 
218
220
  it('ATM call price is positive and reasonable (0–10 for S=40)', () => {
219
221
  const r = atm();
@@ -229,8 +231,11 @@ describe('blackScholes', () => {
229
231
  * Put-call parity: C − P = S − K × e^(−rT)
230
232
  */
231
233
  it('put-call parity holds', () => {
232
- const r = atm();
233
- const S = 40, K = 40, T = 0.5, rr = 0.10;
234
+ const r = atm();
235
+ const S = 40,
236
+ K = 40,
237
+ T = 0.5,
238
+ rr = 0.1;
234
239
  const parity = S - K * Math.exp(-rr * T);
235
240
  expect(r.callPrice - r.putPrice).toBeCloseTo(parity, 3);
236
241
  });
@@ -259,25 +264,25 @@ describe('blackScholes', () => {
259
264
  });
260
265
 
261
266
  it('deep ITM call price approaches intrinsic value (S - K × e^{-rT})', () => {
262
- const r = blackScholes(100, 50, 0.5, 0.05, 0.10); // deep ITM
267
+ const r = blackScholes(100, 50, 0.5, 0.05, 0.1); // deep ITM
263
268
  const intrinsic = 100 - 50 * Math.exp(-0.05 * 0.5);
264
269
  expect(r.callPrice).toBeCloseTo(intrinsic, 0);
265
270
  });
266
271
 
267
272
  it('deep OTM call price approaches 0', () => {
268
- const r = blackScholes(40, 200, 0.5, 0.05, 0.20); // deep OTM
273
+ const r = blackScholes(40, 200, 0.5, 0.05, 0.2); // deep OTM
269
274
  expect(r.callPrice).toBeCloseTo(0, 3);
270
275
  });
271
276
 
272
277
  it('higher volatility increases both call and put price', () => {
273
- const lo = blackScholes(40, 40, 0.5, 0.05, 0.20);
274
- const hi = blackScholes(40, 40, 0.5, 0.05, 0.40);
278
+ const lo = blackScholes(40, 40, 0.5, 0.05, 0.2);
279
+ const hi = blackScholes(40, 40, 0.5, 0.05, 0.4);
275
280
  expect(hi.callPrice).toBeGreaterThan(lo.callPrice);
276
281
  expect(hi.putPrice).toBeGreaterThan(lo.putPrice);
277
282
  });
278
283
 
279
284
  it('throws for zero spot price', () => {
280
- expect(() => blackScholes(0, 40, 0.5, 0.05, 0.20)).toThrow();
285
+ expect(() => blackScholes(0, 40, 0.5, 0.05, 0.2)).toThrow();
281
286
  });
282
287
 
283
288
  it('throws for zero volatility', () => {
@@ -290,13 +295,24 @@ describe('blackScholes', () => {
290
295
  describe('analyzeFinance', () => {
291
296
  it('computes all three analyses when all inputs provided', () => {
292
297
  const holdings: PortfolioHolding[] = [
293
- { id: 'A', returns: Array.from({length:24},(_,i)=>0.005+0.01*Math.cos(i)), weight: 0.6 },
294
- { id: 'B', returns: Array.from({length:24},(_,i)=>0.006+0.01*Math.sin(i)), weight: 0.4 },
298
+ {
299
+ id: 'A',
300
+ returns: Array.from({ length: 24 }, (_, i) => 0.005 + 0.01 * Math.cos(i)),
301
+ weight: 0.6,
302
+ },
303
+ {
304
+ id: 'B',
305
+ returns: Array.from({ length: 24 }, (_, i) => 0.006 + 0.01 * Math.sin(i)),
306
+ weight: 0.4,
307
+ },
295
308
  ];
296
309
  const r = analyzeFinance({
297
- bond: { spec: { faceValue: 1000, couponRate: 0.05, periods: 20, periodsPerYear: 2 }, annualYield: 0.05 },
310
+ bond: {
311
+ spec: { faceValue: 1000, couponRate: 0.05, periods: 20, periodsPerYear: 2 },
312
+ annualYield: 0.05,
313
+ },
298
314
  portfolio: { holdings },
299
- options: { S: 40, K: 40, T: 0.5, r: 0.05, sigma: 0.30 },
315
+ options: { S: 40, K: 40, T: 0.5, r: 0.05, sigma: 0.3 },
300
316
  });
301
317
  expect(r.bond).toBeDefined();
302
318
  expect(r.portfolio).toBeDefined();
@@ -309,7 +325,10 @@ describe('analyzeFinance', () => {
309
325
 
310
326
  describe('buildFinanceReceipt', () => {
311
327
  const bondResult = analyzeFinance({
312
- bond: { spec: { faceValue: 1000, couponRate: 0.05, periods: 20, periodsPerYear: 2 }, annualYield: 0.05 },
328
+ bond: {
329
+ spec: { faceValue: 1000, couponRate: 0.05, periods: 20, periodsPerYear: 2 },
330
+ annualYield: 0.05,
331
+ },
313
332
  });
314
333
 
315
334
  it('produces receipt with plugin=banking-finance and CAEL event', () => {
@@ -15,95 +15,92 @@
15
15
  * Black & Scholes (1973) "The Pricing of Options..." J. Political Economy.
16
16
  */
17
17
 
18
- import {
19
- DOMAIN_SIMULATION_RECEIPT_SCHEMA,
20
- buildDomainSimulationReceipt,
21
- } from '@holoscript/core';
18
+ import { DOMAIN_SIMULATION_RECEIPT_SCHEMA, buildDomainSimulationReceipt } from '@holoscript/core';
22
19
 
23
20
  // ─── Types ────────────────────────────────────────────────────────────────────
24
21
 
25
22
  export interface BondSpec {
26
23
  /** Face (par) value */
27
- faceValue: number;
24
+ faceValue: number;
28
25
  /** Annual coupon rate (e.g. 0.05 = 5%) */
29
- couponRate: number;
26
+ couponRate: number;
30
27
  /** Periods to maturity (coupon payments; typically semi-annual = years × 2) */
31
- periods: number;
28
+ periods: number;
32
29
  /** Periods per year (2 = semi-annual, 1 = annual) */
33
30
  periodsPerYear: number;
34
31
  /** Market price (for YTM calculation; optional) */
35
- marketPrice?: number;
32
+ marketPrice?: number;
36
33
  }
37
34
 
38
35
  export interface BondResult {
39
36
  /** Clean price (sum of discounted cash flows) at given yield */
40
- price: number;
37
+ price: number;
41
38
  /** Yield to maturity (annual, bond-equivalent) */
42
- ytm: number | null;
39
+ ytm: number | null;
43
40
  /** Macaulay duration (years) */
44
- macaulayDuration: number;
41
+ macaulayDuration: number;
45
42
  /** Modified duration (% price change per 1% yield change) */
46
- modifiedDuration: number;
43
+ modifiedDuration: number;
47
44
  /** Dollar duration (DV01): price change for 1 bp yield change */
48
- dv01: number;
45
+ dv01: number;
49
46
  /** Convexity */
50
- convexity: number;
47
+ convexity: number;
51
48
  }
52
49
 
53
50
  export interface PortfolioHolding {
54
- id: string;
51
+ id: string;
55
52
  /** Historical returns (one per period) */
56
- returns: number[];
53
+ returns: number[];
57
54
  /** Portfolio weight (0–1, must sum to 1 across holdings) */
58
- weight: number;
55
+ weight: number;
59
56
  }
60
57
 
61
58
  export interface PortfolioResult {
62
- expectedReturn: number;
63
- variance: number;
64
- stdDev: number;
65
- sharpeRatio: number | null; // null if riskFreeRate not provided
66
- beta: number | null; // null if benchmark returns not provided
59
+ expectedReturn: number;
60
+ variance: number;
61
+ stdDev: number;
62
+ sharpeRatio: number | null; // null if riskFreeRate not provided
63
+ beta: number | null; // null if benchmark returns not provided
67
64
  /** Historical VaR at 95% confidence */
68
- var95: number;
65
+ var95: number;
69
66
  /** Historical CVaR (expected shortfall) at 95% */
70
- cvar95: number;
67
+ cvar95: number;
71
68
  }
72
69
 
73
70
  export interface BlackScholesResult {
74
71
  callPrice: number;
75
- putPrice: number;
72
+ putPrice: number;
76
73
  /** Greeks */
77
74
  callDelta: number;
78
- putDelta: number;
79
- gamma: number;
80
- vega: number;
75
+ putDelta: number;
76
+ gamma: number;
77
+ vega: number;
81
78
  callTheta: number;
82
- putTheta: number;
83
- callRho: number;
84
- putRho: number;
79
+ putTheta: number;
80
+ callRho: number;
81
+ putRho: number;
85
82
  }
86
83
 
87
84
  export interface FinanceAnalysisResult {
88
- bond?: BondResult;
85
+ bond?: BondResult;
89
86
  portfolio?: PortfolioResult;
90
- options?: BlackScholesResult;
91
- converged: boolean;
87
+ options?: BlackScholesResult;
88
+ converged: boolean;
92
89
  }
93
90
 
94
91
  export interface FinanceReceipt {
95
- plugin: string;
96
- runId: string;
97
- payloadHash: string;
92
+ plugin: string;
93
+ runId: string;
94
+ payloadHash: string;
98
95
  hashAlgorithm: string;
99
- cael: { event: string; solverType: string; version: string };
100
- acceptance: { accepted: boolean; violations: Array<{ criterion: string; message: string }> };
96
+ cael: { event: string; solverType: string; version: string };
97
+ acceptance: { accepted: boolean; violations: Array<{ criterion: string; message: string }> };
101
98
  resultSummary: {
102
- bondYTM?: number;
103
- bondDuration?: number;
104
- portfolioReturn?: number;
105
- portfolioSharpe?: number;
106
- optionCallPrice?: number;
99
+ bondYTM?: number;
100
+ bondDuration?: number;
101
+ portfolioReturn?: number;
102
+ portfolioSharpe?: number;
103
+ optionCallPrice?: number;
107
104
  };
108
105
  }
109
106
 
@@ -112,12 +109,12 @@ export interface FinanceReceipt {
112
109
  /** Standard normal CDF via Abramowitz & Stegun (7-term; error < 3e-7) */
113
110
  function normalCDF(x: number): number {
114
111
  if (x < -8) return 0;
115
- if (x > 8) return 1;
112
+ if (x > 8) return 1;
116
113
  const k = 1 / (1 + 0.2316419 * Math.abs(x));
117
- const poly = ((((1.330274429 * k - 1.821255978) * k + 1.781477937) * k
118
- - 0.356563782) * k + 0.319381530) * k;
119
- const pdf = Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI);
120
- const cdf = 1 - pdf * poly;
114
+ const poly =
115
+ ((((1.330274429 * k - 1.821255978) * k + 1.781477937) * k - 0.356563782) * k + 0.31938153) * k;
116
+ const pdf = Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI);
117
+ const cdf = 1 - pdf * poly;
121
118
  return x >= 0 ? cdf : 1 - cdf;
122
119
  }
123
120
 
@@ -148,43 +145,50 @@ function bondPrice(spec: BondSpec, yieldPerPeriod: number): number {
148
145
  */
149
146
  export function analyzeBond(spec: BondSpec, annualYield: number): BondResult {
150
147
  const { faceValue, couponRate, periods, periodsPerYear } = spec;
151
- if (periods <= 0) throw new Error('[finance] periods must be > 0');
152
- if (faceValue <= 0) throw new Error('[finance] faceValue must be > 0');
153
- if (periodsPerYear <= 0) throw new Error('[finance] periodsPerYear must be > 0');
154
- if (annualYield < 0) throw new Error('[finance] annualYield must be ≥ 0');
148
+ if (periods <= 0) throw new Error('[finance] periods must be > 0');
149
+ if (faceValue <= 0) throw new Error('[finance] faceValue must be > 0');
150
+ if (periodsPerYear <= 0) throw new Error('[finance] periodsPerYear must be > 0');
151
+ if (annualYield < 0) throw new Error('[finance] annualYield must be ≥ 0');
155
152
 
156
- const r = annualYield / periodsPerYear; // yield per period
157
- const C = faceValue * (couponRate / periodsPerYear);
153
+ const r = annualYield / periodsPerYear; // yield per period
154
+ const C = faceValue * (couponRate / periodsPerYear);
158
155
  const price = bondPrice(spec, r);
159
156
 
160
157
  // Macaulay duration: Σ t × PV(CF_t) / price
161
158
  let durationWeightedSum = 0;
162
- let convexitySum = 0;
159
+ let convexitySum = 0;
163
160
  for (let t = 1; t <= periods; t++) {
164
- const cf = t < periods ? C : C + faceValue;
165
- const pv = cf / Math.pow(1 + r, t);
161
+ const cf = t < periods ? C : C + faceValue;
162
+ const pv = cf / Math.pow(1 + r, t);
166
163
  durationWeightedSum += (t / periodsPerYear) * pv;
167
- convexitySum += (t * (t + 1)) * pv / Math.pow(1 + r, 2);
164
+ convexitySum += (t * (t + 1) * pv) / Math.pow(1 + r, 2);
168
165
  }
169
- const macaulayDuration = durationWeightedSum / price;
170
- const modifiedDuration = macaulayDuration / (1 + r);
171
- const dv01 = modifiedDuration * price * 0.0001; // per 1 bp
172
- const convexity = convexitySum / (price * Math.pow(periodsPerYear, 2));
166
+ const macaulayDuration = durationWeightedSum / price;
167
+ const modifiedDuration = macaulayDuration / (1 + r);
168
+ const dv01 = modifiedDuration * price * 0.0001; // per 1 bp
169
+ const convexity = convexitySum / (price * Math.pow(periodsPerYear, 2));
173
170
 
174
171
  // YTM from market price (if provided)
175
172
  let ytm: number | null = null;
176
173
  if (spec.marketPrice !== undefined && spec.marketPrice > 0) {
177
174
  const mktPrice = spec.marketPrice;
178
175
  // Bisect on annual yield: find y s.t. bondPrice(y/periodsPerYear) = mktPrice
179
- let lo = 0, hi = 2.0;
180
- if (Math.sign(bondPrice(spec, lo / periodsPerYear) - mktPrice) !==
181
- Math.sign(bondPrice(spec, hi / periodsPerYear) - mktPrice)) {
176
+ let lo = 0,
177
+ hi = 2.0;
178
+ if (
179
+ Math.sign(bondPrice(spec, lo / periodsPerYear) - mktPrice) !==
180
+ Math.sign(bondPrice(spec, hi / periodsPerYear) - mktPrice)
181
+ ) {
182
182
  for (let i = 0; i < 100; i++) {
183
- const mid = (lo + hi) / 2;
183
+ const mid = (lo + hi) / 2;
184
184
  const delta = bondPrice(spec, mid / periodsPerYear) - mktPrice;
185
- if (Math.abs(delta) < 0.0001 || (hi - lo) < 1e-10) { ytm = mid; break; }
185
+ if (Math.abs(delta) < 0.0001 || hi - lo < 1e-10) {
186
+ ytm = mid;
187
+ break;
188
+ }
186
189
  Math.sign(delta) === Math.sign(bondPrice(spec, lo / periodsPerYear) - mktPrice)
187
- ? (lo = mid) : (hi = mid);
190
+ ? (lo = mid)
191
+ : (hi = mid);
188
192
  ytm = (lo + hi) / 2;
189
193
  }
190
194
  }
@@ -203,32 +207,34 @@ export function analyzeBond(spec: BondSpec, annualYield: number): BondResult {
203
207
  * @param benchmarkReturns Benchmark return series; optional — required for beta
204
208
  */
205
209
  export function analyzePortfolio(
206
- holdings: PortfolioHolding[],
207
- riskFreeRate?: number,
208
- benchmarkReturns?: number[],
210
+ holdings: PortfolioHolding[],
211
+ riskFreeRate?: number,
212
+ benchmarkReturns?: number[]
209
213
  ): PortfolioResult {
210
214
  if (holdings.length === 0) throw new Error('[finance] at least one holding required');
211
215
  const n = holdings[0].returns.length;
212
216
  if (n < 2) throw new Error('[finance] at least 2 return observations required');
213
- if (holdings.some((h) => h.returns.length !== n)) throw new Error('[finance] all return series must have same length');
217
+ if (holdings.some((h) => h.returns.length !== n))
218
+ throw new Error('[finance] all return series must have same length');
214
219
 
215
220
  const totalWeight = holdings.reduce((s, h) => s + h.weight, 0);
216
- if (Math.abs(totalWeight - 1) > 1e-6) throw new Error(`[finance] weights must sum to 1 (got ${totalWeight.toFixed(6)})`);
221
+ if (Math.abs(totalWeight - 1) > 1e-6)
222
+ throw new Error(`[finance] weights must sum to 1 (got ${totalWeight.toFixed(6)})`);
217
223
 
218
224
  // Portfolio returns (weighted sum each period)
219
225
  const portReturns = Array.from({ length: n }, (_, t) =>
220
- holdings.reduce((s, h) => s + h.weight * h.returns[t], 0),
226
+ holdings.reduce((s, h) => s + h.weight * h.returns[t], 0)
221
227
  );
222
228
 
223
229
  const expectedReturn = portReturns.reduce((s, r) => s + r, 0) / n;
224
- const variance = portReturns.reduce((s, r) => s + (r - expectedReturn) ** 2, 0) / (n - 1);
225
- const stdDev = Math.sqrt(variance);
230
+ const variance = portReturns.reduce((s, r) => s + (r - expectedReturn) ** 2, 0) / (n - 1);
231
+ const stdDev = Math.sqrt(variance);
226
232
 
227
233
  // Sharpe ratio (annualised using period count as proxy)
228
234
  let sharpeRatio: number | null = null;
229
235
  if (riskFreeRate !== undefined) {
230
- const periodsPerYear = n; // single-period approximation
231
- const excessReturn = expectedReturn - riskFreeRate / periodsPerYear;
236
+ const periodsPerYear = n; // single-period approximation
237
+ const excessReturn = expectedReturn - riskFreeRate / periodsPerYear;
232
238
  sharpeRatio = stdDev > 0 ? (excessReturn / stdDev) * Math.sqrt(n) : null;
233
239
  }
234
240
 
@@ -236,16 +242,21 @@ export function analyzePortfolio(
236
242
  let beta: number | null = null;
237
243
  if (benchmarkReturns && benchmarkReturns.length === n) {
238
244
  const benchMean = benchmarkReturns.reduce((s, r) => s + r, 0) / n;
239
- const cov = portReturns.reduce((s, r, i) => s + (r - expectedReturn) * (benchmarkReturns[i] - benchMean), 0) / (n - 1);
240
- const benchVar = benchmarkReturns.reduce((s, r) => s + (r - benchMean) ** 2, 0) / (n - 1);
245
+ const cov =
246
+ portReturns.reduce(
247
+ (s, r, i) => s + (r - expectedReturn) * (benchmarkReturns[i] - benchMean),
248
+ 0
249
+ ) /
250
+ (n - 1);
251
+ const benchVar = benchmarkReturns.reduce((s, r) => s + (r - benchMean) ** 2, 0) / (n - 1);
241
252
  beta = benchVar > 0 ? cov / benchVar : null;
242
253
  }
243
254
 
244
255
  // Historical VaR and CVaR at 95%
245
256
  const sorted = [...portReturns].sort((a, b) => a - b);
246
- const idx = Math.ceil(0.05 * n) - 1;
247
- const var95 = -sorted[Math.max(0, idx)]; // convert return to loss
248
- const tail = sorted.slice(0, idx + 1);
257
+ const idx = Math.ceil(0.05 * n) - 1;
258
+ const var95 = -sorted[Math.max(0, idx)]; // convert return to loss
259
+ const tail = sorted.slice(0, idx + 1);
249
260
  const cvar95 = -(tail.reduce((s, v) => s + v, 0) / tail.length);
250
261
 
251
262
  return { expectedReturn, variance, stdDev, sharpeRatio, beta, var95, cvar95 };
@@ -262,50 +273,70 @@ export function analyzePortfolio(
262
273
  * @param r Risk-free rate (annual, continuously compounded)
263
274
  * @param σ Volatility (annual)
264
275
  */
265
- export function blackScholes(S: number, K: number, T: number, r: number, sigma: number): BlackScholesResult {
266
- if (S <= 0) throw new Error('[finance] spot price must be > 0');
267
- if (K <= 0) throw new Error('[finance] strike must be > 0');
268
- if (T <= 0) throw new Error('[finance] time to expiry must be > 0');
276
+ export function blackScholes(
277
+ S: number,
278
+ K: number,
279
+ T: number,
280
+ r: number,
281
+ sigma: number
282
+ ): BlackScholesResult {
283
+ if (S <= 0) throw new Error('[finance] spot price must be > 0');
284
+ if (K <= 0) throw new Error('[finance] strike must be > 0');
285
+ if (T <= 0) throw new Error('[finance] time to expiry must be > 0');
269
286
  if (sigma <= 0) throw new Error('[finance] volatility must be > 0');
270
287
 
271
288
  const sqrtT = Math.sqrt(T);
272
- const d1 = (Math.log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * sqrtT);
273
- const d2 = d1 - sigma * sqrtT;
289
+ const d1 = (Math.log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * sqrtT);
290
+ const d2 = d1 - sigma * sqrtT;
274
291
 
275
- const Nd1 = normalCDF(d1);
276
- const Nd2 = normalCDF(d2);
277
- const Nnd1 = normalCDF(-d1);
278
- const Nnd2 = normalCDF(-d2);
279
- const nd1 = normalPDF(d1);
280
- const disc = Math.exp(-r * T);
292
+ const Nd1 = normalCDF(d1);
293
+ const Nd2 = normalCDF(d2);
294
+ const Nnd1 = normalCDF(-d1);
295
+ const Nnd2 = normalCDF(-d2);
296
+ const nd1 = normalPDF(d1);
297
+ const disc = Math.exp(-r * T);
281
298
 
282
- const callPrice = S * Nd1 - K * disc * Nd2;
283
- const putPrice = K * disc * Nnd2 - S * Nnd1;
299
+ const callPrice = S * Nd1 - K * disc * Nd2;
300
+ const putPrice = K * disc * Nnd2 - S * Nnd1;
284
301
 
285
302
  const callDelta = Nd1;
286
- const putDelta = Nd1 - 1;
287
- const gamma = nd1 / (S * sigma * sqrtT);
288
- const vega = S * nd1 * sqrtT / 100; // per 1% vol move
303
+ const putDelta = Nd1 - 1;
304
+ const gamma = nd1 / (S * sigma * sqrtT);
305
+ const vega = (S * nd1 * sqrtT) / 100; // per 1% vol move
289
306
  const callTheta = (-(S * nd1 * sigma) / (2 * sqrtT) - r * K * disc * Nd2) / 365;
290
- const putTheta = (-(S * nd1 * sigma) / (2 * sqrtT) + r * K * disc * Nnd2) / 365;
291
- const callRho = K * T * disc * Nd2 / 100;
292
- const putRho = -K * T * disc * Nnd2 / 100;
293
-
294
- return { callPrice, putPrice, callDelta, putDelta, gamma, vega, callTheta, putTheta, callRho, putRho };
307
+ const putTheta = (-(S * nd1 * sigma) / (2 * sqrtT) + r * K * disc * Nnd2) / 365;
308
+ const callRho = (K * T * disc * Nd2) / 100;
309
+ const putRho = (-K * T * disc * Nnd2) / 100;
310
+
311
+ return {
312
+ callPrice,
313
+ putPrice,
314
+ callDelta,
315
+ putDelta,
316
+ gamma,
317
+ vega,
318
+ callTheta,
319
+ putTheta,
320
+ callRho,
321
+ putRho,
322
+ };
295
323
  }
296
324
 
297
325
  // ─── Combined analysis ────────────────────────────────────────────────────────
298
326
 
299
327
  export function analyzeFinance(params: {
300
- bond?: { spec: BondSpec; annualYield: number };
328
+ bond?: { spec: BondSpec; annualYield: number };
301
329
  portfolio?: { holdings: PortfolioHolding[]; riskFreeRate?: number; benchmarkReturns?: number[] };
302
- options?: { S: number; K: number; T: number; r: number; sigma: number };
330
+ options?: { S: number; K: number; T: number; r: number; sigma: number };
303
331
  }): FinanceAnalysisResult {
304
332
  const result: FinanceAnalysisResult = { converged: true };
305
- if (params.bond) result.bond = analyzeBond(params.bond.spec, params.bond.annualYield);
306
- if (params.portfolio) result.portfolio = analyzePortfolio(
307
- params.portfolio.holdings, params.portfolio.riskFreeRate, params.portfolio.benchmarkReturns,
308
- );
333
+ if (params.bond) result.bond = analyzeBond(params.bond.spec, params.bond.annualYield);
334
+ if (params.portfolio)
335
+ result.portfolio = analyzePortfolio(
336
+ params.portfolio.holdings,
337
+ params.portfolio.riskFreeRate,
338
+ params.portfolio.benchmarkReturns
339
+ );
309
340
  if (params.options) {
310
341
  const { S, K, T, r, sigma } = params.options;
311
342
  result.options = blackScholes(S, K, T, r, sigma);
@@ -316,8 +347,8 @@ export function analyzeFinance(params: {
316
347
  // ─── Receipt ─────────────────────────────────────────────────────────────────
317
348
 
318
349
  export function buildFinanceReceipt(
319
- result: FinanceAnalysisResult,
320
- options?: { runId?: string },
350
+ result: FinanceAnalysisResult,
351
+ options?: { runId?: string }
321
352
  ): FinanceReceipt {
322
353
  const violations: Array<{ criterion: string; message: string }> = [];
323
354
  if (result.bond?.price !== undefined && result.bond.price <= 0)
@@ -325,7 +356,7 @@ export function buildFinanceReceipt(
325
356
 
326
357
  const summary: Record<string, number | null | undefined> = {};
327
358
  if (result.bond) {
328
- summary.bondYTM = result.bond.ytm;
359
+ summary.bondYTM = result.bond.ytm;
329
360
  summary.bondDuration = result.bond.modifiedDuration;
330
361
  }
331
362
  if (result.portfolio) {
@@ -337,22 +368,22 @@ export function buildFinanceReceipt(
337
368
  }
338
369
 
339
370
  const raw = buildDomainSimulationReceipt({
340
- plugin: 'banking-finance',
371
+ plugin: 'banking-finance',
341
372
  pluginVersion: '1.0.0',
342
- runId: options?.runId ?? `fin-${Date.now().toString(36)}`,
373
+ runId: options?.runId ?? `fin-${Date.now().toString(36)}`,
343
374
  solverConfig: {
344
375
  solverType: 'fixed-income-portfolio',
345
- scale: 'instrument',
346
- hasBond: result.bond !== undefined,
376
+ scale: 'instrument',
377
+ hasBond: result.bond !== undefined,
347
378
  hasPortfolio: result.portfolio !== undefined,
348
- hasOptions: result.options !== undefined,
379
+ hasOptions: result.options !== undefined,
349
380
  },
350
381
  resultSummary: Object.fromEntries(
351
- Object.entries(summary).filter(([, v]) => v !== undefined && v !== null),
382
+ Object.entries(summary).filter(([, v]) => v !== undefined && v !== null)
352
383
  ) as Record<string, number>,
353
384
  cael: {
354
- version: 'cael.v1',
355
- event: 'banking_finance.fixed_income',
385
+ version: 'cael.v1',
386
+ event: 'banking_finance.fixed_income',
356
387
  solverType: 'banking-finance.fixed-income-portfolio',
357
388
  },
358
389
  acceptance: { accepted: violations.length === 0, violations },
package/src/index.ts CHANGED
@@ -1,8 +1,21 @@
1
1
  export { createAccountHandler, type AccountConfig, type AccountType } from './traits/AccountTrait';
2
- export { createTransactionHandler, type TransactionConfig, type TransactionType, type TransactionStatus } from './traits/TransactionTrait';
2
+ export {
3
+ createTransactionHandler,
4
+ type TransactionConfig,
5
+ type TransactionType,
6
+ type TransactionStatus,
7
+ } from './traits/TransactionTrait';
3
8
  export { createKYCHandler, type KYCConfig, type KYCLevel, type KYCStatus } from './traits/KYCTrait';
4
- export { createPortfolioHandler, type PortfolioConfig, type Holding } from './traits/PortfolioTrait';
5
- export { createRiskModelHandler, type RiskModelConfig, type RiskCategory } from './traits/RiskModelTrait';
9
+ export {
10
+ createPortfolioHandler,
11
+ type PortfolioConfig,
12
+ type Holding,
13
+ } from './traits/PortfolioTrait';
14
+ export {
15
+ createRiskModelHandler,
16
+ type RiskModelConfig,
17
+ type RiskCategory,
18
+ } from './traits/RiskModelTrait';
6
19
  export * from './traits/types';
7
20
 
8
21
  import { createAccountHandler } from './traits/AccountTrait';
@@ -13,8 +26,18 @@ import { createRiskModelHandler } from './traits/RiskModelTrait';
13
26
 
14
27
  export * from './fixedincome';
15
28
 
16
- export const pluginMeta = { name: '@holoscript/plugin-banking-finance', version: '1.0.0', traits: ['account', 'transaction', 'kyc', 'portfolio', 'risk_model', 'fixed_income_solver'] };
17
- export const traitHandlers = [createAccountHandler(), createTransactionHandler(), createKYCHandler(), createPortfolioHandler(), createRiskModelHandler()];
29
+ export const pluginMeta = {
30
+ name: '@holoscript/plugin-banking-finance',
31
+ version: '1.0.0',
32
+ traits: ['account', 'transaction', 'kyc', 'portfolio', 'risk_model', 'fixed_income_solver'],
33
+ };
34
+ export const traitHandlers = [
35
+ createAccountHandler(),
36
+ createTransactionHandler(),
37
+ createKYCHandler(),
38
+ createPortfolioHandler(),
39
+ createRiskModelHandler(),
40
+ ];
18
41
 
19
42
  // Runtime integration — behavioral trait handler + registrar that wire the
20
43
  // deterministic bond-pricing solver into HoloScriptRuntime's dispatch. Closes
package/src/runtime.ts CHANGED
@@ -72,12 +72,16 @@ export interface TraitDispatchContext {
72
72
 
73
73
  export interface RuntimeTraitHandler {
74
74
  name: string;
75
- onAttach?: (node: unknown, config: FixedIncomeSolverTraitConfig, context: TraitDispatchContext) => void;
75
+ onAttach?: (
76
+ node: unknown,
77
+ config: FixedIncomeSolverTraitConfig,
78
+ context: TraitDispatchContext
79
+ ) => void;
76
80
  onUpdate?: (
77
81
  node: unknown,
78
82
  config: FixedIncomeSolverTraitConfig,
79
83
  context: TraitDispatchContext,
80
- delta: number,
84
+ delta: number
81
85
  ) => void;
82
86
  }
83
87
 
@@ -92,7 +96,7 @@ interface FixedIncomeSolverNode {
92
96
  function solveOntoNode(
93
97
  node: unknown,
94
98
  config: FixedIncomeSolverTraitConfig | undefined,
95
- context: TraitDispatchContext,
99
+ context: TraitDispatchContext
96
100
  ): void {
97
101
  const carrier = node as FixedIncomeSolverNode;
98
102
  const nodeId = carrier.id ?? carrier.name ?? 'unknown';
@@ -2,22 +2,78 @@
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
4
  export type AccountType = 'checking' | 'savings' | 'credit' | 'investment' | 'loan' | 'escrow';
5
- export interface AccountConfig { accountNumber: string; accountType: AccountType; currency: string; balance: number; interestRate: number; ownerKycId: string; }
6
- export interface AccountState { currentBalance: number; availableBalance: number; pendingTransactions: number; isFrozen: boolean; }
5
+ export interface AccountConfig {
6
+ accountNumber: string;
7
+ accountType: AccountType;
8
+ currency: string;
9
+ balance: number;
10
+ interestRate: number;
11
+ ownerKycId: string;
12
+ }
13
+ export interface AccountState {
14
+ currentBalance: number;
15
+ availableBalance: number;
16
+ pendingTransactions: number;
17
+ isFrozen: boolean;
18
+ }
7
19
 
8
- const defaultConfig: AccountConfig = { accountNumber: '', accountType: 'checking', currency: 'USD', balance: 0, interestRate: 0, ownerKycId: '' };
20
+ const defaultConfig: AccountConfig = {
21
+ accountNumber: '',
22
+ accountType: 'checking',
23
+ currency: 'USD',
24
+ balance: 0,
25
+ interestRate: 0,
26
+ ownerKycId: '',
27
+ };
9
28
 
10
29
  export function createAccountHandler(): TraitHandler<AccountConfig> {
11
- return { name: 'account', defaultConfig,
12
- onAttach(n: HSPlusNode, c: AccountConfig, ctx: TraitContext) { n.__acctState = { currentBalance: c.balance, availableBalance: c.balance, pendingTransactions: 0, isFrozen: false }; ctx.emit?.('account:opened', { type: c.accountType }); },
13
- onDetach(n: HSPlusNode, _c: AccountConfig, ctx: TraitContext) { delete n.__acctState; ctx.emit?.('account:closed'); },
30
+ return {
31
+ name: 'account',
32
+ defaultConfig,
33
+ onAttach(n: HSPlusNode, c: AccountConfig, ctx: TraitContext) {
34
+ n.__acctState = {
35
+ currentBalance: c.balance,
36
+ availableBalance: c.balance,
37
+ pendingTransactions: 0,
38
+ isFrozen: false,
39
+ };
40
+ ctx.emit?.('account:opened', { type: c.accountType });
41
+ },
42
+ onDetach(n: HSPlusNode, _c: AccountConfig, ctx: TraitContext) {
43
+ delete n.__acctState;
44
+ ctx.emit?.('account:closed');
45
+ },
14
46
  onUpdate() {},
15
47
  onEvent(n: HSPlusNode, _c: AccountConfig, ctx: TraitContext, e: TraitEvent) {
16
- const s = n.__acctState as AccountState | undefined; if (!s) return;
17
- if (s.isFrozen) { ctx.emit?.('account:frozen_rejection'); return; }
18
- if (e.type === 'account:credit') { const amt = e.payload?.amount as number; s.currentBalance += amt; s.availableBalance += amt; ctx.emit?.('account:credited', { amount: amt, balance: s.currentBalance }); }
19
- if (e.type === 'account:debit') { const amt = e.payload?.amount as number; if (s.availableBalance >= amt) { s.currentBalance -= amt; s.availableBalance -= amt; ctx.emit?.('account:debited', { amount: amt, balance: s.currentBalance }); } else { ctx.emit?.('account:insufficient_funds', { requested: amt, available: s.availableBalance }); } }
20
- if (e.type === 'account:freeze') { s.isFrozen = true; ctx.emit?.('account:frozen'); }
48
+ const s = n.__acctState as AccountState | undefined;
49
+ if (!s) return;
50
+ if (s.isFrozen) {
51
+ ctx.emit?.('account:frozen_rejection');
52
+ return;
53
+ }
54
+ if (e.type === 'account:credit') {
55
+ const amt = e.payload?.amount as number;
56
+ s.currentBalance += amt;
57
+ s.availableBalance += amt;
58
+ ctx.emit?.('account:credited', { amount: amt, balance: s.currentBalance });
59
+ }
60
+ if (e.type === 'account:debit') {
61
+ const amt = e.payload?.amount as number;
62
+ if (s.availableBalance >= amt) {
63
+ s.currentBalance -= amt;
64
+ s.availableBalance -= amt;
65
+ ctx.emit?.('account:debited', { amount: amt, balance: s.currentBalance });
66
+ } else {
67
+ ctx.emit?.('account:insufficient_funds', {
68
+ requested: amt,
69
+ available: s.availableBalance,
70
+ });
71
+ }
72
+ }
73
+ if (e.type === 'account:freeze') {
74
+ s.isFrozen = true;
75
+ ctx.emit?.('account:frozen');
76
+ }
21
77
  },
22
78
  };
23
79
  }
@@ -3,20 +3,62 @@ import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types
3
3
 
4
4
  export type KYCLevel = 'none' | 'basic' | 'enhanced' | 'full';
5
5
  export type KYCStatus = 'pending' | 'verified' | 'rejected' | 'expired' | 'under_review';
6
- export interface KYCConfig { level: KYCLevel; requiredDocuments: string[]; expiryDays: number; amlCheck: boolean; pepCheck: boolean; }
6
+ export interface KYCConfig {
7
+ level: KYCLevel;
8
+ requiredDocuments: string[];
9
+ expiryDays: number;
10
+ amlCheck: boolean;
11
+ pepCheck: boolean;
12
+ }
7
13
 
8
- const defaultConfig: KYCConfig = { level: 'basic', requiredDocuments: ['government_id', 'proof_of_address'], expiryDays: 365, amlCheck: true, pepCheck: true };
14
+ const defaultConfig: KYCConfig = {
15
+ level: 'basic',
16
+ requiredDocuments: ['government_id', 'proof_of_address'],
17
+ expiryDays: 365,
18
+ amlCheck: true,
19
+ pepCheck: true,
20
+ };
9
21
 
10
22
  export function createKYCHandler(): TraitHandler<KYCConfig> {
11
- return { name: 'kyc', defaultConfig,
12
- onAttach(n: HSPlusNode, _c: KYCConfig, ctx: TraitContext) { n.__kycState = { status: 'pending' as KYCStatus, documentsSubmitted: [] as string[], verifiedAt: null, riskScore: 0 }; ctx.emit?.('kyc:initiated'); },
13
- onDetach(n: HSPlusNode, _c: KYCConfig, ctx: TraitContext) { delete n.__kycState; ctx.emit?.('kyc:removed'); },
23
+ return {
24
+ name: 'kyc',
25
+ defaultConfig,
26
+ onAttach(n: HSPlusNode, _c: KYCConfig, ctx: TraitContext) {
27
+ n.__kycState = {
28
+ status: 'pending' as KYCStatus,
29
+ documentsSubmitted: [] as string[],
30
+ verifiedAt: null,
31
+ riskScore: 0,
32
+ };
33
+ ctx.emit?.('kyc:initiated');
34
+ },
35
+ onDetach(n: HSPlusNode, _c: KYCConfig, ctx: TraitContext) {
36
+ delete n.__kycState;
37
+ ctx.emit?.('kyc:removed');
38
+ },
14
39
  onUpdate() {},
15
40
  onEvent(n: HSPlusNode, c: KYCConfig, ctx: TraitContext, e: TraitEvent) {
16
- const s = n.__kycState as Record<string, unknown> | undefined; if (!s) return;
17
- if (e.type === 'kyc:submit_document') { (s.documentsSubmitted as string[]).push(e.payload?.documentType as string); const allSubmitted = c.requiredDocuments.every(d => (s.documentsSubmitted as string[]).includes(d)); if (allSubmitted) { s.status = 'under_review'; ctx.emit?.('kyc:review_started'); } }
18
- if (e.type === 'kyc:approve') { s.status = 'verified'; s.verifiedAt = Date.now(); ctx.emit?.('kyc:verified', { level: c.level }); }
19
- if (e.type === 'kyc:reject') { s.status = 'rejected'; ctx.emit?.('kyc:rejected', { reason: e.payload?.reason }); }
41
+ const s = n.__kycState as Record<string, unknown> | undefined;
42
+ if (!s) return;
43
+ if (e.type === 'kyc:submit_document') {
44
+ (s.documentsSubmitted as string[]).push(e.payload?.documentType as string);
45
+ const allSubmitted = c.requiredDocuments.every((d) =>
46
+ (s.documentsSubmitted as string[]).includes(d)
47
+ );
48
+ if (allSubmitted) {
49
+ s.status = 'under_review';
50
+ ctx.emit?.('kyc:review_started');
51
+ }
52
+ }
53
+ if (e.type === 'kyc:approve') {
54
+ s.status = 'verified';
55
+ s.verifiedAt = Date.now();
56
+ ctx.emit?.('kyc:verified', { level: c.level });
57
+ }
58
+ if (e.type === 'kyc:reject') {
59
+ s.status = 'rejected';
60
+ ctx.emit?.('kyc:rejected', { reason: e.payload?.reason });
61
+ }
20
62
  },
21
63
  };
22
64
  }
@@ -1,28 +1,66 @@
1
1
  /** @portfolio Trait — Investment portfolio management. @trait portfolio */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export interface Holding { symbol: string; quantity: number; avgCostBasis: number; currentPrice: number; assetClass: 'equity' | 'bond' | 'crypto' | 'commodity' | 'real_estate' | 'cash'; }
5
- export interface PortfolioConfig { holdings: Holding[]; benchmarkIndex: string; riskTolerance: 'conservative' | 'moderate' | 'aggressive'; rebalanceThreshold: number; }
6
- export interface PortfolioState { totalValue: number; totalCost: number; unrealizedPnL: number; dayChange: number; }
4
+ export interface Holding {
5
+ symbol: string;
6
+ quantity: number;
7
+ avgCostBasis: number;
8
+ currentPrice: number;
9
+ assetClass: 'equity' | 'bond' | 'crypto' | 'commodity' | 'real_estate' | 'cash';
10
+ }
11
+ export interface PortfolioConfig {
12
+ holdings: Holding[];
13
+ benchmarkIndex: string;
14
+ riskTolerance: 'conservative' | 'moderate' | 'aggressive';
15
+ rebalanceThreshold: number;
16
+ }
17
+ export interface PortfolioState {
18
+ totalValue: number;
19
+ totalCost: number;
20
+ unrealizedPnL: number;
21
+ dayChange: number;
22
+ }
7
23
 
8
- const defaultConfig: PortfolioConfig = { holdings: [], benchmarkIndex: 'SPY', riskTolerance: 'moderate', rebalanceThreshold: 5 };
24
+ const defaultConfig: PortfolioConfig = {
25
+ holdings: [],
26
+ benchmarkIndex: 'SPY',
27
+ riskTolerance: 'moderate',
28
+ rebalanceThreshold: 5,
29
+ };
9
30
 
10
31
  export function createPortfolioHandler(): TraitHandler<PortfolioConfig> {
11
- return { name: 'portfolio', defaultConfig,
32
+ return {
33
+ name: 'portfolio',
34
+ defaultConfig,
12
35
  onAttach(n: HSPlusNode, c: PortfolioConfig, ctx: TraitContext) {
13
36
  const totalValue = c.holdings.reduce((s, h) => s + h.currentPrice * h.quantity, 0);
14
37
  const totalCost = c.holdings.reduce((s, h) => s + h.avgCostBasis * h.quantity, 0);
15
- n.__portfolioState = { totalValue, totalCost, unrealizedPnL: totalValue - totalCost, dayChange: 0 };
38
+ n.__portfolioState = {
39
+ totalValue,
40
+ totalCost,
41
+ unrealizedPnL: totalValue - totalCost,
42
+ dayChange: 0,
43
+ };
16
44
  ctx.emit?.('portfolio:loaded', { holdings: c.holdings.length, totalValue });
17
45
  },
18
- onDetach(n: HSPlusNode, _c: PortfolioConfig, ctx: TraitContext) { delete n.__portfolioState; ctx.emit?.('portfolio:closed'); },
46
+ onDetach(n: HSPlusNode, _c: PortfolioConfig, ctx: TraitContext) {
47
+ delete n.__portfolioState;
48
+ ctx.emit?.('portfolio:closed');
49
+ },
19
50
  onUpdate() {},
20
51
  onEvent(n: HSPlusNode, c: PortfolioConfig, ctx: TraitContext, e: TraitEvent) {
21
- const s = n.__portfolioState as PortfolioState | undefined; if (!s) return;
52
+ const s = n.__portfolioState as PortfolioState | undefined;
53
+ if (!s) return;
22
54
  if (e.type === 'portfolio:price_update') {
23
- const sym = e.payload?.symbol as string; const price = e.payload?.price as number;
24
- const holding = c.holdings.find(h => h.symbol === sym);
25
- if (holding) { holding.currentPrice = price; s.totalValue = c.holdings.reduce((sum, h) => sum + h.currentPrice * h.quantity, 0); s.unrealizedPnL = s.totalValue - s.totalCost; ctx.emit?.('portfolio:updated', { totalValue: s.totalValue, pnl: s.unrealizedPnL }); }
55
+ const sym = e.payload?.symbol as string;
56
+ const price = e.payload?.price as number;
57
+ const holding = c.holdings.find((h) => h.symbol === sym);
58
+ if (holding) {
59
+ holding.currentPrice = price;
60
+ s.totalValue = c.holdings.reduce((sum, h) => sum + h.currentPrice * h.quantity, 0);
61
+ s.unrealizedPnL = s.totalValue - s.totalCost;
62
+ ctx.emit?.('portfolio:updated', { totalValue: s.totalValue, pnl: s.unrealizedPnL });
63
+ }
26
64
  }
27
65
  },
28
66
  };
@@ -2,20 +2,61 @@
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
4
  export type RiskCategory = 'market' | 'credit' | 'operational' | 'liquidity' | 'regulatory';
5
- export interface RiskModelConfig { category: RiskCategory; varConfidenceLevel: number; timeHorizonDays: number; stressScenarios: string[]; maxExposure: number; }
6
- export interface RiskModelState { currentVaR: number; stressTestResults: Record<string, number>; riskScore: number; breachCount: number; }
5
+ export interface RiskModelConfig {
6
+ category: RiskCategory;
7
+ varConfidenceLevel: number;
8
+ timeHorizonDays: number;
9
+ stressScenarios: string[];
10
+ maxExposure: number;
11
+ }
12
+ export interface RiskModelState {
13
+ currentVaR: number;
14
+ stressTestResults: Record<string, number>;
15
+ riskScore: number;
16
+ breachCount: number;
17
+ }
7
18
 
8
- const defaultConfig: RiskModelConfig = { category: 'market', varConfidenceLevel: 0.99, timeHorizonDays: 1, stressScenarios: ['crash_2008', 'covid_2020', 'rate_hike'], maxExposure: 1000000 };
19
+ const defaultConfig: RiskModelConfig = {
20
+ category: 'market',
21
+ varConfidenceLevel: 0.99,
22
+ timeHorizonDays: 1,
23
+ stressScenarios: ['crash_2008', 'covid_2020', 'rate_hike'],
24
+ maxExposure: 1000000,
25
+ };
9
26
 
10
27
  export function createRiskModelHandler(): TraitHandler<RiskModelConfig> {
11
- return { name: 'risk_model', defaultConfig,
12
- onAttach(n: HSPlusNode, _c: RiskModelConfig, ctx: TraitContext) { n.__riskState = { currentVaR: 0, stressTestResults: {}, riskScore: 0, breachCount: 0 }; ctx.emit?.('risk:model_loaded'); },
13
- onDetach(n: HSPlusNode, _c: RiskModelConfig, ctx: TraitContext) { delete n.__riskState; ctx.emit?.('risk:model_removed'); },
28
+ return {
29
+ name: 'risk_model',
30
+ defaultConfig,
31
+ onAttach(n: HSPlusNode, _c: RiskModelConfig, ctx: TraitContext) {
32
+ n.__riskState = { currentVaR: 0, stressTestResults: {}, riskScore: 0, breachCount: 0 };
33
+ ctx.emit?.('risk:model_loaded');
34
+ },
35
+ onDetach(n: HSPlusNode, _c: RiskModelConfig, ctx: TraitContext) {
36
+ delete n.__riskState;
37
+ ctx.emit?.('risk:model_removed');
38
+ },
14
39
  onUpdate() {},
15
40
  onEvent(n: HSPlusNode, c: RiskModelConfig, ctx: TraitContext, e: TraitEvent) {
16
- const s = n.__riskState as RiskModelState | undefined; if (!s) return;
17
- if (e.type === 'risk:calculate_var') { s.currentVaR = (e.payload?.portfolioValue as number ?? 0) * (1 - c.varConfidenceLevel) * Math.sqrt(c.timeHorizonDays); ctx.emit?.('risk:var_calculated', { var: s.currentVaR }); if (s.currentVaR > c.maxExposure) { s.breachCount++; ctx.emit?.('risk:breach', { var: s.currentVaR, limit: c.maxExposure }); } }
18
- if (e.type === 'risk:stress_test') { for (const scenario of c.stressScenarios) { s.stressTestResults[scenario] = Math.random() * c.maxExposure * 0.3; } ctx.emit?.('risk:stress_complete', { results: s.stressTestResults }); }
41
+ const s = n.__riskState as RiskModelState | undefined;
42
+ if (!s) return;
43
+ if (e.type === 'risk:calculate_var') {
44
+ s.currentVaR =
45
+ ((e.payload?.portfolioValue as number) ?? 0) *
46
+ (1 - c.varConfidenceLevel) *
47
+ Math.sqrt(c.timeHorizonDays);
48
+ ctx.emit?.('risk:var_calculated', { var: s.currentVaR });
49
+ if (s.currentVaR > c.maxExposure) {
50
+ s.breachCount++;
51
+ ctx.emit?.('risk:breach', { var: s.currentVaR, limit: c.maxExposure });
52
+ }
53
+ }
54
+ if (e.type === 'risk:stress_test') {
55
+ for (const scenario of c.stressScenarios) {
56
+ s.stressTestResults[scenario] = Math.random() * c.maxExposure * 0.3;
57
+ }
58
+ ctx.emit?.('risk:stress_complete', { results: s.stressTestResults });
59
+ }
19
60
  },
20
61
  };
21
62
  }
@@ -1,23 +1,73 @@
1
1
  /** @transaction Trait — Financial transaction record. @trait transaction */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export type TransactionType = 'deposit' | 'withdrawal' | 'transfer' | 'payment' | 'fee' | 'interest' | 'refund';
4
+ export type TransactionType =
5
+ | 'deposit'
6
+ | 'withdrawal'
7
+ | 'transfer'
8
+ | 'payment'
9
+ | 'fee'
10
+ | 'interest'
11
+ | 'refund';
5
12
  export type TransactionStatus = 'pending' | 'completed' | 'failed' | 'reversed' | 'held';
6
- export interface TransactionConfig { type: TransactionType; amount: number; currency: string; fromAccount: string; toAccount: string; description: string; reference: string; }
7
- export interface TransactionState { status: TransactionStatus; timestamp: number; settledAt: number | null; }
13
+ export interface TransactionConfig {
14
+ type: TransactionType;
15
+ amount: number;
16
+ currency: string;
17
+ fromAccount: string;
18
+ toAccount: string;
19
+ description: string;
20
+ reference: string;
21
+ }
22
+ export interface TransactionState {
23
+ status: TransactionStatus;
24
+ timestamp: number;
25
+ settledAt: number | null;
26
+ }
8
27
 
9
- const defaultConfig: TransactionConfig = { type: 'transfer', amount: 0, currency: 'USD', fromAccount: '', toAccount: '', description: '', reference: '' };
28
+ const defaultConfig: TransactionConfig = {
29
+ type: 'transfer',
30
+ amount: 0,
31
+ currency: 'USD',
32
+ fromAccount: '',
33
+ toAccount: '',
34
+ description: '',
35
+ reference: '',
36
+ };
10
37
 
11
38
  export function createTransactionHandler(): TraitHandler<TransactionConfig> {
12
- return { name: 'transaction', defaultConfig,
13
- onAttach(n: HSPlusNode, c: TransactionConfig, ctx: TraitContext) { n.__txnState = { status: 'pending' as TransactionStatus, timestamp: Date.now(), settledAt: null }; ctx.emit?.('transaction:created', { type: c.type, amount: c.amount }); },
14
- onDetach(n: HSPlusNode, _c: TransactionConfig, ctx: TraitContext) { delete n.__txnState; ctx.emit?.('transaction:removed'); },
39
+ return {
40
+ name: 'transaction',
41
+ defaultConfig,
42
+ onAttach(n: HSPlusNode, c: TransactionConfig, ctx: TraitContext) {
43
+ n.__txnState = {
44
+ status: 'pending' as TransactionStatus,
45
+ timestamp: Date.now(),
46
+ settledAt: null,
47
+ };
48
+ ctx.emit?.('transaction:created', { type: c.type, amount: c.amount });
49
+ },
50
+ onDetach(n: HSPlusNode, _c: TransactionConfig, ctx: TraitContext) {
51
+ delete n.__txnState;
52
+ ctx.emit?.('transaction:removed');
53
+ },
15
54
  onUpdate() {},
16
55
  onEvent(n: HSPlusNode, c: TransactionConfig, ctx: TraitContext, e: TraitEvent) {
17
- const s = n.__txnState as TransactionState | undefined; if (!s) return;
18
- if (e.type === 'transaction:settle') { s.status = 'completed'; s.settledAt = Date.now(); ctx.emit?.('transaction:settled', { amount: c.amount, reference: c.reference }); }
19
- if (e.type === 'transaction:fail') { s.status = 'failed'; ctx.emit?.('transaction:failed', { reason: e.payload?.reason }); }
20
- if (e.type === 'transaction:reverse') { s.status = 'reversed'; ctx.emit?.('transaction:reversed', { amount: c.amount }); }
56
+ const s = n.__txnState as TransactionState | undefined;
57
+ if (!s) return;
58
+ if (e.type === 'transaction:settle') {
59
+ s.status = 'completed';
60
+ s.settledAt = Date.now();
61
+ ctx.emit?.('transaction:settled', { amount: c.amount, reference: c.reference });
62
+ }
63
+ if (e.type === 'transaction:fail') {
64
+ s.status = 'failed';
65
+ ctx.emit?.('transaction:failed', { reason: e.payload?.reason });
66
+ }
67
+ if (e.type === 'transaction:reverse') {
68
+ s.status = 'reversed';
69
+ ctx.emit?.('transaction:reversed', { amount: c.amount });
70
+ }
21
71
  },
22
72
  };
23
73
  }
@@ -1,4 +1,22 @@
1
- export interface HSPlusNode { id?: string; properties?: Record<string, unknown>; [key: string]: unknown; }
2
- export interface TraitContext { emit?: (event: string, payload?: unknown) => void; [key: string]: unknown; }
3
- export interface TraitEvent { type: string; payload?: Record<string, unknown>; [key: string]: unknown; }
4
- export interface TraitHandler<T = unknown> { name: string; defaultConfig: T; onAttach(n: HSPlusNode, c: T, ctx: TraitContext): void; onDetach(n: HSPlusNode, c: T, ctx: TraitContext): void; onUpdate(n: HSPlusNode, c: T, ctx: TraitContext, d: number): void; onEvent(n: HSPlusNode, c: T, ctx: TraitContext, e: TraitEvent): void; }
1
+ export interface HSPlusNode {
2
+ id?: string;
3
+ properties?: Record<string, unknown>;
4
+ [key: string]: unknown;
5
+ }
6
+ export interface TraitContext {
7
+ emit?: (event: string, payload?: unknown) => void;
8
+ [key: string]: unknown;
9
+ }
10
+ export interface TraitEvent {
11
+ type: string;
12
+ payload?: Record<string, unknown>;
13
+ [key: string]: unknown;
14
+ }
15
+ export interface TraitHandler<T = unknown> {
16
+ name: string;
17
+ defaultConfig: T;
18
+ onAttach(n: HSPlusNode, c: T, ctx: TraitContext): void;
19
+ onDetach(n: HSPlusNode, c: T, ctx: TraitContext): void;
20
+ onUpdate(n: HSPlusNode, c: T, ctx: TraitContext, d: number): void;
21
+ onEvent(n: HSPlusNode, c: T, ctx: TraitContext, e: TraitEvent): void;
22
+ }
package/tsconfig.json CHANGED
@@ -1 +1,5 @@
1
- { "extends": "../../../tsconfig.json", "compilerOptions": { "outDir": "dist", "rootDir": "src", "declaration": true }, "include": ["src"] }
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": { "outDir": "dist", "rootDir": "src", "declaration": true },
4
+ "include": ["src"]
5
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025-2026 HoloScript Contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.