Push merge conflict

This commit is contained in:
2025-07-04 11:02:53 +07:00
parent 88f195c0ca
commit 59c5de7df7
27 changed files with 1133 additions and 1118 deletions

View File

@@ -21,6 +21,7 @@ const Modal: React.FC<IModalProps> = ({
titleHeader={titleHeader}
onClose={onClose}
onSubmit={onSubmit}
showModal={showModal}
/>
{children}
</div>

View File

@@ -10,9 +10,7 @@ import Logo from '../../../assets/img/logo.png'
import {Loader} from '../../atoms'
const navigation = [
{ href: '/desk', name: 'Desk' },
{ href: '/bots', name: 'Bots' },
{ href: '/workflow', name: 'Workflows' },
{ href: '/scenarios', name: 'Scenarios' },
{ href: '/backtest', name: 'Backtest' },
{ href: '/tools', name: 'Tools' },

View File

@@ -1,6 +1,6 @@
import type { FC } from 'react'
import type {FC} from 'react'
import type { ITabsProps } from '../../../global/type'
import type {ITabsProps} from '../../../global/type.tsx'
/**
* Avalible Props
@@ -19,7 +19,7 @@ const Tabs: FC<ITabsProps> = ({
addButton = false,
onAddButton,
}) => {
const Panel = tabs && tabs.find((tab) => tab.index === selectedTab)
const Panel = tabs && tabs.find((tab: any) => tab.index === selectedTab)
return (
<div
@@ -28,7 +28,7 @@ const Tabs: FC<ITabsProps> = ({
}
>
<div className="tabs" role="tablist" aria-orientation={orientation}>
{tabs.map((tab) => (
{tabs.map((tab: any) => (
<button
className={
'mb-5 tab tab-bordered ' +

View File

@@ -21,377 +21,374 @@ import TradeChart from '../Trading/TradeChart/TradeChart'
import {BotNameModal} from '../index'
function baseBadgeClass(isOutlined = false) {
let classes = 'text-xs badge '
let classes = 'text-xs badge '
if (isOutlined) {
classes += 'badge-outline '
}
return classes
if (isOutlined) {
classes += 'badge-outline '
}
return classes
}
function botStatusResult(
growthPercentage: number | undefined,
hodlPercentage: number | undefined
growthPercentage: number | undefined,
hodlPercentage: number | undefined
) {
if (growthPercentage != undefined && hodlPercentage != undefined) {
const isWinning = growthPercentage > hodlPercentage
const classes =
baseBadgeClass() + (isWinning ? 'badge-success' : 'badge-content')
return <div className={classes}>{isWinning ? 'Winning' : 'Losing'}</div>
}
if (growthPercentage != undefined && hodlPercentage != undefined) {
const isWinning = growthPercentage > hodlPercentage
const classes =
baseBadgeClass() + (isWinning ? 'badge-success' : 'badge-content')
return <div className={classes}>{isWinning ? 'Winning' : 'Losing'}</div>
}
}
// function that return the number of day between a date and today
function daysBetween(date: Date) {
const oneDay = 24 * 60 * 60 * 1000 // hours*minutes*seconds*milliseconds
const firstDate = new Date(date)
const secondDate = new Date()
const diffDays = Math.round(
Math.abs((firstDate.getTime() - secondDate.getTime()) / oneDay)
)
return diffDays
const oneDay = 24 * 60 * 60 * 1000 // hours*minutes*seconds*milliseconds
const firstDate = new Date(date)
const secondDate = new Date()
const diffDays = Math.round(
Math.abs((firstDate.getTime() - secondDate.getTime()) / oneDay)
)
return diffDays
}
const BacktestCards: React.FC<IBacktestCards> = ({ list, setBacktests }) => {
console.log(list)
const { apiUrl } = useApiUrlStore()
const [showMoneyManagementModal, setShowMoneyManagementModal] =
React.useState(false)
const [selectedMoneyManagement, setSelectedMoneyManagement] =
React.useState<MoneyManagement>()
const [showBotNameModal, setShowBotNameModal] = useState(false)
const [isForWatchOnly, setIsForWatchOnly] = useState(false)
const [currentBacktest, setCurrentBacktest] = useState<Backtest | null>(null)
const [selectedMoneyManagementName, setSelectedMoneyManagementName] = useState<string>('')
const BacktestCards: React.FC<IBacktestCards> = ({list, setBacktests}) => {
console.log(list)
const {apiUrl} = useApiUrlStore()
const [showMoneyManagementModal, setShowMoneyManagementModal] =
React.useState(false)
const [selectedMoneyManagement, setSelectedMoneyManagement] =
React.useState<MoneyManagement>()
const [showBotNameModal, setShowBotNameModal] = useState(false)
const [isForWatchOnly, setIsForWatchOnly] = useState(false)
const [currentBacktest, setCurrentBacktest] = useState<Backtest | null>(null)
const [selectedMoneyManagementName, setSelectedMoneyManagementName] = useState<string>('')
// Fetch money managements
const { data: moneyManagements } = useQuery({
queryFn: async () => {
const moneyManagementClient = new MoneyManagementClient({}, apiUrl)
return await moneyManagementClient.moneyManagement_GetMoneyManagements()
},
queryKey: ['moneyManagements'],
})
// Fetch money managements
const {data: moneyManagements} = useQuery({
queryFn: async () => {
const moneyManagementClient = new MoneyManagementClient({}, apiUrl)
return await moneyManagementClient.moneyManagement_GetMoneyManagements()
},
queryKey: ['moneyManagements'],
})
// Set the first money management as default when the data is loaded
useEffect(() => {
if (moneyManagements && moneyManagements.length > 0) {
setSelectedMoneyManagementName(moneyManagements[0].name)
}
}, [moneyManagements])
// Set the first money management as default when the data is loaded
useEffect(() => {
if (moneyManagements && moneyManagements.length > 0) {
setSelectedMoneyManagementName(moneyManagements[0].name)
}
}, [moneyManagements])
async function runBot(botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) {
const t = new Toast('Bot is starting')
const client = new BotClient({}, apiUrl)
async function runBot(botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) {
const t = new Toast('Bot is starting')
const client = new BotClient({}, apiUrl)
// Check if the money management name is "custom" or contains "custom"
const isCustomMoneyManagement =
!moneyManagementName ||
moneyManagementName.toLowerCase() === 'custom' ||
moneyManagementName.toLowerCase().includes('custom');
// Check if the money management name is "custom" or contains "custom"
const isCustomMoneyManagement =
!moneyManagementName ||
moneyManagementName.toLowerCase() === 'custom' ||
moneyManagementName.toLowerCase().includes('custom');
// Create TradingBotConfig from the backtest configuration
const tradingBotConfig: TradingBotConfig = {
accountName: backtest.config.accountName,
ticker: backtest.config.ticker,
scenarioName: backtest.config.scenarioName,
timeframe: backtest.config.timeframe,
botType: backtest.config.botType,
isForWatchingOnly: isForWatchOnly,
isForBacktest: false, // This is for running a live bot
cooldownPeriod: backtest.config.cooldownPeriod,
maxLossStreak: backtest.config.maxLossStreak,
maxPositionTimeHours: backtest.config.maxPositionTimeHours,
flipOnlyWhenInProfit: backtest.config.flipOnlyWhenInProfit,
flipPosition: backtest.config.flipPosition,
name: botName,
botTradingBalance: initialTradingBalance,
// Use the optimized or original money management from backtest if it's custom
moneyManagement: isCustomMoneyManagement ?
(backtest.optimizedMoneyManagement || backtest.config.moneyManagement || {
name: 'default',
leverage: 1,
stopLoss: 0.01,
takeProfit: 0.02,
timeframe: backtest.config.timeframe
}) :
backtest.config.moneyManagement, // Always provide a valid MoneyManagement object
closeEarlyWhenProfitable: backtest.config.closeEarlyWhenProfitable || false
};
// Create TradingBotConfig from the backtest configuration
const tradingBotConfig: TradingBotConfig = {
accountName: backtest.config.accountName,
ticker: backtest.config.ticker,
scenarioName: backtest.config.scenarioName,
timeframe: backtest.config.timeframe,
isForWatchingOnly: isForWatchOnly,
isForBacktest: false, // This is for running a live bot
cooldownPeriod: backtest.config.cooldownPeriod,
maxLossStreak: backtest.config.maxLossStreak,
maxPositionTimeHours: backtest.config.maxPositionTimeHours,
flipOnlyWhenInProfit: backtest.config.flipOnlyWhenInProfit,
flipPosition: backtest.config.flipPosition,
name: botName,
botTradingBalance: initialTradingBalance,
// Use the optimized or original money management from backtest if it's custom
moneyManagement: isCustomMoneyManagement ?
(backtest.optimizedMoneyManagement || backtest.config.moneyManagement || {
name: 'default',
leverage: 1,
stopLoss: 0.01,
takeProfit: 0.02,
timeframe: backtest.config.timeframe
}) :
backtest.config.moneyManagement, // Always provide a valid MoneyManagement object
closeEarlyWhenProfitable: backtest.config.closeEarlyWhenProfitable || false
};
const request: StartBotRequest = {
config: tradingBotConfig as unknown as TradingBotConfigRequest,
const request: StartBotRequest = {
config: tradingBotConfig as unknown as TradingBotConfigRequest,
}
await client
.bot_Start(request)
.then((botStatus: string) => {
t.update('info', 'Bot status: ' + botStatus)
})
.catch((err) => {
t.update('error', 'Error: ' + err)
})
}
await client
.bot_Start(request)
.then((botStatus: string) => {
t.update('info', 'Bot status: ' + botStatus)
})
.catch((err) => {
t.update('error', 'Error: ' + err)
})
}
const handleOpenBotNameModal = (backtest: Backtest, isForWatchOnly: boolean) => {
setCurrentBacktest(backtest)
setIsForWatchOnly(isForWatchOnly)
setShowBotNameModal(true)
}
const handleCloseBotNameModal = () => {
setShowBotNameModal(false)
}
const handleSubmitBotName = (botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) => {
runBot(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance)
setShowBotNameModal(false)
}
async function runOptimizedBacktest(backtest: Backtest) {
const t = new Toast('Optimized backtest is running')
const client = new BacktestClient({}, apiUrl)
// Calculate dates for the API call
const startDate = backtest.candles[0].date
const endDate = backtest.candles[backtest.candles.length - 1].date
// Create optimized backtest config
const optimizedConfig: TradingBotConfig = {
...backtest.config,
name: `${backtest.config.ticker}-${backtest.config.scenarioName}-Optimized`,
moneyManagement: backtest.optimizedMoneyManagement || backtest.config.moneyManagement
const handleOpenBotNameModal = (backtest: Backtest, isForWatchOnly: boolean) => {
setCurrentBacktest(backtest)
setIsForWatchOnly(isForWatchOnly)
setShowBotNameModal(true)
}
const request: RunBacktestRequest = {
config: optimizedConfig as unknown as TradingBotConfigRequest,
startDate: startDate,
endDate: endDate,
balance: backtest.walletBalances[0].value,
watchOnly: false,
save: false,
const handleCloseBotNameModal = () => {
setShowBotNameModal(false)
}
await client
.backtest_Run(request)
.then((backtest: Backtest) => {
t.update('success', `${backtest.config.ticker} Backtest Succeeded`)
setBacktests((arr: Backtest[]) => [...arr, backtest])
})
.catch((err) => {
t.update('error', 'Error :' + err)
})
}
const handleSubmitBotName = (botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) => {
runBot(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance)
setShowBotNameModal(false)
}
function saveMoneyManagement(moneyManagement: MoneyManagement) {
setSelectedMoneyManagement(moneyManagement)
setShowMoneyManagementModal(true)
}
async function runOptimizedBacktest(backtest: Backtest) {
const t = new Toast('Optimized backtest is running')
const client = new BacktestClient({}, apiUrl)
return (
<div className="flex flex-wrap m-4 -mx-4">
{list?.map((backtest: Backtest, index: number) => (
<div
key={index.toString()}
className="sm:w-1/2 md:w-1/2 xl:w-1/2 w-full p-2"
>
<div className="indicator">
<div className="indicator-item indicator-top">
<button className="btn btn-primary h-5 min-h-0 px-2 mr-5 rounded-full">
<TrashIcon width={15}></TrashIcon>
</button>
</div>
// Calculate dates for the API call
const startDate = backtest.candles[0].date
const endDate = backtest.candles[backtest.candles.length - 1].date
<div className="card bg-base-300 shadow-xl">
<figure className="z-0">
{
<TradeChart
candles={backtest.candles}
positions={backtest.positions}
walletBalances={backtest.walletBalances}
signals={backtest.signals}
indicatorsValues={backtest.indicatorsValues}
width={720}
height={512}
></TradeChart>
}
</figure>
// Create optimized backtest config
const optimizedConfig: TradingBotConfig = {
...backtest.config,
name: `${backtest.config.ticker}-${backtest.config.scenarioName}-Optimized`,
moneyManagement: backtest.optimizedMoneyManagement || backtest.config.moneyManagement
}
<div className="card-body">
<h2 className="card-title text-sm">
<div className="dropdown">
<label
htmlFor={index.toString()}
tabIndex={index}
className=""
>
<DotsVerticalIcon className="text-primary w-5 h-5" />
</label>
<ul
id={index.toString()}
className="dropdown-content menu bg-base-100 rounded-box w-52 p-2 shadow"
>
<li>
<button
className="text-xs"
onClick={() => handleOpenBotNameModal(backtest, false)}
>
Run bot
</button>
</li>
<li>
<button
className="text-xs"
onClick={() => handleOpenBotNameModal(backtest, true)}
>
Run watcher
</button>
</li>
<li>
<button
className="text-xs"
onClick={() =>
saveMoneyManagement(backtest.config.moneyManagement)
}
>
Save money management
</button>
</li>
<li>
<button
className="text-xs"
onClick={() => runOptimizedBacktest(backtest)}
>
Run optimized money management
</button>
</li>
</ul>
</div>
{backtest.config.ticker}
{botStatusResult(
backtest.growthPercentage,
backtest.hodlPercentage
)}
</h2>
<div className="columns-4 mb-2">
<div>
<CardText
title="Ticker"
content={backtest.config.ticker}
></CardText>
<CardText
title="Account"
content={backtest.config.accountName}
></CardText>
<CardText
title="Scenario"
content={backtest.config.scenarioName}
></CardText>
<CardText
title="Timeframe"
content={backtest.config.timeframe?.toString()}
></CardText>
</div>
</div>
<div className="columns-4 mb-2">
<CardText
title="Duration"
content={moment
.duration(
moment(
backtest.candles[backtest.candles.length - 1].date
).diff(backtest.candles[0].date)
)
.humanize()}
></CardText>
{/* <CardSignal signals={backtest.signals}></CardSignal> */}
<CardPosition
positivePosition={true}
positions={backtest.positions.filter((p) => {
const realized = p.profitAndLoss?.realized ?? 0
return realized > 0 ? p : null
})}
></CardPosition>
<CardPosition
positivePosition={false}
positions={backtest.positions.filter((p) => {
const realized = p.profitAndLoss?.realized ?? 0
return realized <= 0 ? p : null
})}
></CardPosition>
<CardPositionItem
positions={backtest.positions}
></CardPositionItem>
const request: RunBacktestRequest = {
config: optimizedConfig as unknown as TradingBotConfigRequest,
startDate: startDate,
endDate: endDate,
save: false,
}
await client
.backtest_Run(request)
.then((backtest: Backtest) => {
t.update('success', `${backtest.config.ticker} Backtest Succeeded`)
setBacktests((arr: Backtest[]) => [...arr, backtest])
})
.catch((err) => {
t.update('error', 'Error :' + err)
})
}
function saveMoneyManagement(moneyManagement: MoneyManagement) {
setSelectedMoneyManagement(moneyManagement)
setShowMoneyManagementModal(true)
}
return (
<div className="flex flex-wrap m-4 -mx-4">
{list?.map((backtest: Backtest, index: number) => (
<div
key={index.toString()}
className="sm:w-1/2 md:w-1/2 xl:w-1/2 w-full p-2"
>
<div className="indicator">
<div className="indicator-item indicator-top">
<button className="btn btn-primary h-5 min-h-0 px-2 mr-5 rounded-full">
<TrashIcon width={15}></TrashIcon>
</button>
</div>
<div className="card bg-base-300 shadow-xl">
<figure className="z-0">
{
<TradeChart
candles={backtest.candles}
positions={backtest.positions}
walletBalances={backtest.walletBalances}
signals={backtest.signals}
indicatorsValues={backtest.indicatorsValues}
width={720}
height={512}
></TradeChart>
}
</figure>
<div className="card-body">
<h2 className="card-title text-sm">
<div className="dropdown">
<label
htmlFor={index.toString()}
tabIndex={index}
className=""
>
<DotsVerticalIcon className="text-primary w-5 h-5"/>
</label>
<ul
id={index.toString()}
className="dropdown-content menu bg-base-100 rounded-box w-52 p-2 shadow"
>
<li>
<button
className="text-xs"
onClick={() => handleOpenBotNameModal(backtest, false)}
>
Run bot
</button>
</li>
<li>
<button
className="text-xs"
onClick={() => handleOpenBotNameModal(backtest, true)}
>
Run watcher
</button>
</li>
<li>
<button
className="text-xs"
onClick={() =>
saveMoneyManagement(backtest.config.moneyManagement)
}
>
Save money management
</button>
</li>
<li>
<button
className="text-xs"
onClick={() => runOptimizedBacktest(backtest)}
>
Run optimized money management
</button>
</li>
</ul>
</div>
{backtest.config.ticker}
{botStatusResult(
backtest.growthPercentage,
backtest.hodlPercentage
)}
</h2>
<div className="columns-4 mb-2">
<div>
<CardText
title="Ticker"
content={backtest.config.ticker}
></CardText>
<CardText
title="Account"
content={backtest.config.accountName}
></CardText>
<CardText
title="Scenario"
content={backtest.config.scenarioName ?? backtest.config.scenario?.name}
></CardText>
<CardText
title="Timeframe"
content={backtest.config.timeframe?.toString()}
></CardText>
</div>
</div>
<div className="columns-4 mb-2">
<CardText
title="Duration"
content={moment
.duration(
moment(
backtest.candles[backtest.candles.length - 1].date
).diff(backtest.candles[0].date)
)
.humanize()}
></CardText>
{/* <CardSignal signals={backtest.signals}></CardSignal> */}
<CardPosition
positivePosition={true}
positions={backtest.positions.filter((p) => {
const realized = p.profitAndLoss?.realized ?? 0
return realized > 0 ? p : null
})}
></CardPosition>
<CardPosition
positivePosition={false}
positions={backtest.positions.filter((p) => {
const realized = p.profitAndLoss?.realized ?? 0
return realized <= 0 ? p : null
})}
></CardPosition>
<CardPositionItem
positions={backtest.positions}
></CardPositionItem>
</div>
<div className="columns-4 mb-2">
<div>
<CardText
title="Max Drowdown"
content={
backtest.statistics.maxDrawdown?.toFixed(4).toString() +
'$'
}
></CardText>
<CardText
title="PNL"
content={backtest.finalPnl?.toFixed(4).toString() + '$'}
></CardText>
<CardText
title="Sharpe Ratio"
content={
(backtest.statistics.sharpeRatio
? backtest.statistics.sharpeRatio * 100
: 0
)
.toFixed(4)
.toString() + '%'
}
></CardText>
<CardText
title="%Hodl"
content={
backtest.hodlPercentage?.toFixed(2).toString() + '%'
}
></CardText>
</div>
</div>
<div className="card-actions justify-center pt-2 text-sm">
<div className={baseBadgeClass(true)}>
WR {backtest.winRate?.toFixed(2).toString()} %
</div>
<div className={baseBadgeClass(true)}>
PNL {backtest.growthPercentage?.toFixed(2).toString()} %
</div>
</div>
</div>
</div>
</div>
</div>
))}
<div className="columns-4 mb-2">
<div>
<CardText
title="Max Drowdown"
content={
backtest.statistics.maxDrawdown?.toFixed(4).toString() +
'$'
}
></CardText>
<CardText
title="PNL"
content={backtest.finalPnl?.toFixed(4).toString() + '$'}
></CardText>
<CardText
title="Sharpe Ratio"
content={
(backtest.statistics.sharpeRatio
? backtest.statistics.sharpeRatio * 100
: 0
)
.toFixed(4)
.toString() + '%'
}
></CardText>
<CardText
title="%Hodl"
content={
backtest.hodlPercentage?.toFixed(2).toString() + '%'
}
></CardText>
</div>
</div>
<div className="card-actions justify-center pt-2 text-sm">
<div className={baseBadgeClass(true)}>
WR {backtest.winRate?.toFixed(2).toString()} %
</div>
<div className={baseBadgeClass(true)}>
PNL {backtest.growthPercentage?.toFixed(2).toString()} %
</div>
</div>
</div>
</div>
</div>
<MoneyManagementModal
showModal={showMoneyManagementModal}
moneyManagement={selectedMoneyManagement}
onClose={() => setShowMoneyManagementModal(false)}
/>
{showBotNameModal && currentBacktest && moneyManagements && (
<BotNameModal
showModal={showBotNameModal}
onClose={handleCloseBotNameModal}
backtest={currentBacktest}
isForWatchOnly={isForWatchOnly}
onSubmitBotName={(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance) =>
handleSubmitBotName(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance)
}
moneyManagements={moneyManagements}
selectedMoneyManagement={selectedMoneyManagementName}
setSelectedMoneyManagement={setSelectedMoneyManagementName}
/>
)}
</div>
))}
<MoneyManagementModal
showModal={showMoneyManagementModal}
moneyManagement={selectedMoneyManagement}
onClose={() => setShowMoneyManagementModal(false)}
/>
{showBotNameModal && currentBacktest && moneyManagements && (
<BotNameModal
showModal={showBotNameModal}
onClose={handleCloseBotNameModal}
backtest={currentBacktest}
isForWatchOnly={isForWatchOnly}
onSubmitBotName={(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance) =>
handleSubmitBotName(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance)
}
moneyManagements={moneyManagements}
selectedMoneyManagement={selectedMoneyManagementName}
setSelectedMoneyManagement={setSelectedMoneyManagementName}
/>
)}
</div>
)
)
}
export default BacktestCards

View File

@@ -351,16 +351,15 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
content={getAverageTradesPerDay() + " trades/day"}
></CardText>
</div>
<div>
<figure>
<div className="w-full">
<figure className="w-full">
<TradeChart
width={1400}
height={1100}
candles={candles}
positions={positions}
walletBalances={walletBalances}
indicatorsValues={indicatorsValues}
signals={signals}
height={1000}
></TradeChart>
</figure>
</div>

View File

@@ -4,491 +4,491 @@ import React, {useEffect, useState} from 'react'
import useApiUrlStore from '../../../app/store/apiStore'
import type {Backtest} from '../../../generated/ManagingApi'
import {BacktestClient} from '../../../generated/ManagingApi'
import type {IBacktestCards} from '../../../global/type'
import type {IBacktestCards} from '../../../global/type.tsx'
import {CardText, SelectColumnFilter, Table} from '../../mollecules'
import {UnifiedTradingModal} from '../index'
import Toast from '../../mollecules/Toast/Toast'
import BacktestRowDetails from './backtestRowDetails'
const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktests }) => {
const [rows, setRows] = useState<Backtest[]>([])
const { apiUrl } = useApiUrlStore()
const [optimizedMoneyManagement, setOptimizedMoneyManagement] = useState({
stopLoss: 0,
takeProfit: 0,
})
const [positionTimingStats, setPositionTimingStats] = useState({
averageOpenTime: 0,
medianOpenTime: 0,
losingPositionsAverageOpenTime: 0,
})
const [cooldownRecommendations, setCooldownRecommendations] = useState({
averageCooldown: 0,
medianCooldown: 0,
})
// Bot configuration modal state
const [showBotConfigModal, setShowBotConfigModal] = useState(false)
const [selectedBacktest, setSelectedBacktest] = useState<Backtest | null>(null)
const BacktestTable: React.FC<IBacktestCards> = ({list, isFetching, setBacktests}) => {
const [rows, setRows] = useState<Backtest[]>([])
const {apiUrl} = useApiUrlStore()
const [optimizedMoneyManagement, setOptimizedMoneyManagement] = useState({
stopLoss: 0,
takeProfit: 0,
})
const [positionTimingStats, setPositionTimingStats] = useState({
averageOpenTime: 0,
medianOpenTime: 0,
losingPositionsAverageOpenTime: 0,
})
const [cooldownRecommendations, setCooldownRecommendations] = useState({
averageCooldown: 0,
medianCooldown: 0,
})
// Backtest configuration modal state
const [showBacktestConfigModal, setShowBacktestConfigModal] = useState(false)
const [selectedBacktestForRerun, setSelectedBacktestForRerun] = useState<Backtest | null>(null)
// Bot configuration modal state
const [showBotConfigModal, setShowBotConfigModal] = useState(false)
const [selectedBacktest, setSelectedBacktest] = useState<Backtest | null>(null)
const handleOpenBotConfigModal = (backtest: Backtest) => {
setSelectedBacktest(backtest)
setShowBotConfigModal(true)
}
// Backtest configuration modal state
const [showBacktestConfigModal, setShowBacktestConfigModal] = useState(false)
const [selectedBacktestForRerun, setSelectedBacktestForRerun] = useState<Backtest | null>(null)
const handleCloseBotConfigModal = () => {
setShowBotConfigModal(false)
setSelectedBacktest(null)
}
const handleOpenBotConfigModal = (backtest: Backtest) => {
setSelectedBacktest(backtest)
setShowBotConfigModal(true)
}
const handleOpenBacktestConfigModal = (backtest: Backtest) => {
setSelectedBacktestForRerun(backtest)
setShowBacktestConfigModal(true)
}
const handleCloseBotConfigModal = () => {
setShowBotConfigModal(false)
setSelectedBacktest(null)
}
const handleCloseBacktestConfigModal = () => {
setShowBacktestConfigModal(false)
setSelectedBacktestForRerun(null)
}
const handleOpenBacktestConfigModal = (backtest: Backtest) => {
setSelectedBacktestForRerun(backtest)
setShowBacktestConfigModal(true)
}
async function deleteBacktest(id: string) {
const t = new Toast('Deleting backtest')
const client = new BacktestClient({}, apiUrl)
const handleCloseBacktestConfigModal = () => {
setShowBacktestConfigModal(false)
setSelectedBacktestForRerun(null)
}
await client
.backtest_DeleteBacktest(id)
.then(() => {
t.update('success', 'Backtest deleted')
// Remove the deleted backtest from the list
if (list) {
const updatedList = list.filter(backtest => backtest.id !== id);
setBacktests(updatedList);
}
})
.catch((err) => {
t.update('error', err)
})
}
async function deleteBacktest(id: string) {
const t = new Toast('Deleting backtest')
const client = new BacktestClient({}, apiUrl)
const getScoreColor = (score: number) => {
if (score >= 75) return '#08C25F'; // success
if (score >= 50) return '#B0DB43'; // info
if (score >= 25) return '#EB6F22'; // warning
return '#FF5340'; // error
};
await client
.backtest_DeleteBacktest(id)
.then(() => {
t.update('success', 'Backtest deleted')
// Remove the deleted backtest from the list
if (list) {
const updatedList = list.filter(backtest => backtest.id !== id);
setBacktests(updatedList);
}
})
.catch((err) => {
t.update('error', err)
})
}
const columns = React.useMemo(
() => [
{
Header: 'Informations',
columns: [
{
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 getScoreColor = (score: number) => {
if (score >= 75) return '#08C25F'; // success
if (score >= 50) return '#B0DB43'; // info
if (score >= 25) return '#EB6F22'; // warning
return '#FF5340'; // error
};
const columns = React.useMemo(
() => [
{
Header: 'Informations',
columns: [
{
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',
},
{
Header: 'Score',
accessor: 'score',
Cell: ({ cell }: any) => (
<span style={{
color: getScoreColor(cell.row.values.score),
fontWeight: 500,
display: 'inline-block',
width: '60px'
}}>
),
// Build our expander column
id: 'expander',
},
{
Header: 'Score',
accessor: 'score',
Cell: ({cell}: any) => (
<span style={{
color: getScoreColor(cell.row.values.score),
fontWeight: 500,
display: 'inline-block',
width: '60px'
}}>
{cell.row.values.score.toFixed(2)}
</span>
),
disableFilters: true,
},
{
Filter: SelectColumnFilter,
Header: 'Ticker',
accessor: 'config.ticker',
disableSortBy: true,
},
{
Filter: SelectColumnFilter,
Header: 'Timeframe',
accessor: 'config.timeframe',
disableSortBy: true,
},
{
Filter: SelectColumnFilter,
Header: 'Scenario',
accessor: 'config.scenarioName',
disableSortBy: true,
},
{
Filter: SelectColumnFilter,
Header: 'BotType',
accessor: 'config.botType',
disableSortBy: true,
},
),
disableFilters: true,
},
{
Filter: SelectColumnFilter,
Header: 'Ticker',
accessor: 'config.ticker',
disableSortBy: true,
},
{
Filter: SelectColumnFilter,
Header: 'Timeframe',
accessor: 'config.timeframe',
disableSortBy: true,
},
{
Filter: SelectColumnFilter,
Header: 'Scenario',
accessor: 'config.scenarioName',
disableSortBy: true,
},
{
Filter: SelectColumnFilter,
Header: 'BotType',
accessor: 'config.botType',
disableSortBy: true,
},
],
},
{
Header: 'Results',
columns: [
{
Cell: ({cell}: any) => (
<>{cell.row.values.finalPnl.toFixed(2)} $</>
),
Header: 'Pnl $',
accessor: 'finalPnl',
disableFilters: true,
sortType: 'basic',
},
{
Cell: ({cell}: any) => (
<>{cell.row.values.hodlPercentage.toFixed(2)} %</>
),
Header: 'Hodl %',
accessor: 'hodlPercentage',
disableFilters: true,
sortType: 'basic',
},
{
Cell: ({cell}: any) => <>{cell.row.values.winRate} %</>,
Header: 'Winrate',
accessor: 'winRate',
disableFilters: true,
},
{
Cell: ({cell}: any) => (
<>{cell.row.values.growthPercentage.toFixed(2)} %</>
),
Header: 'Pnl %',
accessor: 'growthPercentage',
disableFilters: true,
sortType: 'basic',
},
{
Cell: ({cell}: any) => (
<>
{(
cell.row.values.growthPercentage -
cell.row.values.hodlPercentage
).toFixed(2)}
</>
),
Header: 'H/P',
accessor: 'diff',
disableFilters: true,
sortType: 'basic',
},
],
},
{
Header: 'Action',
columns: [
{
Cell: ({cell}: any) => (
<>
<div className="tooltip" data-tip="Delete backtest">
<button
data-value={cell.row.values.name}
onClick={() => deleteBacktest(cell.row.values.id)}
>
<TrashIcon className="text-accent w-4"></TrashIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'id',
disableFilters: true,
},
{
Cell: ({cell}: any) => (
<>
<div className="tooltip" data-tip="Re-run backtest with same config">
<button
data-value={cell.row.values.name}
onClick={() => handleOpenBacktestConfigModal(cell.row.original as Backtest)}
>
<CogIcon className="text-info w-4"></CogIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'rerun',
disableFilters: true,
},
{
Cell: ({cell}: any) => (
<>
<div className="tooltip" data-tip="Create bot from backtest">
<button
data-value={cell.row.values.name}
onClick={() => handleOpenBotConfigModal(cell.row.original as Backtest)}
>
<PlayIcon className="text-primary w-4"></PlayIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'runner',
disableFilters: true,
}
],
},
],
},
{
Header: 'Results',
columns: [
{
Cell: ({ cell }: any) => (
<>{cell.row.values.finalPnl.toFixed(2)} $</>
),
Header: 'Pnl $',
accessor: 'finalPnl',
disableFilters: true,
sortType: 'basic',
},
{
Cell: ({ cell }: any) => (
<>{cell.row.values.hodlPercentage.toFixed(2)} %</>
),
Header: 'Hodl %',
accessor: 'hodlPercentage',
disableFilters: true,
sortType: 'basic',
},
{
Cell: ({ cell }: any) => <>{cell.row.values.winRate} %</>,
Header: 'Winrate',
accessor: 'winRate',
disableFilters: true,
},
{
Cell: ({ cell }: any) => (
<>{cell.row.values.growthPercentage.toFixed(2)} %</>
),
Header: 'Pnl %',
accessor: 'growthPercentage',
disableFilters: true,
sortType: 'basic',
},
{
Cell: ({ cell }: any) => (
<>
{(
cell.row.values.growthPercentage -
cell.row.values.hodlPercentage
).toFixed(2)}
</>
),
Header: 'H/P',
accessor: 'diff',
disableFilters: true,
sortType: 'basic',
},
],
},
{
Header: 'Action',
columns: [
{
Cell: ({ cell }: any) => (
<>
<div className="tooltip" data-tip="Delete backtest">
<button
data-value={cell.row.values.name}
onClick={() => deleteBacktest(cell.row.values.id)}
>
<TrashIcon className="text-accent w-4"></TrashIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'id',
disableFilters: true,
},
{
Cell: ({ cell }: any) => (
<>
<div className="tooltip" data-tip="Re-run backtest with same config">
<button
data-value={cell.row.values.name}
onClick={() => handleOpenBacktestConfigModal(cell.row.original as Backtest)}
>
<CogIcon className="text-info w-4"></CogIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'rerun',
disableFilters: true,
},
{
Cell: ({ cell }: any) => (
<>
<div className="tooltip" data-tip="Create bot from backtest">
<button
data-value={cell.row.values.name}
onClick={() => handleOpenBotConfigModal(cell.row.original as Backtest)}
>
<PlayIcon className="text-primary w-4"></PlayIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'runner',
disableFilters: true,
}
],
},
],
[]
)
[]
)
useEffect(() => {
if (list) {
setRows(list)
// Calculate average optimized money management
if (list.length > 0) {
const optimized = list.map((b) => b.optimizedMoneyManagement);
const stopLoss = optimized.reduce((acc, curr) => acc + (curr?.stopLoss ?? 0), 0);
const takeProfit = optimized.reduce((acc, curr) => acc + (curr?.takeProfit ?? 0), 0);
setOptimizedMoneyManagement({
stopLoss: stopLoss / optimized.length,
takeProfit: takeProfit / optimized.length,
});
useEffect(() => {
if (list) {
setRows(list)
// Calculate position timing statistics
const allPositions = list.flatMap(backtest => backtest.positions);
const finishedPositions = allPositions.filter(p => p.status === 'Finished');
if (finishedPositions.length > 0) {
// Calculate position open times in hours
const openTimes = finishedPositions.map(position => {
const openTime = new Date(position.open.date);
// Find the closing trade (either stopLoss or takeProfit that was filled)
let closeTime = new Date();
if (position.stopLoss.status === 'Filled') {
closeTime = new Date(position.stopLoss.date);
} else if (position.takeProfit1.status === 'Filled') {
closeTime = new Date(position.takeProfit1.date);
} else if (position.takeProfit2?.status === 'Filled') {
closeTime = new Date(position.takeProfit2.date);
}
// Return time difference in hours
return (closeTime.getTime() - openTime.getTime()) / (1000 * 60 * 60);
});
// Calculate average optimized money management
if (list.length > 0) {
const optimized = list.map((b) => b.optimizedMoneyManagement);
const stopLoss = optimized.reduce((acc, curr) => acc + (curr?.stopLoss ?? 0), 0);
const takeProfit = optimized.reduce((acc, curr) => acc + (curr?.takeProfit ?? 0), 0);
// Calculate average
const averageOpenTime = openTimes.reduce((sum, time) => sum + time, 0) / openTimes.length;
setOptimizedMoneyManagement({
stopLoss: stopLoss / optimized.length,
takeProfit: takeProfit / optimized.length,
});
// Calculate median
const sortedTimes = [...openTimes].sort((a, b) => a - b);
const medianOpenTime = sortedTimes.length % 2 === 0
? (sortedTimes[sortedTimes.length / 2 - 1] + sortedTimes[sortedTimes.length / 2]) / 2
: sortedTimes[Math.floor(sortedTimes.length / 2)];
// Calculate position timing statistics
const allPositions = list.flatMap(backtest => backtest.positions);
const finishedPositions = allPositions.filter(p => p.status === 'Finished');
// Calculate average for losing positions
const losingPositions = finishedPositions.filter(p => (p.profitAndLoss?.realized ?? 0) < 0);
let losingPositionsAverageOpenTime = 0;
if (losingPositions.length > 0) {
const losingOpenTimes = losingPositions.map(position => {
const openTime = new Date(position.open.date);
let closeTime = new Date();
if (position.stopLoss.status === 'Filled') {
closeTime = new Date(position.stopLoss.date);
} else if (position.takeProfit1.status === 'Filled') {
closeTime = new Date(position.takeProfit1.date);
} else if (position.takeProfit2?.status === 'Filled') {
closeTime = new Date(position.takeProfit2.date);
}
return (closeTime.getTime() - openTime.getTime()) / (1000 * 60 * 60);
});
losingPositionsAverageOpenTime = losingOpenTimes.reduce((sum, time) => sum + time, 0) / losingOpenTimes.length;
}
if (finishedPositions.length > 0) {
// Calculate position open times in hours
const openTimes = finishedPositions.map(position => {
const openTime = new Date(position.open.date);
// Find the closing trade (either stopLoss or takeProfit that was filled)
let closeTime = new Date();
setPositionTimingStats({
averageOpenTime,
medianOpenTime,
losingPositionsAverageOpenTime,
});
}
if (position.stopLoss.status === 'Filled') {
closeTime = new Date(position.stopLoss.date);
} else if (position.takeProfit1.status === 'Filled') {
closeTime = new Date(position.takeProfit1.date);
} else if (position.takeProfit2?.status === 'Filled') {
closeTime = new Date(position.takeProfit2.date);
}
// Calculate cooldown recommendations across all backtests
const allCooldownValues: number[] = [];
list.forEach(backtest => {
if (backtest.positions.length < 2 || !backtest.candles || backtest.candles.length < 2) {
return;
}
// Return time difference in hours
return (closeTime.getTime() - openTime.getTime()) / (1000 * 60 * 60);
});
// Determine candle timeframe in milliseconds
const candleTimeframeMs = new Date(backtest.candles[1].date).getTime() - new Date(backtest.candles[0].date).getTime();
// Calculate average
const averageOpenTime = openTimes.reduce((sum, time) => sum + time, 0) / openTimes.length;
const sortedPositions = [...backtest.positions].sort((a, b) => {
const dateA = new Date(a.open.date).getTime();
const dateB = new Date(b.open.date).getTime();
return dateA - dateB;
});
// Calculate median
const sortedTimes = [...openTimes].sort((a, b) => a - b);
const medianOpenTime = sortedTimes.length % 2 === 0
? (sortedTimes[sortedTimes.length / 2 - 1] + sortedTimes[sortedTimes.length / 2]) / 2
: sortedTimes[Math.floor(sortedTimes.length / 2)];
for (let i = 0; i < sortedPositions.length - 1; i++) {
const currentPosition = sortedPositions[i];
const nextPosition = sortedPositions[i + 1];
// Calculate average for losing positions
const losingPositions = finishedPositions.filter(p => (p.profitAndLoss?.realized ?? 0) < 0);
let losingPositionsAverageOpenTime = 0;
const currentRealized = currentPosition.profitAndLoss?.realized ?? 0;
const nextRealized = nextPosition.profitAndLoss?.realized ?? 0;
if (losingPositions.length > 0) {
const losingOpenTimes = losingPositions.map(position => {
const openTime = new Date(position.open.date);
let closeTime = new Date();
// Check if current position is winning and next position is losing
if (currentRealized > 0 && nextRealized <= 0) {
// Calculate the close time of the current (winning) position
let currentCloseDate: Date | null = null;
if (currentPosition.profitAndLoss?.realized != null) {
if (currentPosition.profitAndLoss.realized > 0) {
currentCloseDate = new Date(currentPosition.takeProfit1.date);
} else {
currentCloseDate = new Date(currentPosition.stopLoss.date);
if (position.stopLoss.status === 'Filled') {
closeTime = new Date(position.stopLoss.date);
} else if (position.takeProfit1.status === 'Filled') {
closeTime = new Date(position.takeProfit1.date);
} else if (position.takeProfit2?.status === 'Filled') {
closeTime = new Date(position.takeProfit2.date);
}
return (closeTime.getTime() - openTime.getTime()) / (1000 * 60 * 60);
});
losingPositionsAverageOpenTime = losingOpenTimes.reduce((sum, time) => sum + time, 0) / losingOpenTimes.length;
}
setPositionTimingStats({
averageOpenTime,
medianOpenTime,
losingPositionsAverageOpenTime,
});
}
}
if (currentCloseDate) {
const nextOpenDate = new Date(nextPosition.open.date);
const gapInMs = nextOpenDate.getTime() - currentCloseDate.getTime();
if (gapInMs >= 0) { // Only consider positive gaps
// Convert milliseconds to number of candles
const gapInCandles = Math.floor(gapInMs / candleTimeframeMs);
allCooldownValues.push(gapInCandles);
// Calculate cooldown recommendations across all backtests
const allCooldownValues: number[] = [];
list.forEach(backtest => {
if (backtest.positions.length < 2 || !backtest.candles || backtest.candles.length < 2) {
return;
}
// Determine candle timeframe in milliseconds
const candleTimeframeMs = new Date(backtest.candles[1].date).getTime() - new Date(backtest.candles[0].date).getTime();
const sortedPositions = [...backtest.positions].sort((a, b) => {
const dateA = new Date(a.open.date).getTime();
const dateB = new Date(b.open.date).getTime();
return dateA - dateB;
});
for (let i = 0; i < sortedPositions.length - 1; i++) {
const currentPosition = sortedPositions[i];
const nextPosition = sortedPositions[i + 1];
const currentRealized = currentPosition.profitAndLoss?.realized ?? 0;
const nextRealized = nextPosition.profitAndLoss?.realized ?? 0;
// Check if current position is winning and next position is losing
if (currentRealized > 0 && nextRealized <= 0) {
// Calculate the close time of the current (winning) position
let currentCloseDate: Date | null = null;
if (currentPosition.profitAndLoss?.realized != null) {
if (currentPosition.profitAndLoss.realized > 0) {
currentCloseDate = new Date(currentPosition.takeProfit1.date);
} else {
currentCloseDate = new Date(currentPosition.stopLoss.date);
}
}
if (currentCloseDate) {
const nextOpenDate = new Date(nextPosition.open.date);
const gapInMs = nextOpenDate.getTime() - currentCloseDate.getTime();
if (gapInMs >= 0) { // Only consider positive gaps
// Convert milliseconds to number of candles
const gapInCandles = Math.floor(gapInMs / candleTimeframeMs);
allCooldownValues.push(gapInCandles);
}
}
}
}
});
if (allCooldownValues.length > 0) {
// Calculate average cooldown
const averageCooldown = allCooldownValues.reduce((sum, value) => sum + value, 0) / allCooldownValues.length;
// Calculate median cooldown
const sortedCooldowns = [...allCooldownValues].sort((a, b) => a - b);
const medianCooldown = sortedCooldowns.length % 2 === 0
? (sortedCooldowns[sortedCooldowns.length / 2 - 1] + sortedCooldowns[sortedCooldowns.length / 2]) / 2
: sortedCooldowns[Math.floor(sortedCooldowns.length / 2)];
setCooldownRecommendations({
averageCooldown: Math.ceil(averageCooldown),
medianCooldown: Math.ceil(medianCooldown),
});
}
}
}
}
});
if (allCooldownValues.length > 0) {
// Calculate average cooldown
const averageCooldown = allCooldownValues.reduce((sum, value) => sum + value, 0) / allCooldownValues.length;
// Calculate median cooldown
const sortedCooldowns = [...allCooldownValues].sort((a, b) => a - b);
const medianCooldown = sortedCooldowns.length % 2 === 0
? (sortedCooldowns[sortedCooldowns.length / 2 - 1] + sortedCooldowns[sortedCooldowns.length / 2]) / 2
: sortedCooldowns[Math.floor(sortedCooldowns.length / 2)];
setCooldownRecommendations({
averageCooldown: Math.ceil(averageCooldown),
medianCooldown: Math.ceil(medianCooldown),
});
}
}
}
}, [list])
}, [list])
return (
<>
{isFetching ? (
<div className="flex justify-center">
<progress className="progress progress-primary w-56"></progress>
</div>
) : (
return (
<>
{list && list.length > 0 && (
<>
<div className="mb-4">
<CardText
title="Average Optimized Money Management"
content={
"SL: " + optimizedMoneyManagement.stopLoss.toFixed(2) + "% | TP: " +
optimizedMoneyManagement.takeProfit.toFixed(2) + "% | R/R: " +
(optimizedMoneyManagement.takeProfit / optimizedMoneyManagement.stopLoss || 0).toFixed(2)
}
/>
</div>
<div className="mb-4">
<CardText
title="Position Timing Statistics"
content={
"Avg: " + positionTimingStats.averageOpenTime.toFixed(1) + "h | " +
"Median: " + positionTimingStats.medianOpenTime.toFixed(1) + "h | " +
"Losing Avg: " + positionTimingStats.losingPositionsAverageOpenTime.toFixed(1) + "h"
}
/>
</div>
<div className="mb-4">
<CardText
title="Cooldown Recommendations"
content={
"Avg: " + cooldownRecommendations.averageCooldown + " candles | " +
"Median: " + cooldownRecommendations.medianCooldown + " candles"
}
/>
</div>
</>
)}
<Table
columns={columns}
data={rows}
renderRowSubCompontent={({ row }: any) => (
<BacktestRowDetails
backtest={row.original}
/>
)}
/>
{/* Bot Configuration Modal */}
{selectedBacktest && (
<UnifiedTradingModal
showModal={showBotConfigModal}
mode="createBot"
backtest={selectedBacktest}
closeModal={handleCloseBotConfigModal}
/>
)}
{isFetching ? (
<div className="flex justify-center">
<progress className="progress progress-primary w-56"></progress>
</div>
) : (
<>
{list && list.length > 0 && (
<>
<div className="mb-4">
<CardText
title="Average Optimized Money Management"
content={
"SL: " + optimizedMoneyManagement.stopLoss.toFixed(2) + "% | TP: " +
optimizedMoneyManagement.takeProfit.toFixed(2) + "% | R/R: " +
(optimizedMoneyManagement.takeProfit / optimizedMoneyManagement.stopLoss || 0).toFixed(2)
}
/>
</div>
<div className="mb-4">
<CardText
title="Position Timing Statistics"
content={
"Avg: " + positionTimingStats.averageOpenTime.toFixed(1) + "h | " +
"Median: " + positionTimingStats.medianOpenTime.toFixed(1) + "h | " +
"Losing Avg: " + positionTimingStats.losingPositionsAverageOpenTime.toFixed(1) + "h"
}
/>
</div>
<div className="mb-4">
<CardText
title="Cooldown Recommendations"
content={
"Avg: " + cooldownRecommendations.averageCooldown + " candles | " +
"Median: " + cooldownRecommendations.medianCooldown + " candles"
}
/>
</div>
</>
)}
<Table
columns={columns}
data={rows}
renderRowSubCompontent={({row}: any) => (
<BacktestRowDetails
backtest={row.original}
/>
)}
/>
{/* Backtest Configuration Modal */}
{selectedBacktestForRerun && (
<UnifiedTradingModal
showModal={showBacktestConfigModal}
mode="backtest"
backtest={selectedBacktestForRerun}
closeModal={handleCloseBacktestConfigModal}
setBacktests={setBacktests}
showLoopSlider={true}
/>
)}
{/* Bot Configuration Modal */}
{selectedBacktest && (
<UnifiedTradingModal
showModal={showBotConfigModal}
mode="createBot"
backtest={selectedBacktest}
closeModal={handleCloseBotConfigModal}
/>
)}
{/* Backtest Configuration Modal */}
{selectedBacktestForRerun && (
<UnifiedTradingModal
showModal={showBacktestConfigModal}
mode="backtest"
backtest={selectedBacktestForRerun}
closeModal={handleCloseBacktestConfigModal}
setBacktests={setBacktests}
showLoopSlider={true}
/>
)}
</>
)}
</>
)}
</>
)
)
}
export default BacktestTable

View File

@@ -47,8 +47,8 @@ type ITradeChartProps = {
walletBalances?: KeyValuePairOfDateTimeAndDecimal[] | null
indicatorsValues?: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; } | null;
stream?: Candle | null
width: number
height: number
width?: number
height?: number
}
const TradeChart = ({
@@ -62,12 +62,88 @@ const TradeChart = ({
height,
}: ITradeChartProps) => {
const chartRef = React.useRef<HTMLDivElement>(null)
const containerRef = React.useRef<HTMLDivElement>(null)
const chart = useRef<IChartApi>()
const {themeProperty} = useTheme()
const theme = themeProperty()
const series1 = useRef<ISeriesApi<'Candlestick'>>()
const [timeDiff, setTimeDiff] = useState<number>(0)
const [candleCount, setCandleCount] = useState<number>(candles.length)
const [chartDimensions, setChartDimensions] = useState({ width: 0, height: 0 })
// Get responsive dimensions
const getResponsiveDimensions = () => {
if (!containerRef.current) return { width: width || 510, height: height || 300 }
const containerWidth = containerRef.current.offsetWidth
const containerHeight = containerRef.current.offsetHeight
// Use provided dimensions if available, otherwise calculate responsive ones
if (width && height) {
return { width, height }
}
// For responsive mode, calculate based on container
const calculatedWidth = containerWidth > 0 ? containerWidth : 510
// Use different aspect ratios for different screen sizes
let aspectRatio = 0.6 // Default ratio (height/width)
if (containerWidth < 768) { // Mobile
aspectRatio = 0.8 // Taller on mobile for better visibility
} else if (containerWidth < 1024) { // Tablet
aspectRatio = 0.65
}
const calculatedHeight = height || Math.max(250, calculatedWidth * aspectRatio)
return {
width: calculatedWidth,
height: calculatedHeight
}
}
// Resize observer to handle container size changes
useEffect(() => {
if (!containerRef.current) return
const resizeObserver = new ResizeObserver(() => {
const newDimensions = getResponsiveDimensions()
setChartDimensions(newDimensions)
if (chart.current) {
chart.current.applyOptions({
width: newDimensions.width,
height: newDimensions.height
})
}
})
resizeObserver.observe(containerRef.current)
return () => {
resizeObserver.disconnect()
}
}, [width, height])
// Handle window resize for additional responsiveness
useEffect(() => {
const handleResize = () => {
setTimeout(() => {
const newDimensions = getResponsiveDimensions()
setChartDimensions(newDimensions)
if (chart.current) {
chart.current.applyOptions({
width: newDimensions.width,
height: newDimensions.height
})
}
}, 100) // Small delay to ensure container has updated
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [width, height])
function buildLine(
color: string,
@@ -164,7 +240,10 @@ const TradeChart = ({
}
useEffect(() => {
if (chartRef.current) {
if (chartRef.current && containerRef.current) {
const initialDimensions = getResponsiveDimensions()
setChartDimensions(initialDimensions)
const lineColor = theme['base-100']
chart.current = createChart(chartRef.current, {
crosshair: {
@@ -194,7 +273,7 @@ const TradeChart = ({
visible: false,
},
},
height: height,
height: initialDimensions.height,
layout: {
background: {color: theme['base-300']},
textColor: theme.accent,
@@ -213,7 +292,7 @@ const TradeChart = ({
secondsVisible: true,
timeVisible: true,
},
width: width,
width: initialDimensions.width,
})
prepareChart()
@@ -710,7 +789,15 @@ const TradeChart = ({
}
}
return <div ref={chartRef}/>
return (
<div
ref={containerRef}
className="w-full h-full"
style={{ minHeight: height || 250 }}
>
<div ref={chartRef} className="w-full h-full" />
</div>
)
}
export default TradeChart

View File

@@ -6,25 +6,24 @@ import useApiUrlStore from '../../../app/store/apiStore'
import {useCustomMoneyManagement} from '../../../app/store/customMoneyManagement'
import {useCustomScenario} from '../../../app/store/customScenario'
import {
AccountClient,
BacktestClient,
BotClient,
BotType,
DataClient,
MoneyManagement,
MoneyManagementClient,
RiskManagement,
RiskToleranceLevel,
RunBacktestRequest,
Scenario,
ScenarioClient,
ScenarioRequest,
SignalType,
StartBotRequest,
Ticker,
Timeframe,
TradingBotConfigRequest,
UpdateBotConfigRequest,
AccountClient,
BacktestClient,
BotClient,
DataClient,
MoneyManagement,
MoneyManagementClient,
RiskManagement,
RiskToleranceLevel,
RunBacktestRequest,
Scenario,
ScenarioClient,
ScenarioRequest,
SignalType,
StartBotRequest,
Ticker,
Timeframe,
TradingBotConfigRequest,
UpdateBotConfigRequest,
} from '../../../generated/ManagingApi'
import type {IUnifiedTradingConfigInput, UnifiedTradingModalProps} from '../../../global/type'
import {Loader, Slider} from '../../atoms'
@@ -197,12 +196,11 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
setGlobalCustomScenario((backtest.config as any).scenario); // Also update global store for prefilling
setSelectedScenario('custom');
} else if (backtest.config.scenarioName) {
setSelectedScenario(backtest.config.scenarioName);
setShowCustomScenario(false);
setSelectedScenario(backtest.config.scenarioName);
setShowCustomScenario(false);
}
setValue('timeframe', backtest.config.timeframe);
setValue('botType', backtest.config.botType);
setValue('cooldownPeriod', backtest.config.cooldownPeriod);
setValue('maxLossStreak', backtest.config.maxLossStreak);
setValue('maxPositionTimeHours', backtest.config.maxPositionTimeHours);
@@ -245,7 +243,6 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
}
setValue('timeframe', backtest.config.timeframe);
setValue('botType', backtest.config.botType);
setValue('cooldownPeriod', backtest.config.cooldownPeriod);
setValue('maxLossStreak', backtest.config.maxLossStreak);
setValue('maxPositionTimeHours', backtest.config.maxPositionTimeHours);
@@ -305,7 +302,6 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
}
setValue('scenarioName', config.scenarioName || '');
setValue('timeframe', config.timeframe);
setValue('botType', config.botType);
setValue('cooldownPeriod', config.cooldownPeriod);
setValue('maxLossStreak', config.maxLossStreak);
setValue('maxPositionTimeHours', config.maxPositionTimeHours);
@@ -443,13 +439,14 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
const onMoneyManagementChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
if (e.target.value === 'custom') {
setShowCustomMoneyManagement(true);
setSelectedMoneyManagement('custom'); // Set selected to 'custom'
setCustomMoneyManagement(undefined);
setGlobalCustomMoneyManagement(null); // Clear global store when creating new custom
} else {
setShowCustomMoneyManagement(false);
setSelectedMoneyManagement(e.target.value); // Update selected money management
setCustomMoneyManagement(undefined);
setGlobalCustomMoneyManagement(null); // Clear global store when switching away from custom
setSelectedMoneyManagement(e.target.value);
}
};
@@ -544,7 +541,7 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
}
console.log(form)
console.log(moneyManagement)
console.log(customMoneyManagement)
if (!moneyManagement) {
t.update('error', 'Money management is required');
return;
@@ -557,8 +554,8 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
scenario: customScenario ? convertScenarioToRequest(customScenario) : undefined,
scenarioName: customScenario ? undefined : form.scenarioName,
timeframe: form.timeframe,
botType: form.botType,
isForWatchingOnly: form.isForWatchingOnly || false,
flipPosition: false, // Default to false since we're only using isForWatchingOnly checkbox
cooldownPeriod: form.cooldownPeriod,
maxLossStreak: form.maxLossStreak,
maxPositionTimeHours: form.maxPositionTimeHours,
@@ -592,6 +589,7 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
closeModal();
} catch (error: any) {
console.error(error);
t.update('error', `Error: ${error.message || error}`);
}
};
@@ -607,7 +605,6 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
scenario: customScenario ? convertScenarioToRequest(customScenario) : undefined,
scenarioName: customScenario ? undefined : form.scenarioName,
timeframe: form.timeframe,
botType: form.botType,
isForWatchingOnly: false,
cooldownPeriod: form.cooldownPeriod,
maxLossStreak: form.maxLossStreak,
@@ -622,14 +619,13 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
useForDynamicStopLoss: form.useForDynamicStopLoss ?? true,
moneyManagementName: showCustomMoneyManagement ? undefined : selectedMoneyManagement,
moneyManagement: customMoneyManagement,
flipPosition: form.isForWatchingOnly ?? false,
};
const request: RunBacktestRequest = {
config: tradingBotConfigRequest,
startDate: new Date(form.startDate),
endDate: new Date(form.endDate),
balance: form.balance,
watchOnly: false,
save: form.save || false,
};
@@ -724,45 +720,29 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
</FormInput>
</div>
{/* Second Row: Money Management & Bot Type */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormInput label="Money Management" htmlFor="moneyManagement">
<select
className="select select-bordered w-full"
value={selectedMoneyManagement || (showCustomMoneyManagement ? 'custom' : '')}
onChange={onMoneyManagementChange}
>
{moneyManagements.length === 0 ? (
<option value="" disabled>No money management available - create a custom one below</option>
) : (
<>
{moneyManagements.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
</>
)}
<option key="custom" value="custom">
{moneyManagements.length === 0 ? 'Create Custom Money Management' : 'Custom'}
</option>
</select>
</FormInput>
<FormInput label="Bot Type" htmlFor="botType">
<select
className="select select-bordered w-full"
{...register('botType', { required: true })}
disabled={mode === 'updateBot'} // Can't change bot type for existing bots
>
{[BotType.ScalpingBot, BotType.FlippingBot].map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</FormInput>
</div>
{/* Money Management */}
<FormInput label="Money Management" htmlFor="moneyManagement">
<select
className="select select-bordered w-full"
value={selectedMoneyManagement || (showCustomMoneyManagement ? 'custom' : '')}
onChange={onMoneyManagementChange}
>
{moneyManagements.length === 0 ? (
<option value="" disabled>No money management available - create a custom one below</option>
) : (
<>
{moneyManagements.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
</>
)}
<option key="custom" value="custom">
{moneyManagements.length === 0 ? 'Create Custom Money Management' : 'Custom'}
</option>
</select>
</FormInput>
{/* Custom Money Management */}
{showCustomMoneyManagement && (

View File

@@ -504,12 +504,8 @@ export class BotClient extends AuthorizedApiBase {
return Promise.resolve<string>(null as any);
}
bot_Stop(botType: BotType | undefined, identifier: string | null | undefined): Promise<string> {
bot_Stop(identifier: string | null | undefined): Promise<string> {
let url_ = this.baseUrl + "/Bot/Stop?";
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)
url_ += "identifier=" + encodeURIComponent("" + identifier) + "&";
url_ = url_.replace(/[?&]$/, "");
@@ -2868,7 +2864,6 @@ export interface TradingBotConfig {
timeframe: Timeframe;
isForWatchingOnly: boolean;
botTradingBalance: number;
botType: BotType;
isForBacktest: boolean;
cooldownPeriod: number;
maxLossStreak: number;
@@ -3014,12 +3009,6 @@ export enum Ticker {
Unknown = "Unknown",
}
export enum BotType {
SimpleBot = "SimpleBot",
ScalpingBot = "ScalpingBot",
FlippingBot = "FlippingBot",
}
export interface RiskManagement {
adverseProbabilityThreshold: number;
favorableProbabilityThreshold: number;
@@ -3336,8 +3325,6 @@ export interface RunBacktestRequest {
config?: TradingBotConfigRequest | null;
startDate?: Date;
endDate?: Date;
balance?: number;
watchOnly?: boolean;
save?: boolean;
}
@@ -3347,8 +3334,8 @@ export interface TradingBotConfigRequest {
timeframe: Timeframe;
isForWatchingOnly: boolean;
botTradingBalance: number;
botType: BotType;
name: string;
flipPosition: boolean;
cooldownPeriod: number;
maxLossStreak: number;
scenario?: ScenarioRequest | null;
@@ -3397,6 +3384,12 @@ export interface StartBotRequest {
config?: TradingBotConfigRequest | null;
}
export enum BotType {
SimpleBot = "SimpleBot",
ScalpingBot = "ScalpingBot",
FlippingBot = "FlippingBot",
}
export interface TradingBotResponse {
status: string;
signals: Signal[];

View File

@@ -111,7 +111,6 @@ export interface TradingBotConfig {
timeframe: Timeframe;
isForWatchingOnly: boolean;
botTradingBalance: number;
botType: BotType;
isForBacktest: boolean;
cooldownPeriod: number;
maxLossStreak: number;
@@ -257,12 +256,6 @@ export enum Ticker {
Unknown = "Unknown",
}
export enum BotType {
SimpleBot = "SimpleBot",
ScalpingBot = "ScalpingBot",
FlippingBot = "FlippingBot",
}
export interface RiskManagement {
adverseProbabilityThreshold: number;
favorableProbabilityThreshold: number;
@@ -579,8 +572,6 @@ export interface RunBacktestRequest {
config?: TradingBotConfigRequest | null;
startDate?: Date;
endDate?: Date;
balance?: number;
watchOnly?: boolean;
save?: boolean;
}
@@ -590,8 +581,8 @@ export interface TradingBotConfigRequest {
timeframe: Timeframe;
isForWatchingOnly: boolean;
botTradingBalance: number;
botType: BotType;
name: string;
flipPosition: boolean;
cooldownPeriod: number;
maxLossStreak: number;
scenario?: ScenarioRequest | null;
@@ -640,6 +631,12 @@ export interface StartBotRequest {
config?: TradingBotConfigRequest | null;
}
export enum BotType {
SimpleBot = "SimpleBot",
ScalpingBot = "ScalpingBot",
FlippingBot = "FlippingBot",
}
export interface TradingBotResponse {
status: string;
signals: Signal[];

View File

@@ -6,8 +6,7 @@ import {CardPosition, CardSignal, CardText, Toast,} from '../../components/molle
import ManualPositionModal from '../../components/mollecules/ManualPositionModal'
import TradesModal from '../../components/mollecules/TradesModal/TradesModal'
import {TradeChart, UnifiedTradingModal} from '../../components/organism'
import type {BotType, MoneyManagement, Position, TradingBotResponse} from '../../generated/ManagingApi'
import {BotClient} from '../../generated/ManagingApi'
import {BotClient, BotType, MoneyManagement, Position, TradingBotResponse} from '../../generated/ManagingApi'
import type {IBotList} from '../../global/type.tsx'
import MoneyManagementModal from '../settingsPage/moneymanagement/moneyManagementModal'
@@ -162,7 +161,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
if (status == 'Up') {
client
.bot_Stop(botType, identifier)
.bot_Stop(identifier)
.then(() => {
t.update('success', 'Bot stopped')
})
@@ -195,7 +194,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
}
function getUpdateBotBadge(bot: TradingBotResponse) {
const classes = baseBadgeClass() + ' bg-warning'
const classes = baseBadgeClass() + ' bg-orange-500'
return (
<button className={classes} onClick={() => openUpdateBotModal(bot)}>
<p className="text-primary-content flex">
@@ -246,14 +245,12 @@ const BotList: React.FC<IBotList> = ({ list }) => {
className="sm:w-1 md:w-1/2 xl:w-1/2 w-full p-2"
>
<div className={cardClasses(bot.status)}>
<figure>
<figure className="w-full">
{
<TradeChart
candles={bot.candles}
positions={bot.positions}
signals={bot.signals}
width={510}
height={300}
></TradeChart>
}
</figure>
@@ -274,7 +271,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
{/* Action Badges */}
<div className="flex flex-wrap gap-1">
{getToggleBotStatusBadge(bot.status, bot.identifier, bot.config.botType)}
{getToggleBotStatusBadge(bot.status, bot.identifier, bot.config.flipPosition ? BotType.FlippingBot : BotType.SimpleBot)}
{getUpdateBotBadge(bot)}
{getManualPositionBadge(bot.identifier)}
{getDeleteBadge(bot.identifier)}
@@ -294,7 +291,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
</div>
<div className="columns-2">
<CardSignal signals={bot.signals}></CardSignal>
<CardText title="Type" content={bot.config.botType}></CardText>
<CardText title="Type" content={bot.config.flipPosition ? 'Flipping' : 'Simple'}></CardText>
</div>
<div className="columns-2">
<CardPosition