@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
4
|
import {
|
|
5
5
|
Badge,
|
|
6
6
|
Box,
|
|
@@ -36,81 +36,25 @@ import {
|
|
|
36
36
|
type OrdersDrawerSummaryItem,
|
|
37
37
|
} from '#components/Shared/OrdersDrawer';
|
|
38
38
|
import type { RuntimeStrategyView } from '#app/lib/runtimeStrategies';
|
|
39
|
+
import {
|
|
40
|
+
buildQuarterlyMonthlyStats,
|
|
41
|
+
buildStrategyPerformanceViewModel,
|
|
42
|
+
calculateMaxLossStreak,
|
|
43
|
+
} from '#app/lib/strategyPerformance';
|
|
39
44
|
import { AdvancedMetricsPanel } from './AdvancedMetricsPanel';
|
|
40
45
|
import { RuntimeStrategyChart } from './RuntimeStrategyChart';
|
|
46
|
+
import {
|
|
47
|
+
ChartPanel,
|
|
48
|
+
DrawdownTimelineChart,
|
|
49
|
+
PnlDistributionChart,
|
|
50
|
+
RollingPerformanceChart,
|
|
51
|
+
TimeOfDaySessionChart,
|
|
52
|
+
WinLossStreakTimelineChart,
|
|
53
|
+
} from './StrategyPerformanceCharts';
|
|
41
54
|
import { RuntimeStrategyConfigDrawer } from './RuntimeStrategyConfigDrawer';
|
|
42
55
|
|
|
43
56
|
type RuntimeOrderView = RuntimeStrategyView['orders'][number];
|
|
44
57
|
const RUNTIME_ORDER_ROW_HEIGHT = 306;
|
|
45
|
-
const CHART_WIDTH = 640;
|
|
46
|
-
const CHART_HEIGHT = 120;
|
|
47
|
-
const CHART_PADDING = 10;
|
|
48
|
-
const POSITIVE_CHART_COLOR = '#5eead4';
|
|
49
|
-
const NEGATIVE_CHART_COLOR = '#f87171';
|
|
50
|
-
const NEUTRAL_CHART_COLOR = '#6b7280';
|
|
51
|
-
|
|
52
|
-
type TradingSession = 'Asia' | 'Europe' | 'US';
|
|
53
|
-
|
|
54
|
-
interface RuntimeTradePoint {
|
|
55
|
-
index: number;
|
|
56
|
-
timestamp: number;
|
|
57
|
-
pnl: number;
|
|
58
|
-
equity: number;
|
|
59
|
-
hour: number;
|
|
60
|
-
session: TradingSession;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface DrawdownPoint {
|
|
64
|
-
timestamp: number;
|
|
65
|
-
drawdownPercent: number;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
interface RollingPerformancePoint {
|
|
69
|
-
index: number;
|
|
70
|
-
winRate: number;
|
|
71
|
-
pnl: number;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
interface DistributionBin {
|
|
75
|
-
id: string;
|
|
76
|
-
min: number;
|
|
77
|
-
max: number;
|
|
78
|
-
count: number;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
interface MonthlyStat {
|
|
82
|
-
id: string;
|
|
83
|
-
year: number;
|
|
84
|
-
monthIndex: number;
|
|
85
|
-
monthLabel: string;
|
|
86
|
-
orders: number;
|
|
87
|
-
wins: number;
|
|
88
|
-
pnl: number;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
interface YearlyMonthlyStats {
|
|
92
|
-
year: number;
|
|
93
|
-
months: MonthlyStat[];
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
interface QuarterlyMonthlyStats {
|
|
97
|
-
label: string;
|
|
98
|
-
monthIndexes: readonly number[];
|
|
99
|
-
months: (MonthlyStat | null)[];
|
|
100
|
-
hasData: boolean;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
interface SessionPnlStat {
|
|
104
|
-
session: TradingSession;
|
|
105
|
-
pnl: number;
|
|
106
|
-
orders: number;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
interface HourlyPnlStat {
|
|
110
|
-
hour: number;
|
|
111
|
-
pnl: number;
|
|
112
|
-
orders: number;
|
|
113
|
-
}
|
|
114
58
|
|
|
115
59
|
interface RuntimeSymbolPnlRank {
|
|
116
60
|
symbol: string;
|
|
@@ -159,13 +103,6 @@ const getColorByLevel = (level: ThresholdLevel) => {
|
|
|
159
103
|
|
|
160
104
|
const getMetricColor = (level: ThresholdLevel) => getColorByLevel(level);
|
|
161
105
|
|
|
162
|
-
const getChartPnlColor = (value: number) =>
|
|
163
|
-
value > 0
|
|
164
|
-
? POSITIVE_CHART_COLOR
|
|
165
|
-
: value < 0
|
|
166
|
-
? NEGATIVE_CHART_COLOR
|
|
167
|
-
: NEUTRAL_CHART_COLOR;
|
|
168
|
-
|
|
169
106
|
const getPnlBarColor = (value: number) => {
|
|
170
107
|
if (value > 0) {
|
|
171
108
|
return 'teal.500';
|
|
@@ -176,318 +113,6 @@ const getPnlBarColor = (value: number) => {
|
|
|
176
113
|
return 'gray.500';
|
|
177
114
|
};
|
|
178
115
|
|
|
179
|
-
const resolveTradingSession = (hour: number): TradingSession => {
|
|
180
|
-
if (hour < 8) {
|
|
181
|
-
return 'Asia';
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (hour < 16) {
|
|
185
|
-
return 'Europe';
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return 'US';
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const getStepPnl = (
|
|
192
|
-
orderLog: RuntimeStrategyView['orderLog'],
|
|
193
|
-
index: number,
|
|
194
|
-
) => {
|
|
195
|
-
const current = orderLog[index];
|
|
196
|
-
const previous = orderLog[index - 1];
|
|
197
|
-
|
|
198
|
-
if (!current || !previous) {
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const pnl = current[1] - previous[1];
|
|
203
|
-
return Number.isFinite(pnl) ? pnl : null;
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
const calculateMaxLossStreak = (orderLog: RuntimeStrategyView['orderLog']) => {
|
|
207
|
-
let currentStreak = 0;
|
|
208
|
-
let maxStreak = 0;
|
|
209
|
-
|
|
210
|
-
for (let index = 1; index < orderLog.length; index += 1) {
|
|
211
|
-
const pnl = getStepPnl(orderLog, index);
|
|
212
|
-
if (typeof pnl !== 'number' || !Number.isFinite(pnl)) {
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (pnl < 0) {
|
|
217
|
-
currentStreak += 1;
|
|
218
|
-
maxStreak = Math.max(maxStreak, currentStreak);
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
currentStreak = 0;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return maxStreak;
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
const buildRuntimeTradePoints = (
|
|
229
|
-
orderLog: RuntimeStrategyView['orderLog'],
|
|
230
|
-
): RuntimeTradePoint[] => {
|
|
231
|
-
const points: RuntimeTradePoint[] = [];
|
|
232
|
-
|
|
233
|
-
for (let index = 1; index < orderLog.length; index += 1) {
|
|
234
|
-
const current = orderLog[index];
|
|
235
|
-
const previous = orderLog[index - 1];
|
|
236
|
-
if (!current || !previous) {
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const [timestamp, equity] = current;
|
|
241
|
-
const pnl = equity - previous[1];
|
|
242
|
-
if (
|
|
243
|
-
!Number.isFinite(timestamp) ||
|
|
244
|
-
!Number.isFinite(equity) ||
|
|
245
|
-
!Number.isFinite(pnl)
|
|
246
|
-
) {
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const hour = new Date(timestamp).getUTCHours();
|
|
251
|
-
points.push({
|
|
252
|
-
index,
|
|
253
|
-
timestamp,
|
|
254
|
-
pnl,
|
|
255
|
-
equity,
|
|
256
|
-
hour,
|
|
257
|
-
session: resolveTradingSession(hour),
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return points;
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
const buildDrawdownPoints = (
|
|
265
|
-
orderLog: RuntimeStrategyView['orderLog'],
|
|
266
|
-
): DrawdownPoint[] => {
|
|
267
|
-
let peak = orderLog[0]?.[1] ?? 0;
|
|
268
|
-
|
|
269
|
-
return orderLog
|
|
270
|
-
.map(([timestamp, equity]) => {
|
|
271
|
-
if (!Number.isFinite(timestamp) || !Number.isFinite(equity)) {
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
peak = Math.max(peak, equity);
|
|
276
|
-
const drawdownPercent = peak > 0 ? ((peak - equity) / peak) * 100 : 0;
|
|
277
|
-
|
|
278
|
-
return {
|
|
279
|
-
timestamp,
|
|
280
|
-
drawdownPercent,
|
|
281
|
-
};
|
|
282
|
-
})
|
|
283
|
-
.filter((point): point is DrawdownPoint => point != null);
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
const buildRollingPerformance = (
|
|
287
|
-
trades: RuntimeTradePoint[],
|
|
288
|
-
windowSize = 50,
|
|
289
|
-
): RollingPerformancePoint[] =>
|
|
290
|
-
trades.map((trade, index) => {
|
|
291
|
-
const windowTrades = trades.slice(
|
|
292
|
-
Math.max(0, index - windowSize + 1),
|
|
293
|
-
index + 1,
|
|
294
|
-
);
|
|
295
|
-
const wins = windowTrades.filter((item) => item.pnl > 0).length;
|
|
296
|
-
const pnl = windowTrades.reduce((sum, item) => sum + item.pnl, 0);
|
|
297
|
-
|
|
298
|
-
return {
|
|
299
|
-
index: trade.index,
|
|
300
|
-
winRate: windowTrades.length > 0 ? (wins / windowTrades.length) * 100 : 0,
|
|
301
|
-
pnl,
|
|
302
|
-
};
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
const buildPnlDistribution = (
|
|
306
|
-
trades: RuntimeTradePoint[],
|
|
307
|
-
binCount = 12,
|
|
308
|
-
): DistributionBin[] => {
|
|
309
|
-
if (!trades.length) {
|
|
310
|
-
return [];
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const pnlValues = trades.map((trade) => trade.pnl);
|
|
314
|
-
const min = Math.min(...pnlValues);
|
|
315
|
-
const max = Math.max(...pnlValues);
|
|
316
|
-
|
|
317
|
-
if (!Number.isFinite(min) || !Number.isFinite(max)) {
|
|
318
|
-
return [];
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (min === max) {
|
|
322
|
-
return [
|
|
323
|
-
{
|
|
324
|
-
id: `${min}:${max}`,
|
|
325
|
-
min,
|
|
326
|
-
max,
|
|
327
|
-
count: trades.length,
|
|
328
|
-
},
|
|
329
|
-
];
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const step = (max - min) / binCount;
|
|
333
|
-
const bins = Array.from({ length: binCount }, (_, index) => ({
|
|
334
|
-
id: String(index),
|
|
335
|
-
min: min + step * index,
|
|
336
|
-
max: index === binCount - 1 ? max : min + step * (index + 1),
|
|
337
|
-
count: 0,
|
|
338
|
-
}));
|
|
339
|
-
|
|
340
|
-
for (const pnl of pnlValues) {
|
|
341
|
-
const rawIndex = Math.floor((pnl - min) / step);
|
|
342
|
-
const index = Math.max(0, Math.min(binCount - 1, rawIndex));
|
|
343
|
-
const bin = bins[index];
|
|
344
|
-
if (bin) {
|
|
345
|
-
bin.count += 1;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return bins;
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
const buildSessionPnlStats = (
|
|
353
|
-
trades: RuntimeTradePoint[],
|
|
354
|
-
): SessionPnlStat[] => {
|
|
355
|
-
const stats = new Map<TradingSession, SessionPnlStat>(
|
|
356
|
-
(['Asia', 'Europe', 'US'] as const).map((session) => [
|
|
357
|
-
session,
|
|
358
|
-
{ session, pnl: 0, orders: 0 },
|
|
359
|
-
]),
|
|
360
|
-
);
|
|
361
|
-
|
|
362
|
-
for (const trade of trades) {
|
|
363
|
-
const stat = stats.get(trade.session);
|
|
364
|
-
if (!stat) {
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
stat.pnl += trade.pnl;
|
|
369
|
-
stat.orders += 1;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
return [...stats.values()];
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
const buildHourlyPnlStats = (trades: RuntimeTradePoint[]): HourlyPnlStat[] => {
|
|
376
|
-
const stats = Array.from({ length: 24 }, (_, hour) => ({
|
|
377
|
-
hour,
|
|
378
|
-
pnl: 0,
|
|
379
|
-
orders: 0,
|
|
380
|
-
}));
|
|
381
|
-
|
|
382
|
-
for (const trade of trades) {
|
|
383
|
-
const stat = stats[trade.hour];
|
|
384
|
-
if (!stat) {
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
stat.pnl += trade.pnl;
|
|
389
|
-
stat.orders += 1;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
return stats;
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
const getMonthLabel = (monthIndex: number) =>
|
|
396
|
-
new Date(Date.UTC(2026, monthIndex - 1, 1)).toLocaleString('en-US', {
|
|
397
|
-
month: 'short',
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
const monthQuarters = [
|
|
401
|
-
{ label: 'Q1', months: [1, 2, 3] },
|
|
402
|
-
{ label: 'Q2', months: [4, 5, 6] },
|
|
403
|
-
{ label: 'Q3', months: [7, 8, 9] },
|
|
404
|
-
{ label: 'Q4', months: [10, 11, 12] },
|
|
405
|
-
] as const;
|
|
406
|
-
|
|
407
|
-
const buildQuarterlyMonthlyStats = (
|
|
408
|
-
months: MonthlyStat[],
|
|
409
|
-
): QuarterlyMonthlyStats[] => {
|
|
410
|
-
const byMonth = new Map(months.map((month) => [month.monthIndex, month]));
|
|
411
|
-
|
|
412
|
-
return monthQuarters
|
|
413
|
-
.map((quarter) => {
|
|
414
|
-
const quarterMonths = quarter.months.map(
|
|
415
|
-
(monthIndex) => byMonth.get(monthIndex) ?? null,
|
|
416
|
-
);
|
|
417
|
-
|
|
418
|
-
return {
|
|
419
|
-
label: quarter.label,
|
|
420
|
-
monthIndexes: quarter.months,
|
|
421
|
-
months: quarterMonths,
|
|
422
|
-
hasData: quarterMonths.some((month) => month != null),
|
|
423
|
-
};
|
|
424
|
-
})
|
|
425
|
-
.filter((quarter) => quarter.hasData);
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
const buildMonthlyStats = (
|
|
429
|
-
orderLog: RuntimeStrategyView['orderLog'],
|
|
430
|
-
): YearlyMonthlyStats[] => {
|
|
431
|
-
const grouped = new Map<string, MonthlyStat>();
|
|
432
|
-
|
|
433
|
-
for (let index = 1; index < orderLog.length; index += 1) {
|
|
434
|
-
const current = orderLog[index];
|
|
435
|
-
const previous = orderLog[index - 1];
|
|
436
|
-
if (!current || !previous) {
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const [timestamp, amount] = current;
|
|
441
|
-
const previousAmount = previous[1];
|
|
442
|
-
if (
|
|
443
|
-
!Number.isFinite(timestamp) ||
|
|
444
|
-
!Number.isFinite(amount) ||
|
|
445
|
-
!Number.isFinite(previousAmount)
|
|
446
|
-
) {
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const date = new Date(timestamp);
|
|
451
|
-
const year = date.getUTCFullYear();
|
|
452
|
-
const monthIndex = date.getUTCMonth() + 1;
|
|
453
|
-
const id = `${year}-${String(monthIndex).padStart(2, '0')}`;
|
|
454
|
-
const pnl = amount - previousAmount;
|
|
455
|
-
const existing = grouped.get(id) ?? {
|
|
456
|
-
id,
|
|
457
|
-
year,
|
|
458
|
-
monthIndex,
|
|
459
|
-
monthLabel: getMonthLabel(monthIndex),
|
|
460
|
-
orders: 0,
|
|
461
|
-
wins: 0,
|
|
462
|
-
pnl: 0,
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
existing.orders += 1;
|
|
466
|
-
existing.wins += pnl > 0 ? 1 : 0;
|
|
467
|
-
existing.pnl += pnl;
|
|
468
|
-
grouped.set(id, existing);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
const monthlyStats = [...grouped.values()].sort(
|
|
472
|
-
(left, right) =>
|
|
473
|
-
left.year - right.year || left.monthIndex - right.monthIndex,
|
|
474
|
-
);
|
|
475
|
-
const yearlyStats = new Map<number, MonthlyStat[]>();
|
|
476
|
-
|
|
477
|
-
for (const month of monthlyStats) {
|
|
478
|
-
const months = yearlyStats.get(month.year) ?? [];
|
|
479
|
-
months.push(month);
|
|
480
|
-
yearlyStats.set(month.year, months);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
return [...yearlyStats.entries()]
|
|
484
|
-
.sort(([leftYear], [rightYear]) => leftYear - rightYear)
|
|
485
|
-
.map(([year, months]) => ({
|
|
486
|
-
year,
|
|
487
|
-
months,
|
|
488
|
-
}));
|
|
489
|
-
};
|
|
490
|
-
|
|
491
116
|
const StatItem = ({
|
|
492
117
|
stat,
|
|
493
118
|
id,
|
|
@@ -901,389 +526,6 @@ const buildRuntimeDirectionStats = (
|
|
|
901
526
|
};
|
|
902
527
|
});
|
|
903
528
|
|
|
904
|
-
const buildPolylinePoints = (values: number[]) => {
|
|
905
|
-
if (!values.length) {
|
|
906
|
-
return '';
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
const min = Math.min(...values);
|
|
910
|
-
const max = Math.max(...values);
|
|
911
|
-
const range = max - min || 1;
|
|
912
|
-
const drawableWidth = CHART_WIDTH - CHART_PADDING * 2;
|
|
913
|
-
const drawableHeight = CHART_HEIGHT - CHART_PADDING * 2;
|
|
914
|
-
|
|
915
|
-
return values
|
|
916
|
-
.map((value, index) => {
|
|
917
|
-
const x =
|
|
918
|
-
CHART_PADDING +
|
|
919
|
-
(values.length === 1
|
|
920
|
-
? 0
|
|
921
|
-
: (index / (values.length - 1)) * drawableWidth);
|
|
922
|
-
const y = CHART_PADDING + ((max - value) / range) * drawableHeight;
|
|
923
|
-
|
|
924
|
-
return `${x.toFixed(2)},${y.toFixed(2)}`;
|
|
925
|
-
})
|
|
926
|
-
.join(' ');
|
|
927
|
-
};
|
|
928
|
-
|
|
929
|
-
const ChartPanel = ({
|
|
930
|
-
title,
|
|
931
|
-
subtitle,
|
|
932
|
-
children,
|
|
933
|
-
}: {
|
|
934
|
-
title: string;
|
|
935
|
-
subtitle?: string;
|
|
936
|
-
children: ReactNode;
|
|
937
|
-
}) => (
|
|
938
|
-
<Box
|
|
939
|
-
p={4}
|
|
940
|
-
borderWidth="1px"
|
|
941
|
-
borderColor="gray.800"
|
|
942
|
-
borderRadius="md"
|
|
943
|
-
bg="gray.900"
|
|
944
|
-
minH="210px"
|
|
945
|
-
>
|
|
946
|
-
<Flex justify="space-between" align="baseline" gap={3} mb={3}>
|
|
947
|
-
<Text fontSize="sm" color="gray.300" fontWeight="semibold">
|
|
948
|
-
{title}
|
|
949
|
-
</Text>
|
|
950
|
-
{subtitle ? (
|
|
951
|
-
<Text fontSize="xs" color="gray.500" textAlign="right">
|
|
952
|
-
{subtitle}
|
|
953
|
-
</Text>
|
|
954
|
-
) : null}
|
|
955
|
-
</Flex>
|
|
956
|
-
{children}
|
|
957
|
-
</Box>
|
|
958
|
-
);
|
|
959
|
-
|
|
960
|
-
const EmptyChart = () => (
|
|
961
|
-
<Flex h="140px" align="center" justify="center">
|
|
962
|
-
<Text fontSize="sm" color="gray.500">
|
|
963
|
-
No trade data
|
|
964
|
-
</Text>
|
|
965
|
-
</Flex>
|
|
966
|
-
);
|
|
967
|
-
|
|
968
|
-
const DrawdownTimelineChart = ({ points }: { points: DrawdownPoint[] }) => {
|
|
969
|
-
if (points.length < 2) {
|
|
970
|
-
return <EmptyChart />;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
const values = points.map((point) => -point.drawdownPercent);
|
|
974
|
-
const maxDrawdown = Math.max(...points.map((point) => point.drawdownPercent));
|
|
975
|
-
const linePoints = buildPolylinePoints(values);
|
|
976
|
-
|
|
977
|
-
return (
|
|
978
|
-
<Box>
|
|
979
|
-
<svg
|
|
980
|
-
width="100%"
|
|
981
|
-
viewBox={`0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`}
|
|
982
|
-
role="img"
|
|
983
|
-
aria-label="Drawdown timeline"
|
|
984
|
-
>
|
|
985
|
-
<line
|
|
986
|
-
x1={CHART_PADDING}
|
|
987
|
-
x2={CHART_WIDTH - CHART_PADDING}
|
|
988
|
-
y1={CHART_PADDING}
|
|
989
|
-
y2={CHART_PADDING}
|
|
990
|
-
stroke="#374151"
|
|
991
|
-
strokeDasharray="4 4"
|
|
992
|
-
/>
|
|
993
|
-
<polyline
|
|
994
|
-
points={linePoints}
|
|
995
|
-
fill="none"
|
|
996
|
-
stroke={NEGATIVE_CHART_COLOR}
|
|
997
|
-
strokeWidth="2"
|
|
998
|
-
/>
|
|
999
|
-
</svg>
|
|
1000
|
-
<Flex justify="space-between" align="center" mt={2}>
|
|
1001
|
-
<Text fontSize="xs" color="gray.500">
|
|
1002
|
-
max drawdown
|
|
1003
|
-
</Text>
|
|
1004
|
-
<Text
|
|
1005
|
-
fontSize="sm"
|
|
1006
|
-
color="orange.300"
|
|
1007
|
-
fontFamily="mono"
|
|
1008
|
-
fontWeight="bold"
|
|
1009
|
-
>
|
|
1010
|
-
{formatPercent(maxDrawdown)}
|
|
1011
|
-
</Text>
|
|
1012
|
-
</Flex>
|
|
1013
|
-
</Box>
|
|
1014
|
-
);
|
|
1015
|
-
};
|
|
1016
|
-
|
|
1017
|
-
const WinLossStreakTimelineChart = ({
|
|
1018
|
-
trades,
|
|
1019
|
-
}: {
|
|
1020
|
-
trades: RuntimeTradePoint[];
|
|
1021
|
-
}) => {
|
|
1022
|
-
if (!trades.length) {
|
|
1023
|
-
return <EmptyChart />;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
const maxAbsPnl = Math.max(...trades.map((trade) => Math.abs(trade.pnl)), 1);
|
|
1027
|
-
const centerY = CHART_HEIGHT / 2;
|
|
1028
|
-
const barWidth = CHART_WIDTH / trades.length;
|
|
1029
|
-
|
|
1030
|
-
return (
|
|
1031
|
-
<Box>
|
|
1032
|
-
<svg
|
|
1033
|
-
width="100%"
|
|
1034
|
-
viewBox={`0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`}
|
|
1035
|
-
role="img"
|
|
1036
|
-
aria-label="Win loss streak timeline"
|
|
1037
|
-
>
|
|
1038
|
-
<line
|
|
1039
|
-
x1="0"
|
|
1040
|
-
x2={CHART_WIDTH}
|
|
1041
|
-
y1={centerY}
|
|
1042
|
-
y2={centerY}
|
|
1043
|
-
stroke="#374151"
|
|
1044
|
-
/>
|
|
1045
|
-
{trades.map((trade, index) => {
|
|
1046
|
-
const height = Math.max(2, (Math.abs(trade.pnl) / maxAbsPnl) * 48);
|
|
1047
|
-
const isWin = trade.pnl > 0;
|
|
1048
|
-
const y = isWin ? centerY - height : centerY;
|
|
1049
|
-
|
|
1050
|
-
return (
|
|
1051
|
-
<rect
|
|
1052
|
-
key={`${trade.timestamp}-${index}`}
|
|
1053
|
-
x={index * barWidth}
|
|
1054
|
-
y={y}
|
|
1055
|
-
width={Math.max(1, barWidth - 0.4)}
|
|
1056
|
-
height={height}
|
|
1057
|
-
fill={getChartPnlColor(trade.pnl)}
|
|
1058
|
-
opacity="0.9"
|
|
1059
|
-
/>
|
|
1060
|
-
);
|
|
1061
|
-
})}
|
|
1062
|
-
</svg>
|
|
1063
|
-
<Flex justify="space-between" align="center" mt={2}>
|
|
1064
|
-
<Text fontSize="xs" color="gray.500">
|
|
1065
|
-
wins above line, losses below
|
|
1066
|
-
</Text>
|
|
1067
|
-
<Text
|
|
1068
|
-
fontSize="sm"
|
|
1069
|
-
color="gray.300"
|
|
1070
|
-
fontFamily="mono"
|
|
1071
|
-
fontWeight="bold"
|
|
1072
|
-
>
|
|
1073
|
-
{formatInteger(trades.length)} trades
|
|
1074
|
-
</Text>
|
|
1075
|
-
</Flex>
|
|
1076
|
-
</Box>
|
|
1077
|
-
);
|
|
1078
|
-
};
|
|
1079
|
-
|
|
1080
|
-
const PnlDistributionChart = ({ bins }: { bins: DistributionBin[] }) => {
|
|
1081
|
-
if (!bins.length) {
|
|
1082
|
-
return <EmptyChart />;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
const maxCount = Math.max(...bins.map((bin) => bin.count), 1);
|
|
1086
|
-
const barWidth = CHART_WIDTH / bins.length;
|
|
1087
|
-
|
|
1088
|
-
return (
|
|
1089
|
-
<Box>
|
|
1090
|
-
<svg
|
|
1091
|
-
width="100%"
|
|
1092
|
-
viewBox={`0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`}
|
|
1093
|
-
role="img"
|
|
1094
|
-
aria-label="P and L distribution"
|
|
1095
|
-
>
|
|
1096
|
-
{bins.map((bin, index) => {
|
|
1097
|
-
const height = Math.max(2, (bin.count / maxCount) * 92);
|
|
1098
|
-
const x = index * barWidth + 2;
|
|
1099
|
-
const y = CHART_HEIGHT - height - CHART_PADDING;
|
|
1100
|
-
const midpoint = (bin.min + bin.max) / 2;
|
|
1101
|
-
|
|
1102
|
-
return (
|
|
1103
|
-
<rect
|
|
1104
|
-
key={bin.id}
|
|
1105
|
-
x={x}
|
|
1106
|
-
y={y}
|
|
1107
|
-
width={Math.max(2, barWidth - 4)}
|
|
1108
|
-
height={height}
|
|
1109
|
-
fill={getChartPnlColor(midpoint)}
|
|
1110
|
-
/>
|
|
1111
|
-
);
|
|
1112
|
-
})}
|
|
1113
|
-
</svg>
|
|
1114
|
-
<Flex justify="space-between" align="center" mt={2}>
|
|
1115
|
-
<Text fontSize="xs" color="gray.500">
|
|
1116
|
-
trade P&L buckets
|
|
1117
|
-
</Text>
|
|
1118
|
-
<Text
|
|
1119
|
-
fontSize="sm"
|
|
1120
|
-
color="gray.300"
|
|
1121
|
-
fontFamily="mono"
|
|
1122
|
-
fontWeight="bold"
|
|
1123
|
-
>
|
|
1124
|
-
{formatInteger(bins.reduce((sum, bin) => sum + bin.count, 0))}
|
|
1125
|
-
</Text>
|
|
1126
|
-
</Flex>
|
|
1127
|
-
</Box>
|
|
1128
|
-
);
|
|
1129
|
-
};
|
|
1130
|
-
|
|
1131
|
-
const RollingPerformanceChart = ({
|
|
1132
|
-
points,
|
|
1133
|
-
}: {
|
|
1134
|
-
points: RollingPerformancePoint[];
|
|
1135
|
-
}) => {
|
|
1136
|
-
if (points.length < 2) {
|
|
1137
|
-
return <EmptyChart />;
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
const winRateLine = buildPolylinePoints(points.map((point) => point.winRate));
|
|
1141
|
-
const pnlLine = buildPolylinePoints(points.map((point) => point.pnl));
|
|
1142
|
-
const latest = points[points.length - 1];
|
|
1143
|
-
|
|
1144
|
-
return (
|
|
1145
|
-
<Box>
|
|
1146
|
-
<svg
|
|
1147
|
-
width="100%"
|
|
1148
|
-
viewBox={`0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`}
|
|
1149
|
-
role="img"
|
|
1150
|
-
aria-label="Rolling win rate and rolling P and L"
|
|
1151
|
-
>
|
|
1152
|
-
<line
|
|
1153
|
-
x1={CHART_PADDING}
|
|
1154
|
-
x2={CHART_WIDTH - CHART_PADDING}
|
|
1155
|
-
y1={CHART_HEIGHT / 2}
|
|
1156
|
-
y2={CHART_HEIGHT / 2}
|
|
1157
|
-
stroke="#374151"
|
|
1158
|
-
strokeDasharray="4 4"
|
|
1159
|
-
/>
|
|
1160
|
-
<polyline
|
|
1161
|
-
points={pnlLine}
|
|
1162
|
-
fill="none"
|
|
1163
|
-
stroke={POSITIVE_CHART_COLOR}
|
|
1164
|
-
strokeWidth="2"
|
|
1165
|
-
/>
|
|
1166
|
-
<polyline
|
|
1167
|
-
points={winRateLine}
|
|
1168
|
-
fill="none"
|
|
1169
|
-
stroke="#fbbf24"
|
|
1170
|
-
strokeWidth="2"
|
|
1171
|
-
opacity="0.9"
|
|
1172
|
-
/>
|
|
1173
|
-
</svg>
|
|
1174
|
-
<Flex justify="space-between" align="center" mt={2}>
|
|
1175
|
-
<Text fontSize="xs" color="gray.500">
|
|
1176
|
-
teal P&L, yellow win rate
|
|
1177
|
-
</Text>
|
|
1178
|
-
<Text
|
|
1179
|
-
fontSize="sm"
|
|
1180
|
-
color="gray.300"
|
|
1181
|
-
fontFamily="mono"
|
|
1182
|
-
fontWeight="bold"
|
|
1183
|
-
>
|
|
1184
|
-
{formatPercent(latest?.winRate)} /{' '}
|
|
1185
|
-
{formatSignedNumber(latest?.pnl ?? null)}
|
|
1186
|
-
</Text>
|
|
1187
|
-
</Flex>
|
|
1188
|
-
</Box>
|
|
1189
|
-
);
|
|
1190
|
-
};
|
|
1191
|
-
|
|
1192
|
-
const TimeOfDaySessionChart = ({
|
|
1193
|
-
sessions,
|
|
1194
|
-
hours,
|
|
1195
|
-
}: {
|
|
1196
|
-
sessions: SessionPnlStat[];
|
|
1197
|
-
hours: HourlyPnlStat[];
|
|
1198
|
-
}) => {
|
|
1199
|
-
if (!sessions.some((session) => session.orders > 0)) {
|
|
1200
|
-
return <EmptyChart />;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
const maxSessionAbsPnl = Math.max(
|
|
1204
|
-
...sessions.map((session) => Math.abs(session.pnl)),
|
|
1205
|
-
1,
|
|
1206
|
-
);
|
|
1207
|
-
const maxHourAbsPnl = Math.max(...hours.map((hour) => Math.abs(hour.pnl)), 1);
|
|
1208
|
-
|
|
1209
|
-
return (
|
|
1210
|
-
<Flex direction="column" gap={4}>
|
|
1211
|
-
<SimpleGrid columns={3} gap={3}>
|
|
1212
|
-
{sessions.map((session) => {
|
|
1213
|
-
const width = Math.max(
|
|
1214
|
-
4,
|
|
1215
|
-
(Math.abs(session.pnl) / maxSessionAbsPnl) * 100,
|
|
1216
|
-
);
|
|
1217
|
-
|
|
1218
|
-
return (
|
|
1219
|
-
<Box key={session.session}>
|
|
1220
|
-
<Flex justify="space-between" align="center" mb={1}>
|
|
1221
|
-
<Text fontSize="xs" color="gray.400" fontWeight="semibold">
|
|
1222
|
-
{session.session}
|
|
1223
|
-
</Text>
|
|
1224
|
-
<Text
|
|
1225
|
-
fontSize="xs"
|
|
1226
|
-
color={getPnlColor(session.pnl)}
|
|
1227
|
-
fontFamily="mono"
|
|
1228
|
-
fontWeight="bold"
|
|
1229
|
-
>
|
|
1230
|
-
{formatSignedNumber(session.pnl)}
|
|
1231
|
-
</Text>
|
|
1232
|
-
</Flex>
|
|
1233
|
-
<Box h="8px" bg="gray.800">
|
|
1234
|
-
<Box
|
|
1235
|
-
h="full"
|
|
1236
|
-
w={`${width}%`}
|
|
1237
|
-
bg={getChartPnlColor(session.pnl)}
|
|
1238
|
-
/>
|
|
1239
|
-
</Box>
|
|
1240
|
-
<Text mt={1} fontSize="xs" color="gray.500" fontFamily="mono">
|
|
1241
|
-
{formatInteger(session.orders)} orders
|
|
1242
|
-
</Text>
|
|
1243
|
-
</Box>
|
|
1244
|
-
);
|
|
1245
|
-
})}
|
|
1246
|
-
</SimpleGrid>
|
|
1247
|
-
|
|
1248
|
-
<svg
|
|
1249
|
-
width="100%"
|
|
1250
|
-
viewBox={`0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`}
|
|
1251
|
-
role="img"
|
|
1252
|
-
aria-label="P and L by UTC hour"
|
|
1253
|
-
>
|
|
1254
|
-
<line
|
|
1255
|
-
x1="0"
|
|
1256
|
-
x2={CHART_WIDTH}
|
|
1257
|
-
y1={CHART_HEIGHT / 2}
|
|
1258
|
-
y2={CHART_HEIGHT / 2}
|
|
1259
|
-
stroke="#374151"
|
|
1260
|
-
/>
|
|
1261
|
-
{hours.map((hour) => {
|
|
1262
|
-
const barWidth = CHART_WIDTH / 24;
|
|
1263
|
-
const height = Math.max(1, (Math.abs(hour.pnl) / maxHourAbsPnl) * 46);
|
|
1264
|
-
const isPositive = hour.pnl >= 0;
|
|
1265
|
-
const y = isPositive ? CHART_HEIGHT / 2 - height : CHART_HEIGHT / 2;
|
|
1266
|
-
|
|
1267
|
-
return (
|
|
1268
|
-
<rect
|
|
1269
|
-
key={hour.hour}
|
|
1270
|
-
x={hour.hour * barWidth + 2}
|
|
1271
|
-
y={y}
|
|
1272
|
-
width={Math.max(2, barWidth - 4)}
|
|
1273
|
-
height={height}
|
|
1274
|
-
fill={getChartPnlColor(hour.pnl)}
|
|
1275
|
-
opacity={hour.orders > 0 ? 0.95 : 0.2}
|
|
1276
|
-
/>
|
|
1277
|
-
);
|
|
1278
|
-
})}
|
|
1279
|
-
</svg>
|
|
1280
|
-
<Text fontSize="xs" color="gray.500">
|
|
1281
|
-
UTC hours, sessions: Asia 00-07, Europe 08-15, US 16-23
|
|
1282
|
-
</Text>
|
|
1283
|
-
</Flex>
|
|
1284
|
-
);
|
|
1285
|
-
};
|
|
1286
|
-
|
|
1287
529
|
const renderPnlRanking = ({
|
|
1288
530
|
title,
|
|
1289
531
|
subtitle,
|
|
@@ -1431,6 +673,50 @@ const mapRuntimeOrder = (order: RuntimeOrderView): OrdersDrawerOrder => {
|
|
|
1431
673
|
};
|
|
1432
674
|
};
|
|
1433
675
|
|
|
676
|
+
export const buildRuntimeStrategyCardViewModel = (
|
|
677
|
+
strategy: RuntimeStrategyView,
|
|
678
|
+
) => {
|
|
679
|
+
const symbolPnlRanking = buildRuntimeSymbolPnlRanking(strategy.orders);
|
|
680
|
+
const firstPoint = strategy.orderLog[0];
|
|
681
|
+
const lastPoint = strategy.orderLog[strategy.orderLog.length - 1];
|
|
682
|
+
|
|
683
|
+
return {
|
|
684
|
+
lastTrade: strategy.recentTrades[0],
|
|
685
|
+
runtimeOrders: strategy.orders.map(mapRuntimeOrder),
|
|
686
|
+
runtimeOrderSummaryItems: getRuntimeOrdersSummaryItems(strategy.orders),
|
|
687
|
+
drawerMetrics: buildRuntimeDrawerMetrics(strategy),
|
|
688
|
+
performance: buildStrategyPerformanceViewModel(strategy.orderLog),
|
|
689
|
+
symbolPnlRanking,
|
|
690
|
+
symbolConcentration: buildRuntimeSymbolConcentration(strategy.orders).slice(
|
|
691
|
+
0,
|
|
692
|
+
8,
|
|
693
|
+
),
|
|
694
|
+
topSymbolPnlRanking: [...symbolPnlRanking]
|
|
695
|
+
.sort(
|
|
696
|
+
(left, right) =>
|
|
697
|
+
right.pnl - left.pnl || left.symbol.localeCompare(right.symbol),
|
|
698
|
+
)
|
|
699
|
+
.slice(0, 10),
|
|
700
|
+
worstSymbolPnlRanking: [...symbolPnlRanking]
|
|
701
|
+
.sort(
|
|
702
|
+
(left, right) =>
|
|
703
|
+
left.pnl - right.pnl || left.symbol.localeCompare(right.symbol),
|
|
704
|
+
)
|
|
705
|
+
.slice(0, 10),
|
|
706
|
+
symbolRankingMaxAbsPnl: Math.max(
|
|
707
|
+
...symbolPnlRanking.map((rank) => Math.abs(rank.pnl)),
|
|
708
|
+
1,
|
|
709
|
+
),
|
|
710
|
+
directionStats: buildRuntimeDirectionStats(strategy.orders),
|
|
711
|
+
advancedMetrics: calculateAdvancedTradeMetrics({
|
|
712
|
+
trades: buildRuntimeAdvancedTrades(strategy.orders),
|
|
713
|
+
orderLog: strategy.orderLog,
|
|
714
|
+
startTimestamp: firstPoint?.[0] ?? null,
|
|
715
|
+
endTimestamp: lastPoint?.[0] ?? null,
|
|
716
|
+
}),
|
|
717
|
+
};
|
|
718
|
+
};
|
|
719
|
+
|
|
1434
720
|
export const RuntimeStrategyCard = ({
|
|
1435
721
|
strategy,
|
|
1436
722
|
provider,
|
|
@@ -1447,94 +733,31 @@ export const RuntimeStrategyCard = ({
|
|
|
1447
733
|
const [configOpen, setConfigOpen] = useState(false);
|
|
1448
734
|
const [ordersOpen, setOrdersOpen] = useState(false);
|
|
1449
735
|
const [statsOpen, setStatsOpen] = useState(false);
|
|
1450
|
-
const
|
|
1451
|
-
|
|
1452
|
-
() => strategy.orders.map(mapRuntimeOrder),
|
|
1453
|
-
[strategy.orders],
|
|
1454
|
-
);
|
|
1455
|
-
const runtimeOrderSummaryItems = useMemo(
|
|
1456
|
-
() => getRuntimeOrdersSummaryItems(strategy.orders),
|
|
1457
|
-
[strategy.orders],
|
|
1458
|
-
);
|
|
1459
|
-
const drawerMetrics = useMemo(
|
|
1460
|
-
() => buildRuntimeDrawerMetrics(strategy),
|
|
736
|
+
const viewModel = useMemo(
|
|
737
|
+
() => buildRuntimeStrategyCardViewModel(strategy),
|
|
1461
738
|
[strategy],
|
|
1462
739
|
);
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
() => buildSessionPnlStats(runtimeTradePoints),
|
|
1485
|
-
[runtimeTradePoints],
|
|
1486
|
-
);
|
|
1487
|
-
const hourlyPnlStats = useMemo(
|
|
1488
|
-
() => buildHourlyPnlStats(runtimeTradePoints),
|
|
1489
|
-
[runtimeTradePoints],
|
|
1490
|
-
);
|
|
1491
|
-
const symbolPnlRanking = useMemo(
|
|
1492
|
-
() => buildRuntimeSymbolPnlRanking(strategy.orders),
|
|
1493
|
-
[strategy.orders],
|
|
1494
|
-
);
|
|
1495
|
-
const symbolConcentration = useMemo(
|
|
1496
|
-
() => buildRuntimeSymbolConcentration(strategy.orders).slice(0, 8),
|
|
1497
|
-
[strategy.orders],
|
|
1498
|
-
);
|
|
1499
|
-
const topSymbolPnlRanking = useMemo(
|
|
1500
|
-
() =>
|
|
1501
|
-
[...symbolPnlRanking]
|
|
1502
|
-
.sort(
|
|
1503
|
-
(left, right) =>
|
|
1504
|
-
right.pnl - left.pnl || left.symbol.localeCompare(right.symbol),
|
|
1505
|
-
)
|
|
1506
|
-
.slice(0, 10),
|
|
1507
|
-
[symbolPnlRanking],
|
|
1508
|
-
);
|
|
1509
|
-
const worstSymbolPnlRanking = useMemo(
|
|
1510
|
-
() =>
|
|
1511
|
-
[...symbolPnlRanking]
|
|
1512
|
-
.sort(
|
|
1513
|
-
(left, right) =>
|
|
1514
|
-
left.pnl - right.pnl || left.symbol.localeCompare(right.symbol),
|
|
1515
|
-
)
|
|
1516
|
-
.slice(0, 10),
|
|
1517
|
-
[symbolPnlRanking],
|
|
1518
|
-
);
|
|
1519
|
-
const symbolRankingMaxAbsPnl = useMemo(
|
|
1520
|
-
() => Math.max(...symbolPnlRanking.map((rank) => Math.abs(rank.pnl)), 1),
|
|
1521
|
-
[symbolPnlRanking],
|
|
1522
|
-
);
|
|
1523
|
-
const directionStats = useMemo(
|
|
1524
|
-
() => buildRuntimeDirectionStats(strategy.orders),
|
|
1525
|
-
[strategy.orders],
|
|
1526
|
-
);
|
|
1527
|
-
const advancedMetrics = useMemo(() => {
|
|
1528
|
-
const firstPoint = strategy.orderLog[0];
|
|
1529
|
-
const lastPoint = strategy.orderLog[strategy.orderLog.length - 1];
|
|
1530
|
-
|
|
1531
|
-
return calculateAdvancedTradeMetrics({
|
|
1532
|
-
trades: buildRuntimeAdvancedTrades(strategy.orders),
|
|
1533
|
-
orderLog: strategy.orderLog,
|
|
1534
|
-
startTimestamp: firstPoint?.[0] ?? null,
|
|
1535
|
-
endTimestamp: lastPoint?.[0] ?? null,
|
|
1536
|
-
});
|
|
1537
|
-
}, [strategy.orderLog, strategy.orders]);
|
|
740
|
+
const {
|
|
741
|
+
lastTrade,
|
|
742
|
+
runtimeOrders,
|
|
743
|
+
runtimeOrderSummaryItems,
|
|
744
|
+
drawerMetrics,
|
|
745
|
+
symbolConcentration,
|
|
746
|
+
topSymbolPnlRanking,
|
|
747
|
+
worstSymbolPnlRanking,
|
|
748
|
+
symbolRankingMaxAbsPnl,
|
|
749
|
+
directionStats,
|
|
750
|
+
advancedMetrics,
|
|
751
|
+
} = viewModel;
|
|
752
|
+
const {
|
|
753
|
+
monthlyStats,
|
|
754
|
+
tradePoints: runtimeTradePoints,
|
|
755
|
+
drawdownPoints,
|
|
756
|
+
rollingPerformancePoints,
|
|
757
|
+
pnlDistributionBins,
|
|
758
|
+
sessionPnlStats,
|
|
759
|
+
hourlyPnlStats,
|
|
760
|
+
} = viewModel.performance;
|
|
1538
761
|
|
|
1539
762
|
return (
|
|
1540
763
|
<Box
|
|
@@ -2182,9 +1405,7 @@ export const RuntimeStrategyCard = ({
|
|
|
2182
1405
|
<RuntimeStrategyChart
|
|
2183
1406
|
orderLog={strategy.orderLog}
|
|
2184
1407
|
stat={strategy.stat}
|
|
2185
|
-
|
|
2186
|
-
aiGateChanges={strategy.aiGateChanges}
|
|
2187
|
-
maxLossValueTimeline={strategy.maxLossValueTimeline}
|
|
1408
|
+
evidenceTimeline={strategy.evidenceTimeline}
|
|
2188
1409
|
startTimestamp={startTimestamp}
|
|
2189
1410
|
endTimestamp={endTimestamp}
|
|
2190
1411
|
/>
|