@tradejs/app 2.0.18 → 2.0.20
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/package.json +14 -8
- package/src/app/actions/backtest.ts +2 -1
- package/src/app/actions/scanner.ts +2 -1
- package/src/app/api/backtest/files/route.ts +2 -1
- package/src/app/api/backtest/test/[strategy]/[name]/route.ts +1 -1
- package/src/app/api/derivatives/[symbol]/[interval]/route.ts +1 -1
- package/src/app/api/derivatives/summary/route.ts +1 -1
- package/src/app/api/signal/[symbol]/[signalId]/route.ts +2 -1
- package/src/app/api/spread/[symbol]/[interval]/route.ts +1 -1
- package/src/app/api/spread/summary/route.ts +1 -1
- package/src/app/api/strategies/runtime/route.ts +4 -674
- package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +1 -1
- package/src/app/api/user/runtime-deployments/route.ts +2 -2
- package/src/app/api/user/runtime-strategy-configs/route.ts +22 -212
- package/src/app/api/user/trading-accounts/[accountId]/route.ts +1 -1
- package/src/app/components/Backtest/TestList/index.tsx +1 -1
- package/src/app/components/Dashboard/KlineChart/figures/circle.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/figures/diamond.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/figures/label.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/figures/rectangle.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/figures/star.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/index.tsx +2 -1
- package/src/app/components/Shared/Filters/Root/index.tsx +1 -1
- package/src/app/components/Shared/Filters/context.ts +1 -1
- package/src/app/components/Strategies/RuntimeStrategyCard.tsx +82 -861
- package/src/app/components/Strategies/RuntimeStrategyChart.tsx +115 -184
- package/src/app/components/Strategies/StrategyEvidencePopover.tsx +259 -0
- package/src/app/components/Strategies/StrategyPerformanceCharts.tsx +419 -0
- package/src/app/components/Strategies/StrategySnapshotCard.tsx +122 -937
- package/src/app/components/UI/Segment/index.tsx +1 -1
- package/src/app/components/UI/Select/index.tsx +1 -1
- package/src/app/components/UI/SelectWithSearch/index.tsx +1 -1
- package/src/app/lib/backtestJobContracts.ts +67 -0
- package/src/app/lib/backtestJobProgress.ts +28 -0
- package/src/app/lib/backtestJobRequest.ts +107 -0
- package/src/app/lib/backtestJobs.ts +25 -257
- package/src/app/lib/runtimeDashboard.ts +700 -0
- package/src/app/lib/runtimeStrategies.ts +22 -457
- package/src/app/lib/runtimeStrategyConfigService.ts +279 -0
- package/src/app/lib/runtimeStrategyLineage.ts +264 -0
- package/src/app/lib/runtimeTradeReconciliation.ts +113 -0
- package/src/app/lib/runtimeTradeSync.ts +1 -1
- package/src/app/lib/strategyEvidenceTimeline.ts +298 -0
- package/src/app/lib/strategyPerformance.ts +387 -0
- package/src/app/routes/dashboard/Dashboard.tsx +2 -6
- package/src/app/routes/derivatives/derivativesViewModel.ts +253 -0
- package/src/app/routes/derivatives/page.tsx +70 -262
- package/src/app/store/filters.ts +2 -1
- package/src/app/store/indicators.ts +2 -1
- package/src/app/store/tests.ts +1 -1
- package/src/app/store/tickers.ts +2 -1
- package/src/app/types/ui.ts +20 -0
|
@@ -5,27 +5,40 @@ import {
|
|
|
5
5
|
parseStrategyOrderLinkKey,
|
|
6
6
|
} from '@tradejs/core/trade';
|
|
7
7
|
import type {
|
|
8
|
-
ClosedPnlRecord,
|
|
9
8
|
ExchangeEntryRecord,
|
|
10
9
|
PositionPnlSnapshot,
|
|
11
10
|
RuntimeTradeRecord,
|
|
12
|
-
RuntimeLineage,
|
|
13
11
|
SimpleOrderLogData,
|
|
14
12
|
StrategyConfig,
|
|
13
|
+
StrategyEvidenceTimeline,
|
|
15
14
|
TestStat,
|
|
16
15
|
MarketUniverse,
|
|
17
16
|
Interval,
|
|
18
17
|
} from '@tradejs/types';
|
|
18
|
+
import {
|
|
19
|
+
takeExactClosedPnlMatch,
|
|
20
|
+
type ClosedPnlRecordWithOrderLinkId,
|
|
21
|
+
} from './runtimeTradeReconciliation';
|
|
22
|
+
export {
|
|
23
|
+
assignLegacyRuntimeTradeAccountScopes,
|
|
24
|
+
buildRuntimeStrategyAiGateChanges,
|
|
25
|
+
buildRuntimeStrategyIdentityKey,
|
|
26
|
+
buildRuntimeStrategyMaxLossValueTimeline,
|
|
27
|
+
getRuntimeStrategyAiGateObservedFrom,
|
|
28
|
+
isRuntimeStrategyLineageScope,
|
|
29
|
+
} from './runtimeStrategyLineage';
|
|
30
|
+
export type {
|
|
31
|
+
RuntimeStrategyAccountScope,
|
|
32
|
+
RuntimeStrategyAiGateChange,
|
|
33
|
+
RuntimeStrategyLineageScope,
|
|
34
|
+
RuntimeStrategyMaxLossValueChange,
|
|
35
|
+
RuntimeStrategyMaxLossValueTimeline,
|
|
36
|
+
} from './runtimeStrategyLineage';
|
|
37
|
+
export { takeClosedPnlMatch } from './runtimeTradeReconciliation';
|
|
19
38
|
|
|
20
39
|
const MS_IN_DAY = 24 * 60 * 60 * 1000;
|
|
21
40
|
const AVG_DAYS_IN_MONTH = 30.4375;
|
|
22
41
|
|
|
23
|
-
type ClosedPnlRecordWithOrderLinkId = ClosedPnlRecord & {
|
|
24
|
-
direction?: RuntimeTradeRecord['direction'];
|
|
25
|
-
entryTimestamp?: number;
|
|
26
|
-
orderLinkId?: string;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
42
|
type RuntimeTradeWithResolvedPnl = RuntimeTradeRecord & {
|
|
30
43
|
resolvedPnl: number;
|
|
31
44
|
resolvedTimestamp: number;
|
|
@@ -73,35 +86,6 @@ export interface RuntimeStrategyTradeView {
|
|
|
73
86
|
lastSyncedAt: number | null;
|
|
74
87
|
}
|
|
75
88
|
|
|
76
|
-
export interface RuntimeStrategyLineageScope {
|
|
77
|
-
strategy: string;
|
|
78
|
-
symbol: string;
|
|
79
|
-
runtimeConfigId?: string;
|
|
80
|
-
lineage: RuntimeLineage & {
|
|
81
|
-
maxLossValue?: number | null;
|
|
82
|
-
};
|
|
83
|
-
firstTimestamp: number;
|
|
84
|
-
lastTimestamp: number;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface RuntimeStrategyAiGateChange {
|
|
88
|
-
timestamp: number;
|
|
89
|
-
previousFingerprint: string;
|
|
90
|
-
fingerprint: string;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface RuntimeStrategyMaxLossValueChange {
|
|
94
|
-
timestamp: number;
|
|
95
|
-
previousValue: number;
|
|
96
|
-
value: number;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export interface RuntimeStrategyMaxLossValueTimeline {
|
|
100
|
-
observedFrom: number | null;
|
|
101
|
-
initialValue: number | null;
|
|
102
|
-
changes: RuntimeStrategyMaxLossValueChange[];
|
|
103
|
-
}
|
|
104
|
-
|
|
105
89
|
export interface RuntimeStrategyView {
|
|
106
90
|
runtimeKey: string;
|
|
107
91
|
strategyName: string;
|
|
@@ -119,72 +103,11 @@ export interface RuntimeStrategyView {
|
|
|
119
103
|
stat: TestStat;
|
|
120
104
|
summary: RuntimeStrategyTradeSummary;
|
|
121
105
|
orderLog: SimpleOrderLogData;
|
|
122
|
-
|
|
123
|
-
aiGateChanges: RuntimeStrategyAiGateChange[];
|
|
124
|
-
maxLossValueTimeline: RuntimeStrategyMaxLossValueTimeline;
|
|
106
|
+
evidenceTimeline: StrategyEvidenceTimeline;
|
|
125
107
|
recentTrades: RuntimeStrategyTradeView[];
|
|
126
108
|
orders: RuntimeStrategyTradeView[];
|
|
127
109
|
}
|
|
128
110
|
|
|
129
|
-
export const buildRuntimeStrategyIdentityKey = ({
|
|
130
|
-
strategyName,
|
|
131
|
-
configId,
|
|
132
|
-
universe,
|
|
133
|
-
accountId,
|
|
134
|
-
deploymentId,
|
|
135
|
-
policyProfileId,
|
|
136
|
-
}: {
|
|
137
|
-
strategyName: string;
|
|
138
|
-
configId?: string;
|
|
139
|
-
universe?: MarketUniverse;
|
|
140
|
-
accountId?: string;
|
|
141
|
-
deploymentId?: string;
|
|
142
|
-
policyProfileId?: string;
|
|
143
|
-
}) =>
|
|
144
|
-
[
|
|
145
|
-
strategyName,
|
|
146
|
-
configId ?? 'config',
|
|
147
|
-
universe ?? 'crypto',
|
|
148
|
-
accountId ?? 'default',
|
|
149
|
-
deploymentId ?? 'default',
|
|
150
|
-
policyProfileId ?? 'default',
|
|
151
|
-
].join(':');
|
|
152
|
-
|
|
153
|
-
export interface RuntimeStrategyAccountScope {
|
|
154
|
-
strategyName: string;
|
|
155
|
-
configId: string;
|
|
156
|
-
universe: MarketUniverse;
|
|
157
|
-
accountId?: string;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export const assignLegacyRuntimeTradeAccountScopes = (
|
|
161
|
-
trades: RuntimeTradeRecord[],
|
|
162
|
-
scopes: RuntimeStrategyAccountScope[],
|
|
163
|
-
): RuntimeTradeRecord[] =>
|
|
164
|
-
trades.map((trade) => {
|
|
165
|
-
if (trade.accountId || trade.deploymentId) {
|
|
166
|
-
return trade;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const matchingAccountIds = new Set(
|
|
170
|
-
scopes
|
|
171
|
-
.filter(
|
|
172
|
-
(scope) =>
|
|
173
|
-
scope.strategyName === trade.strategy &&
|
|
174
|
-
scope.configId === (trade.runtimeConfigId ?? 'config') &&
|
|
175
|
-
scope.universe === (trade.universe ?? 'crypto'),
|
|
176
|
-
)
|
|
177
|
-
.map((scope) => scope.accountId)
|
|
178
|
-
.filter((accountId): accountId is string => Boolean(accountId)),
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
if (matchingAccountIds.size !== 1) {
|
|
182
|
-
return trade;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return { ...trade, accountId: [...matchingAccountIds][0] };
|
|
186
|
-
});
|
|
187
|
-
|
|
188
111
|
export interface RuntimeStrategiesResponse {
|
|
189
112
|
provider: string;
|
|
190
113
|
hours: number;
|
|
@@ -197,212 +120,6 @@ export interface RuntimeStrategiesResponse {
|
|
|
197
120
|
strategies: RuntimeStrategyView[];
|
|
198
121
|
}
|
|
199
122
|
|
|
200
|
-
export const getRuntimeStrategyAiGateObservedFrom = ({
|
|
201
|
-
scopes,
|
|
202
|
-
strategyName,
|
|
203
|
-
configId,
|
|
204
|
-
endTime,
|
|
205
|
-
}: {
|
|
206
|
-
scopes: RuntimeStrategyLineageScope[];
|
|
207
|
-
strategyName: string;
|
|
208
|
-
configId?: string;
|
|
209
|
-
endTime: number;
|
|
210
|
-
}) => {
|
|
211
|
-
const normalizedConfigId = configId ?? 'config';
|
|
212
|
-
let observedFrom: number | null = null;
|
|
213
|
-
|
|
214
|
-
for (const scope of scopes) {
|
|
215
|
-
if (
|
|
216
|
-
scope.strategy !== strategyName ||
|
|
217
|
-
(scope.runtimeConfigId ?? 'config') !== normalizedConfigId ||
|
|
218
|
-
scope.firstTimestamp > endTime
|
|
219
|
-
) {
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
observedFrom =
|
|
224
|
-
observedFrom == null
|
|
225
|
-
? scope.firstTimestamp
|
|
226
|
-
: Math.min(observedFrom, scope.firstTimestamp);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return observedFrom;
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
export const buildRuntimeStrategyMaxLossValueTimeline = ({
|
|
233
|
-
scopes,
|
|
234
|
-
strategyName,
|
|
235
|
-
configId,
|
|
236
|
-
startTime,
|
|
237
|
-
endTime,
|
|
238
|
-
}: {
|
|
239
|
-
scopes: RuntimeStrategyLineageScope[];
|
|
240
|
-
strategyName: string;
|
|
241
|
-
configId?: string;
|
|
242
|
-
startTime: number;
|
|
243
|
-
endTime: number;
|
|
244
|
-
}): RuntimeStrategyMaxLossValueTimeline => {
|
|
245
|
-
const normalizedConfigId = configId ?? 'config';
|
|
246
|
-
const observationsByTimestamp = new Map<
|
|
247
|
-
number,
|
|
248
|
-
{ value: number; lastTimestamp: number }
|
|
249
|
-
>();
|
|
250
|
-
|
|
251
|
-
for (const scope of scopes) {
|
|
252
|
-
const value = scope.lineage.maxLossValue;
|
|
253
|
-
if (
|
|
254
|
-
scope.strategy !== strategyName ||
|
|
255
|
-
(scope.runtimeConfigId ?? 'config') !== normalizedConfigId ||
|
|
256
|
-
scope.firstTimestamp > endTime ||
|
|
257
|
-
typeof value !== 'number' ||
|
|
258
|
-
!Number.isFinite(value)
|
|
259
|
-
) {
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const existing = observationsByTimestamp.get(scope.firstTimestamp);
|
|
264
|
-
if (
|
|
265
|
-
!existing ||
|
|
266
|
-
scope.lastTimestamp > existing.lastTimestamp ||
|
|
267
|
-
(scope.lastTimestamp === existing.lastTimestamp && value > existing.value)
|
|
268
|
-
) {
|
|
269
|
-
observationsByTimestamp.set(scope.firstTimestamp, {
|
|
270
|
-
value,
|
|
271
|
-
lastTimestamp: scope.lastTimestamp,
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const observations = [...observationsByTimestamp.entries()].sort(
|
|
277
|
-
([leftTimestamp], [rightTimestamp]) => leftTimestamp - rightTimestamp,
|
|
278
|
-
);
|
|
279
|
-
const changes: RuntimeStrategyMaxLossValueChange[] = [];
|
|
280
|
-
let observedFrom: number | null = null;
|
|
281
|
-
let initialValue: number | null = null;
|
|
282
|
-
let currentValue: number | null = null;
|
|
283
|
-
|
|
284
|
-
for (const [timestamp, observation] of observations) {
|
|
285
|
-
if (currentValue == null) {
|
|
286
|
-
observedFrom = timestamp;
|
|
287
|
-
initialValue = observation.value;
|
|
288
|
-
currentValue = observation.value;
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
if (observation.value === currentValue) {
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (timestamp >= startTime) {
|
|
296
|
-
changes.push({
|
|
297
|
-
timestamp,
|
|
298
|
-
previousValue: currentValue,
|
|
299
|
-
value: observation.value,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
currentValue = observation.value;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return { observedFrom, initialValue, changes };
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
export const isRuntimeStrategyLineageScope = (
|
|
309
|
-
value: unknown,
|
|
310
|
-
): value is RuntimeStrategyLineageScope => {
|
|
311
|
-
if (!value || typeof value !== 'object') {
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const record = value as Record<string, unknown>;
|
|
316
|
-
const lineage = record.lineage as Record<string, unknown> | undefined;
|
|
317
|
-
|
|
318
|
-
return (
|
|
319
|
-
typeof record.strategy === 'string' &&
|
|
320
|
-
typeof record.symbol === 'string' &&
|
|
321
|
-
typeof record.firstTimestamp === 'number' &&
|
|
322
|
-
Number.isFinite(record.firstTimestamp) &&
|
|
323
|
-
typeof record.lastTimestamp === 'number' &&
|
|
324
|
-
Number.isFinite(record.lastTimestamp) &&
|
|
325
|
-
lineage != null &&
|
|
326
|
-
typeof lineage.gateFingerprint === 'string' &&
|
|
327
|
-
lineage.gateFingerprint.trim().length > 0
|
|
328
|
-
);
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
export const buildRuntimeStrategyAiGateChanges = ({
|
|
332
|
-
scopes,
|
|
333
|
-
strategyName,
|
|
334
|
-
configId,
|
|
335
|
-
startTime,
|
|
336
|
-
endTime,
|
|
337
|
-
}: {
|
|
338
|
-
scopes: RuntimeStrategyLineageScope[];
|
|
339
|
-
strategyName: string;
|
|
340
|
-
configId?: string;
|
|
341
|
-
startTime: number;
|
|
342
|
-
endTime: number;
|
|
343
|
-
}): RuntimeStrategyAiGateChange[] => {
|
|
344
|
-
const normalizedConfigId = configId ?? 'config';
|
|
345
|
-
const observationsByTimestamp = new Map<
|
|
346
|
-
number,
|
|
347
|
-
{ fingerprint: string; lastTimestamp: number }
|
|
348
|
-
>();
|
|
349
|
-
|
|
350
|
-
for (const scope of scopes) {
|
|
351
|
-
if (
|
|
352
|
-
scope.strategy !== strategyName ||
|
|
353
|
-
(scope.runtimeConfigId ?? 'config') !== normalizedConfigId ||
|
|
354
|
-
scope.firstTimestamp > endTime
|
|
355
|
-
) {
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const fingerprint = scope.lineage.gateFingerprint.trim();
|
|
360
|
-
const existing = observationsByTimestamp.get(scope.firstTimestamp);
|
|
361
|
-
|
|
362
|
-
// Multiple symbols are evaluated for the same strategy timestamp. If a
|
|
363
|
-
// deploy happens during that cycle, prefer the lineage that kept running
|
|
364
|
-
// afterwards instead of making the marker order depend on the symbol name.
|
|
365
|
-
if (
|
|
366
|
-
!existing ||
|
|
367
|
-
scope.lastTimestamp > existing.lastTimestamp ||
|
|
368
|
-
(scope.lastTimestamp === existing.lastTimestamp &&
|
|
369
|
-
fingerprint > existing.fingerprint)
|
|
370
|
-
) {
|
|
371
|
-
observationsByTimestamp.set(scope.firstTimestamp, {
|
|
372
|
-
fingerprint,
|
|
373
|
-
lastTimestamp: scope.lastTimestamp,
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const observations = [...observationsByTimestamp.entries()].sort(
|
|
379
|
-
([leftTimestamp], [rightTimestamp]) => leftTimestamp - rightTimestamp,
|
|
380
|
-
);
|
|
381
|
-
const changes: RuntimeStrategyAiGateChange[] = [];
|
|
382
|
-
let currentFingerprint: string | null = null;
|
|
383
|
-
|
|
384
|
-
for (const [timestamp, observation] of observations) {
|
|
385
|
-
if (currentFingerprint == null) {
|
|
386
|
-
currentFingerprint = observation.fingerprint;
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
if (observation.fingerprint === currentFingerprint) {
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (timestamp >= startTime) {
|
|
394
|
-
changes.push({
|
|
395
|
-
timestamp,
|
|
396
|
-
previousFingerprint: currentFingerprint,
|
|
397
|
-
fingerprint: observation.fingerprint,
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
currentFingerprint = observation.fingerprint;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return changes;
|
|
404
|
-
};
|
|
405
|
-
|
|
406
123
|
const roundValue = (value: number, digits = 2) => {
|
|
407
124
|
if (!Number.isFinite(value)) {
|
|
408
125
|
return 0;
|
|
@@ -804,37 +521,6 @@ const createEmptyRuntimeStat = (
|
|
|
804
521
|
};
|
|
805
522
|
};
|
|
806
523
|
|
|
807
|
-
export const resolveStrategyNameByConfigKey = (
|
|
808
|
-
userName: string,
|
|
809
|
-
key: string,
|
|
810
|
-
): string | null => {
|
|
811
|
-
return (
|
|
812
|
-
resolveStrategyConfigIdentityByKey(userName, key)?.strategyName ?? null
|
|
813
|
-
);
|
|
814
|
-
};
|
|
815
|
-
|
|
816
|
-
export const resolveStrategyConfigIdentityByKey = (
|
|
817
|
-
userName: string,
|
|
818
|
-
key: string,
|
|
819
|
-
): { strategyName: string; configId: string } | null => {
|
|
820
|
-
const parts = key.split(':');
|
|
821
|
-
if (parts.length !== 5) return null;
|
|
822
|
-
const [users, keyUserName, strategies, strategyName, configId] = parts;
|
|
823
|
-
if (
|
|
824
|
-
users !== 'users' ||
|
|
825
|
-
keyUserName !== userName ||
|
|
826
|
-
strategies !== 'strategies' ||
|
|
827
|
-
!strategyName ||
|
|
828
|
-
strategyName === 'charts' ||
|
|
829
|
-
!configId ||
|
|
830
|
-
configId === 'results' ||
|
|
831
|
-
!/^[a-zA-Z0-9_-]+$/.test(configId)
|
|
832
|
-
) {
|
|
833
|
-
return null;
|
|
834
|
-
}
|
|
835
|
-
return { strategyName, configId };
|
|
836
|
-
};
|
|
837
|
-
|
|
838
524
|
const buildStrategyNameKeyMap = (strategyNames: string[]) => {
|
|
839
525
|
const map = new Map<string, string>();
|
|
840
526
|
|
|
@@ -1134,127 +820,6 @@ const removeClosedPnlFromExactMaps = ({
|
|
|
1134
820
|
}
|
|
1135
821
|
};
|
|
1136
822
|
|
|
1137
|
-
const removeClosedPnlFromSymbolBuckets = (
|
|
1138
|
-
buckets: Map<string, ClosedPnlRecordWithOrderLinkId[]>,
|
|
1139
|
-
row: ClosedPnlRecordWithOrderLinkId,
|
|
1140
|
-
) => {
|
|
1141
|
-
const rows = buckets.get(row.symbol);
|
|
1142
|
-
if (!rows?.length) {
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
const index = rows.findIndex((candidate) => candidate === row);
|
|
1147
|
-
if (index >= 0) {
|
|
1148
|
-
rows.splice(index, 1);
|
|
1149
|
-
}
|
|
1150
|
-
};
|
|
1151
|
-
|
|
1152
|
-
const takeExactClosedPnlMatch = ({
|
|
1153
|
-
exactByOrderLinkId,
|
|
1154
|
-
exactByOrderId,
|
|
1155
|
-
symbolBuckets,
|
|
1156
|
-
orderLinkId,
|
|
1157
|
-
orderId,
|
|
1158
|
-
}: {
|
|
1159
|
-
exactByOrderLinkId: Map<string, ClosedPnlRecordWithOrderLinkId>;
|
|
1160
|
-
exactByOrderId: Map<string, ClosedPnlRecordWithOrderLinkId>;
|
|
1161
|
-
symbolBuckets: Map<string, ClosedPnlRecordWithOrderLinkId[]>;
|
|
1162
|
-
orderLinkId?: string | null;
|
|
1163
|
-
orderId?: string | null;
|
|
1164
|
-
}) => {
|
|
1165
|
-
const exactKeys: Array<
|
|
1166
|
-
[Map<string, ClosedPnlRecordWithOrderLinkId>, string | null | undefined]
|
|
1167
|
-
> = [
|
|
1168
|
-
[exactByOrderLinkId, orderLinkId],
|
|
1169
|
-
[exactByOrderId, orderId],
|
|
1170
|
-
];
|
|
1171
|
-
|
|
1172
|
-
for (const [bucket, key] of exactKeys) {
|
|
1173
|
-
const normalizedKey = toNonEmptyString(key);
|
|
1174
|
-
|
|
1175
|
-
if (!normalizedKey) {
|
|
1176
|
-
continue;
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
const exactMatch = bucket.get(normalizedKey);
|
|
1180
|
-
if (!exactMatch) {
|
|
1181
|
-
continue;
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
removeClosedPnlFromExactMaps({
|
|
1185
|
-
exactByOrderLinkId,
|
|
1186
|
-
exactByOrderId,
|
|
1187
|
-
row: exactMatch,
|
|
1188
|
-
});
|
|
1189
|
-
removeClosedPnlFromSymbolBuckets(symbolBuckets, exactMatch);
|
|
1190
|
-
return exactMatch;
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
return null;
|
|
1194
|
-
};
|
|
1195
|
-
|
|
1196
|
-
export const takeClosedPnlMatch = ({
|
|
1197
|
-
exactByOrderLinkId,
|
|
1198
|
-
exactByOrderId = new Map<string, ClosedPnlRecordWithOrderLinkId>(),
|
|
1199
|
-
symbolBuckets,
|
|
1200
|
-
trade,
|
|
1201
|
-
}: {
|
|
1202
|
-
exactByOrderLinkId: Map<string, ClosedPnlRecordWithOrderLinkId>;
|
|
1203
|
-
exactByOrderId?: Map<string, ClosedPnlRecordWithOrderLinkId>;
|
|
1204
|
-
symbolBuckets: Map<string, ClosedPnlRecordWithOrderLinkId[]>;
|
|
1205
|
-
trade: RuntimeTradeRecord;
|
|
1206
|
-
}) => {
|
|
1207
|
-
const exactMatch = takeExactClosedPnlMatch({
|
|
1208
|
-
exactByOrderLinkId,
|
|
1209
|
-
exactByOrderId,
|
|
1210
|
-
symbolBuckets,
|
|
1211
|
-
orderLinkId: trade.orderId,
|
|
1212
|
-
orderId: trade.orderId,
|
|
1213
|
-
});
|
|
1214
|
-
|
|
1215
|
-
if (exactMatch) {
|
|
1216
|
-
return exactMatch;
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
const rows = symbolBuckets.get(trade.symbol);
|
|
1220
|
-
if (!rows?.length) {
|
|
1221
|
-
return null;
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
const minimumClosedAt = trade.entryTimestamp - 5 * 60_000;
|
|
1225
|
-
const matchIndex = rows.reduce((bestIndex, row, index) => {
|
|
1226
|
-
if (
|
|
1227
|
-
!Number.isFinite(row.closedAt) ||
|
|
1228
|
-
row.closedAt < minimumClosedAt ||
|
|
1229
|
-
(row.direction && row.direction !== trade.direction)
|
|
1230
|
-
) {
|
|
1231
|
-
return bestIndex;
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
if (bestIndex < 0) {
|
|
1235
|
-
return index;
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
const best = rows[bestIndex];
|
|
1239
|
-
return row.closedAt < best.closedAt ? index : bestIndex;
|
|
1240
|
-
}, -1);
|
|
1241
|
-
|
|
1242
|
-
if (matchIndex < 0) {
|
|
1243
|
-
return null;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
const [row] = rows.splice(matchIndex, 1);
|
|
1247
|
-
if (row) {
|
|
1248
|
-
removeClosedPnlFromExactMaps({
|
|
1249
|
-
exactByOrderLinkId,
|
|
1250
|
-
exactByOrderId,
|
|
1251
|
-
row,
|
|
1252
|
-
});
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
return row ?? null;
|
|
1256
|
-
};
|
|
1257
|
-
|
|
1258
823
|
const takeClosedPnlMatchForExchangeEntry = ({
|
|
1259
824
|
exactByOrderLinkId,
|
|
1260
825
|
exactByOrderId,
|