Add front for bundle
This commit is contained in:
@@ -716,13 +716,11 @@ export class BacktestClient extends AuthorizedApiBase {
|
||||
return Promise.resolve<Backtest>(null as any);
|
||||
}
|
||||
|
||||
backtest_RunBundle(name: string | null | undefined, requests: RunBacktestRequest[]): Promise<BundleBacktestRequest> {
|
||||
let url_ = this.baseUrl + "/Backtest/Bundle?";
|
||||
if (name !== undefined && name !== null)
|
||||
url_ += "name=" + encodeURIComponent("" + name) + "&";
|
||||
backtest_RunBundle(request: RunBundleBacktestRequest): Promise<BundleBacktestRequest> {
|
||||
let url_ = this.baseUrl + "/Backtest/BacktestBundle";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
const content_ = JSON.stringify(requests);
|
||||
const content_ = JSON.stringify(request);
|
||||
|
||||
let options_: RequestInit = {
|
||||
body: content_,
|
||||
@@ -4064,6 +4062,11 @@ export enum BundleBacktestRequestStatus {
|
||||
Cancelled = "Cancelled",
|
||||
}
|
||||
|
||||
export interface RunBundleBacktestRequest {
|
||||
name: string;
|
||||
requests: RunBacktestRequest[];
|
||||
}
|
||||
|
||||
export interface GeneticRequest {
|
||||
requestId: string;
|
||||
user: User;
|
||||
|
||||
@@ -713,6 +713,11 @@ export enum BundleBacktestRequestStatus {
|
||||
Cancelled = "Cancelled",
|
||||
}
|
||||
|
||||
export interface RunBundleBacktestRequest {
|
||||
name: string;
|
||||
requests: RunBacktestRequest[];
|
||||
}
|
||||
|
||||
export interface GeneticRequest {
|
||||
requestId: string;
|
||||
user: User;
|
||||
|
||||
@@ -6,10 +6,16 @@ import BacktestScanner from './backtestScanner'
|
||||
import BacktestUpload from './backtestUpload'
|
||||
import BacktestGenetic from './backtestGenetic'
|
||||
import BacktestGeneticBundle from './backtestGeneticBundle'
|
||||
import BacktestBundleForm from './backtestBundleForm';
|
||||
import type {TabsType} from '../../global/type.tsx'
|
||||
|
||||
// Tabs Array
|
||||
const tabs: TabsType = [
|
||||
{
|
||||
Component: BacktestBundleForm,
|
||||
index: 0,
|
||||
label: 'Bundle',
|
||||
},
|
||||
{
|
||||
Component: BacktestScanner,
|
||||
index: 1,
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
import React, {useState} from 'react';
|
||||
import {BacktestClient} from '../../generated/ManagingApi';
|
||||
import type {
|
||||
MoneyManagementRequest,
|
||||
RunBacktestRequest,
|
||||
ScenarioRequest,
|
||||
TradingBotConfigRequest,
|
||||
} from '../../generated/ManagingApiTypes';
|
||||
import {Ticker, Timeframe} from '../../generated/ManagingApiTypes';
|
||||
import CustomScenario from '../../components/organism/CustomScenario/CustomScenario';
|
||||
import {useCustomScenario} from '../../app/store/customScenario';
|
||||
import useApiUrlStore from '../../app/store/apiStore';
|
||||
import Toast from '../../components/mollecules/Toast/Toast';
|
||||
import BundleRequestsTable from './bundleRequestsTable';
|
||||
|
||||
// Placeholder types (replace with your actual types)
|
||||
type Indicator = { name: string; params?: Record<string, any> };
|
||||
type MoneyManagementVariant = { leverage: number; tp: number; sl: number };
|
||||
type TimeRangeVariant = { start: string; end: string };
|
||||
|
||||
type Asset = 'BTC' | 'ETH' | 'GMX';
|
||||
|
||||
const allAssets: Asset[] = ['BTC', 'ETH', 'GMX'];
|
||||
const allIndicators: Indicator[] = [
|
||||
{ name: 'EMA Cross' },
|
||||
{ name: 'MACD Cross' },
|
||||
{ name: 'SuperTrend', params: { period: 12, multiplier: 4 } },
|
||||
{ name: 'EMA Trend' },
|
||||
{ name: 'Chandelier Exit' },
|
||||
];
|
||||
|
||||
const allTimeframes = [
|
||||
{ label: '5 minutes', value: '5m' },
|
||||
{ label: '15 minutes', value: '15m' },
|
||||
{ label: '1 hour', value: '1h' },
|
||||
{ label: '4 hours', value: '4h' },
|
||||
{ label: '1 day', value: '1d' },
|
||||
];
|
||||
|
||||
const tickerMap: Record<string, Ticker> = {
|
||||
BTC: Ticker.BTC,
|
||||
ETH: Ticker.ETH,
|
||||
GMX: Ticker.GMX,
|
||||
};
|
||||
const timeframeMap: Record<string, Timeframe> = {
|
||||
'5m': Timeframe.FiveMinutes,
|
||||
'15m': Timeframe.FifteenMinutes,
|
||||
'1h': Timeframe.OneHour,
|
||||
'4h': Timeframe.FourHour,
|
||||
'1d': Timeframe.OneDay,
|
||||
};
|
||||
|
||||
const BacktestBundleForm: React.FC = () => {
|
||||
const {apiUrl} = useApiUrlStore()
|
||||
// Form state
|
||||
const [strategyName, setStrategyName] = useState('');
|
||||
const [loopback, setLoopback] = useState(14);
|
||||
// Remove selectedIndicators, use scenario from store
|
||||
const [selectedAssets, setSelectedAssets] = useState<Asset[]>([]);
|
||||
const [timeframe, setTimeframe] = useState('5m');
|
||||
const [moneyManagementVariants, setMoneyManagementVariants] = useState<MoneyManagementVariant[]>([
|
||||
{ leverage: 2, tp: 1.5, sl: 1 },
|
||||
]);
|
||||
const [timeRangeVariants, setTimeRangeVariants] = useState<TimeRangeVariant[]>([
|
||||
{ start: '', end: '' },
|
||||
]);
|
||||
const [cooldown, setCooldown] = useState(0);
|
||||
const [maxLossStreak, setMaxLossStreak] = useState(0);
|
||||
const [maxPositionTime, setMaxPositionTime] = useState(0);
|
||||
const [positionFlipping, setPositionFlipping] = useState(false);
|
||||
const [flipOnlyInProfit, setFlipOnlyInProfit] = useState(false);
|
||||
const [closeEarly, setCloseEarly] = useState(false);
|
||||
const [startingCapital, setStartingCapital] = useState(10000);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
||||
const { scenario, setCustomScenario } = useCustomScenario();
|
||||
|
||||
// Placeholder for cart summary
|
||||
const totalBacktests =
|
||||
moneyManagementVariants.length * timeRangeVariants.length * (selectedAssets.length || 1);
|
||||
|
||||
// Handlers (add/remove variants, select assets/indicators, etc.)
|
||||
// ...
|
||||
|
||||
// Generate all combinations of variants using scenario from store
|
||||
const generateRequests = (): RunBacktestRequest[] => {
|
||||
const requests: RunBacktestRequest[] = [];
|
||||
if (!scenario) return requests;
|
||||
selectedAssets.forEach(asset => {
|
||||
moneyManagementVariants.forEach(mm => {
|
||||
timeRangeVariants.forEach(tr => {
|
||||
const mmReq: MoneyManagementRequest = {
|
||||
name: `${strategyName}-MM`,
|
||||
leverage: mm.leverage,
|
||||
takeProfit: mm.tp,
|
||||
stopLoss: mm.sl,
|
||||
timeframe: timeframeMap[timeframe],
|
||||
};
|
||||
const config: TradingBotConfigRequest = {
|
||||
accountName: 'default', // TODO: let user pick
|
||||
ticker: tickerMap[asset],
|
||||
timeframe: timeframeMap[timeframe],
|
||||
isForWatchingOnly: false,
|
||||
botTradingBalance: startingCapital,
|
||||
name: `${strategyName} - ${asset}`,
|
||||
flipPosition: positionFlipping,
|
||||
cooldownPeriod: cooldown,
|
||||
maxLossStreak: maxLossStreak,
|
||||
scenario: scenario as ScenarioRequest,
|
||||
moneyManagement: mmReq,
|
||||
maxPositionTimeHours: maxPositionTime,
|
||||
closeEarlyWhenProfitable: closeEarly,
|
||||
flipOnlyWhenInProfit: flipOnlyInProfit,
|
||||
useSynthApi: false,
|
||||
useForPositionSizing: true,
|
||||
useForSignalFiltering: true,
|
||||
useForDynamicStopLoss: true,
|
||||
};
|
||||
requests.push({
|
||||
config,
|
||||
startDate: tr.start ? new Date(tr.start) : undefined,
|
||||
endDate: tr.end ? new Date(tr.end) : undefined,
|
||||
save: false,
|
||||
withCandles: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
return requests;
|
||||
};
|
||||
|
||||
// API call
|
||||
const handleRunBundle = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
const toast = new Toast('Starting bundle backtest...', true);
|
||||
try {
|
||||
const client = new BacktestClient({} as any, apiUrl);
|
||||
const requests = generateRequests();
|
||||
if (!strategyName) throw new Error('Strategy name is required');
|
||||
if (requests.length === 0) throw new Error('No backtest variants to run');
|
||||
await client.backtest_RunBundle({ name: strategyName, requests });
|
||||
setSuccess('Bundle backtest started successfully!');
|
||||
toast.update('success', 'Bundle backtest started successfully!');
|
||||
} catch (e: any) {
|
||||
setError(e.message || 'Failed to start bundle backtest');
|
||||
toast.update('error', e.message || 'Failed to start bundle backtest');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-10 max-w-7xl mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-6">Bundle Backtest</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Left column: Main form */}
|
||||
<div>
|
||||
{/* Name your strategy */}
|
||||
<div className="mb-4">
|
||||
<label className="label">Name your strategy</label>
|
||||
<input
|
||||
className="input input-bordered w-full"
|
||||
value={strategyName}
|
||||
onChange={e => setStrategyName(e.target.value)}
|
||||
placeholder="Road to $100k..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Scenario/Indicators section */}
|
||||
<div className="mb-4">
|
||||
<CustomScenario
|
||||
onCreateScenario={setCustomScenario}
|
||||
showCustomScenario={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Select asset(s) */}
|
||||
<div className="mb-4">
|
||||
<label className="label">Select asset(s)</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{allAssets.map(asset => (
|
||||
<button
|
||||
key={asset}
|
||||
className={`btn btn-sm ${selectedAssets.includes(asset) ? 'btn-primary' : 'btn-outline'}`}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setSelectedAssets(sel =>
|
||||
sel.includes(asset)
|
||||
? sel.filter(a => a !== asset)
|
||||
: [...sel, asset]
|
||||
);
|
||||
}}
|
||||
>
|
||||
{asset}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Select timeframe */}
|
||||
<div className="mb-4">
|
||||
<label className="label">Select timeframe</label>
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
value={timeframe}
|
||||
onChange={e => setTimeframe(e.target.value)}
|
||||
>
|
||||
{allTimeframes.map(tf => (
|
||||
<option key={tf.value} value={tf.value}>
|
||||
{tf.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Money management variants */}
|
||||
<div className="mb-4">
|
||||
<label className="label">Choose your money management approach(s)</label>
|
||||
{moneyManagementVariants.map((mm, idx) => (
|
||||
<div key={idx} className="flex gap-2 items-center mb-2">
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered w-16"
|
||||
value={mm.leverage}
|
||||
onChange={e => {
|
||||
const v = [...moneyManagementVariants];
|
||||
v[idx].leverage = Number(e.target.value);
|
||||
setMoneyManagementVariants(v);
|
||||
}}
|
||||
placeholder="Leverage"
|
||||
min={1}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered w-16"
|
||||
value={mm.tp}
|
||||
onChange={e => {
|
||||
const v = [...moneyManagementVariants];
|
||||
v[idx].tp = Number(e.target.value);
|
||||
setMoneyManagementVariants(v);
|
||||
}}
|
||||
placeholder="TP %"
|
||||
min={0}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered w-16"
|
||||
value={mm.sl}
|
||||
onChange={e => {
|
||||
const v = [...moneyManagementVariants];
|
||||
v[idx].sl = Number(e.target.value);
|
||||
setMoneyManagementVariants(v);
|
||||
}}
|
||||
placeholder="SL %"
|
||||
min={0}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-xs btn-error"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setMoneyManagementVariants(v => v.filter((_, i) => i !== idx));
|
||||
}}
|
||||
disabled={moneyManagementVariants.length === 1}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
className="btn btn-sm btn-outline"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setMoneyManagementVariants(v => [...v, { leverage: 1, tp: 1, sl: 1 }]);
|
||||
}}
|
||||
>
|
||||
+ Add variant
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right column: Test period, advanced params, capital, cart */}
|
||||
<div>
|
||||
{/* Test period variants */}
|
||||
<div className="mb-4">
|
||||
<label className="label">Select the test period</label>
|
||||
{timeRangeVariants.map((tr, idx) => (
|
||||
<div key={idx} className="flex gap-2 items-center mb-2">
|
||||
<input
|
||||
type="date"
|
||||
className="input input-bordered"
|
||||
value={tr.start}
|
||||
onChange={e => {
|
||||
const v = [...timeRangeVariants];
|
||||
v[idx].start = e.target.value;
|
||||
setTimeRangeVariants(v);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
className="input input-bordered"
|
||||
value={tr.end}
|
||||
onChange={e => {
|
||||
const v = [...timeRangeVariants];
|
||||
v[idx].end = e.target.value;
|
||||
setTimeRangeVariants(v);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-xs btn-error"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setTimeRangeVariants(v => v.filter((_, i) => i !== idx));
|
||||
}}
|
||||
disabled={timeRangeVariants.length === 1}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
className="btn btn-sm btn-outline"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setTimeRangeVariants(v => [...v, { start: '', end: '' }]);
|
||||
}}
|
||||
>
|
||||
+ Add variant
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Advanced Parameters */}
|
||||
<div className="mb-4">
|
||||
<div className="collapse collapse-arrow bg-base-200">
|
||||
<input type="checkbox" />
|
||||
<div className="collapse-title font-medium">Advanced Parameters</div>
|
||||
<div className="collapse-content">
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered w-20"
|
||||
value={cooldown}
|
||||
onChange={e => setCooldown(Number(e.target.value))}
|
||||
placeholder="Cooldown"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered w-20"
|
||||
value={maxLossStreak}
|
||||
onChange={e => setMaxLossStreak(Number(e.target.value))}
|
||||
placeholder="Max Loss Streak"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered w-20"
|
||||
value={maxPositionTime}
|
||||
onChange={e => setMaxPositionTime(Number(e.target.value))}
|
||||
placeholder="Max Position Time"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-control mb-2">
|
||||
<label className="cursor-pointer label">
|
||||
<span className="label-text">Position flipping</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="toggle"
|
||||
checked={positionFlipping}
|
||||
onChange={e => setPositionFlipping(e.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control mb-2">
|
||||
<label className="cursor-pointer label">
|
||||
<span className="label-text">Flip only when in profit</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="toggle"
|
||||
checked={flipOnlyInProfit}
|
||||
onChange={e => setFlipOnlyInProfit(e.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control mb-2">
|
||||
<label className="cursor-pointer label">
|
||||
<span className="label-text">Close early when profitable</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="toggle"
|
||||
checked={closeEarly}
|
||||
onChange={e => setCloseEarly(e.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Starting Capital */}
|
||||
<div className="mb-4">
|
||||
<label className="label">Starting Capital</label>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered w-full"
|
||||
value={startingCapital}
|
||||
onChange={e => setStartingCapital(Number(e.target.value))}
|
||||
min={1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Backtest Cart */}
|
||||
<div className="mb-4 bg-base-200 rounded-lg p-4">
|
||||
<div className="font-bold mb-2">Backtest Cart</div>
|
||||
<div>Total number of backtests: <span className="font-mono">{totalBacktests}</span></div>
|
||||
<div>Total number of credits used: <span className="font-mono">{totalBacktests}</span></div>
|
||||
<div>Estimated time: <span className="font-mono">~ 1 min</span></div>
|
||||
<button className="btn btn-primary w-full mt-4" onClick={handleRunBundle}>
|
||||
Run Backtest
|
||||
</button>
|
||||
<button className="btn btn-outline w-full mt-2">Save this backtest</button>
|
||||
<button className="btn btn-ghost w-full mt-2">Clear all</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-10">
|
||||
<BundleRequestsTable />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BacktestBundleForm;
|
||||
@@ -0,0 +1,146 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {BacktestClient} from '../../generated/ManagingApi';
|
||||
import useApiUrlStore from '../../app/store/apiStore';
|
||||
import Table from '../../components/mollecules/Table/Table';
|
||||
import {BundleBacktestRequest} from '../../generated/ManagingApiTypes';
|
||||
import Toast from '../../components/mollecules/Toast/Toast';
|
||||
|
||||
const BundleRequestsTable = () => {
|
||||
const { apiUrl } = useApiUrlStore();
|
||||
const [data, setData] = useState<BundleBacktestRequest[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
|
||||
const fetchData = () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const client = new BacktestClient({} as any, apiUrl);
|
||||
client.backtest_GetBundleBacktestRequests()
|
||||
.then((res) => setData(res))
|
||||
.catch((e) => setError(e.message || 'Failed to fetch bundle requests'))
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
// eslint-disable-next-line
|
||||
}, [apiUrl]);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
setDeletingId(id);
|
||||
const toast = new Toast('Deleting bundle request...', true);
|
||||
try {
|
||||
const client = new BacktestClient({} as any, apiUrl);
|
||||
await client.backtest_DeleteBundleBacktestRequest(id);
|
||||
toast.update('success', 'Bundle request deleted');
|
||||
fetchData();
|
||||
} catch (e: any) {
|
||||
toast.update('error', e.message || 'Failed to delete bundle request');
|
||||
} finally {
|
||||
setDeletingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to get badge color
|
||||
const getStatusBadgeColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Pending':
|
||||
return 'badge-warning';
|
||||
case 'Running':
|
||||
return 'badge-info';
|
||||
case 'Completed':
|
||||
return 'badge-success';
|
||||
case 'Failed':
|
||||
return 'badge-error';
|
||||
case 'Cancelled':
|
||||
return 'badge-neutral';
|
||||
default:
|
||||
return 'badge-neutral';
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
Header: 'Request ID',
|
||||
accessor: 'requestId',
|
||||
Cell: ({ value }: { value: string }) => (
|
||||
<span className="font-mono text-xs cursor-pointer" title="Copy RequestId" onClick={() => {navigator.clipboard.writeText(value); new Toast('Copied RequestId!', false);}}>{value}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
},
|
||||
{
|
||||
Header: 'Progress & Status',
|
||||
accessor: 'completedBacktests',
|
||||
Cell: ({ row }: any) => {
|
||||
const completed = row.original.completedBacktests || 0;
|
||||
const total = row.original.totalBacktests || 1;
|
||||
const failed = row.original.failedBacktests || 0;
|
||||
const status = row.original.status;
|
||||
const percent = Math.round((completed / total) * 100);
|
||||
// Progress color
|
||||
const getProgressColor = (p: number) => {
|
||||
if (status === 'Failed') return 'progress-error';
|
||||
if (status === 'Completed') return 'progress-success';
|
||||
if (p <= 25) return 'progress-error';
|
||||
if (p <= 50) return 'progress-warning';
|
||||
if (p <= 75) return 'progress-info';
|
||||
return 'progress-success';
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={`badge badge-sm ${getStatusBadgeColor(status)}`}>{status}</span>
|
||||
<div className="text-xs">
|
||||
{completed} / {total} {failed > 0 && <span className="text-error ml-1">({failed} failed)</span>}
|
||||
</div>
|
||||
</div>
|
||||
<progress className={`progress w-40 ${getProgressColor(percent)}`} value={percent} max="100" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: 'Created At',
|
||||
accessor: 'createdAt',
|
||||
Cell: ({ value }: { value: string }) => value ? new Date(value).toLocaleString() : '-',
|
||||
},
|
||||
{
|
||||
Header: 'Completed At',
|
||||
accessor: 'completedAt',
|
||||
Cell: ({ value }: { value: string }) => value ? new Date(value).toLocaleString() : '-',
|
||||
},
|
||||
{
|
||||
Header: 'Actions',
|
||||
accessor: 'actions',
|
||||
disableSortBy: true,
|
||||
Cell: ({ row }: any) => (
|
||||
<div className="flex gap-2">
|
||||
<button className="btn btn-xs btn-outline" onClick={() => new Toast(`RequestId: ${row.original.requestId}`, false)}>View</button>
|
||||
<button
|
||||
className="btn btn-xs btn-error"
|
||||
onClick={() => handleDelete(row.original.requestId)}
|
||||
disabled={deletingId === row.original.requestId}
|
||||
>
|
||||
{deletingId === row.original.requestId ? 'Deleting...' : 'Delete'}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (loading) return <div className="text-center">Loading bundle requests...</div>;
|
||||
if (error) return <div className="text-error">{error}</div>;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<h2 className="text-lg font-bold mb-2">Bundle Backtest Requests</h2>
|
||||
<Table columns={columns} data={data} showPagination={true} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BundleRequestsTable;
|
||||
Reference in New Issue
Block a user