diff --git a/src/layouts/top-navigation/index.tsx b/src/layouts/top-navigation/index.tsx index c2125e4..1db8dcf 100644 --- a/src/layouts/top-navigation/index.tsx +++ b/src/layouts/top-navigation/index.tsx @@ -34,11 +34,11 @@ function TopNavigation() { message_text: 'Your transaction was successful', }, ]; - //useEffect(() => { + // useEffect(() => { // if (data) { // setNotifications(data); // } - //}, [data]); + // }, [data]); const notifications = data; useEffect(() => { diff --git a/src/pages/overview-bots/components/BotView.tsx b/src/pages/overview-bots/components/BotView.tsx index 480cc64..6c26233 100644 --- a/src/pages/overview-bots/components/BotView.tsx +++ b/src/pages/overview-bots/components/BotView.tsx @@ -25,6 +25,11 @@ import TradeTable from './TradeTable'; import GoBack from './GoBack'; import CryptoStats from './CryptoStats'; +const truncateDecimals = (value: number, decimalPlaces: number = 2): number => { + const multiplier = 10 ** decimalPlaces; + return Math.trunc(value * multiplier) / multiplier; +}; + interface IBotViewProps { bot: IBotData; onBack: () => void; @@ -73,11 +78,6 @@ const SingleBotView: React.FC = ({ cardBotData, }) => { const getChartTypeQuery = searchParams.get('chart') || 'trades'; - const [tradePair, setTradePair] = useState(''); - - const getTradePair = (pair: string) => { - setTradePair(pair); - }; return ( <> @@ -102,11 +102,12 @@ const SingleBotView: React.FC = ({ label={ <>

- {bot.pnl.isPositive ? '+' : '-'}${bot.pnl.value} + {bot.pnl.isPositive ? '+' : '-'}$ + {truncateDecimals(bot.pnl.value)}

{bot.pnl.isPositive ? '+' : '-'} - {bot.pnl.percentage}% P&L + {truncateDecimals(bot.pnl.percentage)}% P&L

} diff --git a/src/pages/overview-bots/components/Bots.tsx b/src/pages/overview-bots/components/Bots.tsx index 388d0e3..7945938 100644 --- a/src/pages/overview-bots/components/Bots.tsx +++ b/src/pages/overview-bots/components/Bots.tsx @@ -43,8 +43,12 @@ interface IBotsProps { handleOpenWalletModal: () => void; } +const truncateDecimals = (value: number, decimalPlaces: number = 2): number => { + const multiplier = 10 ** decimalPlaces; + return Math.trunc(value * multiplier) / multiplier; +}; + const Bots: React.FC = ({ - botsData, timeTabs, stratTabs, tradeDateTabs, @@ -63,9 +67,8 @@ const Bots: React.FC = ({ cardBotData, searchParams, setSearchParams, - handleOpenWalletModal, }) => { - const { deposit, withdraw } = useTransaction(); + const { withdraw } = useTransaction(); const headers = [ 'BOT', 'STATUS', @@ -78,6 +81,8 @@ const Bots: React.FC = ({ ]; const [selectedBot, setSelectedBot] = useState(null); + const botsData = useAppSelector((state) => state.auth.botsData); + const handleBotClick = (bot: IBotData | undefined) => { if (!bot) return; @@ -112,24 +117,9 @@ const Bots: React.FC = ({ ); } - const navigateToTraining = () => { - setSearchParams({ tab: 'training' }); - }; - - const { address } = useAppSelector((state) => state.auth); - - const handleCreateNewBot = () => { - if (!address) { - setSearchParams({ redirectToTraining: 'true' }); - handleOpenWalletModal(); - } else { - navigateToTraining(); - } - }; - return (
-

Trading bots list

+

Trading bot list

{['Best', 'Worst', 'OTN', 'SOL', 'BTC', 'USDC'].map((tab) => ( @@ -148,24 +138,6 @@ const Bots: React.FC = ({ />
- @@ -211,9 +183,7 @@ const Bots: React.FC = ({ /> - + diff --git a/src/pages/overview-bots/components/CustomText.tsx b/src/pages/overview-bots/components/CustomText.tsx index df350af..d1aecd0 100644 --- a/src/pages/overview-bots/components/CustomText.tsx +++ b/src/pages/overview-bots/components/CustomText.tsx @@ -13,7 +13,17 @@ interface ICustomTextProps { type ToolTipType = { [key: string]: boolean }; const CustomText = forwardRef( - ({ text, toolTipText, showOptText, hasQuestionMark = true, xtraStyle, toolTipWidth }, ref) => { + ( + { + text, + toolTipText, + showOptText, + hasQuestionMark = true, + xtraStyle, + toolTipWidth, + }, + ref + ) => { const [showToolTip, setShowToolTip] = useState(false); const spanRef = useRef(null); @@ -36,9 +46,13 @@ const CustomText = forwardRef( }, []); return ( -

+

{text} - {showOptText && (Optional)} + {showOptText && ( + (Optional) + )} {hasQuestionMark && (

( ? {showToolTip && ( - + )}
)} diff --git a/src/pages/overview-bots/components/DepositModal.tsx b/src/pages/overview-bots/components/DepositModal.tsx new file mode 100644 index 0000000..22329b9 --- /dev/null +++ b/src/pages/overview-bots/components/DepositModal.tsx @@ -0,0 +1,82 @@ +import React, { useState } from 'react'; +import { useTransactions } from '@shared/hooks/useTransaction'; +import CustomBtn from '@components/ui/CustomBtn'; +import { toast } from 'sonner'; + +interface DepositModalProps { + isOpen: boolean; + onClose: () => void; +} + +const DepositModal: React.FC = ({ isOpen, onClose }) => { + const [usdcAmount, setUsdcAmount] = useState(''); + const [solAmount, setSolAmount] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const { deposit } = useTransactions(); + + if (!isOpen) return null; + + const handleDeposit = async () => { + try { + setIsLoading(true); + await deposit(usdcAmount, solAmount); + onClose(); + } catch (error) { + console.error('Deposit failed:', error); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+

Deposit Funds

+ +
+
+ + setUsdcAmount(e.target.value)} + className="w-full p-2 border rounded-lg" + placeholder="Enter USDC amount" + /> +
+ +
+ + setSolAmount(e.target.value)} + className="w-full p-2 border rounded-lg" + placeholder="Enter SOL amount" + /> +
+
+ +
+ + +
+
+
+ ); +}; + +export default DepositModal; diff --git a/src/pages/overview-bots/components/Overview.tsx b/src/pages/overview-bots/components/Overview.tsx index a197951..5537360 100644 --- a/src/pages/overview-bots/components/Overview.tsx +++ b/src/pages/overview-bots/components/Overview.tsx @@ -1,7 +1,4 @@ -import { - SetURLSearchParams, - useNavigate, -} from 'react-router-dom'; +import { SetURLSearchParams, useNavigate } from 'react-router-dom'; import CustomBtn from '@components/ui/CustomBtn'; import React, { useCallback, useEffect, useState } from 'react'; import { useAppSelector } from '@shared/hooks/useStore'; diff --git a/src/pages/overview-bots/components/Tooltip.tsx b/src/pages/overview-bots/components/Tooltip.tsx index adbc835..f28ebcc 100644 --- a/src/pages/overview-bots/components/Tooltip.tsx +++ b/src/pages/overview-bots/components/Tooltip.tsx @@ -64,7 +64,7 @@ const Tooltip = forwardRef( ref={tooltipRef} style={isOverflow.right ? { right: -16.5 } : { left: -25 }} className={`absolute bottom-8 z-50 ${ - width ? width : 'w-56' + width || 'w-56' } p-4 text-xs text-white bg-dark-400 rounded-[20px] h-fit`} > {text} diff --git a/src/pages/overview-bots/components/Training.tsx b/src/pages/overview-bots/components/Training.tsx index f97a470..82815f4 100644 --- a/src/pages/overview-bots/components/Training.tsx +++ b/src/pages/overview-bots/components/Training.tsx @@ -1,4 +1,3 @@ -import { strategiesConfigData as config } from '../../../utils/strategyConfigData'; import React, { ChangeEvent, useCallback, @@ -10,6 +9,7 @@ import { useGetHistoricalCandlesMutation } from '@store/market/api'; import { SetURLSearchParams } from 'react-router-dom'; import CustomBtn from '@components/ui/CustomBtn'; import { FadeLoader } from 'react-spinners'; +import { strategiesConfigData as config } from '../../../utils/strategyConfigData'; import { ICardBotData, IResultStrat, @@ -31,6 +31,7 @@ import LineTab from './LineTab'; import { transformData } from '../../../utils/transformData'; import { formatText } from '../../../utils/formatText.util'; import ToggleButton from './ToggleButton'; +import DepositModal from './DepositModal'; export interface ITrainingProps { timeQuery: ITimeTab; @@ -59,6 +60,8 @@ const Training: React.FC = ({ setSearchParams, cardBotData, }) => { + const [isDepositModalOpen, setIsDepositModalOpen] = useState(false); + const [historicalCandlesData, { isLoading, data, error }] = useGetHistoricalCandlesMutation(); const [currentStep, setCurrentStep] = useState(1); @@ -124,7 +127,10 @@ const Training: React.FC = ({ }; const handleNextStep = () => { - if (currentStep === 2) return; + if (currentStep === 2) { + setIsDepositModalOpen(true); + return; + } setCurrentStep((prevState) => prevState + 1); }; @@ -450,6 +456,10 @@ const Training: React.FC = ({
+ setIsDepositModalOpen(false)} + /> ); }; diff --git a/src/pages/overview-bots/hooks/useProfile.ts b/src/pages/overview-bots/hooks/useProfile.ts index 8a009ab..75324b8 100644 --- a/src/pages/overview-bots/hooks/useProfile.ts +++ b/src/pages/overview-bots/hooks/useProfile.ts @@ -78,10 +78,7 @@ export interface IBotData { isPositive: boolean; chartData: number[]; }; - portfolio: { - value: number; - percentage: number; - }; + portfolio: number; accuracy: number; sharpeRatio: number; apr: number; @@ -758,10 +755,7 @@ export default () => { isPositive: true, chartData: [50, 60, 40, 49, 38, 34, 80, 76, 95, 100], }, - portfolio: { - value: 9186, - percentage: 20, - }, + portfolio: 9186, accuracy: 65, sharpeRatio: 2.81, apr: 210, @@ -778,10 +772,7 @@ export default () => { isPositive: true, chartData: [50, 60, 40, 49, 38, 34, 80, 76, 95, 100], }, - portfolio: { - value: 7036, - percentage: 11, - }, + portfolio: 7036, accuracy: 59, sharpeRatio: 2.01, apr: 187, @@ -798,10 +789,7 @@ export default () => { isPositive: false, chartData: [90, 85, 80, 70, 60, 65, 75, 76, 95, 80], }, - portfolio: { - value: 3127, - percentage: 1, - }, + portfolio: 1, accuracy: 49, sharpeRatio: 1.75, apr: 165, diff --git a/src/shared/components/AppModal.tsx b/src/shared/components/AppModal.tsx index b28f46a..dbfe80d 100644 --- a/src/shared/components/AppModal.tsx +++ b/src/shared/components/AppModal.tsx @@ -8,7 +8,13 @@ interface AppModalProps { children: React.ReactNode; } -const AppModal: React.FC = ({ title, isOpen, handleClose, handleSuccsessfulLogin, children }) => { +const AppModal: React.FC = ({ + title, + isOpen, + handleClose, + handleSuccsessfulLogin, + children, +}) => { if (!isOpen) return null; return ( @@ -23,4 +29,3 @@ const AppModal: React.FC = ({ title, isOpen, handleClose, handleS }; export default AppModal; - diff --git a/src/shared/components/LoginForm.tsx b/src/shared/components/LoginForm.tsx index 3120c74..6719d2a 100644 --- a/src/shared/components/LoginForm.tsx +++ b/src/shared/components/LoginForm.tsx @@ -29,7 +29,7 @@ const LoginForm: React.FC = ({ onSuccessfulLogin }) => { if (onSuccessfulLogin) { onSuccessfulLogin(); } - + // Check if the user came from the Overview page if (location.state && location.state.from === 'overview') { navigate('/training'); // Adjust this path to match your route for the Training component diff --git a/src/shared/hooks/useTransaction.ts b/src/shared/hooks/useTransaction.ts index b5a3431..9ee9402 100644 --- a/src/shared/hooks/useTransaction.ts +++ b/src/shared/hooks/useTransaction.ts @@ -29,7 +29,7 @@ interface TransactionError extends Error { } export const useTransactions = () => { - const { address, botsData } = useAppSelector((state) => state.auth); + const { address } = useAppSelector((state) => state.auth); const { signTransaction } = useWallet(); const [createInstance] = useCreateInstanceMutation(); const [startInstance] = useStartInstanceMutation(); @@ -73,21 +73,6 @@ export const useTransactions = () => { [signTransaction] ); - const handleTransactionError = useCallback( - (error: unknown, operation: string) => { - console.error(`${operation} failed:`, error); - const transactionError = error as TransactionError; - const errorMessage = - transactionError.error || - transactionError.data?.message || - transactionError.message || - `${operation} failed. Please try again.`; - toast.error(errorMessage); - throw transactionError; - }, - [] - ); - const getRandomStrategy = useCallback(() => { if (!strategies) return { name: 'default_strategy', parameters: {} }; @@ -111,7 +96,7 @@ export const useTransactions = () => { }, [strategies]); const deposit = useCallback( - async (amount: number): Promise => { + async (usdcAmount: string, solAmount: string): Promise => { if (!address) throw new Error('Wallet not connected'); try { @@ -134,7 +119,8 @@ export const useTransactions = () => { }, body: JSON.stringify({ owner: address, - amount, + usdcTreasury: usdcAmount, + feesAmount: solAmount, delegate: wallet_address, }), } @@ -170,17 +156,16 @@ export const useTransactions = () => { mangoAccount: result.mangoAccount, }; } catch (error: any) { - console.error('Deposit error:', error); - return handleTransactionError(error, 'Deposit'); + const transactionError = error as TransactionError; + const errorMessage = + transactionError.error || + transactionError.data?.message || + transactionError.message; + toast.error('Error depositing, make sure you have enough SOL and USDC'); + throw Error(errorMessage); } }, - [ - address, - createInstance, - startInstance, - signAndSendTransaction, - handleTransactionError, - ] + [address, createInstance, startInstance, signAndSendTransaction] ); const withdraw = useCallback( @@ -217,10 +202,18 @@ export const useTransactions = () => { toast.success(`Withdrawal successful`); return { signature }; } catch (error) { - return handleTransactionError(error, 'Withdrawal'); + const transactionError = error as TransactionError; + const errorMessage = + transactionError.error || + transactionError.data?.message || + transactionError.message; + toast.error( + 'Error withdrawing, make sure you have enough SOL and USDC, contact us' + ); + throw Error(errorMessage); } }, - [address, signAndSendTransaction, handleTransactionError] + [address, signAndSendTransaction] ); return { deposit, withdraw }; diff --git a/src/store/auth/slice.ts b/src/store/auth/slice.ts index 734bc02..5d3119b 100644 --- a/src/store/auth/slice.ts +++ b/src/store/auth/slice.ts @@ -44,6 +44,9 @@ const authSlice = createSlice({ setBotData: (state, action: PayloadAction) => { state.botsData = action.payload; }, + addBot: (state, action: PayloadAction) => { + state.botsData.unshift(action.payload); + }, updateBotStats: (state, action: PayloadAction>) => { const index = state.botsData.findIndex( (bot) => bot.id === action.payload.id @@ -65,6 +68,7 @@ export const { setLoginStatus, setUsdcBalance, setBotData, + addBot, updateBotStats, resetAuth, } = authSlice.actions; diff --git a/src/store/robotterApi.ts b/src/store/robotterApi.ts new file mode 100644 index 0000000..ea5d598 --- /dev/null +++ b/src/store/robotterApi.ts @@ -0,0 +1,189 @@ +import { robotterApi } from './config'; + +const robotterEndpoints = robotterApi.injectEndpoints({ + endpoints: (builder) => ({ + createInstance: builder.mutation< + { instance_id: string; wallet_address: string; market: string }, + { + strategy_name: string; + strategy_parameters: Record; + market: string; + } + >({ + query: (data) => { + console.log('createInstance request body:', data); + return { + url: '/instances', + method: 'POST', + data, + }; + }, + }), + + getInstanceWallet: builder.query({ + query: (instanceId) => ({ + url: `/instances/${instanceId}/wallet`, + method: 'GET', + }), + }), + + startInstance: builder.mutation< + string, + { + instanceId: string; + strategy_name: string; + parameters: Record; + } + >({ + query: ({ instanceId, ...data }) => ({ + url: `/instances/${instanceId}/start`, + method: 'POST', + body: data, + }), + }), + + stopInstance: builder.mutation({ + query: (instanceId) => ({ + url: `/instances/${instanceId}/stop`, + method: 'POST', + }), + }), + + getStrategies: builder.query>, void>({ + query: () => ({ + url: '/strategies', + method: 'GET', + }), + }), + + getHistoricalCandles: builder.mutation< + { + data: Array<{ + o: number; + h: number; + l: number; + c: number; + v: number; + unixTime: number; + address: string; + type: string; + }>; + }, + { + connector_name: string; + trading_pair: string; + market_address: string; + interval: string; + start_time: number; + end_time: number; + limit?: number; + } + >({ + query: (body) => ({ + url: '/historical-candles', + method: 'POST', + body, + }), + }), + + runBacktest: builder.mutation< + { + executors: Array<{ + level_id: string; + timestamp: number; + connector_name: string; + trading_pair: string; + entry_price: number; + amount: number; + side: string; + leverage: number; + position_mode: string; + }>; + processed_data: { + features: Record>; + }; + results: { + total_pnl: number; + total_trades: number; + win_rate: number; + profit_loss_ratio: number; + sharpe_ratio: number; + max_drawdown: number; + start_timestamp: number; + end_timestamp: number; + }; + }, + { + start_time: number; + end_time: number; + backtesting_resolution?: string; + trade_cost?: number; + config?: any; + } + >({ + query: (body) => ({ + url: '/backtest', + method: 'POST', + body, + }), + }), + + requestChallenge: builder.mutation< + { + address: string; + chain: 'SOL' | 'ETH'; + valid_til: number; + challenge: string; + }, + { address: string; chain: 'SOL' | 'ETH' } + >({ + query: ({ address, chain }) => ({ + url: `/authorization/challenge?address=${address}&chain=${chain}`, + method: 'POST', + }), + }), + + solveChallenge: builder.mutation< + { + address: string; + chain: 'SOL' | 'ETH'; + valid_til: number; + token: string; + }, + { address: string; chain: 'SOL' | 'ETH'; signature: string } + >({ + query: ({ address, chain, signature }) => ({ + url: `/authorization/solve?address=${address}&chain=${chain}&signature=${signature}`, + method: 'POST', + }), + }), + + refreshToken: builder.mutation< + { + address: string; + chain: 'SOL' | 'ETH'; + valid_til: number; + token: string; + }, + { token: string } + >({ + query: ({ token }) => ({ + url: `/authorization/refresh?token=${token}`, + method: 'POST', + }), + }), + }), +}); + +export const { + useCreateInstanceMutation, + useGetInstanceWalletQuery, + useStartInstanceMutation, + useStopInstanceMutation, + useGetStrategiesQuery, + useGetHistoricalCandlesMutation, + useRunBacktestMutation, + useRequestChallengeMutation, + useSolveChallengeMutation, + useRefreshTokenMutation, +} = robotterEndpoints; diff --git a/src/store/strategies/api.ts b/src/store/strategies/api.ts index b07f36b..0d84d9f 100644 --- a/src/store/strategies/api.ts +++ b/src/store/strategies/api.ts @@ -11,4 +11,4 @@ const strategiesApi = robotterApi.injectEndpoints({ }), }); -export const { useGetStrategiesQuery } = strategiesApi; \ No newline at end of file +export const { useGetStrategiesQuery } = strategiesApi; diff --git a/src/store/wsApi.ts b/src/store/wsApi.ts index 0d48dc1..ec27fe9 100644 --- a/src/store/wsApi.ts +++ b/src/store/wsApi.ts @@ -1,5 +1,10 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; -import { setBotData, updateBotStats, setUsdcBalance } from '@store/auth/slice'; +import { + setBotData, + updateBotStats, + setUsdcBalance, + addBot, +} from '@store/auth/slice'; export const websocketApi = createApi({ reducerPath: 'websocketApi', @@ -20,11 +25,14 @@ export const websocketApi = createApi({ const listener = (event: MessageEvent) => { const data = JSON.parse(event.data); - console.log(data); switch (data.type) { case 'connectionSuccess': dispatch(setBotData(data.payload.bots)); break; + case 'newBot': + console.log(data); + dispatch(addBot(data.payload.bot)); + break; case 'update': handleUpdate(data.payload.event, dispatch); break; @@ -66,8 +74,7 @@ function handleUpdate(event: any, dispatch: any) { dispatch( updateBotStats({ id: event.botId, - ...event.data, - events: [event, ...event.events], // Add the new event to the beginning of the array + events: event, }) ); break; diff --git a/src/utils/strategyConfigData.ts b/src/utils/strategyConfigData.ts index 6fbedbb..47aa7c7 100644 --- a/src/utils/strategyConfigData.ts +++ b/src/utils/strategyConfigData.ts @@ -59,7 +59,7 @@ export const strategiesConfigData: IStrategiesConfigData = { name: 'candles_config', type: 'CandlesConfig', prompt: '', - default: [1,2,3], + default: [1, 2, 3], required: true, min_value: null, max_value: null, @@ -155,7 +155,7 @@ export const strategiesConfigData: IStrategiesConfigData = { name: 'sell_amounts_pct', type: 'Decimal', prompt: '', - default: 56.90, + default: 56.9, required: false, min_value: 0, max_value: null, @@ -461,7 +461,7 @@ export const strategiesConfigData: IStrategiesConfigData = { name: 'candles_config', type: 'CandlesConfig', prompt: '', - default: [3,4,5], + default: [3, 4, 5], required: true, min_value: null, max_value: null, @@ -815,7 +815,7 @@ export const strategiesConfigData: IStrategiesConfigData = { name: 'candles_config', type: 'CandlesConfig', prompt: '', - default: [7,8,9], + default: [7, 8, 9], required: true, min_value: null, max_value: null, @@ -1105,7 +1105,7 @@ export const strategiesConfigData: IStrategiesConfigData = { name: 'candles_config', type: 'CandlesConfig', prompt: '', - default: [10,11,12], + default: [10, 11, 12], required: true, min_value: null, max_value: null, @@ -1523,7 +1523,7 @@ export const strategiesConfigData: IStrategiesConfigData = { name: 'candles_config', type: 'CandlesConfig', prompt: '', - default: [13,14,15], + default: [13, 14, 15], required: true, min_value: null, max_value: null, @@ -1861,7 +1861,7 @@ export const strategiesConfigData: IStrategiesConfigData = { name: 'candles_config', type: 'CandlesConfig', prompt: '', - default: [16,17,18], + default: [16, 17, 18], required: true, min_value: null, max_value: null, @@ -2183,7 +2183,7 @@ export const strategiesConfigData: IStrategiesConfigData = { name: 'candles_config', type: 'CandlesConfig', prompt: '', - default: [19,20,21], + default: [19, 20, 21], required: true, min_value: null, max_value: null, @@ -2569,7 +2569,7 @@ export const strategiesConfigData: IStrategiesConfigData = { name: 'candles_config', type: 'CandlesConfig', prompt: '', - default: [22,23,24], + default: [22, 23, 24], required: true, min_value: null, max_value: null, @@ -2763,7 +2763,7 @@ export const strategiesConfigData: IStrategiesConfigData = { name: 'candles_config', type: 'CandlesConfig', prompt: '', - default: [25,26,27], + default: [25, 26, 27], required: true, min_value: null, max_value: null, diff --git a/yarn.lock b/yarn.lock index 9c65eac..b9fdc5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2956,7 +2956,7 @@ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== -"@types/uuid@^10.0.0": +"@types/uuid@10.0.0": version "10.0.0" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==
- ${bot.portfolio.value} ({bot.portfolio.percentage}%) - ${bot.portfolio} {bot.accuracy}% {bot.sharpeRatio} {bot.apr}%