Trading bot grain (#33)

* Trading bot Grain

* Fix a bit more of the trading bot

* Advance on the tradingbot grain

* Fix build

* Fix db script

* Fix user login

* Fix a bit backtest

* Fix cooldown and backtest

* start fixing bot start

* Fix startup

* Setup local db

* Fix build and update candles and scenario

* Add bot registry

* Add reminder

* Updateing the grains

* fix bootstraping

* Save stats on tick

* Save bot data every tick

* Fix serialization

* fix save bot stats

* Fix get candles

* use dict instead of list for position

* Switch hashset to dict

* Fix a bit

* Fix bot launch and bot view

* add migrations

* Remove the tolist

* Add agent grain

* Save agent summary

* clean

* Add save bot

* Update get bots

* Add get bots

* Fix stop/restart

* fix Update config

* Update scanner table on new backtest saved

* Fix backtestRowDetails.tsx

* Fix agentIndex

* Update agentIndex

* Fix more things

* Update user cache

* Fix

* Fix account load/start/restart/run
This commit is contained in:
Oda
2025-08-04 23:07:06 +02:00
committed by GitHub
parent cd378587aa
commit 082ae8714b
215 changed files with 9562 additions and 14028 deletions

View File

@@ -1,10 +1,10 @@
import {create} from 'zustand'
import type {Scenario} from '../../generated/ManagingApi'
import type {LightScenario} from '../../generated/ManagingApi'
type CustomScenarioStore = {
setCustomScenario: (custom: Scenario | null) => void
scenario: Scenario | null
setCustomScenario: (custom: LightScenario | null) => void
scenario: LightScenario | null
}
export const useCustomScenario = create<CustomScenarioStore>((set) => ({

View File

@@ -1,8 +1,8 @@
import ArrowDownIcon from '@heroicons/react/solid/ArrowDownIcon'
import ArrowUpIcon from '@heroicons/react/solid/ArrowUpIcon'
import {Position, TradeDirection} from '../../../generated/ManagingApi'
import type {ICardPosition, ICardSignal, ICardText} from '../../../global/type'
import {LightSignal, Position, TradeDirection} from '../../../generated/ManagingApi'
import type {ICardPosition, ICardSignal, ICardText} from '../../../global/type.tsx'
function getItemTextHeaderClass() {
return 'text-xs opacity-50 '
@@ -38,7 +38,7 @@ export function CardPosition({ positions, positivePosition }: ICardPosition) {
display="initial"
></ArrowUpIcon>{' '}
{
positions.filter((p) => p.originDirection == TradeDirection.Short)
positions.filter((p: Position) => p.originDirection == TradeDirection.Short)
.length
}{' '}
<ArrowDownIcon
@@ -58,13 +58,13 @@ export function CardSignal({ signals }: ICardSignal) {
<div>
<p className={getItemTextHeaderClass()}>Signals</p>
<p className={getItemTextValueClass()}>
{signals.filter((p) => p.direction == TradeDirection.Long).length}{' '}
{signals.filter((p: LightSignal) => p.direction == TradeDirection.Long).length}{' '}
<ArrowUpIcon
width="10"
className="text-primary inline"
display="initial"
></ArrowUpIcon>{' '}
{signals.filter((p) => p.direction == TradeDirection.Short).length}{' '}
{signals.filter((p: LightSignal) => p.direction == TradeDirection.Short).length}{' '}
<ArrowDownIcon
width="10"
className="text-accent inline"

View File

@@ -9,6 +9,7 @@ import useApiUrlStore from '../../../app/store/apiStore'
import {UserClient} from '../../../generated/ManagingApi'
import Logo from '../../../assets/img/logo.png'
import {Loader} from '../../atoms'
import useCookie from '../../../hooks/useCookie'
const navigation = [
{ href: '/bots', name: 'Bots' },
@@ -47,13 +48,17 @@ const GlobalLoader = () => {
const PrivyWalletButton = () => {
const { login, logout, authenticated, user } = usePrivy()
const { apiUrl } = useApiUrlStore()
const { getCookie } = useCookie()
const api = new UserClient({}, apiUrl)
// Get JWT token from cookies
const jwtToken = getCookie('token')
// Fetch user information from the API
const { data: userInfo } = useQuery({
queryKey: ['user'],
queryFn: () => api.user_GetCurrentUser(),
enabled: authenticated, // Only fetch when authenticated
enabled: authenticated && !!jwtToken, // Only fetch when authenticated AND JWT token exists
})
if (!authenticated) {

View File

@@ -1,212 +1,116 @@
import {ArrowDownIcon, ArrowUpIcon, ChevronDownIcon, ChevronRightIcon, PlayIcon,} from '@heroicons/react/solid'
import React, {useEffect, useState} from 'react'
import {Hub} from '../../../app/providers/Hubs'
import useApiUrlStore from '../../../app/store/apiStore'
import type {Account, TradingBotResponse} from '../../../generated/ManagingApi'
import {AccountClient, BotClient, TradeDirection, TradeStatus,} from '../../../generated/ManagingApi'
import {IndicatorsDisplay, SelectColumnFilter, Table} from '../../mollecules'
import {ChevronDownIcon, ChevronRightIcon,} from '@heroicons/react/solid'
import React from 'react'
import useBots from '../../../hooks/useBots'
import {SelectColumnFilter, Table} from '../../mollecules'
import StatusBadge from '../StatusBadge/StatusBadge'
import Summary from '../Trading/Summary'
import BotRowDetails from './botRowDetails'
export default function ActiveBots() {
const [bots, setBots] = useState<TradingBotResponse[]>([])
const [accounts, setAccounts] = useState<Account[]>([])
const { apiUrl } = useApiUrlStore()
const {data: bots = []} = useBots({})
const columns = React.useMemo(
() => [
{
Cell: ({ row }: any) => (
// Use Cell to render an expander for each row.
// We can use the getToggleRowExpandedProps prop-getter
// to build the expander.
<span {...row.getToggleRowExpandedProps()}>
const columns = React.useMemo(
() => [
{
Cell: ({row}: any) => (
// Use Cell to render an expander for each row.
// We can use the getToggleRowExpandedProps prop-getter
// to build the expander.
<span {...row.getToggleRowExpandedProps()}>
{row.isExpanded ? (
<ChevronDownIcon></ChevronDownIcon>
<ChevronDownIcon></ChevronDownIcon>
) : (
<ChevronRightIcon></ChevronRightIcon>
<ChevronRightIcon></ChevronRightIcon>
)}
</span>
),
),
// Make sure it has an ID
Header: ({ getToggleAllRowsExpandedProps, isAllRowsExpanded }: any) => (
<span {...getToggleAllRowsExpandedProps()}>
// Make sure it has an ID
Header: ({getToggleAllRowsExpandedProps, isAllRowsExpanded}: any) => (
<span {...getToggleAllRowsExpandedProps()}>
{isAllRowsExpanded ? 'v' : '>'}
</span>
),
// Build our expander column
id: 'expander',
},
{
Cell: ({ cell }: any) => (
<>
<StatusBadge
status={cell.row.values.status}
isForWatchOnly={cell.row.values.isForWatchingOnly}
/>
</>
),
Header: 'Status',
accessor: 'status',
disableFilters: true,
disableSortBy: true,
search: false,
},
{
accessor: 'isForWatchingOnly',
disableFilters: true,
disableSortBy: true,
search: false,
},
{
Filter: SelectColumnFilter,
Header: 'Ticker',
accessor: 'config.ticker',
disableSortBy: true,
},
{
Header: 'Name',
accessor: 'config.name',
},
{
Filter: SelectColumnFilter,
Header: 'Timeframe',
accessor: 'config.timeframe',
disableSortBy: true,
},
{
Header: 'Indicators',
accessor: 'config.scenario.indicators',
Cell: ({cell}: any) => {
const bot = cell.row.original as TradingBotResponse;
const indicators = bot.config?.scenario?.indicators || [];
return (
<IndicatorsDisplay indicators={indicators} />
);
},
disableFilters: true,
disableSortBy: true,
},
{
Cell: ({ cell }: any) => (
<>
),
// Build our expander column
id: 'expander',
},
{
<>
{
cell.row.values.positions.filter(
(p: any) => p.originDirection == TradeDirection.Long
).length
}{' '}
<ArrowUpIcon
width="10"
className="text-primary inline"
display="initial"
></ArrowUpIcon>
{' | '}
{
cell.row.values.positions.filter(
(p: any) => p.originDirection == TradeDirection.Short
).length
}{' '}
<ArrowDownIcon
width="10"
className="text-accent inline"
display="initial"
></ArrowDownIcon>{' '}
{
cell.row.values.positions.filter(
(p: any) => p.status == TradeStatus.Filled
).length
}{' '}
<PlayIcon
width="10"
className="text-accent inline"
display="initial"
></PlayIcon>{' '}
</>
}
</>
Cell: ({cell}: any) => (
<>
<StatusBadge
status={cell.row.values.status}
isForWatchOnly={cell.row.values.isForWatchingOnly}
/>
</>
),
Header: 'Status',
accessor: 'status',
disableFilters: true,
disableSortBy: true,
search: false,
},
{
accessor: 'isForWatchingOnly',
disableFilters: true,
disableSortBy: true,
search: false,
},
{
Filter: SelectColumnFilter,
Header: 'Ticker',
accessor: 'ticker',
disableSortBy: true,
},
{
Header: 'Name',
accessor: 'name',
},
{
Filter: SelectColumnFilter,
Header: 'Timeframe',
accessor: 'timeframe',
disableSortBy: true,
},
{
Cell: ({cell}: any) => <>{cell.row.values.winRate} %</>,
Header: 'Winrate',
accessor: 'winRate',
disableFilters: true,
},
{
Cell: ({cell}: any) => <>{cell.row.values.profitAndLoss} $</>,
Header: 'PNL',
accessor: 'profitAndLoss',
disableFilters: true,
},
],
[]
)
const renderRowSubComponent = React.useCallback(
({row}: any) => (
<>
<BotRowDetails
bot={row.original}
></BotRowDetails>
</>
),
Header: 'Positions',
accessor: 'positions',
disableFilters: true,
},
{
Cell: ({ cell }) => <>{cell.row.values.winRate} %</>,
Header: 'Winrate',
accessor: 'winRate',
disableFilters: true,
},
{
Cell: ({ cell }) => <>{cell.row.values.profitAndLoss} $</>,
Header: 'PNL',
accessor: 'profitAndLoss',
disableFilters: true,
},
],
[]
)
[]
)
useEffect(() => {
setupHubConnection().then(() => {
if (bots.length == 0) {
const client = new BotClient({}, apiUrl)
client.bot_GetActiveBots().then((data) => {
setBots(data)
})
}
})
const client = new AccountClient({}, apiUrl)
client.account_GetAccounts().then((data) => {
setAccounts(data)
})
}, [])
const setupHubConnection = async () => {
const hub = new Hub('bothub', apiUrl).hub
hub.on('BotsSubscription', (data: TradingBotResponse[]) => {
// eslint-disable-next-line no-console
console.log(
'bot List',
bots.map((bot: TradingBotResponse) => {
return bot.config.name
})
)
setBots(data)
})
return hub
}
const renderRowSubComponent = React.useCallback(
({ row }: any) => (
<>
<BotRowDetails
bot={row.original}
></BotRowDetails>
</>
),
[]
)
return (
<>
<div className="flex flex-wrap">
<Summary bots={bots} accounts={accounts}></Summary>
</div>
<div className="flex flex-wrap">
<Table
columns={columns}
data={bots}
renderRowSubCompontent={renderRowSubComponent}
hiddenColumns={['isForWatchingOnly']}
/>
</div>
</>
)
return (
<>
<div className="flex flex-wrap">
<Summary bots={bots}></Summary>
</div>
<div className="flex flex-wrap">
<Table
columns={columns}
data={bots}
renderRowSubCompontent={renderRowSubComponent}
hiddenColumns={['isForWatchingOnly']}
/>
</div>
</>
)
}

View File

@@ -6,7 +6,6 @@ import useApiUrlStore from '../../../app/store/apiStore'
import {
AccountClient,
BacktestClient,
BotType,
DataClient,
MoneyManagement,
MoneyManagementClient,
@@ -379,16 +378,6 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
</option>
</select>
</FormInput>
<FormInput label="Bot Type" htmlFor="botType">
<select className="select select-bordered w-full" {...register('botType')}>
{[BotType.ScalpingBot, BotType.FlippingBot].map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</FormInput>
</div>
{/* Losing streak info */}

View File

@@ -6,6 +6,7 @@ import {
DataClient,
GetCandlesWithIndicatorsRequest,
IndicatorType,
LightSignal,
Position,
SignalType
} from '../../../generated/ManagingApi'
@@ -42,7 +43,7 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
if (backtest.candles && backtest.candles.length > 0) {
return {
candles: backtest.candles,
indicatorsValues: backtest.indicatorsValues || {}
indicatorsValues: {} // Default empty object since Backtest doesn't have indicatorsValues
};
}
@@ -83,17 +84,17 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
// Use the data from query or fallback to backtest data
const candles = candlesData?.candles || currentBacktest.candles || [];
const indicatorsValues = candlesData?.indicatorsValues || currentBacktest.indicatorsValues || {};
const indicatorsValues = candlesData?.indicatorsValues || {};
// Only destructure these properties if we have full backtest data
const positions = fullBacktestData?.positions || [];
const walletBalances = fullBacktestData?.walletBalances || [];
const signals = fullBacktestData?.signals || [];
const statistics = fullBacktestData?.statistics;
// Convert positions and signals objects to arrays
const positionsArray: Position[] = Object.values(currentBacktest.positions || {});
const signalsArray: LightSignal[] = Object.values(currentBacktest.signals || {});
const walletBalances = currentBacktest.walletBalances || [];
const statistics = currentBacktest.statistics;
const config = currentBacktest.config;
// Helper function to calculate position open time in hours
const calculateOpenTimeInHours = (position: Position) => {
const calculateOpenTimeInHours = (position: Position): number => {
const openDate = new Date(position.Open.date);
let closeDate: Date | null = null;
@@ -116,15 +117,15 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
};
// Calculate average open time for winning positions
const getAverageOpenTimeWinning = () => {
const winningPositions = positions.filter((p) => {
const getAverageOpenTimeWinning = (): string => {
const winningPositions = positionsArray.filter((p: Position) => {
const realized = p.ProfitAndLoss?.realized ?? 0;
return realized > 0;
});
if (winningPositions.length === 0) return "0.00";
const totalHours = winningPositions.reduce((sum, position) => {
const totalHours = winningPositions.reduce((sum: number, position: Position) => {
return sum + calculateOpenTimeInHours(position);
}, 0);
@@ -133,15 +134,15 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
};
// Calculate average open time for losing positions
const getAverageOpenTimeLosing = () => {
const losingPositions = positions.filter((p) => {
const getAverageOpenTimeLosing = (): string => {
const losingPositions = positionsArray.filter((p: Position) => {
const realized = p.ProfitAndLoss?.realized ?? 0;
return realized <= 0;
});
if (losingPositions.length === 0) return "0.00";
const totalHours = losingPositions.reduce((sum, position) => {
const totalHours = losingPositions.reduce((sum: number, position: Position) => {
return sum + calculateOpenTimeInHours(position);
}, 0);
@@ -150,25 +151,25 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
};
// Calculate maximum open time for winning positions
const getMaxOpenTimeWinning = () => {
const winningPositions = positions.filter((p) => {
const getMaxOpenTimeWinning = (): string => {
const winningPositions = positionsArray.filter((p: Position) => {
const realized = p.ProfitAndLoss?.realized ?? 0;
return realized > 0;
});
if (winningPositions.length === 0) return "0.00";
const openTimes = winningPositions.map(position => calculateOpenTimeInHours(position));
const openTimes = winningPositions.map((position: Position) => calculateOpenTimeInHours(position));
const maxHours = Math.max(...openTimes);
return maxHours.toFixed(2);
};
// Calculate median opening time for all positions
const getMedianOpenTime = () => {
if (positions.length === 0) return "0.00";
const getMedianOpenTime = (): string => {
if (positionsArray.length === 0) return "0.00";
const openTimes = positions.map(position => calculateOpenTimeInHours(position));
const sortedTimes = openTimes.sort((a, b) => a - b);
const openTimes = positionsArray.map((position: Position) => calculateOpenTimeInHours(position));
const sortedTimes = openTimes.sort((a: number, b: number) => a - b);
const mid = Math.floor(sortedTimes.length / 2);
const median = sortedTimes.length % 2 === 0
@@ -179,10 +180,10 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
};
// Calculate total volume traded with leverage
const getTotalVolumeTraded = () => {
const getTotalVolumeTraded = (): number => {
let totalVolume = 0;
positions.forEach((position) => {
positionsArray.forEach((position: Position) => {
// Calculate volume for open trade
const openLeverage = position.Open.leverage || 1;
const openVolume = position.Open.quantity * position.Open.price * openLeverage;
@@ -211,7 +212,7 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
};
// Calculate estimated UI fee (0.02% of total volume)
const getEstimatedUIFee = () => {
const getEstimatedUIFee = (): number => {
const totalVolume = getTotalVolumeTraded();
const uiFeePercentage = 0.001; // 0.1%
return totalVolume * uiFeePercentage;
@@ -219,14 +220,14 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
// Calculate recommended cooldown based on positions that fail after a win
const getCooldownRecommendations = () => {
if (positions?.length < 2 || !candles || candles?.length < 2) {
if (positionsArray.length < 2 || !candles || candles.length < 2) {
return { percentile75: "0", average: "0", median: "0" };
}
// Determine candle timeframe in milliseconds
const candleTimeframeMs = new Date(candles[1].date).getTime() - new Date(candles[0].date).getTime();
const sortedPositions = [...positions].sort((a, b) => {
const sortedPositions = [...positionsArray].sort((a: Position, b: Position) => {
const dateA = new Date(a.Open.date).getTime();
const dateB = new Date(b.Open.date).getTime();
return dateA - dateB;
@@ -271,12 +272,12 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
}
// Calculate the 75th percentile
const sortedGaps = [...failAfterWinGaps].sort((a, b) => a - b);
const sortedGaps = [...failAfterWinGaps].sort((a: number, b: number) => a - b);
const percentile75Index = Math.floor(sortedGaps.length * 0.75);
const percentile75 = sortedGaps[percentile75Index] || 0;
// Calculate the average
const sum = failAfterWinGaps.reduce((acc, gap) => acc + gap, 0);
const sum = failAfterWinGaps.reduce((acc: number, gap: number) => acc + gap, 0);
const average = sum / failAfterWinGaps.length;
// Calculate the median
@@ -295,13 +296,13 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
const cooldownRecommendations = getCooldownRecommendations();
// Calculate average trades per day
const getAverageTradesPerDay = () => {
if (positions.length === 0) return "0.00";
const getAverageTradesPerDay = (): string => {
if (positionsArray.length === 0) return "0.00";
// Get all trade dates and sort them
const tradeDates = positions.map(position => new Date(position.Open.date)).sort((a, b) => a.getTime() - b.getTime());
const tradeDates = positionsArray.map((position: Position) => new Date(position.Open.date)).sort((a: Date, b: Date) => a.getTime() - b.getTime());
if (tradeDates.length < 2) return positions.length.toString();
if (tradeDates.length < 2) return positionsArray.length.toString();
// Calculate the date range in days
const firstTradeDate = tradeDates[0];
@@ -309,7 +310,7 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
const diffInMs = lastTradeDate.getTime() - firstTradeDate.getTime();
const diffInDays = Math.max(1, diffInMs / (1000 * 60 * 60 * 24)); // Ensure at least 1 day
const averageTradesPerDay = positions.length / diffInDays;
const averageTradesPerDay = positionsArray.length / diffInDays;
return averageTradesPerDay.toFixed(2);
};
@@ -327,19 +328,19 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
<div className="grid grid-cols-4 p-5">
<CardPosition
positivePosition={true}
positions={positions.filter((p) => {
positions={positionsArray.filter((p: Position) => {
const realized = p.ProfitAndLoss?.realized ?? 0
return realized > 0 ? p : null
})}
></CardPosition>
<CardPosition
positivePosition={false}
positions={positions.filter((p) => {
positions={positionsArray.filter((p: Position) => {
const realized = p.ProfitAndLoss?.realized ?? 0
return realized <= 0 ? p : null
})}
></CardPosition>
<CardPositionItem positions={positions}></CardPositionItem>
<CardPositionItem positions={positionsArray}></CardPositionItem>
<CardText
title="Max Drowdown"
content={
@@ -431,10 +432,10 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
<figure className="w-full">
<TradeChart
candles={candles}
positions={positions}
positions={positionsArray}
walletBalances={walletBalances}
indicatorsValues={indicatorsValues}
signals={signals}
signals={signalsArray}
height={1000}
></TradeChart>
</figure>

View File

@@ -135,11 +135,12 @@ interface BacktestTableProps {
displaySummary?: boolean
onSortChange?: (sortBy: string, sortOrder: 'asc' | 'desc') => void
currentSort?: { sortBy: string; sortOrder: 'asc' | 'desc' }
onBacktestDeleted?: () => void // Callback when a backtest is deleted
}
const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortChange, currentSort}) => {
const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortChange, currentSort, onBacktestDeleted}) => {
const [rows, setRows] = useState<LightBacktestResponse[]>([])
const {apiUrl} = useApiUrlStore()
const {removeBacktest} = useBacktestStore()
@@ -213,6 +214,10 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortCh
t.update('success', 'Backtest deleted')
// Remove the deleted backtest from the store
removeBacktest(id)
// Call the callback to invalidate queries
if (onBacktestDeleted) {
onBacktestDeleted()
}
})
.catch((err) => {
t.update('error', err)

View File

@@ -1,12 +1,12 @@
import React, {useEffect, useState} from 'react'
import type {Indicator, Scenario} from '../../../generated/ManagingApi'
import type {LightIndicator, LightScenario} from '../../../generated/ManagingApi'
import {IndicatorType} from '../../../generated/ManagingApi'
import FormInput from '../../mollecules/FormInput/FormInput'
import {useCustomScenario} from '../../../app/store/customScenario'
type ICustomScenario = {
onCreateScenario: (scenario: Scenario) => void
onCreateScenario: (scenario: LightScenario) => void
showCustomScenario: boolean
}
@@ -18,7 +18,7 @@ const CustomScenario: React.FC<ICustomScenario> = ({
const [name, setName] = useState<string>(scenario?.name || 'Custom Scenario')
const [loopbackPeriod, setLoopbackPeriod] = useState<number>(scenario?.loopbackPeriod || 1)
const [indicators, setIndicators] = useState<Indicator[]>(scenario?.indicators || [])
const [indicators, setIndicators] = useState<LightIndicator[]>(scenario?.indicators || [])
// Available indicator types with their required parameters
const indicatorTypes = Object.values(IndicatorType).map(type => {
@@ -124,7 +124,7 @@ const CustomScenario: React.FC<ICustomScenario> = ({
});
const addIndicator = () => {
const newIndicator: Indicator = {
const newIndicator: LightIndicator = {
name: `Indicator ${indicators.length + 1}`,
type: indicatorTypes[0].type,
period: 14,
@@ -154,7 +154,7 @@ const CustomScenario: React.FC<ICustomScenario> = ({
}
const handleCreateScenario = () => {
const scenario: Scenario = {
const scenario: LightScenario = {
name,
indicators,
loopbackPeriod,
@@ -262,7 +262,7 @@ const CustomScenario: React.FC<ICustomScenario> = ({
{getRequiredParams(indicator.type || indicatorTypes[0].type).map((param) => (
<FormInput key={param} label={param.charAt(0).toUpperCase() + param.slice(1)} htmlFor={`${param}-${index}`} inline={false}>
<input
value={indicator[param as keyof Indicator] as number || ''}
value={indicator[param as keyof LightIndicator] as number || ''}
onChange={(e) => updateIndicator(index, param, param.includes('multiplier') ? parseFloat(e.target.value) : parseInt(e.target.value))}
type='number'
step={param.includes('multiplier') ? '0.1' : '1'}

View File

@@ -1,200 +1,124 @@
import {ArrowDownIcon, ArrowUpIcon} from '@heroicons/react/solid'
import React, {useEffect, useState} from 'react'
import useApiUrlStore from '../../../app/store/apiStore'
import type {PlatformSummaryViewModel, TradingBot} from '../../../generated/ManagingApi'
import {DataClient, PositionStatus, TradeDirection} from '../../../generated/ManagingApi'
import type {IAccountBalanceProps} from '../../../global/type'
import type {PlatformSummaryViewModel, TradingBotResponse} from '../../../generated/ManagingApi'
import {DataClient} from '../../../generated/ManagingApi'
// Time filter options matching backend
const TIME_FILTERS = ['24H', '3D', '1W', '1M', '1Y', 'Total']
function GetGlobalWinrate(bots: TradingBot[]) {
if (bots == null || bots == undefined || bots.length == 0) {
return 0
}
let totalPositions = 0
let winningPosition = 0
bots.forEach((bot) => {
totalPositions += bot.positions.filter(
(p) => p.status != PositionStatus.New
).length
winningPosition += bot.positions.filter((p) => {
const realized = p.profitAndLoss?.realized ?? 0
return realized > 0 &&
(p.status == PositionStatus.Finished ||
p.status == PositionStatus.Flipped)
? p
: null
}).length
})
if (totalPositions == 0) return 0
return (winningPosition * 100) / totalPositions
}
function GetPositionCount(
bots: TradingBot[],
direction: TradeDirection,
status: PositionStatus
) {
let totalPositions = 0
if (bots == null || bots == undefined) {
return 0
}
bots.forEach((bot) => {
totalPositions += bot.positions.filter(
(p) => p.status == status && p.originDirection == direction
).length
})
return totalPositions
}
const Summary: React.FC<IAccountBalanceProps> = ({ bots }) => {
const [globalPnl, setGlobalPnl] = useState<number>(0)
const [globalWinrate, setGlobalWinrate] = useState<number>(0)
const [selectedTimeFilter, setSelectedTimeFilter] = useState<string>('Total')
const [platformStats, setPlatformStats] = useState<PlatformSummaryViewModel | null>(null)
const [isLoading, setIsLoading] = useState<boolean>(false)
const [openLong, setLong] = useState<number>(0)
const [openShort, setShort] = useState<number>(0)
const [closedLong, setClosedLong] = useState<number>(0)
const [closedShort, setClosedShort] = useState<number>(0)
const { apiUrl } = useApiUrlStore()
useEffect(() => {
if (bots) {
const pnl = bots.reduce((acc, bot) => {
return acc + bot.profitAndLoss
}, 0)
setGlobalPnl(pnl)
setGlobalWinrate(GetGlobalWinrate(bots))
setLong(
GetPositionCount(bots, TradeDirection.Long, PositionStatus.Filled)
)
setShort(
GetPositionCount(bots, TradeDirection.Short, PositionStatus.Filled)
)
setClosedLong(
GetPositionCount(bots, TradeDirection.Long, PositionStatus.Finished) +
GetPositionCount(bots, TradeDirection.Long, PositionStatus.Flipped)
)
setClosedShort(
GetPositionCount(bots, TradeDirection.Short, PositionStatus.Finished) +
GetPositionCount(bots, TradeDirection.Short, PositionStatus.Flipped)
)
}
}, [bots])
// Fetch platform summary data
useEffect(() => {
const fetchPlatformStats = async () => {
setIsLoading(true)
try {
const dataClient = new DataClient({}, apiUrl)
const data = await dataClient.data_GetPlatformSummary(selectedTimeFilter)
setPlatformStats(data)
} catch (error) {
console.error('Error fetching platform stats:', error)
} finally {
setIsLoading(false)
}
function GetGlobalWinrate(bots: TradingBotResponse[]) {
if (bots == null || bots == undefined || bots.length == 0) {
return 0
}
fetchPlatformStats()
}, [apiUrl, selectedTimeFilter])
return bots.reduce((acc, bot) => {
return acc + bot.winRate
}, 0) / bots.length
}
return (
<div className="p-4">
<div className="mb-4 flex justify-between items-center">
<h2 className="text-xl font-bold">Platform Overview</h2>
<div className="join">
{TIME_FILTERS.map((filter) => (
<button
key={filter}
className={`btn btn-sm join-item ${selectedTimeFilter === filter ? 'btn-primary' : 'btn-ghost'}`}
onClick={() => setSelectedTimeFilter(filter)}
>
{filter}
</button>
))}
</div>
</div>
const Summary: React.FC<{ bots: TradingBotResponse[] }> = ({bots}) => {
const [globalPnl, setGlobalPnl] = useState<number>(0)
const [globalWinrate, setGlobalWinrate] = useState<number>(0)
const [selectedTimeFilter, setSelectedTimeFilter] = useState<string>('Total')
const [platformStats, setPlatformStats] = useState<PlatformSummaryViewModel | null>(null)
const [isLoading, setIsLoading] = useState<boolean>(false)
<div className="stats bg-primary text-primary-content mb-4">
<div className="stat">
<div className="stat-title">Total Agents</div>
<div className="stat-value">{platformStats?.totalAgents ?? 0}</div>
</div>
const {apiUrl} = useApiUrlStore()
<div className="stat">
<div className="stat-title">Active Strategies</div>
<div className="stat-value">{platformStats?.totalActiveStrategies ?? 0}</div>
</div>
useEffect(() => {
if (bots) {
const pnl = bots.reduce((acc, bot) => {
return acc + bot.profitAndLoss
}, 0)
setGlobalPnl(pnl)
setGlobalWinrate(GetGlobalWinrate(bots))
<div className="stat">
<div className="stat-title">Total Platform PnL</div>
<div className="stat-value">{(platformStats?.totalPlatformPnL ?? 0).toFixed(2)} $</div>
</div>
}
}, [bots])
<div className="stat">
<div className="stat-title">Volume (Total)</div>
<div className="stat-value">{(platformStats?.totalPlatformVolume ?? 0).toFixed(2)} $</div>
</div>
// Fetch platform summary data
useEffect(() => {
const fetchPlatformStats = async () => {
setIsLoading(true)
try {
const dataClient = new DataClient({}, apiUrl)
const data = await dataClient.data_GetPlatformSummary(selectedTimeFilter)
setPlatformStats(data)
} catch (error) {
console.error('Error fetching platform stats:', error)
} finally {
setIsLoading(false)
}
}
<div className="stat">
<div className="stat-title">Volume (24h)</div>
<div className="stat-value">{(platformStats?.totalPlatformVolumeLast24h ?? 0).toFixed(2)} $</div>
</div>
</div>
fetchPlatformStats()
}, [apiUrl, selectedTimeFilter])
<div className="stats bg-primary text-primary-content">
<div className="stat">
<div className="stat-title">Bots running</div>
<div className="stat-value">{bots.length}</div>
</div>
return (
<div className="p-4">
<div className="mb-4 flex justify-between items-center">
<h2 className="text-xl font-bold">Platform Overview</h2>
<div className="join">
{TIME_FILTERS.map((filter) => (
<button
key={filter}
className={`btn btn-sm join-item ${selectedTimeFilter === filter ? 'btn-primary' : 'btn-ghost'}`}
onClick={() => setSelectedTimeFilter(filter)}
>
{filter}
</button>
))}
</div>
</div>
<div className="stat">
<div className="stat-title">Total Profit</div>
<div className="stat-value">{globalPnl.toFixed(4)} $</div>
</div>
<div className="stats bg-primary text-primary-content mb-4">
<div className="stat">
<div className="stat-title">Total Agents</div>
<div className="stat-value">{platformStats?.totalAgents ?? 0}</div>
</div>
<div className="stat">
<div className="stat-title">Global Winrate</div>
<div className="stat-value">
{globalWinrate ? globalWinrate.toFixed(2) : 0} %
</div>
</div>
<div className="stat">
<div className="stat-title">Active Strategies</div>
<div className="stat-value">{platformStats?.totalActiveStrategies ?? 0}</div>
</div>
<div className="stat">
<div className="stat-title">Positions Openend</div>
<div className="stat-value">
{openLong} <ArrowUpIcon width={20} className="inline"></ArrowUpIcon>{' '}
{openShort}{' '}
<ArrowDownIcon width={20} className="inline"></ArrowDownIcon>{' '}
</div>
<div className="stat">
<div className="stat-title">Total Platform PnL</div>
<div className="stat-value">{(platformStats?.totalPlatformPnL ?? 0).toFixed(2)} $</div>
</div>
<div className="stat">
<div className="stat-title">Volume (Total)</div>
<div className="stat-value">{(platformStats?.totalPlatformVolume ?? 0).toFixed(2)} $</div>
</div>
<div className="stat">
<div className="stat-title">Volume (24h)</div>
<div className="stat-value">{(platformStats?.totalPlatformVolumeLast24h ?? 0).toFixed(2)} $</div>
</div>
</div>
<div className="stats bg-primary text-primary-content">
<div className="stat">
<div className="stat-title">Bots running</div>
<div className="stat-value">{bots.length}</div>
</div>
<div className="stat">
<div className="stat-title">Total Profit</div>
<div className="stat-value">{globalPnl.toFixed(4)} $</div>
</div>
<div className="stat">
<div className="stat-title">Global Winrate</div>
<div className="stat-value">
{globalWinrate ? globalWinrate.toFixed(2) : 0} %
</div>
</div>
</div>
</div>
<div className="stat">
<div className="stat-title">Positions Closed</div>
<div className="stat-value">
{closedLong}{' '}
<ArrowUpIcon width={20} className="inline"></ArrowUpIcon>{' '}
{closedShort}{' '}
<ArrowDownIcon width={20} className="inline"></ArrowDownIcon>{' '}
</div>
</div>
</div>
</div>
)
)
}
export default Summary

View File

@@ -12,12 +12,12 @@ import {
BotClient,
DataClient,
LightBacktestResponse,
LightScenario,
MoneyManagement,
MoneyManagementClient,
RiskManagement,
RiskToleranceLevel,
RunBacktestRequest,
Scenario,
ScenarioClient,
ScenarioRequest,
SignalType,
@@ -41,6 +41,7 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
setBacktests,
backtest,
existingBot,
onBacktestComplete,
}) => {
// Default dates for backtests
const defaultStartDate = new Date();
@@ -128,7 +129,7 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
const [selectedMoneyManagement, setSelectedMoneyManagement] = useState<string>();
const [showCustomMoneyManagement, setShowCustomMoneyManagement] = useState(false);
const [customScenario, setCustomScenario] = useState<Scenario | undefined>(undefined);
const [customScenario, setCustomScenario] = useState<LightScenario | undefined>(undefined);
const [selectedScenario, setSelectedScenario] = useState<string>();
const [showCustomScenario, setShowCustomScenario] = useState(false);
@@ -334,10 +335,11 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
}
// Handle scenario - check if we have scenario data or just a name reference
if ((config as any).scenario) {
if (config.scenario) {
setShowCustomScenario(true);
setCustomScenario((config as any).scenario);
setGlobalCustomScenario((config as any).scenario); // Also update global store for prefilling
// Use LightScenario directly since CustomScenario now supports it
setCustomScenario(config.scenario);
setGlobalCustomScenario(config.scenario); // Also update global store for prefilling
setSelectedScenario('custom'); // Set dropdown to show "custom"
} else if (config.scenarioName) {
setValue('scenarioName', config.scenarioName);
@@ -556,7 +558,7 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
};
// Helper function to convert custom scenario to ScenarioRequest format
const convertScenarioToRequest = (scenario: Scenario): ScenarioRequest => {
const convertScenarioToRequest = (scenario: LightScenario): ScenarioRequest => {
return {
name: scenario.name || 'Custom Scenario',
loopbackPeriod: scenario.loopbackPeriod || null,
@@ -605,9 +607,13 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
}
};
// Bot submission handler
const handleBotSubmission = async (form: IUnifiedTradingConfigInput) => {
const t = new Toast(mode === 'createBot' ? 'Creating bot...' : 'Updating bot...');
// Unified bot submission handler
const handleBotSubmission = async (form: IUnifiedTradingConfigInput, saveOnly: boolean = false) => {
const t = new Toast(
mode === 'createBot'
? (saveOnly ? 'Saving bot configuration...' : 'Creating bot...')
: 'Updating bot...'
);
try {
// Create money management object
@@ -657,8 +663,14 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
const request: StartBotRequest = {
config: tradingBotConfigRequest,
};
await botClient.bot_Start(request);
t.update('success', 'Bot created successfully!');
if (saveOnly) {
await botClient.bot_Save(request);
t.update('success', 'Bot configuration saved successfully!');
} else {
await botClient.bot_Start(request);
t.update('success', 'Bot created successfully!');
}
} else {
const request: UpdateBotConfigRequest = {
identifier: existingBot!.identifier,
@@ -715,6 +727,11 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
t.update('success', `${ticker} Backtest Succeeded`);
addBacktest(backtest as unknown as LightBacktestResponse);
// Call the callback to notify parent component that backtest is completed
if (onBacktestComplete) {
onBacktestComplete();
}
} catch (err: any) {
@@ -1595,9 +1612,24 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
>
Cancel
</button>
<button type="submit" className="btn btn-primary">
{mode === 'backtest' ? 'Run Backtest' : mode === 'createBot' ? 'Create Bot' : 'Update Bot'}
</button>
{mode === 'createBot' ? (
<>
<button
type="button"
className="btn btn-secondary"
onClick={handleSubmit((form) => handleBotSubmission(form, true))}
>
Save
</button>
<button type="submit" className="btn btn-primary">
Create Bot
</button>
</>
) : (
<button type="submit" className="btn btn-primary">
{mode === 'backtest' ? 'Run Backtest' : 'Update Bot'}
</button>
)}
</div>
</Modal>
);

View File

@@ -1161,9 +1161,50 @@ export class BotClient extends AuthorizedApiBase {
return Promise.resolve<string>(null as any);
}
bot_Stop(identifier: string | null | undefined): Promise<string> {
bot_Save(request: SaveBotRequest): Promise<string> {
let url_ = this.baseUrl + "/Bot/Save";
url_ = url_.replace(/[?&]$/, "");
const content_ = JSON.stringify(request);
let options_: RequestInit = {
body: content_,
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processBot_Save(_response);
});
}
protected processBot_Save(response: Response): Promise<string> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as string;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<string>(null as any);
}
bot_Stop(identifier: string | undefined): Promise<BotStatus> {
let url_ = this.baseUrl + "/Bot/Stop?";
if (identifier !== undefined && identifier !== null)
if (identifier === null)
throw new Error("The parameter 'identifier' cannot be null.");
else if (identifier !== undefined)
url_ += "identifier=" + encodeURIComponent("" + identifier) + "&";
url_ = url_.replace(/[?&]$/, "");
@@ -1181,13 +1222,13 @@ export class BotClient extends AuthorizedApiBase {
});
}
protected processBot_Stop(response: Response): Promise<string> {
protected processBot_Stop(response: Response): Promise<BotStatus> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as string;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BotStatus;
return result200;
});
} else if (status !== 200 && status !== 204) {
@@ -1195,12 +1236,14 @@ export class BotClient extends AuthorizedApiBase {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<string>(null as any);
return Promise.resolve<BotStatus>(null as any);
}
bot_Delete(identifier: string | null | undefined): Promise<boolean> {
bot_Delete(identifier: string | undefined): Promise<boolean> {
let url_ = this.baseUrl + "/Bot/Delete?";
if (identifier !== undefined && identifier !== null)
if (identifier === null)
throw new Error("The parameter 'identifier' cannot be null.");
else if (identifier !== undefined)
url_ += "identifier=" + encodeURIComponent("" + identifier) + "&";
url_ = url_.replace(/[?&]$/, "");
@@ -1235,48 +1278,11 @@ export class BotClient extends AuthorizedApiBase {
return Promise.resolve<boolean>(null as any);
}
bot_StopAll(): Promise<string> {
let url_ = this.baseUrl + "/Bot/stop-all";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "POST",
headers: {
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processBot_StopAll(_response);
});
}
protected processBot_StopAll(response: Response): Promise<string> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as string;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<string>(null as any);
}
bot_Restart(botType: BotType | undefined, identifier: string | null | undefined): Promise<string> {
bot_Restart(identifier: string | undefined): Promise<string> {
let url_ = this.baseUrl + "/Bot/Restart?";
if (botType === null)
throw new Error("The parameter 'botType' cannot be null.");
else if (botType !== undefined)
url_ += "botType=" + encodeURIComponent("" + botType) + "&";
if (identifier !== undefined && identifier !== null)
if (identifier === null)
throw new Error("The parameter 'identifier' cannot be null.");
else if (identifier !== undefined)
url_ += "identifier=" + encodeURIComponent("" + identifier) + "&";
url_ = url_.replace(/[?&]$/, "");
@@ -1311,78 +1317,6 @@ export class BotClient extends AuthorizedApiBase {
return Promise.resolve<string>(null as any);
}
bot_RestartAll(): Promise<string> {
let url_ = this.baseUrl + "/Bot/restart-all";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "POST",
headers: {
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processBot_RestartAll(_response);
});
}
protected processBot_RestartAll(response: Response): Promise<string> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as string;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<string>(null as any);
}
bot_ToggleIsForWatching(identifier: string | null | undefined): Promise<string> {
let url_ = this.baseUrl + "/Bot/ToggleIsForWatching?";
if (identifier !== undefined && identifier !== null)
url_ += "identifier=" + encodeURIComponent("" + identifier) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processBot_ToggleIsForWatching(_response);
});
}
protected processBot_ToggleIsForWatching(response: Response): Promise<string> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as string;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<string>(null as any);
}
bot_GetActiveBots(): Promise<TradingBotResponse[]> {
let url_ = this.baseUrl + "/Bot";
url_ = url_.replace(/[?&]$/, "");
@@ -1457,6 +1391,134 @@ export class BotClient extends AuthorizedApiBase {
return Promise.resolve<MoneyManagement>(null as any);
}
bot_GetBotsByStatus(status: BotStatus): Promise<TradingBotResponse[]> {
let url_ = this.baseUrl + "/Bot/ByStatus/{status}";
if (status === undefined || status === null)
throw new Error("The parameter 'status' must be defined.");
url_ = url_.replace("{status}", encodeURIComponent("" + status));
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processBot_GetBotsByStatus(_response);
});
}
protected processBot_GetBotsByStatus(response: Response): Promise<TradingBotResponse[]> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as TradingBotResponse[];
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<TradingBotResponse[]>(null as any);
}
bot_GetMySavedBots(): Promise<TradingBotResponse[]> {
let url_ = this.baseUrl + "/Bot/GetMySavedBots";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processBot_GetMySavedBots(_response);
});
}
protected processBot_GetMySavedBots(response: Response): Promise<TradingBotResponse[]> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as TradingBotResponse[];
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<TradingBotResponse[]>(null as any);
}
bot_GetBotsPaginated(pageNumber: number | undefined, pageSize: number | undefined, status: BotStatus | null | undefined, name: string | null | undefined, ticker: string | null | undefined, agentName: string | null | undefined, sortBy: string | null | undefined, sortDirection: string | null | undefined): Promise<PaginatedResponseOfTradingBotResponse> {
let url_ = this.baseUrl + "/Bot/Paginated?";
if (pageNumber === null)
throw new Error("The parameter 'pageNumber' cannot be null.");
else if (pageNumber !== undefined)
url_ += "pageNumber=" + encodeURIComponent("" + pageNumber) + "&";
if (pageSize === null)
throw new Error("The parameter 'pageSize' cannot be null.");
else if (pageSize !== undefined)
url_ += "pageSize=" + encodeURIComponent("" + pageSize) + "&";
if (status !== undefined && status !== null)
url_ += "status=" + encodeURIComponent("" + status) + "&";
if (name !== undefined && name !== null)
url_ += "name=" + encodeURIComponent("" + name) + "&";
if (ticker !== undefined && ticker !== null)
url_ += "ticker=" + encodeURIComponent("" + ticker) + "&";
if (agentName !== undefined && agentName !== null)
url_ += "agentName=" + encodeURIComponent("" + agentName) + "&";
if (sortBy !== undefined && sortBy !== null)
url_ += "sortBy=" + encodeURIComponent("" + sortBy) + "&";
if (sortDirection !== undefined && sortDirection !== null)
url_ += "sortDirection=" + encodeURIComponent("" + sortDirection) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processBot_GetBotsPaginated(_response);
});
}
protected processBot_GetBotsPaginated(response: Response): Promise<PaginatedResponseOfTradingBotResponse> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as PaginatedResponseOfTradingBotResponse;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<PaginatedResponseOfTradingBotResponse>(null as any);
}
bot_OpenPositionManually(request: OpenPositionManuallyRequest): Promise<Position> {
let url_ = this.baseUrl + "/Bot/OpenPosition";
url_ = url_.replace(/[?&]$/, "");
@@ -1535,6 +1597,44 @@ export class BotClient extends AuthorizedApiBase {
return Promise.resolve<Position>(null as any);
}
bot_GetBotConfig(identifier: string): Promise<TradingBotConfig> {
let url_ = this.baseUrl + "/Bot/GetConfig/{identifier}";
if (identifier === undefined || identifier === null)
throw new Error("The parameter 'identifier' must be defined.");
url_ = url_.replace("{identifier}", encodeURIComponent("" + identifier));
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processBot_GetBotConfig(_response);
});
}
protected processBot_GetBotConfig(response: Response): Promise<TradingBotConfig> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as TradingBotConfig;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<TradingBotConfig>(null as any);
}
bot_UpdateBotConfig(request: UpdateBotConfigRequest): Promise<string> {
let url_ = this.baseUrl + "/Bot/UpdateConfig";
url_ = url_.replace(/[?&]$/, "");
@@ -1882,47 +1982,8 @@ export class DataClient extends AuthorizedApiBase {
return Promise.resolve<PlatformSummaryViewModel>(null as any);
}
data_GetAgentIndex(timeFilter: string | null | undefined): Promise<AgentIndexViewModel> {
let url_ = this.baseUrl + "/Data/GetAgentIndex?";
if (timeFilter !== undefined && timeFilter !== null)
url_ += "timeFilter=" + encodeURIComponent("" + timeFilter) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processData_GetAgentIndex(_response);
});
}
protected processData_GetAgentIndex(response: Response): Promise<AgentIndexViewModel> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as AgentIndexViewModel;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<AgentIndexViewModel>(null as any);
}
data_GetAgentIndexPaginated(timeFilter: string | null | undefined, page: number | undefined, pageSize: number | undefined, sortBy: string | null | undefined, sortOrder: string | null | undefined, agentNames: string | null | undefined): Promise<PaginatedAgentIndexResponse> {
data_GetAgentIndexPaginated(page: number | undefined, pageSize: number | undefined, sortBy: SortableFields | undefined, sortOrder: string | null | undefined, agentNames: string | null | undefined): Promise<PaginatedAgentIndexResponse> {
let url_ = this.baseUrl + "/Data/GetAgentIndexPaginated?";
if (timeFilter !== undefined && timeFilter !== null)
url_ += "timeFilter=" + encodeURIComponent("" + timeFilter) + "&";
if (page === null)
throw new Error("The parameter 'page' cannot be null.");
else if (page !== undefined)
@@ -1931,7 +1992,9 @@ export class DataClient extends AuthorizedApiBase {
throw new Error("The parameter 'pageSize' cannot be null.");
else if (pageSize !== undefined)
url_ += "pageSize=" + encodeURIComponent("" + pageSize) + "&";
if (sortBy !== undefined && sortBy !== null)
if (sortBy === null)
throw new Error("The parameter 'sortBy' cannot be null.");
else if (sortBy !== undefined)
url_ += "sortBy=" + encodeURIComponent("" + sortBy) + "&";
if (sortOrder !== undefined && sortOrder !== null)
url_ += "sortOrder=" + encodeURIComponent("" + sortOrder) + "&";
@@ -2983,9 +3046,11 @@ export class TradingClient extends AuthorizedApiBase {
return Promise.resolve<Trade>(null as any);
}
trading_ClosePosition(identifier: string | null | undefined): Promise<Position> {
trading_ClosePosition(identifier: string | undefined): Promise<Position> {
let url_ = this.baseUrl + "/Trading/ClosePosition?";
if (identifier !== undefined && identifier !== null)
if (identifier === null)
throw new Error("The parameter 'identifier' cannot be null.");
else if (identifier !== undefined)
url_ += "identifier=" + encodeURIComponent("" + identifier) + "&";
url_ = url_.replace(/[?&]$/, "");
@@ -3133,7 +3198,7 @@ export class UserClient extends AuthorizedApiBase {
}
user_CreateToken(login: LoginRequest): Promise<string> {
let url_ = this.baseUrl + "/User";
let url_ = this.baseUrl + "/User/create-token";
url_ = url_.replace(/[?&]$/, "");
const content_ = JSON.stringify(login);
@@ -3550,6 +3615,7 @@ export enum AccountType {
}
export interface User {
id?: number;
name?: string | null;
accounts?: Account[] | null;
agentName?: string | null;
@@ -3736,8 +3802,8 @@ export interface Backtest {
growthPercentage: number;
hodlPercentage: number;
config: TradingBotConfig;
positions: Position[];
signals: LightSignal[];
positions: { [key: string]: Position; };
signals: { [key: string]: LightSignal; };
candles: Candle[];
startDate: Date;
endDate: Date;
@@ -3745,7 +3811,6 @@ export interface Backtest {
fees: number;
walletBalances: KeyValuePairOfDateTimeAndDecimal[];
user: User;
indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; };
score: number;
requestId?: string;
metadata?: any | null;
@@ -3948,10 +4013,7 @@ export enum PositionInitiator {
CopyTrading = "CopyTrading",
}
export interface ValueObject {
}
export interface LightSignal extends ValueObject {
export interface LightSignal {
status: SignalStatus;
direction: TradeDirection;
confidence: Confidence;
@@ -4008,96 +4070,6 @@ export interface KeyValuePairOfDateTimeAndDecimal {
value?: number;
}
export interface IndicatorsResultBase {
ema?: EmaResult[] | null;
fastEma?: EmaResult[] | null;
slowEma?: EmaResult[] | null;
macd?: MacdResult[] | null;
rsi?: RsiResult[] | null;
stoch?: StochResult[] | null;
stochRsi?: StochRsiResult[] | null;
bollingerBands?: BollingerBandsResult[] | null;
chandelierShort?: ChandelierResult[] | null;
stc?: StcResult[] | null;
stdDev?: StdDevResult[] | null;
superTrend?: SuperTrendResult[] | null;
chandelierLong?: ChandelierResult[] | null;
}
export interface ResultBase {
date?: Date;
}
export interface EmaResult extends ResultBase {
ema?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface MacdResult extends ResultBase {
macd?: number | null;
signal?: number | null;
histogram?: number | null;
fastEma?: number | null;
slowEma?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface RsiResult extends ResultBase {
rsi?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
/** Stochastic indicator results includes aliases for those who prefer the simpler K,D,J outputs. See documentation for more information. */
export interface StochResult extends ResultBase {
oscillator?: number | null;
signal?: number | null;
percentJ?: number | null;
k?: number | null;
d?: number | null;
j?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StochRsiResult extends ResultBase {
stochRsi?: number | null;
signal?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface BollingerBandsResult extends ResultBase {
sma?: number | null;
upperBand?: number | null;
lowerBand?: number | null;
percentB?: number | null;
zScore?: number | null;
width?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface ChandelierResult extends ResultBase {
chandelierExit?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StcResult extends ResultBase {
stc?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StdDevResult extends ResultBase {
stdDev?: number | null;
mean?: number | null;
zScore?: number | null;
stdDevSma?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface SuperTrendResult extends ResultBase {
superTrend?: number | null;
upperBand?: number | null;
lowerBand?: number | null;
}
export interface DeleteBacktestsRequest {
backtestIds: string[];
}
@@ -4332,34 +4304,48 @@ export interface StartBotRequest {
config?: TradingBotConfigRequest | null;
}
export enum BotType {
SimpleBot = "SimpleBot",
ScalpingBot = "ScalpingBot",
FlippingBot = "FlippingBot",
export interface SaveBotRequest extends StartBotRequest {
}
export enum BotStatus {
None = "None",
Down = "Down",
Up = "Up",
}
export interface TradingBotResponse {
status: string;
signals: LightSignal[];
positions: Position[];
signals: { [key: string]: LightSignal; };
positions: { [key: string]: Position; };
candles: Candle[];
winRate: number;
profitAndLoss: number;
identifier: string;
agentName: string;
config: TradingBotConfig;
createDate: Date;
startupTime: Date;
name: string;
ticker: Ticker;
}
export interface PaginatedResponseOfTradingBotResponse {
items?: TradingBotResponse[] | null;
totalCount?: number;
pageNumber?: number;
pageSize?: number;
totalPages?: number;
hasPreviousPage?: boolean;
hasNextPage?: boolean;
}
export interface OpenPositionManuallyRequest {
identifier?: string | null;
identifier?: string;
direction?: TradeDirection;
}
export interface ClosePositionRequest {
identifier?: string | null;
positionId?: string | null;
identifier?: string;
positionId?: string;
}
export interface UpdateBotConfigRequest {
@@ -4388,14 +4374,13 @@ export interface Spotlight {
export interface Scenario {
name?: string | null;
indicators?: Indicator[] | null;
indicators?: IndicatorBase[] | null;
loopbackPeriod?: number | null;
user?: User | null;
}
export interface Indicator {
export interface IndicatorBase {
name?: string | null;
candles?: FixedSizeQueueOfCandle | null;
type?: IndicatorType;
signalType?: SignalType;
minimumHistory?: number;
@@ -4410,15 +4395,6 @@ export interface Indicator {
user?: User | null;
}
export interface Anonymous {
maxSize?: number;
}
export interface FixedSizeQueueOfCandle extends Anonymous {
[key: string]: any;
}
export interface TickerSignal {
ticker: Ticker;
fiveMinutes: LightSignal[];
@@ -4433,6 +4409,96 @@ export interface CandlesWithIndicatorsResponse {
indicatorsValues?: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; } | null;
}
export interface IndicatorsResultBase {
ema?: EmaResult[] | null;
fastEma?: EmaResult[] | null;
slowEma?: EmaResult[] | null;
macd?: MacdResult[] | null;
rsi?: RsiResult[] | null;
stoch?: StochResult[] | null;
stochRsi?: StochRsiResult[] | null;
bollingerBands?: BollingerBandsResult[] | null;
chandelierShort?: ChandelierResult[] | null;
stc?: StcResult[] | null;
stdDev?: StdDevResult[] | null;
superTrend?: SuperTrendResult[] | null;
chandelierLong?: ChandelierResult[] | null;
}
export interface ResultBase {
date?: Date;
}
export interface EmaResult extends ResultBase {
ema?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface MacdResult extends ResultBase {
macd?: number | null;
signal?: number | null;
histogram?: number | null;
fastEma?: number | null;
slowEma?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface RsiResult extends ResultBase {
rsi?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
/** Stochastic indicator results includes aliases for those who prefer the simpler K,D,J outputs. See documentation for more information. */
export interface StochResult extends ResultBase {
oscillator?: number | null;
signal?: number | null;
percentJ?: number | null;
k?: number | null;
d?: number | null;
j?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StochRsiResult extends ResultBase {
stochRsi?: number | null;
signal?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface BollingerBandsResult extends ResultBase {
sma?: number | null;
upperBand?: number | null;
lowerBand?: number | null;
percentB?: number | null;
zScore?: number | null;
width?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface ChandelierResult extends ResultBase {
chandelierExit?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StcResult extends ResultBase {
stc?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StdDevResult extends ResultBase {
stdDev?: number | null;
mean?: number | null;
zScore?: number | null;
stdDevSma?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface SuperTrendResult extends ResultBase {
superTrend?: number | null;
upperBand?: number | null;
lowerBand?: number | null;
}
export interface GetCandlesWithIndicatorsRequest {
ticker?: Ticker;
startDate?: Date;
@@ -4467,9 +4533,9 @@ export interface UserStrategyDetailsViewModel {
volumeLast24H?: number;
wins?: number;
losses?: number;
positions?: Position[] | null;
positions?: { [key: string]: Position; } | null;
identifier?: string | null;
scenarioName?: string | null;
walletBalances?: { [key: string]: number; } | null;
}
export interface PlatformSummaryViewModel {
@@ -4481,25 +4547,6 @@ export interface PlatformSummaryViewModel {
timeFilter?: string | null;
}
export interface AgentIndexViewModel {
agentSummaries?: AgentSummaryViewModel[] | null;
timeFilter?: string | null;
}
export interface AgentSummaryViewModel {
agentName?: string | null;
totalPnL?: number;
pnLLast24h?: number;
totalROI?: number;
roiLast24h?: number;
wins?: number;
losses?: number;
averageWinRate?: number;
activeStrategiesCount?: number;
totalVolume?: number;
volumeLast24h?: number;
}
export interface PaginatedAgentIndexResponse {
agentSummaries?: AgentSummaryViewModel[] | null;
totalCount?: number;
@@ -4509,11 +4556,31 @@ export interface PaginatedAgentIndexResponse {
hasNextPage?: boolean;
hasPreviousPage?: boolean;
timeFilter?: string | null;
sortBy?: string | null;
sortBy?: SortableFields;
sortOrder?: string | null;
filteredAgentNames?: string | null;
}
export interface AgentSummaryViewModel {
agentName?: string | null;
totalPnL?: number;
totalROI?: number;
wins?: number;
losses?: number;
activeStrategiesCount?: number;
totalVolume?: number;
}
export enum SortableFields {
TotalPnL = "TotalPnL",
TotalROI = "TotalROI",
Wins = "Wins",
Losses = "Losses",
AgentName = "AgentName",
CreatedAt = "CreatedAt",
UpdatedAt = "UpdatedAt",
}
export interface AgentBalanceHistory {
agentName?: string | null;
agentBalances?: AgentBalance[] | null;

View File

@@ -38,6 +38,7 @@ export enum AccountType {
}
export interface User {
id?: number;
name?: string | null;
accounts?: Account[] | null;
agentName?: string | null;
@@ -224,8 +225,8 @@ export interface Backtest {
growthPercentage: number;
hodlPercentage: number;
config: TradingBotConfig;
positions: Position[];
signals: LightSignal[];
positions: { [key: string]: Position; };
signals: { [key: string]: LightSignal; };
candles: Candle[];
startDate: Date;
endDate: Date;
@@ -233,7 +234,6 @@ export interface Backtest {
fees: number;
walletBalances: KeyValuePairOfDateTimeAndDecimal[];
user: User;
indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; };
score: number;
requestId?: string;
metadata?: any | null;
@@ -436,10 +436,7 @@ export enum PositionInitiator {
CopyTrading = "CopyTrading",
}
export interface ValueObject {
}
export interface LightSignal extends ValueObject {
export interface LightSignal {
status: SignalStatus;
direction: TradeDirection;
confidence: Confidence;
@@ -496,96 +493,6 @@ export interface KeyValuePairOfDateTimeAndDecimal {
value?: number;
}
export interface IndicatorsResultBase {
ema?: EmaResult[] | null;
fastEma?: EmaResult[] | null;
slowEma?: EmaResult[] | null;
macd?: MacdResult[] | null;
rsi?: RsiResult[] | null;
stoch?: StochResult[] | null;
stochRsi?: StochRsiResult[] | null;
bollingerBands?: BollingerBandsResult[] | null;
chandelierShort?: ChandelierResult[] | null;
stc?: StcResult[] | null;
stdDev?: StdDevResult[] | null;
superTrend?: SuperTrendResult[] | null;
chandelierLong?: ChandelierResult[] | null;
}
export interface ResultBase {
date?: Date;
}
export interface EmaResult extends ResultBase {
ema?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface MacdResult extends ResultBase {
macd?: number | null;
signal?: number | null;
histogram?: number | null;
fastEma?: number | null;
slowEma?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface RsiResult extends ResultBase {
rsi?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
/** Stochastic indicator results includes aliases for those who prefer the simpler K,D,J outputs. See documentation for more information. */
export interface StochResult extends ResultBase {
oscillator?: number | null;
signal?: number | null;
percentJ?: number | null;
k?: number | null;
d?: number | null;
j?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StochRsiResult extends ResultBase {
stochRsi?: number | null;
signal?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface BollingerBandsResult extends ResultBase {
sma?: number | null;
upperBand?: number | null;
lowerBand?: number | null;
percentB?: number | null;
zScore?: number | null;
width?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface ChandelierResult extends ResultBase {
chandelierExit?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StcResult extends ResultBase {
stc?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StdDevResult extends ResultBase {
stdDev?: number | null;
mean?: number | null;
zScore?: number | null;
stdDevSma?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface SuperTrendResult extends ResultBase {
superTrend?: number | null;
upperBand?: number | null;
lowerBand?: number | null;
}
export interface DeleteBacktestsRequest {
backtestIds: string[];
}
@@ -820,34 +727,48 @@ export interface StartBotRequest {
config?: TradingBotConfigRequest | null;
}
export enum BotType {
SimpleBot = "SimpleBot",
ScalpingBot = "ScalpingBot",
FlippingBot = "FlippingBot",
export interface SaveBotRequest extends StartBotRequest {
}
export enum BotStatus {
None = "None",
Down = "Down",
Up = "Up",
}
export interface TradingBotResponse {
status: string;
signals: LightSignal[];
positions: Position[];
signals: { [key: string]: LightSignal; };
positions: { [key: string]: Position; };
candles: Candle[];
winRate: number;
profitAndLoss: number;
identifier: string;
agentName: string;
config: TradingBotConfig;
createDate: Date;
startupTime: Date;
name: string;
ticker: Ticker;
}
export interface PaginatedResponseOfTradingBotResponse {
items?: TradingBotResponse[] | null;
totalCount?: number;
pageNumber?: number;
pageSize?: number;
totalPages?: number;
hasPreviousPage?: boolean;
hasNextPage?: boolean;
}
export interface OpenPositionManuallyRequest {
identifier?: string | null;
identifier?: string;
direction?: TradeDirection;
}
export interface ClosePositionRequest {
identifier?: string | null;
positionId?: string | null;
identifier?: string;
positionId?: string;
}
export interface UpdateBotConfigRequest {
@@ -876,14 +797,13 @@ export interface Spotlight {
export interface Scenario {
name?: string | null;
indicators?: Indicator[] | null;
indicators?: IndicatorBase[] | null;
loopbackPeriod?: number | null;
user?: User | null;
}
export interface Indicator {
export interface IndicatorBase {
name?: string | null;
candles?: FixedSizeQueueOfCandle | null;
type?: IndicatorType;
signalType?: SignalType;
minimumHistory?: number;
@@ -898,15 +818,6 @@ export interface Indicator {
user?: User | null;
}
export interface Anonymous {
maxSize?: number;
}
export interface FixedSizeQueueOfCandle extends Anonymous {
[key: string]: any;
}
export interface TickerSignal {
ticker: Ticker;
fiveMinutes: LightSignal[];
@@ -921,6 +832,96 @@ export interface CandlesWithIndicatorsResponse {
indicatorsValues?: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; } | null;
}
export interface IndicatorsResultBase {
ema?: EmaResult[] | null;
fastEma?: EmaResult[] | null;
slowEma?: EmaResult[] | null;
macd?: MacdResult[] | null;
rsi?: RsiResult[] | null;
stoch?: StochResult[] | null;
stochRsi?: StochRsiResult[] | null;
bollingerBands?: BollingerBandsResult[] | null;
chandelierShort?: ChandelierResult[] | null;
stc?: StcResult[] | null;
stdDev?: StdDevResult[] | null;
superTrend?: SuperTrendResult[] | null;
chandelierLong?: ChandelierResult[] | null;
}
export interface ResultBase {
date?: Date;
}
export interface EmaResult extends ResultBase {
ema?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface MacdResult extends ResultBase {
macd?: number | null;
signal?: number | null;
histogram?: number | null;
fastEma?: number | null;
slowEma?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface RsiResult extends ResultBase {
rsi?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
/** Stochastic indicator results includes aliases for those who prefer the simpler K,D,J outputs. See documentation for more information. */
export interface StochResult extends ResultBase {
oscillator?: number | null;
signal?: number | null;
percentJ?: number | null;
k?: number | null;
d?: number | null;
j?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StochRsiResult extends ResultBase {
stochRsi?: number | null;
signal?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface BollingerBandsResult extends ResultBase {
sma?: number | null;
upperBand?: number | null;
lowerBand?: number | null;
percentB?: number | null;
zScore?: number | null;
width?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface ChandelierResult extends ResultBase {
chandelierExit?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StcResult extends ResultBase {
stc?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StdDevResult extends ResultBase {
stdDev?: number | null;
mean?: number | null;
zScore?: number | null;
stdDevSma?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface SuperTrendResult extends ResultBase {
superTrend?: number | null;
upperBand?: number | null;
lowerBand?: number | null;
}
export interface GetCandlesWithIndicatorsRequest {
ticker?: Ticker;
startDate?: Date;
@@ -955,9 +956,9 @@ export interface UserStrategyDetailsViewModel {
volumeLast24H?: number;
wins?: number;
losses?: number;
positions?: Position[] | null;
positions?: { [key: string]: Position; } | null;
identifier?: string | null;
scenarioName?: string | null;
walletBalances?: { [key: string]: number; } | null;
}
export interface PlatformSummaryViewModel {
@@ -969,25 +970,6 @@ export interface PlatformSummaryViewModel {
timeFilter?: string | null;
}
export interface AgentIndexViewModel {
agentSummaries?: AgentSummaryViewModel[] | null;
timeFilter?: string | null;
}
export interface AgentSummaryViewModel {
agentName?: string | null;
totalPnL?: number;
pnLLast24h?: number;
totalROI?: number;
roiLast24h?: number;
wins?: number;
losses?: number;
averageWinRate?: number;
activeStrategiesCount?: number;
totalVolume?: number;
volumeLast24h?: number;
}
export interface PaginatedAgentIndexResponse {
agentSummaries?: AgentSummaryViewModel[] | null;
totalCount?: number;
@@ -997,11 +979,31 @@ export interface PaginatedAgentIndexResponse {
hasNextPage?: boolean;
hasPreviousPage?: boolean;
timeFilter?: string | null;
sortBy?: string | null;
sortBy?: SortableFields;
sortOrder?: string | null;
filteredAgentNames?: string | null;
}
export interface AgentSummaryViewModel {
agentName?: string | null;
totalPnL?: number;
totalROI?: number;
wins?: number;
losses?: number;
activeStrategiesCount?: number;
totalVolume?: number;
}
export enum SortableFields {
TotalPnL = "TotalPnL",
TotalROI = "TotalROI",
Wins = "Wins",
Losses = "Losses",
AgentName = "AgentName",
CreatedAt = "CreatedAt",
UpdatedAt = "UpdatedAt",
}
export interface AgentBalanceHistory {
agentName?: string | null;
agentBalances?: AgentBalance[] | null;

View File

@@ -39,6 +39,7 @@ export interface UnifiedTradingModalProps {
// For backtests
setBacktests?: React.Dispatch<React.SetStateAction<Backtest[]>>
onBacktestComplete?: () => void // Callback when backtest is completed
// For bot creation/update
backtest?: Backtest // Backtest object when creating from backtest

View File

@@ -0,0 +1,32 @@
import React from 'react'
import {useQuery} from '@tanstack/react-query'
import useApiUrlStore from '../app/store/apiStore'
import {BotClient, BotStatus, type TradingBotResponse} from '../generated/ManagingApi'
type UseBotsProps = {
status?: BotStatus
callback?: (data: TradingBotResponse[]) => void | undefined
}
const useBots = ({status = BotStatus.None, callback}: UseBotsProps) => {
const {apiUrl} = useApiUrlStore()
const botClient = new BotClient({}, apiUrl)
const query = useQuery({
queryFn: () => botClient.bot_GetBotsByStatus(status),
queryKey: ['bots', status],
refetchInterval: 5000, // Refetch every 5 seconds for real-time updates
})
// Call callback when data changes
React.useEffect(() => {
if (query.data && callback) {
callback(query.data)
}
}, [query.data, callback])
return query
}
export default useBots

View File

@@ -1,9 +1,9 @@
import {ColorSwatchIcon, TrashIcon} from '@heroicons/react/solid'
import {useQuery, useQueryClient} from '@tanstack/react-query'
import React, {useEffect, useState} from 'react'
import 'react-toastify/dist/ReactToastify.css'
import useApiUrlStore from '../../app/store/apiStore'
import useBacktestStore from '../../app/store/backtestStore'
import {Loader, Slider} from '../../components/atoms'
import {Modal, Toast} from '../../components/mollecules'
import {BacktestTable, UnifiedTradingModal} from '../../components/organism'
@@ -21,43 +21,53 @@ const BacktestScanner: React.FC = () => {
score: 50
})
const [currentPage, setCurrentPage] = useState(1)
const [totalBacktests, setTotalBacktests] = useState(0)
const [totalPages, setTotalPages] = useState(0)
const [currentSort, setCurrentSort] = useState<{ sortBy: string; sortOrder: 'asc' | 'desc' }>({
sortBy: 'score',
sortOrder: 'desc'
})
const [backtests, setBacktests] = useState<LightBacktestResponse[]>([])
const [isLoading, setIsLoading] = useState(false)
const { apiUrl } = useApiUrlStore()
const { setBacktests: setBacktestsFromStore, setLoading } = useBacktestStore()
const queryClient = useQueryClient()
const backtestClient = new BacktestClient({}, apiUrl)
// Fetch paginated/sorted backtests
const fetchBacktests = async (page = 1, sort = currentSort) => {
setIsLoading(true)
try {
const response = await backtestClient.backtest_GetBacktestsPaginated(page, PAGE_SIZE, sort.sortBy, sort.sortOrder)
setBacktests((response.backtests as LightBacktestResponse[]) || [])
setTotalBacktests(response.totalCount || 0)
setTotalPages(response.totalPages || 0)
} catch (err) {
// Use TanStack Query for fetching backtests
const {
data: backtestData,
isLoading,
error,
refetch
} = useQuery({
queryKey: ['backtests', currentPage, currentSort.sortBy, currentSort.sortOrder],
queryFn: async () => {
const response = await backtestClient.backtest_GetBacktestsPaginated(
currentPage,
PAGE_SIZE,
currentSort.sortBy,
currentSort.sortOrder
)
return {
backtests: (response.backtests as LightBacktestResponse[]) || [],
totalCount: response.totalCount || 0,
totalPages: response.totalPages || 0
}
},
staleTime: 30000, // Consider data fresh for 30 seconds
gcTime: 5 * 60 * 1000, // Keep in cache for 5 minutes (formerly cacheTime)
})
const backtests = backtestData?.backtests || []
const totalBacktests = backtestData?.totalCount || 0
const totalPages = backtestData?.totalPages || 0
// Note: We no longer need to sync with the store since we're using TanStack Query
// The store is kept for backward compatibility with other components
// Handle errors
useEffect(() => {
if (error) {
new Toast('Failed to load backtests', false)
} finally {
setIsLoading(false)
}
}
useEffect(() => {
fetchBacktests(currentPage, currentSort)
// eslint-disable-next-line
}, [currentPage, currentSort])
useEffect(() => {
setBacktestsFromStore(backtests as any) // Cast to any for backward compatibility
setLoading(isLoading)
}, [backtests, setBacktestsFromStore, setLoading, isLoading])
}, [error])
useEffect(() => {
if (backtests && showModalRemoveBacktest) {
@@ -150,8 +160,8 @@ const BacktestScanner: React.FC = () => {
await backtestClient.backtest_DeleteBacktests({ backtestIds })
notify.update('success', `${backTestToDelete.length} backtests deleted successfully`)
// Refetch backtests to update the list
fetchBacktests(currentPage, currentSort)
// Invalidate and refetch backtests to update the list
queryClient.invalidateQueries({ queryKey: ['backtests'] })
} catch (err: any) {
notify.update('error', err?.message || 'An error occurred while deleting backtests')
}
@@ -202,6 +212,10 @@ const BacktestScanner: React.FC = () => {
isFetching={isLoading}
onSortChange={handleSortChange}
currentSort={currentSort}
onBacktestDeleted={() => {
// Invalidate backtest queries when a backtest is deleted
queryClient.invalidateQueries({ queryKey: ['backtests'] })
}}
/>
{/* Pagination controls */}
{totalPages > 1 && (
@@ -220,6 +234,10 @@ const BacktestScanner: React.FC = () => {
mode="backtest"
showModal={showModal}
closeModal={closeModal}
onBacktestComplete={() => {
// Invalidate backtest queries when a new backtest is completed
queryClient.invalidateQueries({ queryKey: ['backtests'] })
}}
/>
{/****************************/}

View File

@@ -8,15 +8,16 @@ import TradesModal from '../../components/mollecules/TradesModal/TradesModal'
import {TradeChart, UnifiedTradingModal} from '../../components/organism'
import {
BotClient,
BotType,
MoneyManagement,
Position,
TradingBotConfig,
TradingBotResponse,
UserClient
} from '../../generated/ManagingApi'
import type {IBotList} from '../../global/type.tsx'
import MoneyManagementModal from '../settingsPage/moneymanagement/moneyManagementModal'
import {useQuery} from '@tanstack/react-query'
import useCookie from '../../hooks/useCookie'
function baseBadgeClass(isOutlined = false) {
let classes = 'text-xs badge badge-sm transition-all duration-200 hover:scale-105 '
@@ -38,10 +39,15 @@ const BotList: React.FC<IBotList> = ({ list }) => {
const { apiUrl } = useApiUrlStore()
const client = new BotClient({}, apiUrl)
const userClient = new UserClient({}, apiUrl)
const { getCookie } = useCookie()
// Get JWT token from cookies
const jwtToken = getCookie('token')
const { data: currentUser } = useQuery({
queryFn: () => userClient.user_GetCurrentUser(),
queryKey: ['currentUser'],
enabled: !!jwtToken, // Only fetch when JWT token exists
})
const [showMoneyManagementModal, setShowMoneyManagementModal] =
@@ -55,7 +61,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
const [showBotConfigModal, setShowBotConfigModal] = useState(false)
const [selectedBotForUpdate, setSelectedBotForUpdate] = useState<{
identifier: string
config: any
config: TradingBotConfig
} | null>(null)
// Helper function to check if current user owns the bot
@@ -63,32 +69,6 @@ const BotList: React.FC<IBotList> = ({ list }) => {
return currentUser?.agentName === botAgentName
}
function getIsForWatchingBadge(isForWatchingOnly: boolean, identifier: string) {
const classes =
baseBadgeClass() + (isForWatchingOnly ? ' bg-accent' : ' bg-primary')
return (
<button className={classes} onClick={() => toggleIsForWatchingOnly(identifier)}>
{isForWatchingOnly ? (
<p className="text-accent-content flex">
<EyeIcon width={12}></EyeIcon>
Watch Only
</p>
) : (
<p className="text-primary-content flex">
<PlayIcon width={12}></PlayIcon>
Trading
</p>
)}
</button>
)
}
function toggleIsForWatchingOnly(identifier: string) {
const t = new Toast('Switch watch only')
client.bot_ToggleIsForWatching(identifier).then(() => {
t.update('success', 'Bot updated')
})
}
function getDeleteBadge(identifier: string) {
const classes = baseBadgeClass() + 'bg-error'
return (
@@ -99,17 +79,13 @@ const BotList: React.FC<IBotList> = ({ list }) => {
</button>
)
}
function getToggleBotStatusBadge(
status: string,
identifier: string,
botType: BotType
) {
function getToggleBotStatusBadge(status: string, identifier: string) {
const classes =
baseBadgeClass() + (status == 'Up' ? ' bg-error' : ' bg-success')
return (
<button
className={classes}
onClick={() => toggleBotStatus(status, identifier, botType)}
onClick={() => toggleBotStatus(status, identifier)}
>
{status == 'Up' ? (
<p className="text-accent-content flex">
@@ -174,10 +150,11 @@ const BotList: React.FC<IBotList> = ({ list }) => {
setShowTradesModal(true)
}
function toggleBotStatus(status: string, identifier: string, botType: BotType) {
function toggleBotStatus(status: string, identifier: string) {
const isUp = status == 'Up'
const t = new Toast(isUp ? 'Stoping bot' : 'Restarting bot')
console.log('toggleBotStatus', status, identifier)
if (status == 'Up') {
client
.bot_Stop(identifier)
@@ -187,9 +164,9 @@ const BotList: React.FC<IBotList> = ({ list }) => {
.catch((err) => {
t.update('error', err)
})
} else if (status == 'Down') {
} else if (status == 'Down' || status == 'None') {
client
.bot_Restart(botType, identifier)
.bot_Restart(identifier)
.then(() => {
t.update('success', 'Bot up')
})
@@ -215,7 +192,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
function getUpdateBotBadge(bot: TradingBotResponse) {
const classes = baseBadgeClass() + ' bg-orange-500'
return (
<button className={classes} onClick={() => openUpdateBotModal(bot)}>
<button className={classes} onClick={() => openUpdateBotModal(bot).catch(console.error)}>
<p className="text-primary-content flex">
<CogIcon width={15}></CogIcon>
</p>
@@ -223,12 +200,21 @@ const BotList: React.FC<IBotList> = ({ list }) => {
)
}
function openUpdateBotModal(bot: TradingBotResponse) {
setSelectedBotForUpdate({
identifier: bot.identifier,
config: bot.config
})
setShowBotConfigModal(true)
async function openUpdateBotModal(bot: TradingBotResponse) {
const t = new Toast('Loading bot configuration...')
try {
const config = await client.bot_GetBotConfig(bot.identifier)
setSelectedBotForUpdate({
identifier: bot.identifier,
config: config
})
setShowBotConfigModal(true)
t.update('success', 'Bot configuration loaded')
} catch (error: any) {
t.update('error', `Error loading bot configuration: ${error.message || error}`)
console.error('Error fetching bot config:', error)
}
}
return (
@@ -243,8 +229,8 @@ const BotList: React.FC<IBotList> = ({ list }) => {
{bot.candles && bot.candles.length > 0 ? (
<TradeChart
candles={bot.candles}
positions={bot.positions}
signals={bot.signals}
positions={Object.values(bot.positions)}
signals={Object.values(bot.signals)}
></TradeChart>
) : null}
</figure>
@@ -252,21 +238,16 @@ const BotList: React.FC<IBotList> = ({ list }) => {
<div className="mb-4">
{/* Bot Name - Always on its own line */}
<h2 className="card-title text-sm mb-3">
{bot.config.name}
{bot.name}
</h2>
{/* Badge Container - Responsive */}
<div className="flex flex-wrap gap-1 sm:gap-2">
{/* Info Badges */}
<div className="flex flex-wrap gap-1">
{getMoneyManagementBadge(bot.config.moneyManagement)}
</div>
{/* Action Badges - Only show for bot owners */}
{isBotOwner(bot.agentName) && (
<div className="flex flex-wrap gap-1">
{getIsForWatchingBadge(bot.config.isForWatchingOnly, bot.identifier)}
{getToggleBotStatusBadge(bot.status, bot.identifier, bot.config.flipPosition ? BotType.FlippingBot : BotType.SimpleBot)}
{getToggleBotStatusBadge(bot.status, bot.identifier)}
{getUpdateBotBadge(bot)}
{getManualPositionBadge(bot.identifier)}
{getDeleteBadge(bot.identifier)}
@@ -280,11 +261,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
<div>
<CardText
title="Ticker"
content={bot.config.ticker}
></CardText>
<CardText
title="Scenario"
content={bot.config.scenarioName ?? bot.config.scenario?.name}
content={bot.ticker}
></CardText>
</div>
</div>
@@ -294,19 +271,19 @@ const BotList: React.FC<IBotList> = ({ list }) => {
title="Agent"
content={bot.agentName}
></CardText>
<CardSignal signals={bot.signals}></CardSignal>
<CardSignal signals={Object.values(bot.signals ?? {})}></CardSignal>
</div>
<div className="columns-2">
<CardPosition
positivePosition={true}
positions={bot.positions.filter((p: Position) => {
positions={Object.values(bot.positions ?? {}).filter((p: Position) => {
const realized = p.ProfitAndLoss?.realized ?? 0
return realized > 0 ? p : null
})}
></CardPosition>
<CardPosition
positivePosition={false}
positions={bot.positions.filter((p: Position) => {
positions={Object.values(bot.positions ?? {}).filter((p: Position) => {
const realized = p.ProfitAndLoss?.realized ?? 0
return realized <= 0 ? p : null
})}

View File

@@ -1,11 +1,9 @@
import {PlayIcon, StopIcon, ViewGridAddIcon} from '@heroicons/react/solid'
import {ViewGridAddIcon} from '@heroicons/react/solid'
import React, {useState} from 'react'
import 'react-toastify/dist/ReactToastify.css'
import useApiUrlStore from '../../app/store/apiStore'
import {Toast} from '../../components/mollecules'
import {UnifiedTradingModal} from '../../components/organism'
import type {TradingBotResponse,} from '../../generated/ManagingApi'
import {BotClient, UserClient,} from '../../generated/ManagingApi'
import {BotClient, BotStatus, UserClient} from '../../generated/ManagingApi'
import BotList from './botList'
import {useQuery} from '@tanstack/react-query'
@@ -13,87 +11,55 @@ import {useQuery} from '@tanstack/react-query'
const Bots: React.FC = () => {
const [activeTab, setActiveTab] = useState(0)
const [showBotConfigModal, setShowBotConfigModal] = useState(false)
const [pageNumber, setPageNumber] = useState(1)
const [pageSize] = useState(50) // Fixed page size for now
// Reset page number when tab changes
const handleTabChange = (newTab: number) => {
setActiveTab(newTab)
setPageNumber(1) // Reset to first page when changing tabs
}
const { apiUrl } = useApiUrlStore()
const botClient = new BotClient({}, apiUrl)
const userClient = new UserClient({}, apiUrl)
const { data: bots } = useQuery({
queryFn: () => botClient.bot_GetActiveBots(),
queryKey: ['bots'],
})
const { data: currentUser } = useQuery({
queryFn: () => userClient.user_GetCurrentUser(),
queryKey: ['currentUser'],
})
// Filter bots based on active tab and current user
const getFilteredBots = (): TradingBotResponse[] => {
if (!bots || !currentUser) return []
switch (activeTab) {
case 0: // All Active Bots
return bots.filter(bot => bot.status === 'Up')
case 1: // My Active Bots
return bots.filter(bot => bot.status === 'Up' && bot.agentName === currentUser.agentName)
case 2: // My Down Bots
return bots.filter(bot => bot.status === 'Down' && bot.agentName === currentUser.agentName)
default:
return bots
}
}
// Query for paginated bots using the new endpoint
const { data: paginatedBots } = useQuery({
queryFn: () => {
switch (activeTab) {
case 0: // All Active Bots
return botClient.bot_GetBotsPaginated(pageNumber, pageSize, BotStatus.Up, undefined, undefined, undefined, 'CreatedAt', 'Desc')
case 1: // My Active Bots
return botClient.bot_GetBotsPaginated(pageNumber, pageSize, BotStatus.Up, undefined, undefined, currentUser?.agentName, 'CreatedAt', 'Desc')
case 2: // My Down Bots
return botClient.bot_GetBotsPaginated(pageNumber, pageSize, BotStatus.Down, undefined, undefined, currentUser?.agentName, 'CreatedAt', 'Desc')
case 3: // Saved Bots
return botClient.bot_GetBotsPaginated(pageNumber, pageSize, BotStatus.None, undefined, undefined, currentUser?.agentName, 'CreatedAt', 'Desc')
default:
return botClient.bot_GetBotsPaginated(pageNumber, pageSize, undefined, undefined, undefined, undefined, 'CreatedAt', 'Desc')
}
},
queryKey: ['paginatedBots', activeTab, pageNumber, pageSize, currentUser?.agentName],
enabled: !!currentUser,
})
const filteredBots = getFilteredBots()
const filteredBots = paginatedBots?.items || []
function openCreateBotModal() {
setShowBotConfigModal(true)
}
// const setupHubConnection = async () => {
// const hub = new Hub('bothub', apiUrl).hub
// hub.on('BotsSubscription', (bots: TradingBotResponse[]) => {
// // eslint-disable-next-line no-console
// console.log(
// 'bot List',
// bots.map((bot) => {
// return bot.name
// })
// )
// setBots(bots)
// })
// return hub
// }
async function stopAllBots() {
const t = new Toast('Stoping all bots')
await botClient
.bot_StopAll()
.then((result) => {
t.update('success', 'All bots stopped')
})
.catch((reason) => {
t.update('error', reason)
})
}
async function restartAllBots() {
const t = new Toast('Restarting all bots')
await botClient
.bot_RestartAll()
.then(() => {
t.update('success', 'All bots restarted')
})
.catch((reason) => {
t.update('error', reason)
})
}
const tabs = [
{ label: 'All Active Bots', index: 0 },
{ label: 'My Active Bots', index: 1 },
{ label: 'My Down Bots', index: 2 },
{ label: 'Saved Bots', index: 3 },
]
return (
@@ -106,19 +72,6 @@ const Bots: React.FC = () => {
<ViewGridAddIcon width="20"></ViewGridAddIcon>
</button>
</div>
<div className="tooltip" data-tip="Stop all bots">
<button className="btn btn-error m-1 text-xs" onClick={stopAllBots}>
<StopIcon width="20"></StopIcon>
</button>
</div>
<div className="tooltip" data-tip="Restart all bots">
<button
className="btn btn-success m-1 text-xs"
onClick={restartAllBots}
>
<PlayIcon width="20"></PlayIcon>
</button>
</div>
</div>
{/* Tabs */}
@@ -127,7 +80,7 @@ const Bots: React.FC = () => {
<button
key={tab.index}
className={`tab ${activeTab === tab.index ? 'tab-active' : ''}`}
onClick={() => setActiveTab(tab.index)}
onClick={() => handleTabChange(tab.index)}
>
{tab.label}
</button>

View File

@@ -15,15 +15,12 @@ const TIME_FILTERS = [
const SORT_OPTIONS = [
{ label: 'Total PnL', value: 'TotalPnL' },
{ label: '24H PnL', value: 'PnLLast24h' },
{ label: 'Total ROI', value: 'TotalROI' },
{ label: '24H ROI', value: 'ROILast24h' },
{ label: 'Wins', value: 'Wins' },
{ label: 'Losses', value: 'Losses' },
{ label: 'Win Rate', value: 'AverageWinRate' },
{ label: 'Active Strategies', value: 'ActiveStrategiesCount' },
{ label: 'Total Volume', value: 'TotalVolume' },
{ label: '24H Volume', value: 'VolumeLast24h' },
{ label: 'Agent Name', value: 'AgentName' },
{ label: 'Created At', value: 'CreatedAt' },
{ label: 'Updated At', value: 'UpdatedAt' },
]
function AgentIndex({ index }: { index: number }) {
@@ -40,6 +37,7 @@ function AgentIndex({ index }: { index: number }) {
const [timeFilter, setTimeFilter] = useState('Total')
const [sortBy, setSortBy] = useState('TotalPnL')
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc')
const [agentNameFilter, setAgentNameFilter] = useState('')
const fetchData = async () => {
setIsLoading(true)
@@ -48,11 +46,11 @@ function AgentIndex({ index }: { index: number }) {
try {
const client = new DataClient({}, apiUrl)
const response = await client.data_GetAgentIndexPaginated(
timeFilter,
currentPage,
pageSize,
sortBy,
sortOrder
sortBy as any, // Cast to enum type
sortOrder,
agentNameFilter || undefined,
)
setData(response)
} catch (err) {
@@ -65,7 +63,7 @@ function AgentIndex({ index }: { index: number }) {
useEffect(() => {
fetchData()
}, [currentPage, pageSize, timeFilter, sortBy, sortOrder])
}, [currentPage, pageSize, timeFilter, sortBy, sortOrder, agentNameFilter])
const handleSort = (columnId: string) => {
if (sortBy === columnId) {
@@ -76,6 +74,15 @@ function AgentIndex({ index }: { index: number }) {
}
}
const handlePageChange = (newPage: number) => {
setCurrentPage(newPage)
}
const handleAgentNameFilterChange = (value: string) => {
setAgentNameFilter(value)
setCurrentPage(1) // Reset to first page when filtering
}
const columns = useMemo(() => [
{
Header: 'Agent Name',
@@ -93,15 +100,6 @@ function AgentIndex({ index }: { index: number }) {
</span>
),
},
{
Header: '24H PnL',
accessor: 'pnLLast24h',
Cell: ({ value }: { value: number }) => (
<span className={value >= 0 ? 'text-green-500' : 'text-red-500'}>
{value >= 0 ? '+' : ''}${value.toLocaleString(undefined, { maximumFractionDigits: 2 })}
</span>
),
},
{
Header: 'Total ROI',
accessor: 'totalROI',
@@ -111,15 +109,6 @@ function AgentIndex({ index }: { index: number }) {
</span>
),
},
{
Header: '24H ROI',
accessor: 'roiLast24h',
Cell: ({ value }: { value: number }) => (
<span className={value >= 0 ? 'text-green-500' : 'text-red-500'}>
{value >= 0 ? '+' : ''}{value.toFixed(2)}%
</span>
),
},
{
Header: 'Wins/Losses',
accessor: 'wins',
@@ -150,31 +139,28 @@ function AgentIndex({ index }: { index: number }) {
<span>${value.toLocaleString(undefined, { maximumFractionDigits: 2 })}</span>
),
},
{
Header: '24H Volume',
accessor: 'volumeLast24h',
Cell: ({ value }: { value: number }) => (
<span>${value.toLocaleString(undefined, { maximumFractionDigits: 2 })}</span>
),
},
], [])
const tableData = useMemo(() => {
if (!data?.agentSummaries) return []
return data.agentSummaries.map(agent => ({
...agent,
// Ensure all numeric values are numbers for proper sorting
totalPnL: Number(agent.totalPnL) || 0,
pnLLast24h: Number(agent.pnLLast24h) || 0,
totalROI: Number(agent.totalROI) || 0,
roiLast24h: Number(agent.roiLast24h) || 0,
wins: Number(agent.wins) || 0,
losses: Number(agent.losses) || 0,
averageWinRate: Number(agent.averageWinRate) || 0,
activeStrategiesCount: Number(agent.activeStrategiesCount) || 0,
totalVolume: Number(agent.totalVolume) || 0,
volumeLast24h: Number(agent.volumeLast24h) || 0,
}))
return data.agentSummaries.map(agent => {
const wins = Number(agent.wins) || 0
const losses = Number(agent.losses) || 0
const totalTrades = wins + losses
const averageWinRate = totalTrades > 0 ? (wins * 100) / totalTrades : 0
return {
...agent,
// Ensure all numeric values are numbers for proper sorting
totalPnL: Number(agent.totalPnL) || 0,
totalROI: Number(agent.totalROI) || 0,
wins,
losses,
averageWinRate,
activeStrategiesCount: Number(agent.activeStrategiesCount) || 0,
totalVolume: Number(agent.totalVolume) || 0,
}
})
}, [data?.agentSummaries])
return (
@@ -183,18 +169,14 @@ function AgentIndex({ index }: { index: number }) {
{/* Filters and Controls */}
<div className="flex flex-wrap gap-4 mb-6">
<div className="flex items-center gap-2">
<label className="text-sm font-medium">Time Filter:</label>
<select
className="select select-bordered select-sm"
value={timeFilter}
onChange={(e) => setTimeFilter(e.target.value)}
>
{TIME_FILTERS.map(filter => (
<option key={filter.value} value={filter.value}>
{filter.label}
</option>
))}
</select>
<label className="text-sm font-medium">Agent Name:</label>
<input
type="text"
className="input input-bordered input-sm w-48"
placeholder="Filter by agent name..."
value={agentNameFilter}
onChange={(e) => handleAgentNameFilterChange(e.target.value)}
/>
</div>
<div className="flex items-center gap-2">
@@ -229,7 +211,10 @@ function AgentIndex({ index }: { index: number }) {
<select
className="select select-bordered select-sm"
value={pageSize}
onChange={(e) => setPageSize(Number(e.target.value))}
onChange={(e) => {
setPageSize(Number(e.target.value))
setCurrentPage(1) // Reset to first page when changing page size
}}
>
{[10, 20, 50, 100].map(size => (
<option key={size} value={size}>
@@ -260,7 +245,10 @@ function AgentIndex({ index }: { index: number }) {
<div className="mb-4 p-4 bg-base-200 rounded-lg">
<div className="flex justify-between items-center">
<div>
<span className="text-sm opacity-70">Showing {data.agentSummaries?.length || 0} of {data.totalCount || 0} agents</span>
<span className="text-sm opacity-70">
Showing {data.agentSummaries?.length || 0} of {data.totalCount || 0} agents
{agentNameFilter && ` (filtered by "${agentNameFilter}")`}
</span>
</div>
<div className="text-sm opacity-70">
Page {data.currentPage || 1} of {data.totalPages || 1}
@@ -284,33 +272,55 @@ function AgentIndex({ index }: { index: number }) {
<div className="flex justify-center items-center gap-2 mt-4">
<button
className="btn btn-sm"
onClick={() => setCurrentPage(1)}
onClick={() => handlePageChange(1)}
disabled={currentPage === 1}
>
{'<<'}
</button>
<button
className="btn btn-sm"
onClick={() => setCurrentPage(currentPage - 1)}
onClick={() => handlePageChange(currentPage - 1)}
disabled={!data.hasPreviousPage}
>
{'<'}
</button>
<span className="px-4">
Page {currentPage} of {data.totalPages}
</span>
{/* Page numbers */}
<div className="flex gap-1">
{Array.from({ length: Math.min(5, data.totalPages || 1) }, (_, i) => {
let pageNum
if ((data.totalPages || 1) <= 5) {
pageNum = i + 1
} else if (currentPage <= 3) {
pageNum = i + 1
} else if (currentPage >= (data.totalPages || 1) - 2) {
pageNum = (data.totalPages || 1) - 4 + i
} else {
pageNum = currentPage - 2 + i
}
return (
<button
key={pageNum}
className={`btn btn-sm ${currentPage === pageNum ? 'btn-primary' : ''}`}
onClick={() => handlePageChange(pageNum)}
>
{pageNum}
</button>
)
})}
</div>
<button
className="btn btn-sm"
onClick={() => setCurrentPage(currentPage + 1)}
onClick={() => handlePageChange(currentPage + 1)}
disabled={!data.hasNextPage}
>
{'>'}
</button>
<button
className="btn btn-sm"
onClick={() => setCurrentPage(data.totalPages || 1)}
onClick={() => handlePageChange(data.totalPages || 1)}
disabled={currentPage === data.totalPages}
>
{'>>'}
@@ -321,7 +331,20 @@ function AgentIndex({ index }: { index: number }) {
{/* No Data State */}
{data && (!data.agentSummaries || data.agentSummaries.length === 0) && !isLoading && (
<div className="text-center py-8">
<p>No agents found for the selected criteria.</p>
<p>
{agentNameFilter
? `No agents found matching "${agentNameFilter}".`
: 'No agents found for the selected criteria.'
}
</p>
{agentNameFilter && (
<button
className="btn btn-sm btn-outline mt-2"
onClick={() => handleAgentNameFilterChange('')}
>
Clear Filter
</button>
)}
</div>
)}
</GridTile>

View File

@@ -5,6 +5,7 @@ import {UserClient} from '../../generated/ManagingApi'
import Modal from '../../components/mollecules/Modal/Modal'
import useApiUrlStore from '../../app/store/apiStore'
import {Toast} from '../../components/mollecules'
import useCookie from '../../hooks/useCookie'
type UpdateAgentNameForm = {
agentName: string
@@ -25,10 +26,15 @@ function UserInfoSettings() {
const queryClient = useQueryClient()
const { apiUrl } = useApiUrlStore()
const api = new UserClient({}, apiUrl)
const { getCookie } = useCookie()
// Get JWT token from cookies
const jwtToken = getCookie('token')
const { data: user } = useQuery({
queryKey: ['user'],
queryFn: () => api.user_GetCurrentUser(),
enabled: !!jwtToken, // Only fetch when JWT token exists
})
const {