Add custom scenario
This commit is contained in:
@@ -118,9 +118,9 @@ public class BacktestController : BaseController
|
||||
return BadRequest("Account name is required");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(request.Config.ScenarioName))
|
||||
if (string.IsNullOrEmpty(request.Config.ScenarioName) && request.Config.Scenario == null)
|
||||
{
|
||||
return BadRequest("Scenario name is required");
|
||||
return BadRequest("Either scenario name or scenario object is required");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(request.MoneyManagementName) && request.MoneyManagement == null)
|
||||
@@ -131,13 +131,9 @@ public class BacktestController : BaseController
|
||||
try
|
||||
{
|
||||
Backtest backtestResult = null;
|
||||
var scenario = _scenarioService.GetScenario(request.Config.ScenarioName);
|
||||
var account = await _accountService.GetAccount(request.Config.AccountName, true, false);
|
||||
var user = await GetUser();
|
||||
|
||||
if (scenario == null)
|
||||
return BadRequest("No scenario found");
|
||||
|
||||
// Get money management
|
||||
MoneyManagement moneyManagement;
|
||||
if (!string.IsNullOrEmpty(request.MoneyManagementName))
|
||||
@@ -152,13 +148,14 @@ public class BacktestController : BaseController
|
||||
moneyManagement?.FormatPercentage();
|
||||
}
|
||||
|
||||
// Update config with money management
|
||||
// Update config with money management - TradingBot will handle scenario loading
|
||||
var backtestConfig = new TradingBotConfig
|
||||
{
|
||||
AccountName = request.Config.AccountName,
|
||||
MoneyManagement = moneyManagement,
|
||||
Ticker = request.Config.Ticker,
|
||||
ScenarioName = request.Config.ScenarioName,
|
||||
Scenario = request.Config.Scenario,
|
||||
Timeframe = request.Config.Timeframe,
|
||||
IsForWatchingOnly = request.WatchOnly,
|
||||
BotTradingBalance = request.Balance,
|
||||
@@ -170,9 +167,8 @@ public class BacktestController : BaseController
|
||||
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
|
||||
FlipPosition = request.Config.FlipPosition,
|
||||
Name = request.Config.Name ??
|
||||
$"Backtest-{request.Config.ScenarioName}-{DateTime.UtcNow:yyyyMMdd-HHmmss}",
|
||||
$"Backtest-{request.Config.ScenarioName ?? request.Config.Scenario?.Name ?? "Custom"}-{DateTime.UtcNow:yyyyMMdd-HHmmss}",
|
||||
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable,
|
||||
Scenario = scenario,
|
||||
};
|
||||
|
||||
switch (request.Config.BotType)
|
||||
|
||||
18
src/Managing.WebApp/src/app/store/customScenario.tsx
Normal file
18
src/Managing.WebApp/src/app/store/customScenario.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import {create} from 'zustand'
|
||||
|
||||
import type {Scenario} from '../../generated/ManagingApi'
|
||||
|
||||
type CustomScenarioStore = {
|
||||
setCustomScenario: (custom: Scenario) => void
|
||||
scenario: Scenario | null
|
||||
}
|
||||
|
||||
export const useCustomScenario = create<CustomScenarioStore>((set) => ({
|
||||
scenario: null,
|
||||
setCustomScenario: (custom) => {
|
||||
set((state) => ({
|
||||
...state,
|
||||
scenario: custom,
|
||||
}))
|
||||
},
|
||||
}))
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
MoneyManagement,
|
||||
MoneyManagementClient,
|
||||
RunBacktestRequest,
|
||||
Scenario,
|
||||
ScenarioClient,
|
||||
Ticker,
|
||||
Timeframe,
|
||||
@@ -21,6 +22,7 @@ import {Loader, Slider} from '../../atoms'
|
||||
import {Modal, Toast} from '../../mollecules'
|
||||
import FormInput from '../../mollecules/FormInput/FormInput'
|
||||
import CustomMoneyManagement from '../CustomMoneyManagement/CustomMoneyManagement'
|
||||
import CustomScenario from '../CustomScenario/CustomScenario'
|
||||
|
||||
const BacktestModal: React.FC<BacktestModalProps> = ({
|
||||
showModal,
|
||||
@@ -66,6 +68,13 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
|
||||
const [showCustomMoneyManagement, setShowCustomMoneyManagement] =
|
||||
useState(false)
|
||||
|
||||
const [customScenario, setCustomScenario] =
|
||||
React.useState<Scenario | undefined>(undefined)
|
||||
const [selectedScenario, setSelectedScenario] =
|
||||
useState<string>()
|
||||
const [showCustomScenario, setShowCustomScenario] =
|
||||
useState(false)
|
||||
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
|
||||
const scenarioClient = new ScenarioClient({}, apiUrl)
|
||||
@@ -75,8 +84,15 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
|
||||
const backtestClient = new BacktestClient({}, apiUrl)
|
||||
|
||||
const onSubmit: SubmitHandler<IBacktestsFormInput> = async (form) => {
|
||||
if (!form.scenarioName) {
|
||||
const t = new Toast('Please select a scenario', false);
|
||||
// Check if we have either a selected scenario or a custom scenario
|
||||
if (!form.scenarioName && !customScenario) {
|
||||
const t = new Toast('Please select a scenario or create a custom scenario', false);
|
||||
return;
|
||||
}
|
||||
|
||||
// If using custom scenario, validate it has indicators
|
||||
if (customScenario && (!customScenario.indicators || customScenario.indicators.length === 0)) {
|
||||
const t = new Toast('Custom scenario must have at least one indicator', false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -108,13 +124,15 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
|
||||
const t = new Toast(ticker + ' is running')
|
||||
|
||||
console.log(customMoneyManagement)
|
||||
console.log(customScenario)
|
||||
|
||||
try {
|
||||
// Create the TradingBotConfig
|
||||
const tradingBotConfig: TradingBotConfig = {
|
||||
accountName: form.accountName,
|
||||
ticker: ticker as Ticker,
|
||||
scenarioName: scenarioName,
|
||||
scenarioName: customScenario ? undefined : scenarioName,
|
||||
scenario: customScenario,
|
||||
timeframe: form.timeframe,
|
||||
botType: form.botType,
|
||||
isForWatchingOnly: false, // Always false for backtests
|
||||
@@ -124,7 +142,7 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
|
||||
maxPositionTimeHours: form.maxPositionTimeHours || null,
|
||||
flipOnlyWhenInProfit: form.flipOnlyWhenInProfit ?? true,
|
||||
flipPosition: form.botType === BotType.FlippingBot, // Set based on bot type
|
||||
name: `Backtest-${scenarioName}-${ticker}-${new Date().toISOString()}`,
|
||||
name: `Backtest-${customScenario ? customScenario.name : scenarioName}-${ticker}-${new Date().toISOString()}`,
|
||||
botTradingBalance: form.balance,
|
||||
moneyManagement: customMoneyManagement || moneyManagements?.find(m => m.name === selectedMoneyManagement) || moneyManagements?.[0] || {
|
||||
name: 'placeholder',
|
||||
@@ -189,6 +207,17 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
function onScenarioChange(e: any) {
|
||||
if (e.target.value === 'custom') {
|
||||
setShowCustomScenario(true)
|
||||
setCustomScenario(e.target.value)
|
||||
} else {
|
||||
setShowCustomScenario(false)
|
||||
setCustomScenario(undefined)
|
||||
setSelectedScenario(e.target.value)
|
||||
}
|
||||
}
|
||||
|
||||
const { data: accounts } = useQuery({
|
||||
queryFn: () => accountClient.account_GetAccounts(),
|
||||
queryKey: ['accounts'],
|
||||
@@ -370,7 +399,11 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
|
||||
<FormInput label="Scenario" htmlFor="scenarioName">
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
{...register('scenarioName')}
|
||||
{...register('scenarioName', {
|
||||
onChange(event) {
|
||||
onScenarioChange(event)
|
||||
},
|
||||
})}
|
||||
>
|
||||
<option value="" disabled>Select a scenario</option>
|
||||
{scenarios.map((item) => (
|
||||
@@ -378,6 +411,9 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
|
||||
{item.name || 'Unnamed Scenario'}
|
||||
</option>
|
||||
))}
|
||||
<option key="custom" value="custom">
|
||||
Custom
|
||||
</option>
|
||||
</select>
|
||||
</FormInput>
|
||||
|
||||
@@ -397,6 +433,15 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
|
||||
</FormInput>
|
||||
</div>
|
||||
|
||||
{showCustomScenario && (
|
||||
<div className="mt-6">
|
||||
<CustomScenario
|
||||
onCreateScenario={setCustomScenario}
|
||||
showCustomScenario={showCustomScenario}
|
||||
></CustomScenario>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Fourth Row: Balance & Cooldown Period */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormInput label="Balance" htmlFor="balance">
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
import React, {useEffect, useState} from 'react'
|
||||
|
||||
import type {Indicator, Scenario} from '../../../generated/ManagingApi'
|
||||
import {IndicatorType} from '../../../generated/ManagingApi'
|
||||
import FormInput from '../../mollecules/FormInput/FormInput'
|
||||
import {useCustomScenario} from '../../../app/store/customScenario'
|
||||
|
||||
type ICustomScenario = {
|
||||
onCreateScenario: (scenario: Scenario) => void
|
||||
showCustomScenario: boolean
|
||||
}
|
||||
|
||||
const CustomScenario: React.FC<ICustomScenario> = ({
|
||||
onCreateScenario,
|
||||
showCustomScenario,
|
||||
}) => {
|
||||
const { scenario, setCustomScenario } = useCustomScenario()
|
||||
|
||||
const [name, setName] = useState<string>(scenario?.name || 'Custom Scenario')
|
||||
const [loopbackPeriod, setLoopbackPeriod] = useState<number>(scenario?.loopbackPeriod || 1)
|
||||
const [indicators, setIndicators] = useState<Indicator[]>(scenario?.indicators || [])
|
||||
|
||||
// Available indicator types with their required parameters
|
||||
const indicatorTypes = Object.values(IndicatorType).map(type => {
|
||||
let params: string[] = [];
|
||||
let label = '';
|
||||
|
||||
// Determine required parameters based on indicator type
|
||||
switch (type) {
|
||||
case IndicatorType.RsiDivergence:
|
||||
case IndicatorType.RsiDivergenceConfirm:
|
||||
case IndicatorType.EmaTrend:
|
||||
case IndicatorType.EmaCross:
|
||||
case IndicatorType.StDev:
|
||||
case IndicatorType.ThreeWhiteSoldiers:
|
||||
params = ['period'];
|
||||
break;
|
||||
|
||||
case IndicatorType.MacdCross:
|
||||
params = ['fastPeriods', 'slowPeriods', 'signalPeriods'];
|
||||
break;
|
||||
|
||||
case IndicatorType.DualEmaCross:
|
||||
params = ['fastPeriods', 'slowPeriods'];
|
||||
break;
|
||||
|
||||
case IndicatorType.SuperTrend:
|
||||
case IndicatorType.SuperTrendCrossEma:
|
||||
case IndicatorType.ChandelierExit:
|
||||
params = ['period', 'multiplier'];
|
||||
break;
|
||||
|
||||
case IndicatorType.StochRsiTrend:
|
||||
params = ['period', 'stochPeriods', 'signalPeriods', 'smoothPeriods'];
|
||||
break;
|
||||
|
||||
case IndicatorType.Stc:
|
||||
case IndicatorType.LaggingStc:
|
||||
params = ['cyclePeriods', 'fastPeriods', 'slowPeriods'];
|
||||
break;
|
||||
|
||||
case IndicatorType.Composite:
|
||||
params = []; // Composite might not need specific parameters
|
||||
break;
|
||||
|
||||
default:
|
||||
params = ['period']; // Default fallback
|
||||
break;
|
||||
}
|
||||
|
||||
// Create more user-friendly labels
|
||||
switch (type) {
|
||||
case IndicatorType.RsiDivergence:
|
||||
label = 'RSI Divergence';
|
||||
break;
|
||||
case IndicatorType.RsiDivergenceConfirm:
|
||||
label = 'RSI Divergence Confirm';
|
||||
break;
|
||||
case IndicatorType.MacdCross:
|
||||
label = 'MACD Cross';
|
||||
break;
|
||||
case IndicatorType.EmaCross:
|
||||
label = 'EMA Cross';
|
||||
break;
|
||||
case IndicatorType.DualEmaCross:
|
||||
label = 'Dual EMA Cross';
|
||||
break;
|
||||
case IndicatorType.SuperTrend:
|
||||
label = 'Super Trend';
|
||||
break;
|
||||
case IndicatorType.ChandelierExit:
|
||||
label = 'Chandelier Exit';
|
||||
break;
|
||||
case IndicatorType.EmaTrend:
|
||||
label = 'EMA Trend';
|
||||
break;
|
||||
case IndicatorType.StochRsiTrend:
|
||||
label = 'Stoch RSI Trend';
|
||||
break;
|
||||
case IndicatorType.Stc:
|
||||
label = 'STC';
|
||||
break;
|
||||
case IndicatorType.StDev:
|
||||
label = 'Standard Deviation';
|
||||
break;
|
||||
case IndicatorType.LaggingStc:
|
||||
label = 'Lagging STC';
|
||||
break;
|
||||
case IndicatorType.SuperTrendCrossEma:
|
||||
label = 'Super Trend Cross EMA';
|
||||
break;
|
||||
case IndicatorType.ThreeWhiteSoldiers:
|
||||
label = 'Three White Soldiers';
|
||||
break;
|
||||
case IndicatorType.Composite:
|
||||
label = 'Composite';
|
||||
break;
|
||||
default:
|
||||
label = type;
|
||||
break;
|
||||
}
|
||||
|
||||
return { type, label, params };
|
||||
});
|
||||
|
||||
const addIndicator = () => {
|
||||
const newIndicator: Indicator = {
|
||||
name: `Indicator ${indicators.length + 1}`,
|
||||
type: indicatorTypes[0].type,
|
||||
period: 14,
|
||||
fastPeriods: 12,
|
||||
slowPeriods: 26,
|
||||
signalPeriods: 9,
|
||||
multiplier: 3.0,
|
||||
stochPeriods: 14,
|
||||
smoothPeriods: 3,
|
||||
cyclePeriods: 10
|
||||
}
|
||||
setIndicators([...indicators, newIndicator])
|
||||
}
|
||||
|
||||
const removeIndicator = (index: number) => {
|
||||
setIndicators(indicators.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const updateIndicator = (index: number, field: string, value: any) => {
|
||||
const updated = indicators.map((indicator, i) => {
|
||||
if (i === index) {
|
||||
return { ...indicator, [field]: value }
|
||||
}
|
||||
return indicator
|
||||
})
|
||||
setIndicators(updated)
|
||||
}
|
||||
|
||||
const handleCreateScenario = () => {
|
||||
const scenario: Scenario = {
|
||||
name,
|
||||
indicators,
|
||||
loopbackPeriod,
|
||||
}
|
||||
onCreateScenario(scenario)
|
||||
setCustomScenario(scenario)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleCreateScenario()
|
||||
}, [name, loopbackPeriod, indicators])
|
||||
|
||||
const getRequiredParams = (indicatorType: IndicatorType) => {
|
||||
return indicatorTypes.find(t => t.type === indicatorType)?.params || []
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showCustomScenario ? (
|
||||
<div className="collapse bg-base-200">
|
||||
<input type="checkbox" />
|
||||
<div className="collapse-title text-xs font-medium">
|
||||
Custom Scenario Builder
|
||||
</div>
|
||||
<div className="collapse-content">
|
||||
<div className="space-y-4">
|
||||
{/* Scenario basic info */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormInput label="Scenario Name" htmlFor="scenarioName" inline={false}>
|
||||
<input
|
||||
id="scenarioName"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
type='text'
|
||||
className='input input-bordered w-full'
|
||||
placeholder="Enter scenario name"
|
||||
/>
|
||||
</FormInput>
|
||||
|
||||
<FormInput label="Lookback Period" htmlFor="loopbackPeriod" inline={false}>
|
||||
<input
|
||||
id="loopbackPeriod"
|
||||
value={loopbackPeriod}
|
||||
onChange={(e) => setLoopbackPeriod(Number(e.target.value))}
|
||||
type='number'
|
||||
min="1"
|
||||
step="1"
|
||||
className='input input-bordered w-full'
|
||||
/>
|
||||
</FormInput>
|
||||
</div>
|
||||
|
||||
{/* Indicators section */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<h4 className="font-medium">Indicators</h4>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addIndicator}
|
||||
className="btn btn-sm btn-primary"
|
||||
>
|
||||
Add Indicator
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{indicators.map((indicator, index) => (
|
||||
<div key={index} className="border rounded-lg p-4 space-y-3 bg-base-100">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 flex-1">
|
||||
<FormInput label="Indicator Name" htmlFor={`indicatorName-${index}`} inline={false}>
|
||||
<input
|
||||
value={indicator.name || ''}
|
||||
onChange={(e) => updateIndicator(index, 'name', e.target.value)}
|
||||
type='text'
|
||||
className='input input-bordered w-full'
|
||||
/>
|
||||
</FormInput>
|
||||
|
||||
<FormInput label="Indicator Type" htmlFor={`indicatorType-${index}`} inline={false}>
|
||||
<select
|
||||
value={indicator.type || indicatorTypes[0].type}
|
||||
onChange={(e) => updateIndicator(index, 'type', e.target.value)}
|
||||
className='select select-bordered w-full'
|
||||
>
|
||||
{indicatorTypes.map((type) => (
|
||||
<option key={type.type} value={type.type}>
|
||||
{type.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormInput>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeIndicator(index)}
|
||||
className="btn btn-sm btn-error ml-2"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Dynamic parameter inputs based on indicator type */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{getRequiredParams(indicator.type || indicatorTypes[0].type).map((param) => (
|
||||
<FormInput key={param} label={param.charAt(0).toUpperCase() + param.slice(1)} htmlFor={`${param}-${index}`} inline={false}>
|
||||
<input
|
||||
value={indicator[param as keyof Indicator] as number || ''}
|
||||
onChange={(e) => updateIndicator(index, param, param.includes('multiplier') ? parseFloat(e.target.value) : parseInt(e.target.value))}
|
||||
type='number'
|
||||
step={param.includes('multiplier') ? '0.1' : '1'}
|
||||
min={param.includes('multiplier') ? '0.1' : '1'}
|
||||
className='input input-bordered w-full'
|
||||
/>
|
||||
</FormInput>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{indicators.length === 0 && (
|
||||
<div className="text-center text-gray-500 py-4">
|
||||
No indicators added. Click "Add Indicator" to start building your scenario.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomScenario
|
||||
Reference in New Issue
Block a user