@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,119 @@
1
+ 'use client';
2
+
3
+ import { SimpleGrid, Stat, Card, Heading } from '@chakra-ui/react';
4
+ import { getFormatted } from '@tradejs/core/backtest';
5
+ import { useTestContext } from '../context';
6
+ import { ThresholdLevel, TestThresholdsKey } from '@tradejs/types';
7
+
8
+ const getColorByLevel = (level: ThresholdLevel) => {
9
+ switch (level) {
10
+ case 'success':
11
+ return 'teal.500';
12
+ case 'warning':
13
+ return 'fg.warning';
14
+ case 'error':
15
+ default:
16
+ return 'fg.error';
17
+ }
18
+ };
19
+
20
+ interface StatItemProps {
21
+ id: TestThresholdsKey;
22
+ size?: 'sm' | 'md' | 'lg';
23
+ title: string;
24
+ }
25
+
26
+ const StatItem = ({ id, title, size = 'md' }: StatItemProps) => {
27
+ const {
28
+ testResult: { stat },
29
+ } = useTestContext();
30
+
31
+ const { formatted, level } = getFormatted(stat, id);
32
+
33
+ return (
34
+ <Stat.Root size={size}>
35
+ <Stat.Label>{title}</Stat.Label>
36
+ <Stat.ValueText color={getColorByLevel(level)}>
37
+ {formatted}
38
+ </Stat.ValueText>
39
+ </Stat.Root>
40
+ );
41
+ };
42
+
43
+ export const TestCardStatLine = () => {
44
+ return (
45
+ <SimpleGrid columns={{ base: 4, md: 8 }} p={4}>
46
+ <StatItem id="netProfit" title="P&L" />
47
+ <StatItem id="minAmount" title="Min Amount" />
48
+ <StatItem id="maxDrawdown" title="Drawdown" />
49
+ <StatItem id="orders" title="Orders" />
50
+ <StatItem id="winRate" title="Win Rate" />
51
+ <StatItem id="riskRewardRatio" title="Risk Ratio" />
52
+ <StatItem id="sharpeRatio" title="Sharpe" />
53
+ <StatItem id="exposure" title="Exposure" />
54
+ </SimpleGrid>
55
+ );
56
+ };
57
+
58
+ export const TestCardStatTable = () => {
59
+ return (
60
+ <>
61
+ <Card.Root bg="gray.900" mb={4} size="sm">
62
+ <Card.Header>
63
+ <Heading size="md">Period & Activity</Heading>
64
+ </Card.Header>
65
+ <Card.Body>
66
+ <SimpleGrid columns={{ base: 2, md: 5 }} p={4}>
67
+ <StatItem id="periodDays" title="Days" />
68
+ <StatItem id="periodMonths" title="Months" />
69
+ <StatItem id="orders" title="Orders" />
70
+ <StatItem id="ordersPerMonth" title="Orders / month" />
71
+ <StatItem id="exposure" title="Exposure" />
72
+ </SimpleGrid>
73
+ </Card.Body>
74
+ </Card.Root>
75
+
76
+ <Card.Root bg="gray.900" mb={4} size="sm">
77
+ <Card.Header>
78
+ <Heading size="md">Performance</Heading>
79
+ </Card.Header>
80
+ <Card.Body>
81
+ <SimpleGrid columns={{ base: 2, md: 5 }} p={4}>
82
+ <StatItem id="amount" title="Final amount" />
83
+ <StatItem id="netProfit" title="Net P&L" />
84
+ <StatItem id="totalReturn" title="Total return" />
85
+ <StatItem id="cagr" title="CAGR" />
86
+ </SimpleGrid>
87
+ </Card.Body>
88
+ </Card.Root>
89
+
90
+ <Card.Root bg="gray.900" mb={4} size="sm">
91
+ <Card.Header>
92
+ <Heading size="md">Risk & Risk-Adjusted</Heading>
93
+ </Card.Header>
94
+ <Card.Body>
95
+ <SimpleGrid columns={{ base: 2, md: 5 }} p={4}>
96
+ <StatItem id="maxDrawdown" title="Max drawdown" />
97
+ <StatItem id="calmar" title="Calmar" />
98
+ <StatItem id="sharpeRatio" title="Sharpe" />
99
+ </SimpleGrid>
100
+ </Card.Body>
101
+ </Card.Root>
102
+
103
+ <Card.Root bg="gray.900" mb={4} size="sm">
104
+ <Card.Header>
105
+ <Heading size="md">Trade Quality & Consistency</Heading>
106
+ </Card.Header>
107
+ <Card.Body>
108
+ <SimpleGrid columns={{ base: 2, md: 5 }} p={4}>
109
+ <StatItem id="winRate" title="Win rate" />
110
+ <StatItem id="riskRewardRatio" title="R/R (payoff)" />
111
+ <StatItem id="expectancy" title="Expectancy / trade" />
112
+ <StatItem id="maxConsecutiveWins" title="Win streak" />
113
+ <StatItem id="maxConsecutiveLosses" title="Loss streak" />
114
+ </SimpleGrid>
115
+ </Card.Body>
116
+ </Card.Root>
117
+ </>
118
+ );
119
+ };
@@ -0,0 +1,84 @@
1
+ 'use client';
2
+
3
+ import { PropsWithChildren, ReactNode } from 'react';
4
+ import { Text, Flex } from '@chakra-ui/react';
5
+ import { getBacktestScore } from '@tradejs/core/backtest';
6
+ import { useTestContext } from '../context';
7
+
8
+ interface TestCardTitleProps {
9
+ leftSlot?: ReactNode;
10
+ }
11
+
12
+ export const TestCardTitle = ({
13
+ children,
14
+ leftSlot,
15
+ }: PropsWithChildren<TestCardTitleProps>) => {
16
+ const {
17
+ testResult: { stat, test },
18
+ } = useTestContext();
19
+ const score = getBacktestScore(stat);
20
+
21
+ return (
22
+ <Flex gap="4" p={4} mb={3}>
23
+ {leftSlot ? <Flex alignItems="center">{leftSlot}</Flex> : null}
24
+
25
+ <Text fontSize="lg" fontWeight="bold" color={'gray.200'}>
26
+ {test.symbol}
27
+ </Text>
28
+
29
+ <Flex gap="1">
30
+ <Text fontSize="sm" fontWeight="bold" color={'gray.400'} mt={1}>
31
+ score:
32
+ </Text>
33
+
34
+ <Text fontSize="lg" fontWeight="bold" color={'teal.500'}>
35
+ {score}
36
+ </Text>
37
+ </Flex>
38
+
39
+ <Flex gap="1">
40
+ <Text fontSize="sm" fontWeight="bold" color={'gray.400'} mt={1}>
41
+ strategy:
42
+ </Text>
43
+
44
+ <Text fontSize="lg" fontWeight="bold" color={'gray.200'}>
45
+ {test.strategyName}
46
+ </Text>
47
+ </Flex>
48
+
49
+ <Flex gap="1">
50
+ <Text fontSize="sm" fontWeight="bold" color={'gray.400'} mt={1}>
51
+ connector:
52
+ </Text>
53
+
54
+ <Text fontSize="lg" fontWeight="bold" color={'gray.200'}>
55
+ {test.connectorName}
56
+ </Text>
57
+ </Flex>
58
+
59
+ <Flex gap="1">
60
+ <Text fontSize="sm" fontWeight="bold" color={'gray.400'} mt={1}>
61
+ suite Id:
62
+ </Text>
63
+
64
+ <Text fontSize="lg" fontWeight="bold" color={'gray.200'}>
65
+ {test.testSuiteId}
66
+ </Text>
67
+ </Flex>
68
+
69
+ <Flex gap="1">
70
+ <Text fontSize="sm" fontWeight="bold" color={'gray.400'} mt={1}>
71
+ test Id:
72
+ </Text>
73
+
74
+ <Text fontSize="lg" fontWeight="bold" color={'gray.200'}>
75
+ {test.testId}
76
+ </Text>
77
+ </Flex>
78
+
79
+ <Flex gap="2" ml={'auto'} mt={-1}>
80
+ {children}
81
+ </Flex>
82
+ </Flex>
83
+ );
84
+ };
@@ -0,0 +1,14 @@
1
+ import { createContext, useContext } from 'react';
2
+ import { TestResult } from '@tradejs/types';
3
+
4
+ interface TestResultContextProps {
5
+ testResult: TestResult;
6
+ }
7
+
8
+ export const TestResultContext = createContext<TestResultContextProps>(
9
+ {} as TestResultContextProps,
10
+ );
11
+
12
+ export const useTestContext = () => {
13
+ return useContext(TestResultContext);
14
+ };
@@ -0,0 +1,28 @@
1
+ 'use client';
2
+
3
+ import { TestCardChart } from './Chart';
4
+ import { TestCardRoot } from './Root';
5
+ import { TestCardStatLine, TestCardStatTable } from './Stat';
6
+ import { TestCardTitle } from './Title';
7
+ import { TestCardSkeleton } from './Skeleton';
8
+ import { TestCardConfigDrawer } from './ConfigDrawer';
9
+ import { TestCardCompareButton } from './CompareButton';
10
+ import { TestCardFavoriteIndicator } from './FavoriteIndicator';
11
+ import { TestCardOpenReportButton } from './OpenReportButton';
12
+ import { TestCardOpenDashboardButton } from './OpenDashboardButton';
13
+ import { TestCardDeleteButton } from './DeleteButton';
14
+
15
+ export const TestCard = {
16
+ Root: TestCardRoot,
17
+ Title: TestCardTitle,
18
+ StatLine: TestCardStatLine,
19
+ StatTable: TestCardStatTable,
20
+ Chart: TestCardChart,
21
+ Skeleton: TestCardSkeleton,
22
+ ConfigDrawer: TestCardConfigDrawer,
23
+ CompareButton: TestCardCompareButton,
24
+ FavoriteIndicator: TestCardFavoriteIndicator,
25
+ OpenDashboardButton: TestCardOpenDashboardButton,
26
+ OpenReportButton: TestCardOpenReportButton,
27
+ DeleteButton: TestCardDeleteButton,
28
+ };
@@ -0,0 +1,124 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useMemo } from 'react';
4
+ import AutoSizer from 'react-virtualized-auto-sizer';
5
+ import { FixedSizeList, ListChildComponentProps } from 'react-window';
6
+ import { FiFolder } from 'react-icons/fi';
7
+ import { Box, Checkbox, Code } from '@chakra-ui/react';
8
+ import { TestCard } from '@components/Backtest/TestCard';
9
+ import { EmptyState } from '@UI';
10
+ import { Items } from '@tradejs/types';
11
+
12
+ interface ListProps {
13
+ tests: Items;
14
+ loadding: boolean;
15
+ fulFilled: boolean;
16
+ noData: boolean;
17
+ selectedTestNames: string[];
18
+ onToggleSelection: (testName: string, checked: boolean) => void;
19
+ overscan?: number;
20
+ }
21
+
22
+ const ITEM_HEIGHT = 548;
23
+
24
+ export const TestList = ({
25
+ tests,
26
+ loadding,
27
+ fulFilled,
28
+ noData,
29
+ selectedTestNames,
30
+ onToggleSelection,
31
+ overscan = 2,
32
+ }: ListProps) => {
33
+ const selectedTestSet = useMemo(
34
+ () => new Set(selectedTestNames),
35
+ [selectedTestNames],
36
+ );
37
+
38
+ const itemKey = useCallback(
39
+ (index: number) => tests[index]?.value ?? String(index),
40
+ [tests],
41
+ );
42
+
43
+ const Row = useCallback(
44
+ ({ index, style }: ListChildComponentProps) => {
45
+ const item = tests[index];
46
+ return (
47
+ <Box style={style} px={2}>
48
+ <TestCard.Root key={item.value} testName={item.value}>
49
+ <TestCard.Title
50
+ leftSlot={
51
+ <Checkbox.Root
52
+ size="sm"
53
+ colorPalette="teal"
54
+ checked={selectedTestSet.has(item.value)}
55
+ onCheckedChange={(details) =>
56
+ onToggleSelection(item.value, details.checked === true)
57
+ }
58
+ >
59
+ <Checkbox.HiddenInput />
60
+ <Checkbox.Control bg="gray.800" borderColor="gray.500" />
61
+ </Checkbox.Root>
62
+ }
63
+ >
64
+ <TestCard.CompareButton />
65
+ <TestCard.FavoriteIndicator />
66
+ <TestCard.ConfigDrawer />
67
+ <TestCard.OpenDashboardButton />
68
+ <TestCard.OpenReportButton />
69
+ <TestCard.DeleteButton />
70
+ </TestCard.Title>
71
+ <TestCard.Chart />
72
+ <TestCard.StatLine />
73
+ </TestCard.Root>
74
+ </Box>
75
+ );
76
+ },
77
+ [onToggleSelection, selectedTestSet, tests],
78
+ );
79
+
80
+ if (!fulFilled && loadding) {
81
+ return (
82
+ <>
83
+ <TestCard.Skeleton />
84
+ <TestCard.Skeleton />
85
+ </>
86
+ );
87
+ }
88
+
89
+ if (fulFilled && !loadding && noData) {
90
+ return (
91
+ <EmptyState
92
+ icon={FiFolder}
93
+ title="No tests found"
94
+ description={
95
+ <>
96
+ Please run
97
+ <Code ml={1} colorPalette="teal" variant="subtle">
98
+ yarn backtest --help
99
+ </Code>
100
+ </>
101
+ }
102
+ />
103
+ );
104
+ }
105
+
106
+ return (
107
+ <>
108
+ <AutoSizer>
109
+ {({ height: h, width }) => (
110
+ <FixedSizeList
111
+ height={h}
112
+ width={width}
113
+ itemCount={tests.length}
114
+ itemSize={ITEM_HEIGHT}
115
+ overscanCount={overscan}
116
+ itemKey={itemKey}
117
+ >
118
+ {Row}
119
+ </FixedSizeList>
120
+ )}
121
+ </AutoSizer>
122
+ </>
123
+ );
124
+ };
@@ -0,0 +1,34 @@
1
+ import { Timeline, Box, Text } from '@chakra-ui/react';
2
+ import { PiRobotFill, PiUserFill } from 'react-icons/pi';
3
+ import { AIChatMessage } from '@tradejs/types';
4
+
5
+ interface MessageProps {
6
+ message: AIChatMessage;
7
+ index: number;
8
+ }
9
+
10
+ export const Message = ({ message, index }: MessageProps) => {
11
+ return (
12
+ <Timeline.Item>
13
+ <Timeline.Connector>
14
+ <Timeline.Separator />
15
+ <Timeline.Indicator>
16
+ {message.from === 'user' ? <PiUserFill /> : <PiRobotFill />}
17
+ </Timeline.Indicator>
18
+ </Timeline.Connector>
19
+
20
+ <Timeline.Content>
21
+ <Timeline.Title fontSize="sm" fontWeight="bold">
22
+ {message.from === 'user' ? 'User' : 'AI'}
23
+ </Timeline.Title>
24
+ <Timeline.Description fontSize="xs" color="gray.500">
25
+ {/* В будущем сюда можно вставить timestamp */}
26
+ Message #{index + 1}
27
+ </Timeline.Description>
28
+ <Box mt={1} py={2} borderRadius="lg" w="fit-content" maxW="full">
29
+ <Text fontSize="sm">{message.text}</Text>
30
+ </Box>
31
+ </Timeline.Content>
32
+ </Timeline.Item>
33
+ );
34
+ };
@@ -0,0 +1,163 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import {
5
+ Button,
6
+ CloseButton,
7
+ Drawer,
8
+ Portal,
9
+ Icon,
10
+ Textarea,
11
+ Timeline,
12
+ HStack,
13
+ Stack,
14
+ Text,
15
+ SkeletonCircle,
16
+ SkeletonText,
17
+ } from '@chakra-ui/react';
18
+ import { AIChatMessage, AIChatHistory } from '@tradejs/types';
19
+ import { GiArtificialHive } from 'react-icons/gi';
20
+ import { useFilters } from '@store';
21
+ import { sendMessage, getHistory } from '@actions/ai';
22
+ import { Message } from './Message';
23
+
24
+ export const AiDrawer = () => {
25
+ const [open, setOpen] = useState(false);
26
+ const [loading, setLoading] = useState(true);
27
+ const [input, setInput] = useState('');
28
+ const [messages, setMessages] = useState<AIChatHistory>([]);
29
+ const { filters } = useFilters();
30
+
31
+ const loadHistory = useCallback(async () => {
32
+ setLoading(true);
33
+
34
+ const history = await getHistory(filters.symbol);
35
+
36
+ setMessages(history);
37
+
38
+ setLoading(false);
39
+ }, [filters.symbol]);
40
+
41
+ useEffect(() => {
42
+ void loadHistory();
43
+ }, [loadHistory]);
44
+
45
+ const handleSend = async () => {
46
+ if (!input.trim()) return;
47
+
48
+ const message = {
49
+ from: 'user',
50
+ text: input,
51
+ command: 'prompt',
52
+ } as AIChatMessage;
53
+
54
+ setMessages((state) => [...state, message]);
55
+
56
+ const response = await sendMessage({ message, filters });
57
+
58
+ setMessages((state) => [...state, response]);
59
+
60
+ setInput('');
61
+ };
62
+
63
+ const handleQuick = async (command: string) => {
64
+ let message: AIChatMessage | null = null;
65
+
66
+ if (command === '/line') {
67
+ message = {
68
+ from: 'user',
69
+ text: 'Какие наклонные линии можно построить на данном графике',
70
+ command,
71
+ };
72
+ }
73
+
74
+ if (!message) {
75
+ return;
76
+ }
77
+
78
+ setMessages((state) => [...state, message as AIChatMessage]);
79
+
80
+ const response = await sendMessage({ message, filters });
81
+
82
+ setMessages((state) => [...state, response]);
83
+ };
84
+
85
+ return (
86
+ <Drawer.Root open={open} onOpenChange={(e) => setOpen(e.open)} size={'lg'}>
87
+ <Drawer.Trigger asChild>
88
+ <Button
89
+ colorPalette="teal"
90
+ size="sm"
91
+ variant={open ? 'surface' : 'outline'}
92
+ >
93
+ <Icon as={GiArtificialHive} boxSize={6} />
94
+ <Text>AI</Text>
95
+ </Button>
96
+ </Drawer.Trigger>
97
+ <Portal>
98
+ <Drawer.Backdrop />
99
+ <Drawer.Positioner>
100
+ <Drawer.Content display="flex" flexDirection="column">
101
+ <Drawer.Header>
102
+ <Drawer.Title>AI assistant</Drawer.Title>
103
+ <Drawer.CloseTrigger asChild>
104
+ <CloseButton position="absolute" right="3" top="3" />
105
+ </Drawer.CloseTrigger>
106
+ </Drawer.Header>
107
+
108
+ <Drawer.Body overflowY="auto" flex="1">
109
+ {loading ? (
110
+ <Stack gap="4" maxW="xs">
111
+ <HStack width="full">
112
+ <SkeletonCircle size="6" />
113
+ <SkeletonText noOfLines={2} />
114
+ </HStack>
115
+ <SkeletonText ml="8" noOfLines={3} />
116
+ </Stack>
117
+ ) : (
118
+ <Timeline.Root>
119
+ {messages.map((message, index) => (
120
+ <Message key={index} message={message} index={index} />
121
+ ))}
122
+ </Timeline.Root>
123
+ )}
124
+ </Drawer.Body>
125
+
126
+ <Drawer.Footer flexDirection="column" alignItems="stretch" gap={3}>
127
+ <HStack>
128
+ <Button
129
+ size="sm"
130
+ variant="outline"
131
+ onClick={() => handleQuick('/line')}
132
+ >
133
+ /line
134
+ </Button>
135
+ <Button
136
+ size="sm"
137
+ variant="outline"
138
+ onClick={() => handleQuick('/analyze')}
139
+ >
140
+ /analyze
141
+ </Button>
142
+ </HStack>
143
+
144
+ <Textarea
145
+ size="sm"
146
+ placeholder="Введите сообщение..."
147
+ autoresize
148
+ rows={3}
149
+ maxH="15lh"
150
+ value={input}
151
+ onChange={(e) => setInput(e.target.value)}
152
+ />
153
+
154
+ <Button mt={2} size={'sm'} variant="subtle" onClick={handleSend}>
155
+ Send
156
+ </Button>
157
+ </Drawer.Footer>
158
+ </Drawer.Content>
159
+ </Drawer.Positioner>
160
+ </Portal>
161
+ </Drawer.Root>
162
+ );
163
+ };
@@ -0,0 +1,7 @@
1
+ export type {
2
+ EntryLineExtendData,
3
+ EntryPointsExtendData,
4
+ EntryZoneExtendData,
5
+ MarkerMeta,
6
+ MarkerShape,
7
+ } from '@tradejs/core/figures';
@@ -0,0 +1,76 @@
1
+ import { MarkerMeta } from './backtestFigureTypes';
2
+
3
+ type Coordinate = { x: number; y: number };
4
+
5
+ type CreatePointFiguresParams = {
6
+ coordinates: Coordinate[];
7
+ overlay: { extendData?: MarkerMeta[][] };
8
+ };
9
+
10
+ const green = '#84cc16';
11
+ const red = '#dc2626';
12
+
13
+ export const createBacktestMarkersPointFigure = ({
14
+ coordinates,
15
+ overlay,
16
+ }: CreatePointFiguresParams) => {
17
+ const markerGroups = overlay.extendData ?? [];
18
+ const figures: any[] = [];
19
+
20
+ for (let coordIndex = 0; coordIndex < coordinates.length; coordIndex++) {
21
+ const coord = coordinates[coordIndex];
22
+ const group = markerGroups[coordIndex];
23
+ if (!coord || !group) continue;
24
+
25
+ group.forEach((meta, localIdx) => {
26
+ const { shape, color, type, profit } = meta;
27
+
28
+ const baseX = coord.x;
29
+ const baseY = coord.y - localIdx * 14;
30
+
31
+ const width = 10;
32
+ const height = 10;
33
+
34
+ let figureType: string;
35
+ switch (shape) {
36
+ case 'RECT':
37
+ figureType = 'btRect';
38
+ break;
39
+ case 'SQUARE':
40
+ figureType = 'btSquare';
41
+ break;
42
+ case 'DIAMOND':
43
+ figureType = 'btDiamond';
44
+ break;
45
+ case 'TRIANGLE':
46
+ figureType = 'btTriangle';
47
+ break;
48
+ case 'STAR':
49
+ figureType = 'btStar';
50
+ break;
51
+ case 'CIRCLE':
52
+ default:
53
+ figureType = 'btCircle';
54
+ break;
55
+ }
56
+
57
+ figures.push({
58
+ type: figureType,
59
+ attrs: { x: baseX, y: baseY, width, height, color },
60
+ });
61
+
62
+ const labelText = `${type} ${profit.toFixed(2)}`;
63
+ figures.push({
64
+ type: 'btLabel',
65
+ attrs: {
66
+ x: baseX + 8,
67
+ y: baseY,
68
+ text: labelText,
69
+ color: profit >= 0 ? green : red,
70
+ },
71
+ });
72
+ });
73
+ }
74
+
75
+ return figures;
76
+ };
@@ -0,0 +1,15 @@
1
+ import { Figure } from '@tradejs/types';
2
+
3
+ export const circle = ({ ctx, x, y, width, height, color }: Figure) => {
4
+ const radius = Math.min(width, height) / 2;
5
+ ctx.beginPath();
6
+ ctx.arc(x + radius, y + radius, radius, 0, 2 * Math.PI);
7
+ ctx.fillStyle = color;
8
+ ctx.fill();
9
+
10
+ ctx.beginPath();
11
+ ctx.arc(x + radius, y + radius, radius, 0, 2 * Math.PI);
12
+ ctx.lineWidth = 1;
13
+ ctx.strokeStyle = 'white';
14
+ ctx.stroke();
15
+ };
@@ -0,0 +1,25 @@
1
+ import { Figure } from '@tradejs/types';
2
+
3
+ export const diamond = ({
4
+ ctx,
5
+ x,
6
+ y,
7
+ width: baseWidth,
8
+ height: baseHeight,
9
+ color,
10
+ }: Figure) => {
11
+ const width = baseWidth * 1.5;
12
+ const height = baseHeight * 1.5;
13
+ ctx.beginPath();
14
+ ctx.moveTo(x - width / 2, y);
15
+ ctx.lineTo(x, y - height / 2);
16
+ ctx.lineTo(x + width / 2, y);
17
+ ctx.lineTo(x, y + height / 2);
18
+ ctx.closePath();
19
+ ctx.fillStyle = color;
20
+ ctx.fill();
21
+
22
+ ctx.lineWidth = 1;
23
+ ctx.strokeStyle = 'white';
24
+ ctx.stroke();
25
+ };