@tradejs/app 3.1.10 → 3.1.11

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tradejs/app",
3
- "version": "3.1.10",
3
+ "version": "3.1.11",
4
4
  "description": "Installable Next.js UI for the TradeJS TypeScript framework: dashboards, backtests, charts, and runtime data.",
5
5
  "keywords": [
6
6
  "tradejs",
@@ -52,11 +52,11 @@
52
52
  "@chakra-ui/charts": "3.36.1",
53
53
  "@chakra-ui/react": "3.36.1",
54
54
  "@emotion/react": "^11.14.0",
55
- "@tradejs/core": "^3.1.10",
56
- "@tradejs/indicators": "^3.1.10",
57
- "@tradejs/infra": "^3.1.10",
58
- "@tradejs/node": "^3.1.10",
59
- "@tradejs/types": "^3.1.10",
55
+ "@tradejs/core": "^3.1.11",
56
+ "@tradejs/indicators": "^3.1.11",
57
+ "@tradejs/infra": "^3.1.11",
58
+ "@tradejs/node": "^3.1.11",
59
+ "@tradejs/types": "^3.1.11",
60
60
  "@types/bcryptjs": "2.4.6",
61
61
  "@types/lodash": "4.17.24",
62
62
  "@types/node": "24.13.3",
@@ -12,13 +12,17 @@ import {
12
12
  ReferenceLine,
13
13
  ResponsiveContainer,
14
14
  } from 'recharts';
15
- import { useTestsCompare } from '#store';
15
+ import { useBacktest, useTestsCompare } from '#store';
16
16
  import { useTestContext } from '../context';
17
17
  import { TestCompareList } from '@tradejs/types';
18
18
  import { mapOrderLogToChartData, getChartData } from './utils';
19
19
  import { getFormatted, getTimeline } from '@tradejs/core/backtest';
20
20
  import { formatTimeSeriesTooltipTimestamp } from '#app/lib/timeSeriesChart';
21
21
  import { TimeSeriesXAxis } from '#shared/Charts/TimeSeriesXAxis';
22
+ import {
23
+ buildBacktestTradeOutcomePoints,
24
+ TradeOutcomeMarkers,
25
+ } from '#shared/Charts/TradeOutcomeMarkers';
22
26
 
23
27
  interface TestCardChartProps {
24
28
  height?: string | number;
@@ -28,6 +32,7 @@ export const TestCardChart = ({ height = '350px' }: TestCardChartProps) => {
28
32
  const { testResult } = useTestContext();
29
33
  const { test, stat } = testResult;
30
34
  const { compareList } = useTestsCompare();
35
+ const { backtest } = useBacktest(test.name);
31
36
 
32
37
  const chartData = useMemo(() => {
33
38
  if (!compareList.length) {
@@ -56,6 +61,10 @@ export const TestCardChart = ({ height = '350px' }: TestCardChartProps) => {
56
61
  ]);
57
62
 
58
63
  const chart = useChart(chartData as any);
64
+ const tradeOutcomePoints = useMemo(
65
+ () => buildBacktestTradeOutcomePoints(backtest),
66
+ [backtest],
67
+ );
59
68
 
60
69
  const { formatted: maxAmount } = getFormatted(stat, 'maxAmount');
61
70
  const { formatted: minAmount } = getFormatted(stat, 'minAmount');
@@ -122,6 +131,11 @@ export const TestCardChart = ({ height = '350px' }: TestCardChartProps) => {
122
131
  activeDot={{ r: 5, strokeWidth: 2 }}
123
132
  />
124
133
  ))}
134
+ <TradeOutcomeMarkers
135
+ points={tradeOutcomePoints}
136
+ positiveColor={chart.color('green.400')}
137
+ negativeColor={chart.color('red.400')}
138
+ />
125
139
  </LineChart>
126
140
  </Chart.Root>
127
141
  </ResponsiveContainer>
@@ -0,0 +1,93 @@
1
+ 'use client';
2
+
3
+ import { ReferenceDot } from 'recharts';
4
+ import type {
5
+ OrderLogData,
6
+ SimpleOrderLogData,
7
+ StrategyChartOrder,
8
+ } from '@tradejs/types';
9
+
10
+ export interface TradeOutcomePoint {
11
+ timestamp: number;
12
+ equity: number;
13
+ pnl: number;
14
+ }
15
+
16
+ interface TradeOutcomeCandidate {
17
+ timestamp?: number | null;
18
+ equity?: number | null;
19
+ pnl?: number | null;
20
+ }
21
+
22
+ export const normalizeTradeOutcomePoints = (
23
+ candidates: readonly TradeOutcomeCandidate[],
24
+ ): TradeOutcomePoint[] =>
25
+ candidates.flatMap(({ timestamp, equity, pnl }) =>
26
+ typeof timestamp === 'number' &&
27
+ Number.isFinite(timestamp) &&
28
+ typeof equity === 'number' &&
29
+ Number.isFinite(equity) &&
30
+ typeof pnl === 'number' &&
31
+ Number.isFinite(pnl) &&
32
+ pnl !== 0
33
+ ? [{ timestamp, equity, pnl }]
34
+ : [],
35
+ );
36
+
37
+ export const buildEquityTradeOutcomePoints = (
38
+ orderLog: SimpleOrderLogData,
39
+ ): TradeOutcomePoint[] =>
40
+ normalizeTradeOutcomePoints(
41
+ orderLog.slice(1).map(([timestamp, equity], index) => ({
42
+ timestamp,
43
+ equity,
44
+ pnl: equity - orderLog[index][1],
45
+ })),
46
+ );
47
+
48
+ export const buildSnapshotTradeOutcomePoints = (
49
+ orders: readonly StrategyChartOrder[],
50
+ ): TradeOutcomePoint[] =>
51
+ normalizeTradeOutcomePoints(
52
+ orders.map((order) => ({
53
+ timestamp: order.exitTimestamp,
54
+ equity: order.equityAfter,
55
+ pnl: order.pnl,
56
+ })),
57
+ );
58
+
59
+ export const buildBacktestTradeOutcomePoints = (
60
+ orders: OrderLogData,
61
+ ): TradeOutcomePoint[] =>
62
+ normalizeTradeOutcomePoints(
63
+ orders
64
+ .filter((order) => !order.type.startsWith('OPEN'))
65
+ .map((order) => ({
66
+ timestamp: order.timestamp,
67
+ equity: order.amount,
68
+ pnl: order.profit,
69
+ })),
70
+ );
71
+
72
+ export const TradeOutcomeMarkers = ({
73
+ points,
74
+ positiveColor,
75
+ negativeColor,
76
+ }: {
77
+ points: readonly TradeOutcomePoint[];
78
+ positiveColor: string;
79
+ negativeColor: string;
80
+ }) => (
81
+ <>
82
+ {points.map((point, index) => (
83
+ <ReferenceDot
84
+ key={`${point.timestamp}:${point.equity}:${index}`}
85
+ x={point.timestamp}
86
+ y={point.equity}
87
+ r={2.5}
88
+ fill={point.pnl > 0 ? positiveColor : negativeColor}
89
+ stroke="none"
90
+ />
91
+ ))}
92
+ </>
93
+ );
@@ -16,6 +16,10 @@ import { getFormatted } from '@tradejs/core/backtest';
16
16
  import type { SimpleOrderLogData, TestStat } from '@tradejs/types';
17
17
  import { formatTimeSeriesTooltipTimestamp } from '#app/lib/timeSeriesChart';
18
18
  import { TimeSeriesXAxis } from '#shared/Charts/TimeSeriesXAxis';
19
+ import {
20
+ buildEquityTradeOutcomePoints,
21
+ TradeOutcomeMarkers,
22
+ } from '#shared/Charts/TradeOutcomeMarkers';
19
23
 
20
24
  interface RuntimeStrategyChartProps {
21
25
  orderLog: SimpleOrderLogData;
@@ -47,6 +51,10 @@ export const RuntimeStrategyChart = ({
47
51
  }),
48
52
  [orderLog],
49
53
  );
54
+ const tradeOutcomePoints = useMemo(
55
+ () => buildEquityTradeOutcomePoints(orderLog),
56
+ [orderLog],
57
+ );
50
58
 
51
59
  const chart = useChart(chartData as any);
52
60
  const { formatted: maxAmount } = getFormatted(stat, 'maxAmount');
@@ -114,6 +122,11 @@ export const RuntimeStrategyChart = ({
114
122
  activeDot={{ r: 5, strokeWidth: 2 }}
115
123
  />
116
124
  ))}
125
+ <TradeOutcomeMarkers
126
+ points={tradeOutcomePoints}
127
+ positiveColor={chart.color('green.400')}
128
+ negativeColor={chart.color('red.400')}
129
+ />
117
130
  </LineChart>
118
131
  </Chart.Root>
119
132
  </ResponsiveContainer>
@@ -32,6 +32,10 @@ export const RuntimeStrategyConfigDrawer = ({
32
32
  { label: 'Config ID', value: strategy.configId },
33
33
  { label: 'Runtime key', value: strategy.runtimeKey },
34
34
  { label: 'Deployment', value: strategy.deploymentId },
35
+ {
36
+ label: 'Configured tickers',
37
+ value: strategy.selection?.tickers?.length ?? 'All available',
38
+ },
35
39
  { label: 'Account label', value: strategy.accountLabel ?? 'Not set' },
36
40
  { label: 'Account ID', value: strategy.accountId ?? 'Not assigned' },
37
41
  {
@@ -120,7 +124,22 @@ export const RuntimeStrategyConfigDrawer = ({
120
124
  </SimpleGrid>
121
125
  <Box mt={4} pt={4} borderTopWidth="1px" borderColor="gray.800">
122
126
  <Text fontSize="xs" color="gray.500">
123
- Symbols
127
+ Configured ticker selection
128
+ </Text>
129
+ <Text
130
+ mt={1}
131
+ mb={4}
132
+ fontFamily="mono"
133
+ fontSize="sm"
134
+ color="gray.200"
135
+ overflowWrap="anywhere"
136
+ >
137
+ {strategy.selection?.tickers?.length
138
+ ? strategy.selection.tickers.join(', ')
139
+ : 'All available tickers'}
140
+ </Text>
141
+ <Text fontSize="xs" color="gray.500">
142
+ Symbols with trades
124
143
  </Text>
125
144
  <Text
126
145
  mt={1}
@@ -31,13 +31,8 @@ export {
31
31
  type SymbolPnlRank,
32
32
  } from './StrategySnapshotCard.ranking.presenter';
33
33
 
34
- const formatDatasetCreatedAt = (datasetId?: string) => {
35
- if (!datasetId || !/^\d{12,}$/.test(datasetId)) {
36
- return '';
37
- }
38
-
39
- const timestamp = Number(datasetId);
40
- if (!Number.isSafeInteger(timestamp)) {
34
+ const formatGeneratedAt = (timestamp: number) => {
35
+ if (!Number.isFinite(timestamp)) {
41
36
  return '';
42
37
  }
43
38
 
@@ -48,7 +43,7 @@ const formatDatasetCreatedAt = (datasetId?: string) => {
48
43
 
49
44
  return new Intl.DateTimeFormat('en-GB', {
50
45
  dateStyle: 'medium',
51
- timeStyle: 'short',
46
+ timeStyle: 'medium',
52
47
  }).format(date);
53
48
  };
54
49
 
@@ -109,8 +104,8 @@ export const buildStrategySnapshotCardViewModel = (
109
104
  sourceLabel: mode === 'ai' && snapshot.datasetId ? 'dataset:' : 'symbols:',
110
105
  sourceValue:
111
106
  mode === 'ai' && snapshot.datasetId ? snapshot.datasetId : symbolsLabel,
112
- datasetCreatedAtLabel:
113
- mode === 'ai' ? formatDatasetCreatedAt(snapshot.datasetId) : '',
107
+ generatedAtLabel:
108
+ mode === 'ai' ? formatGeneratedAt(snapshot.generatedAt) : '',
114
109
  tagsLabel: snapshot.tags?.join(' · ') ?? '',
115
110
  displaySubtitle:
116
111
  mode === 'ai'
@@ -53,7 +53,7 @@ export const StrategySnapshotCard = ({
53
53
  snapshotOrders,
54
54
  sourceLabel,
55
55
  sourceValue,
56
- datasetCreatedAtLabel,
56
+ generatedAtLabel,
57
57
  tagsLabel,
58
58
  displaySubtitle,
59
59
  metrics,
@@ -132,13 +132,13 @@ export const StrategySnapshotCard = ({
132
132
  </Text>
133
133
  </Flex>
134
134
 
135
- {datasetCreatedAtLabel ? (
135
+ {generatedAtLabel ? (
136
136
  <Flex gap="1">
137
137
  <Text fontSize="sm" fontWeight="bold" color="gray.400" mt={1}>
138
- exported:
138
+ generated:
139
139
  </Text>
140
140
  <Text fontSize="sm" color="gray.300" mt={1}>
141
- {datasetCreatedAtLabel}
141
+ {generatedAtLabel}
142
142
  </Text>
143
143
  </Flex>
144
144
  ) : null}
@@ -289,6 +289,8 @@ export const StrategySnapshotCard = ({
289
289
 
290
290
  <StrategySnapshotChart
291
291
  orderLog={snapshot.orderLog}
292
+ orders={snapshot.orders}
293
+ mode={mode}
292
294
  emptyText={emptyText}
293
295
  />
294
296
 
@@ -11,16 +11,25 @@ import {
11
11
  Tooltip,
12
12
  YAxis,
13
13
  } from 'recharts';
14
- import type { SimpleOrderLogData } from '@tradejs/types';
14
+ import type { SimpleOrderLogData, StrategyChartOrder } from '@tradejs/types';
15
15
  import { formatTimeSeriesTooltipTimestamp } from '#app/lib/timeSeriesChart';
16
16
  import { TimeSeriesXAxis } from '#shared/Charts/TimeSeriesXAxis';
17
+ import {
18
+ buildEquityTradeOutcomePoints,
19
+ buildSnapshotTradeOutcomePoints,
20
+ TradeOutcomeMarkers,
21
+ } from '#shared/Charts/TradeOutcomeMarkers';
17
22
 
18
23
  export const StrategySnapshotChart = ({
19
24
  orderLog,
25
+ orders,
26
+ mode,
20
27
  height = '350px',
21
28
  emptyText = 'No chart data for the selected run.',
22
29
  }: {
23
30
  orderLog: SimpleOrderLogData;
31
+ orders: StrategyChartOrder[];
32
+ mode: 'replay' | 'ai';
24
33
  height?: string | number;
25
34
  emptyText?: string;
26
35
  }) => {
@@ -39,6 +48,13 @@ export const StrategySnapshotChart = ({
39
48
  }),
40
49
  [orderLog],
41
50
  );
51
+ const tradeOutcomePoints = useMemo(
52
+ () =>
53
+ mode === 'ai'
54
+ ? buildEquityTradeOutcomePoints(orderLog)
55
+ : buildSnapshotTradeOutcomePoints(orders),
56
+ [mode, orderLog, orders],
57
+ );
42
58
 
43
59
  const chart = useChart(chartData as any);
44
60
 
@@ -96,6 +112,11 @@ export const StrategySnapshotChart = ({
96
112
  activeDot={{ r: 5, strokeWidth: 2 }}
97
113
  />
98
114
  ))}
115
+ <TradeOutcomeMarkers
116
+ points={tradeOutcomePoints}
117
+ positiveColor={chart.color('green.400')}
118
+ negativeColor={chart.color('red.400')}
119
+ />
99
120
  </LineChart>
100
121
  </Chart.Root>
101
122
  </ResponsiveContainer>
@@ -37,6 +37,21 @@ export const StrategySnapshotList = ({
37
37
  [strategies],
38
38
  );
39
39
 
40
+ const renderCard = useCallback(
41
+ (strategy: StrategyChartSnapshot) => (
42
+ <StrategySnapshotCard
43
+ key={strategy.cardId}
44
+ snapshot={strategy}
45
+ mode={mode}
46
+ onDeleted={onDeleted}
47
+ selected={selectedCardIds.has(strategy.cardId)}
48
+ onToggleSelection={onToggleSelection}
49
+ emptyText={emptyText}
50
+ />
51
+ ),
52
+ [emptyText, mode, onDeleted, onToggleSelection, selectedCardIds],
53
+ );
54
+
40
55
  const Row = useCallback(
41
56
  ({ index, style }: ListChildComponentProps) => {
42
57
  const strategy = strategies[index];
@@ -44,29 +59,15 @@ export const StrategySnapshotList = ({
44
59
  return null;
45
60
  }
46
61
 
47
- return (
48
- <Box style={style}>
49
- <StrategySnapshotCard
50
- snapshot={strategy}
51
- mode={mode}
52
- onDeleted={onDeleted}
53
- selected={selectedCardIds.has(strategy.cardId)}
54
- onToggleSelection={onToggleSelection}
55
- emptyText={emptyText}
56
- />
57
- </Box>
58
- );
62
+ return <Box style={style}>{renderCard(strategy)}</Box>;
59
63
  },
60
- [
61
- emptyText,
62
- mode,
63
- onDeleted,
64
- onToggleSelection,
65
- selectedCardIds,
66
- strategies,
67
- ],
64
+ [renderCard, strategies],
68
65
  );
69
66
 
67
+ if (mode === 'replay') {
68
+ return <Box w="full">{strategies.map(renderCard)}</Box>;
69
+ }
70
+
70
71
  return (
71
72
  <AutoSizer>
72
73
  {({ height, width }) => (