Add Genetic workers

This commit is contained in:
2025-07-10 19:15:57 +07:00
parent c2c181e417
commit 0b4f2173e0
20 changed files with 1752 additions and 3 deletions

View File

@@ -536,6 +536,161 @@ export class BacktestClient extends AuthorizedApiBase {
}
return Promise.resolve<Backtest>(null as any);
}
backtest_RunGenetic(request: RunGeneticRequest): Promise<GeneticRequest> {
let url_ = this.baseUrl + "/Backtest/Genetic";
url_ = url_.replace(/[?&]$/, "");
const content_ = JSON.stringify(request);
let options_: RequestInit = {
body: content_,
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processBacktest_RunGenetic(_response);
});
}
protected processBacktest_RunGenetic(response: Response): Promise<GeneticRequest> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as GeneticRequest;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<GeneticRequest>(null as any);
}
backtest_GetGeneticRequests(): Promise<GeneticRequest[]> {
let url_ = this.baseUrl + "/Backtest/Genetic";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processBacktest_GetGeneticRequests(_response);
});
}
protected processBacktest_GetGeneticRequests(response: Response): Promise<GeneticRequest[]> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as GeneticRequest[];
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<GeneticRequest[]>(null as any);
}
backtest_GetGeneticRequest(id: string): Promise<GeneticRequest> {
let url_ = this.baseUrl + "/Backtest/Genetic/{id}";
if (id === undefined || id === null)
throw new Error("The parameter 'id' must be defined.");
url_ = url_.replace("{id}", encodeURIComponent("" + id));
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processBacktest_GetGeneticRequest(_response);
});
}
protected processBacktest_GetGeneticRequest(response: Response): Promise<GeneticRequest> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as GeneticRequest;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<GeneticRequest>(null as any);
}
backtest_DeleteGeneticRequest(id: string): Promise<FileResponse> {
let url_ = this.baseUrl + "/Backtest/Genetic/{id}";
if (id === undefined || id === null)
throw new Error("The parameter 'id' must be defined.");
url_ = url_.replace("{id}", encodeURIComponent("" + id));
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "DELETE",
headers: {
"Accept": "application/octet-stream"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processBacktest_DeleteGeneticRequest(_response);
});
}
protected processBacktest_DeleteGeneticRequest(response: Response): Promise<FileResponse> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200 || status === 206) {
const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined;
let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined;
let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined;
if (fileName) {
fileName = decodeURIComponent(fileName);
} else {
fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
}
return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; });
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<FileResponse>(null as any);
}
}
export class BotClient extends AuthorizedApiBase {
@@ -3513,6 +3668,54 @@ export interface MoneyManagementRequest {
leverage: number;
}
export interface GeneticRequest {
requestId: string;
user: User;
createdAt: Date;
completedAt?: Date | null;
status: GeneticRequestStatus;
ticker: Ticker;
timeframe: Timeframe;
startDate: Date;
endDate: Date;
balance: number;
populationSize: number;
generations: number;
mutationRate: number;
selectionMethod: string;
elitismPercentage: number;
maxTakeProfit: number;
eligibleIndicators: IndicatorType[];
results?: Backtest[] | null;
bestFitness?: number | null;
bestIndividual?: string | null;
errorMessage?: string | null;
progressInfo?: string | null;
}
export enum GeneticRequestStatus {
Pending = "Pending",
Running = "Running",
Completed = "Completed",
Failed = "Failed",
Cancelled = "Cancelled",
}
export interface RunGeneticRequest {
ticker?: Ticker;
timeframe?: Timeframe;
startDate?: Date;
endDate?: Date;
balance?: number;
populationSize?: number;
generations?: number;
mutationRate?: number;
selectionMethod?: string | null;
elitismPercentage?: number;
maxTakeProfit?: number;
eligibleIndicators?: IndicatorType[] | null;
}
export interface StartBotRequest {
config?: TradingBotConfigRequest | null;
}

View File

@@ -653,6 +653,54 @@ export interface MoneyManagementRequest {
leverage: number;
}
export interface GeneticRequest {
requestId: string;
user: User;
createdAt: Date;
completedAt?: Date | null;
status: GeneticRequestStatus;
ticker: Ticker;
timeframe: Timeframe;
startDate: Date;
endDate: Date;
balance: number;
populationSize: number;
generations: number;
mutationRate: number;
selectionMethod: string;
elitismPercentage: number;
maxTakeProfit: number;
eligibleIndicators: IndicatorType[];
results?: Backtest[] | null;
bestFitness?: number | null;
bestIndividual?: string | null;
errorMessage?: string | null;
progressInfo?: string | null;
}
export enum GeneticRequestStatus {
Pending = "Pending",
Running = "Running",
Completed = "Completed",
Failed = "Failed",
Cancelled = "Cancelled",
}
export interface RunGeneticRequest {
ticker?: Ticker;
timeframe?: Timeframe;
startDate?: Date;
endDate?: Date;
balance?: number;
populationSize?: number;
generations?: number;
mutationRate?: number;
selectionMethod?: string | null;
elitismPercentage?: number;
maxTakeProfit?: number;
eligibleIndicators?: IndicatorType[] | null;
}
export interface StartBotRequest {
config?: TradingBotConfigRequest | null;
}

View File

@@ -6,6 +6,7 @@ import BacktestPlayground from './backtestPlayground'
import BacktestScanner from './backtestScanner'
import BacktestUpload from './backtestUpload'
import BacktestGenetic from './backtestGenetic'
import BacktestGeneticBundle from './backtestGeneticBundle'
import type {TabsType} from '../../global/type.tsx'
// Tabs Array
@@ -30,6 +31,11 @@ const tabs: TabsType = [
index: 4,
label: 'Genetic',
},
{
Component: BacktestGeneticBundle,
index: 5,
label: 'GeneticBundle',
},
]
const Backtest: React.FC = () => {

View File

@@ -0,0 +1,472 @@
import React, {useState} from 'react'
import {useForm} from 'react-hook-form'
import {useQuery} from '@tanstack/react-query'
import useApiUrlStore from '../../app/store/apiStore'
import {
BacktestClient,
type GeneticRequest,
IndicatorType,
type RunGeneticRequest,
Ticker,
Timeframe,
} from '../../generated/ManagingApi'
import {Toast} from '../../components/mollecules'
// Available Indicator Types
const ALL_INDICATORS = [
IndicatorType.RsiDivergence,
IndicatorType.RsiDivergenceConfirm,
IndicatorType.MacdCross,
IndicatorType.EmaCross,
IndicatorType.ThreeWhiteSoldiers,
IndicatorType.SuperTrend,
IndicatorType.ChandelierExit,
IndicatorType.EmaTrend,
IndicatorType.StochRsiTrend,
IndicatorType.Stc,
IndicatorType.StDev,
IndicatorType.LaggingStc,
IndicatorType.SuperTrendCrossEma,
IndicatorType.DualEmaCross,
]
// Form Interface
interface GeneticBundleFormData {
ticker: Ticker
timeframe: Timeframe
startDate: string
endDate: string
balance: number
populationSize: number
generations: number
mutationRate: number
selectionMethod: string
elitismPercentage: number
maxTakeProfit: number
eligibleIndicators: IndicatorType[]
}
const BacktestGeneticBundle: React.FC = () => {
const {apiUrl} = useApiUrlStore()
const backtestClient = new BacktestClient({}, apiUrl)
// State
const [isSubmitting, setIsSubmitting] = useState(false)
const [selectedIndicators, setSelectedIndicators] = useState<IndicatorType[]>(ALL_INDICATORS)
const [geneticRequests, setGeneticRequests] = useState<GeneticRequest[]>([])
// Form setup
const {register, handleSubmit, watch, setValue, formState: {errors}} = useForm<GeneticBundleFormData>({
defaultValues: {
ticker: Ticker.BTC,
timeframe: Timeframe.OneHour,
startDate: getDefaultDateRange().startDate,
endDate: getDefaultDateRange().endDate,
balance: 10000,
populationSize: 10,
generations: 5,
mutationRate: 0.3,
selectionMethod: 'tournament',
elitismPercentage: 10,
maxTakeProfit: 2.0,
eligibleIndicators: ALL_INDICATORS,
}
})
const formValues = watch()
// Get default date range (last 30 days)
function getDefaultDateRange() {
const endDate = new Date()
const startDate = new Date()
startDate.setDate(startDate.getDate() - 30)
return {
startDate: startDate.toISOString().split('T')[0],
endDate: endDate.toISOString().split('T')[0],
}
}
// Fetch existing genetic requests
const {data: existingRequests, refetch: refetchRequests} = useQuery({
queryKey: ['geneticRequests'],
queryFn: async () => {
try {
const requests = await backtestClient.backtest_GetGeneticRequests()
setGeneticRequests(requests)
return requests
} catch (error) {
console.error('Error fetching genetic requests:', error)
return []
}
},
})
// Handle form submission
const onSubmit = async (data: GeneticBundleFormData) => {
if (selectedIndicators.length === 0) {
new Toast('Please select at least one indicator', false)
return
}
setIsSubmitting(true)
const t = new Toast('Creating genetic request...')
try {
const request: RunGeneticRequest = {
ticker: data.ticker,
timeframe: data.timeframe,
startDate: new Date(data.startDate),
endDate: new Date(data.endDate),
balance: data.balance,
populationSize: data.populationSize,
generations: data.generations,
mutationRate: data.mutationRate,
selectionMethod: data.selectionMethod,
elitismPercentage: data.elitismPercentage,
maxTakeProfit: data.maxTakeProfit,
eligibleIndicators: selectedIndicators,
}
const geneticRequest = await backtestClient.backtest_RunGenetic(request)
t.update('success', `Genetic request created successfully! ID: ${geneticRequest.requestId}`)
// Refresh the list of genetic requests
await refetchRequests()
// Reset form
setValue('ticker', Ticker.BTC)
setValue('timeframe', Timeframe.OneHour)
setValue('startDate', getDefaultDateRange().startDate)
setValue('endDate', getDefaultDateRange().endDate)
setValue('balance', 10000)
setValue('populationSize', 10)
setValue('generations', 5)
setValue('mutationRate', 0.3)
setValue('selectionMethod', 'tournament')
setValue('elitismPercentage', 10)
setValue('maxTakeProfit', 2.0)
setSelectedIndicators(ALL_INDICATORS)
} catch (error) {
console.error('Error creating genetic request:', error)
t.update('error', 'Failed to create genetic request. Please try again.')
} finally {
setIsSubmitting(false)
}
}
// Handle indicator selection
const handleIndicatorToggle = (indicator: IndicatorType) => {
setSelectedIndicators(prev => {
if (prev.includes(indicator)) {
return prev.filter(i => i !== indicator)
} else {
return [...prev, indicator]
}
})
}
// Get status 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'
}
}
return (
<div className="space-y-6">
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title">Genetic Algorithm Bundle</h2>
<p className="text-sm text-gray-600 mb-4">
Create a genetic algorithm request that will be processed in the background.
The algorithm will optimize trading parameters and indicator combinations.
</p>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="form-control">
<label className="label">
<span className="label-text">Ticker</span>
</label>
<select
className="select select-bordered w-full"
{...register('ticker')}
>
{Object.values(Ticker).map(ticker => (
<option key={ticker} value={ticker}>{ticker}</option>
))}
</select>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Timeframe</span>
</label>
<select
className="select select-bordered w-full"
{...register('timeframe')}
>
{Object.values(Timeframe).map(tf => (
<option key={tf} value={tf}>{tf}</option>
))}
</select>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Start Date</span>
</label>
<input
type="date"
className="input input-bordered w-full"
{...register('startDate')}
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">End Date</span>
</label>
<input
type="date"
className="input input-bordered w-full"
{...register('endDate')}
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Balance</span>
</label>
<input
type="number"
className="input input-bordered w-full"
{...register('balance', {valueAsNumber: true})}
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Population Size</span>
</label>
<input
type="number"
min="5"
max="100"
className="input input-bordered w-full"
{...register('populationSize', {valueAsNumber: true})}
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Generations</span>
</label>
<input
type="number"
min="1"
max="50"
className="input input-bordered w-full"
{...register('generations', {valueAsNumber: true})}
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Mutation Rate</span>
</label>
<input
type="number"
step="0.1"
min="0"
max="1"
className="input input-bordered w-full"
{...register('mutationRate', {valueAsNumber: true})}
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Selection Method</span>
</label>
<select
className="select select-bordered w-full"
{...register('selectionMethod')}
>
<option value="tournament">Tournament Selection</option>
<option value="roulette">Roulette Wheel</option>
<option value="fitness-weighted">Fitness Weighted</option>
</select>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Elitism Percentage</span>
</label>
<input
type="number"
min="1"
max="50"
step="1"
className="input input-bordered w-full"
{...register('elitismPercentage', {valueAsNumber: true})}
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Max Take Profit (%)</span>
</label>
<input
type="number"
min="0.9"
max="8"
step="0.1"
className="input input-bordered w-full"
{...register('maxTakeProfit', {valueAsNumber: true})}
/>
</div>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Eligible Indicators</span>
</label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 max-h-40 overflow-y-auto border rounded-lg p-4">
{ALL_INDICATORS.map(indicator => (
<label key={indicator} className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={selectedIndicators.includes(indicator)}
onChange={() => handleIndicatorToggle(indicator)}
className="checkbox checkbox-sm"
/>
<span className="text-sm">{indicator}</span>
</label>
))}
</div>
<div className="text-xs text-gray-500 mt-1">
{selectedIndicators.length} of {ALL_INDICATORS.length} indicators selected
</div>
</div>
<div className="flex gap-4">
<button
type="submit"
disabled={isSubmitting || selectedIndicators.length === 0}
className="btn btn-primary"
>
{isSubmitting ? 'Creating...' : 'Create Genetic Request'}
</button>
<button
type="button"
onClick={() => setSelectedIndicators(ALL_INDICATORS)}
className="btn btn-secondary"
>
Select All Indicators
</button>
<button
type="button"
onClick={() => setSelectedIndicators([])}
className="btn btn-secondary"
>
Clear All
</button>
</div>
</form>
</div>
</div>
{/* Existing Genetic Requests */}
{geneticRequests.length > 0 && (
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h3 className="card-title">Existing Genetic Requests</h3>
<div className="overflow-x-auto">
<table className="table table-zebra">
<thead>
<tr>
<th>ID</th>
<th>Ticker</th>
<th>Timeframe</th>
<th>Status</th>
<th>Created</th>
<th>Completed</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{geneticRequests.map((request) => (
<tr key={request.requestId}>
<td className="font-mono text-xs">{request.requestId.slice(0, 8)}...</td>
<td>{request.ticker}</td>
<td>{request.timeframe}</td>
<td>
<span className={`badge ${getStatusBadgeColor(request.status)}`}>
{request.status}
</span>
</td>
<td>{new Date(request.createdAt).toLocaleDateString()}</td>
<td>
{request.completedAt
? new Date(request.completedAt).toLocaleDateString()
: '-'
}
</td>
<td>
<div className="flex gap-2">
<button
onClick={() => {
// TODO: Implement view details
new Toast('View details not implemented yet', false)
}}
className="btn btn-sm btn-outline"
>
View
</button>
<button
onClick={async () => {
try {
await backtestClient.backtest_DeleteGeneticRequest(request.requestId)
new Toast('Request deleted successfully', false)
await refetchRequests()
} catch (error) {
new Toast('Failed to delete request', false)
}
}}
className="btn btn-sm btn-error"
>
Delete
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
</div>
)
}
export default BacktestGeneticBundle