@tradejs/app 1.0.0

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.
Files changed (126) hide show
  1. package/README.md +12 -0
  2. package/bin/tradejs-app.mjs +54 -0
  3. package/next-env.d.ts +6 -0
  4. package/next.config.mjs +31 -0
  5. package/package.json +60 -0
  6. package/src/app/actions/ai.ts +33 -0
  7. package/src/app/actions/backtest.ts +55 -0
  8. package/src/app/actions/kline.ts +18 -0
  9. package/src/app/actions/scanner.ts +10 -0
  10. package/src/app/actions/signal.ts +19 -0
  11. package/src/app/api/ai/route.ts +151 -0
  12. package/src/app/api/auth/[...nextauth]/route.ts +5 -0
  13. package/src/app/api/backtest/files/route.ts +60 -0
  14. package/src/app/api/backtest/order-log/[strategy]/[name]/route.ts +47 -0
  15. package/src/app/api/backtest/result/[strategy]/[name]/route.ts +63 -0
  16. package/src/app/api/backtest/test/[strategy]/[name]/route.ts +57 -0
  17. package/src/app/api/cron/route.ts +4 -0
  18. package/src/app/api/derivatives/[symbol]/[interval]/route.ts +57 -0
  19. package/src/app/api/derivatives/summary/route.ts +20 -0
  20. package/src/app/api/files/screenshot/[name]/route.ts +42 -0
  21. package/src/app/api/indicators/route.ts +24 -0
  22. package/src/app/api/kline/[provider]/[symbol]/[interval]/route.ts +123 -0
  23. package/src/app/api/scanner/[provider]/route.ts +41 -0
  24. package/src/app/api/scanner/route.ts +31 -0
  25. package/src/app/api/signal/[symbol]/[signalId]/route.ts +42 -0
  26. package/src/app/api/spread/[symbol]/[interval]/route.ts +57 -0
  27. package/src/app/api/spread/summary/route.ts +20 -0
  28. package/src/app/auth.ts +76 -0
  29. package/src/app/components/Backtest/CompareList/index.tsx +34 -0
  30. package/src/app/components/Backtest/TestCard/Chart/index.tsx +118 -0
  31. package/src/app/components/Backtest/TestCard/Chart/utils/index.ts +81 -0
  32. package/src/app/components/Backtest/TestCard/CompareButton/index.tsx +21 -0
  33. package/src/app/components/Backtest/TestCard/ConfigDrawer/JsonCodeBlock.tsx +46 -0
  34. package/src/app/components/Backtest/TestCard/ConfigDrawer/index.tsx +94 -0
  35. package/src/app/components/Backtest/TestCard/DeleteButton/index.tsx +128 -0
  36. package/src/app/components/Backtest/TestCard/FavoriteIndicator/index.tsx +18 -0
  37. package/src/app/components/Backtest/TestCard/OpenDashboardButton/index.tsx +40 -0
  38. package/src/app/components/Backtest/TestCard/OpenReportButton/index.tsx +24 -0
  39. package/src/app/components/Backtest/TestCard/Root/index.tsx +55 -0
  40. package/src/app/components/Backtest/TestCard/Skeleton/index.tsx +21 -0
  41. package/src/app/components/Backtest/TestCard/Stat/index.tsx +119 -0
  42. package/src/app/components/Backtest/TestCard/Title/index.tsx +84 -0
  43. package/src/app/components/Backtest/TestCard/context.ts +14 -0
  44. package/src/app/components/Backtest/TestCard/index.ts +28 -0
  45. package/src/app/components/Backtest/TestList/index.tsx +124 -0
  46. package/src/app/components/Dashboard/AiDrawer/Message.tsx +34 -0
  47. package/src/app/components/Dashboard/AiDrawer/index.tsx +163 -0
  48. package/src/app/components/Dashboard/KlineChart/figures/backtestFigureTypes.ts +7 -0
  49. package/src/app/components/Dashboard/KlineChart/figures/backtestMarkersPointFigure.ts +76 -0
  50. package/src/app/components/Dashboard/KlineChart/figures/circle.ts +15 -0
  51. package/src/app/components/Dashboard/KlineChart/figures/diamond.ts +25 -0
  52. package/src/app/components/Dashboard/KlineChart/figures/entryLinePointFigure.ts +1 -0
  53. package/src/app/components/Dashboard/KlineChart/figures/entryPointsPointFigure.ts +1 -0
  54. package/src/app/components/Dashboard/KlineChart/figures/entryZonePointFigure.ts +1 -0
  55. package/src/app/components/Dashboard/KlineChart/figures/index.ts +213 -0
  56. package/src/app/components/Dashboard/KlineChart/figures/label.ts +14 -0
  57. package/src/app/components/Dashboard/KlineChart/figures/rectangle.ts +20 -0
  58. package/src/app/components/Dashboard/KlineChart/figures/square.ts +21 -0
  59. package/src/app/components/Dashboard/KlineChart/figures/star.ts +39 -0
  60. package/src/app/components/Dashboard/KlineChart/figures/tradeZonePointFigure.ts +44 -0
  61. package/src/app/components/Dashboard/KlineChart/figures/trendLinePointFigure.ts +37 -0
  62. package/src/app/components/Dashboard/KlineChart/figures/trendLinePointsPointFigure.ts +26 -0
  63. package/src/app/components/Dashboard/KlineChart/figures/triangle.ts +23 -0
  64. package/src/app/components/Dashboard/KlineChart/hooks/index.ts +14 -0
  65. package/src/app/components/Dashboard/KlineChart/hooks/indicatorShared.ts +30 -0
  66. package/src/app/components/Dashboard/KlineChart/hooks/useAtrIndicator.ts +75 -0
  67. package/src/app/components/Dashboard/KlineChart/hooks/useBacktest.ts +533 -0
  68. package/src/app/components/Dashboard/KlineChart/hooks/useBbIndicator.ts +74 -0
  69. package/src/app/components/Dashboard/KlineChart/hooks/useBtcCorrelation.ts +155 -0
  70. package/src/app/components/Dashboard/KlineChart/hooks/useBtcIndicator.ts +185 -0
  71. package/src/app/components/Dashboard/KlineChart/hooks/useEmaIndicator.ts +62 -0
  72. package/src/app/components/Dashboard/KlineChart/hooks/useMaIndicator.ts +62 -0
  73. package/src/app/components/Dashboard/KlineChart/hooks/useManagedIndicator.ts +140 -0
  74. package/src/app/components/Dashboard/KlineChart/hooks/usePluginIndicators.ts +212 -0
  75. package/src/app/components/Dashboard/KlineChart/hooks/useResize.ts +29 -0
  76. package/src/app/components/Dashboard/KlineChart/hooks/useSetup.ts +122 -0
  77. package/src/app/components/Dashboard/KlineChart/hooks/useSignal.ts +85 -0
  78. package/src/app/components/Dashboard/KlineChart/hooks/useSpreadIndicator.ts +243 -0
  79. package/src/app/components/Dashboard/KlineChart/hooks/useSupportResistanceLines.ts +125 -0
  80. package/src/app/components/Dashboard/KlineChart/hooks/useTrendLine.ts +139 -0
  81. package/src/app/components/Dashboard/KlineChart/hooks/useVolIndicator.ts +18 -0
  82. package/src/app/components/Dashboard/KlineChart/hooks/useWmaIndicator.ts +62 -0
  83. package/src/app/components/Dashboard/KlineChart/index.tsx +169 -0
  84. package/src/app/components/Dashboard/KlineChart/styles.ts +70 -0
  85. package/src/app/components/Dashboard/MainChart/index.tsx +35 -0
  86. package/src/app/components/Shared/AppShell.tsx +28 -0
  87. package/src/app/components/Shared/FavoriteButton/index.tsx +23 -0
  88. package/src/app/components/Shared/Filters/Backtest/index.tsx +164 -0
  89. package/src/app/components/Shared/Filters/FavoriteIndicator/index.tsx +18 -0
  90. package/src/app/components/Shared/Filters/Indicators/index.tsx +21 -0
  91. package/src/app/components/Shared/Filters/Interval/index.tsx +31 -0
  92. package/src/app/components/Shared/Filters/Interval/intervals.ts +6 -0
  93. package/src/app/components/Shared/Filters/Provider/index.tsx +32 -0
  94. package/src/app/components/Shared/Filters/Root/index.tsx +28 -0
  95. package/src/app/components/Shared/Filters/Symbol/index.tsx +49 -0
  96. package/src/app/components/Shared/Filters/context.ts +17 -0
  97. package/src/app/components/Shared/Filters/index.ts +17 -0
  98. package/src/app/components/Shared/Sidebar/index.tsx +72 -0
  99. package/src/app/components/UI/ColorMode/index.tsx +112 -0
  100. package/src/app/components/UI/EmptyState/index.tsx +28 -0
  101. package/src/app/components/UI/OverlaySpinner/index.tsx +23 -0
  102. package/src/app/components/UI/Segment/index.tsx +23 -0
  103. package/src/app/components/UI/Select/index.tsx +81 -0
  104. package/src/app/components/UI/SelectWithSearch/index.tsx +104 -0
  105. package/src/app/components/UI/Switcher/index.tsx +24 -0
  106. package/src/app/components/UI/Toaster/index.tsx +45 -0
  107. package/src/app/components/UI/index.ts +8 -0
  108. package/src/app/favicon.ico +0 -0
  109. package/src/app/globals.css +5 -0
  110. package/src/app/layout.tsx +31 -0
  111. package/src/app/page.tsx +14 -0
  112. package/src/app/provider.tsx +39 -0
  113. package/src/app/routes/backtest/[test]/page.tsx +33 -0
  114. package/src/app/routes/backtest/page.tsx +374 -0
  115. package/src/app/routes/dashboard/[provider]/[symbol]/[interval]/page.tsx +124 -0
  116. package/src/app/routes/dashboard/page.tsx +20 -0
  117. package/src/app/routes/derivatives/page.tsx +202 -0
  118. package/src/app/routes/signin/page.tsx +155 -0
  119. package/src/app/store/data.ts +144 -0
  120. package/src/app/store/filters.ts +29 -0
  121. package/src/app/store/index.ts +13 -0
  122. package/src/app/store/indicators.ts +229 -0
  123. package/src/app/store/tests.ts +464 -0
  124. package/src/app/store/tickers.ts +89 -0
  125. package/src/proxy.ts +142 -0
  126. package/tsconfig.json +40 -0
@@ -0,0 +1,118 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+ import { Box } from '@chakra-ui/react';
5
+ import { Chart, useChart } from '@chakra-ui/charts';
6
+ import {
7
+ LineChart,
8
+ CartesianGrid,
9
+ Line,
10
+ XAxis,
11
+ YAxis,
12
+ Tooltip,
13
+ ReferenceLine,
14
+ ResponsiveContainer,
15
+ } from 'recharts';
16
+ import { useTestsCompare } from '@store';
17
+ import { useTestContext } from '../context';
18
+ import { TestCompareList } from '@tradejs/types';
19
+ import { mapOrderLogToChartData, getChartData } from './utils';
20
+ import { getFormatted, getTimeline } from '@tradejs/core/backtest';
21
+
22
+ interface TestCardChartProps {
23
+ height?: string | number;
24
+ }
25
+
26
+ export const TestCardChart = ({ height = '350px' }: TestCardChartProps) => {
27
+ const { testResult } = useTestContext();
28
+ const { test, stat } = testResult;
29
+ const { compareList } = useTestsCompare();
30
+
31
+ const chartData = useMemo(() => {
32
+ if (!compareList.length) {
33
+ return mapOrderLogToChartData(testResult);
34
+ }
35
+
36
+ const timeline = getTimeline(test.options.start, test.options.end);
37
+
38
+ const testList: TestCompareList = [
39
+ ...compareList.filter(
40
+ ({ testResult }) => testResult.test.testId !== test.testId,
41
+ ),
42
+ {
43
+ testResult,
44
+ color: 'teal',
45
+ },
46
+ ];
47
+
48
+ return getChartData(testList, timeline);
49
+ }, [
50
+ compareList,
51
+ test.options.end,
52
+ test.options.start,
53
+ test.testId,
54
+ testResult,
55
+ ]);
56
+
57
+ const chart = useChart(chartData as any);
58
+
59
+ const { formatted: maxAmount } = getFormatted(stat, 'maxAmount');
60
+ const { formatted: minAmount } = getFormatted(stat, 'minAmount');
61
+
62
+ return (
63
+ <Box w="100%" minW="600px" h={height} pr={2}>
64
+ <ResponsiveContainer width="100%" height="100%">
65
+ <Chart.Root maxH="md" chart={chart}>
66
+ <LineChart data={chart.data}>
67
+ <CartesianGrid stroke={chart.color('border')} vertical={false} />
68
+ <ReferenceLine
69
+ stroke={chart.color('gray.600')}
70
+ strokeDasharray="5 5"
71
+ y={stat.maxAmount}
72
+ label={{
73
+ value: `Max: ${maxAmount}`,
74
+ offset: 10,
75
+ fill: chart.color('gray.600'),
76
+ position: 'top',
77
+ }}
78
+ />
79
+ <ReferenceLine
80
+ stroke={chart.color('gray.600')}
81
+ strokeDasharray="8 8"
82
+ y={100}
83
+ />
84
+ <ReferenceLine
85
+ stroke={chart.color('gray.600')}
86
+ strokeDasharray="5 5"
87
+ y={stat.minAmount}
88
+ label={{
89
+ value: `Min: ${minAmount}`,
90
+ offset: 10,
91
+ fill: chart.color('gray.600'),
92
+ position: 'bottom',
93
+ }}
94
+ />
95
+ <XAxis dataKey="timestamp" />
96
+ <YAxis tickCount={10} domain={[stat.minAmount - 10, 'auto']} />
97
+ <Tooltip
98
+ animationDuration={100}
99
+ cursor={false}
100
+ content={<Chart.Tooltip />}
101
+ />
102
+
103
+ {chart.series.map((item) => (
104
+ <Line
105
+ key={item.name as string}
106
+ isAnimationActive={false}
107
+ dataKey={chart.key(item.name) as string}
108
+ stroke={chart.color(item.color)}
109
+ strokeWidth={2}
110
+ dot={false}
111
+ />
112
+ ))}
113
+ </LineChart>
114
+ </Chart.Root>
115
+ </ResponsiveContainer>
116
+ </Box>
117
+ );
118
+ };
@@ -0,0 +1,81 @@
1
+ import { format } from 'date-fns';
2
+ import {
3
+ SimpleOrderLogData,
4
+ TestResult,
5
+ TestCompareList,
6
+ } from '@tradejs/types';
7
+
8
+ const getLineName = (testResult: TestResult) =>
9
+ `${testResult.test.symbol}-${testResult.test.testId}`;
10
+
11
+ export const mapOrderLogToChartData = (testResult: TestResult) => {
12
+ const data = testResult.orderLog.map(([timestamp, amount]) => ({
13
+ [getLineName(testResult)]: amount,
14
+ timestamp: format(timestamp, 'dd.MM'),
15
+ }));
16
+
17
+ const series = [
18
+ {
19
+ name: getLineName(testResult),
20
+ color: 'teal.solid',
21
+ },
22
+ ];
23
+
24
+ return {
25
+ data,
26
+ series,
27
+ };
28
+ };
29
+
30
+ const getAmountFromOrderLog = (
31
+ ind: number,
32
+ timeline: number[],
33
+ orderLog: SimpleOrderLogData,
34
+ fallback: number,
35
+ ) => {
36
+ if (ind < 1) {
37
+ return fallback;
38
+ }
39
+
40
+ const order = orderLog.findLast(
41
+ (log) => log[0] <= timeline[ind] && log[0] > timeline[ind - 1],
42
+ );
43
+
44
+ if (!order) {
45
+ return fallback;
46
+ }
47
+
48
+ return order[1];
49
+ };
50
+
51
+ export const getChartData = (testList: TestCompareList, timeline: number[]) => {
52
+ const values: Record<string, number> = {};
53
+
54
+ const data = timeline.map((timestamp, ind) => {
55
+ const formattedTimestamp = format(timestamp, 'dd.MM');
56
+
57
+ testList.forEach(({ testResult }) => {
58
+ values[getLineName(testResult)] = getAmountFromOrderLog(
59
+ ind,
60
+ timeline,
61
+ testResult.orderLog,
62
+ values[getLineName(testResult)] || 100,
63
+ );
64
+ });
65
+
66
+ return {
67
+ ...values,
68
+ timestamp: formattedTimestamp,
69
+ };
70
+ });
71
+
72
+ const series = testList.map(({ testResult, color }) => ({
73
+ name: getLineName(testResult),
74
+ color: `${color}.solid`,
75
+ }));
76
+
77
+ return {
78
+ data,
79
+ series,
80
+ };
81
+ };
@@ -0,0 +1,21 @@
1
+ import { IconButton } from '@chakra-ui/react';
2
+ import { TbArrowsLeftRight } from 'react-icons/tb';
3
+ import { useTestsCompare } from '@store';
4
+ import { useTestContext } from '../context';
5
+
6
+ export const TestCardCompareButton = () => {
7
+ const { testResult } = useTestContext();
8
+ const { checkIsCompared, onChangeCompare } = useTestsCompare();
9
+ const isCompared = checkIsCompared(testResult.test.name);
10
+
11
+ return (
12
+ <IconButton
13
+ size="xs"
14
+ colorPalette="teal"
15
+ variant={isCompared ? 'solid' : 'outline'}
16
+ onClick={() => onChangeCompare(testResult.test.name)}
17
+ >
18
+ <TbArrowsLeftRight />
19
+ </IconButton>
20
+ );
21
+ };
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+
3
+ import { CodeBlock, createShikiAdapter, IconButton } from '@chakra-ui/react';
4
+ import type { HighlighterGeneric } from 'shiki';
5
+ import { useMemo } from 'react';
6
+
7
+ interface JsonCodeBlockProps {
8
+ tab: string;
9
+ code: string;
10
+ }
11
+
12
+ const JsonCodeBlock = ({ code, tab }: JsonCodeBlockProps) => {
13
+ const adapter = useMemo(() => {
14
+ return createShikiAdapter<HighlighterGeneric<any, any>>({
15
+ async load() {
16
+ const { createHighlighter } = await import('shiki');
17
+ return createHighlighter({
18
+ langs: ['json'],
19
+ themes: ['github-dark'],
20
+ });
21
+ },
22
+ });
23
+ }, []);
24
+
25
+ return (
26
+ <CodeBlock.AdapterProvider value={adapter}>
27
+ <CodeBlock.Root code={code} language={'json'}>
28
+ <CodeBlock.Header>
29
+ <CodeBlock.Title>{tab}</CodeBlock.Title>
30
+ <CodeBlock.CopyTrigger asChild>
31
+ <IconButton variant="ghost" size="2xs">
32
+ <CodeBlock.CopyIndicator />
33
+ </IconButton>
34
+ </CodeBlock.CopyTrigger>
35
+ </CodeBlock.Header>
36
+ <CodeBlock.Content>
37
+ <CodeBlock.Code>
38
+ <CodeBlock.CodeText />
39
+ </CodeBlock.Code>
40
+ </CodeBlock.Content>
41
+ </CodeBlock.Root>
42
+ </CodeBlock.AdapterProvider>
43
+ );
44
+ };
45
+
46
+ export default JsonCodeBlock;
@@ -0,0 +1,94 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import _ from 'lodash';
5
+ import {
6
+ IconButton,
7
+ CloseButton,
8
+ Drawer,
9
+ Portal,
10
+ Tabs,
11
+ } from '@chakra-ui/react';
12
+ import dynamic from 'next/dynamic';
13
+ import { FiSettings } from 'react-icons/fi';
14
+ import { useTestContext } from '../context';
15
+
16
+ const JsonCodeBlock = dynamic(() => import('./JsonCodeBlock'), { ssr: false });
17
+
18
+ type TabType = 'test' | 'bot';
19
+
20
+ export const TestCardConfigDrawer = () => {
21
+ const [open, setOpen] = useState(false);
22
+ const [tab, setTab] = useState<TabType>('test');
23
+ const { testResult } = useTestContext();
24
+
25
+ const getTestConf = () => JSON.stringify(testResult.test, null, 2);
26
+ const getBotConf = () =>
27
+ JSON.stringify(
28
+ _.omit(
29
+ { ...testResult.test, disabled: false },
30
+ 'name',
31
+ 'testSuiteId',
32
+ 'options',
33
+ ),
34
+ null,
35
+ 2,
36
+ );
37
+
38
+ return (
39
+ <Tabs.Root
40
+ value={tab}
41
+ onValueChange={(e) => setTab(e.value as TabType)}
42
+ variant={'line'}
43
+ >
44
+ <Drawer.Root
45
+ open={open}
46
+ onOpenChange={(e) => setOpen(e.open)}
47
+ size={'lg'}
48
+ >
49
+ <Drawer.Trigger asChild>
50
+ <IconButton
51
+ colorPalette="teal"
52
+ size="xs"
53
+ variant={open ? 'surface' : 'outline'}
54
+ >
55
+ <FiSettings />
56
+ </IconButton>
57
+ </Drawer.Trigger>
58
+ <Portal>
59
+ <Drawer.Backdrop />
60
+ <Drawer.Positioner>
61
+ <Drawer.Content display="flex" flexDirection="column">
62
+ <Drawer.Header>
63
+ <Drawer.Title>
64
+ Configuration
65
+ <Tabs.List mt={2}>
66
+ <Tabs.Trigger colorPalette={'teal'} value="test">
67
+ Test
68
+ </Tabs.Trigger>
69
+ <Tabs.Trigger colorPalette={'teal'} value="bot">
70
+ Bot
71
+ </Tabs.Trigger>
72
+ </Tabs.List>
73
+ </Drawer.Title>
74
+
75
+ <Drawer.CloseTrigger asChild>
76
+ <CloseButton position="absolute" right="3" top="3" />
77
+ </Drawer.CloseTrigger>
78
+ </Drawer.Header>
79
+
80
+ <Drawer.Body overflowY="auto" flex="1">
81
+ <Tabs.Content value="test">
82
+ <JsonCodeBlock tab={tab} code={getTestConf()} />
83
+ </Tabs.Content>
84
+ <Tabs.Content value="bot">
85
+ <JsonCodeBlock tab={tab} code={getBotConf()} />
86
+ </Tabs.Content>
87
+ </Drawer.Body>
88
+ </Drawer.Content>
89
+ </Drawer.Positioner>
90
+ </Portal>
91
+ </Drawer.Root>
92
+ </Tabs.Root>
93
+ );
94
+ };
@@ -0,0 +1,128 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import {
5
+ Button,
6
+ CloseButton,
7
+ Dialog,
8
+ IconButton,
9
+ Portal,
10
+ Text,
11
+ } from '@chakra-ui/react';
12
+ import { FiTrash2 } from 'react-icons/fi';
13
+ import { deleteBacktest } from '@actions/backtest';
14
+ import { useBacktestMutations } from '@store';
15
+ import { toaster } from '@UI';
16
+ import { useTestContext } from '../context';
17
+
18
+ export const TestCardDeleteButton = () => {
19
+ const [open, setOpen] = useState(false);
20
+ const [isDeleting, setIsDeleting] = useState(false);
21
+ const [error, setError] = useState<string | null>(null);
22
+ const { removeBacktestTest } = useBacktestMutations();
23
+ const { testResult } = useTestContext();
24
+
25
+ const handleDelete = async () => {
26
+ if (isDeleting) {
27
+ return;
28
+ }
29
+
30
+ setIsDeleting(true);
31
+ setError(null);
32
+
33
+ try {
34
+ const isDeleted = await deleteBacktest(
35
+ testResult.test.name,
36
+ testResult.test.strategyName,
37
+ );
38
+
39
+ if (!isDeleted) {
40
+ setError('Failed to delete backtest.');
41
+ toaster.error({
42
+ title: 'Delete failed',
43
+ description: 'Backtest was not deleted.',
44
+ });
45
+ return;
46
+ }
47
+
48
+ await removeBacktestTest(testResult.test.name);
49
+ setOpen(false);
50
+ toaster.success({
51
+ title: 'Backtest deleted',
52
+ description: testResult.test.name,
53
+ });
54
+ } catch {
55
+ setError('Failed to delete backtest.');
56
+ toaster.error({
57
+ title: 'Delete failed',
58
+ description: 'Unexpected error while deleting backtest.',
59
+ });
60
+ } finally {
61
+ setIsDeleting(false);
62
+ }
63
+ };
64
+
65
+ return (
66
+ <Dialog.Root
67
+ open={open}
68
+ onOpenChange={(e) => {
69
+ setOpen(e.open);
70
+ if (!e.open) {
71
+ setError(null);
72
+ }
73
+ }}
74
+ >
75
+ <Dialog.Trigger asChild>
76
+ <IconButton
77
+ size="xs"
78
+ colorPalette="red"
79
+ variant="outline"
80
+ aria-label="Delete backtest"
81
+ >
82
+ <FiTrash2 />
83
+ </IconButton>
84
+ </Dialog.Trigger>
85
+ <Portal>
86
+ <Dialog.Backdrop />
87
+ <Dialog.Positioner>
88
+ <Dialog.Content>
89
+ <Dialog.Header>
90
+ <Dialog.Title>Delete test</Dialog.Title>
91
+ <Dialog.CloseTrigger asChild>
92
+ <CloseButton position="absolute" right="3" top="3" />
93
+ </Dialog.CloseTrigger>
94
+ </Dialog.Header>
95
+ <Dialog.Body>
96
+ <Text fontSize="sm" color="gray.200">
97
+ Delete backtest <b>{testResult.test.name}</b>?
98
+ </Text>
99
+ <Text fontSize="sm" color="gray.400" mt={2}>
100
+ This action cannot be undone.
101
+ </Text>
102
+ {error ? (
103
+ <Text fontSize="sm" color="red.400" mt={3}>
104
+ {error}
105
+ </Text>
106
+ ) : null}
107
+ </Dialog.Body>
108
+ <Dialog.Footer>
109
+ <Dialog.ActionTrigger asChild>
110
+ <Button variant="outline" size="sm" disabled={isDeleting}>
111
+ Cancel
112
+ </Button>
113
+ </Dialog.ActionTrigger>
114
+ <Button
115
+ colorPalette="red"
116
+ size="sm"
117
+ onClick={handleDelete}
118
+ loading={isDeleting}
119
+ >
120
+ Delete
121
+ </Button>
122
+ </Dialog.Footer>
123
+ </Dialog.Content>
124
+ </Dialog.Positioner>
125
+ </Portal>
126
+ </Dialog.Root>
127
+ );
128
+ };
@@ -0,0 +1,18 @@
1
+ import { FavoriteButton } from '@shared/FavoriteButton';
2
+ import { useFavoriteTests } from '@store';
3
+ import { useTestContext } from '../context';
4
+
5
+ export const TestCardFavoriteIndicator = () => {
6
+ const { testResult } = useTestContext();
7
+ const { checkIsFavorite, toggleFavorite } = useFavoriteTests();
8
+ const isFavorite = checkIsFavorite(testResult.test.name);
9
+
10
+ return (
11
+ <FavoriteButton
12
+ isFavorite={isFavorite}
13
+ onChangeFavorite={() =>
14
+ toggleFavorite(testResult.test.name, testResult.stat.netProfit)
15
+ }
16
+ />
17
+ );
18
+ };
@@ -0,0 +1,40 @@
1
+ import { IconButton } from '@chakra-ui/react';
2
+ import { FiBarChart2 } from 'react-icons/fi';
3
+ import { useTestContext } from '../context';
4
+
5
+ const connectorNameToProvider: Record<
6
+ string,
7
+ 'bybit' | 'binance' | 'coinbase'
8
+ > = {
9
+ bybit: 'bybit',
10
+ binance: 'binance',
11
+ coinbase: 'coinbase',
12
+ };
13
+
14
+ export const TestCardOpenDashboardButton = () => {
15
+ const { testResult } = useTestContext();
16
+ const provider =
17
+ connectorNameToProvider[testResult.test.connectorName.toLowerCase()] ||
18
+ 'bybit';
19
+ const params = new URLSearchParams();
20
+
21
+ params.set('backtestId', testResult.test.name);
22
+ params.set('backtestStrategy', testResult.test.strategyName);
23
+
24
+ return (
25
+ <IconButton
26
+ size="xs"
27
+ colorPalette="teal"
28
+ variant="outline"
29
+ onClick={() =>
30
+ window.open(
31
+ `/routes/dashboard/${provider}/${testResult.test.symbol}/15?${params.toString()}`,
32
+ '_blank',
33
+ 'noopener,noreferrer',
34
+ )
35
+ }
36
+ >
37
+ <FiBarChart2 />
38
+ </IconButton>
39
+ );
40
+ };
@@ -0,0 +1,24 @@
1
+ import { IconButton } from '@chakra-ui/react';
2
+ import { FiExternalLink } from 'react-icons/fi';
3
+ import { useTestContext } from '../context';
4
+
5
+ export const TestCardOpenReportButton = () => {
6
+ const { testResult } = useTestContext();
7
+
8
+ return (
9
+ <IconButton
10
+ size="xs"
11
+ colorPalette="teal"
12
+ variant="outline"
13
+ onClick={() =>
14
+ window.open(
15
+ `/routes/backtest/${testResult.test.name}`,
16
+ '_blank',
17
+ 'noopener,noreferrer',
18
+ )
19
+ }
20
+ >
21
+ <FiExternalLink />
22
+ </IconButton>
23
+ );
24
+ };
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { PropsWithChildren } from 'react';
4
+ import _ from 'lodash';
5
+ import { Box } from '@chakra-ui/react';
6
+ import { TestResultContext } from '../context';
7
+ import { TestCardSkeleton } from '../Skeleton';
8
+ import { useTest, useFavoriteTests } from '@store';
9
+
10
+ interface TestRootProps {
11
+ testName: string;
12
+ noWrapper?: boolean;
13
+ }
14
+
15
+ export const TestCardRoot = ({
16
+ testName,
17
+ noWrapper,
18
+ children,
19
+ }: PropsWithChildren<TestRootProps>) => {
20
+ const testResult = useTest(testName);
21
+ const { checkIsFavorite } = useFavoriteTests();
22
+
23
+ if (_.isEmpty(testResult)) {
24
+ return <TestCardSkeleton />;
25
+ }
26
+
27
+ const isFavorite = checkIsFavorite(testResult.test.name);
28
+
29
+ const getBorderColor = () => {
30
+ if (noWrapper) {
31
+ return 'transparent';
32
+ }
33
+ if (isFavorite) {
34
+ return 'yellow.800';
35
+ }
36
+ return 'gray.800';
37
+ };
38
+
39
+ return (
40
+ <TestResultContext.Provider value={{ testResult: testResult }}>
41
+ <Box
42
+ p={2}
43
+ mb={4}
44
+ maxW="1400px"
45
+ borderRadius="md"
46
+ shadow={noWrapper ? undefined : 'sm'}
47
+ borderWidth="1px"
48
+ borderColor={getBorderColor()}
49
+ overflowX="auto"
50
+ >
51
+ {children}
52
+ </Box>
53
+ </TestResultContext.Provider>
54
+ );
55
+ };
@@ -0,0 +1,21 @@
1
+ import { Box, SkeletonText, Skeleton, Stack } from '@chakra-ui/react';
2
+
3
+ export const TestCardSkeleton = () => (
4
+ <Box
5
+ p={2}
6
+ mb={4}
7
+ width="1400px"
8
+ height="628px"
9
+ bg="gray.900"
10
+ borderRadius="md"
11
+ shadow="sm"
12
+ borderWidth="1px"
13
+ overflowX="auto"
14
+ >
15
+ <Stack gap="6">
16
+ <SkeletonText noOfLines={2} gap="6" />
17
+ <Skeleton height="400px" />
18
+ <SkeletonText noOfLines={3} gap="6" />
19
+ </Stack>
20
+ </Box>
21
+ );