Add bundle backtest refact + fix whitelist

This commit is contained in:
2025-10-12 14:40:20 +07:00
parent 4543246871
commit 5acc77650f
21 changed files with 2961 additions and 628 deletions

View File

@@ -4296,8 +4296,12 @@ export interface RunBacktestRequest {
config?: TradingBotConfigRequest | null;
startDate?: Date;
endDate?: Date;
balance?: number;
watchOnly?: boolean;
save?: boolean;
withCandles?: boolean;
moneyManagementName?: string | null;
moneyManagement?: MoneyManagement | null;
}
export interface TradingBotConfigRequest {
@@ -4352,6 +4356,10 @@ export interface MoneyManagementRequest {
leverage: number;
}
export interface MoneyManagement extends LightMoneyManagement {
user?: User | null;
}
export interface BundleBacktestRequest {
requestId: string;
user: User;
@@ -4359,8 +4367,11 @@ export interface BundleBacktestRequest {
completedAt?: Date | null;
status: BundleBacktestRequestStatus;
name: string;
backtestRequestsJson: string;
results?: string[] | null;
universalConfigJson: string;
dateTimeRangesJson: string;
moneyManagementVariantsJson: string;
tickerVariantsJson: string;
results?: string[];
totalBacktests: number;
completedBacktests: number;
failedBacktests: number;
@@ -4381,7 +4392,42 @@ export enum BundleBacktestRequestStatus {
export interface RunBundleBacktestRequest {
name: string;
requests: RunBacktestRequest[];
universalConfig: BundleBacktestUniversalConfig;
dateTimeRanges: DateTimeRange[];
moneyManagementVariants: MoneyManagementVariant[];
tickerVariants: Ticker[];
}
export interface BundleBacktestUniversalConfig {
accountName: string;
timeframe: Timeframe;
isForWatchingOnly: boolean;
botTradingBalance: number;
botName: string;
flipPosition: boolean;
cooldownPeriod?: number | null;
maxLossStreak?: number;
scenario?: ScenarioRequest | null;
scenarioName?: string | null;
maxPositionTimeHours?: number | null;
closeEarlyWhenProfitable?: boolean;
flipOnlyWhenInProfit?: boolean;
useSynthApi?: boolean;
useForPositionSizing?: boolean;
useForSignalFiltering?: boolean;
useForDynamicStopLoss?: boolean;
watchOnly?: boolean;
save?: boolean;
withCandles?: boolean;
}
export interface DateTimeRange {
startDate: Date;
endDate: Date;
}
export interface MoneyManagementVariant {
moneyManagement?: MoneyManagementRequest;
}
export interface GeneticRequest {
@@ -4472,10 +4518,6 @@ export interface RunGeneticRequest {
eligibleIndicators?: IndicatorType[] | null;
}
export interface MoneyManagement extends LightMoneyManagement {
user?: User | null;
}
export interface StartBotRequest {
config?: TradingBotConfigRequest | null;
}

View File

@@ -553,8 +553,12 @@ export interface RunBacktestRequest {
config?: TradingBotConfigRequest | null;
startDate?: Date;
endDate?: Date;
balance?: number;
watchOnly?: boolean;
save?: boolean;
withCandles?: boolean;
moneyManagementName?: string | null;
moneyManagement?: MoneyManagement | null;
}
export interface TradingBotConfigRequest {
@@ -609,6 +613,10 @@ export interface MoneyManagementRequest {
leverage: number;
}
export interface MoneyManagement extends LightMoneyManagement {
user?: User | null;
}
export interface BundleBacktestRequest {
requestId: string;
user: User;
@@ -616,8 +624,11 @@ export interface BundleBacktestRequest {
completedAt?: Date | null;
status: BundleBacktestRequestStatus;
name: string;
backtestRequestsJson: string;
results?: string[] | null;
universalConfigJson: string;
dateTimeRangesJson: string;
moneyManagementVariantsJson: string;
tickerVariantsJson: string;
results?: string[];
totalBacktests: number;
completedBacktests: number;
failedBacktests: number;
@@ -638,7 +649,42 @@ export enum BundleBacktestRequestStatus {
export interface RunBundleBacktestRequest {
name: string;
requests: RunBacktestRequest[];
universalConfig: BundleBacktestUniversalConfig;
dateTimeRanges: DateTimeRange[];
moneyManagementVariants: MoneyManagementVariant[];
tickerVariants: Ticker[];
}
export interface BundleBacktestUniversalConfig {
accountName: string;
timeframe: Timeframe;
isForWatchingOnly: boolean;
botTradingBalance: number;
botName: string;
flipPosition: boolean;
cooldownPeriod?: number | null;
maxLossStreak?: number;
scenario?: ScenarioRequest | null;
scenarioName?: string | null;
maxPositionTimeHours?: number | null;
closeEarlyWhenProfitable?: boolean;
flipOnlyWhenInProfit?: boolean;
useSynthApi?: boolean;
useForPositionSizing?: boolean;
useForSignalFiltering?: boolean;
useForDynamicStopLoss?: boolean;
watchOnly?: boolean;
save?: boolean;
withCandles?: boolean;
}
export interface DateTimeRange {
startDate: Date;
endDate: Date;
}
export interface MoneyManagementVariant {
moneyManagement?: MoneyManagementRequest;
}
export interface GeneticRequest {
@@ -729,10 +775,6 @@ export interface RunGeneticRequest {
eligibleIndicators?: IndicatorType[] | null;
}
export interface MoneyManagement extends LightMoneyManagement {
user?: User | null;
}
export interface StartBotRequest {
config?: TradingBotConfigRequest | null;
}

View File

@@ -1,24 +1,281 @@
import React, {useEffect, useRef, useState} from 'react';
import {BundleBacktestRequest, LightBacktestResponse, Ticker, Timeframe} from '../../generated/ManagingApiTypes';
import {BacktestClient} from '../../generated/ManagingApi';
import {
AccountClient,
BacktestClient,
BundleBacktestRequest,
BundleBacktestUniversalConfig,
DataClient,
DateTimeRange,
IndicatorType,
LightBacktestResponse,
MoneyManagementRequest,
MoneyManagementVariant,
RunBundleBacktestRequest,
SignalType,
Ticker,
Timeframe
} from '../../generated/ManagingApi';
import useApiUrlStore from '../../app/store/apiStore';
import Toast from '../../components/mollecules/Toast/Toast';
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 FormInput from '../../components/mollecules/FormInput/FormInput';
import CustomScenario from '../../components/organism/CustomScenario/CustomScenario';
import {useCustomScenario} from '../../app/store/customScenario';
interface BundleRequestModalProps {
open: boolean;
onClose: () => void;
bundle: BundleBacktestRequest | null;
onCreateBundle?: (request: RunBundleBacktestRequest) => void;
}
const BundleRequestModal: React.FC<BundleRequestModalProps> = ({ open, onClose, bundle }) => {
const BundleRequestModal: React.FC<BundleRequestModalProps> = ({
open,
onClose,
bundle,
onCreateBundle
}) => {
const { apiUrl } = useApiUrlStore();
const [backtests, setBacktests] = useState<LightBacktestResponse[]>([]);
const signalRRef = useRef<any>(null);
// Custom scenario hook
const { scenario, setCustomScenario } = useCustomScenario();
// Form state for creating new bundle requests
const [strategyName, setStrategyName] = useState<string>('');
const [selectedAccount, setSelectedAccount] = useState<string>('');
const [selectedTimeframe, setSelectedTimeframe] = useState<Timeframe>(Timeframe.FifteenMinutes);
const [selectedTickers, setSelectedTickers] = useState<Ticker[]>([]);
const [startingCapital, setStartingCapital] = useState<number>(10000);
// Advanced parameters state
const [cooldownPeriod, setCooldownPeriod] = useState<number>(0);
const [maxLossStreak, setMaxLossStreak] = useState<number>(0);
const [maxPositionTime, setMaxPositionTime] = useState<number>(0);
const [flipPosition, setFlipPosition] = useState<boolean>(false);
const [flipOnlyWhenInProfit, setFlipOnlyWhenInProfit] = useState<boolean>(false);
const [closeEarlyWhenProfitable, setCloseEarlyWhenProfitable] = useState<boolean>(false);
// Variant arrays
const [dateTimeRanges, setDateTimeRanges] = useState<DateTimeRange[]>([
{ startDate: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000), endDate: new Date() }
]);
const [moneyManagementVariants, setMoneyManagementVariants] = useState<MoneyManagementVariant[]>([
{ moneyManagement: { name: 'Default', timeframe: Timeframe.FifteenMinutes, stopLoss: 0.01, takeProfit: 0.02, leverage: 1 } }
]);
// API clients
const backtestClient = new BacktestClient({} as any, apiUrl);
const accountClient = new AccountClient({} as any, apiUrl);
const dataClient = new DataClient({} as any, apiUrl);
// Fetch data
const { data: accounts } = useQuery({
queryFn: () => accountClient.account_GetAccounts(),
queryKey: ['accounts'],
});
const { data: tickers } = useQuery({
queryFn: () => dataClient.data_GetTickers(selectedTimeframe),
queryKey: ['tickers', selectedTimeframe],
enabled: !!selectedTimeframe,
});
// Calculate total backtests
const totalBacktests = dateTimeRanges.length * moneyManagementVariants.length * selectedTickers.length;
// Calculate estimated time for bundle backtests
const calculateEstimatedTime = (): number => {
if (totalBacktests === 0) return 0;
// Base time per timeframe (in seconds) - much lower realistic values
const timeframeBaseTime: Record<Timeframe, number> = {
[Timeframe.OneMinute]: 0.01,
[Timeframe.FiveMinutes]: 0.02,
[Timeframe.FifteenMinutes]: 0.011, // ~15 seconds for 1,344 candles
[Timeframe.ThirtyMinutes]: 0.03,
[Timeframe.OneHour]: 0.04,
[Timeframe.FourHour]: 0.05,
[Timeframe.OneDay]: 0.1,
};
// Calculate total time for all date ranges
let totalTimeSeconds = 0;
dateTimeRanges.forEach(range => {
const timeDiffMs = range.endDate.getTime() - range.startDate.getTime();
const timeDiffDays = timeDiffMs / (1000 * 60 * 60 * 24);
// Get base time for the selected timeframe
const baseTimePerCandle = timeframeBaseTime[selectedTimeframe] || 15;
// Calculate candles per day based on timeframe
const candlesPerDay: Record<Timeframe, number> = {
[Timeframe.OneMinute]: 1440, // 24 * 60
[Timeframe.FiveMinutes]: 288, // 24 * 12
[Timeframe.FifteenMinutes]: 96, // 24 * 4
[Timeframe.ThirtyMinutes]: 48, // 24 * 2
[Timeframe.OneHour]: 24,
[Timeframe.FourHour]: 6,
[Timeframe.OneDay]: 1,
};
const candlesInRange = timeDiffDays * candlesPerDay[selectedTimeframe];
const timeForThisRange = candlesInRange * baseTimePerCandle;
totalTimeSeconds += timeForThisRange;
});
// Multiply by number of variants (money management and tickers)
const variantMultiplier = moneyManagementVariants.length * selectedTickers.length;
const totalEstimatedSeconds = totalTimeSeconds * variantMultiplier;
return Math.ceil(totalEstimatedSeconds);
};
const estimatedTimeSeconds = calculateEstimatedTime();
// Add date range variant
const addDateTimeRange = () => {
const newRange: DateTimeRange = {
startDate: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000),
endDate: new Date()
};
setDateTimeRanges(prev => [...prev, newRange]);
};
// Remove date range variant
const removeDateTimeRange = (index: number) => {
if (dateTimeRanges.length > 1) {
setDateTimeRanges(prev => prev.filter((_, i) => i !== index));
}
};
// Update date range
const updateDateTimeRange = (index: number, field: 'startDate' | 'endDate', value: Date) => {
setDateTimeRanges(prev => prev.map((range, i) =>
i === index ? { ...range, [field]: value } : range
));
};
// Add money management variant
const addMoneyManagementVariant = () => {
const newVariant: MoneyManagementVariant = {
moneyManagement: {
name: `MM ${moneyManagementVariants.length + 1}`,
timeframe: selectedTimeframe,
stopLoss: 0.01,
takeProfit: 0.02,
leverage: 1
}
};
setMoneyManagementVariants(prev => [...prev, newVariant]);
};
// Remove money management variant
const removeMoneyManagementVariant = (index: number) => {
if (moneyManagementVariants.length > 1) {
setMoneyManagementVariants(prev => prev.filter((_, i) => i !== index));
}
};
// Update money management variant
const updateMoneyManagementVariant = (index: number, field: keyof MoneyManagementRequest, value: any) => {
setMoneyManagementVariants(prev => prev.map((variant, i) =>
i === index ? {
...variant,
moneyManagement: {
...variant.moneyManagement!,
[field]: value
}
} : variant
));
};
// Handle ticker selection
const handleTickerToggle = (ticker: Ticker) => {
setSelectedTickers(prev => {
const isSelected = prev.includes(ticker);
if (isSelected) {
return prev.filter(t => t !== ticker);
} else {
return [...prev, ticker];
}
});
};
// Create bundle backtest request
const handleCreateBundle = async () => {
if (!strategyName || !selectedAccount || selectedTickers.length === 0) {
new Toast('Please fill in all required fields', false);
return;
}
if (!scenario) {
new Toast('Please create a scenario with indicators', false);
return;
}
const universalConfig: BundleBacktestUniversalConfig = {
accountName: selectedAccount,
timeframe: selectedTimeframe,
isForWatchingOnly: false,
botTradingBalance: startingCapital,
botName: strategyName,
flipPosition: flipPosition,
cooldownPeriod: cooldownPeriod,
maxLossStreak: maxLossStreak,
scenario: scenario ? {
name: scenario.name || 'Custom Scenario',
indicators: (scenario.indicators || []).map(indicator => ({
name: indicator.name || 'Indicator',
type: indicator.type || IndicatorType.EmaCross,
signalType: indicator.signalType || SignalType.Signal,
period: indicator.period || 14,
fastPeriods: indicator.fastPeriods || 12,
slowPeriods: indicator.slowPeriods || 26,
signalPeriods: indicator.signalPeriods || 9,
multiplier: indicator.multiplier || 3.0,
stochPeriods: indicator.stochPeriods || 14,
smoothPeriods: indicator.smoothPeriods || 3,
cyclePeriods: indicator.cyclePeriods || 10
})),
loopbackPeriod: scenario.loopbackPeriod || 1
} : undefined,
maxPositionTimeHours: maxPositionTime > 0 ? maxPositionTime : null,
closeEarlyWhenProfitable: closeEarlyWhenProfitable,
flipOnlyWhenInProfit: flipOnlyWhenInProfit,
useSynthApi: false,
useForPositionSizing: true,
useForSignalFiltering: true,
useForDynamicStopLoss: true,
watchOnly: false,
save: true,
withCandles: false
};
const request: RunBundleBacktestRequest = {
name: strategyName,
universalConfig,
dateTimeRanges,
moneyManagementVariants,
tickerVariants: selectedTickers
};
try {
await onCreateBundle?.(request);
new Toast('Bundle backtest request created successfully!', true);
onClose();
} catch (error) {
new Toast('Failed to create bundle backtest request', false);
}
};
// Existing bundle viewing logic
const {
data: queryBacktests,
isLoading,
@@ -28,8 +285,7 @@ const BundleRequestModal: React.FC<BundleRequestModalProps> = ({ open, onClose,
queryKey: ['bundle-backtests', bundle?.requestId],
queryFn: async () => {
if (!open || !bundle) return [];
const client = new BacktestClient({} as any, apiUrl);
const res = await client.backtest_GetBacktestsByRequestId(bundle.requestId);
const res = await backtestClient.backtest_GetBacktestsByRequestId(bundle.requestId);
if (!res) return [];
return res.map((b: any) => {
// Map enums for ticker and timeframe
@@ -61,11 +317,12 @@ const BundleRequestModal: React.FC<BundleRequestModalProps> = ({ open, onClose,
enabled: !!open && !!bundle,
refetchOnWindowFocus: false,
});
useEffect(() => {
if (queryBacktests) setBacktests(queryBacktests);
}, [queryBacktests]);
// SignalR live updates
// SignalR live updates for existing bundles
useEffect(() => {
if (!open || !bundle) return;
if (bundle.status !== 'Pending' && bundle.status !== 'Running') return;
@@ -132,26 +389,471 @@ const BundleRequestModal: React.FC<BundleRequestModalProps> = ({ open, onClose,
};
}, [open, bundle, apiUrl]);
if (!open || !bundle) return null;
if (!open) return null;
// If viewing an existing bundle
if (bundle) {
return (
<div className="modal modal-open">
<div className="modal-box max-w-4xl">
<h3 className="font-bold text-lg mb-2">Bundle: {bundle.name}</h3>
<div className="mb-2 text-sm">
<div><b>Request ID:</b> <span className="font-mono text-xs">{bundle.requestId}</span></div>
<div><b>Status:</b> <span className={`badge badge-sm ml-1`}>{bundle.status}</span></div>
<div><b>Created:</b> {bundle.createdAt ? new Date(bundle.createdAt).toLocaleString() : '-'}</div>
<div><b>Completed:</b> {bundle.completedAt ? new Date(bundle.completedAt).toLocaleString() : '-'}</div>
</div>
<div className="divider">Backtest Results</div>
{isLoading ? (
<div>Loading backtests...</div>
) : queryError ? (
<div className="text-error">{(queryError as any)?.message || 'Failed to fetch backtests'}</div>
) : (
<BacktestTable list={backtests} />
)}
<div className="modal-action">
<button className="btn" onClick={onClose}>Close</button>
</div>
</div>
</div>
);
}
// Create new bundle form
return (
<div className="modal modal-open">
<div className="modal-box max-w-4xl">
<h3 className="font-bold text-lg mb-2">Bundle: {bundle.name}</h3>
<div className="mb-2 text-sm">
<div><b>Request ID:</b> <span className="font-mono text-xs">{bundle.requestId}</span></div>
<div><b>Status:</b> <span className={`badge badge-sm ml-1`}>{bundle.status}</span></div>
<div><b>Created:</b> {bundle.createdAt ? new Date(bundle.createdAt).toLocaleString() : '-'}</div>
<div><b>Completed:</b> {bundle.completedAt ? new Date(bundle.completedAt).toLocaleString() : '-'}</div>
<div className="modal-box max-w-6xl">
<h3 className="font-bold text-lg mb-6">Create Bundle Backtest</h3>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Left Column - Strategy Configuration */}
<div className="space-y-6">
{/* Strategy Name */}
<FormInput label="Name your strategy" htmlFor="strategyName">
<input
type="text"
className="input input-bordered w-full"
placeholder="Road to $100k..."
value={strategyName}
onChange={(e) => setStrategyName(e.target.value)}
/>
</FormInput>
{/* Account Selection */}
<FormInput label="Select Account" htmlFor="account">
<select
className="select select-bordered w-full"
value={selectedAccount}
onChange={(e) => setSelectedAccount(e.target.value)}
>
<option value="" disabled>Select an account</option>
{accounts?.map((account) => (
<option key={account.name} value={account.name}>
{account.name}
</option>
))}
</select>
</FormInput>
{/* Scenario Builder */}
<div>
<h4 className="font-semibold mb-2">Build your trading scenario</h4>
<p className="text-sm text-gray-600 mb-4">
Create a custom scenario with indicators and parameters for your automated trading strategy.
</p>
<CustomScenario
onCreateScenario={setCustomScenario}
showCustomScenario={true}
/>
</div>
{/* Asset Selection */}
<FormInput label="Select asset(s)" htmlFor="tickers">
<p className="text-sm text-gray-600 mb-2">Select what your agent trades.</p>
<div className="grid grid-cols-3 gap-2 max-h-40 overflow-y-auto">
{tickers?.map((tickerInfo) => (
<button
key={tickerInfo.ticker}
className={`btn btn-sm ${
selectedTickers.includes(tickerInfo.ticker!)
? 'btn-primary'
: 'btn-outline'
}`}
onClick={() => handleTickerToggle(tickerInfo.ticker!)}
>
{tickerInfo.ticker}
</button>
))}
</div>
</FormInput>
{/* Timeframe Selection */}
<FormInput label="Select timeframe" htmlFor="timeframe">
<p className="text-sm text-gray-600 mb-2">This sets how often your strategy reads and reacts to market data</p>
<select
className="select select-bordered w-full"
value={selectedTimeframe}
onChange={(e) => setSelectedTimeframe(e.target.value as Timeframe)}
>
{Object.values(Timeframe).map((tf) => (
<option key={tf} value={tf}>{tf}</option>
))}
</select>
</FormInput>
{/* Money Management Variants */}
<div>
<h4 className="font-semibold mb-2">Choose your money management approach(s)</h4>
<p className="text-sm text-gray-600 mb-4">
Select the approach that fits your goals and keep your strategy's risk in check.
</p>
{moneyManagementVariants.map((variant, index) => (
<div key={index} className="card bg-base-200 p-4 mb-4">
<div className="flex justify-between items-center mb-3">
<h5 className="font-medium">Money Management {index + 1}</h5>
{moneyManagementVariants.length > 1 && (
<button
className="btn btn-error btn-sm"
onClick={() => removeMoneyManagementVariant(index)}
>
Remove
</button>
)}
</div>
<div className="grid grid-cols-3 gap-4">
<FormInput label="Leverage" htmlFor={`leverage-${index}`}>
<input
type="number"
className="input input-bordered w-full"
value={variant.moneyManagement?.leverage || 1}
onChange={(e) => updateMoneyManagementVariant(index, 'leverage', parseFloat(e.target.value))}
min="1"
step="0.1"
/>
</FormInput>
<FormInput label="TP %" htmlFor={`takeProfit-${index}`}>
<input
type="number"
className="input input-bordered w-full"
value={variant.moneyManagement?.takeProfit || 0}
onChange={(e) => updateMoneyManagementVariant(index, 'takeProfit', parseFloat(e.target.value))}
min="0"
step="0.01"
/>
</FormInput>
<FormInput label="SL %" htmlFor={`stopLoss-${index}`}>
<input
type="number"
className="input input-bordered w-full"
value={variant.moneyManagement?.stopLoss || 0}
onChange={(e) => updateMoneyManagementVariant(index, 'stopLoss', parseFloat(e.target.value))}
min="0"
step="0.01"
/>
</FormInput>
</div>
</div>
))}
<button
className="btn btn-outline btn-sm"
onClick={addMoneyManagementVariant}
>
+ Add variant
</button>
</div>
</div>
{/* Right Column - Test Period & Backtest */}
<div className="space-y-6">
{/* Test Period */}
<div>
<h4 className="font-semibold mb-2">Select the test period</h4>
<p className="text-sm text-gray-600 mb-4">
Pick a historical range to evaluate your strategy.
</p>
<div className="flex justify-between items-center mb-4">
<label className="label">Date range</label>
<button
className="btn btn-outline btn-sm"
onClick={addDateTimeRange}
>
+ Add variant
</button>
</div>
{dateTimeRanges.map((range, index) => (
<div key={index} className="card bg-base-200 p-4 mb-4">
<div className="flex justify-between items-center mb-3">
<h5 className="font-medium">Date Range {index + 1}</h5>
{dateTimeRanges.length > 1 && (
<button
className="btn btn-error btn-sm"
onClick={() => removeDateTimeRange(index)}
>
Remove
</button>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<FormInput label="Start Date" htmlFor={`startDate-${index}`}>
<input
type="date"
className="input input-bordered w-full"
value={range.startDate.toISOString().split('T')[0]}
onChange={(e) => updateDateTimeRange(index, 'startDate', new Date(e.target.value))}
/>
</FormInput>
<FormInput label="End Date" htmlFor={`endDate-${index}`}>
<input
type="date"
className="input input-bordered w-full"
value={range.endDate.toISOString().split('T')[0]}
onChange={(e) => updateDateTimeRange(index, 'endDate', new Date(e.target.value))}
/>
</FormInput>
</div>
</div>
))}
</div>
{/* Advanced Parameters */}
<div className="collapse collapse-arrow bg-base-200">
<input type="checkbox" />
<div className="collapse-title font-medium flex items-center justify-between">
<div>
<h4 className="font-semibold">Advanced Parameters</h4>
<p className="text-sm text-gray-600 font-normal">
Refine your strategy with additional limits.
</p>
</div>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</div>
<div className="collapse-content">
<div className="space-y-6">
{/* Cooldown Period */}
<div className="flex items-center justify-between py-3 border-b border-base-300">
<label className="label-text font-medium text-base">Cooldown Period</label>
<div className="flex items-center">
<button
type="button"
className="btn btn-sm btn-outline rounded-r-none"
onClick={() => setCooldownPeriod(Math.max(0, cooldownPeriod - 1))}
>
-
</button>
<input
type="number"
className="input input-bordered rounded-none text-center w-20"
value={cooldownPeriod}
onChange={(e) => setCooldownPeriod(Math.max(0, parseInt(e.target.value) || 0))}
min="0"
/>
<button
type="button"
className="btn btn-sm btn-outline rounded-l-none"
onClick={() => setCooldownPeriod(cooldownPeriod + 1)}
>
+
</button>
</div>
</div>
{/* Max Loss Streak */}
<div className="flex items-center justify-between py-3 border-b border-base-300">
<label className="label-text font-medium text-base">Max Loss Streak</label>
<div className="flex items-center">
<button
type="button"
className="btn btn-sm btn-outline rounded-r-none"
onClick={() => setMaxLossStreak(Math.max(0, maxLossStreak - 1))}
>
-
</button>
<input
type="number"
className="input input-bordered rounded-none text-center w-20"
value={maxLossStreak}
onChange={(e) => setMaxLossStreak(Math.max(0, parseInt(e.target.value) || 0))}
min="0"
/>
<button
type="button"
className="btn btn-sm btn-outline rounded-l-none"
onClick={() => setMaxLossStreak(maxLossStreak + 1)}
>
+
</button>
</div>
</div>
{/* Max Position Time */}
<div className="flex items-center justify-between py-3 border-b border-base-300">
<label className="label-text font-medium text-base">Max Position Time</label>
<div className="flex items-center">
<button
type="button"
className="btn btn-sm btn-outline rounded-r-none"
onClick={() => setMaxPositionTime(Math.max(0, maxPositionTime - 1))}
>
-
</button>
<input
type="number"
className="input input-bordered rounded-none text-center w-20"
value={maxPositionTime}
onChange={(e) => setMaxPositionTime(Math.max(0, parseInt(e.target.value) || 0))}
min="0"
/>
<button
type="button"
className="btn btn-sm btn-outline rounded-l-none"
onClick={() => setMaxPositionTime(maxPositionTime + 1)}
>
+
</button>
</div>
</div>
{/* Toggle Switches */}
<div className="space-y-4">
{/* Position Flipping */}
<div className="form-control">
<div className="flex items-center justify-between">
<div className="flex-1">
<label className="label-text font-medium">Position flipping</label>
<p className="text-sm text-gray-600">
When this switch is on, the bot can flip between long and short positions as signals change - an aggressive style.
</p>
</div>
<input
type="checkbox"
className="toggle toggle-primary"
checked={flipPosition}
onChange={(e) => setFlipPosition(e.target.checked)}
/>
</div>
</div>
{/* Flip Only When In Profit */}
<div className="form-control">
<div className="flex items-center justify-between">
<div className="flex-1">
<label className="label-text font-medium">Flip only when in profit</label>
<p className="text-sm text-gray-600">
When this switch is on, the bot flips sides only if the current position is profitable, limiting flips during drawdowns.
</p>
</div>
<input
type="checkbox"
className="toggle toggle-primary"
checked={flipOnlyWhenInProfit}
onChange={(e) => setFlipOnlyWhenInProfit(e.target.checked)}
/>
</div>
</div>
{/* Close Early When Profitable */}
<div className="form-control">
<div className="flex items-center justify-between">
<div className="flex-1">
<label className="label-text font-medium">Close early when profitable</label>
<p className="text-sm text-gray-600">
When this switch is on, the bot exits positions as soon as they turn profitable, locking in gains sooner.
</p>
</div>
<input
type="checkbox"
className="toggle toggle-primary"
checked={closeEarlyWhenProfitable}
onChange={(e) => setCloseEarlyWhenProfitable(e.target.checked)}
/>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Starting Capital */}
<FormInput label="Starting Capital" htmlFor="startingCapital">
<input
type="number"
className="input input-bordered w-full"
value={startingCapital}
onChange={(e) => setStartingCapital(parseFloat(e.target.value))}
min="1"
step="0.01"
/>
</FormInput>
{/* Backtest Cart */}
<div className="card bg-base-200 p-6">
<div className="flex items-center mb-4">
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clipRule="evenodd" />
</svg>
<h4 className="font-semibold">Backtest Cart</h4>
</div>
<p className="text-sm text-gray-600 mb-4">
A summary of your strategy creation.
</p>
<div className="space-y-2 mb-4">
<div className="flex justify-between">
<span>Total number of backtests</span>
<span className="font-medium">{totalBacktests}</span>
</div>
<div className="flex justify-between">
<span>Total number of credits used</span>
<span className="font-medium flex items-center">
{totalBacktests}
<svg className="w-4 h-4 ml-1 text-orange-500" fill="currentColor" viewBox="0 0 20 20">
<circle cx="10" cy="10" r="8" />
</svg>
</span>
</div>
<div className="flex justify-between">
<span>Estimated time</span>
<span className="font-medium">
{estimatedTimeSeconds < 60
? `${estimatedTimeSeconds}s`
: estimatedTimeSeconds < 3600
? `${Math.ceil(estimatedTimeSeconds / 60)}m`
: `${Math.ceil(estimatedTimeSeconds / 3600)}h ${Math.ceil((estimatedTimeSeconds % 3600) / 60)}m`
}
</span>
</div>
</div>
<button
className="btn btn-primary w-full mb-4"
onClick={handleCreateBundle}
disabled={!strategyName || !selectedAccount || selectedTickers.length === 0 || !scenario}
>
Run Backtest
</button>
<div className="flex justify-between">
<button className="btn btn-outline btn-sm">
<svg className="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4 4a2 2 0 00-2 2v8a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2H4zm12 12V6H4v10h12zM8 8a1 1 0 000 2h4a1 1 0 100-2H8z" clipRule="evenodd" />
</svg>
Save this backtest
</button>
<button className="btn btn-ghost btn-sm">
Clear all
</button>
</div>
</div>
</div>
</div>
<div className="divider">Backtest Results</div>
{isLoading ? (
<div>Loading backtests...</div>
) : queryError ? (
<div className="text-error">{(queryError as any)?.message || 'Failed to fetch backtests'}</div>
) : (
<BacktestTable list={backtests} />
)}
<div className="modal-action">
<button className="btn" onClick={onClose}>Close</button>
</div>

View File

@@ -1,476 +1,11 @@
import React, {useEffect, useState} from 'react';
import {AccountClient, 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 React from 'react';
import BundleRequestsTable from './bundleRequestsTable';
import {useQuery} from '@tanstack/react-query';
// 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()
// API clients
const accountClient = new AccountClient({}, apiUrl);
// Data fetching
const { data: accounts, isSuccess } = useQuery({
queryFn: async () => {
const fetchedAccounts = await accountClient.account_GetAccounts();
return fetchedAccounts;
},
queryKey: ['accounts'],
});
// 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 [accountName, setAccountName] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const { scenario, setCustomScenario } = useCustomScenario();
// Set account name when accounts are loaded
useEffect(() => {
if (accounts && accounts.length > 0 && !accountName) {
setAccountName(accounts[0].name);
}
}, [accounts, accountName]);
// 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: accountName,
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 (!accountName) throw new Error('Account selection 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>
{/* Select account */}
<div className="mb-4">
<label className="label">Select account</label>
<select
className="select select-bordered w-full"
value={accountName}
onChange={e => setAccountName(e.target.value)}
disabled={!accounts || accounts.length === 0}
>
{!accounts || accounts.length === 0 ? (
<option value="">Loading accounts...</option>
) : (
accounts.map(account => (
<option key={account.name} value={account.name}>
{account.name}
</option>
))
)}
</select>
</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>
<BundleRequestsTable />
</div>
);
};

View File

@@ -148,12 +148,28 @@ const BundleRequestsTable = () => {
return (
<div className="w-full">
<h2 className="text-lg font-bold mb-2">Bundle Backtest Requests</h2>
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-bold">Bundle Backtest Requests</h2>
<button
className="btn btn-primary"
onClick={() => {
setSelectedBundle(null);
setModalOpen(true);
}}
>
Create New Bundle
</button>
</div>
<Table columns={columns} data={data} showPagination={true} />
<BundleRequestModal
open={modalOpen}
onClose={() => setModalOpen(false)}
bundle={selectedBundle}
onCreateBundle={async (request) => {
const client = new BacktestClient({} as any, apiUrl);
await client.backtest_RunBundle(request);
fetchData(); // Refresh the table
}}
/>
</div>
);