Add bundle version number on the backtest name

This commit is contained in:
2025-11-12 18:11:39 +07:00
parent e8a21a03d9
commit 57ba32f31e
3 changed files with 19 additions and 80 deletions

View File

@@ -616,6 +616,11 @@ public class BacktestController : BaseController
return BadRequest("Universal configuration is required"); return BadRequest("Universal configuration is required");
} }
if (request.UniversalConfig.Scenario == null)
{
return BadRequest("Scenario object must be provided in universal configuration for bundle backtest");
}
if (request.DateTimeRanges == null || !request.DateTimeRanges.Any()) if (request.DateTimeRanges == null || !request.DateTimeRanges.Any())
{ {
return BadRequest("At least one DateTime range is required"); return BadRequest("At least one DateTime range is required");
@@ -705,7 +710,8 @@ public class BacktestController : BaseController
return; return;
} }
var savedBundleRequest = backtester.GetBundleBacktestRequestByIdForUser(reloadedUser, bundleRequestId); var savedBundleRequest =
backtester.GetBundleBacktestRequestByIdForUser(reloadedUser, bundleRequestId);
if (savedBundleRequest != null) if (savedBundleRequest != null)
{ {
await backtester.CreateJobsForBundleRequestAsync(savedBundleRequest); await backtester.CreateJobsForBundleRequestAsync(savedBundleRequest);

View File

@@ -195,7 +195,7 @@ public class JobService
MaxPositionTimeHours = backtestRequest.Config.MaxPositionTimeHours, MaxPositionTimeHours = backtestRequest.Config.MaxPositionTimeHours,
FlipOnlyWhenInProfit = backtestRequest.Config.FlipOnlyWhenInProfit, FlipOnlyWhenInProfit = backtestRequest.Config.FlipOnlyWhenInProfit,
FlipPosition = backtestRequest.Config.FlipPosition, FlipPosition = backtestRequest.Config.FlipPosition,
Name = $"{bundleRequest.Name} #{i + 1}", Name = $"{bundleRequest.Name} v{bundleRequest.Version} #{i + 1}",
CloseEarlyWhenProfitable = backtestRequest.Config.CloseEarlyWhenProfitable, CloseEarlyWhenProfitable = backtestRequest.Config.CloseEarlyWhenProfitable,
UseSynthApi = backtestRequest.Config.UseSynthApi, UseSynthApi = backtestRequest.Config.UseSynthApi,
UseForPositionSizing = backtestRequest.Config.UseForPositionSizing, UseForPositionSizing = backtestRequest.Config.UseForPositionSizing,
@@ -233,7 +233,8 @@ public class JobService
{ {
try try
{ {
var refundSuccess = await _kaigenService.RefundUserCreditsAsync(creditRequestId, bundleRequest.User); var refundSuccess =
await _kaigenService.RefundUserCreditsAsync(creditRequestId, bundleRequest.User);
if (refundSuccess) if (refundSuccess)
{ {
_logger.LogInformation( _logger.LogInformation(
@@ -243,7 +244,8 @@ public class JobService
} }
catch (Exception refundEx) catch (Exception refundEx)
{ {
_logger.LogError(refundEx, "Error during refund attempt for user {UserName}", bundleRequest.User.Name); _logger.LogError(refundEx, "Error during refund attempt for user {UserName}",
bundleRequest.User.Name);
} }
} }
@@ -270,7 +272,8 @@ public class JobService
// Running jobs should be handled by stale job recovery, not manual retry // Running jobs should be handled by stale job recovery, not manual retry
if (job.Status != JobStatus.Failed && job.Status != JobStatus.Cancelled) if (job.Status != JobStatus.Failed && job.Status != JobStatus.Cancelled)
{ {
throw new InvalidOperationException($"Cannot retry job with status {job.Status}. Only Failed or Cancelled jobs can be retried."); throw new InvalidOperationException(
$"Cannot retry job with status {job.Status}. Only Failed or Cancelled jobs can be retried.");
} }
// Reset job to pending state // Reset job to pending state
@@ -304,4 +307,3 @@ public class JobService
_logger.LogInformation("Deleted job {JobId}", jobId); _logger.LogInformation("Deleted job {JobId}", jobId);
} }
} }

View File

@@ -18,8 +18,6 @@ import {
import useApiUrlStore from '../../app/store/apiStore'; import useApiUrlStore from '../../app/store/apiStore';
import Toast from '../../components/mollecules/Toast/Toast'; import Toast from '../../components/mollecules/Toast/Toast';
import {useQuery} from '@tanstack/react-query'; import {useQuery} from '@tanstack/react-query';
import * as signalR from '@microsoft/signalr';
import AuthorizedApiBase from '../../generated/AuthorizedApiBase';
import BacktestTable from '../../components/organism/Backtest/backtestTable'; import BacktestTable from '../../components/organism/Backtest/backtestTable';
import FormInput from '../../components/mollecules/FormInput/FormInput'; import FormInput from '../../components/mollecules/FormInput/FormInput';
import CustomScenario from '../../components/organism/CustomScenario/CustomScenario'; import CustomScenario from '../../components/organism/CustomScenario/CustomScenario';
@@ -337,73 +335,6 @@ const BundleRequestModal: React.FC<BundleRequestModalProps> = ({
if (queryBacktests) setBacktests(queryBacktests); if (queryBacktests) setBacktests(queryBacktests);
}, [queryBacktests]); }, [queryBacktests]);
// SignalR live updates for existing bundles
useEffect(() => {
if (!open || !bundle) return;
if (bundle.status !== 'Pending' && bundle.status !== 'Running') return;
let connection: any = null;
let connectionId: string = '';
let unsubscribed = false;
(async () => {
try {
connection = new signalR.HubConnectionBuilder()
.withUrl(`${apiUrl.replace(/\/$/, '')}/backtestHub`)
.withAutomaticReconnect()
.build();
await connection.start();
connectionId = connection.connectionId;
// Subscribe to bundle updates
const authBase = new AuthorizedApiBase({} as any);
let fetchOptions: any = {
method: 'POST',
headers: { 'X-SignalR-ConnectionId': connectionId },
};
fetchOptions = await authBase.transformOptions(fetchOptions);
await fetch(`${apiUrl}/backtest/Bundle/Subscribe?requestId=${bundle.requestId}`, fetchOptions);
connection.on('BundleBacktestUpdate', (result: LightBacktestResponse) => {
// Map enums if needed
if (result.config) {
if (typeof result.config.ticker === 'number') {
result.config.ticker = Ticker[result.config.ticker as keyof typeof Ticker];
} else if (typeof result.config.ticker === 'string' && Ticker[result.config.ticker as keyof typeof Ticker]) {
result.config.ticker = Ticker[result.config.ticker as keyof typeof Ticker];
}
if (typeof result.config.timeframe === 'number') {
result.config.timeframe = Timeframe[result.config.timeframe as keyof typeof Timeframe];
} else if (typeof result.config.timeframe === 'string' && Timeframe[result.config.timeframe as keyof typeof Timeframe]) {
result.config.timeframe = Timeframe[result.config.timeframe as keyof typeof Timeframe];
}
}
setBacktests((prev) => {
if (prev.some((b) => b.id === result.id)) return prev;
return [...prev, result];
});
});
signalRRef.current = connection;
} catch (e: any) {
new Toast('Failed to subscribe to live updates', false);
}
})();
return () => {
unsubscribed = true;
if (connection && connectionId) {
(async () => {
const authBase = new AuthorizedApiBase({} as any);
let fetchOptions: any = {
method: 'POST',
headers: { 'X-SignalR-ConnectionId': connectionId },
};
fetchOptions = await authBase.transformOptions(fetchOptions);
await fetch(`${apiUrl}/backtest/Bundle/Unsubscribe?requestId=${bundle.requestId}`, fetchOptions);
})();
}
if (signalRRef.current) {
signalRRef.current.stop();
signalRRef.current = null;
}
};
}, [open, bundle, apiUrl]);
if (!open) return null; if (!open) return null;
// If viewing an existing bundle // If viewing an existing bundle