@tradejs/core 3.0.0 → 3.1.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 +2 -0
- package/dist/backtest.mjs +4 -4
- package/dist/chunk-NYJU3J7Y.mjs +265 -0
- package/dist/http.d.mts +8 -0
- package/dist/http.d.ts +8 -0
- package/dist/http.js +79 -0
- package/dist/http.mjs +54 -0
- package/dist/indicators.mjs +2 -2
- package/dist/runtimeTrades.d.mts +82 -0
- package/dist/runtimeTrades.d.ts +82 -0
- package/dist/runtimeTrades.js +577 -0
- package/dist/runtimeTrades.mjs +478 -0
- package/dist/strategies.mjs +2 -2
- package/dist/trade.mjs +14 -250
- package/package.json +12 -2
- package/dist/{chunk-QRRQA4TU.mjs → chunk-OLHUGX7X.mjs} +3 -3
|
@@ -0,0 +1,577 @@
|
|
|
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/runtimeTrades.ts
|
|
31
|
+
var runtimeTrades_exports = {};
|
|
32
|
+
__export(runtimeTrades_exports, {
|
|
33
|
+
assignLegacyRuntimeTradeAccountScopes: () => assignLegacyRuntimeTradeAccountScopes,
|
|
34
|
+
buildRuntimeStrategyAiGateChanges: () => buildRuntimeStrategyAiGateChanges,
|
|
35
|
+
buildRuntimeStrategyAnalytics: () => buildRuntimeStrategyAnalytics,
|
|
36
|
+
buildRuntimeStrategyIdentityKey: () => buildRuntimeStrategyIdentityKey,
|
|
37
|
+
buildRuntimeStrategyMaxLossValueTimeline: () => buildRuntimeStrategyMaxLossValueTimeline,
|
|
38
|
+
getRuntimeStrategyAiGateObservedFrom: () => getRuntimeStrategyAiGateObservedFrom,
|
|
39
|
+
isRuntimeStrategyLineageScope: () => isRuntimeStrategyLineageScope,
|
|
40
|
+
isRuntimeTradeRecord: () => isRuntimeTradeRecord,
|
|
41
|
+
resolveStrategyNameByOrderLinkId: () => resolveStrategyNameByOrderLinkId,
|
|
42
|
+
selectTradesForWindow: () => selectTradesForWindow,
|
|
43
|
+
toRuntimeTradeView: () => toRuntimeTradeView
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(runtimeTrades_exports);
|
|
46
|
+
|
|
47
|
+
// src/utils/runtimeTradeAnalytics.ts
|
|
48
|
+
var import_date_fns = require("date-fns");
|
|
49
|
+
|
|
50
|
+
// src/constants/index.ts
|
|
51
|
+
var BACKTEST_EXECUTION_DELAY_MS = 5 * 6e4;
|
|
52
|
+
var INITIAL_BACKTEST_AMOUNT = 100;
|
|
53
|
+
var DERIVATIVES_CONTEXT_BASE_REFERENCE_SYMBOLS = [
|
|
54
|
+
"BTCUSDT",
|
|
55
|
+
"ETHUSDT"
|
|
56
|
+
];
|
|
57
|
+
var DERIVATIVES_CONTEXT_DEFAULT_EXTRA_REFERENCE_SYMBOLS = [
|
|
58
|
+
"BNBUSDT",
|
|
59
|
+
"SOLUSDT",
|
|
60
|
+
"TRXUSDT",
|
|
61
|
+
"XRPUSDT"
|
|
62
|
+
];
|
|
63
|
+
var DERIVATIVES_CONTEXT_REFERENCE_SYMBOLS = [
|
|
64
|
+
...DERIVATIVES_CONTEXT_BASE_REFERENCE_SYMBOLS,
|
|
65
|
+
...DERIVATIVES_CONTEXT_DEFAULT_EXTRA_REFERENCE_SYMBOLS
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// src/utils/array.ts
|
|
69
|
+
var import_lodash = __toESM(require("lodash"));
|
|
70
|
+
|
|
71
|
+
// src/trade.ts
|
|
72
|
+
var ORDER_LINK_PREFIX = "tjs-";
|
|
73
|
+
var ORDER_LINK_SEPARATOR = "--";
|
|
74
|
+
var STRATEGY_SLUG_LENGTH = 10;
|
|
75
|
+
var STRATEGY_HASH_LENGTH = 5;
|
|
76
|
+
var toBase36Hash = (value) => {
|
|
77
|
+
let hash = 0;
|
|
78
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
79
|
+
hash = hash * 31 + value.charCodeAt(index) >>> 0;
|
|
80
|
+
}
|
|
81
|
+
return hash.toString(36).padStart(STRATEGY_HASH_LENGTH, "0");
|
|
82
|
+
};
|
|
83
|
+
var normalizeStrategyOrderLinkKey = (strategyName) => {
|
|
84
|
+
const normalized = String(strategyName ?? "").trim().toLowerCase();
|
|
85
|
+
if (!normalized) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const slug = normalized.replace(/[^a-z0-9]+/g, "").slice(0, STRATEGY_SLUG_LENGTH) || "strategy";
|
|
89
|
+
const hash = toBase36Hash(normalized).slice(0, STRATEGY_HASH_LENGTH);
|
|
90
|
+
return `${slug}-${hash}`;
|
|
91
|
+
};
|
|
92
|
+
var parseStrategyOrderLinkKey = (orderLinkId) => {
|
|
93
|
+
const normalized = String(orderLinkId ?? "").trim().toLowerCase();
|
|
94
|
+
if (!normalized.startsWith(ORDER_LINK_PREFIX)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const remainder = normalized.slice(ORDER_LINK_PREFIX.length);
|
|
98
|
+
const separatorIndex = remainder.indexOf(ORDER_LINK_SEPARATOR);
|
|
99
|
+
if (separatorIndex <= 0) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const strategyPart = remainder.slice(0, separatorIndex).trim();
|
|
103
|
+
if (!strategyPart) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return strategyPart;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// src/utils/runtimeTradeAnalytics.ts
|
|
110
|
+
var MS_IN_DAY = 24 * 60 * 60 * 1e3;
|
|
111
|
+
var AVG_DAYS_IN_MONTH = 30.4375;
|
|
112
|
+
var roundValue = (value, digits = 2) => {
|
|
113
|
+
if (!Number.isFinite(value)) return 0;
|
|
114
|
+
const factor = 10 ** digits;
|
|
115
|
+
return Math.round(value * factor) / factor;
|
|
116
|
+
};
|
|
117
|
+
var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
118
|
+
var getTradePnl = (trade) => trade.status === "closed" ? trade.closedPnl ?? trade.currentPnl ?? null : trade.currentPnl ?? null;
|
|
119
|
+
var getTradeResolvedTimestamp = (trade, endTime) => typeof trade.exitTimestamp === "number" && Number.isFinite(trade.exitTimestamp) ? trade.exitTimestamp : endTime;
|
|
120
|
+
var resolveTradesWithKnownPnl = (trades, endTime) => trades.map((trade) => {
|
|
121
|
+
const resolvedPnl = getTradePnl(trade);
|
|
122
|
+
const resolvedTimestamp = getTradeResolvedTimestamp(trade, endTime);
|
|
123
|
+
if (typeof resolvedPnl !== "number" || !Number.isFinite(resolvedPnl) || !Number.isFinite(resolvedTimestamp)) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
...trade,
|
|
128
|
+
resolvedPnl,
|
|
129
|
+
resolvedTimestamp: Math.max(trade.entryTimestamp, resolvedTimestamp)
|
|
130
|
+
};
|
|
131
|
+
}).filter((trade) => trade != null).sort(
|
|
132
|
+
(left, right) => left.resolvedTimestamp - right.resolvedTimestamp || left.entryTimestamp - right.entryTimestamp
|
|
133
|
+
);
|
|
134
|
+
var calculateMaxDrawdown = (amounts) => {
|
|
135
|
+
let peak = amounts[0] ?? 0;
|
|
136
|
+
let maxDrawdown = 0;
|
|
137
|
+
for (const amount of amounts) {
|
|
138
|
+
peak = Math.max(peak, amount);
|
|
139
|
+
if (peak > 0) {
|
|
140
|
+
maxDrawdown = Math.max(maxDrawdown, (peak - amount) / peak * 100);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return roundValue(maxDrawdown);
|
|
144
|
+
};
|
|
145
|
+
var calculateSharpeRatio = (orderLog, startTime, endTime) => {
|
|
146
|
+
if (!orderLog.length || endTime <= startTime) return null;
|
|
147
|
+
const points = [...orderLog].map(([ts, amount]) => ({ ts, amount })).sort((left, right) => left.ts - right.ts);
|
|
148
|
+
const eomSeries = [];
|
|
149
|
+
let pointIndex = 0;
|
|
150
|
+
let monthCursor = (0, import_date_fns.startOfMonth)(new Date(startTime));
|
|
151
|
+
const lastMonth = (0, import_date_fns.endOfMonth)(new Date(endTime));
|
|
152
|
+
let lastAmount = points[0]?.amount ?? INITIAL_BACKTEST_AMOUNT;
|
|
153
|
+
while (monthCursor <= lastMonth) {
|
|
154
|
+
const eomTs = (0, import_date_fns.endOfMonth)(monthCursor).getTime();
|
|
155
|
+
while (pointIndex < points.length && points[pointIndex].ts <= eomTs) {
|
|
156
|
+
lastAmount = points[pointIndex].amount;
|
|
157
|
+
pointIndex += 1;
|
|
158
|
+
}
|
|
159
|
+
eomSeries.push(lastAmount);
|
|
160
|
+
monthCursor = (0, import_date_fns.addMonths)(monthCursor, 1);
|
|
161
|
+
}
|
|
162
|
+
if (eomSeries.length < 2) return null;
|
|
163
|
+
const monthlyReturns = eomSeries.slice(1).map((amount, index) => {
|
|
164
|
+
const previous = eomSeries[index];
|
|
165
|
+
return previous > 0 ? amount / previous - 1 : 0;
|
|
166
|
+
});
|
|
167
|
+
const mean = monthlyReturns.reduce((sum, value) => sum + value, 0) / monthlyReturns.length;
|
|
168
|
+
const variance = monthlyReturns.reduce((sum, value) => sum + (value - mean) ** 2, 0) / monthlyReturns.length;
|
|
169
|
+
const standardDeviation = Math.sqrt(variance);
|
|
170
|
+
return standardDeviation > 0 && Number.isFinite(standardDeviation) ? roundValue(mean / standardDeviation * Math.sqrt(12)) : null;
|
|
171
|
+
};
|
|
172
|
+
var calculateExposurePercent = (trades, startTime, endTime) => {
|
|
173
|
+
if (endTime <= startTime) return 0;
|
|
174
|
+
const intervals = trades.map((trade) => ({
|
|
175
|
+
start: Math.max(startTime, trade.entryTimestamp),
|
|
176
|
+
end: Math.min(endTime, getTradeResolvedTimestamp(trade, endTime))
|
|
177
|
+
})).filter(({ start, end }) => end > start).sort((left, right) => left.start - right.start);
|
|
178
|
+
const merged = [];
|
|
179
|
+
for (const interval of intervals) {
|
|
180
|
+
const last = merged[merged.length - 1];
|
|
181
|
+
if (!last || interval.start > last.end) {
|
|
182
|
+
merged.push({ ...interval });
|
|
183
|
+
} else {
|
|
184
|
+
last.end = Math.max(last.end, interval.end);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const coveredMs = merged.reduce(
|
|
188
|
+
(sum, interval) => sum + interval.end - interval.start,
|
|
189
|
+
0
|
|
190
|
+
);
|
|
191
|
+
return roundValue(coveredMs / (endTime - startTime) * 100);
|
|
192
|
+
};
|
|
193
|
+
var calculateStreaks = (pnls) => {
|
|
194
|
+
let currentWins = 0;
|
|
195
|
+
let currentLosses = 0;
|
|
196
|
+
let maxConsecutiveWins = 0;
|
|
197
|
+
let maxConsecutiveLosses = 0;
|
|
198
|
+
for (const pnl of pnls) {
|
|
199
|
+
if (pnl > 0) {
|
|
200
|
+
currentWins += 1;
|
|
201
|
+
currentLosses = 0;
|
|
202
|
+
} else if (pnl < 0) {
|
|
203
|
+
currentLosses += 1;
|
|
204
|
+
currentWins = 0;
|
|
205
|
+
} else {
|
|
206
|
+
currentWins = 0;
|
|
207
|
+
currentLosses = 0;
|
|
208
|
+
}
|
|
209
|
+
maxConsecutiveWins = Math.max(maxConsecutiveWins, currentWins);
|
|
210
|
+
maxConsecutiveLosses = Math.max(maxConsecutiveLosses, currentLosses);
|
|
211
|
+
}
|
|
212
|
+
return { maxConsecutiveWins, maxConsecutiveLosses };
|
|
213
|
+
};
|
|
214
|
+
var calculateSymbolConcentration = (trades, limit) => {
|
|
215
|
+
const pnlBySymbol = /* @__PURE__ */ new Map();
|
|
216
|
+
for (const trade of trades) {
|
|
217
|
+
pnlBySymbol.set(
|
|
218
|
+
trade.symbol,
|
|
219
|
+
(pnlBySymbol.get(trade.symbol) ?? 0) + Math.abs(trade.resolvedPnl)
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const values = [...pnlBySymbol.values()].sort((left, right) => right - left);
|
|
223
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
224
|
+
return total > 0 ? roundValue(
|
|
225
|
+
values.slice(0, limit).reduce((sum, value) => sum + value, 0) / total * 100
|
|
226
|
+
) : null;
|
|
227
|
+
};
|
|
228
|
+
var createEmptyStat = (startTime, endTime) => {
|
|
229
|
+
const periodDays = Math.max(0, (endTime - startTime) / MS_IN_DAY);
|
|
230
|
+
return {
|
|
231
|
+
periodDays: roundValue(periodDays),
|
|
232
|
+
periodMonths: roundValue(periodDays / AVG_DAYS_IN_MONTH),
|
|
233
|
+
orders: 0,
|
|
234
|
+
wins: 0,
|
|
235
|
+
losses: 0,
|
|
236
|
+
ordersPerMonth: 0,
|
|
237
|
+
exposure: 0,
|
|
238
|
+
amount: INITIAL_BACKTEST_AMOUNT,
|
|
239
|
+
maxAmount: INITIAL_BACKTEST_AMOUNT,
|
|
240
|
+
minAmount: INITIAL_BACKTEST_AMOUNT,
|
|
241
|
+
netProfit: 0,
|
|
242
|
+
totalReturn: 0,
|
|
243
|
+
cagr: 0,
|
|
244
|
+
maxDrawdown: 0,
|
|
245
|
+
calmar: null,
|
|
246
|
+
winRate: 0,
|
|
247
|
+
riskRewardRatio: null,
|
|
248
|
+
expectancy: 0,
|
|
249
|
+
maxConsecutiveWins: 0,
|
|
250
|
+
maxConsecutiveLosses: 0,
|
|
251
|
+
sharpeRatio: null,
|
|
252
|
+
score: 0
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
var resolveStrategyNameByOrderLinkId = ({
|
|
256
|
+
orderLinkId,
|
|
257
|
+
strategyNames
|
|
258
|
+
}) => {
|
|
259
|
+
const strategyKey = parseStrategyOrderLinkKey(orderLinkId);
|
|
260
|
+
if (!strategyKey) return null;
|
|
261
|
+
const strategyByKey = /* @__PURE__ */ new Map();
|
|
262
|
+
for (const strategyName of strategyNames) {
|
|
263
|
+
const key = normalizeStrategyOrderLinkKey(strategyName);
|
|
264
|
+
if (key && !strategyByKey.has(key)) strategyByKey.set(key, strategyName);
|
|
265
|
+
}
|
|
266
|
+
return strategyByKey.get(strategyKey) ?? null;
|
|
267
|
+
};
|
|
268
|
+
var isRuntimeTradeRecord = (value) => {
|
|
269
|
+
if (!value || typeof value !== "object") return false;
|
|
270
|
+
const record = value;
|
|
271
|
+
return typeof record.orderId === "string" && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.entryTimestamp === "number" && typeof record.entryPrice === "number" && typeof record.qty === "number";
|
|
272
|
+
};
|
|
273
|
+
var selectTradesForWindow = (trades, startTime, activeOrderIds = /* @__PURE__ */ new Set()) => trades.filter((trade) => {
|
|
274
|
+
if (trade.status === "active") {
|
|
275
|
+
return activeOrderIds.has(trade.orderId) || trade.entryTimestamp >= startTime;
|
|
276
|
+
}
|
|
277
|
+
return trade.entryTimestamp >= startTime || typeof trade.exitTimestamp === "number" && trade.exitTimestamp >= startTime;
|
|
278
|
+
});
|
|
279
|
+
var buildRuntimeStrategyAnalytics = ({
|
|
280
|
+
trades,
|
|
281
|
+
startTime,
|
|
282
|
+
endTime
|
|
283
|
+
}) => {
|
|
284
|
+
const resolvedTrades = resolveTradesWithKnownPnl(trades, endTime);
|
|
285
|
+
const orderLog = [[startTime, INITIAL_BACKTEST_AMOUNT]];
|
|
286
|
+
let runningAmount = INITIAL_BACKTEST_AMOUNT;
|
|
287
|
+
for (const trade of resolvedTrades) {
|
|
288
|
+
runningAmount = roundValue(runningAmount + trade.resolvedPnl);
|
|
289
|
+
orderLog.push([trade.resolvedTimestamp, runningAmount]);
|
|
290
|
+
}
|
|
291
|
+
if (orderLog[orderLog.length - 1]?.[0] !== endTime) {
|
|
292
|
+
orderLog.push([endTime, runningAmount]);
|
|
293
|
+
}
|
|
294
|
+
const pnls = resolvedTrades.map(({ resolvedPnl }) => resolvedPnl);
|
|
295
|
+
const wins = pnls.filter((pnl) => pnl > 0).length;
|
|
296
|
+
const losses = pnls.filter((pnl) => pnl < 0).length;
|
|
297
|
+
const averageWin = wins ? pnls.filter((pnl) => pnl > 0).reduce((sum, pnl) => sum + pnl, 0) / wins : 0;
|
|
298
|
+
const averageLoss = losses ? Math.abs(
|
|
299
|
+
pnls.filter((pnl) => pnl < 0).reduce((sum, pnl) => sum + pnl, 0) / losses
|
|
300
|
+
) : 0;
|
|
301
|
+
let amountBeforeTrade = INITIAL_BACKTEST_AMOUNT;
|
|
302
|
+
const returnSeries = resolvedTrades.map((trade) => {
|
|
303
|
+
const value = amountBeforeTrade > 0 ? trade.resolvedPnl / amountBeforeTrade : 0;
|
|
304
|
+
amountBeforeTrade += trade.resolvedPnl;
|
|
305
|
+
return value;
|
|
306
|
+
});
|
|
307
|
+
const periodDays = Math.max(0, (endTime - startTime) / MS_IN_DAY);
|
|
308
|
+
const periodMonths = periodDays / AVG_DAYS_IN_MONTH;
|
|
309
|
+
const amount = orderLog[orderLog.length - 1]?.[1] ?? INITIAL_BACKTEST_AMOUNT;
|
|
310
|
+
const netProfit = amount - INITIAL_BACKTEST_AMOUNT;
|
|
311
|
+
const totalReturn = netProfit / INITIAL_BACKTEST_AMOUNT * 100;
|
|
312
|
+
const cagr = periodMonths > 0 ? (Math.pow(amount / INITIAL_BACKTEST_AMOUNT, 12 / periodMonths) - 1) * 100 : 0;
|
|
313
|
+
const amounts = orderLog.map(([, value]) => value);
|
|
314
|
+
const maxDrawdown = calculateMaxDrawdown(amounts);
|
|
315
|
+
const streaks = calculateStreaks(pnls);
|
|
316
|
+
const stat = trades.length ? {
|
|
317
|
+
periodDays: roundValue(periodDays),
|
|
318
|
+
periodMonths: roundValue(periodMonths),
|
|
319
|
+
orders: trades.length,
|
|
320
|
+
wins,
|
|
321
|
+
losses,
|
|
322
|
+
ordersPerMonth: periodMonths ? roundValue(trades.length / periodMonths) : 0,
|
|
323
|
+
exposure: calculateExposurePercent(trades, startTime, endTime),
|
|
324
|
+
amount: roundValue(amount),
|
|
325
|
+
maxAmount: roundValue(Math.max(...amounts)),
|
|
326
|
+
minAmount: roundValue(Math.min(...amounts)),
|
|
327
|
+
netProfit: roundValue(netProfit),
|
|
328
|
+
totalReturn: roundValue(totalReturn),
|
|
329
|
+
cagr: roundValue(cagr),
|
|
330
|
+
maxDrawdown,
|
|
331
|
+
calmar: maxDrawdown > 0 ? roundValue(cagr / maxDrawdown) : null,
|
|
332
|
+
winRate: roundValue(wins / trades.length * 100),
|
|
333
|
+
riskRewardRatio: averageLoss > 0 ? roundValue(averageWin / averageLoss) : null,
|
|
334
|
+
expectancy: returnSeries.length ? roundValue(
|
|
335
|
+
returnSeries.reduce((sum, value) => sum + value, 0) / returnSeries.length * 100
|
|
336
|
+
) : 0,
|
|
337
|
+
...streaks,
|
|
338
|
+
sharpeRatio: calculateSharpeRatio(orderLog, startTime, endTime),
|
|
339
|
+
score: 0
|
|
340
|
+
} : createEmptyStat(startTime, endTime);
|
|
341
|
+
const activeTrades = trades.filter(({ status }) => status === "active");
|
|
342
|
+
const closedTrades = trades.filter(({ status }) => status === "closed");
|
|
343
|
+
const sumPnl = (rows, primary) => roundValue(
|
|
344
|
+
rows.reduce((sum, trade) => {
|
|
345
|
+
const value = trade[primary] ?? trade.currentPnl;
|
|
346
|
+
return sum + (typeof value === "number" && Number.isFinite(value) ? value : 0);
|
|
347
|
+
}, 0)
|
|
348
|
+
);
|
|
349
|
+
const activePnl = sumPnl(activeTrades, "currentPnl");
|
|
350
|
+
const closedPnl = sumPnl(closedTrades, "closedPnl");
|
|
351
|
+
const summary = {
|
|
352
|
+
totalTrades: trades.length,
|
|
353
|
+
activeTrades: activeTrades.length,
|
|
354
|
+
closedTrades: closedTrades.length,
|
|
355
|
+
wins,
|
|
356
|
+
losses,
|
|
357
|
+
activePnl,
|
|
358
|
+
closedPnl,
|
|
359
|
+
totalPnl: roundValue(activePnl + closedPnl),
|
|
360
|
+
symbolConcentrationTop1: calculateSymbolConcentration(resolvedTrades, 1),
|
|
361
|
+
symbolConcentrationTop5: calculateSymbolConcentration(resolvedTrades, 5)
|
|
362
|
+
};
|
|
363
|
+
return { orderLog, stat, summary };
|
|
364
|
+
};
|
|
365
|
+
var getLevelPercent = (trade, levelPrice, kind) => {
|
|
366
|
+
if (typeof levelPrice !== "number" || !Number.isFinite(levelPrice) || !Number.isFinite(trade.entryPrice) || trade.entryPrice <= 0) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
const raw = trade.direction === "LONG" ? (levelPrice - trade.entryPrice) / trade.entryPrice * 100 : (trade.entryPrice - levelPrice) / trade.entryPrice * 100;
|
|
370
|
+
return roundValue(kind === "stopLoss" ? Math.abs(raw) : raw);
|
|
371
|
+
};
|
|
372
|
+
var getSlippagePercent = (expectedPrice, actualPrice) => typeof expectedPrice === "number" && Number.isFinite(expectedPrice) && expectedPrice > 0 && typeof actualPrice === "number" && Number.isFinite(actualPrice) ? roundValue((actualPrice - expectedPrice) / expectedPrice * 100, 4) : null;
|
|
373
|
+
var getTotalFee = (trade) => {
|
|
374
|
+
const explicit = toFiniteNumberOrNull(trade.totalFee);
|
|
375
|
+
if (explicit != null) return explicit;
|
|
376
|
+
const fees = [trade.openFee, trade.closeFee, trade.fundingFee].map(toFiniteNumberOrNull).filter((value) => value != null);
|
|
377
|
+
return fees.length ? Number(fees.reduce((sum, value) => sum + value, 0).toFixed(12)) : null;
|
|
378
|
+
};
|
|
379
|
+
var toRuntimeTradeView = (trade, endTime = Date.now()) => {
|
|
380
|
+
const resolvedTimestamp = getTradeResolvedTimestamp(trade, endTime);
|
|
381
|
+
const durationHours = Number.isFinite(resolvedTimestamp) && resolvedTimestamp >= trade.entryTimestamp ? roundValue((resolvedTimestamp - trade.entryTimestamp) / 36e5) : null;
|
|
382
|
+
const expectedExitPrice = trade.exitType === "tp" ? trade.aiAnalysis?.takeProfitPrice : trade.exitType === "sl" ? trade.aiAnalysis?.stopLossPrice : null;
|
|
383
|
+
return {
|
|
384
|
+
orderId: trade.orderId,
|
|
385
|
+
symbol: trade.symbol,
|
|
386
|
+
direction: trade.direction,
|
|
387
|
+
status: trade.status,
|
|
388
|
+
qty: trade.qty,
|
|
389
|
+
entryTimestamp: trade.entryTimestamp,
|
|
390
|
+
entryPrice: trade.entryPrice,
|
|
391
|
+
actualEntryPrice: toFiniteNumberOrNull(trade.actualEntryPrice),
|
|
392
|
+
exitTimestamp: toFiniteNumberOrNull(trade.exitTimestamp),
|
|
393
|
+
exitPrice: toFiniteNumberOrNull(trade.exitPrice),
|
|
394
|
+
actualExitPrice: toFiniteNumberOrNull(trade.actualExitPrice),
|
|
395
|
+
currentPrice: toFiniteNumberOrNull(trade.currentPrice),
|
|
396
|
+
pnl: getTradePnl(trade),
|
|
397
|
+
durationHours,
|
|
398
|
+
entrySlippagePercent: getSlippagePercent(
|
|
399
|
+
trade.entryPrice,
|
|
400
|
+
trade.actualEntryPrice
|
|
401
|
+
),
|
|
402
|
+
exitSlippagePercent: getSlippagePercent(
|
|
403
|
+
expectedExitPrice,
|
|
404
|
+
trade.actualExitPrice ?? trade.exitPrice
|
|
405
|
+
),
|
|
406
|
+
exitType: trade.exitType ?? null,
|
|
407
|
+
takeProfitPrice: toFiniteNumberOrNull(trade.aiAnalysis?.takeProfitPrice),
|
|
408
|
+
stopLossPrice: toFiniteNumberOrNull(trade.aiAnalysis?.stopLossPrice),
|
|
409
|
+
takeProfitPercent: getLevelPercent(
|
|
410
|
+
trade,
|
|
411
|
+
trade.aiAnalysis?.takeProfitPrice,
|
|
412
|
+
"takeProfit"
|
|
413
|
+
),
|
|
414
|
+
stopLossPercent: getLevelPercent(
|
|
415
|
+
trade,
|
|
416
|
+
trade.aiAnalysis?.stopLossPrice,
|
|
417
|
+
"stopLoss"
|
|
418
|
+
),
|
|
419
|
+
openFee: toFiniteNumberOrNull(trade.openFee),
|
|
420
|
+
closeFee: toFiniteNumberOrNull(trade.closeFee),
|
|
421
|
+
fundingFee: toFiniteNumberOrNull(trade.fundingFee),
|
|
422
|
+
totalFee: getTotalFee(trade),
|
|
423
|
+
lastSyncedAt: toFiniteNumberOrNull(trade.lastSyncedAt)
|
|
424
|
+
};
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// src/utils/runtimeTradeLineage.ts
|
|
428
|
+
var buildRuntimeStrategyIdentityKey = ({
|
|
429
|
+
strategyName,
|
|
430
|
+
configId,
|
|
431
|
+
universe,
|
|
432
|
+
accountId,
|
|
433
|
+
deploymentId,
|
|
434
|
+
policyProfileId
|
|
435
|
+
}) => [
|
|
436
|
+
strategyName,
|
|
437
|
+
configId ?? "config",
|
|
438
|
+
universe ?? "crypto",
|
|
439
|
+
accountId ?? "default",
|
|
440
|
+
deploymentId ?? "default",
|
|
441
|
+
policyProfileId ?? "default"
|
|
442
|
+
].join(":");
|
|
443
|
+
var assignLegacyRuntimeTradeAccountScopes = (trades, scopes) => trades.map((trade) => {
|
|
444
|
+
if (trade.accountId || trade.deploymentId) return trade;
|
|
445
|
+
const matchingAccountIds = new Set(
|
|
446
|
+
scopes.filter(
|
|
447
|
+
(scope) => scope.strategyName === trade.strategy && scope.configId === (trade.runtimeConfigId ?? "config") && scope.universe === (trade.universe ?? "crypto")
|
|
448
|
+
).map((scope) => scope.accountId).filter((accountId) => Boolean(accountId))
|
|
449
|
+
);
|
|
450
|
+
return matchingAccountIds.size === 1 ? { ...trade, accountId: [...matchingAccountIds][0] } : trade;
|
|
451
|
+
});
|
|
452
|
+
var getRuntimeStrategyAiGateObservedFrom = ({
|
|
453
|
+
scopes,
|
|
454
|
+
strategyName,
|
|
455
|
+
configId,
|
|
456
|
+
endTime
|
|
457
|
+
}) => {
|
|
458
|
+
const normalizedConfigId = configId ?? "config";
|
|
459
|
+
let observedFrom = null;
|
|
460
|
+
for (const scope of scopes) {
|
|
461
|
+
if (scope.strategy !== strategyName || (scope.runtimeConfigId ?? "config") !== normalizedConfigId || scope.firstTimestamp > endTime) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
observedFrom = observedFrom == null ? scope.firstTimestamp : Math.min(observedFrom, scope.firstTimestamp);
|
|
465
|
+
}
|
|
466
|
+
return observedFrom;
|
|
467
|
+
};
|
|
468
|
+
var buildRuntimeStrategyMaxLossValueTimeline = ({
|
|
469
|
+
scopes,
|
|
470
|
+
strategyName,
|
|
471
|
+
configId,
|
|
472
|
+
startTime,
|
|
473
|
+
endTime
|
|
474
|
+
}) => {
|
|
475
|
+
const normalizedConfigId = configId ?? "config";
|
|
476
|
+
const observationsByTimestamp = /* @__PURE__ */ new Map();
|
|
477
|
+
for (const scope of scopes) {
|
|
478
|
+
const value = scope.lineage.maxLossValue;
|
|
479
|
+
if (scope.strategy !== strategyName || (scope.runtimeConfigId ?? "config") !== normalizedConfigId || scope.firstTimestamp > endTime || typeof value !== "number" || !Number.isFinite(value)) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const existing = observationsByTimestamp.get(scope.firstTimestamp);
|
|
483
|
+
if (!existing || scope.lastTimestamp > existing.lastTimestamp || scope.lastTimestamp === existing.lastTimestamp && value > existing.value) {
|
|
484
|
+
observationsByTimestamp.set(scope.firstTimestamp, {
|
|
485
|
+
value,
|
|
486
|
+
lastTimestamp: scope.lastTimestamp
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const changes = [];
|
|
491
|
+
let observedFrom = null;
|
|
492
|
+
let initialValue = null;
|
|
493
|
+
let currentValue = null;
|
|
494
|
+
for (const [timestamp, observation] of [
|
|
495
|
+
...observationsByTimestamp.entries()
|
|
496
|
+
].sort(([left], [right]) => left - right)) {
|
|
497
|
+
if (currentValue == null) {
|
|
498
|
+
observedFrom = timestamp;
|
|
499
|
+
initialValue = observation.value;
|
|
500
|
+
currentValue = observation.value;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (observation.value === currentValue) continue;
|
|
504
|
+
if (timestamp >= startTime) {
|
|
505
|
+
changes.push({
|
|
506
|
+
timestamp,
|
|
507
|
+
previousValue: currentValue,
|
|
508
|
+
value: observation.value
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
currentValue = observation.value;
|
|
512
|
+
}
|
|
513
|
+
return { observedFrom, initialValue, changes };
|
|
514
|
+
};
|
|
515
|
+
var isRuntimeStrategyLineageScope = (value) => {
|
|
516
|
+
if (!value || typeof value !== "object") return false;
|
|
517
|
+
const record = value;
|
|
518
|
+
const lineage = record.lineage;
|
|
519
|
+
return typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.firstTimestamp === "number" && Number.isFinite(record.firstTimestamp) && typeof record.lastTimestamp === "number" && Number.isFinite(record.lastTimestamp) && lineage != null && typeof lineage.gateFingerprint === "string" && lineage.gateFingerprint.trim().length > 0;
|
|
520
|
+
};
|
|
521
|
+
var buildRuntimeStrategyAiGateChanges = ({
|
|
522
|
+
scopes,
|
|
523
|
+
strategyName,
|
|
524
|
+
configId,
|
|
525
|
+
startTime,
|
|
526
|
+
endTime
|
|
527
|
+
}) => {
|
|
528
|
+
const normalizedConfigId = configId ?? "config";
|
|
529
|
+
const observationsByTimestamp = /* @__PURE__ */ new Map();
|
|
530
|
+
for (const scope of scopes) {
|
|
531
|
+
if (scope.strategy !== strategyName || (scope.runtimeConfigId ?? "config") !== normalizedConfigId || scope.firstTimestamp > endTime) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const fingerprint = scope.lineage.gateFingerprint.trim();
|
|
535
|
+
const existing = observationsByTimestamp.get(scope.firstTimestamp);
|
|
536
|
+
if (!existing || scope.lastTimestamp > existing.lastTimestamp || scope.lastTimestamp === existing.lastTimestamp && fingerprint > existing.fingerprint) {
|
|
537
|
+
observationsByTimestamp.set(scope.firstTimestamp, {
|
|
538
|
+
fingerprint,
|
|
539
|
+
lastTimestamp: scope.lastTimestamp
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const changes = [];
|
|
544
|
+
let currentFingerprint = null;
|
|
545
|
+
for (const [timestamp, observation] of [
|
|
546
|
+
...observationsByTimestamp.entries()
|
|
547
|
+
].sort(([left], [right]) => left - right)) {
|
|
548
|
+
if (currentFingerprint == null) {
|
|
549
|
+
currentFingerprint = observation.fingerprint;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (observation.fingerprint === currentFingerprint) continue;
|
|
553
|
+
if (timestamp >= startTime) {
|
|
554
|
+
changes.push({
|
|
555
|
+
timestamp,
|
|
556
|
+
previousFingerprint: currentFingerprint,
|
|
557
|
+
fingerprint: observation.fingerprint
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
currentFingerprint = observation.fingerprint;
|
|
561
|
+
}
|
|
562
|
+
return changes;
|
|
563
|
+
};
|
|
564
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
565
|
+
0 && (module.exports = {
|
|
566
|
+
assignLegacyRuntimeTradeAccountScopes,
|
|
567
|
+
buildRuntimeStrategyAiGateChanges,
|
|
568
|
+
buildRuntimeStrategyAnalytics,
|
|
569
|
+
buildRuntimeStrategyIdentityKey,
|
|
570
|
+
buildRuntimeStrategyMaxLossValueTimeline,
|
|
571
|
+
getRuntimeStrategyAiGateObservedFrom,
|
|
572
|
+
isRuntimeStrategyLineageScope,
|
|
573
|
+
isRuntimeTradeRecord,
|
|
574
|
+
resolveStrategyNameByOrderLinkId,
|
|
575
|
+
selectTradesForWindow,
|
|
576
|
+
toRuntimeTradeView
|
|
577
|
+
});
|