@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
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import {
|
|
2
|
+
uuid
|
|
3
|
+
} from "./chunk-NQ7D3T4E.mjs";
|
|
4
|
+
import {
|
|
5
|
+
absReturns,
|
|
6
|
+
equityPoints,
|
|
7
|
+
mean,
|
|
8
|
+
relReturns,
|
|
9
|
+
round,
|
|
10
|
+
sum
|
|
11
|
+
} from "./chunk-AYC2QVKI.mjs";
|
|
12
|
+
import {
|
|
13
|
+
compactOrderLog,
|
|
14
|
+
getTimeline,
|
|
15
|
+
getTimestamp
|
|
16
|
+
} from "./chunk-PXLXXXLA.mjs";
|
|
17
|
+
import {
|
|
18
|
+
BACKTEST_PRELOAD_DAYS,
|
|
19
|
+
TestThresholdsConfig
|
|
20
|
+
} from "./chunk-JG2QPVAV.mjs";
|
|
21
|
+
|
|
22
|
+
// src/utils/grid.ts
|
|
23
|
+
import _ from "lodash";
|
|
24
|
+
var generateParamGrid = (paramOptions) => {
|
|
25
|
+
const keys = Object.keys(paramOptions);
|
|
26
|
+
const combinations = [];
|
|
27
|
+
const helper = (index = 0, current = {}) => {
|
|
28
|
+
if (index === keys.length) {
|
|
29
|
+
combinations.push(current);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const key = keys[index];
|
|
33
|
+
for (const value of paramOptions[key] || []) {
|
|
34
|
+
const copiedValue = typeof value === "object" && value !== null ? structuredClone(value) : value;
|
|
35
|
+
helper(index + 1, {
|
|
36
|
+
...current,
|
|
37
|
+
[key]: copiedValue
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
helper();
|
|
42
|
+
return combinations;
|
|
43
|
+
};
|
|
44
|
+
var generateName = (prefix) => `${prefix}_${uuid(6)}`;
|
|
45
|
+
var mergeConfigs = (configs) => {
|
|
46
|
+
const result = {};
|
|
47
|
+
for (const config of configs) {
|
|
48
|
+
for (const [key, value] of Object.entries(config)) {
|
|
49
|
+
if (!result[key]) {
|
|
50
|
+
result[key] = [];
|
|
51
|
+
}
|
|
52
|
+
const clonedValue = typeof value === "object" && value !== null ? _.cloneDeep(value) : value;
|
|
53
|
+
const isDuplicate = result[key].some(
|
|
54
|
+
(existing) => _.isEqual(existing, value)
|
|
55
|
+
);
|
|
56
|
+
if (!isDuplicate) {
|
|
57
|
+
result[key].push(clonedValue);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const key in result) {
|
|
62
|
+
if (result[key].every((v) => typeof v === "number")) {
|
|
63
|
+
result[key] = _.sortBy(result[key]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
};
|
|
68
|
+
var createTestSuite = (userName, tickers, strategyName, backtestConfig, connectorName) => {
|
|
69
|
+
const start = getTimestamp(BACKTEST_PRELOAD_DAYS);
|
|
70
|
+
const end = getTimestamp();
|
|
71
|
+
const testSuiteId = uuid(6);
|
|
72
|
+
const paramGrid = generateParamGrid(backtestConfig);
|
|
73
|
+
return tickers.flatMap(
|
|
74
|
+
(symbol) => paramGrid.map((params) => {
|
|
75
|
+
const testId = uuid(6);
|
|
76
|
+
return {
|
|
77
|
+
userName,
|
|
78
|
+
name: `${symbol}_${testSuiteId}_${testId}`,
|
|
79
|
+
testId,
|
|
80
|
+
testSuiteId,
|
|
81
|
+
symbol,
|
|
82
|
+
options: { start, end },
|
|
83
|
+
strategyName,
|
|
84
|
+
strategyConfig: params,
|
|
85
|
+
connectorName
|
|
86
|
+
};
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// src/utils/tests.ts
|
|
92
|
+
var parseTestName = (testName) => {
|
|
93
|
+
const [symbol, testSuiteId, testId] = testName.split("_");
|
|
94
|
+
return { symbol, testSuiteId, testId };
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// src/utils/stat.ts
|
|
98
|
+
import {
|
|
99
|
+
startOfMonth,
|
|
100
|
+
endOfMonth,
|
|
101
|
+
addMonths,
|
|
102
|
+
differenceInMilliseconds
|
|
103
|
+
} from "date-fns";
|
|
104
|
+
var calcStreaks = (retsAbs) => {
|
|
105
|
+
let maxW = 0, maxL = 0, cw = 0, cl = 0;
|
|
106
|
+
for (const r of retsAbs) {
|
|
107
|
+
if (r > 0) {
|
|
108
|
+
cw++;
|
|
109
|
+
cl = 0;
|
|
110
|
+
if (cw > maxW) maxW = cw;
|
|
111
|
+
} else if (r < 0) {
|
|
112
|
+
cl++;
|
|
113
|
+
cw = 0;
|
|
114
|
+
if (cl > maxL) maxL = cl;
|
|
115
|
+
} else {
|
|
116
|
+
cw = 0;
|
|
117
|
+
cl = 0;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { maxConsecutiveWins: maxW, maxConsecutiveLosses: maxL };
|
|
121
|
+
};
|
|
122
|
+
var calculateMaxDrawdown = (amounts) => {
|
|
123
|
+
let max = amounts[0];
|
|
124
|
+
let maxDrawdown = 0;
|
|
125
|
+
for (const amount of amounts) {
|
|
126
|
+
if (amount > max) {
|
|
127
|
+
max = amount;
|
|
128
|
+
}
|
|
129
|
+
const drawdown = (max - amount) / max * 100;
|
|
130
|
+
if (drawdown > maxDrawdown) {
|
|
131
|
+
maxDrawdown = drawdown;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return maxDrawdown;
|
|
135
|
+
};
|
|
136
|
+
var computeMonthlyEquityStats = (positionLogData, opts) => {
|
|
137
|
+
const MAR = opts?.mar ?? 0;
|
|
138
|
+
const useSample = !!opts?.sampleStd;
|
|
139
|
+
const tsMul = (opts?.tsUnit ?? "ms") === "s" ? 1e3 : 1;
|
|
140
|
+
if (!positionLogData.length) {
|
|
141
|
+
return {
|
|
142
|
+
eomSeries: [],
|
|
143
|
+
monthlyReturns: [],
|
|
144
|
+
monthlyMean: 0,
|
|
145
|
+
monthlyStd: 0,
|
|
146
|
+
monthlyDownsideStd: 0,
|
|
147
|
+
sharpeMonthly: null,
|
|
148
|
+
sharpeMonthlyAnnualized: null,
|
|
149
|
+
sortinoMonthly: null,
|
|
150
|
+
sortinoMonthlyAnnualized: null,
|
|
151
|
+
positiveMonths: 0,
|
|
152
|
+
maxMonthlyGain: 0,
|
|
153
|
+
maxMonthlyDrop: 0
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const equityPoints2 = positionLogData.flatMap((p) => [
|
|
157
|
+
{ ts: p.open.timestamp * tsMul, amount: p.open.amount },
|
|
158
|
+
{ ts: p.close.timestamp * tsMul, amount: p.close.amount }
|
|
159
|
+
]).sort((a, b) => a.ts - b.ts);
|
|
160
|
+
const startTs = equityPoints2[0].ts;
|
|
161
|
+
const endTs = equityPoints2[equityPoints2.length - 1].ts;
|
|
162
|
+
const eomSeries = [];
|
|
163
|
+
let monthCursor = startOfMonth(new Date(startTs));
|
|
164
|
+
const lastMonth = endOfMonth(new Date(endTs));
|
|
165
|
+
let i = 0;
|
|
166
|
+
let lastAmount = equityPoints2[0].amount;
|
|
167
|
+
while (monthCursor <= lastMonth) {
|
|
168
|
+
const eom = endOfMonth(monthCursor);
|
|
169
|
+
const eomTs = eom.getTime();
|
|
170
|
+
while (i < equityPoints2.length && equityPoints2[i].ts <= eomTs) {
|
|
171
|
+
lastAmount = equityPoints2[i].amount;
|
|
172
|
+
i += 1;
|
|
173
|
+
}
|
|
174
|
+
const key = `${eom.getFullYear()}-${String(eom.getMonth() + 1).padStart(2, "0")}`;
|
|
175
|
+
eomSeries.push({ month: key, ts: eomTs, amount: lastAmount });
|
|
176
|
+
monthCursor = addMonths(monthCursor, 1);
|
|
177
|
+
}
|
|
178
|
+
const monthlyReturns = [];
|
|
179
|
+
for (let k = 1; k < eomSeries.length; k++) {
|
|
180
|
+
const prev = eomSeries[k - 1].amount;
|
|
181
|
+
const curr = eomSeries[k].amount;
|
|
182
|
+
monthlyReturns.push(prev > 0 ? curr / prev - 1 : 0);
|
|
183
|
+
}
|
|
184
|
+
const n = monthlyReturns.length;
|
|
185
|
+
const monthlyMean = n ? monthlyReturns.reduce((a, b) => a + b, 0) / n : 0;
|
|
186
|
+
const variance = n ? monthlyReturns.reduce((a, v) => a + (v - monthlyMean) ** 2, 0) / (useSample && n > 1 ? n - 1 : n) : 0;
|
|
187
|
+
const monthlyStd = Math.sqrt(variance);
|
|
188
|
+
const downside = monthlyReturns.map((r) => Math.min(r - MAR, 0)).filter((v) => v < 0);
|
|
189
|
+
const nd = downside.length;
|
|
190
|
+
const downsideVar = nd ? downside.reduce((a, v) => a + v * v, 0) / (useSample && nd > 1 ? nd - 1 : nd) : 0;
|
|
191
|
+
const monthlyDownsideStd = Math.sqrt(downsideVar);
|
|
192
|
+
const sharpeMonthly = monthlyStd > 0 ? (monthlyMean - MAR) / monthlyStd : null;
|
|
193
|
+
const sortinoMonthly = monthlyDownsideStd > 0 ? (monthlyMean - MAR) / monthlyDownsideStd : null;
|
|
194
|
+
const sharpeMonthlyAnnualized = sharpeMonthly === null ? null : sharpeMonthly * Math.sqrt(12);
|
|
195
|
+
const sortinoMonthlyAnnualized = sortinoMonthly === null ? null : sortinoMonthly * Math.sqrt(12);
|
|
196
|
+
const positiveMonths = monthlyReturns.filter((r) => r > 0).length;
|
|
197
|
+
const maxMonthlyGain = n ? Math.max(...monthlyReturns) : 0;
|
|
198
|
+
const maxMonthlyDrop = n ? Math.min(...monthlyReturns) : 0;
|
|
199
|
+
return {
|
|
200
|
+
eomSeries,
|
|
201
|
+
monthlyReturns,
|
|
202
|
+
monthlyMean,
|
|
203
|
+
monthlyStd,
|
|
204
|
+
monthlyDownsideStd,
|
|
205
|
+
sharpeMonthly,
|
|
206
|
+
sharpeMonthlyAnnualized,
|
|
207
|
+
sortinoMonthly,
|
|
208
|
+
sortinoMonthlyAnnualized,
|
|
209
|
+
positiveMonths,
|
|
210
|
+
maxMonthlyGain,
|
|
211
|
+
maxMonthlyDrop
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
var calculateStatsFull = (positionLogData) => {
|
|
215
|
+
if (!positionLogData.length) return null;
|
|
216
|
+
const retsAbs = absReturns(positionLogData);
|
|
217
|
+
const retsRel = relReturns(positionLogData);
|
|
218
|
+
const points = equityPoints(positionLogData);
|
|
219
|
+
const startTs = points[0].ts;
|
|
220
|
+
const endTs = points[points.length - 1].ts;
|
|
221
|
+
const periodMs = differenceInMilliseconds(new Date(endTs), new Date(startTs));
|
|
222
|
+
const periodDays = periodMs / (1e3 * 60 * 60 * 24);
|
|
223
|
+
const periodMonths = periodDays / 30.4375;
|
|
224
|
+
const trades = positionLogData.length;
|
|
225
|
+
const tradesPerMonth = periodMonths > 0 ? trades / periodMonths : 0;
|
|
226
|
+
const durations = positionLogData.map(
|
|
227
|
+
(p) => p.close.timestamp - p.open.timestamp
|
|
228
|
+
);
|
|
229
|
+
const totalTime = endTs - startTs;
|
|
230
|
+
const exposure = totalTime > 0 ? sum(durations) / totalTime * 100 : 0;
|
|
231
|
+
const initialAmount = points[0].amount;
|
|
232
|
+
const finalAmount = points[points.length - 1].amount;
|
|
233
|
+
const netProfit = finalAmount - initialAmount;
|
|
234
|
+
const totalReturn = initialAmount > 0 ? (finalAmount / initialAmount - 1) * 100 : 0;
|
|
235
|
+
const cagr = periodMonths > 0 && initialAmount > 0 ? (Math.pow(finalAmount / initialAmount, 12 / periodMonths) - 1) * 100 : 0;
|
|
236
|
+
const allAmounts = points.map((p) => p.amount);
|
|
237
|
+
const maxDrawdown = calculateMaxDrawdown(allAmounts);
|
|
238
|
+
const calmar = maxDrawdown > 0 ? cagr / maxDrawdown : null;
|
|
239
|
+
const wins = retsAbs.filter((x) => x > 0).length;
|
|
240
|
+
const losses = retsAbs.filter((x) => x <= 0).length;
|
|
241
|
+
const winRate = trades ? wins / trades * 100 : 0;
|
|
242
|
+
const avgWinAbs = mean(retsAbs.filter((x) => x > 0));
|
|
243
|
+
const avgLossAbs = Math.abs(mean(retsAbs.filter((x) => x < 0)));
|
|
244
|
+
const payoff = avgLossAbs > 0 ? avgWinAbs / avgLossAbs : null;
|
|
245
|
+
const avgWinRel = mean(retsRel.filter((x) => x > 0));
|
|
246
|
+
const avgLossRel = Math.abs(mean(retsRel.filter((x) => x < 0)));
|
|
247
|
+
const pWin = trades ? wins / trades : 0;
|
|
248
|
+
const expectancyPerTrade = (pWin * avgWinRel - (1 - pWin) * avgLossRel) * 100;
|
|
249
|
+
const { maxConsecutiveWins, maxConsecutiveLosses } = calcStreaks(retsAbs);
|
|
250
|
+
const monthly = computeMonthlyEquityStats(positionLogData, {
|
|
251
|
+
mar: 0,
|
|
252
|
+
// MAR=0, при желании можно параметризовать
|
|
253
|
+
sampleStd: false,
|
|
254
|
+
// population std
|
|
255
|
+
tsUnit: "ms"
|
|
256
|
+
// timestamps в миллисекундах
|
|
257
|
+
});
|
|
258
|
+
const sharpe = (monthly.sharpeMonthly ?? null) !== null ? monthly.sharpeMonthly * Math.sqrt(12) : null;
|
|
259
|
+
const res = {
|
|
260
|
+
// Период и частота
|
|
261
|
+
periodDays: round(periodDays),
|
|
262
|
+
periodMonths: round(periodMonths),
|
|
263
|
+
orders: trades,
|
|
264
|
+
wins,
|
|
265
|
+
losses,
|
|
266
|
+
ordersPerMonth: round(tradesPerMonth),
|
|
267
|
+
exposure: round(exposure),
|
|
268
|
+
// Доходность
|
|
269
|
+
amount: round(finalAmount),
|
|
270
|
+
maxAmount: round(Math.max(...allAmounts)),
|
|
271
|
+
minAmount: round(Math.min(...allAmounts)),
|
|
272
|
+
netProfit: round(netProfit),
|
|
273
|
+
totalReturn: round(totalReturn),
|
|
274
|
+
cagr: round(cagr),
|
|
275
|
+
// Риск и Calmar
|
|
276
|
+
maxDrawdown: round(maxDrawdown),
|
|
277
|
+
calmar: calmar === null ? null : round(calmar),
|
|
278
|
+
// Качество сделок
|
|
279
|
+
winRate: round(winRate),
|
|
280
|
+
riskRewardRatio: payoff === null ? null : round(payoff),
|
|
281
|
+
expectancy: round(expectancyPerTrade),
|
|
282
|
+
maxConsecutiveWins,
|
|
283
|
+
maxConsecutiveLosses,
|
|
284
|
+
// Sharpe (годовой) по месячным ретёрнам equity
|
|
285
|
+
sharpeRatio: sharpe === null ? null : round(sharpe)
|
|
286
|
+
};
|
|
287
|
+
const score = getBacktestScore(res);
|
|
288
|
+
return {
|
|
289
|
+
...res,
|
|
290
|
+
score
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
var classifyMetric = (name, value) => {
|
|
294
|
+
const { thresholds, direction } = TestThresholdsConfig[name];
|
|
295
|
+
if (direction === "higher") {
|
|
296
|
+
if (value >= thresholds[1]) return "success";
|
|
297
|
+
if (value >= thresholds[0]) return "warning";
|
|
298
|
+
return "error";
|
|
299
|
+
} else {
|
|
300
|
+
if (value <= thresholds[1]) return "success";
|
|
301
|
+
if (value <= thresholds[0]) return "warning";
|
|
302
|
+
return "error";
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
var getBacktestScore = (stat) => {
|
|
306
|
+
if (!stat) {
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
const netProfit = Number(stat.netProfit ?? 0);
|
|
310
|
+
const winRate = Number(stat.winRate ?? 0);
|
|
311
|
+
if (!Number.isFinite(netProfit) || !Number.isFinite(winRate)) {
|
|
312
|
+
return 0;
|
|
313
|
+
}
|
|
314
|
+
return Math.round(netProfit * winRate);
|
|
315
|
+
};
|
|
316
|
+
var sortBestTests = (results, limit = 5) => {
|
|
317
|
+
return results.sort((a, b) => (b.stat.amount ?? 0) - (a.stat.amount ?? 0)).slice(0, limit);
|
|
318
|
+
};
|
|
319
|
+
var getFormatted = (stat, key) => {
|
|
320
|
+
if (!stat) {
|
|
321
|
+
return {
|
|
322
|
+
formatted: "0",
|
|
323
|
+
level: "error"
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const raw = stat[key];
|
|
327
|
+
if (raw == null || typeof raw === "string") {
|
|
328
|
+
return {
|
|
329
|
+
formatted: String(raw ?? "-"),
|
|
330
|
+
level: "error"
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const config = TestThresholdsConfig[key];
|
|
334
|
+
const level = config ? classifyMetric(key, raw) : "success";
|
|
335
|
+
const formatted = config ? `${raw.toFixed(config.precision)}${config.isPercent ? "%" : ""}${config.isAmount ? "$" : ""}` : String(raw);
|
|
336
|
+
return {
|
|
337
|
+
formatted,
|
|
338
|
+
level
|
|
339
|
+
};
|
|
340
|
+
};
|
|
341
|
+
export {
|
|
342
|
+
calculateMaxDrawdown,
|
|
343
|
+
calculateStatsFull,
|
|
344
|
+
classifyMetric,
|
|
345
|
+
compactOrderLog,
|
|
346
|
+
createTestSuite,
|
|
347
|
+
generateName,
|
|
348
|
+
generateParamGrid,
|
|
349
|
+
getBacktestScore,
|
|
350
|
+
getFormatted,
|
|
351
|
+
getTimeline,
|
|
352
|
+
mergeConfigs,
|
|
353
|
+
parseTestName,
|
|
354
|
+
sortBestTests
|
|
355
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/utils/math.ts
|
|
2
|
+
import _ from "lodash";
|
|
3
|
+
var diffRel = (a, b) => {
|
|
4
|
+
const min = _.min([a, b]) || 0;
|
|
5
|
+
const max = _.max([a, b]) || 0;
|
|
6
|
+
if (!min || !max) {
|
|
7
|
+
return 0;
|
|
8
|
+
}
|
|
9
|
+
return (max - min) / max;
|
|
10
|
+
};
|
|
11
|
+
var round = (value, precision = 2) => precision > 0 ? Math.round(value * 10 ** precision) / 10 ** precision : Math.round(value);
|
|
12
|
+
var sum = (xs) => xs.reduce((a, b) => a + b, 0);
|
|
13
|
+
var mean = (xs) => xs.length ? sum(xs) / xs.length : 0;
|
|
14
|
+
var absReturns = (data) => data.map((p) => p.close.amount - p.open.amount);
|
|
15
|
+
var relReturns = (data) => data.map((p) => (p.close.amount - p.open.amount) / p.open.amount);
|
|
16
|
+
var equityPoints = (data) => data.flatMap((p) => [
|
|
17
|
+
{ ts: p.open.timestamp, amount: p.open.amount },
|
|
18
|
+
{ ts: p.close.timestamp, amount: p.close.amount }
|
|
19
|
+
]).sort((a, b) => a.ts - b.ts);
|
|
20
|
+
var formatNumber = (n, digits = 6) => {
|
|
21
|
+
if (n === null || n === void 0 || !Number.isFinite(n)) return null;
|
|
22
|
+
const useDigits = Math.abs(n) >= 1e3 ? 0 : digits;
|
|
23
|
+
return n.toFixed(useDigits);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
diffRel,
|
|
28
|
+
round,
|
|
29
|
+
sum,
|
|
30
|
+
mean,
|
|
31
|
+
absReturns,
|
|
32
|
+
relReturns,
|
|
33
|
+
equityPoints,
|
|
34
|
+
formatNumber
|
|
35
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// src/constants/index.ts
|
|
2
|
+
var FEE_PERCENT = 5e-3;
|
|
3
|
+
var CORRELATION_WINDOW = 50;
|
|
4
|
+
var SPREAD_WINDOW = 50;
|
|
5
|
+
var PRELOAD_DAYS = 200;
|
|
6
|
+
var SIGNALS_PRELOAD_DAYS = 60;
|
|
7
|
+
var BACKTEST_PRELOAD_DAYS = 160;
|
|
8
|
+
var DASHBOARD_PRELOAD_DAYS = 160;
|
|
9
|
+
var BOT_PRELOAD_DAYS = 160;
|
|
10
|
+
var PRELOAD_FALLBACK_DAYS = 160;
|
|
11
|
+
var TTL_1H = 3600;
|
|
12
|
+
var TTL_3H = 10800;
|
|
13
|
+
var TTL_12H = 43300;
|
|
14
|
+
var TTL_1D = 86400;
|
|
15
|
+
var TTL_1M = 26e5;
|
|
16
|
+
var TTL_3M = 78e5;
|
|
17
|
+
var TESTS_TOP_LIMIT = 50;
|
|
18
|
+
var TESTS_LIMIT = 1e5;
|
|
19
|
+
var TESTS_ORDERS_MIN_LIMIT = 3;
|
|
20
|
+
var MARKET_CATEGORY = "linear";
|
|
21
|
+
var ML_CANDLE_FEATURE_WINDOW = 50;
|
|
22
|
+
var ML_BASE_CANDLES_WINDOW = 50;
|
|
23
|
+
var TRENDLINE_DEFAULTS = {
|
|
24
|
+
maxLines: 20,
|
|
25
|
+
range: 15,
|
|
26
|
+
firstRange: 80,
|
|
27
|
+
epsilon: 3e-3,
|
|
28
|
+
epsilonOffset: 5e-3,
|
|
29
|
+
minTouches: 4,
|
|
30
|
+
minDistance: 50,
|
|
31
|
+
minTouchGap: 15,
|
|
32
|
+
maxTouchGap: 60,
|
|
33
|
+
offset: 1e3,
|
|
34
|
+
capture: false,
|
|
35
|
+
bestLines: 4,
|
|
36
|
+
maxDistance: 2e3
|
|
37
|
+
};
|
|
38
|
+
var TestThresholdsConfig = {
|
|
39
|
+
// Период и частота — используем как требования к качеству теста, в скоринг не влияют
|
|
40
|
+
periodDays: {
|
|
41
|
+
thresholds: [30, 120],
|
|
42
|
+
direction: "higher",
|
|
43
|
+
precision: 0
|
|
44
|
+
},
|
|
45
|
+
periodMonths: {
|
|
46
|
+
thresholds: [1, 6],
|
|
47
|
+
direction: "higher",
|
|
48
|
+
precision: 2
|
|
49
|
+
},
|
|
50
|
+
orders: {
|
|
51
|
+
thresholds: [30, 200],
|
|
52
|
+
direction: "higher",
|
|
53
|
+
precision: 0
|
|
54
|
+
},
|
|
55
|
+
wins: {
|
|
56
|
+
thresholds: [30, 100],
|
|
57
|
+
direction: "higher",
|
|
58
|
+
precision: 0
|
|
59
|
+
},
|
|
60
|
+
losses: {
|
|
61
|
+
thresholds: [20, 50],
|
|
62
|
+
direction: "lower",
|
|
63
|
+
precision: 0
|
|
64
|
+
},
|
|
65
|
+
ordersPerMonth: {
|
|
66
|
+
thresholds: [4, 20],
|
|
67
|
+
direction: "higher",
|
|
68
|
+
precision: 2
|
|
69
|
+
},
|
|
70
|
+
exposure: {
|
|
71
|
+
thresholds: [20, 60],
|
|
72
|
+
direction: "higher",
|
|
73
|
+
isPercent: true,
|
|
74
|
+
precision: 1
|
|
75
|
+
},
|
|
76
|
+
// Доходность
|
|
77
|
+
amount: {
|
|
78
|
+
thresholds: [105, 120],
|
|
79
|
+
direction: "higher",
|
|
80
|
+
isAmount: true,
|
|
81
|
+
precision: 2
|
|
82
|
+
},
|
|
83
|
+
maxAmount: {
|
|
84
|
+
thresholds: [140, 180],
|
|
85
|
+
direction: "higher",
|
|
86
|
+
isAmount: true,
|
|
87
|
+
precision: 2
|
|
88
|
+
},
|
|
89
|
+
minAmount: {
|
|
90
|
+
thresholds: [80, 90],
|
|
91
|
+
direction: "higher",
|
|
92
|
+
isAmount: true,
|
|
93
|
+
precision: 2
|
|
94
|
+
},
|
|
95
|
+
netProfit: {
|
|
96
|
+
thresholds: [5, 20],
|
|
97
|
+
direction: "higher",
|
|
98
|
+
isAmount: true,
|
|
99
|
+
precision: 2
|
|
100
|
+
},
|
|
101
|
+
totalReturn: {
|
|
102
|
+
thresholds: [10, 50],
|
|
103
|
+
direction: "higher",
|
|
104
|
+
isPercent: true,
|
|
105
|
+
precision: 1
|
|
106
|
+
},
|
|
107
|
+
cagr: {
|
|
108
|
+
thresholds: [15, 40],
|
|
109
|
+
direction: "higher",
|
|
110
|
+
isPercent: true,
|
|
111
|
+
precision: 1
|
|
112
|
+
},
|
|
113
|
+
// Риск и риск/доходность
|
|
114
|
+
maxDrawdown: {
|
|
115
|
+
thresholds: [25, 12],
|
|
116
|
+
direction: "lower",
|
|
117
|
+
isPercent: true,
|
|
118
|
+
precision: 1
|
|
119
|
+
},
|
|
120
|
+
calmar: {
|
|
121
|
+
thresholds: [0.5, 2],
|
|
122
|
+
direction: "higher",
|
|
123
|
+
precision: 2
|
|
124
|
+
},
|
|
125
|
+
// Качество сделок
|
|
126
|
+
winRate: {
|
|
127
|
+
thresholds: [40, 60],
|
|
128
|
+
direction: "higher",
|
|
129
|
+
isPercent: true,
|
|
130
|
+
precision: 1
|
|
131
|
+
},
|
|
132
|
+
riskRewardRatio: {
|
|
133
|
+
thresholds: [1.5, 2.5],
|
|
134
|
+
direction: "higher",
|
|
135
|
+
precision: 2
|
|
136
|
+
},
|
|
137
|
+
expectancy: {
|
|
138
|
+
thresholds: [0.3, 1],
|
|
139
|
+
direction: "higher",
|
|
140
|
+
isPercent: true,
|
|
141
|
+
precision: 2
|
|
142
|
+
},
|
|
143
|
+
maxConsecutiveWins: {
|
|
144
|
+
thresholds: [2, 6],
|
|
145
|
+
direction: "higher",
|
|
146
|
+
precision: 0
|
|
147
|
+
},
|
|
148
|
+
maxConsecutiveLosses: {
|
|
149
|
+
thresholds: [5, 2],
|
|
150
|
+
direction: "lower",
|
|
151
|
+
precision: 0
|
|
152
|
+
},
|
|
153
|
+
// Sharpe (годовой, по месячным ретернам equity)
|
|
154
|
+
sharpeRatio: {
|
|
155
|
+
thresholds: [0.5, 1.5],
|
|
156
|
+
direction: "higher",
|
|
157
|
+
precision: 2
|
|
158
|
+
},
|
|
159
|
+
score: {
|
|
160
|
+
thresholds: [10, 100],
|
|
161
|
+
direction: "higher",
|
|
162
|
+
precision: 0
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export {
|
|
167
|
+
FEE_PERCENT,
|
|
168
|
+
CORRELATION_WINDOW,
|
|
169
|
+
SPREAD_WINDOW,
|
|
170
|
+
PRELOAD_DAYS,
|
|
171
|
+
SIGNALS_PRELOAD_DAYS,
|
|
172
|
+
BACKTEST_PRELOAD_DAYS,
|
|
173
|
+
DASHBOARD_PRELOAD_DAYS,
|
|
174
|
+
BOT_PRELOAD_DAYS,
|
|
175
|
+
PRELOAD_FALLBACK_DAYS,
|
|
176
|
+
TTL_1H,
|
|
177
|
+
TTL_3H,
|
|
178
|
+
TTL_12H,
|
|
179
|
+
TTL_1D,
|
|
180
|
+
TTL_1M,
|
|
181
|
+
TTL_3M,
|
|
182
|
+
TESTS_TOP_LIMIT,
|
|
183
|
+
TESTS_LIMIT,
|
|
184
|
+
TESTS_ORDERS_MIN_LIMIT,
|
|
185
|
+
MARKET_CATEGORY,
|
|
186
|
+
ML_CANDLE_FEATURE_WINDOW,
|
|
187
|
+
ML_BASE_CANDLES_WINDOW,
|
|
188
|
+
TRENDLINE_DEFAULTS,
|
|
189
|
+
TestThresholdsConfig
|
|
190
|
+
};
|