@tradejs/app 2.0.18 → 2.0.19
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/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 +81 -858
- 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 +684 -0
- package/src/app/lib/runtimeStrategies.ts +24 -454
- 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/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,43 @@ 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,
|
|
15
13
|
TestStat,
|
|
16
14
|
MarketUniverse,
|
|
17
15
|
Interval,
|
|
18
16
|
} from '@tradejs/types';
|
|
17
|
+
import type {
|
|
18
|
+
RuntimeStrategyAiGateChange,
|
|
19
|
+
RuntimeStrategyMaxLossValueTimeline,
|
|
20
|
+
} from './runtimeStrategyLineage';
|
|
21
|
+
import {
|
|
22
|
+
takeExactClosedPnlMatch,
|
|
23
|
+
type ClosedPnlRecordWithOrderLinkId,
|
|
24
|
+
} from './runtimeTradeReconciliation';
|
|
25
|
+
export {
|
|
26
|
+
assignLegacyRuntimeTradeAccountScopes,
|
|
27
|
+
buildRuntimeStrategyAiGateChanges,
|
|
28
|
+
buildRuntimeStrategyIdentityKey,
|
|
29
|
+
buildRuntimeStrategyMaxLossValueTimeline,
|
|
30
|
+
getRuntimeStrategyAiGateObservedFrom,
|
|
31
|
+
isRuntimeStrategyLineageScope,
|
|
32
|
+
} from './runtimeStrategyLineage';
|
|
33
|
+
export type {
|
|
34
|
+
RuntimeStrategyAccountScope,
|
|
35
|
+
RuntimeStrategyAiGateChange,
|
|
36
|
+
RuntimeStrategyLineageScope,
|
|
37
|
+
RuntimeStrategyMaxLossValueChange,
|
|
38
|
+
RuntimeStrategyMaxLossValueTimeline,
|
|
39
|
+
} from './runtimeStrategyLineage';
|
|
40
|
+
export { takeClosedPnlMatch } from './runtimeTradeReconciliation';
|
|
19
41
|
|
|
20
42
|
const MS_IN_DAY = 24 * 60 * 60 * 1000;
|
|
21
43
|
const AVG_DAYS_IN_MONTH = 30.4375;
|
|
22
44
|
|
|
23
|
-
type ClosedPnlRecordWithOrderLinkId = ClosedPnlRecord & {
|
|
24
|
-
direction?: RuntimeTradeRecord['direction'];
|
|
25
|
-
entryTimestamp?: number;
|
|
26
|
-
orderLinkId?: string;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
45
|
type RuntimeTradeWithResolvedPnl = RuntimeTradeRecord & {
|
|
30
46
|
resolvedPnl: number;
|
|
31
47
|
resolvedTimestamp: number;
|
|
@@ -73,35 +89,6 @@ export interface RuntimeStrategyTradeView {
|
|
|
73
89
|
lastSyncedAt: number | null;
|
|
74
90
|
}
|
|
75
91
|
|
|
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
92
|
export interface RuntimeStrategyView {
|
|
106
93
|
runtimeKey: string;
|
|
107
94
|
strategyName: string;
|
|
@@ -126,65 +113,6 @@ export interface RuntimeStrategyView {
|
|
|
126
113
|
orders: RuntimeStrategyTradeView[];
|
|
127
114
|
}
|
|
128
115
|
|
|
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
116
|
export interface RuntimeStrategiesResponse {
|
|
189
117
|
provider: string;
|
|
190
118
|
hours: number;
|
|
@@ -197,212 +125,6 @@ export interface RuntimeStrategiesResponse {
|
|
|
197
125
|
strategies: RuntimeStrategyView[];
|
|
198
126
|
}
|
|
199
127
|
|
|
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
128
|
const roundValue = (value: number, digits = 2) => {
|
|
407
129
|
if (!Number.isFinite(value)) {
|
|
408
130
|
return 0;
|
|
@@ -804,37 +526,6 @@ const createEmptyRuntimeStat = (
|
|
|
804
526
|
};
|
|
805
527
|
};
|
|
806
528
|
|
|
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
529
|
const buildStrategyNameKeyMap = (strategyNames: string[]) => {
|
|
839
530
|
const map = new Map<string, string>();
|
|
840
531
|
|
|
@@ -1134,127 +825,6 @@ const removeClosedPnlFromExactMaps = ({
|
|
|
1134
825
|
}
|
|
1135
826
|
};
|
|
1136
827
|
|
|
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
828
|
const takeClosedPnlMatchForExchangeEntry = ({
|
|
1259
829
|
exactByOrderLinkId,
|
|
1260
830
|
exactByOrderId,
|