@tradejs/core 1.0.0
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/README.md +60 -0
- package/dist/api.d.mts +7 -0
- package/dist/api.d.ts +7 -0
- package/dist/api.js +64 -0
- package/dist/api.mjs +39 -0
- package/dist/async.d.mts +4 -0
- package/dist/async.d.ts +4 -0
- package/dist/async.js +48 -0
- package/dist/async.mjs +20 -0
- package/dist/backtest.d.mts +45 -0
- package/dist/backtest.d.ts +45 -0
- package/dist/backtest.js +574 -0
- package/dist/backtest.mjs +355 -0
- package/dist/chunk-AYC2QVKI.mjs +35 -0
- package/dist/chunk-JG2QPVAV.mjs +190 -0
- package/dist/chunk-LIGD3WWX.mjs +1545 -0
- package/dist/chunk-M7QGVZ3J.mjs +61 -0
- package/dist/chunk-NQ7D3T4E.mjs +10 -0
- package/dist/chunk-PXLXXXLA.mjs +67 -0
- package/dist/config.d.mts +14 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +49 -0
- package/dist/config.mjs +21 -0
- package/dist/constants.d.mts +41 -0
- package/dist/constants.d.ts +41 -0
- package/dist/constants.js +238 -0
- package/dist/constants.mjs +50 -0
- package/dist/data.d.mts +9 -0
- package/dist/data.d.ts +9 -0
- package/dist/data.js +100 -0
- package/dist/data.mjs +12 -0
- package/dist/figures.d.mts +103 -0
- package/dist/figures.d.ts +103 -0
- package/dist/figures.js +274 -0
- package/dist/figures.mjs +239 -0
- package/dist/indicators-x3xKl3_W.d.mts +90 -0
- package/dist/indicators-x3xKl3_W.d.ts +90 -0
- package/dist/indicators.d.mts +124 -0
- package/dist/indicators.d.ts +124 -0
- package/dist/indicators.js +1631 -0
- package/dist/indicators.mjs +66 -0
- package/dist/json.d.mts +3 -0
- package/dist/json.d.ts +3 -0
- package/dist/json.js +34 -0
- package/dist/json.mjs +7 -0
- package/dist/math.d.mts +35 -0
- package/dist/math.d.ts +35 -0
- package/dist/math.js +98 -0
- package/dist/math.mjs +38 -0
- package/dist/pine.d.mts +29 -0
- package/dist/pine.d.ts +29 -0
- package/dist/pine.js +59 -0
- package/dist/pine.mjs +29 -0
- package/dist/strategies.d.mts +104 -0
- package/dist/strategies.d.ts +104 -0
- package/dist/strategies.js +1080 -0
- package/dist/strategies.mjs +390 -0
- package/dist/tickers.d.mts +7 -0
- package/dist/tickers.d.ts +7 -0
- package/dist/tickers.js +166 -0
- package/dist/tickers.mjs +125 -0
- package/dist/time-DEyFa2vI.d.mts +11 -0
- package/dist/time-DEyFa2vI.d.ts +11 -0
- package/dist/time.d.mts +2 -0
- package/dist/time.d.ts +2 -0
- package/dist/time.js +58 -0
- package/dist/time.mjs +15 -0
- package/package.json +99 -0
package/dist/backtest.js
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/backtest.ts
|
|
31
|
+
var backtest_exports = {};
|
|
32
|
+
__export(backtest_exports, {
|
|
33
|
+
calculateMaxDrawdown: () => calculateMaxDrawdown,
|
|
34
|
+
calculateStatsFull: () => calculateStatsFull,
|
|
35
|
+
classifyMetric: () => classifyMetric,
|
|
36
|
+
compactOrderLog: () => compactOrderLog,
|
|
37
|
+
createTestSuite: () => createTestSuite,
|
|
38
|
+
generateName: () => generateName,
|
|
39
|
+
generateParamGrid: () => generateParamGrid,
|
|
40
|
+
getBacktestScore: () => getBacktestScore,
|
|
41
|
+
getFormatted: () => getFormatted,
|
|
42
|
+
getTimeline: () => getTimeline,
|
|
43
|
+
mergeConfigs: () => mergeConfigs,
|
|
44
|
+
parseTestName: () => parseTestName,
|
|
45
|
+
sortBestTests: () => sortBestTests
|
|
46
|
+
});
|
|
47
|
+
module.exports = __toCommonJS(backtest_exports);
|
|
48
|
+
|
|
49
|
+
// src/utils/grid.ts
|
|
50
|
+
var import_lodash = __toESM(require("lodash"));
|
|
51
|
+
|
|
52
|
+
// src/constants/index.ts
|
|
53
|
+
var BACKTEST_PRELOAD_DAYS = 160;
|
|
54
|
+
var TestThresholdsConfig = {
|
|
55
|
+
// Период и частота — используем как требования к качеству теста, в скоринг не влияют
|
|
56
|
+
periodDays: {
|
|
57
|
+
thresholds: [30, 120],
|
|
58
|
+
direction: "higher",
|
|
59
|
+
precision: 0
|
|
60
|
+
},
|
|
61
|
+
periodMonths: {
|
|
62
|
+
thresholds: [1, 6],
|
|
63
|
+
direction: "higher",
|
|
64
|
+
precision: 2
|
|
65
|
+
},
|
|
66
|
+
orders: {
|
|
67
|
+
thresholds: [30, 200],
|
|
68
|
+
direction: "higher",
|
|
69
|
+
precision: 0
|
|
70
|
+
},
|
|
71
|
+
wins: {
|
|
72
|
+
thresholds: [30, 100],
|
|
73
|
+
direction: "higher",
|
|
74
|
+
precision: 0
|
|
75
|
+
},
|
|
76
|
+
losses: {
|
|
77
|
+
thresholds: [20, 50],
|
|
78
|
+
direction: "lower",
|
|
79
|
+
precision: 0
|
|
80
|
+
},
|
|
81
|
+
ordersPerMonth: {
|
|
82
|
+
thresholds: [4, 20],
|
|
83
|
+
direction: "higher",
|
|
84
|
+
precision: 2
|
|
85
|
+
},
|
|
86
|
+
exposure: {
|
|
87
|
+
thresholds: [20, 60],
|
|
88
|
+
direction: "higher",
|
|
89
|
+
isPercent: true,
|
|
90
|
+
precision: 1
|
|
91
|
+
},
|
|
92
|
+
// Доходность
|
|
93
|
+
amount: {
|
|
94
|
+
thresholds: [105, 120],
|
|
95
|
+
direction: "higher",
|
|
96
|
+
isAmount: true,
|
|
97
|
+
precision: 2
|
|
98
|
+
},
|
|
99
|
+
maxAmount: {
|
|
100
|
+
thresholds: [140, 180],
|
|
101
|
+
direction: "higher",
|
|
102
|
+
isAmount: true,
|
|
103
|
+
precision: 2
|
|
104
|
+
},
|
|
105
|
+
minAmount: {
|
|
106
|
+
thresholds: [80, 90],
|
|
107
|
+
direction: "higher",
|
|
108
|
+
isAmount: true,
|
|
109
|
+
precision: 2
|
|
110
|
+
},
|
|
111
|
+
netProfit: {
|
|
112
|
+
thresholds: [5, 20],
|
|
113
|
+
direction: "higher",
|
|
114
|
+
isAmount: true,
|
|
115
|
+
precision: 2
|
|
116
|
+
},
|
|
117
|
+
totalReturn: {
|
|
118
|
+
thresholds: [10, 50],
|
|
119
|
+
direction: "higher",
|
|
120
|
+
isPercent: true,
|
|
121
|
+
precision: 1
|
|
122
|
+
},
|
|
123
|
+
cagr: {
|
|
124
|
+
thresholds: [15, 40],
|
|
125
|
+
direction: "higher",
|
|
126
|
+
isPercent: true,
|
|
127
|
+
precision: 1
|
|
128
|
+
},
|
|
129
|
+
// Риск и риск/доходность
|
|
130
|
+
maxDrawdown: {
|
|
131
|
+
thresholds: [25, 12],
|
|
132
|
+
direction: "lower",
|
|
133
|
+
isPercent: true,
|
|
134
|
+
precision: 1
|
|
135
|
+
},
|
|
136
|
+
calmar: {
|
|
137
|
+
thresholds: [0.5, 2],
|
|
138
|
+
direction: "higher",
|
|
139
|
+
precision: 2
|
|
140
|
+
},
|
|
141
|
+
// Качество сделок
|
|
142
|
+
winRate: {
|
|
143
|
+
thresholds: [40, 60],
|
|
144
|
+
direction: "higher",
|
|
145
|
+
isPercent: true,
|
|
146
|
+
precision: 1
|
|
147
|
+
},
|
|
148
|
+
riskRewardRatio: {
|
|
149
|
+
thresholds: [1.5, 2.5],
|
|
150
|
+
direction: "higher",
|
|
151
|
+
precision: 2
|
|
152
|
+
},
|
|
153
|
+
expectancy: {
|
|
154
|
+
thresholds: [0.3, 1],
|
|
155
|
+
direction: "higher",
|
|
156
|
+
isPercent: true,
|
|
157
|
+
precision: 2
|
|
158
|
+
},
|
|
159
|
+
maxConsecutiveWins: {
|
|
160
|
+
thresholds: [2, 6],
|
|
161
|
+
direction: "higher",
|
|
162
|
+
precision: 0
|
|
163
|
+
},
|
|
164
|
+
maxConsecutiveLosses: {
|
|
165
|
+
thresholds: [5, 2],
|
|
166
|
+
direction: "lower",
|
|
167
|
+
precision: 0
|
|
168
|
+
},
|
|
169
|
+
// Sharpe (годовой, по месячным ретернам equity)
|
|
170
|
+
sharpeRatio: {
|
|
171
|
+
thresholds: [0.5, 1.5],
|
|
172
|
+
direction: "higher",
|
|
173
|
+
precision: 2
|
|
174
|
+
},
|
|
175
|
+
score: {
|
|
176
|
+
thresholds: [10, 100],
|
|
177
|
+
direction: "higher",
|
|
178
|
+
precision: 0
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// src/utils/timestamp.ts
|
|
183
|
+
var import_date_fns = require("date-fns");
|
|
184
|
+
var import_date_fns2 = require("date-fns");
|
|
185
|
+
var TIMELINE_STEP = 864e5;
|
|
186
|
+
var getTimestamp = (days = 0) => {
|
|
187
|
+
if (days > 0) {
|
|
188
|
+
return (0, import_date_fns2.getUnixTime)((0, import_date_fns2.subDays)(/* @__PURE__ */ new Date(), days)) * 1e3;
|
|
189
|
+
}
|
|
190
|
+
return (0, import_date_fns2.getUnixTime)(/* @__PURE__ */ new Date()) * 1e3;
|
|
191
|
+
};
|
|
192
|
+
var getTimeline = (start = getTimestamp(BACKTEST_PRELOAD_DAYS), end = getTimestamp(), step = TIMELINE_STEP) => {
|
|
193
|
+
const res = new Array();
|
|
194
|
+
for (let ind = start; ind <= end; ind += step) {
|
|
195
|
+
res.push(ind);
|
|
196
|
+
}
|
|
197
|
+
return res;
|
|
198
|
+
};
|
|
199
|
+
var compactOrderLog = (timeline, orderLog) => {
|
|
200
|
+
const result = [];
|
|
201
|
+
let currentAmount = orderLog.length > 0 && orderLog[0].amount != null ? orderLog[0].amount : 100;
|
|
202
|
+
let orderLogCursor = 0;
|
|
203
|
+
for (let timelineIndex = 0; timelineIndex < timeline.length; timelineIndex++) {
|
|
204
|
+
const currentTimestamp = timeline[timelineIndex];
|
|
205
|
+
let lastApplicableOrderIndex = -1;
|
|
206
|
+
let nextCursor = orderLogCursor;
|
|
207
|
+
for (let checkIndex = orderLogCursor; checkIndex < orderLog.length; checkIndex++) {
|
|
208
|
+
const checkOrder = orderLog[checkIndex];
|
|
209
|
+
if (checkOrder.timestamp <= currentTimestamp) {
|
|
210
|
+
lastApplicableOrderIndex = checkIndex;
|
|
211
|
+
nextCursor = checkIndex + 1;
|
|
212
|
+
} else {
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (lastApplicableOrderIndex !== -1) {
|
|
217
|
+
currentAmount = orderLog[lastApplicableOrderIndex].amount;
|
|
218
|
+
orderLogCursor = nextCursor;
|
|
219
|
+
}
|
|
220
|
+
result.push([currentTimestamp, currentAmount]);
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// src/utils/uuid.ts
|
|
226
|
+
var import_uuid = require("uuid");
|
|
227
|
+
var uuid = (len = 12) => {
|
|
228
|
+
const uuid2 = (0, import_uuid.v4)();
|
|
229
|
+
return uuid2.slice(-len);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// src/utils/grid.ts
|
|
233
|
+
var generateParamGrid = (paramOptions) => {
|
|
234
|
+
const keys = Object.keys(paramOptions);
|
|
235
|
+
const combinations = [];
|
|
236
|
+
const helper = (index = 0, current = {}) => {
|
|
237
|
+
if (index === keys.length) {
|
|
238
|
+
combinations.push(current);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const key = keys[index];
|
|
242
|
+
for (const value of paramOptions[key] || []) {
|
|
243
|
+
const copiedValue = typeof value === "object" && value !== null ? structuredClone(value) : value;
|
|
244
|
+
helper(index + 1, {
|
|
245
|
+
...current,
|
|
246
|
+
[key]: copiedValue
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
helper();
|
|
251
|
+
return combinations;
|
|
252
|
+
};
|
|
253
|
+
var generateName = (prefix) => `${prefix}_${uuid(6)}`;
|
|
254
|
+
var mergeConfigs = (configs) => {
|
|
255
|
+
const result = {};
|
|
256
|
+
for (const config of configs) {
|
|
257
|
+
for (const [key, value] of Object.entries(config)) {
|
|
258
|
+
if (!result[key]) {
|
|
259
|
+
result[key] = [];
|
|
260
|
+
}
|
|
261
|
+
const clonedValue = typeof value === "object" && value !== null ? import_lodash.default.cloneDeep(value) : value;
|
|
262
|
+
const isDuplicate = result[key].some(
|
|
263
|
+
(existing) => import_lodash.default.isEqual(existing, value)
|
|
264
|
+
);
|
|
265
|
+
if (!isDuplicate) {
|
|
266
|
+
result[key].push(clonedValue);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
for (const key in result) {
|
|
271
|
+
if (result[key].every((v) => typeof v === "number")) {
|
|
272
|
+
result[key] = import_lodash.default.sortBy(result[key]);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
};
|
|
277
|
+
var createTestSuite = (userName, tickers, strategyName, backtestConfig, connectorName) => {
|
|
278
|
+
const start = getTimestamp(BACKTEST_PRELOAD_DAYS);
|
|
279
|
+
const end = getTimestamp();
|
|
280
|
+
const testSuiteId = uuid(6);
|
|
281
|
+
const paramGrid = generateParamGrid(backtestConfig);
|
|
282
|
+
return tickers.flatMap(
|
|
283
|
+
(symbol) => paramGrid.map((params) => {
|
|
284
|
+
const testId = uuid(6);
|
|
285
|
+
return {
|
|
286
|
+
userName,
|
|
287
|
+
name: `${symbol}_${testSuiteId}_${testId}`,
|
|
288
|
+
testId,
|
|
289
|
+
testSuiteId,
|
|
290
|
+
symbol,
|
|
291
|
+
options: { start, end },
|
|
292
|
+
strategyName,
|
|
293
|
+
strategyConfig: params,
|
|
294
|
+
connectorName
|
|
295
|
+
};
|
|
296
|
+
})
|
|
297
|
+
);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// src/utils/tests.ts
|
|
301
|
+
var parseTestName = (testName) => {
|
|
302
|
+
const [symbol, testSuiteId, testId] = testName.split("_");
|
|
303
|
+
return { symbol, testSuiteId, testId };
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// src/utils/stat.ts
|
|
307
|
+
var import_date_fns3 = require("date-fns");
|
|
308
|
+
|
|
309
|
+
// src/utils/math.ts
|
|
310
|
+
var import_lodash2 = __toESM(require("lodash"));
|
|
311
|
+
var round = (value, precision = 2) => precision > 0 ? Math.round(value * 10 ** precision) / 10 ** precision : Math.round(value);
|
|
312
|
+
var sum = (xs) => xs.reduce((a, b) => a + b, 0);
|
|
313
|
+
var mean = (xs) => xs.length ? sum(xs) / xs.length : 0;
|
|
314
|
+
var absReturns = (data) => data.map((p) => p.close.amount - p.open.amount);
|
|
315
|
+
var relReturns = (data) => data.map((p) => (p.close.amount - p.open.amount) / p.open.amount);
|
|
316
|
+
var equityPoints = (data) => data.flatMap((p) => [
|
|
317
|
+
{ ts: p.open.timestamp, amount: p.open.amount },
|
|
318
|
+
{ ts: p.close.timestamp, amount: p.close.amount }
|
|
319
|
+
]).sort((a, b) => a.ts - b.ts);
|
|
320
|
+
|
|
321
|
+
// src/utils/stat.ts
|
|
322
|
+
var calcStreaks = (retsAbs) => {
|
|
323
|
+
let maxW = 0, maxL = 0, cw = 0, cl = 0;
|
|
324
|
+
for (const r of retsAbs) {
|
|
325
|
+
if (r > 0) {
|
|
326
|
+
cw++;
|
|
327
|
+
cl = 0;
|
|
328
|
+
if (cw > maxW) maxW = cw;
|
|
329
|
+
} else if (r < 0) {
|
|
330
|
+
cl++;
|
|
331
|
+
cw = 0;
|
|
332
|
+
if (cl > maxL) maxL = cl;
|
|
333
|
+
} else {
|
|
334
|
+
cw = 0;
|
|
335
|
+
cl = 0;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return { maxConsecutiveWins: maxW, maxConsecutiveLosses: maxL };
|
|
339
|
+
};
|
|
340
|
+
var calculateMaxDrawdown = (amounts) => {
|
|
341
|
+
let max = amounts[0];
|
|
342
|
+
let maxDrawdown = 0;
|
|
343
|
+
for (const amount of amounts) {
|
|
344
|
+
if (amount > max) {
|
|
345
|
+
max = amount;
|
|
346
|
+
}
|
|
347
|
+
const drawdown = (max - amount) / max * 100;
|
|
348
|
+
if (drawdown > maxDrawdown) {
|
|
349
|
+
maxDrawdown = drawdown;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return maxDrawdown;
|
|
353
|
+
};
|
|
354
|
+
var computeMonthlyEquityStats = (positionLogData, opts) => {
|
|
355
|
+
const MAR = opts?.mar ?? 0;
|
|
356
|
+
const useSample = !!opts?.sampleStd;
|
|
357
|
+
const tsMul = (opts?.tsUnit ?? "ms") === "s" ? 1e3 : 1;
|
|
358
|
+
if (!positionLogData.length) {
|
|
359
|
+
return {
|
|
360
|
+
eomSeries: [],
|
|
361
|
+
monthlyReturns: [],
|
|
362
|
+
monthlyMean: 0,
|
|
363
|
+
monthlyStd: 0,
|
|
364
|
+
monthlyDownsideStd: 0,
|
|
365
|
+
sharpeMonthly: null,
|
|
366
|
+
sharpeMonthlyAnnualized: null,
|
|
367
|
+
sortinoMonthly: null,
|
|
368
|
+
sortinoMonthlyAnnualized: null,
|
|
369
|
+
positiveMonths: 0,
|
|
370
|
+
maxMonthlyGain: 0,
|
|
371
|
+
maxMonthlyDrop: 0
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
const equityPoints2 = positionLogData.flatMap((p) => [
|
|
375
|
+
{ ts: p.open.timestamp * tsMul, amount: p.open.amount },
|
|
376
|
+
{ ts: p.close.timestamp * tsMul, amount: p.close.amount }
|
|
377
|
+
]).sort((a, b) => a.ts - b.ts);
|
|
378
|
+
const startTs = equityPoints2[0].ts;
|
|
379
|
+
const endTs = equityPoints2[equityPoints2.length - 1].ts;
|
|
380
|
+
const eomSeries = [];
|
|
381
|
+
let monthCursor = (0, import_date_fns3.startOfMonth)(new Date(startTs));
|
|
382
|
+
const lastMonth = (0, import_date_fns3.endOfMonth)(new Date(endTs));
|
|
383
|
+
let i = 0;
|
|
384
|
+
let lastAmount = equityPoints2[0].amount;
|
|
385
|
+
while (monthCursor <= lastMonth) {
|
|
386
|
+
const eom = (0, import_date_fns3.endOfMonth)(monthCursor);
|
|
387
|
+
const eomTs = eom.getTime();
|
|
388
|
+
while (i < equityPoints2.length && equityPoints2[i].ts <= eomTs) {
|
|
389
|
+
lastAmount = equityPoints2[i].amount;
|
|
390
|
+
i += 1;
|
|
391
|
+
}
|
|
392
|
+
const key = `${eom.getFullYear()}-${String(eom.getMonth() + 1).padStart(2, "0")}`;
|
|
393
|
+
eomSeries.push({ month: key, ts: eomTs, amount: lastAmount });
|
|
394
|
+
monthCursor = (0, import_date_fns3.addMonths)(monthCursor, 1);
|
|
395
|
+
}
|
|
396
|
+
const monthlyReturns = [];
|
|
397
|
+
for (let k = 1; k < eomSeries.length; k++) {
|
|
398
|
+
const prev = eomSeries[k - 1].amount;
|
|
399
|
+
const curr = eomSeries[k].amount;
|
|
400
|
+
monthlyReturns.push(prev > 0 ? curr / prev - 1 : 0);
|
|
401
|
+
}
|
|
402
|
+
const n = monthlyReturns.length;
|
|
403
|
+
const monthlyMean = n ? monthlyReturns.reduce((a, b) => a + b, 0) / n : 0;
|
|
404
|
+
const variance = n ? monthlyReturns.reduce((a, v) => a + (v - monthlyMean) ** 2, 0) / (useSample && n > 1 ? n - 1 : n) : 0;
|
|
405
|
+
const monthlyStd = Math.sqrt(variance);
|
|
406
|
+
const downside = monthlyReturns.map((r) => Math.min(r - MAR, 0)).filter((v) => v < 0);
|
|
407
|
+
const nd = downside.length;
|
|
408
|
+
const downsideVar = nd ? downside.reduce((a, v) => a + v * v, 0) / (useSample && nd > 1 ? nd - 1 : nd) : 0;
|
|
409
|
+
const monthlyDownsideStd = Math.sqrt(downsideVar);
|
|
410
|
+
const sharpeMonthly = monthlyStd > 0 ? (monthlyMean - MAR) / monthlyStd : null;
|
|
411
|
+
const sortinoMonthly = monthlyDownsideStd > 0 ? (monthlyMean - MAR) / monthlyDownsideStd : null;
|
|
412
|
+
const sharpeMonthlyAnnualized = sharpeMonthly === null ? null : sharpeMonthly * Math.sqrt(12);
|
|
413
|
+
const sortinoMonthlyAnnualized = sortinoMonthly === null ? null : sortinoMonthly * Math.sqrt(12);
|
|
414
|
+
const positiveMonths = monthlyReturns.filter((r) => r > 0).length;
|
|
415
|
+
const maxMonthlyGain = n ? Math.max(...monthlyReturns) : 0;
|
|
416
|
+
const maxMonthlyDrop = n ? Math.min(...monthlyReturns) : 0;
|
|
417
|
+
return {
|
|
418
|
+
eomSeries,
|
|
419
|
+
monthlyReturns,
|
|
420
|
+
monthlyMean,
|
|
421
|
+
monthlyStd,
|
|
422
|
+
monthlyDownsideStd,
|
|
423
|
+
sharpeMonthly,
|
|
424
|
+
sharpeMonthlyAnnualized,
|
|
425
|
+
sortinoMonthly,
|
|
426
|
+
sortinoMonthlyAnnualized,
|
|
427
|
+
positiveMonths,
|
|
428
|
+
maxMonthlyGain,
|
|
429
|
+
maxMonthlyDrop
|
|
430
|
+
};
|
|
431
|
+
};
|
|
432
|
+
var calculateStatsFull = (positionLogData) => {
|
|
433
|
+
if (!positionLogData.length) return null;
|
|
434
|
+
const retsAbs = absReturns(positionLogData);
|
|
435
|
+
const retsRel = relReturns(positionLogData);
|
|
436
|
+
const points = equityPoints(positionLogData);
|
|
437
|
+
const startTs = points[0].ts;
|
|
438
|
+
const endTs = points[points.length - 1].ts;
|
|
439
|
+
const periodMs = (0, import_date_fns3.differenceInMilliseconds)(new Date(endTs), new Date(startTs));
|
|
440
|
+
const periodDays = periodMs / (1e3 * 60 * 60 * 24);
|
|
441
|
+
const periodMonths = periodDays / 30.4375;
|
|
442
|
+
const trades = positionLogData.length;
|
|
443
|
+
const tradesPerMonth = periodMonths > 0 ? trades / periodMonths : 0;
|
|
444
|
+
const durations = positionLogData.map(
|
|
445
|
+
(p) => p.close.timestamp - p.open.timestamp
|
|
446
|
+
);
|
|
447
|
+
const totalTime = endTs - startTs;
|
|
448
|
+
const exposure = totalTime > 0 ? sum(durations) / totalTime * 100 : 0;
|
|
449
|
+
const initialAmount = points[0].amount;
|
|
450
|
+
const finalAmount = points[points.length - 1].amount;
|
|
451
|
+
const netProfit = finalAmount - initialAmount;
|
|
452
|
+
const totalReturn = initialAmount > 0 ? (finalAmount / initialAmount - 1) * 100 : 0;
|
|
453
|
+
const cagr = periodMonths > 0 && initialAmount > 0 ? (Math.pow(finalAmount / initialAmount, 12 / periodMonths) - 1) * 100 : 0;
|
|
454
|
+
const allAmounts = points.map((p) => p.amount);
|
|
455
|
+
const maxDrawdown = calculateMaxDrawdown(allAmounts);
|
|
456
|
+
const calmar = maxDrawdown > 0 ? cagr / maxDrawdown : null;
|
|
457
|
+
const wins = retsAbs.filter((x) => x > 0).length;
|
|
458
|
+
const losses = retsAbs.filter((x) => x <= 0).length;
|
|
459
|
+
const winRate = trades ? wins / trades * 100 : 0;
|
|
460
|
+
const avgWinAbs = mean(retsAbs.filter((x) => x > 0));
|
|
461
|
+
const avgLossAbs = Math.abs(mean(retsAbs.filter((x) => x < 0)));
|
|
462
|
+
const payoff = avgLossAbs > 0 ? avgWinAbs / avgLossAbs : null;
|
|
463
|
+
const avgWinRel = mean(retsRel.filter((x) => x > 0));
|
|
464
|
+
const avgLossRel = Math.abs(mean(retsRel.filter((x) => x < 0)));
|
|
465
|
+
const pWin = trades ? wins / trades : 0;
|
|
466
|
+
const expectancyPerTrade = (pWin * avgWinRel - (1 - pWin) * avgLossRel) * 100;
|
|
467
|
+
const { maxConsecutiveWins, maxConsecutiveLosses } = calcStreaks(retsAbs);
|
|
468
|
+
const monthly = computeMonthlyEquityStats(positionLogData, {
|
|
469
|
+
mar: 0,
|
|
470
|
+
// MAR=0, при желании можно параметризовать
|
|
471
|
+
sampleStd: false,
|
|
472
|
+
// population std
|
|
473
|
+
tsUnit: "ms"
|
|
474
|
+
// timestamps в миллисекундах
|
|
475
|
+
});
|
|
476
|
+
const sharpe = (monthly.sharpeMonthly ?? null) !== null ? monthly.sharpeMonthly * Math.sqrt(12) : null;
|
|
477
|
+
const res = {
|
|
478
|
+
// Период и частота
|
|
479
|
+
periodDays: round(periodDays),
|
|
480
|
+
periodMonths: round(periodMonths),
|
|
481
|
+
orders: trades,
|
|
482
|
+
wins,
|
|
483
|
+
losses,
|
|
484
|
+
ordersPerMonth: round(tradesPerMonth),
|
|
485
|
+
exposure: round(exposure),
|
|
486
|
+
// Доходность
|
|
487
|
+
amount: round(finalAmount),
|
|
488
|
+
maxAmount: round(Math.max(...allAmounts)),
|
|
489
|
+
minAmount: round(Math.min(...allAmounts)),
|
|
490
|
+
netProfit: round(netProfit),
|
|
491
|
+
totalReturn: round(totalReturn),
|
|
492
|
+
cagr: round(cagr),
|
|
493
|
+
// Риск и Calmar
|
|
494
|
+
maxDrawdown: round(maxDrawdown),
|
|
495
|
+
calmar: calmar === null ? null : round(calmar),
|
|
496
|
+
// Качество сделок
|
|
497
|
+
winRate: round(winRate),
|
|
498
|
+
riskRewardRatio: payoff === null ? null : round(payoff),
|
|
499
|
+
expectancy: round(expectancyPerTrade),
|
|
500
|
+
maxConsecutiveWins,
|
|
501
|
+
maxConsecutiveLosses,
|
|
502
|
+
// Sharpe (годовой) по месячным ретёрнам equity
|
|
503
|
+
sharpeRatio: sharpe === null ? null : round(sharpe)
|
|
504
|
+
};
|
|
505
|
+
const score = getBacktestScore(res);
|
|
506
|
+
return {
|
|
507
|
+
...res,
|
|
508
|
+
score
|
|
509
|
+
};
|
|
510
|
+
};
|
|
511
|
+
var classifyMetric = (name, value) => {
|
|
512
|
+
const { thresholds, direction } = TestThresholdsConfig[name];
|
|
513
|
+
if (direction === "higher") {
|
|
514
|
+
if (value >= thresholds[1]) return "success";
|
|
515
|
+
if (value >= thresholds[0]) return "warning";
|
|
516
|
+
return "error";
|
|
517
|
+
} else {
|
|
518
|
+
if (value <= thresholds[1]) return "success";
|
|
519
|
+
if (value <= thresholds[0]) return "warning";
|
|
520
|
+
return "error";
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
var getBacktestScore = (stat) => {
|
|
524
|
+
if (!stat) {
|
|
525
|
+
return 0;
|
|
526
|
+
}
|
|
527
|
+
const netProfit = Number(stat.netProfit ?? 0);
|
|
528
|
+
const winRate = Number(stat.winRate ?? 0);
|
|
529
|
+
if (!Number.isFinite(netProfit) || !Number.isFinite(winRate)) {
|
|
530
|
+
return 0;
|
|
531
|
+
}
|
|
532
|
+
return Math.round(netProfit * winRate);
|
|
533
|
+
};
|
|
534
|
+
var sortBestTests = (results, limit = 5) => {
|
|
535
|
+
return results.sort((a, b) => (b.stat.amount ?? 0) - (a.stat.amount ?? 0)).slice(0, limit);
|
|
536
|
+
};
|
|
537
|
+
var getFormatted = (stat, key) => {
|
|
538
|
+
if (!stat) {
|
|
539
|
+
return {
|
|
540
|
+
formatted: "0",
|
|
541
|
+
level: "error"
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
const raw = stat[key];
|
|
545
|
+
if (raw == null || typeof raw === "string") {
|
|
546
|
+
return {
|
|
547
|
+
formatted: String(raw ?? "-"),
|
|
548
|
+
level: "error"
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
const config = TestThresholdsConfig[key];
|
|
552
|
+
const level = config ? classifyMetric(key, raw) : "success";
|
|
553
|
+
const formatted = config ? `${raw.toFixed(config.precision)}${config.isPercent ? "%" : ""}${config.isAmount ? "$" : ""}` : String(raw);
|
|
554
|
+
return {
|
|
555
|
+
formatted,
|
|
556
|
+
level
|
|
557
|
+
};
|
|
558
|
+
};
|
|
559
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
560
|
+
0 && (module.exports = {
|
|
561
|
+
calculateMaxDrawdown,
|
|
562
|
+
calculateStatsFull,
|
|
563
|
+
classifyMetric,
|
|
564
|
+
compactOrderLog,
|
|
565
|
+
createTestSuite,
|
|
566
|
+
generateName,
|
|
567
|
+
generateParamGrid,
|
|
568
|
+
getBacktestScore,
|
|
569
|
+
getFormatted,
|
|
570
|
+
getTimeline,
|
|
571
|
+
mergeConfigs,
|
|
572
|
+
parseTestName,
|
|
573
|
+
sortBestTests
|
|
574
|
+
});
|