Add bundle backtest refact + fix whitelist
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user