Add custom scenario
This commit is contained in:
@@ -118,9 +118,9 @@ public class BacktestController : BaseController
|
|||||||
return BadRequest("Account name is required");
|
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)
|
if (string.IsNullOrEmpty(request.MoneyManagementName) && request.MoneyManagement == null)
|
||||||
@@ -131,13 +131,9 @@ public class BacktestController : BaseController
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Backtest backtestResult = null;
|
Backtest backtestResult = null;
|
||||||
var scenario = _scenarioService.GetScenario(request.Config.ScenarioName);
|
|
||||||
var account = await _accountService.GetAccount(request.Config.AccountName, true, false);
|
var account = await _accountService.GetAccount(request.Config.AccountName, true, false);
|
||||||
var user = await GetUser();
|
var user = await GetUser();
|
||||||
|
|
||||||
if (scenario == null)
|
|
||||||
return BadRequest("No scenario found");
|
|
||||||
|
|
||||||
// Get money management
|
// Get money management
|
||||||
MoneyManagement moneyManagement;
|
MoneyManagement moneyManagement;
|
||||||
if (!string.IsNullOrEmpty(request.MoneyManagementName))
|
if (!string.IsNullOrEmpty(request.MoneyManagementName))
|
||||||
@@ -152,13 +148,14 @@ public class BacktestController : BaseController
|
|||||||
moneyManagement?.FormatPercentage();
|
moneyManagement?.FormatPercentage();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update config with money management
|
// Update config with money management - TradingBot will handle scenario loading
|
||||||
var backtestConfig = new TradingBotConfig
|
var backtestConfig = new TradingBotConfig
|
||||||
{
|
{
|
||||||
AccountName = request.Config.AccountName,
|
AccountName = request.Config.AccountName,
|
||||||
MoneyManagement = moneyManagement,
|
MoneyManagement = moneyManagement,
|
||||||
Ticker = request.Config.Ticker,
|
Ticker = request.Config.Ticker,
|
||||||
ScenarioName = request.Config.ScenarioName,
|
ScenarioName = request.Config.ScenarioName,
|
||||||
|
Scenario = request.Config.Scenario,
|
||||||
Timeframe = request.Config.Timeframe,
|
Timeframe = request.Config.Timeframe,
|
||||||
IsForWatchingOnly = request.WatchOnly,
|
IsForWatchingOnly = request.WatchOnly,
|
||||||
BotTradingBalance = request.Balance,
|
BotTradingBalance = request.Balance,
|
||||||
@@ -170,9 +167,8 @@ public class BacktestController : BaseController
|
|||||||
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
|
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
|
||||||
FlipPosition = request.Config.FlipPosition,
|
FlipPosition = request.Config.FlipPosition,
|
||||||
Name = request.Config.Name ??
|
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,
|
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable,
|
||||||
Scenario = scenario,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (request.Config.BotType)
|
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,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
}))
|
||||||
@@ -4,23 +4,25 @@ import {type SubmitHandler, useForm} from 'react-hook-form'
|
|||||||
|
|
||||||
import useApiUrlStore from '../../../app/store/apiStore'
|
import useApiUrlStore from '../../../app/store/apiStore'
|
||||||
import {
|
import {
|
||||||
AccountClient,
|
AccountClient,
|
||||||
BacktestClient,
|
BacktestClient,
|
||||||
BotType,
|
BotType,
|
||||||
DataClient,
|
DataClient,
|
||||||
MoneyManagement,
|
MoneyManagement,
|
||||||
MoneyManagementClient,
|
MoneyManagementClient,
|
||||||
RunBacktestRequest,
|
RunBacktestRequest,
|
||||||
ScenarioClient,
|
Scenario,
|
||||||
Ticker,
|
ScenarioClient,
|
||||||
Timeframe,
|
Ticker,
|
||||||
TradingBotConfig,
|
Timeframe,
|
||||||
|
TradingBotConfig,
|
||||||
} from '../../../generated/ManagingApi'
|
} from '../../../generated/ManagingApi'
|
||||||
import type {BacktestModalProps, IBacktestsFormInput,} from '../../../global/type'
|
import type {BacktestModalProps, IBacktestsFormInput,} from '../../../global/type'
|
||||||
import {Loader, Slider} from '../../atoms'
|
import {Loader, Slider} from '../../atoms'
|
||||||
import {Modal, Toast} from '../../mollecules'
|
import {Modal, Toast} from '../../mollecules'
|
||||||
import FormInput from '../../mollecules/FormInput/FormInput'
|
import FormInput from '../../mollecules/FormInput/FormInput'
|
||||||
import CustomMoneyManagement from '../CustomMoneyManagement/CustomMoneyManagement'
|
import CustomMoneyManagement from '../CustomMoneyManagement/CustomMoneyManagement'
|
||||||
|
import CustomScenario from '../CustomScenario/CustomScenario'
|
||||||
|
|
||||||
const BacktestModal: React.FC<BacktestModalProps> = ({
|
const BacktestModal: React.FC<BacktestModalProps> = ({
|
||||||
showModal,
|
showModal,
|
||||||
@@ -66,6 +68,13 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
|
|||||||
const [showCustomMoneyManagement, setShowCustomMoneyManagement] =
|
const [showCustomMoneyManagement, setShowCustomMoneyManagement] =
|
||||||
useState(false)
|
useState(false)
|
||||||
|
|
||||||
|
const [customScenario, setCustomScenario] =
|
||||||
|
React.useState<Scenario | undefined>(undefined)
|
||||||
|
const [selectedScenario, setSelectedScenario] =
|
||||||
|
useState<string>()
|
||||||
|
const [showCustomScenario, setShowCustomScenario] =
|
||||||
|
useState(false)
|
||||||
|
|
||||||
const { apiUrl } = useApiUrlStore()
|
const { apiUrl } = useApiUrlStore()
|
||||||
|
|
||||||
const scenarioClient = new ScenarioClient({}, apiUrl)
|
const scenarioClient = new ScenarioClient({}, apiUrl)
|
||||||
@@ -75,8 +84,15 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
|
|||||||
const backtestClient = new BacktestClient({}, apiUrl)
|
const backtestClient = new BacktestClient({}, apiUrl)
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<IBacktestsFormInput> = async (form) => {
|
const onSubmit: SubmitHandler<IBacktestsFormInput> = async (form) => {
|
||||||
if (!form.scenarioName) {
|
// Check if we have either a selected scenario or a custom scenario
|
||||||
const t = new Toast('Please select a scenario', false);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,13 +124,15 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
|
|||||||
const t = new Toast(ticker + ' is running')
|
const t = new Toast(ticker + ' is running')
|
||||||
|
|
||||||
console.log(customMoneyManagement)
|
console.log(customMoneyManagement)
|
||||||
|
console.log(customScenario)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create the TradingBotConfig
|
// Create the TradingBotConfig
|
||||||
const tradingBotConfig: TradingBotConfig = {
|
const tradingBotConfig: TradingBotConfig = {
|
||||||
accountName: form.accountName,
|
accountName: form.accountName,
|
||||||
ticker: ticker as Ticker,
|
ticker: ticker as Ticker,
|
||||||
scenarioName: scenarioName,
|
scenarioName: customScenario ? undefined : scenarioName,
|
||||||
|
scenario: customScenario,
|
||||||
timeframe: form.timeframe,
|
timeframe: form.timeframe,
|
||||||
botType: form.botType,
|
botType: form.botType,
|
||||||
isForWatchingOnly: false, // Always false for backtests
|
isForWatchingOnly: false, // Always false for backtests
|
||||||
@@ -124,7 +142,7 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
|
|||||||
maxPositionTimeHours: form.maxPositionTimeHours || null,
|
maxPositionTimeHours: form.maxPositionTimeHours || null,
|
||||||
flipOnlyWhenInProfit: form.flipOnlyWhenInProfit ?? true,
|
flipOnlyWhenInProfit: form.flipOnlyWhenInProfit ?? true,
|
||||||
flipPosition: form.botType === BotType.FlippingBot, // Set based on bot type
|
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,
|
botTradingBalance: form.balance,
|
||||||
moneyManagement: customMoneyManagement || moneyManagements?.find(m => m.name === selectedMoneyManagement) || moneyManagements?.[0] || {
|
moneyManagement: customMoneyManagement || moneyManagements?.find(m => m.name === selectedMoneyManagement) || moneyManagements?.[0] || {
|
||||||
name: 'placeholder',
|
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({
|
const { data: accounts } = useQuery({
|
||||||
queryFn: () => accountClient.account_GetAccounts(),
|
queryFn: () => accountClient.account_GetAccounts(),
|
||||||
queryKey: ['accounts'],
|
queryKey: ['accounts'],
|
||||||
@@ -370,7 +399,11 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
|
|||||||
<FormInput label="Scenario" htmlFor="scenarioName">
|
<FormInput label="Scenario" htmlFor="scenarioName">
|
||||||
<select
|
<select
|
||||||
className="select select-bordered w-full"
|
className="select select-bordered w-full"
|
||||||
{...register('scenarioName')}
|
{...register('scenarioName', {
|
||||||
|
onChange(event) {
|
||||||
|
onScenarioChange(event)
|
||||||
|
},
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<option value="" disabled>Select a scenario</option>
|
<option value="" disabled>Select a scenario</option>
|
||||||
{scenarios.map((item) => (
|
{scenarios.map((item) => (
|
||||||
@@ -378,6 +411,9 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
|
|||||||
{item.name || 'Unnamed Scenario'}
|
{item.name || 'Unnamed Scenario'}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
|
<option key="custom" value="custom">
|
||||||
|
Custom
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</FormInput>
|
</FormInput>
|
||||||
|
|
||||||
@@ -397,6 +433,15 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
|
|||||||
</FormInput>
|
</FormInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showCustomScenario && (
|
||||||
|
<div className="mt-6">
|
||||||
|
<CustomScenario
|
||||||
|
onCreateScenario={setCustomScenario}
|
||||||
|
showCustomScenario={showCustomScenario}
|
||||||
|
></CustomScenario>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Fourth Row: Balance & Cooldown Period */}
|
{/* Fourth Row: Balance & Cooldown Period */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<FormInput label="Balance" htmlFor="balance">
|
<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