@stvy/fund-indicators 1.0.0 → 1.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.
Files changed (52) hide show
  1. package/.github/workflows/publish.yml +79 -0
  2. package/AGENTS.md +322 -0
  3. package/dist/index.cjs +1615 -0
  4. package/dist/index.d.cts +779 -0
  5. package/dist/index.d.cts.map +1 -0
  6. package/dist/index.mjs +1518 -0
  7. package/package.json +10 -29
  8. package/pnpm-workspace.yaml +2 -0
  9. package/src/dca.ts +420 -0
  10. package/src/index.ts +133 -0
  11. package/src/jstat.d.ts +17 -0
  12. package/src/pattern.ts +447 -0
  13. package/src/risk.ts +516 -0
  14. package/src/statistics.ts +428 -0
  15. package/src/technical.ts +738 -0
  16. package/src/types.ts +369 -0
  17. package/test/index.test.ts +355 -0
  18. package/tsconfig.json +20 -0
  19. package/dist/browser/fund-indicators.esm.js +0 -7505
  20. package/dist/browser/fund-indicators.esm.min.js +0 -8
  21. package/dist/browser/fund-indicators.esm.min.js.map +0 -7
  22. package/dist/browser/fund-indicators.js +0 -7517
  23. package/dist/browser/fund-indicators.min.js +0 -8
  24. package/dist/browser/fund-indicators.min.js.map +0 -7
  25. package/dist/dca.d.ts +0 -91
  26. package/dist/dca.d.ts.map +0 -1
  27. package/dist/dca.js +0 -354
  28. package/dist/dca.js.map +0 -1
  29. package/dist/index.d.ts +0 -18
  30. package/dist/index.d.ts.map +0 -1
  31. package/dist/index.js +0 -141
  32. package/dist/index.js.map +0 -1
  33. package/dist/pattern.d.ts +0 -60
  34. package/dist/pattern.d.ts.map +0 -1
  35. package/dist/pattern.js +0 -386
  36. package/dist/pattern.js.map +0 -1
  37. package/dist/risk.d.ts +0 -115
  38. package/dist/risk.d.ts.map +0 -1
  39. package/dist/risk.js +0 -502
  40. package/dist/risk.js.map +0 -1
  41. package/dist/statistics.d.ts +0 -78
  42. package/dist/statistics.d.ts.map +0 -1
  43. package/dist/statistics.js +0 -402
  44. package/dist/statistics.js.map +0 -1
  45. package/dist/technical.d.ts +0 -105
  46. package/dist/technical.d.ts.map +0 -1
  47. package/dist/technical.js +0 -633
  48. package/dist/technical.js.map +0 -1
  49. package/dist/types.d.ts +0 -327
  50. package/dist/types.d.ts.map +0 -1
  51. package/dist/types.js +0 -7
  52. package/dist/types.js.map +0 -1
package/dist/index.mjs ADDED
@@ -0,0 +1,1518 @@
1
+ import { ADX, ATR, BollingerBands, CCI, EMA, Stochastic, MACD, ROC, RSI, PSAR, SMA, StochasticRSI, WilliamsR, WMA } from 'technicalindicators';
2
+ import * as ss from 'simple-statistics';
3
+ import { jStat } from 'jstat';
4
+
5
+ function navToHLC(nav) {
6
+ return nav.map((v) => ({ high: v, low: v, close: v }));
7
+ }
8
+ function padLeft(arr, totalLen) {
9
+ const padLen = totalLen - arr.length;
10
+ const result = new Array(padLen).fill(null);
11
+ for (const v of arr) {
12
+ result.push(v ?? null);
13
+ }
14
+ return result;
15
+ }
16
+ function lastNonNull(arr) {
17
+ for (let i = arr.length - 1; i >= 0; i--) {
18
+ if (arr[i] != null) return arr[i];
19
+ }
20
+ return null;
21
+ }
22
+ function sma(nav, period) {
23
+ const raw = SMA.calculate({ values: nav, period });
24
+ const values = padLeft(raw, nav.length);
25
+ return { values, current: lastNonNull(values), period, type: "SMA" };
26
+ }
27
+ function ema(nav, period) {
28
+ const raw = EMA.calculate({ values: nav, period });
29
+ const values = padLeft(raw, nav.length);
30
+ return { values, current: lastNonNull(values), period, type: "EMA" };
31
+ }
32
+ function wma(nav, period) {
33
+ const raw = WMA.calculate({ values: nav, period });
34
+ const values = padLeft(raw, nav.length);
35
+ return { values, current: lastNonNull(values), period, type: "WMA" };
36
+ }
37
+ function dema(nav, period) {
38
+ const ema1 = EMA.calculate({ values: nav, period });
39
+ const ema1Padded = padLeft(ema1, nav.length);
40
+ const ema1Values = ema1.filter((v) => v !== void 0);
41
+ const ema2 = EMA.calculate({ values: ema1Values, period });
42
+ const values = new Array(nav.length).fill(null);
43
+ const offset = nav.length - ema1Values.length;
44
+ for (let i = 0; i < ema2.length; i++) {
45
+ const idx = offset + i;
46
+ if (idx < nav.length && ema2[i] != null && ema1Padded[idx] != null) {
47
+ values[idx] = 2 * ema1Padded[idx] - ema2[i];
48
+ }
49
+ }
50
+ return { values, current: lastNonNull(values), period, type: "DEMA" };
51
+ }
52
+ function tema(nav, period) {
53
+ const ema1Raw = EMA.calculate({ values: nav, period });
54
+ const ema1Vals = ema1Raw.filter((v) => v !== void 0);
55
+ const ema2Raw = EMA.calculate({ values: ema1Vals, period });
56
+ const ema2Vals = ema2Raw.filter((v) => v !== void 0);
57
+ const ema3Raw = EMA.calculate({ values: ema2Vals, period });
58
+ const ema1Padded = padLeft(ema1Raw, nav.length);
59
+ const offset2 = nav.length - ema1Vals.length;
60
+ const ema2Full = new Array(nav.length).fill(null);
61
+ for (let i = 0; i < ema2Raw.length; i++) {
62
+ const idx = offset2 + i;
63
+ if (idx < nav.length) ema2Full[idx] = ema2Raw[i] ?? null;
64
+ }
65
+ const offset3 = offset2 + (ema1Vals.length - ema2Vals.length);
66
+ const ema3Full = new Array(nav.length).fill(null);
67
+ for (let i = 0; i < ema3Raw.length; i++) {
68
+ const idx = offset3 + i;
69
+ if (idx < nav.length) ema3Full[idx] = ema3Raw[i] ?? null;
70
+ }
71
+ const values = new Array(nav.length).fill(null);
72
+ for (let i = 0; i < nav.length; i++) {
73
+ const v1 = ema1Padded[i];
74
+ const v2 = ema2Full[i];
75
+ const v3 = ema3Full[i];
76
+ if (v1 != null && v2 != null && v3 != null) {
77
+ values[i] = 3 * v1 - 3 * v2 + v3;
78
+ }
79
+ }
80
+ return { values, current: lastNonNull(values), period, type: "TEMA" };
81
+ }
82
+ function kama(nav, period = 10, fast = 2, slow = 30) {
83
+ const fastSC = 2 / (fast + 1);
84
+ const slowSC = 2 / (slow + 1);
85
+ const values = new Array(nav.length).fill(null);
86
+ if (nav.length <= period) return { values, current: null, period, type: "KAMA" };
87
+ values[period] = nav[period];
88
+ for (let i = period + 1; i < nav.length; i++) {
89
+ const direction = Math.abs(nav[i] - nav[i - period]);
90
+ let volatility = 0;
91
+ for (let j = i - period + 1; j <= i; j++) {
92
+ volatility += Math.abs(nav[j] - nav[j - 1]);
93
+ }
94
+ const er = volatility === 0 ? 0 : direction / volatility;
95
+ const sc = Math.pow(er * (fastSC - slowSC) + slowSC, 2);
96
+ const prevKama = values[i - 1];
97
+ values[i] = prevKama + sc * (nav[i] - prevKama);
98
+ }
99
+ return { values, current: lastNonNull(values), period, type: "KAMA" };
100
+ }
101
+ function macd(nav, fastPeriod = 12, slowPeriod = 26, signalPeriod = 9) {
102
+ const raw = MACD.calculate({
103
+ values: nav,
104
+ fastPeriod,
105
+ slowPeriod,
106
+ signalPeriod,
107
+ SimpleMAOscillator: false,
108
+ SimpleMASignal: false
109
+ });
110
+ const dif = padLeft(raw.map((r) => r.MACD), nav.length);
111
+ const dea = padLeft(raw.map((r) => r.signal), nav.length);
112
+ const histogram = padLeft(raw.map((r) => r.histogram), nav.length);
113
+ return {
114
+ dif,
115
+ dea,
116
+ histogram,
117
+ currentDIF: lastNonNull(dif),
118
+ currentDEA: lastNonNull(dea),
119
+ currentHistogram: lastNonNull(histogram)
120
+ };
121
+ }
122
+ function rsi(nav, period = 14) {
123
+ const raw = RSI.calculate({ values: nav, period });
124
+ const values = padLeft(raw, nav.length);
125
+ return { values, current: lastNonNull(values), period };
126
+ }
127
+ function kdj(nav, kPeriod = 9, kSmooth = 3, dPeriod = 3) {
128
+ const hlc = navToHLC(nav);
129
+ const raw = Stochastic.calculate({
130
+ high: hlc.map((h) => h.high),
131
+ low: hlc.map((h) => h.low),
132
+ close: hlc.map((h) => h.close),
133
+ period: kPeriod,
134
+ signalPeriod: dPeriod
135
+ });
136
+ const kArr = padLeft(raw.map((r) => r.k), nav.length);
137
+ const dArr = padLeft(raw.map((r) => r.d), nav.length);
138
+ const jArr = kArr.map((kVal, i) => {
139
+ const dVal = dArr[i];
140
+ if (kVal != null && dVal != null) return 3 * kVal - 2 * dVal;
141
+ return null;
142
+ });
143
+ return {
144
+ k: kArr,
145
+ d: dArr,
146
+ j: jArr,
147
+ currentK: lastNonNull(kArr),
148
+ currentD: lastNonNull(dArr),
149
+ currentJ: lastNonNull(jArr)
150
+ };
151
+ }
152
+ function bollingerBands(nav, period = 20, stdDev = 2) {
153
+ const raw = BollingerBands.calculate({ values: nav, period, stdDev });
154
+ const middle = padLeft(raw.map((r) => r.middle), nav.length);
155
+ const upper = padLeft(raw.map((r) => r.upper), nav.length);
156
+ const lower = padLeft(raw.map((r) => r.lower), nav.length);
157
+ const bandwidth = [];
158
+ const percentB = [];
159
+ for (let i = 0; i < nav.length; i++) {
160
+ const m = middle[i];
161
+ const u = upper[i];
162
+ const l = lower[i];
163
+ if (m != null && u != null && l != null) {
164
+ bandwidth.push((u - l) / m);
165
+ percentB.push(u === l ? 0.5 : (nav[i] - l) / (u - l));
166
+ } else {
167
+ bandwidth.push(null);
168
+ percentB.push(null);
169
+ }
170
+ }
171
+ return { middle, upper, lower, bandwidth, percentB };
172
+ }
173
+ function donchianChannel(nav, period = 20) {
174
+ const upper = [];
175
+ const lower = [];
176
+ const middle = [];
177
+ for (let i = 0; i < nav.length; i++) {
178
+ if (i < period - 1) {
179
+ upper.push(null);
180
+ lower.push(null);
181
+ middle.push(null);
182
+ continue;
183
+ }
184
+ const slice = nav.slice(i - period + 1, i + 1);
185
+ const high = Math.max(...slice);
186
+ const low = Math.min(...slice);
187
+ upper.push(high);
188
+ lower.push(low);
189
+ middle.push((high + low) / 2);
190
+ }
191
+ return { upper, lower, middle };
192
+ }
193
+ function keltnerChannel(nav, emaPeriod = 20, atrPeriod = 10, multiplier = 2) {
194
+ const emaResult = ema(nav, emaPeriod);
195
+ const atrResult = atr(nav, atrPeriod);
196
+ const upper = [];
197
+ const lower = [];
198
+ for (let i = 0; i < nav.length; i++) {
199
+ const e = emaResult.values[i];
200
+ const a = atrResult.values[i];
201
+ if (e != null && a != null) {
202
+ upper.push(e + multiplier * a);
203
+ lower.push(e - multiplier * a);
204
+ } else {
205
+ upper.push(null);
206
+ lower.push(null);
207
+ }
208
+ }
209
+ return { upper, lower, middle: emaResult.values };
210
+ }
211
+ function adx(nav, period = 14) {
212
+ const hlc = navToHLC(nav);
213
+ const raw = ADX.calculate({
214
+ high: hlc.map((h) => h.high),
215
+ low: hlc.map((h) => h.low),
216
+ close: hlc.map((h) => h.close),
217
+ period
218
+ });
219
+ const adxArr = padLeft(raw.map((r) => r.adx), nav.length);
220
+ const plusDI = padLeft(raw.map((r) => r.pdi), nav.length);
221
+ const minusDI = padLeft(raw.map((r) => r.mdi), nav.length);
222
+ return {
223
+ adx: adxArr,
224
+ plusDI,
225
+ minusDI,
226
+ currentADX: lastNonNull(adxArr)
227
+ };
228
+ }
229
+ function atr(nav, period = 14) {
230
+ const hlc = navToHLC(nav);
231
+ const raw = ATR.calculate({
232
+ high: hlc.map((h) => h.high),
233
+ low: hlc.map((h) => h.low),
234
+ close: hlc.map((h) => h.close),
235
+ period
236
+ });
237
+ const values = padLeft(raw, nav.length);
238
+ return { values, current: lastNonNull(values), period, type: "SMA" };
239
+ }
240
+ function cci(nav, period = 20) {
241
+ const hlc = navToHLC(nav);
242
+ const raw = CCI.calculate({
243
+ high: hlc.map((h) => h.high),
244
+ low: hlc.map((h) => h.low),
245
+ close: hlc.map((h) => h.close),
246
+ period
247
+ });
248
+ const values = padLeft(raw, nav.length);
249
+ return { values, current: lastNonNull(values), period, type: "SMA" };
250
+ }
251
+ function roc(nav, period = 12) {
252
+ const raw = ROC.calculate({ values: nav, period });
253
+ const values = padLeft(raw, nav.length);
254
+ return { values, current: lastNonNull(values), period, type: "SMA" };
255
+ }
256
+ function momentum(nav, period = 10) {
257
+ const values = [];
258
+ for (let i = 0; i < nav.length; i++) {
259
+ if (i < period) {
260
+ values.push(null);
261
+ } else {
262
+ values.push(nav[i] - nav[i - period]);
263
+ }
264
+ }
265
+ return { values, current: lastNonNull(values), period, type: "SMA" };
266
+ }
267
+ function williamsR(nav, period = 14) {
268
+ const hlc = navToHLC(nav);
269
+ const raw = WilliamsR.calculate({
270
+ high: hlc.map((h) => h.high),
271
+ low: hlc.map((h) => h.low),
272
+ close: hlc.map((h) => h.close),
273
+ period
274
+ });
275
+ const values = padLeft(raw, nav.length);
276
+ return { values, current: lastNonNull(values), period, type: "SMA" };
277
+ }
278
+ function stochasticRSI(nav, rsiPeriod = 14, stochPeriod = 14, kPeriod = 3, dPeriod = 3) {
279
+ const raw = StochasticRSI.calculate({
280
+ values: nav,
281
+ rsiPeriod,
282
+ stochasticPeriod: stochPeriod,
283
+ kPeriod,
284
+ dPeriod
285
+ });
286
+ const k = padLeft(raw.map((r) => r.k), nav.length);
287
+ const d = padLeft(raw.map((r) => r.d), nav.length);
288
+ const j = k.map((kVal, i) => {
289
+ const dVal = d[i];
290
+ return kVal != null && dVal != null ? 3 * kVal - 2 * dVal : null;
291
+ });
292
+ return { k, d, j, currentK: lastNonNull(k), currentD: lastNonNull(d), currentJ: lastNonNull(j) };
293
+ }
294
+ function sar(nav, step = 0.02, max = 0.2) {
295
+ const hlc = navToHLC(nav);
296
+ const raw = PSAR.calculate({
297
+ high: hlc.map((h) => h.high),
298
+ low: hlc.map((h) => h.low),
299
+ step,
300
+ max
301
+ });
302
+ const values = padLeft(raw, nav.length);
303
+ return { values, current: lastNonNull(values) };
304
+ }
305
+ function trix(nav, period = 12) {
306
+ const ema1 = EMA.calculate({ values: nav, period });
307
+ const ema1Vals = ema1.filter((v) => v !== void 0);
308
+ const ema2 = EMA.calculate({ values: ema1Vals, period });
309
+ const ema2Vals = ema2.filter((v) => v !== void 0);
310
+ const ema3 = EMA.calculate({ values: ema2Vals, period });
311
+ const values = new Array(nav.length).fill(null);
312
+ const offset = nav.length - ema3.length;
313
+ for (let i = 1; i < ema3.length; i++) {
314
+ const prev = ema3[i - 1];
315
+ const curr = ema3[i];
316
+ if (prev != null && curr != null && prev !== 0) {
317
+ values[offset + i] = (curr - prev) / prev * 100;
318
+ }
319
+ }
320
+ return { values, current: lastNonNull(values), period, type: "SMA" };
321
+ }
322
+ function dpo(nav, period = 20) {
323
+ const smaResult = sma(nav, period);
324
+ const shift = Math.floor(period / 2) + 1;
325
+ const values = [];
326
+ for (let i = 0; i < nav.length; i++) {
327
+ const smaIdx = i + shift;
328
+ if (smaIdx < nav.length && smaResult.values[smaIdx] != null) {
329
+ values.push(nav[i] - smaResult.values[smaIdx]);
330
+ } else {
331
+ values.push(null);
332
+ }
333
+ }
334
+ return { values, current: lastNonNull(values), period, type: "SMA" };
335
+ }
336
+ function bias(nav, period = 20) {
337
+ const maResult = sma(nav, period);
338
+ const values = [];
339
+ for (let i = 0; i < nav.length; i++) {
340
+ const m = maResult.values[i];
341
+ if (m != null && m !== 0) {
342
+ values.push((nav[i] - m) / m * 100);
343
+ } else {
344
+ values.push(null);
345
+ }
346
+ }
347
+ return { values, current: lastNonNull(values), period, type: "SMA" };
348
+ }
349
+ function navPercentile(nav, lookback) {
350
+ const window = lookback ? nav.slice(-lookback) : nav;
351
+ const current = nav[nav.length - 1];
352
+ const sorted = [...window].sort((a, b) => a - b);
353
+ let rank = 0;
354
+ for (const v of sorted) {
355
+ if (v < current) rank++;
356
+ else if (v === current) rank += 0.5;
357
+ }
358
+ return rank / sorted.length * 100;
359
+ }
360
+ function detectCrossSignal(fastMA, slowMA, lookback = 5) {
361
+ const fVals = fastMA.values;
362
+ const sVals = slowMA.values;
363
+ const len = Math.min(fVals.length, sVals.length);
364
+ for (let i = len - 1; i >= Math.max(1, len - lookback); i--) {
365
+ const fCurr = fVals[i];
366
+ const fPrev = fVals[i - 1];
367
+ const sCurr = sVals[i];
368
+ const sPrev = sVals[i - 1];
369
+ if (fCurr == null || fPrev == null || sCurr == null || sPrev == null) continue;
370
+ if (fPrev <= sPrev && fCurr > sCurr) {
371
+ return { type: "golden_cross", index: i, fastValue: fCurr, slowValue: sCurr };
372
+ }
373
+ if (fPrev >= sPrev && fCurr < sCurr) {
374
+ return { type: "death_cross", index: i, fastValue: fCurr, slowValue: sCurr };
375
+ }
376
+ }
377
+ return { type: "none", index: -1, fastValue: 0, slowValue: 0 };
378
+ }
379
+ function detectMAAlignment(maList) {
380
+ const maValues = maList.map((m) => m.current ?? 0);
381
+ const allValid = maList.every((m) => m.current != null);
382
+ if (!allValid) {
383
+ return { alignment: "neutral", maValues, divergence: 0 };
384
+ }
385
+ let isBullish = true;
386
+ let isBearish = true;
387
+ for (let i = 1; i < maValues.length; i++) {
388
+ if (maValues[i] >= maValues[i - 1]) isBullish = false;
389
+ if (maValues[i] <= maValues[i - 1]) isBearish = false;
390
+ }
391
+ const mean = maValues.reduce((a, b) => a + b, 0) / maValues.length;
392
+ const variance = maValues.reduce((sum, v) => sum + (v - mean) ** 2, 0) / maValues.length;
393
+ const divergence = Math.sqrt(variance);
394
+ const alignment = isBullish ? "bullish" : isBearish ? "bearish" : "neutral";
395
+ return { alignment, maValues, divergence };
396
+ }
397
+ function massIndex(nav, emaPeriod = 9, sumPeriod = 25) {
398
+ const ema1 = EMA.calculate({ values: nav, period: emaPeriod });
399
+ const ema1Vals = ema1.filter((v) => v !== void 0);
400
+ const ema2 = EMA.calculate({ values: ema1Vals, period: emaPeriod });
401
+ const ratios = [];
402
+ for (let i = 0; i < ema2.length; i++) {
403
+ const e1 = ema1Vals[i + (ema1Vals.length - ema2.length)];
404
+ const e2 = ema2[i];
405
+ if (e1 != null && e2 != null && e2 !== 0) {
406
+ ratios.push(e1 / e2);
407
+ } else {
408
+ ratios.push(1);
409
+ }
410
+ }
411
+ const values = new Array(nav.length).fill(null);
412
+ const offset = nav.length - ratios.length;
413
+ for (let i = sumPeriod - 1; i < ratios.length; i++) {
414
+ let sum = 0;
415
+ for (let j = i - sumPeriod + 1; j <= i; j++) {
416
+ sum += ratios[j];
417
+ }
418
+ values[offset + i] = sum;
419
+ }
420
+ return { values, current: lastNonNull(values), period: sumPeriod, type: "SMA" };
421
+ }
422
+
423
+ const TRADING_DAYS_PER_YEAR = 242;
424
+ function navToReturns(nav) {
425
+ const returns = [];
426
+ for (let i = 1; i < nav.length; i++) {
427
+ returns.push((nav[i] - nav[i - 1]) / nav[i - 1]);
428
+ }
429
+ return returns;
430
+ }
431
+ function annualizeReturn(dailyReturns) {
432
+ if (dailyReturns.length === 0) return 0;
433
+ const cumulative = dailyReturns.reduce((acc, r) => acc * (1 + r), 1);
434
+ const years = dailyReturns.length / TRADING_DAYS_PER_YEAR;
435
+ if (years <= 0 || cumulative <= 0) return 0;
436
+ return Math.pow(cumulative, 1 / years) - 1;
437
+ }
438
+ function totalReturn(nav) {
439
+ if (nav.length < 2) return 0;
440
+ return (nav[nav.length - 1] - nav[0]) / nav[0];
441
+ }
442
+ function annualizedVolatility(returns) {
443
+ if (returns.length < 2) return 0;
444
+ return ss.standardDeviation(returns) * Math.sqrt(TRADING_DAYS_PER_YEAR);
445
+ }
446
+ function downsideVolatility(returns, riskFreeRate = 0) {
447
+ const downsideReturns = returns.filter((r) => r < riskFreeRate / TRADING_DAYS_PER_YEAR);
448
+ if (downsideReturns.length < 2) return 0;
449
+ const deviations = downsideReturns.map((r) => Math.pow(r - riskFreeRate / TRADING_DAYS_PER_YEAR, 2));
450
+ const meanSqDev = deviations.reduce((a, b) => a + b, 0) / returns.length;
451
+ return Math.sqrt(meanSqDev) * Math.sqrt(TRADING_DAYS_PER_YEAR);
452
+ }
453
+ function rollingVolatility(returns, window = 20) {
454
+ const result = [];
455
+ for (let i = 0; i < returns.length; i++) {
456
+ if (i < window - 1) {
457
+ result.push(null);
458
+ } else {
459
+ const slice = returns.slice(i - window + 1, i + 1);
460
+ result.push(ss.standardDeviation(slice) * Math.sqrt(TRADING_DAYS_PER_YEAR));
461
+ }
462
+ }
463
+ return result;
464
+ }
465
+ function volatilityCone(returns, windows = [5, 10, 20, 60, 120], quantiles = [0.1, 0.25, 0.5, 0.75, 0.9]) {
466
+ const cone = /* @__PURE__ */ new Map();
467
+ for (const w of windows) {
468
+ const vols = [];
469
+ for (let i = w - 1; i < returns.length; i++) {
470
+ const slice = returns.slice(i - w + 1, i + 1);
471
+ vols.push(ss.standardDeviation(slice) * Math.sqrt(TRADING_DAYS_PER_YEAR));
472
+ }
473
+ const sorted = [...vols].sort((a, b) => a - b);
474
+ const qMap = /* @__PURE__ */ new Map();
475
+ for (const q of quantiles) {
476
+ qMap.set(q, ss.quantileSorted(sorted, q));
477
+ }
478
+ cone.set(w, qMap);
479
+ }
480
+ return cone;
481
+ }
482
+ function maxDrawdown(nav, dates) {
483
+ if (nav.length < 2) {
484
+ return {
485
+ maxDrawdown: 0,
486
+ peakIndex: 0,
487
+ troughIndex: 0,
488
+ peakDate: null,
489
+ troughDate: null,
490
+ recoveryDate: null,
491
+ durationDays: 0,
492
+ recoveryDays: null,
493
+ drawdownSeries: []
494
+ };
495
+ }
496
+ const drawdownSeries = [];
497
+ let peak = nav[0];
498
+ let peakIdx = 0;
499
+ let maxDD = 0;
500
+ let maxPeakIdx = 0;
501
+ let maxTroughIdx = 0;
502
+ for (let i = 0; i < nav.length; i++) {
503
+ if (nav[i] > peak) {
504
+ peak = nav[i];
505
+ peakIdx = i;
506
+ }
507
+ const dd = (nav[i] - peak) / peak;
508
+ drawdownSeries.push(dd);
509
+ if (dd < maxDD) {
510
+ maxDD = dd;
511
+ maxPeakIdx = peakIdx;
512
+ maxTroughIdx = i;
513
+ }
514
+ }
515
+ let recoveryIdx = null;
516
+ const peakValue = nav[maxPeakIdx];
517
+ for (let i = maxTroughIdx + 1; i < nav.length; i++) {
518
+ if (nav[i] >= peakValue) {
519
+ recoveryIdx = i;
520
+ break;
521
+ }
522
+ }
523
+ return {
524
+ maxDrawdown: maxDD,
525
+ peakIndex: maxPeakIdx,
526
+ troughIndex: maxTroughIdx,
527
+ peakDate: dates ? dates[maxPeakIdx] ?? null : null,
528
+ troughDate: dates ? dates[maxTroughIdx] ?? null : null,
529
+ recoveryDate: recoveryIdx != null && dates ? dates[recoveryIdx] ?? null : null,
530
+ durationDays: maxTroughIdx - maxPeakIdx,
531
+ recoveryDays: recoveryIdx != null ? recoveryIdx - maxTroughIdx : null,
532
+ drawdownSeries
533
+ };
534
+ }
535
+ function maxDrawdownDuration(nav) {
536
+ let maxDuration = 0;
537
+ let currentDuration = 0;
538
+ let peak = nav[0];
539
+ for (let i = 0; i < nav.length; i++) {
540
+ if (nav[i] >= peak) {
541
+ peak = nav[i];
542
+ currentDuration = 0;
543
+ } else {
544
+ currentDuration++;
545
+ maxDuration = Math.max(maxDuration, currentDuration);
546
+ }
547
+ }
548
+ return maxDuration;
549
+ }
550
+ function calculateVaR(returns, confidence = 0.95, method = "historical") {
551
+ if (returns.length < 2) return 0;
552
+ if (method === "historical") {
553
+ const sorted = [...returns].sort((a, b) => a - b);
554
+ return ss.quantileSorted(sorted, 1 - confidence);
555
+ } else {
556
+ const mean = ss.mean(returns);
557
+ const std = ss.standardDeviation(returns);
558
+ const z = jStat.normal.inv(1 - confidence, 0, 1);
559
+ return mean + z * std;
560
+ }
561
+ }
562
+ function calculateCVaR(returns, confidence = 0.95) {
563
+ if (returns.length < 2) return 0;
564
+ const varValue = calculateVaR(returns, confidence, "historical");
565
+ const tailReturns = returns.filter((r) => r <= varValue);
566
+ if (tailReturns.length === 0) return varValue;
567
+ return ss.mean(tailReturns);
568
+ }
569
+ function riskMetrics(nav, riskFreeRate = 0.025) {
570
+ const returns = navToReturns(nav);
571
+ const dd = maxDrawdown(nav);
572
+ return {
573
+ annualizedVolatility: annualizedVolatility(returns),
574
+ downsideVolatility: downsideVolatility(returns, riskFreeRate),
575
+ maxDrawdown: dd.maxDrawdown,
576
+ maxDrawdownDuration: maxDrawdownDuration(nav),
577
+ var95: calculateVaR(returns, 0.95),
578
+ var99: calculateVaR(returns, 0.99),
579
+ cvar95: calculateCVaR(returns, 0.95),
580
+ cvar99: calculateCVaR(returns, 0.99)
581
+ };
582
+ }
583
+ function sharpeRatio(nav, riskFreeRate = 0.025) {
584
+ const returns = navToReturns(nav);
585
+ const annReturn = annualizeReturn(returns);
586
+ const annVol = annualizedVolatility(returns);
587
+ if (annVol === 0) return 0;
588
+ return (annReturn - riskFreeRate) / annVol;
589
+ }
590
+ function sortinoRatio(nav, riskFreeRate = 0.025) {
591
+ const returns = navToReturns(nav);
592
+ const annReturn = annualizeReturn(returns);
593
+ const dv = downsideVolatility(returns, riskFreeRate);
594
+ if (dv === 0) return 0;
595
+ return (annReturn - riskFreeRate) / dv;
596
+ }
597
+ function calmarRatio(nav) {
598
+ const returns = navToReturns(nav);
599
+ const annReturn = annualizeReturn(returns);
600
+ const dd = maxDrawdown(nav);
601
+ if (dd.maxDrawdown === 0) return 0;
602
+ return annReturn / Math.abs(dd.maxDrawdown);
603
+ }
604
+ function treynorRatio(nav, benchmarkNav, riskFreeRate = 0.025) {
605
+ const fundReturns = navToReturns(nav);
606
+ const benchReturns = navToReturns(benchmarkNav);
607
+ const minLen = Math.min(fundReturns.length, benchReturns.length);
608
+ const fRet = fundReturns.slice(-minLen);
609
+ const bRet = benchReturns.slice(-minLen);
610
+ const beta = calculateBeta(fRet, bRet);
611
+ if (beta === 0) return null;
612
+ const annReturn = annualizeReturn(fRet);
613
+ return (annReturn - riskFreeRate) / beta;
614
+ }
615
+ function omegaRatio(nav, threshold = 0) {
616
+ const returns = navToReturns(nav);
617
+ const dailyThreshold = threshold / TRADING_DAYS_PER_YEAR;
618
+ let gains = 0;
619
+ let losses = 0;
620
+ for (const r of returns) {
621
+ if (r > dailyThreshold) gains += r - dailyThreshold;
622
+ else losses += dailyThreshold - r;
623
+ }
624
+ if (losses === 0) return gains > 0 ? Infinity : 0;
625
+ return gains / losses;
626
+ }
627
+ function winRate(returns) {
628
+ if (returns.length === 0) return 0;
629
+ const wins = returns.filter((r) => r > 0).length;
630
+ return wins / returns.length;
631
+ }
632
+ function profitLossRatio(returns) {
633
+ const wins = returns.filter((r) => r > 0);
634
+ const losses = returns.filter((r) => r < 0);
635
+ if (losses.length === 0) return wins.length > 0 ? Infinity : 0;
636
+ const avgWin = wins.length > 0 ? ss.mean(wins) : 0;
637
+ const avgLoss = Math.abs(ss.mean(losses));
638
+ if (avgLoss === 0) return 0;
639
+ return avgWin / avgLoss;
640
+ }
641
+ function profitFactor(returns) {
642
+ const totalWins = returns.filter((r) => r > 0).reduce((a, b) => a + b, 0);
643
+ const totalLosses = Math.abs(returns.filter((r) => r < 0).reduce((a, b) => a + b, 0));
644
+ if (totalLosses === 0) return totalWins > 0 ? Infinity : 0;
645
+ return totalWins / totalLosses;
646
+ }
647
+ function consecutiveWinLoss(returns) {
648
+ let maxWins = 0, maxLosses = 0;
649
+ let curWins = 0, curLosses = 0;
650
+ for (const r of returns) {
651
+ if (r > 0) {
652
+ curWins++;
653
+ curLosses = 0;
654
+ maxWins = Math.max(maxWins, curWins);
655
+ } else if (r < 0) {
656
+ curLosses++;
657
+ curWins = 0;
658
+ maxLosses = Math.max(maxLosses, curLosses);
659
+ } else {
660
+ curWins = 0;
661
+ curLosses = 0;
662
+ }
663
+ }
664
+ return { maxWins, maxLosses };
665
+ }
666
+ function performanceMetrics(nav, riskFreeRate = 0.025) {
667
+ const returns = navToReturns(nav);
668
+ const consec = consecutiveWinLoss(returns);
669
+ const dd = maxDrawdown(nav);
670
+ const annReturn = annualizeReturn(returns);
671
+ const annVol = annualizedVolatility(returns);
672
+ const dv = downsideVolatility(returns, riskFreeRate);
673
+ return {
674
+ totalReturn: totalReturn(nav),
675
+ annualizedReturn: annReturn,
676
+ sharpeRatio: annVol > 0 ? (annReturn - riskFreeRate) / annVol : 0,
677
+ sortinoRatio: dv > 0 ? (annReturn - riskFreeRate) / dv : 0,
678
+ calmarRatio: dd.maxDrawdown !== 0 ? annReturn / Math.abs(dd.maxDrawdown) : 0,
679
+ treynorRatio: null,
680
+ // 需要基准数据,单独计算
681
+ omegaRatio: omegaRatio(nav),
682
+ winRate: winRate(returns),
683
+ profitLossRatio: profitLossRatio(returns),
684
+ profitFactor: profitFactor(returns),
685
+ maxConsecutiveWins: consec.maxWins,
686
+ maxConsecutiveLosses: consec.maxLosses
687
+ };
688
+ }
689
+ function calculateBeta(fundReturns, benchmarkReturns) {
690
+ const minLen = Math.min(fundReturns.length, benchmarkReturns.length);
691
+ const f = fundReturns.slice(-minLen);
692
+ const b = benchmarkReturns.slice(-minLen);
693
+ const cov = ss.sampleCovariance(f, b);
694
+ const bVar = ss.variance(b);
695
+ return bVar > 0 ? cov / bVar : 0;
696
+ }
697
+ function calculateAlpha(fundReturns, benchmarkReturns, riskFreeRate = 0.025) {
698
+ const minLen = Math.min(fundReturns.length, benchmarkReturns.length);
699
+ const f = fundReturns.slice(-minLen);
700
+ const b = benchmarkReturns.slice(-minLen);
701
+ const fAnnReturn = annualizeReturn(f);
702
+ const bAnnReturn = annualizeReturn(b);
703
+ const beta = calculateBeta(f, b);
704
+ return fAnnReturn - (riskFreeRate + beta * (bAnnReturn - riskFreeRate));
705
+ }
706
+ function trackingError(fundReturns, benchmarkReturns) {
707
+ const minLen = Math.min(fundReturns.length, benchmarkReturns.length);
708
+ const f = fundReturns.slice(-minLen);
709
+ const b = benchmarkReturns.slice(-minLen);
710
+ const excessReturns = f.map((r, i) => r - b[i]);
711
+ return ss.standardDeviation(excessReturns) * Math.sqrt(TRADING_DAYS_PER_YEAR);
712
+ }
713
+ function informationRatio(fundReturns, benchmarkReturns) {
714
+ const minLen = Math.min(fundReturns.length, benchmarkReturns.length);
715
+ const f = fundReturns.slice(-minLen);
716
+ const b = benchmarkReturns.slice(-minLen);
717
+ const excessReturns = f.map((r, i) => r - b[i]);
718
+ const meanExcess = ss.mean(excessReturns) * TRADING_DAYS_PER_YEAR;
719
+ const te = ss.standardDeviation(excessReturns) * Math.sqrt(TRADING_DAYS_PER_YEAR);
720
+ return te > 0 ? meanExcess / te : 0;
721
+ }
722
+ function benchmarkMetrics(fundNav, benchmarkNav, riskFreeRate = 0.025) {
723
+ const fundReturns = navToReturns(fundNav);
724
+ const benchReturns = navToReturns(benchmarkNav);
725
+ const minLen = Math.min(fundReturns.length, benchReturns.length);
726
+ const f = fundReturns.slice(-minLen);
727
+ const b = benchReturns.slice(-minLen);
728
+ const beta = calculateBeta(f, b);
729
+ const alpha = calculateAlpha(f, b, riskFreeRate);
730
+ const te = trackingError(f, b);
731
+ const ir = te > 0 ? informationRatio(f, b) : 0;
732
+ const corr = ss.sampleCorrelation(f, b);
733
+ const rSquared = corr * corr;
734
+ return {
735
+ alpha,
736
+ beta,
737
+ trackingError: te,
738
+ informationRatio: ir,
739
+ correlation: corr,
740
+ rSquared
741
+ };
742
+ }
743
+
744
+ function statisticalFeatures(nav) {
745
+ const returns = navToReturns(nav);
746
+ if (returns.length < 4) {
747
+ return {
748
+ mean: 0,
749
+ median: 0,
750
+ stdDev: 0,
751
+ skewness: 0,
752
+ kurtosis: 0,
753
+ min: 0,
754
+ max: 0,
755
+ range: 0,
756
+ coefficientOfVariation: 0,
757
+ jarqueBera: 0
758
+ };
759
+ }
760
+ const mean = ss.mean(returns);
761
+ const median = ss.median(returns);
762
+ const stdDev = ss.standardDeviation(returns);
763
+ const skewness = ss.sampleSkewness(returns);
764
+ const kurtosis = ss.sampleKurtosis(returns);
765
+ const min = ss.min(returns);
766
+ const max = ss.max(returns);
767
+ const range = max - min;
768
+ const coefficientOfVariation = mean !== 0 ? stdDev / Math.abs(mean) : 0;
769
+ const n = returns.length;
770
+ const jarqueBera = n / 6 * (skewness * skewness + kurtosis * kurtosis / 4);
771
+ return {
772
+ mean,
773
+ median,
774
+ stdDev,
775
+ skewness,
776
+ kurtosis,
777
+ min,
778
+ max,
779
+ range,
780
+ coefficientOfVariation,
781
+ jarqueBera
782
+ };
783
+ }
784
+ function navStatisticalFeatures(nav) {
785
+ if (nav.length < 4) {
786
+ return {
787
+ mean: 0,
788
+ median: 0,
789
+ stdDev: 0,
790
+ skewness: 0,
791
+ kurtosis: 0,
792
+ min: 0,
793
+ max: 0,
794
+ range: 0,
795
+ coefficientOfVariation: 0,
796
+ jarqueBera: 0
797
+ };
798
+ }
799
+ const mean = ss.mean(nav);
800
+ const median = ss.median(nav);
801
+ const stdDev = ss.standardDeviation(nav);
802
+ const skewness = ss.sampleSkewness(nav);
803
+ const kurtosis = ss.sampleKurtosis(nav);
804
+ const min = ss.min(nav);
805
+ const max = ss.max(nav);
806
+ const range = max - min;
807
+ const coefficientOfVariation = mean !== 0 ? stdDev / Math.abs(mean) : 0;
808
+ const n = nav.length;
809
+ const jarqueBera = n / 6 * (skewness * skewness + kurtosis * kurtosis / 4);
810
+ return {
811
+ mean,
812
+ median,
813
+ stdDev,
814
+ skewness,
815
+ kurtosis,
816
+ min,
817
+ max,
818
+ range,
819
+ coefficientOfVariation,
820
+ jarqueBera
821
+ };
822
+ }
823
+ function hurstExponent(nav, minWindowSize = 16, maxWindowSize, numPoints = 10) {
824
+ const returns = navToReturns(nav);
825
+ const n = returns.length;
826
+ if (n < minWindowSize * 2) {
827
+ return {
828
+ hurstExponent: 0.5,
829
+ interpretation: "random_walk",
830
+ dataPoints: [],
831
+ rSquared: 0
832
+ };
833
+ }
834
+ const maxWin = maxWindowSize ?? Math.floor(n / 2);
835
+ const dataPoints = [];
836
+ const logMin = Math.log(minWindowSize);
837
+ const logMax = Math.log(maxWin);
838
+ for (let p = 0; p < numPoints; p++) {
839
+ const logWindowSize = logMin + p / (numPoints - 1) * (logMax - logMin);
840
+ const windowSize = Math.floor(Math.exp(logWindowSize));
841
+ if (windowSize < 2 || windowSize > n) continue;
842
+ const numSubPeriods = Math.floor(n / windowSize);
843
+ if (numSubPeriods < 1) continue;
844
+ let totalRS = 0;
845
+ let validCount = 0;
846
+ for (let s = 0; s < numSubPeriods; s++) {
847
+ const start = s * windowSize;
848
+ const subPeriod = returns.slice(start, start + windowSize);
849
+ const mean = ss.mean(subPeriod);
850
+ const std = ss.standardDeviation(subPeriod);
851
+ if (std === 0) continue;
852
+ const cumDeviations = [];
853
+ let cumDev = 0;
854
+ for (const r of subPeriod) {
855
+ cumDev += r - mean;
856
+ cumDeviations.push(cumDev);
857
+ }
858
+ const range = Math.max(...cumDeviations) - Math.min(...cumDeviations);
859
+ const rs = range / std;
860
+ totalRS += rs;
861
+ validCount++;
862
+ }
863
+ if (validCount > 0) {
864
+ const avgRS = totalRS / validCount;
865
+ dataPoints.push({ logN: Math.log(windowSize), logRS: Math.log(avgRS) });
866
+ }
867
+ }
868
+ if (dataPoints.length < 3) {
869
+ return {
870
+ hurstExponent: 0.5,
871
+ interpretation: "random_walk",
872
+ dataPoints,
873
+ rSquared: 0
874
+ };
875
+ }
876
+ const regressionData = dataPoints.map((d) => [d.logN, d.logRS]);
877
+ const regression = ss.linearRegression(regressionData);
878
+ const H = regression.m;
879
+ const predicted = dataPoints.map((d) => regression.m * d.logN + regression.b);
880
+ const actual = dataPoints.map((d) => d.logRS);
881
+ const meanActual = ss.mean(actual);
882
+ const ssTotal = actual.reduce((sum, v) => sum + (v - meanActual) ** 2, 0);
883
+ const ssResidual = actual.reduce((sum, v, i) => sum + (v - predicted[i]) ** 2, 0);
884
+ const rSquared = ssTotal > 0 ? 1 - ssResidual / ssTotal : 0;
885
+ let interpretation;
886
+ if (H > 0.55) interpretation = "trending";
887
+ else if (H < 0.45) interpretation = "mean_reverting";
888
+ else interpretation = "random_walk";
889
+ return {
890
+ hurstExponent: Math.max(0, Math.min(1, H)),
891
+ // 限制在 [0, 1]
892
+ interpretation,
893
+ dataPoints,
894
+ rSquared
895
+ };
896
+ }
897
+ function autocorrelation(nav, maxLag = 20) {
898
+ const returns = navToReturns(nav);
899
+ const n = returns.length;
900
+ if (n < maxLag + 2) {
901
+ maxLag = Math.max(1, n - 2);
902
+ }
903
+ const mean = ss.mean(returns);
904
+ const variance = returns.reduce((sum, r) => sum + (r - mean) ** 2, 0) / n;
905
+ const coefficients = [];
906
+ for (let lag = 0; lag <= maxLag; lag++) {
907
+ if (variance === 0) {
908
+ coefficients.push(lag === 0 ? 1 : 0);
909
+ continue;
910
+ }
911
+ let cov = 0;
912
+ for (let i = 0; i < n - lag; i++) {
913
+ cov += (returns[i] - mean) * (returns[i + lag] - mean);
914
+ }
915
+ cov /= n;
916
+ coefficients.push(cov / variance);
917
+ }
918
+ let interpretation;
919
+ const lag1 = coefficients.length > 1 ? coefficients[1] : 0;
920
+ if (lag1 > 0.05) interpretation = "persistent";
921
+ else if (lag1 < -0.05) interpretation = "anti_persistent";
922
+ else interpretation = "no_correlation";
923
+ return { coefficients, maxLag, interpretation };
924
+ }
925
+ function ljungBoxTest(returns, maxLag = 10) {
926
+ const n = returns.length;
927
+ const mean = ss.mean(returns);
928
+ const variance = returns.reduce((sum, r) => sum + (r - mean) ** 2, 0) / n;
929
+ if (variance === 0) return { qStatistic: 0, approximatePValue: 1 };
930
+ const acf = [];
931
+ for (let k = 1; k <= maxLag; k++) {
932
+ let cov = 0;
933
+ for (let i = 0; i < n - k; i++) {
934
+ cov += (returns[i] - mean) * (returns[i + k] - mean);
935
+ }
936
+ cov /= n;
937
+ acf.push(cov / variance);
938
+ }
939
+ let q = 0;
940
+ for (let k = 0; k < maxLag; k++) {
941
+ q += acf[k] * acf[k] / (n - k - 1);
942
+ }
943
+ q *= n * (n + 2);
944
+ const df = maxLag;
945
+ const z = Math.pow(q / df, 1 / 3) - (1 - 2 / (9 * df));
946
+ const se = Math.sqrt(2 / (9 * df));
947
+ const zScore = z / se;
948
+ const pValue = 1 - normalCDF(zScore);
949
+ return { qStatistic: q, approximatePValue: Math.max(0, Math.min(1, pValue)) };
950
+ }
951
+ function normalCDF(x) {
952
+ const a1 = 0.254829592;
953
+ const a2 = -0.284496736;
954
+ const a3 = 1.421413741;
955
+ const a4 = -1.453152027;
956
+ const a5 = 1.061405429;
957
+ const p = 0.3275911;
958
+ const sign = x < 0 ? -1 : 1;
959
+ x = Math.abs(x) / Math.sqrt(2);
960
+ const t = 1 / (1 + p * x);
961
+ const y = 1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
962
+ return 0.5 * (1 + sign * y);
963
+ }
964
+ function returnQuantiles(nav, quantiles = [0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]) {
965
+ const returns = navToReturns(nav);
966
+ const sorted = [...returns].sort((a, b) => a - b);
967
+ const result = /* @__PURE__ */ new Map();
968
+ for (const q of quantiles) {
969
+ result.set(q, ss.quantileSorted(sorted, q));
970
+ }
971
+ return result;
972
+ }
973
+ function rollingSkewness(returns, window = 60) {
974
+ const result = [];
975
+ for (let i = 0; i < returns.length; i++) {
976
+ if (i < window - 1) {
977
+ result.push(null);
978
+ } else {
979
+ const slice = returns.slice(i - window + 1, i + 1);
980
+ result.push(ss.sampleSkewness(slice));
981
+ }
982
+ }
983
+ return result;
984
+ }
985
+ function rollingKurtosis(returns, window = 60) {
986
+ const result = [];
987
+ for (let i = 0; i < returns.length; i++) {
988
+ if (i < window - 1) {
989
+ result.push(null);
990
+ } else {
991
+ const slice = returns.slice(i - window + 1, i + 1);
992
+ result.push(ss.sampleKurtosis(slice));
993
+ }
994
+ }
995
+ return result;
996
+ }
997
+ function garch11(returns) {
998
+ const n = returns.length;
999
+ if (n < 30) {
1000
+ const v = ss.variance(returns);
1001
+ return {
1002
+ conditionalVariance: new Array(n).fill(v),
1003
+ nextPeriodForecast: v,
1004
+ omega: v * 0.1,
1005
+ alpha: 0.1,
1006
+ beta: 0.8
1007
+ };
1008
+ }
1009
+ const unconditionalVar = ss.variance(returns);
1010
+ const squaredReturns = returns.map((r) => r * r);
1011
+ const meanSquared = ss.mean(squaredReturns);
1012
+ let autocov = 0;
1013
+ for (let i = 0; i < n - 1; i++) {
1014
+ autocov += (squaredReturns[i] - meanSquared) * (squaredReturns[i + 1] - meanSquared);
1015
+ }
1016
+ autocov /= n;
1017
+ const autoCorr = meanSquared > 0 ? autocov / ss.variance(squaredReturns) : 0;
1018
+ const alphaBeta = Math.max(0.5, Math.min(0.99, autoCorr + 0.8));
1019
+ const alpha = Math.max(0.01, Math.min(0.3, (1 - alphaBeta) * 2));
1020
+ const beta = alphaBeta - alpha;
1021
+ const omega = unconditionalVar * (1 - alpha - beta);
1022
+ const conditionalVariance = [unconditionalVar];
1023
+ for (let i = 1; i < n; i++) {
1024
+ const prevVar = conditionalVariance[i - 1];
1025
+ const newVar = omega + alpha * returns[i - 1] * returns[i - 1] + beta * prevVar;
1026
+ conditionalVariance.push(Math.max(newVar, 1e-10));
1027
+ }
1028
+ const lastVar = conditionalVariance[n - 1];
1029
+ const lastReturn = returns[n - 1];
1030
+ const nextPeriodForecast = omega + alpha * lastReturn * lastReturn + beta * lastVar;
1031
+ return { conditionalVariance, nextPeriodForecast, omega, alpha, beta };
1032
+ }
1033
+
1034
+ function simulateDCA(nav, config, dates) {
1035
+ const { amount, interval = 1, startIndex = 0 } = config;
1036
+ let totalInvested = 0;
1037
+ let totalShares = 0;
1038
+ const investmentDates = [];
1039
+ const valueHistory = [];
1040
+ for (let i = startIndex; i < nav.length; i += interval) {
1041
+ const navPrice = nav[i];
1042
+ if (navPrice <= 0) continue;
1043
+ const shares = amount / navPrice;
1044
+ totalShares += shares;
1045
+ totalInvested += amount;
1046
+ investmentDates.push(i);
1047
+ const currentValue2 = totalShares * navPrice;
1048
+ valueHistory.push(currentValue2);
1049
+ }
1050
+ if (totalInvested === 0 || totalShares === 0) {
1051
+ return {
1052
+ totalInvestments: 0,
1053
+ totalInvested: 0,
1054
+ currentValue: 0,
1055
+ totalShares: 0,
1056
+ averageCost: 0,
1057
+ currentNav: 0,
1058
+ returnRate: 0,
1059
+ profitLoss: 0,
1060
+ irr: null,
1061
+ investmentDates,
1062
+ valueHistory
1063
+ };
1064
+ }
1065
+ const currentNav = nav[nav.length - 1];
1066
+ const currentValue = totalShares * currentNav;
1067
+ const averageCost = totalInvested / totalShares;
1068
+ const returnRate = (currentValue - totalInvested) / totalInvested;
1069
+ const profitLoss = currentValue - totalInvested;
1070
+ const irr = calculateIRR(nav, investmentDates, amount, totalShares, currentNav);
1071
+ return {
1072
+ totalInvestments: investmentDates.length,
1073
+ totalInvested,
1074
+ currentValue,
1075
+ totalShares,
1076
+ averageCost,
1077
+ currentNav,
1078
+ returnRate,
1079
+ profitLoss,
1080
+ irr,
1081
+ investmentDates,
1082
+ valueHistory
1083
+ };
1084
+ }
1085
+ function calculateIRR(nav, investmentDates, amount, totalShares, currentNav) {
1086
+ if (investmentDates.length < 2) return null;
1087
+ const baseDay = investmentDates[0];
1088
+ const cashFlows = [];
1089
+ for (const d of investmentDates) {
1090
+ cashFlows.push({ day: d - baseDay, amount: -amount });
1091
+ }
1092
+ const lastDay = nav.length - 1 - baseDay;
1093
+ cashFlows.push({ day: lastDay, amount: totalShares * currentNav });
1094
+ const T = 242;
1095
+ function npv(r2) {
1096
+ let sum = 0;
1097
+ for (const cf of cashFlows) {
1098
+ const t = cf.day / T;
1099
+ if (t === 0) sum += cf.amount;
1100
+ else sum += cf.amount / Math.pow(1 + r2, t);
1101
+ }
1102
+ return sum;
1103
+ }
1104
+ function npvDerivative(r2) {
1105
+ let sum = 0;
1106
+ for (const cf of cashFlows) {
1107
+ const t = cf.day / T;
1108
+ if (t <= 0) continue;
1109
+ sum -= t * cf.amount / Math.pow(1 + r2, t + 1);
1110
+ }
1111
+ return sum;
1112
+ }
1113
+ let r = 0.1;
1114
+ const maxIterations = 100;
1115
+ const tolerance = 1e-8;
1116
+ for (let i = 0; i < maxIterations; i++) {
1117
+ const f = npv(r);
1118
+ const fPrime = npvDerivative(r);
1119
+ if (Math.abs(fPrime) < 1e-12) break;
1120
+ const newR = r - f / fPrime;
1121
+ if (Math.abs(newR - r) < tolerance) {
1122
+ return newR;
1123
+ }
1124
+ r = newR;
1125
+ if (r < -0.99) r = -0.99;
1126
+ if (r > 10) r = 10;
1127
+ }
1128
+ return bisectionIRR(npv);
1129
+ }
1130
+ function bisectionIRR(npv) {
1131
+ let low = -0.99;
1132
+ let high = 10;
1133
+ const tolerance = 1e-6;
1134
+ const maxIterations = 200;
1135
+ let fLow = npv(low);
1136
+ let fHigh = npv(high);
1137
+ if (fLow * fHigh > 0) return null;
1138
+ for (let i = 0; i < maxIterations; i++) {
1139
+ const mid = (low + high) / 2;
1140
+ const fMid = npv(mid);
1141
+ if (Math.abs(fMid) < tolerance || (high - low) / 2 < tolerance) {
1142
+ return mid;
1143
+ }
1144
+ if (fLow * fMid < 0) {
1145
+ high = mid;
1146
+ fHigh = fMid;
1147
+ } else {
1148
+ low = mid;
1149
+ fLow = fMid;
1150
+ }
1151
+ }
1152
+ return (low + high) / 2;
1153
+ }
1154
+ function takeProfitStopLoss(nav, costPrice, takeProfitThreshold = 0.3, stopLossThreshold = -0.15) {
1155
+ const currentNav = nav[nav.length - 1];
1156
+ const currentPnL = (currentNav - costPrice) / costPrice;
1157
+ return {
1158
+ takeProfitTriggered: currentPnL >= takeProfitThreshold,
1159
+ stopLossTriggered: currentPnL <= stopLossThreshold,
1160
+ currentPnL,
1161
+ distanceToTakeProfit: takeProfitThreshold - currentPnL,
1162
+ distanceToStopLoss: currentPnL - stopLossThreshold
1163
+ };
1164
+ }
1165
+ function trailingStop(nav, trailPercent = 0.1, costPrice) {
1166
+ let peakNav = nav[0];
1167
+ for (const v of nav) {
1168
+ if (v > peakNav) peakNav = v;
1169
+ }
1170
+ const currentNav = nav[nav.length - 1];
1171
+ const drawdownFromPeak = (peakNav - currentNav) / peakNav;
1172
+ const profitFromCost = costPrice ? (currentNav - costPrice) / costPrice : 0;
1173
+ return {
1174
+ triggered: drawdownFromPeak >= trailPercent && currentNav > (costPrice ?? 0),
1175
+ peakNav,
1176
+ currentNav,
1177
+ drawdownFromPeak,
1178
+ profitFromCost
1179
+ };
1180
+ }
1181
+ function safetyMargin(nav) {
1182
+ const peak = Math.max(...nav);
1183
+ const current = nav[nav.length - 1];
1184
+ return (peak - current) / peak;
1185
+ }
1186
+ function pricePosition(nav) {
1187
+ const peak = Math.max(...nav);
1188
+ const trough = Math.min(...nav);
1189
+ const current = nav[nav.length - 1];
1190
+ if (peak === trough) return 0.5;
1191
+ return (current - trough) / (peak - trough);
1192
+ }
1193
+ function smartDCAMultiplier(nav, maPeriod = 250, maxMultiplier = 2, minMultiplier = 0.5) {
1194
+ if (nav.length < maPeriod) return 1;
1195
+ const recent = nav.slice(-maPeriod);
1196
+ const ma = recent.reduce((a, b) => a + b, 0) / maPeriod;
1197
+ const current = nav[nav.length - 1];
1198
+ const deviation = (current - ma) / ma;
1199
+ const multiplier = 1 - deviation * 4;
1200
+ return Math.max(minMultiplier, Math.min(maxMultiplier, multiplier));
1201
+ }
1202
+ function tieredBuySignal(nav, levels = [0.3, 0.2, 0.1]) {
1203
+ const sorted = [...nav].sort((a, b) => a - b);
1204
+ const current = nav[nav.length - 1];
1205
+ let rank = 0;
1206
+ for (const v of sorted) {
1207
+ if (v < current) rank++;
1208
+ else if (v === current) rank += 0.5;
1209
+ }
1210
+ const percentile = rank / sorted.length;
1211
+ for (let i = 0; i < levels.length; i++) {
1212
+ if (percentile <= levels[i]) return levels.length - i;
1213
+ }
1214
+ return 0;
1215
+ }
1216
+ function tieredSellSignal(nav, levels = [0.7, 0.8, 0.9]) {
1217
+ const sorted = [...nav].sort((a, b) => a - b);
1218
+ const current = nav[nav.length - 1];
1219
+ let rank = 0;
1220
+ for (const v of sorted) {
1221
+ if (v < current) rank++;
1222
+ else if (v === current) rank += 0.5;
1223
+ }
1224
+ const percentile = rank / sorted.length;
1225
+ for (let i = levels.length - 1; i >= 0; i--) {
1226
+ if (percentile >= levels[i]) return i + 1;
1227
+ }
1228
+ return 0;
1229
+ }
1230
+ function positionPnL(nav, buyIndex, sellIndex, amount = 1e4) {
1231
+ const sell = sellIndex ?? nav.length - 1;
1232
+ const buyNav = nav[buyIndex];
1233
+ const sellNav = nav[sell];
1234
+ const shares = amount / buyNav;
1235
+ const value = shares * sellNav;
1236
+ const pnl = value - amount;
1237
+ const returnRate = pnl / amount;
1238
+ const holdDays = sell - buyIndex;
1239
+ const annualizedReturn = holdDays > 0 ? Math.pow(1 + returnRate, 242 / holdDays) - 1 : 0;
1240
+ return {
1241
+ buyNav,
1242
+ sellNav,
1243
+ shares,
1244
+ cost: amount,
1245
+ value,
1246
+ pnl,
1247
+ returnRate,
1248
+ holdDays,
1249
+ annualizedReturn
1250
+ };
1251
+ }
1252
+
1253
+ function supportResistance(nav, tolerance = 0.02, minTouches = 3) {
1254
+ if (nav.length < 10) {
1255
+ return { supports: [], resistances: [] };
1256
+ }
1257
+ const current = nav[nav.length - 1];
1258
+ const levelMap = /* @__PURE__ */ new Map();
1259
+ for (let i = 0; i < nav.length; i++) {
1260
+ const isLocalHigh = i > 0 && i < nav.length - 1 && nav[i] >= nav[i - 1] && nav[i] >= nav[i + 1];
1261
+ const isLocalLow = i > 0 && i < nav.length - 1 && nav[i] <= nav[i - 1] && nav[i] <= nav[i + 1];
1262
+ if (isLocalHigh || isLocalLow) {
1263
+ let merged = false;
1264
+ for (const [level, count] of levelMap.entries()) {
1265
+ if (Math.abs(nav[i] - level) / level < tolerance) {
1266
+ levelMap.set(level, count + 1);
1267
+ merged = true;
1268
+ break;
1269
+ }
1270
+ }
1271
+ if (!merged) {
1272
+ levelMap.set(nav[i], 1);
1273
+ }
1274
+ }
1275
+ }
1276
+ for (const price of nav) {
1277
+ let merged = false;
1278
+ for (const [level, count] of levelMap.entries()) {
1279
+ if (Math.abs(price - level) / level < tolerance) {
1280
+ levelMap.set(level, count + 1);
1281
+ merged = true;
1282
+ break;
1283
+ }
1284
+ }
1285
+ if (!merged) {
1286
+ levelMap.set(price, 1);
1287
+ }
1288
+ }
1289
+ const validLevels = Array.from(levelMap.entries()).filter(([, touches]) => touches >= minTouches).map(([level, touches]) => ({
1290
+ level,
1291
+ touches,
1292
+ strength: touches / nav.length
1293
+ })).sort((a, b) => b.strength - a.strength);
1294
+ const supports = validLevels.filter((l) => l.level < current).sort((a, b) => b.level - a.level);
1295
+ const resistances = validLevels.filter((l) => l.level > current).sort((a, b) => a.level - b.level);
1296
+ return { supports, resistances };
1297
+ }
1298
+ function doubleBottomTop(nav, lookback = 60, tolerance = 0.03, minDistance = 10) {
1299
+ const noResult = {
1300
+ type: "none",
1301
+ firstPointIndex: null,
1302
+ secondPointIndex: null,
1303
+ necklineIndex: null,
1304
+ necklinePrice: null,
1305
+ breakout: false,
1306
+ confidence: 0
1307
+ };
1308
+ if (nav.length < lookback || lookback < minDistance * 2) return noResult;
1309
+ const window = nav.slice(-lookback);
1310
+ const offset = nav.length - lookback;
1311
+ const localMins = [];
1312
+ const localMaxs = [];
1313
+ for (let i = 2; i < window.length - 2; i++) {
1314
+ if (window[i] <= window[i - 1] && window[i] <= window[i + 1] && window[i] <= window[i - 2] && window[i] <= window[i + 2]) {
1315
+ localMins.push({ index: i, value: window[i] });
1316
+ }
1317
+ if (window[i] >= window[i - 1] && window[i] >= window[i + 1] && window[i] >= window[i - 2] && window[i] >= window[i + 2]) {
1318
+ localMaxs.push({ index: i, value: window[i] });
1319
+ }
1320
+ }
1321
+ for (let i = 0; i < localMins.length; i++) {
1322
+ for (let j = i + 1; j < localMins.length; j++) {
1323
+ const first = localMins[i];
1324
+ const second = localMins[j];
1325
+ const distance = second.index - first.index;
1326
+ if (distance < minDistance) continue;
1327
+ const priceDiff = Math.abs(first.value - second.value) / first.value;
1328
+ if (priceDiff > tolerance) continue;
1329
+ const between = window.slice(first.index, second.index + 1);
1330
+ const neckValue = Math.max(...between);
1331
+ const neckIdx = first.index + between.indexOf(neckValue);
1332
+ const currentPrice = window[window.length - 1];
1333
+ const breakout = currentPrice > neckValue;
1334
+ const similarity = 1 - priceDiff / tolerance;
1335
+ const distanceFactor = Math.min(1, distance / lookback * 2);
1336
+ const confidence = (similarity * 0.6 + distanceFactor * 0.4) * (breakout ? 1 : 0.7);
1337
+ return {
1338
+ type: "double_bottom",
1339
+ firstPointIndex: offset + first.index,
1340
+ secondPointIndex: offset + second.index,
1341
+ necklineIndex: offset + neckIdx,
1342
+ necklinePrice: neckValue,
1343
+ breakout,
1344
+ confidence
1345
+ };
1346
+ }
1347
+ }
1348
+ for (let i = 0; i < localMaxs.length; i++) {
1349
+ for (let j = i + 1; j < localMaxs.length; j++) {
1350
+ const first = localMaxs[i];
1351
+ const second = localMaxs[j];
1352
+ const distance = second.index - first.index;
1353
+ if (distance < minDistance) continue;
1354
+ const priceDiff = Math.abs(first.value - second.value) / first.value;
1355
+ if (priceDiff > tolerance) continue;
1356
+ const between = window.slice(first.index, second.index + 1);
1357
+ const neckValue = Math.min(...between);
1358
+ const neckIdx = first.index + between.indexOf(neckValue);
1359
+ const currentPrice = window[window.length - 1];
1360
+ const breakout = currentPrice < neckValue;
1361
+ const similarity = 1 - priceDiff / tolerance;
1362
+ const distanceFactor = Math.min(1, distance / lookback * 2);
1363
+ const confidence = (similarity * 0.6 + distanceFactor * 0.4) * (breakout ? 1 : 0.7);
1364
+ return {
1365
+ type: "double_top",
1366
+ firstPointIndex: offset + first.index,
1367
+ secondPointIndex: offset + second.index,
1368
+ necklineIndex: offset + neckIdx,
1369
+ necklinePrice: neckValue,
1370
+ breakout,
1371
+ confidence
1372
+ };
1373
+ }
1374
+ }
1375
+ return noResult;
1376
+ }
1377
+ function detectGaps(nav, threshold = 0.02) {
1378
+ const gaps = [];
1379
+ for (let i = 1; i < nav.length; i++) {
1380
+ const change = (nav[i] - nav[i - 1]) / nav[i - 1];
1381
+ const absChange = Math.abs(change);
1382
+ if (absChange >= threshold) {
1383
+ const isGapUp = change > 0;
1384
+ const gapTop = isGapUp ? nav[i] : nav[i - 1];
1385
+ const gapBottom = isGapUp ? nav[i - 1] : nav[i];
1386
+ let filled = false;
1387
+ let filledIndex = null;
1388
+ if (isGapUp) {
1389
+ for (let j = i + 1; j < nav.length; j++) {
1390
+ if (nav[j] <= gapBottom) {
1391
+ filled = true;
1392
+ filledIndex = j;
1393
+ break;
1394
+ }
1395
+ }
1396
+ } else {
1397
+ for (let j = i + 1; j < nav.length; j++) {
1398
+ if (nav[j] >= gapTop) {
1399
+ filled = true;
1400
+ filledIndex = j;
1401
+ break;
1402
+ }
1403
+ }
1404
+ }
1405
+ gaps.push({
1406
+ type: isGapUp ? "gap_up" : "gap_down",
1407
+ startIndex: i - 1,
1408
+ endIndex: i,
1409
+ gapTop,
1410
+ gapBottom,
1411
+ gapSize: absChange,
1412
+ filled,
1413
+ filledIndex
1414
+ });
1415
+ }
1416
+ }
1417
+ return gaps;
1418
+ }
1419
+ function trendStrength(nav, period = 20) {
1420
+ if (nav.length < period + 10) return 50;
1421
+ const recent = nav.slice(-period);
1422
+ const returns = [];
1423
+ for (let i = 1; i < recent.length; i++) {
1424
+ returns.push((recent[i] - recent[i - 1]) / recent[i - 1]);
1425
+ }
1426
+ const positiveDays = returns.filter((r) => r > 0).length;
1427
+ const directionScore = positiveDays / returns.length * 100;
1428
+ const indices = returns.map((_, i) => i);
1429
+ const meanY = returns.reduce((a, b) => a + b, 0) / returns.length;
1430
+ const meanX = indices.reduce((a, b) => a + b, 0) / indices.length;
1431
+ let ssXY = 0, ssXX = 0, ssYY = 0;
1432
+ for (let i = 0; i < returns.length; i++) {
1433
+ ssXY += (indices[i] - meanX) * (returns[i] - meanY);
1434
+ ssXX += (indices[i] - meanX) ** 2;
1435
+ ssYY += (returns[i] - meanY) ** 2;
1436
+ }
1437
+ const rSquared = ssXX > 0 && ssYY > 0 ? ssXY * ssXY / (ssXX * ssYY) : 0;
1438
+ const fitScore = rSquared * 100;
1439
+ let maxStreak = 0;
1440
+ let currentStreak = 0;
1441
+ for (const r of returns) {
1442
+ if (r > 0) {
1443
+ currentStreak++;
1444
+ maxStreak = Math.max(maxStreak, currentStreak);
1445
+ } else currentStreak = 0;
1446
+ }
1447
+ const streakScore = Math.min(100, maxStreak / returns.length * 200);
1448
+ return directionScore * 0.4 + fitScore * 0.35 + streakScore * 0.25;
1449
+ }
1450
+ function headAndShoulders(nav, lookback = 90) {
1451
+ const noResult = {
1452
+ type: "none",
1453
+ leftShoulder: null,
1454
+ head: null,
1455
+ rightShoulder: null,
1456
+ neckline: null,
1457
+ confidence: 0
1458
+ };
1459
+ if (nav.length < lookback) return noResult;
1460
+ const window = nav.slice(-lookback);
1461
+ const peaks = [];
1462
+ const troughs = [];
1463
+ for (let i = 3; i < window.length - 3; i++) {
1464
+ const isPeak = window[i] > window[i - 1] && window[i] > window[i + 1] && window[i] > window[i - 2] && window[i] > window[i + 2] && window[i] > window[i - 3] && window[i] > window[i + 3];
1465
+ const isTrough = window[i] < window[i - 1] && window[i] < window[i + 1] && window[i] < window[i - 2] && window[i] < window[i + 2] && window[i] < window[i - 3] && window[i] < window[i + 3];
1466
+ if (isPeak) peaks.push({ index: i, value: window[i] });
1467
+ if (isTrough) troughs.push({ index: i, value: window[i] });
1468
+ }
1469
+ if (peaks.length >= 3) {
1470
+ for (let i = 0; i < peaks.length - 2; i++) {
1471
+ const left = peaks[i];
1472
+ const head = peaks[i + 1];
1473
+ const right = peaks[i + 2];
1474
+ if (head.value <= left.value || head.value <= right.value) continue;
1475
+ const shoulderDiff = Math.abs(left.value - right.value) / left.value;
1476
+ if (shoulderDiff > 0.05) continue;
1477
+ const headPremium = (head.value - (left.value + right.value) / 2) / ((left.value + right.value) / 2);
1478
+ if (headPremium < 0.03) continue;
1479
+ const between = window.slice(left.index, right.index + 1);
1480
+ const neckline = Math.min(...between);
1481
+ const confidence = Math.min(1, (1 - shoulderDiff / 0.05) * 0.5 + Math.min(headPremium / 0.1, 1) * 0.5);
1482
+ return {
1483
+ type: "head_and_shoulders_top",
1484
+ leftShoulder: { index: nav.length - lookback + left.index, value: left.value },
1485
+ head: { index: nav.length - lookback + head.index, value: head.value },
1486
+ rightShoulder: { index: nav.length - lookback + right.index, value: right.value },
1487
+ neckline,
1488
+ confidence
1489
+ };
1490
+ }
1491
+ }
1492
+ if (troughs.length >= 3) {
1493
+ for (let i = 0; i < troughs.length - 2; i++) {
1494
+ const left = troughs[i];
1495
+ const head = troughs[i + 1];
1496
+ const right = troughs[i + 2];
1497
+ if (head.value >= left.value || head.value >= right.value) continue;
1498
+ const shoulderDiff = Math.abs(left.value - right.value) / left.value;
1499
+ if (shoulderDiff > 0.05) continue;
1500
+ const headDiscount = ((left.value + right.value) / 2 - head.value) / ((left.value + right.value) / 2);
1501
+ if (headDiscount < 0.03) continue;
1502
+ const between = window.slice(left.index, right.index + 1);
1503
+ const neckline = Math.max(...between);
1504
+ const confidence = Math.min(1, (1 - shoulderDiff / 0.05) * 0.5 + Math.min(headDiscount / 0.1, 1) * 0.5);
1505
+ return {
1506
+ type: "head_and_shoulders_bottom",
1507
+ leftShoulder: { index: nav.length - lookback + left.index, value: left.value },
1508
+ head: { index: nav.length - lookback + head.index, value: head.value },
1509
+ rightShoulder: { index: nav.length - lookback + right.index, value: right.value },
1510
+ neckline,
1511
+ confidence
1512
+ };
1513
+ }
1514
+ }
1515
+ return noResult;
1516
+ }
1517
+
1518
+ export { adx, annualizeReturn, annualizedVolatility, atr, autocorrelation, benchmarkMetrics, bias, bollingerBands, calculateAlpha, calculateBeta, calculateCVaR, calculateVaR, calmarRatio, cci, consecutiveWinLoss, dema, detectCrossSignal, detectGaps, detectMAAlignment, donchianChannel, doubleBottomTop, downsideVolatility, dpo, ema, garch11, headAndShoulders, hurstExponent, informationRatio, kama, kdj, keltnerChannel, ljungBoxTest, macd, massIndex, maxDrawdown, maxDrawdownDuration, momentum, navPercentile, navStatisticalFeatures, navToReturns, omegaRatio, performanceMetrics, positionPnL, pricePosition, profitFactor, profitLossRatio, returnQuantiles, riskMetrics, roc, rollingKurtosis, rollingSkewness, rollingVolatility, rsi, safetyMargin, sar, sharpeRatio, simulateDCA, sma, smartDCAMultiplier, sortinoRatio, statisticalFeatures, stochasticRSI, supportResistance, takeProfitStopLoss, tema, tieredBuySignal, tieredSellSignal, totalReturn, trackingError, trailingStop, trendStrength, treynorRatio, trix, volatilityCone, williamsR, winRate, wma };