@stvy/fund-indicators 1.0.0 → 1.0.5

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 +73 -0
  2. package/AGENTS.md +322 -0
  3. package/dist/index.cjs +7014 -0
  4. package/dist/index.d.cts +779 -0
  5. package/dist/index.d.cts.map +1 -0
  6. package/dist/index.mjs +6917 -0
  7. package/package.json +15 -32
  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/src/risk.ts ADDED
@@ -0,0 +1,516 @@
1
+ /**
2
+ * 风险与绩效指标模块
3
+ * 包含:波动率、最大回撤、VaR、CVaR、夏普比率、索提诺、卡尔玛、特雷诺、Omega 等
4
+ */
5
+
6
+ import * as ss from 'simple-statistics';
7
+ import { jStat } from 'jstat';
8
+ import {
9
+ NavSeries,
10
+ ReturnSeries,
11
+ DateSeries,
12
+ DrawdownResult,
13
+ RiskMetrics,
14
+ PerformanceMetrics,
15
+ BenchmarkMetrics,
16
+ } from './types';
17
+
18
+ // ============================================================
19
+ // 辅助函数
20
+ // ============================================================
21
+
22
+ const TRADING_DAYS_PER_YEAR = 242; // A股年交易日数
23
+
24
+ /** 净值序列 → 日收益率序列 */
25
+ export function navToReturns(nav: NavSeries): ReturnSeries {
26
+ const returns: ReturnSeries = [];
27
+ for (let i = 1; i < nav.length; i++) {
28
+ returns.push((nav[i] - nav[i - 1]) / nav[i - 1]);
29
+ }
30
+ return returns;
31
+ }
32
+
33
+ /** 日收益率 → 年化收益率(几何平均) */
34
+ export function annualizeReturn(dailyReturns: ReturnSeries): number {
35
+ if (dailyReturns.length === 0) return 0;
36
+ // 几何年化 = (1 + 累计收益) ^ (242/n) - 1
37
+ const cumulative = dailyReturns.reduce((acc, r) => acc * (1 + r), 1);
38
+ const years = dailyReturns.length / TRADING_DAYS_PER_YEAR;
39
+ if (years <= 0 || cumulative <= 0) return 0;
40
+ return Math.pow(cumulative, 1 / years) - 1;
41
+ }
42
+
43
+ /** 累计收益率 */
44
+ export function totalReturn(nav: NavSeries): number {
45
+ if (nav.length < 2) return 0;
46
+ return (nav[nav.length - 1] - nav[0]) / nav[0];
47
+ }
48
+
49
+ // ============================================================
50
+ // 波动率
51
+ // ============================================================
52
+
53
+ /** 年化波动率 */
54
+ export function annualizedVolatility(returns: ReturnSeries): number {
55
+ if (returns.length < 2) return 0;
56
+ return ss.standardDeviation(returns) * Math.sqrt(TRADING_DAYS_PER_YEAR);
57
+ }
58
+
59
+ /** 下行波动率(年化),只计算负收益 */
60
+ export function downsideVolatility(returns: ReturnSeries, riskFreeRate: number = 0): number {
61
+ const downsideReturns = returns.filter((r) => r < riskFreeRate / TRADING_DAYS_PER_YEAR);
62
+ if (downsideReturns.length < 2) return 0;
63
+ // 对低于无风险利率的收益计算标准差
64
+ const deviations = downsideReturns.map((r) => Math.pow(r - riskFreeRate / TRADING_DAYS_PER_YEAR, 2));
65
+ const meanSqDev = deviations.reduce((a, b) => a + b, 0) / returns.length; // 注意分母用总天数
66
+ return Math.sqrt(meanSqDev) * Math.sqrt(TRADING_DAYS_PER_YEAR);
67
+ }
68
+
69
+ /**
70
+ * 滚动波动率
71
+ * @param returns 日收益率
72
+ * @param window 滚动窗口大小
73
+ */
74
+ export function rollingVolatility(returns: ReturnSeries, window: number = 20): (number | null)[] {
75
+ const result: (number | null)[] = [];
76
+ for (let i = 0; i < returns.length; i++) {
77
+ if (i < window - 1) {
78
+ result.push(null);
79
+ } else {
80
+ const slice = returns.slice(i - window + 1, i + 1);
81
+ result.push(ss.standardDeviation(slice) * Math.sqrt(TRADING_DAYS_PER_YEAR));
82
+ }
83
+ }
84
+ return result;
85
+ }
86
+
87
+ /**
88
+ * 波动率锥 - 不同时间窗口的波动率分位数分布
89
+ * @param returns 日收益率
90
+ * @param windows 要分析的时间窗口列表
91
+ * @param quantiles 要计算的分位数列表
92
+ */
93
+ export function volatilityCone(
94
+ returns: ReturnSeries,
95
+ windows: number[] = [5, 10, 20, 60, 120],
96
+ quantiles: number[] = [0.1, 0.25, 0.5, 0.75, 0.9]
97
+ ): Map<number, Map<number, number>> {
98
+ const cone = new Map<number, Map<number, number>>();
99
+
100
+ for (const w of windows) {
101
+ const vols: number[] = [];
102
+ for (let i = w - 1; i < returns.length; i++) {
103
+ const slice = returns.slice(i - w + 1, i + 1);
104
+ vols.push(ss.standardDeviation(slice) * Math.sqrt(TRADING_DAYS_PER_YEAR));
105
+ }
106
+ const sorted = [...vols].sort((a, b) => a - b);
107
+ const qMap = new Map<number, number>();
108
+ for (const q of quantiles) {
109
+ qMap.set(q, ss.quantileSorted(sorted, q));
110
+ }
111
+ cone.set(w, qMap);
112
+ }
113
+
114
+ return cone;
115
+ }
116
+
117
+ // ============================================================
118
+ // 最大回撤
119
+ // ============================================================
120
+
121
+ /**
122
+ * 最大回撤分析
123
+ * @param nav 净值序列
124
+ * @param dates 可选日期序列
125
+ */
126
+ export function maxDrawdown(nav: NavSeries, dates?: DateSeries): DrawdownResult {
127
+ if (nav.length < 2) {
128
+ return {
129
+ maxDrawdown: 0, peakIndex: 0, troughIndex: 0,
130
+ peakDate: null, troughDate: null, recoveryDate: null,
131
+ durationDays: 0, recoveryDays: null, drawdownSeries: [],
132
+ };
133
+ }
134
+
135
+ const drawdownSeries: number[] = [];
136
+ let peak = nav[0];
137
+ let peakIdx = 0;
138
+ let maxDD = 0;
139
+ let maxPeakIdx = 0;
140
+ let maxTroughIdx = 0;
141
+
142
+ for (let i = 0; i < nav.length; i++) {
143
+ if (nav[i] > peak) {
144
+ peak = nav[i];
145
+ peakIdx = i;
146
+ }
147
+ const dd = (nav[i] - peak) / peak;
148
+ drawdownSeries.push(dd);
149
+ if (dd < maxDD) {
150
+ maxDD = dd;
151
+ maxPeakIdx = peakIdx;
152
+ maxTroughIdx = i;
153
+ }
154
+ }
155
+
156
+ // 寻找恢复点
157
+ let recoveryIdx: number | null = null;
158
+ const peakValue = nav[maxPeakIdx];
159
+ for (let i = maxTroughIdx + 1; i < nav.length; i++) {
160
+ if (nav[i] >= peakValue) {
161
+ recoveryIdx = i;
162
+ break;
163
+ }
164
+ }
165
+
166
+ return {
167
+ maxDrawdown: maxDD,
168
+ peakIndex: maxPeakIdx,
169
+ troughIndex: maxTroughIdx,
170
+ peakDate: dates ? dates[maxPeakIdx] ?? null : null,
171
+ troughDate: dates ? dates[maxTroughIdx] ?? null : null,
172
+ recoveryDate: recoveryIdx != null && dates ? dates[recoveryIdx] ?? null : null,
173
+ durationDays: maxTroughIdx - maxPeakIdx,
174
+ recoveryDays: recoveryIdx != null ? recoveryIdx - maxTroughIdx : null,
175
+ drawdownSeries,
176
+ };
177
+ }
178
+
179
+ /**
180
+ * 最大回撤持续天数(从峰顶到下一次创新高)
181
+ */
182
+ export function maxDrawdownDuration(nav: NavSeries): number {
183
+ let maxDuration = 0;
184
+ let currentDuration = 0;
185
+ let peak = nav[0];
186
+
187
+ for (let i = 0; i < nav.length; i++) {
188
+ if (nav[i] >= peak) {
189
+ peak = nav[i];
190
+ currentDuration = 0;
191
+ } else {
192
+ currentDuration++;
193
+ maxDuration = Math.max(maxDuration, currentDuration);
194
+ }
195
+ }
196
+ return maxDuration;
197
+ }
198
+
199
+ // ============================================================
200
+ // VaR / CVaR
201
+ // ============================================================
202
+
203
+ /**
204
+ * VaR(在险价值)
205
+ * @param returns 日收益率
206
+ * @param confidence 置信度(默认 0.95)
207
+ * @param method 计算方法:'historical'(历史模拟)| 'parametric'(参数法/正态假设)
208
+ */
209
+ export function calculateVaR(
210
+ returns: ReturnSeries,
211
+ confidence: number = 0.95,
212
+ method: 'historical' | 'parametric' = 'historical'
213
+ ): number {
214
+ if (returns.length < 2) return 0;
215
+
216
+ if (method === 'historical') {
217
+ const sorted = [...returns].sort((a, b) => a - b);
218
+ return ss.quantileSorted(sorted, 1 - confidence);
219
+ } else {
220
+ // 参数法(正态分布假设)
221
+ const mean = ss.mean(returns);
222
+ const std = ss.standardDeviation(returns);
223
+ const z = jStat.normal.inv(1 - confidence, 0, 1);
224
+ return mean + z * std;
225
+ }
226
+ }
227
+
228
+ /**
229
+ * CVaR / Expected Shortfall(条件在险价值 / 预期亏损)
230
+ * @param returns 日收益率
231
+ * @param confidence 置信度(默认 0.95)
232
+ */
233
+ export function calculateCVaR(returns: ReturnSeries, confidence: number = 0.95): number {
234
+ if (returns.length < 2) return 0;
235
+ const varValue = calculateVaR(returns, confidence, 'historical');
236
+ const tailReturns = returns.filter((r) => r <= varValue);
237
+ if (tailReturns.length === 0) return varValue;
238
+ return ss.mean(tailReturns);
239
+ }
240
+
241
+ // ============================================================
242
+ // 风险指标汇总
243
+ // ============================================================
244
+
245
+ /**
246
+ * 一次性计算所有风险指标
247
+ * @param nav 净值序列
248
+ * @param riskFreeRate 年化无风险利率(默认 0.025 = 2.5%)
249
+ */
250
+ export function riskMetrics(nav: NavSeries, riskFreeRate: number = 0.025): RiskMetrics {
251
+ const returns = navToReturns(nav);
252
+ const dd = maxDrawdown(nav);
253
+
254
+ return {
255
+ annualizedVolatility: annualizedVolatility(returns),
256
+ downsideVolatility: downsideVolatility(returns, riskFreeRate),
257
+ maxDrawdown: dd.maxDrawdown,
258
+ maxDrawdownDuration: maxDrawdownDuration(nav),
259
+ var95: calculateVaR(returns, 0.95),
260
+ var99: calculateVaR(returns, 0.99),
261
+ cvar95: calculateCVaR(returns, 0.95),
262
+ cvar99: calculateCVaR(returns, 0.99),
263
+ };
264
+ }
265
+
266
+ // ============================================================
267
+ // 绩效指标
268
+ // ============================================================
269
+
270
+ /**
271
+ * 夏普比率 = (年化收益 - 无风险利率) / 年化波动率
272
+ */
273
+ export function sharpeRatio(nav: NavSeries, riskFreeRate: number = 0.025): number {
274
+ const returns = navToReturns(nav);
275
+ const annReturn = annualizeReturn(returns);
276
+ const annVol = annualizedVolatility(returns);
277
+ if (annVol === 0) return 0;
278
+ return (annReturn - riskFreeRate) / annVol;
279
+ }
280
+
281
+ /**
282
+ * 索提诺比率 = (年化收益 - 无风险利率) / 下行波动率
283
+ */
284
+ export function sortinoRatio(nav: NavSeries, riskFreeRate: number = 0.025): number {
285
+ const returns = navToReturns(nav);
286
+ const annReturn = annualizeReturn(returns);
287
+ const dv = downsideVolatility(returns, riskFreeRate);
288
+ if (dv === 0) return 0;
289
+ return (annReturn - riskFreeRate) / dv;
290
+ }
291
+
292
+ /**
293
+ * 卡尔玛比率 = 年化收益 / |最大回撤|
294
+ */
295
+ export function calmarRatio(nav: NavSeries): number {
296
+ const returns = navToReturns(nav);
297
+ const annReturn = annualizeReturn(returns);
298
+ const dd = maxDrawdown(nav);
299
+ if (dd.maxDrawdown === 0) return 0;
300
+ return annReturn / Math.abs(dd.maxDrawdown);
301
+ }
302
+
303
+ /**
304
+ * 特雷诺比率 = (年化收益 - 无风险利率) / Beta
305
+ * @param nav 基金净值
306
+ * @param benchmarkNav 基准净值
307
+ * @param riskFreeRate 无风险利率
308
+ */
309
+ export function treynorRatio(
310
+ nav: NavSeries,
311
+ benchmarkNav: NavSeries,
312
+ riskFreeRate: number = 0.025
313
+ ): number | null {
314
+ const fundReturns = navToReturns(nav);
315
+ const benchReturns = navToReturns(benchmarkNav);
316
+ const minLen = Math.min(fundReturns.length, benchReturns.length);
317
+ const fRet = fundReturns.slice(-minLen);
318
+ const bRet = benchReturns.slice(-minLen);
319
+
320
+ const beta = calculateBeta(fRet, bRet);
321
+ if (beta === 0) return null;
322
+
323
+ const annReturn = annualizeReturn(fRet);
324
+ return (annReturn - riskFreeRate) / beta;
325
+ }
326
+
327
+ /**
328
+ * Omega 比率 = 加权上行收益 / 加权下行亏损
329
+ * @param nav 净值序列
330
+ * @param threshold 阈值(默认 0)
331
+ */
332
+ export function omegaRatio(nav: NavSeries, threshold: number = 0): number {
333
+ const returns = navToReturns(nav);
334
+ const dailyThreshold = threshold / TRADING_DAYS_PER_YEAR;
335
+
336
+ let gains = 0;
337
+ let losses = 0;
338
+ for (const r of returns) {
339
+ if (r > dailyThreshold) gains += r - dailyThreshold;
340
+ else losses += dailyThreshold - r;
341
+ }
342
+
343
+ if (losses === 0) return gains > 0 ? Infinity : 0;
344
+ return gains / losses;
345
+ }
346
+
347
+ /** 胜率 = 正收益天数 / 总天数 */
348
+ export function winRate(returns: ReturnSeries): number {
349
+ if (returns.length === 0) return 0;
350
+ const wins = returns.filter((r) => r > 0).length;
351
+ return wins / returns.length;
352
+ }
353
+
354
+ /** 盈亏比 = 平均盈利 / 平均亏损的绝对值 */
355
+ export function profitLossRatio(returns: ReturnSeries): number {
356
+ const wins = returns.filter((r) => r > 0);
357
+ const losses = returns.filter((r) => r < 0);
358
+ if (losses.length === 0) return wins.length > 0 ? Infinity : 0;
359
+ const avgWin = wins.length > 0 ? ss.mean(wins) : 0;
360
+ const avgLoss = Math.abs(ss.mean(losses));
361
+ if (avgLoss === 0) return 0;
362
+ return avgWin / avgLoss;
363
+ }
364
+
365
+ /** 利润因子 = 总盈利 / 总亏损 */
366
+ export function profitFactor(returns: ReturnSeries): number {
367
+ const totalWins = returns.filter((r) => r > 0).reduce((a, b) => a + b, 0);
368
+ const totalLosses = Math.abs(returns.filter((r) => r < 0).reduce((a, b) => a + b, 0));
369
+ if (totalLosses === 0) return totalWins > 0 ? Infinity : 0;
370
+ return totalWins / totalLosses;
371
+ }
372
+
373
+ /** 最大连续盈利/亏损天数 */
374
+ export function consecutiveWinLoss(returns: ReturnSeries): { maxWins: number; maxLosses: number } {
375
+ let maxWins = 0, maxLosses = 0;
376
+ let curWins = 0, curLosses = 0;
377
+
378
+ for (const r of returns) {
379
+ if (r > 0) {
380
+ curWins++;
381
+ curLosses = 0;
382
+ maxWins = Math.max(maxWins, curWins);
383
+ } else if (r < 0) {
384
+ curLosses++;
385
+ curWins = 0;
386
+ maxLosses = Math.max(maxLosses, curLosses);
387
+ } else {
388
+ curWins = 0;
389
+ curLosses = 0;
390
+ }
391
+ }
392
+
393
+ return { maxWins, maxLosses };
394
+ }
395
+
396
+ /**
397
+ * 一次性计算所有绩效指标
398
+ * @param nav 净值序列
399
+ * @param riskFreeRate 年化无风险利率
400
+ */
401
+ export function performanceMetrics(nav: NavSeries, riskFreeRate: number = 0.025): PerformanceMetrics {
402
+ const returns = navToReturns(nav);
403
+ const consec = consecutiveWinLoss(returns);
404
+ const dd = maxDrawdown(nav);
405
+ const annReturn = annualizeReturn(returns);
406
+ const annVol = annualizedVolatility(returns);
407
+ const dv = downsideVolatility(returns, riskFreeRate);
408
+
409
+ return {
410
+ totalReturn: totalReturn(nav),
411
+ annualizedReturn: annReturn,
412
+ sharpeRatio: annVol > 0 ? (annReturn - riskFreeRate) / annVol : 0,
413
+ sortinoRatio: dv > 0 ? (annReturn - riskFreeRate) / dv : 0,
414
+ calmarRatio: dd.maxDrawdown !== 0 ? annReturn / Math.abs(dd.maxDrawdown) : 0,
415
+ treynorRatio: null, // 需要基准数据,单独计算
416
+ omegaRatio: omegaRatio(nav),
417
+ winRate: winRate(returns),
418
+ profitLossRatio: profitLossRatio(returns),
419
+ profitFactor: profitFactor(returns),
420
+ maxConsecutiveWins: consec.maxWins,
421
+ maxConsecutiveLosses: consec.maxLosses,
422
+ };
423
+ }
424
+
425
+ // ============================================================
426
+ // 相对基准指标(Alpha / Beta / 跟踪误差 / 信息比率)
427
+ // ============================================================
428
+
429
+ /** Beta 系数 */
430
+ export function calculateBeta(fundReturns: ReturnSeries, benchmarkReturns: ReturnSeries): number {
431
+ const minLen = Math.min(fundReturns.length, benchmarkReturns.length);
432
+ const f = fundReturns.slice(-minLen);
433
+ const b = benchmarkReturns.slice(-minLen);
434
+ const cov = ss.sampleCovariance(f, b);
435
+ const bVar = ss.variance(b);
436
+ return bVar > 0 ? cov / bVar : 0;
437
+ }
438
+
439
+ /** Alpha(年化超额收益) */
440
+ export function calculateAlpha(
441
+ fundReturns: ReturnSeries,
442
+ benchmarkReturns: ReturnSeries,
443
+ riskFreeRate: number = 0.025
444
+ ): number {
445
+ const minLen = Math.min(fundReturns.length, benchmarkReturns.length);
446
+ const f = fundReturns.slice(-minLen);
447
+ const b = benchmarkReturns.slice(-minLen);
448
+
449
+ const fAnnReturn = annualizeReturn(f);
450
+ const bAnnReturn = annualizeReturn(b);
451
+ const beta = calculateBeta(f, b);
452
+
453
+ return fAnnReturn - (riskFreeRate + beta * (bAnnReturn - riskFreeRate));
454
+ }
455
+
456
+ /** 跟踪误差(年化) */
457
+ export function trackingError(fundReturns: ReturnSeries, benchmarkReturns: ReturnSeries): number {
458
+ const minLen = Math.min(fundReturns.length, benchmarkReturns.length);
459
+ const f = fundReturns.slice(-minLen);
460
+ const b = benchmarkReturns.slice(-minLen);
461
+
462
+ const excessReturns = f.map((r, i) => r - b[i]);
463
+ return ss.standardDeviation(excessReturns) * Math.sqrt(TRADING_DAYS_PER_YEAR);
464
+ }
465
+
466
+ /** 信息比率 = 超额收益 / 跟踪误差 */
467
+ export function informationRatio(
468
+ fundReturns: ReturnSeries,
469
+ benchmarkReturns: ReturnSeries
470
+ ): number {
471
+ const minLen = Math.min(fundReturns.length, benchmarkReturns.length);
472
+ const f = fundReturns.slice(-minLen);
473
+ const b = benchmarkReturns.slice(-minLen);
474
+
475
+ const excessReturns = f.map((r, i) => r - b[i]);
476
+ const meanExcess = ss.mean(excessReturns) * TRADING_DAYS_PER_YEAR;
477
+ const te = ss.standardDeviation(excessReturns) * Math.sqrt(TRADING_DAYS_PER_YEAR);
478
+
479
+ return te > 0 ? meanExcess / te : 0;
480
+ }
481
+
482
+ /**
483
+ * 一次性计算所有相对基准指标
484
+ * @param fundNav 基金净值序列
485
+ * @param benchmarkNav 基准净值序列(如沪深300净值)
486
+ * @param riskFreeRate 年化无风险利率
487
+ */
488
+ export function benchmarkMetrics(
489
+ fundNav: NavSeries,
490
+ benchmarkNav: NavSeries,
491
+ riskFreeRate: number = 0.025
492
+ ): BenchmarkMetrics {
493
+ const fundReturns = navToReturns(fundNav);
494
+ const benchReturns = navToReturns(benchmarkNav);
495
+ const minLen = Math.min(fundReturns.length, benchReturns.length);
496
+ const f = fundReturns.slice(-minLen);
497
+ const b = benchReturns.slice(-minLen);
498
+
499
+ const beta = calculateBeta(f, b);
500
+ const alpha = calculateAlpha(f, b, riskFreeRate);
501
+ const te = trackingError(f, b);
502
+ const ir = te > 0 ? informationRatio(f, b) : 0;
503
+ const corr = ss.sampleCorrelation(f, b);
504
+
505
+ // R² = correlation²
506
+ const rSquared = corr * corr;
507
+
508
+ return {
509
+ alpha,
510
+ beta,
511
+ trackingError: te,
512
+ informationRatio: ir,
513
+ correlation: corr,
514
+ rSquared,
515
+ };
516
+ }