@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 +3 -3
- package/src/__tests__/fixedincome.test.ts +42 -23
- package/src/fixedincome.ts +154 -123
- package/src/index.ts +28 -5
- package/src/runtime.ts +7 -3
- package/src/traits/AccountTrait.ts +67 -11
- package/src/traits/KYCTrait.ts +51 -9
- package/src/traits/PortfolioTrait.ts +49 -11
- package/src/traits/RiskModelTrait.ts +50 -9
- package/src/traits/TransactionTrait.ts +61 -11
- package/src/traits/types.ts +22 -4
- package/tsconfig.json +5 -1
- package/LICENSE +0 -21
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holoscript/plugin-banking-finance",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
5
|
"peerDependencies": {
|
|
6
|
-
"@holoscript/core": "8.0.
|
|
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:
|
|
30
|
-
couponRate:
|
|
31
|
-
periods:
|
|
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
|
|
66
|
-
const r6
|
|
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,
|
|
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],
|
|
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.
|
|
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
|
|
233
|
-
const S
|
|
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
|
|
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.
|
|
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.
|
|
274
|
-
const hi = blackScholes(40, 40, 0.5, 0.05, 0.
|
|
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.
|
|
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
|
-
{
|
|
294
|
-
|
|
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:
|
|
310
|
+
bond: {
|
|
311
|
+
spec: { faceValue: 1000, couponRate: 0.05, periods: 20, periodsPerYear: 2 },
|
|
312
|
+
annualYield: 0.05,
|
|
313
|
+
},
|
|
298
314
|
portfolio: { holdings },
|
|
299
|
-
options:
|
|
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: {
|
|
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', () => {
|
package/src/fixedincome.ts
CHANGED
|
@@ -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:
|
|
24
|
+
faceValue: number;
|
|
28
25
|
/** Annual coupon rate (e.g. 0.05 = 5%) */
|
|
29
|
-
couponRate:
|
|
26
|
+
couponRate: number;
|
|
30
27
|
/** Periods to maturity (coupon payments; typically semi-annual = years × 2) */
|
|
31
|
-
periods:
|
|
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?:
|
|
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:
|
|
37
|
+
price: number;
|
|
41
38
|
/** Yield to maturity (annual, bond-equivalent) */
|
|
42
|
-
ytm:
|
|
39
|
+
ytm: number | null;
|
|
43
40
|
/** Macaulay duration (years) */
|
|
44
|
-
macaulayDuration:
|
|
41
|
+
macaulayDuration: number;
|
|
45
42
|
/** Modified duration (% price change per 1% yield change) */
|
|
46
|
-
modifiedDuration:
|
|
43
|
+
modifiedDuration: number;
|
|
47
44
|
/** Dollar duration (DV01): price change for 1 bp yield change */
|
|
48
|
-
dv01:
|
|
45
|
+
dv01: number;
|
|
49
46
|
/** Convexity */
|
|
50
|
-
convexity:
|
|
47
|
+
convexity: number;
|
|
51
48
|
}
|
|
52
49
|
|
|
53
50
|
export interface PortfolioHolding {
|
|
54
|
-
id:
|
|
51
|
+
id: string;
|
|
55
52
|
/** Historical returns (one per period) */
|
|
56
|
-
returns:
|
|
53
|
+
returns: number[];
|
|
57
54
|
/** Portfolio weight (0–1, must sum to 1 across holdings) */
|
|
58
|
-
weight:
|
|
55
|
+
weight: number;
|
|
59
56
|
}
|
|
60
57
|
|
|
61
58
|
export interface PortfolioResult {
|
|
62
|
-
expectedReturn:
|
|
63
|
-
variance:
|
|
64
|
-
stdDev:
|
|
65
|
-
sharpeRatio:
|
|
66
|
-
beta:
|
|
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:
|
|
65
|
+
var95: number;
|
|
69
66
|
/** Historical CVaR (expected shortfall) at 95% */
|
|
70
|
-
cvar95:
|
|
67
|
+
cvar95: number;
|
|
71
68
|
}
|
|
72
69
|
|
|
73
70
|
export interface BlackScholesResult {
|
|
74
71
|
callPrice: number;
|
|
75
|
-
putPrice:
|
|
72
|
+
putPrice: number;
|
|
76
73
|
/** Greeks */
|
|
77
74
|
callDelta: number;
|
|
78
|
-
putDelta:
|
|
79
|
-
gamma:
|
|
80
|
-
vega:
|
|
75
|
+
putDelta: number;
|
|
76
|
+
gamma: number;
|
|
77
|
+
vega: number;
|
|
81
78
|
callTheta: number;
|
|
82
|
-
putTheta:
|
|
83
|
-
callRho:
|
|
84
|
-
putRho:
|
|
79
|
+
putTheta: number;
|
|
80
|
+
callRho: number;
|
|
81
|
+
putRho: number;
|
|
85
82
|
}
|
|
86
83
|
|
|
87
84
|
export interface FinanceAnalysisResult {
|
|
88
|
-
bond?:
|
|
85
|
+
bond?: BondResult;
|
|
89
86
|
portfolio?: PortfolioResult;
|
|
90
|
-
options?:
|
|
91
|
-
converged:
|
|
87
|
+
options?: BlackScholesResult;
|
|
88
|
+
converged: boolean;
|
|
92
89
|
}
|
|
93
90
|
|
|
94
91
|
export interface FinanceReceipt {
|
|
95
|
-
plugin:
|
|
96
|
-
runId:
|
|
97
|
-
payloadHash:
|
|
92
|
+
plugin: string;
|
|
93
|
+
runId: string;
|
|
94
|
+
payloadHash: string;
|
|
98
95
|
hashAlgorithm: string;
|
|
99
|
-
cael:
|
|
100
|
-
acceptance:
|
|
96
|
+
cael: { event: string; solverType: string; version: string };
|
|
97
|
+
acceptance: { accepted: boolean; violations: Array<{ criterion: string; message: string }> };
|
|
101
98
|
resultSummary: {
|
|
102
|
-
bondYTM?:
|
|
103
|
-
bondDuration?:
|
|
104
|
-
portfolioReturn?:
|
|
105
|
-
portfolioSharpe?:
|
|
106
|
-
optionCallPrice?:
|
|
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 >
|
|
112
|
+
if (x > 8) return 1;
|
|
116
113
|
const k = 1 / (1 + 0.2316419 * Math.abs(x));
|
|
117
|
-
const poly =
|
|
118
|
-
|
|
119
|
-
const pdf
|
|
120
|
-
const cdf
|
|
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)
|
|
152
|
-
if (faceValue <= 0)
|
|
153
|
-
if (periodsPerYear <= 0)
|
|
154
|
-
if (annualYield < 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
|
|
157
|
-
const C
|
|
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
|
|
159
|
+
let convexitySum = 0;
|
|
163
160
|
for (let t = 1; t <= periods; t++) {
|
|
164
|
-
const cf
|
|
165
|
-
const pv
|
|
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
|
|
164
|
+
convexitySum += (t * (t + 1) * pv) / Math.pow(1 + r, 2);
|
|
168
165
|
}
|
|
169
|
-
const macaulayDuration
|
|
170
|
-
const modifiedDuration
|
|
171
|
-
const dv01
|
|
172
|
-
const convexity
|
|
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,
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
183
|
+
const mid = (lo + hi) / 2;
|
|
184
184
|
const delta = bondPrice(spec, mid / periodsPerYear) - mktPrice;
|
|
185
|
-
if (Math.abs(delta) < 0.0001 ||
|
|
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)
|
|
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:
|
|
207
|
-
riskFreeRate?:
|
|
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))
|
|
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)
|
|
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
|
|
225
|
-
const stdDev
|
|
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
|
|
231
|
-
const excessReturn
|
|
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
|
|
240
|
-
|
|
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
|
|
247
|
-
const var95
|
|
248
|
-
const tail
|
|
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(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
273
|
-
const d2
|
|
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
|
|
276
|
-
const Nd2
|
|
277
|
-
const Nnd1
|
|
278
|
-
const Nnd2
|
|
279
|
-
const nd1
|
|
280
|
-
const disc
|
|
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
|
|
283
|
-
const putPrice
|
|
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
|
|
287
|
-
const gamma
|
|
288
|
-
const vega
|
|
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
|
|
291
|
-
const callRho
|
|
292
|
-
const putRho
|
|
293
|
-
|
|
294
|
-
return {
|
|
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?:
|
|
328
|
+
bond?: { spec: BondSpec; annualYield: number };
|
|
301
329
|
portfolio?: { holdings: PortfolioHolding[]; riskFreeRate?: number; benchmarkReturns?: number[] };
|
|
302
|
-
options?:
|
|
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)
|
|
306
|
-
if (params.portfolio)
|
|
307
|
-
|
|
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:
|
|
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
|
|
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:
|
|
371
|
+
plugin: 'banking-finance',
|
|
341
372
|
pluginVersion: '1.0.0',
|
|
342
|
-
runId:
|
|
373
|
+
runId: options?.runId ?? `fin-${Date.now().toString(36)}`,
|
|
343
374
|
solverConfig: {
|
|
344
375
|
solverType: 'fixed-income-portfolio',
|
|
345
|
-
scale:
|
|
346
|
-
hasBond:
|
|
376
|
+
scale: 'instrument',
|
|
377
|
+
hasBond: result.bond !== undefined,
|
|
347
378
|
hasPortfolio: result.portfolio !== undefined,
|
|
348
|
-
hasOptions:
|
|
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:
|
|
355
|
-
event:
|
|
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 {
|
|
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 {
|
|
5
|
-
|
|
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 = {
|
|
17
|
-
|
|
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?: (
|
|
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 {
|
|
6
|
-
|
|
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 = {
|
|
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 {
|
|
12
|
-
|
|
13
|
-
|
|
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;
|
|
17
|
-
if (s
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
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
|
}
|
package/src/traits/KYCTrait.ts
CHANGED
|
@@ -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 {
|
|
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 = {
|
|
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 {
|
|
12
|
-
|
|
13
|
-
|
|
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;
|
|
17
|
-
if (
|
|
18
|
-
if (e.type === 'kyc:
|
|
19
|
-
|
|
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 {
|
|
5
|
-
|
|
6
|
-
|
|
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 = {
|
|
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 {
|
|
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 = {
|
|
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) {
|
|
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;
|
|
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;
|
|
24
|
-
const
|
|
25
|
-
|
|
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 {
|
|
6
|
-
|
|
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 = {
|
|
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 {
|
|
12
|
-
|
|
13
|
-
|
|
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;
|
|
17
|
-
if (
|
|
18
|
-
if (e.type === 'risk:
|
|
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 =
|
|
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 {
|
|
7
|
-
|
|
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 = {
|
|
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 {
|
|
13
|
-
|
|
14
|
-
|
|
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;
|
|
18
|
-
if (
|
|
19
|
-
if (e.type === 'transaction:
|
|
20
|
-
|
|
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
|
}
|
package/src/traits/types.ts
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
export interface HSPlusNode {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
{
|
|
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.
|