Push merge conflict
This commit is contained in:
@@ -21,6 +21,7 @@ const Modal: React.FC<IModalProps> = ({
|
||||
titleHeader={titleHeader}
|
||||
onClose={onClose}
|
||||
onSubmit={onSubmit}
|
||||
showModal={showModal}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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 ' +
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user