@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.
- package/.github/workflows/publish.yml +79 -0
- package/AGENTS.md +322 -0
- package/dist/index.cjs +1615 -0
- package/dist/index.d.cts +779 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.mjs +1518 -0
- package/package.json +10 -29
- package/pnpm-workspace.yaml +2 -0
- package/src/dca.ts +420 -0
- package/src/index.ts +133 -0
- package/src/jstat.d.ts +17 -0
- package/src/pattern.ts +447 -0
- package/src/risk.ts +516 -0
- package/src/statistics.ts +428 -0
- package/src/technical.ts +738 -0
- package/src/types.ts +369 -0
- package/test/index.test.ts +355 -0
- package/tsconfig.json +20 -0
- package/dist/browser/fund-indicators.esm.js +0 -7505
- package/dist/browser/fund-indicators.esm.min.js +0 -8
- package/dist/browser/fund-indicators.esm.min.js.map +0 -7
- package/dist/browser/fund-indicators.js +0 -7517
- package/dist/browser/fund-indicators.min.js +0 -8
- package/dist/browser/fund-indicators.min.js.map +0 -7
- package/dist/dca.d.ts +0 -91
- package/dist/dca.d.ts.map +0 -1
- package/dist/dca.js +0 -354
- package/dist/dca.js.map +0 -1
- package/dist/index.d.ts +0 -18
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -141
- package/dist/index.js.map +0 -1
- package/dist/pattern.d.ts +0 -60
- package/dist/pattern.d.ts.map +0 -1
- package/dist/pattern.js +0 -386
- package/dist/pattern.js.map +0 -1
- package/dist/risk.d.ts +0 -115
- package/dist/risk.d.ts.map +0 -1
- package/dist/risk.js +0 -502
- package/dist/risk.js.map +0 -1
- package/dist/statistics.d.ts +0 -78
- package/dist/statistics.d.ts.map +0 -1
- package/dist/statistics.js +0 -402
- package/dist/statistics.js.map +0 -1
- package/dist/technical.d.ts +0 -105
- package/dist/technical.d.ts.map +0 -1
- package/dist/technical.js +0 -633
- package/dist/technical.js.map +0 -1
- package/dist/types.d.ts +0 -327
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -7
- 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
|
+
}
|