@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 +6 -6
- package/src/app/components/Backtest/TestCard/Chart/index.tsx +15 -1
- package/src/app/components/Shared/Charts/TradeOutcomeMarkers.tsx +93 -0
- package/src/app/components/Strategies/RuntimeStrategyChart.tsx +13 -0
- package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +20 -1
- package/src/app/components/Strategies/StrategySnapshotCard.presenter.ts +5 -10
- package/src/app/components/Strategies/StrategySnapshotCard.tsx +6 -4
- package/src/app/components/Strategies/StrategySnapshotChart.tsx +22 -1
- package/src/app/components/Strategies/StrategySnapshotList.tsx +21 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tradejs/app",
|
|
3
|
-
"version": "3.1.
|
|
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.
|
|
56
|
-
"@tradejs/indicators": "^3.1.
|
|
57
|
-
"@tradejs/infra": "^3.1.
|
|
58
|
-
"@tradejs/node": "^3.1.
|
|
59
|
-
"@tradejs/types": "^3.1.
|
|
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
|
-
|
|
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
|
|
35
|
-
if (!
|
|
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: '
|
|
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
|
-
|
|
113
|
-
mode === 'ai' ?
|
|
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
|
-
|
|
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
|
-
{
|
|
135
|
+
{generatedAtLabel ? (
|
|
136
136
|
<Flex gap="1">
|
|
137
137
|
<Text fontSize="sm" fontWeight="bold" color="gray.400" mt={1}>
|
|
138
|
-
|
|
138
|
+
generated:
|
|
139
139
|
</Text>
|
|
140
140
|
<Text fontSize="sm" color="gray.300" mt={1}>
|
|
141
|
-
{
|
|
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 }) => (
|