@tradejs/app 1.0.9 → 1.0.10

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 (119) hide show
  1. package/README.md +7 -2
  2. package/bin/tradejs-app.mjs +146 -2
  3. package/next.config.mjs +0 -14
  4. package/package.json +35 -19
  5. package/src/app/actions/backtest.ts +54 -0
  6. package/src/app/actions/kline.ts +10 -5
  7. package/src/app/actions/scanner.ts +8 -3
  8. package/src/app/actions/strategies.ts +39 -0
  9. package/src/app/api/ai/route.ts +5 -4
  10. package/src/app/api/auth/[...nextauth]/route.ts +1 -1
  11. package/src/app/api/backtest/configs/route.ts +25 -0
  12. package/src/app/api/backtest/files/route.ts +5 -2
  13. package/src/app/api/backtest/order-log/[strategy]/[name]/route.ts +26 -2
  14. package/src/app/api/backtest/result/[strategy]/[name]/route.ts +27 -2
  15. package/src/app/api/backtest/runs/[jobId]/route.ts +124 -0
  16. package/src/app/api/backtest/runs/route.ts +45 -0
  17. package/src/app/api/backtest/test/[strategy]/[name]/route.ts +19 -2
  18. package/src/app/api/derivatives/summary/route.ts +8 -1
  19. package/src/app/api/kline/[provider]/[universe]/[symbol]/[interval]/route.ts +1 -0
  20. package/src/app/api/kline/{[provider]/[symbol]/[interval]/route.ts → handler.ts} +61 -39
  21. package/src/app/api/scanner/[provider]/[universe]/route.ts +1 -0
  22. package/src/app/api/scanner/[provider]/route.ts +30 -12
  23. package/src/app/api/scanner/route.ts +3 -3
  24. package/src/app/api/signal/[symbol]/[signalId]/route.ts +1 -1
  25. package/src/app/api/strategies/ai/[cardId]/route.ts +22 -0
  26. package/src/app/api/strategies/ai/route.ts +139 -0
  27. package/src/app/api/strategies/replay/[cardId]/route.ts +18 -0
  28. package/src/app/api/strategies/replay/route.ts +44 -0
  29. package/src/app/api/strategies/runtime/route.ts +831 -0
  30. package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +25 -0
  31. package/src/app/api/user/runtime-deployments/route.ts +94 -0
  32. package/src/app/api/user/runtime-strategy-configs/route.ts +241 -0
  33. package/src/app/api/user/settings/route.ts +18 -1
  34. package/src/app/api/user/trading-accounts/[accountId]/route.ts +35 -0
  35. package/src/app/api/user/trading-accounts/route.ts +134 -0
  36. package/src/app/components/Backtest/CompareList/index.tsx +1 -1
  37. package/src/app/components/Backtest/ResultsPageClient.tsx +376 -0
  38. package/src/app/components/Backtest/TestCard/ActionsMenu/index.tsx +123 -0
  39. package/src/app/components/Backtest/TestCard/Chart/index.tsx +1 -1
  40. package/src/app/components/Backtest/TestCard/CompareButton/index.tsx +1 -1
  41. package/src/app/components/Backtest/TestCard/ConfigDrawer/index.tsx +28 -12
  42. package/src/app/components/Backtest/TestCard/DeleteButton/index.tsx +33 -17
  43. package/src/app/components/Backtest/TestCard/FavoriteIndicator/index.tsx +2 -2
  44. package/src/app/components/Backtest/TestCard/OpenDashboardButton/index.tsx +10 -1
  45. package/src/app/components/Backtest/TestCard/OrdersDrawer/index.tsx +128 -0
  46. package/src/app/components/Backtest/TestCard/Root/index.tsx +1 -1
  47. package/src/app/components/Backtest/TestCard/Stat/index.tsx +2 -0
  48. package/src/app/components/Backtest/TestCard/StatDrawer/index.tsx +66 -0
  49. package/src/app/components/Backtest/TestCard/index.ts +6 -2
  50. package/src/app/components/Backtest/TestList/index.tsx +4 -9
  51. package/src/app/components/Dashboard/AiDrawer/index.tsx +1 -1
  52. package/src/app/components/Dashboard/KlineChart/hooks/useBacktest.ts +1 -1
  53. package/src/app/components/Dashboard/KlineChart/hooks/useBbIndicator.ts +10 -5
  54. package/src/app/components/Dashboard/KlineChart/hooks/useBtcCorrelation.ts +1 -1
  55. package/src/app/components/Dashboard/KlineChart/hooks/useBtcIndicator.ts +4 -1
  56. package/src/app/components/Dashboard/KlineChart/hooks/useEmaIndicator.ts +3 -1
  57. package/src/app/components/Dashboard/KlineChart/hooks/usePluginIndicators.ts +1 -1
  58. package/src/app/components/Dashboard/KlineChart/hooks/useSetup.ts +1 -1
  59. package/src/app/components/Dashboard/KlineChart/hooks/useSignal.ts +1 -1
  60. package/src/app/components/Dashboard/KlineChart/hooks/useSpreadIndicator.ts +1 -1
  61. package/src/app/components/Dashboard/KlineChart/hooks/useTrendLine.ts +1 -1
  62. package/src/app/components/Dashboard/KlineChart/hooks/useWmaIndicator.ts +3 -1
  63. package/src/app/components/Dashboard/KlineChart/index.tsx +16 -6
  64. package/src/app/components/Dashboard/MainChart/index.tsx +3 -22
  65. package/src/app/components/Shared/AppShell.tsx +1 -1
  66. package/src/app/components/Shared/BulkSelection/index.tsx +140 -0
  67. package/src/app/components/Shared/BulkSelection/useBulkSelection.ts +97 -0
  68. package/src/app/components/Shared/BulkSelection/utils.ts +126 -0
  69. package/src/app/components/Shared/Filters/Backtest/index.tsx +33 -48
  70. package/src/app/components/Shared/Filters/FavoriteIndicator/index.tsx +5 -3
  71. package/src/app/components/Shared/Filters/Indicators/index.tsx +2 -2
  72. package/src/app/components/Shared/Filters/Interval/index.tsx +1 -1
  73. package/src/app/components/Shared/Filters/Provider/index.tsx +8 -2
  74. package/src/app/components/Shared/Filters/Symbol/index.tsx +4 -3
  75. package/src/app/components/Shared/Filters/Universe/index.tsx +36 -0
  76. package/src/app/components/Shared/Filters/index.ts +2 -0
  77. package/src/app/components/Shared/OrdersDrawer.tsx +629 -0
  78. package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +55 -28
  79. package/src/app/components/Shared/Sidebar/TradingAccountsPanel.tsx +445 -0
  80. package/src/app/components/Shared/Sidebar/index.tsx +17 -3
  81. package/src/app/components/Strategies/AdvancedMetricsPanel.tsx +727 -0
  82. package/src/app/components/Strategies/RuntimeStrategyCard.tsx +2204 -0
  83. package/src/app/components/Strategies/RuntimeStrategyCardSkeleton.tsx +40 -0
  84. package/src/app/components/Strategies/RuntimeStrategyChart.tsx +121 -0
  85. package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +458 -0
  86. package/src/app/components/Strategies/StrategySnapshotCard.tsx +2890 -0
  87. package/src/app/components/Strategies/StrategySnapshotChart.tsx +94 -0
  88. package/src/app/components/Strategies/StrategySnapshotList.tsx +82 -0
  89. package/src/app/components/UI/Segment/index.tsx +8 -1
  90. package/src/app/components/UI/Select/index.tsx +24 -13
  91. package/src/app/components/UI/SelectWithSearch/index.tsx +46 -5
  92. package/src/app/layout.tsx +1 -1
  93. package/src/app/lib/backtestJobs.ts +792 -0
  94. package/src/app/lib/cardPageLayout.ts +1 -0
  95. package/src/app/lib/connectorCreator.ts +28 -0
  96. package/src/app/lib/currentUser.ts +1 -1
  97. package/src/app/lib/errorMessage.ts +34 -0
  98. package/src/app/lib/marketDefaults.ts +12 -0
  99. package/src/app/lib/marketRoutes.ts +58 -0
  100. package/src/app/lib/runtimeStrategies.ts +1376 -0
  101. package/src/app/lib/runtimeStrategyConfigForm.ts +86 -0
  102. package/src/app/provider.tsx +1 -1
  103. package/src/app/routes/backtest/page.tsx +1051 -304
  104. package/src/app/routes/dashboard/{[provider]/[symbol]/[interval]/page.tsx → Dashboard.tsx} +35 -15
  105. package/src/app/routes/dashboard/[provider]/[universe]/[symbol]/[interval]/page.tsx +1 -0
  106. package/src/app/routes/dashboard/page.tsx +15 -3
  107. package/src/app/routes/derivatives/page.tsx +1222 -165
  108. package/src/app/routes/strategies/StrategiesPageClient.tsx +593 -0
  109. package/src/app/routes/strategies/[mode]/page.tsx +20 -0
  110. package/src/app/routes/strategies/page.tsx +7 -0
  111. package/src/app/store/ai.ts +1 -1
  112. package/src/app/store/data.ts +73 -17
  113. package/src/app/store/filters.ts +1 -0
  114. package/src/app/store/marketKlineStream.ts +87 -0
  115. package/src/app/store/tests.ts +1 -1
  116. package/src/app/store/tickers.ts +59 -22
  117. package/tsconfig.json +17 -12
  118. package/src/app/components/Backtest/TestCard/OpenReportButton/index.tsx +0 -24
  119. package/src/app/routes/backtest/[test]/page.tsx +0 -33
@@ -32,7 +32,8 @@ import {
32
32
  hasPresetAiModelsForEndpoint,
33
33
  normalizeAiModel,
34
34
  } from '@tradejs/infra/aiModels';
35
- import { toaster } from '@UI';
35
+ import { toaster } from '#ui';
36
+ import { TradingAccountsPanel } from './TradingAccountsPanel';
36
37
 
37
38
  type SettingsResponse = {
38
39
  userName: string;
@@ -44,6 +45,9 @@ type SettingsResponse = {
44
45
  coinalyze: {
45
46
  apiKey: string;
46
47
  };
48
+ coinmarketcap: {
49
+ apiKey: string;
50
+ };
47
51
  ai: {
48
52
  apiKey: string;
49
53
  apiEndpoint: string;
@@ -66,6 +70,7 @@ type SettingsViewState = {
66
70
  bybitApiKey: string;
67
71
  bybitApiSecret: string;
68
72
  coinalyzeApiKey: string;
73
+ coinmarketcapApiKey: string;
69
74
  aiApiKey: string;
70
75
  aiApiEndpoint: string;
71
76
  aiModel: string;
@@ -81,11 +86,18 @@ type PasswordState = {
81
86
  confirmPassword: string;
82
87
  };
83
88
 
84
- type SectionName = 'bybit' | 'password' | 'coinalyze' | 'ai' | 'telegram';
89
+ type SectionName =
90
+ | 'bybit'
91
+ | 'password'
92
+ | 'coinalyze'
93
+ | 'coinmarketcap'
94
+ | 'ai'
95
+ | 'telegram';
85
96
  type EditableField =
86
97
  | 'bybitApiKey'
87
98
  | 'bybitApiSecret'
88
99
  | 'coinalyzeApiKey'
100
+ | 'coinmarketcapApiKey'
89
101
  | 'aiApiKey'
90
102
  | 'aiApiEndpoint'
91
103
  | 'aiModel'
@@ -98,6 +110,7 @@ const EMPTY_SETTINGS: SettingsViewState = {
98
110
  bybitApiKey: '',
99
111
  bybitApiSecret: '',
100
112
  coinalyzeApiKey: '',
113
+ coinmarketcapApiKey: '',
101
114
  aiApiKey: '',
102
115
  aiApiEndpoint: '',
103
116
  aiModel: '',
@@ -110,6 +123,7 @@ const EMPTY_DRAFTS: SettingsDraftState = {
110
123
  bybitApiKey: '',
111
124
  bybitApiSecret: '',
112
125
  coinalyzeApiKey: '',
126
+ coinmarketcapApiKey: '',
113
127
  aiApiKey: '',
114
128
  aiApiEndpoint: '',
115
129
  aiModel: '',
@@ -127,6 +141,7 @@ const EMPTY_EDITING: Record<EditableField, boolean> = {
127
141
  bybitApiKey: false,
128
142
  bybitApiSecret: false,
129
143
  coinalyzeApiKey: false,
144
+ coinmarketcapApiKey: false,
130
145
  aiApiKey: false,
131
146
  aiApiEndpoint: false,
132
147
  aiModel: false,
@@ -141,6 +156,7 @@ const SECTION_FIELDS: Record<
141
156
  > = {
142
157
  bybit: ['bybitApiKey', 'bybitApiSecret'],
143
158
  coinalyze: ['coinalyzeApiKey'],
159
+ coinmarketcap: ['coinmarketcapApiKey'],
144
160
  ai: ['aiApiKey', 'aiApiEndpoint', 'aiModel', 'aiResponseLanguage'],
145
161
  telegram: ['tgBotToken', 'tgChatId'],
146
162
  };
@@ -149,6 +165,7 @@ const MASKED_FIELDS = new Set<EditableField>([
149
165
  'bybitApiKey',
150
166
  'bybitApiSecret',
151
167
  'coinalyzeApiKey',
168
+ 'coinmarketcapApiKey',
152
169
  'aiApiKey',
153
170
  'tgBotToken',
154
171
  ]);
@@ -158,6 +175,7 @@ const toViewState = (payload: SettingsResponse): SettingsViewState => ({
158
175
  bybitApiKey: payload.settings.bybit.apiKey || '',
159
176
  bybitApiSecret: payload.settings.bybit.apiSecret || '',
160
177
  coinalyzeApiKey: payload.settings.coinalyze.apiKey || '',
178
+ coinmarketcapApiKey: payload.settings.coinmarketcap.apiKey || '',
161
179
  aiApiKey: payload.settings.ai.apiKey || '',
162
180
  aiApiEndpoint:
163
181
  normalizeAiEndpoint(payload.settings.ai.apiEndpoint) ||
@@ -320,6 +338,10 @@ export const AccountSettingsDrawer = () => {
320
338
  return Boolean(drafts.coinalyzeApiKey.trim());
321
339
  }
322
340
 
341
+ if (section === 'coinmarketcap') {
342
+ return Boolean(drafts.coinmarketcapApiKey.trim());
343
+ }
344
+
323
345
  if (section === 'telegram') {
324
346
  return (
325
347
  Boolean(drafts.tgBotToken.trim()) ||
@@ -440,24 +462,31 @@ export const AccountSettingsDrawer = () => {
440
462
  responseLanguage: drafts.aiResponseLanguage,
441
463
  },
442
464
  }
443
- : section === 'telegram'
465
+ : section === 'coinmarketcap'
444
466
  ? {
445
467
  section,
446
468
  data: {
447
- botToken: getSecretUpdateValue('tgBotToken'),
448
- chatId:
449
- drafts.tgChatId !== settings.tgChatId
450
- ? drafts.tgChatId.trim()
451
- : undefined,
469
+ apiKey: getSecretUpdateValue('coinmarketcapApiKey'),
452
470
  },
453
471
  }
454
- : {
455
- section,
456
- data: {
457
- password: passwords.password,
458
- confirmPassword: passwords.confirmPassword,
459
- },
460
- };
472
+ : section === 'telegram'
473
+ ? {
474
+ section,
475
+ data: {
476
+ botToken: getSecretUpdateValue('tgBotToken'),
477
+ chatId:
478
+ drafts.tgChatId !== settings.tgChatId
479
+ ? drafts.tgChatId.trim()
480
+ : undefined,
481
+ },
482
+ }
483
+ : {
484
+ section,
485
+ data: {
486
+ password: passwords.password,
487
+ confirmPassword: passwords.confirmPassword,
488
+ },
489
+ };
461
490
 
462
491
  const response = await fetch('/api/user/settings', {
463
492
  method: 'PATCH',
@@ -636,6 +665,8 @@ export const AccountSettingsDrawer = () => {
636
665
  </Text>
637
666
  </Text>
638
667
 
668
+ <TradingAccountsPanel />
669
+
639
670
  <Box
640
671
  borderWidth="1px"
641
672
  borderColor="gray.700"
@@ -645,27 +676,23 @@ export const AccountSettingsDrawer = () => {
645
676
  >
646
677
  <Stack gap={4}>
647
678
  <Box>
648
- <Text fontWeight="600">Bybit connection</Text>
679
+ <Text fontWeight="600">CoinMarketCap</Text>
649
680
  <Text fontSize="sm" color="gray.400">
650
- API credentials used for exchange access.
681
+ API key stored in the user profile for historical
682
+ global market context ingestion.
651
683
  </Text>
652
684
  </Box>
653
685
  {renderEditableField({
654
- label: 'BYBIT_API_KEY',
655
- field: 'bybitApiKey',
656
- placeholder: 'Enter a new Bybit API key',
657
- })}
658
- {renderEditableField({
659
- label: 'BYBIT_API_SECRET',
660
- field: 'bybitApiSecret',
661
- placeholder: 'Enter a new Bybit API secret',
686
+ label: 'COINMARKETCAP_API_KEY',
687
+ field: 'coinmarketcapApiKey',
688
+ placeholder: 'Enter a new CoinMarketCap API key',
662
689
  })}
663
690
  <Flex justify="flex-end">
664
691
  <Button
665
692
  colorPalette="teal"
666
- loading={savingSection === 'bybit'}
667
- disabled={!isSectionDirty('bybit')}
668
- onClick={() => saveSection('bybit')}
693
+ loading={savingSection === 'coinmarketcap'}
694
+ disabled={!isSectionDirty('coinmarketcap')}
695
+ onClick={() => saveSection('coinmarketcap')}
669
696
  >
670
697
  Save
671
698
  </Button>
@@ -0,0 +1,445 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import {
5
+ Box,
6
+ Button,
7
+ Field,
8
+ Flex,
9
+ HStack,
10
+ IconButton,
11
+ Input,
12
+ Stack,
13
+ Text,
14
+ } from '@chakra-ui/react';
15
+ import { FiEdit2 } from 'react-icons/fi';
16
+ import type { MarketUniverse } from '@tradejs/types';
17
+ import { toaster } from '#ui';
18
+
19
+ type PublicTradingAccount = {
20
+ id: string;
21
+ label: string;
22
+ provider: string;
23
+ enabled: boolean;
24
+ isDefault?: boolean;
25
+ universes: MarketUniverse[];
26
+ environment: 'mainnet' | 'testnet';
27
+ hasApiKey: boolean;
28
+ hasApiSecret: boolean;
29
+ maskedApiKey: string;
30
+ maskedApiSecret: string;
31
+ };
32
+
33
+ const FieldInput = ({
34
+ label,
35
+ value,
36
+ onChange,
37
+ type,
38
+ }: {
39
+ label: string;
40
+ value: string;
41
+ onChange: (value: string) => void;
42
+ type?: string;
43
+ }) => (
44
+ <Field.Root>
45
+ <Field.Label>{label}</Field.Label>
46
+ <Input
47
+ value={value}
48
+ type={type}
49
+ onChange={(event) => onChange(event.target.value)}
50
+ />
51
+ </Field.Root>
52
+ );
53
+
54
+ type CredentialFieldName = 'apiKey' | 'apiSecret';
55
+
56
+ const AccountCredentialField = ({
57
+ label,
58
+ savedValue,
59
+ value,
60
+ editing,
61
+ onEdit,
62
+ onChange,
63
+ }: {
64
+ label: string;
65
+ savedValue: string;
66
+ value: string;
67
+ editing: boolean;
68
+ onEdit: () => void;
69
+ onChange: (value: string) => void;
70
+ }) => (
71
+ <Field.Root width="full">
72
+ <Field.Label>{label}</Field.Label>
73
+ <HStack align="stretch" width="full">
74
+ {editing ? (
75
+ <Input
76
+ flex="1"
77
+ minW="0"
78
+ type="password"
79
+ value={value}
80
+ placeholder={`Enter a new ${label}`}
81
+ onChange={(event) => onChange(event.target.value)}
82
+ autoFocus
83
+ fontFamily="mono"
84
+ />
85
+ ) : (
86
+ <Flex
87
+ flex="1"
88
+ minW="0"
89
+ h="10"
90
+ px="3"
91
+ borderWidth="1px"
92
+ borderColor="whiteAlpha.200"
93
+ borderRadius="md"
94
+ bg="blackAlpha.300"
95
+ align="center"
96
+ color={savedValue ? 'gray.200' : 'gray.500'}
97
+ cursor="default"
98
+ userSelect="none"
99
+ >
100
+ <Text
101
+ width="full"
102
+ overflow="hidden"
103
+ whiteSpace="nowrap"
104
+ textOverflow="ellipsis"
105
+ fontFamily={savedValue ? 'mono' : undefined}
106
+ >
107
+ {savedValue || 'Not set'}
108
+ </Text>
109
+ </Flex>
110
+ )}
111
+ <IconButton
112
+ aria-label={`Edit ${label}`}
113
+ size="md"
114
+ colorPalette="teal"
115
+ variant={editing ? 'solid' : 'outline'}
116
+ flexShrink={0}
117
+ onClick={onEdit}
118
+ >
119
+ <FiEdit2 />
120
+ </IconButton>
121
+ </HStack>
122
+ </Field.Root>
123
+ );
124
+
125
+ export const TradingAccountsPanel = () => {
126
+ const [accounts, setAccounts] = useState<PublicTradingAccount[]>([]);
127
+ const [saving, setSaving] = useState<'account' | 'default' | null>(null);
128
+ const [settingDefaultId, setSettingDefaultId] = useState('');
129
+ const [accountDraft, setAccountDraft] = useState({
130
+ id: '',
131
+ label: '',
132
+ apiKey: '',
133
+ apiSecret: '',
134
+ });
135
+ const [addingAccount, setAddingAccount] = useState(false);
136
+ const [editingCredentials, setEditingCredentials] = useState<
137
+ Record<CredentialFieldName, boolean>
138
+ >({ apiKey: false, apiSecret: false });
139
+
140
+ const load = useCallback(async () => {
141
+ const accountsResponse = await fetch('/api/user/trading-accounts');
142
+ if (accountsResponse.ok) {
143
+ const payload = (await accountsResponse.json()) as {
144
+ accounts?: PublicTradingAccount[];
145
+ };
146
+ const nextAccounts = payload.accounts ?? [];
147
+ setAccounts(nextAccounts);
148
+ }
149
+ }, []);
150
+
151
+ useEffect(() => {
152
+ void load();
153
+ }, [load]);
154
+
155
+ const saveAccount = async () => {
156
+ setSaving('account');
157
+ try {
158
+ const normalizedLabel = accountDraft.label.trim();
159
+ const baseId =
160
+ normalizedLabel
161
+ .toLowerCase()
162
+ .replace(/[^a-z0-9]+/g, '-')
163
+ .replace(/^-|-$/g, '') || 'account';
164
+ const generatedId = `bybit-${baseId}`;
165
+ const id =
166
+ accountDraft.id ||
167
+ (accounts.some((account) => account.id === generatedId)
168
+ ? `${generatedId}-${Date.now().toString(36)}`
169
+ : generatedId);
170
+ const response = await fetch('/api/user/trading-accounts', {
171
+ method: 'POST',
172
+ headers: { 'content-type': 'application/json' },
173
+ body: JSON.stringify({
174
+ ...accountDraft,
175
+ id,
176
+ provider: 'bybit',
177
+ enabled: true,
178
+ universes: ['crypto', 'tradfi'],
179
+ environment: 'mainnet',
180
+ isDefault:
181
+ accounts.length === 0 ||
182
+ accounts.find((account) => account.id === id)?.isDefault === true,
183
+ }),
184
+ });
185
+ const payload = (await response.json()) as { error?: string };
186
+ if (!response.ok) throw new Error(payload.error || 'Save failed');
187
+ setAccountDraft({
188
+ id: '',
189
+ label: '',
190
+ apiKey: '',
191
+ apiSecret: '',
192
+ });
193
+ setAddingAccount(false);
194
+ setEditingCredentials({ apiKey: false, apiSecret: false });
195
+ await load();
196
+ toaster.create({ title: 'Trading account saved', type: 'success' });
197
+ } catch (error) {
198
+ toaster.create({
199
+ title: error instanceof Error ? error.message : 'Save failed',
200
+ type: 'error',
201
+ });
202
+ } finally {
203
+ setSaving(null);
204
+ }
205
+ };
206
+
207
+ const editCredential = (
208
+ account: PublicTradingAccount,
209
+ field: CredentialFieldName,
210
+ ) => {
211
+ if (accountDraft.id !== account.id) {
212
+ setAccountDraft({
213
+ id: account.id,
214
+ label: account.label,
215
+ apiKey: '',
216
+ apiSecret: '',
217
+ });
218
+ setEditingCredentials({
219
+ apiKey: field === 'apiKey',
220
+ apiSecret: field === 'apiSecret',
221
+ });
222
+ return;
223
+ }
224
+ setEditingCredentials((current) => ({ ...current, [field]: true }));
225
+ };
226
+
227
+ const makeDefault = async (account: PublicTradingAccount) => {
228
+ setSaving('default');
229
+ setSettingDefaultId(account.id);
230
+ try {
231
+ const response = await fetch('/api/user/trading-accounts', {
232
+ method: 'POST',
233
+ headers: { 'content-type': 'application/json' },
234
+ body: JSON.stringify({
235
+ id: account.id,
236
+ label: account.label,
237
+ provider: account.provider,
238
+ enabled: account.enabled,
239
+ universes: account.universes,
240
+ environment: account.environment,
241
+ isDefault: true,
242
+ }),
243
+ });
244
+ const payload = (await response.json()) as { error?: string };
245
+ if (!response.ok) throw new Error(payload.error || 'Save failed');
246
+ await load();
247
+ toaster.create({
248
+ title: 'Default trading account updated',
249
+ type: 'success',
250
+ });
251
+ } catch (error) {
252
+ toaster.create({
253
+ title: error instanceof Error ? error.message : 'Save failed',
254
+ type: 'error',
255
+ });
256
+ } finally {
257
+ setSaving(null);
258
+ setSettingDefaultId('');
259
+ }
260
+ };
261
+
262
+ return (
263
+ <Box
264
+ borderWidth="1px"
265
+ borderColor="gray.700"
266
+ borderRadius="lg"
267
+ p={4}
268
+ bg="gray.900"
269
+ >
270
+ <Stack gap={4}>
271
+ <Box>
272
+ <Text fontWeight="600">Bybit</Text>
273
+ <Text fontSize="sm" color="gray.400">
274
+ Trading account credentials used by active strategies.
275
+ </Text>
276
+ </Box>
277
+
278
+ {accounts.map((account) => {
279
+ const isEditingAccount = accountDraft.id === account.id;
280
+ const hasChanges = Boolean(
281
+ isEditingAccount &&
282
+ (accountDraft.apiKey.trim() || accountDraft.apiSecret.trim()),
283
+ );
284
+ return (
285
+ <Stack
286
+ key={account.id}
287
+ borderWidth={accounts.length > 1 ? '1px' : '0'}
288
+ borderColor="gray.700"
289
+ borderRadius="md"
290
+ p={accounts.length > 1 ? 3 : 0}
291
+ gap={3}
292
+ >
293
+ <Flex justify="space-between" align="center" gap={3}>
294
+ <Box>
295
+ <Text fontWeight="600">{account.label}</Text>
296
+ <Text fontSize="xs" color="gray.400">
297
+ Bybit · {account.universes.join(' · ')} ·{' '}
298
+ {account.environment}
299
+ </Text>
300
+ </Box>
301
+ <Flex align="center" gap={2}>
302
+ <Text
303
+ fontSize="xs"
304
+ color={account.enabled ? 'green.300' : 'red.300'}
305
+ >
306
+ {account.isDefault
307
+ ? 'default'
308
+ : account.enabled
309
+ ? 'enabled'
310
+ : 'disabled'}
311
+ </Text>
312
+ {!account.isDefault && account.enabled && (
313
+ <Button
314
+ size="xs"
315
+ variant="outline"
316
+ colorPalette="teal"
317
+ loading={
318
+ saving === 'default' && settingDefaultId === account.id
319
+ }
320
+ onClick={() => makeDefault(account)}
321
+ >
322
+ Make default
323
+ </Button>
324
+ )}
325
+ </Flex>
326
+ </Flex>
327
+ <AccountCredentialField
328
+ label="BYBIT_API_KEY"
329
+ savedValue={account.maskedApiKey}
330
+ value={isEditingAccount ? accountDraft.apiKey : ''}
331
+ editing={isEditingAccount && editingCredentials.apiKey}
332
+ onEdit={() => editCredential(account, 'apiKey')}
333
+ onChange={(apiKey) =>
334
+ setAccountDraft((draft) => ({ ...draft, apiKey }))
335
+ }
336
+ />
337
+ <AccountCredentialField
338
+ label="BYBIT_API_SECRET"
339
+ savedValue={account.maskedApiSecret}
340
+ value={isEditingAccount ? accountDraft.apiSecret : ''}
341
+ editing={isEditingAccount && editingCredentials.apiSecret}
342
+ onEdit={() => editCredential(account, 'apiSecret')}
343
+ onChange={(apiSecret) =>
344
+ setAccountDraft((draft) => ({ ...draft, apiSecret }))
345
+ }
346
+ />
347
+ <Flex justify="flex-end">
348
+ <Button
349
+ colorPalette="teal"
350
+ loading={saving === 'account' && isEditingAccount}
351
+ disabled={!hasChanges}
352
+ onClick={saveAccount}
353
+ >
354
+ Save
355
+ </Button>
356
+ </Flex>
357
+ </Stack>
358
+ );
359
+ })}
360
+
361
+ {(accounts.length === 0 || addingAccount) && (
362
+ <Stack gap={3}>
363
+ <Text fontSize="sm" fontWeight="600">
364
+ Connect account
365
+ </Text>
366
+ <FieldInput
367
+ label="Account name"
368
+ value={accountDraft.label}
369
+ onChange={(label) =>
370
+ setAccountDraft((draft) => ({ ...draft, label }))
371
+ }
372
+ />
373
+ <FieldInput
374
+ label="BYBIT_API_KEY"
375
+ type="password"
376
+ value={accountDraft.apiKey}
377
+ onChange={(apiKey) =>
378
+ setAccountDraft((draft) => ({ ...draft, apiKey }))
379
+ }
380
+ />
381
+ <FieldInput
382
+ label="BYBIT_API_SECRET"
383
+ type="password"
384
+ value={accountDraft.apiSecret}
385
+ onChange={(apiSecret) =>
386
+ setAccountDraft((draft) => ({ ...draft, apiSecret }))
387
+ }
388
+ />
389
+ <Flex justify="flex-end" gap={2}>
390
+ {accounts.length > 0 && (
391
+ <Button
392
+ variant="ghost"
393
+ onClick={() => {
394
+ setAddingAccount(false);
395
+ setAccountDraft({
396
+ id: '',
397
+ label: '',
398
+ apiKey: '',
399
+ apiSecret: '',
400
+ });
401
+ }}
402
+ >
403
+ Cancel
404
+ </Button>
405
+ )}
406
+ <Button
407
+ colorPalette="teal"
408
+ loading={saving === 'account'}
409
+ disabled={
410
+ !accountDraft.label.trim() ||
411
+ !accountDraft.apiKey.trim() ||
412
+ !accountDraft.apiSecret.trim()
413
+ }
414
+ onClick={saveAccount}
415
+ >
416
+ Connect
417
+ </Button>
418
+ </Flex>
419
+ </Stack>
420
+ )}
421
+
422
+ {accounts.length > 0 && !addingAccount && (
423
+ <Button
424
+ alignSelf="flex-start"
425
+ size="sm"
426
+ variant="outline"
427
+ colorPalette="teal"
428
+ onClick={() => {
429
+ setAccountDraft({
430
+ id: '',
431
+ label: '',
432
+ apiKey: '',
433
+ apiSecret: '',
434
+ });
435
+ setEditingCredentials({ apiKey: false, apiSecret: false });
436
+ setAddingAccount(true);
437
+ }}
438
+ >
439
+ Add another account
440
+ </Button>
441
+ )}
442
+ </Stack>
443
+ </Box>
444
+ );
445
+ };
@@ -3,7 +3,13 @@
3
3
  import { Box, Flex, IconButton, VStack } from '@chakra-ui/react';
4
4
  import { useRouter, usePathname } from 'next/navigation';
5
5
  import { signOut } from 'next-auth/react';
6
- import { FiActivity, FiBarChart2, FiLogOut, FiPlay } from 'react-icons/fi';
6
+ import {
7
+ FiActivity,
8
+ FiBarChart2,
9
+ FiLayers,
10
+ FiLogOut,
11
+ FiPlay,
12
+ } from 'react-icons/fi';
7
13
  import { AccountSettingsDrawer } from './AccountSettingsDrawer';
8
14
 
9
15
  export const Sidebar = () => {
@@ -21,6 +27,12 @@ export const Sidebar = () => {
21
27
  label: 'Backtest',
22
28
  path: '/routes/backtest',
23
29
  },
30
+ {
31
+ icon: FiLayers,
32
+ label: 'Strategies',
33
+ path: '/routes/strategies/runtime',
34
+ activePath: '/routes/strategies',
35
+ },
24
36
  {
25
37
  icon: FiActivity,
26
38
  label: 'Derivatives',
@@ -43,14 +55,16 @@ export const Sidebar = () => {
43
55
  >
44
56
  <Flex direction="column" h="100%" justify="space-between">
45
57
  <VStack>
46
- {navItems.map(({ icon: Icon, label, path }) => (
58
+ {navItems.map(({ icon: Icon, label, path, activePath }) => (
47
59
  <IconButton
48
60
  key={path}
49
61
  mb={2}
50
62
  aria-label={label}
51
63
  size="md"
52
64
  colorPalette="teal"
53
- variant={pathname.includes(path) ? 'solid' : 'outline'}
65
+ variant={
66
+ pathname.includes(activePath ?? path) ? 'solid' : 'outline'
67
+ }
54
68
  onClick={() => router.push(path)}
55
69
  >
56
70
  <Icon />