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