Add front for bundle
This commit is contained in:
@@ -411,21 +411,20 @@ public class BacktestController : BaseController
|
|||||||
/// <param name="name">Display name for the bundle (required).</param>
|
/// <param name="name">Display name for the bundle (required).</param>
|
||||||
/// <returns>The bundle backtest request with ID for tracking progress.</returns>
|
/// <returns>The bundle backtest request with ID for tracking progress.</returns>
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("Bundle")]
|
[Route("BacktestBundle")]
|
||||||
public async Task<ActionResult<BundleBacktestRequest>> RunBundle([FromBody] List<RunBacktestRequest> requests,
|
public async Task<ActionResult<BundleBacktestRequest>> RunBundle([FromBody] RunBundleBacktestRequest request)
|
||||||
[FromQuery] string name)
|
|
||||||
{
|
{
|
||||||
if (requests == null || !requests.Any())
|
if (request?.Requests == null || !request.Requests.Any())
|
||||||
{
|
{
|
||||||
return BadRequest("At least one backtest request is required");
|
return BadRequest("At least one backtest request is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requests.Count > 10)
|
if (request.Requests.Count > 10)
|
||||||
{
|
{
|
||||||
return BadRequest("Maximum of 10 backtests allowed per bundle request");
|
return BadRequest("Maximum of 10 backtests allowed per bundle request");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(request.Name))
|
||||||
{
|
{
|
||||||
return BadRequest("Bundle name is required");
|
return BadRequest("Bundle name is required");
|
||||||
}
|
}
|
||||||
@@ -435,24 +434,24 @@ public class BacktestController : BaseController
|
|||||||
var user = await GetUser();
|
var user = await GetUser();
|
||||||
|
|
||||||
// Validate all requests before creating the bundle
|
// Validate all requests before creating the bundle
|
||||||
foreach (var request in requests)
|
foreach (var req in request.Requests)
|
||||||
{
|
{
|
||||||
if (request?.Config == null)
|
if (req?.Config == null)
|
||||||
{
|
{
|
||||||
return BadRequest("Invalid request: Configuration is required");
|
return BadRequest("Invalid request: Configuration is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(request.Config.AccountName))
|
if (string.IsNullOrEmpty(req.Config.AccountName))
|
||||||
{
|
{
|
||||||
return BadRequest("Invalid request: Account name is required");
|
return BadRequest("Invalid request: Account name is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(request.Config.ScenarioName) && request.Config.Scenario == null)
|
if (string.IsNullOrEmpty(req.Config.ScenarioName) && req.Config.Scenario == null)
|
||||||
{
|
{
|
||||||
return BadRequest("Invalid request: Either scenario name or scenario object is required");
|
return BadRequest("Invalid request: Either scenario name or scenario object is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(request.Config.MoneyManagementName) && request.Config.MoneyManagement == null)
|
if (string.IsNullOrEmpty(req.Config.MoneyManagementName) && req.Config.MoneyManagement == null)
|
||||||
{
|
{
|
||||||
return BadRequest(
|
return BadRequest(
|
||||||
"Invalid request: Either money management name or money management object is required");
|
"Invalid request: Either money management name or money management object is required");
|
||||||
@@ -463,16 +462,15 @@ public class BacktestController : BaseController
|
|||||||
var bundleRequest = new BundleBacktestRequest
|
var bundleRequest = new BundleBacktestRequest
|
||||||
{
|
{
|
||||||
User = user,
|
User = user,
|
||||||
BacktestRequestsJson = JsonSerializer.Serialize(requests),
|
BacktestRequestsJson = JsonSerializer.Serialize(request.Requests),
|
||||||
TotalBacktests = requests.Count,
|
TotalBacktests = request.Requests.Count,
|
||||||
CompletedBacktests = 0,
|
CompletedBacktests = 0,
|
||||||
FailedBacktests = 0,
|
FailedBacktests = 0,
|
||||||
Status = BundleBacktestRequestStatus.Pending,
|
Status = BundleBacktestRequestStatus.Pending,
|
||||||
Name = name
|
Name = request.Name
|
||||||
};
|
};
|
||||||
|
|
||||||
_backtester.InsertBundleBacktestRequestForUser(user, bundleRequest);
|
_backtester.InsertBundleBacktestRequestForUser(user, bundleRequest);
|
||||||
|
|
||||||
return Ok(bundleRequest);
|
return Ok(bundleRequest);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
11
src/Managing.Api/Models/Requests/RunBundleBacktestRequest.cs
Normal file
11
src/Managing.Api/Models/Requests/RunBundleBacktestRequest.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Managing.Api.Controllers;
|
||||||
|
|
||||||
|
namespace Managing.Api.Models.Requests;
|
||||||
|
|
||||||
|
public class RunBundleBacktestRequest
|
||||||
|
{
|
||||||
|
[Required] public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required] public List<RunBacktestRequest> Requests { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -716,13 +716,11 @@ export class BacktestClient extends AuthorizedApiBase {
|
|||||||
return Promise.resolve<Backtest>(null as any);
|
return Promise.resolve<Backtest>(null as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
backtest_RunBundle(name: string | null | undefined, requests: RunBacktestRequest[]): Promise<BundleBacktestRequest> {
|
backtest_RunBundle(request: RunBundleBacktestRequest): Promise<BundleBacktestRequest> {
|
||||||
let url_ = this.baseUrl + "/Backtest/Bundle?";
|
let url_ = this.baseUrl + "/Backtest/BacktestBundle";
|
||||||
if (name !== undefined && name !== null)
|
|
||||||
url_ += "name=" + encodeURIComponent("" + name) + "&";
|
|
||||||
url_ = url_.replace(/[?&]$/, "");
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
const content_ = JSON.stringify(requests);
|
const content_ = JSON.stringify(request);
|
||||||
|
|
||||||
let options_: RequestInit = {
|
let options_: RequestInit = {
|
||||||
body: content_,
|
body: content_,
|
||||||
@@ -4064,6 +4062,11 @@ export enum BundleBacktestRequestStatus {
|
|||||||
Cancelled = "Cancelled",
|
Cancelled = "Cancelled",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RunBundleBacktestRequest {
|
||||||
|
name: string;
|
||||||
|
requests: RunBacktestRequest[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface GeneticRequest {
|
export interface GeneticRequest {
|
||||||
requestId: string;
|
requestId: string;
|
||||||
user: User;
|
user: User;
|
||||||
|
|||||||
@@ -713,6 +713,11 @@ export enum BundleBacktestRequestStatus {
|
|||||||
Cancelled = "Cancelled",
|
Cancelled = "Cancelled",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RunBundleBacktestRequest {
|
||||||
|
name: string;
|
||||||
|
requests: RunBacktestRequest[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface GeneticRequest {
|
export interface GeneticRequest {
|
||||||
requestId: string;
|
requestId: string;
|
||||||
user: User;
|
user: User;
|
||||||
|
|||||||
@@ -6,10 +6,16 @@ import BacktestScanner from './backtestScanner'
|
|||||||
import BacktestUpload from './backtestUpload'
|
import BacktestUpload from './backtestUpload'
|
||||||
import BacktestGenetic from './backtestGenetic'
|
import BacktestGenetic from './backtestGenetic'
|
||||||
import BacktestGeneticBundle from './backtestGeneticBundle'
|
import BacktestGeneticBundle from './backtestGeneticBundle'
|
||||||
|
import BacktestBundleForm from './backtestBundleForm';
|
||||||
import type {TabsType} from '../../global/type.tsx'
|
import type {TabsType} from '../../global/type.tsx'
|
||||||
|
|
||||||
// Tabs Array
|
// Tabs Array
|
||||||
const tabs: TabsType = [
|
const tabs: TabsType = [
|
||||||
|
{
|
||||||
|
Component: BacktestBundleForm,
|
||||||
|
index: 0,
|
||||||
|
label: 'Bundle',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Component: BacktestScanner,
|
Component: BacktestScanner,
|
||||||
index: 1,
|
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