Fix front and backtest

This commit is contained in:
2025-07-04 13:14:12 +07:00
parent c9959f7550
commit 2d295fc860
18 changed files with 676 additions and 182 deletions

View File

@@ -30,7 +30,6 @@ namespace Managing.Application.Abstractions
int GetWinRate();
decimal GetProfitAndLoss();
decimal GetTotalFees();
void LoadScenario(string scenarioName);
void LoadScenario(Scenario scenario);
void UpdateIndicatorsValues();
Task LoadAccount();

View File

@@ -1,6 +1,7 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.Bots;
using Managing.Core.FixedSizedQueue;
using Managing.Domain.Accounts;
using Managing.Domain.Backtests;
@@ -113,24 +114,15 @@ namespace Managing.Application.Backtesting
List<Candle> candles,
User user = null)
{
// Set FlipPosition based on BotType
config.FlipPosition = config.FlipPosition;
var tradingBot = _botFactory.CreateBacktestTradingBot(config);
// Load scenario - prefer Scenario object over ScenarioName
if (config.Scenario != null)
// Scenario and indicators should already be loaded in constructor by BotService
// This is just a validation check to ensure everything loaded properly
if (tradingBot is TradingBot bot && !bot.Indicators.Any())
{
tradingBot.LoadScenario(config.Scenario);
}
else if (!string.IsNullOrEmpty(config.ScenarioName))
{
tradingBot.LoadScenario(config.ScenarioName);
}
else
{
throw new ArgumentException(
"Either Scenario object or ScenarioName must be provided in TradingBotConfig");
throw new InvalidOperationException(
$"No indicators were loaded for scenario '{config.ScenarioName ?? config.Scenario?.Name}'. " +
"This indicates a problem with scenario loading.");
}
tradingBot.User = user;
@@ -218,7 +210,6 @@ namespace Managing.Application.Backtesting
_logger.LogInformation("Backtest processing completed. Calculating final results...");
bot.Candles = new HashSet<Candle>(candles);
// bot.UpdateIndicatorsValues();
var indicatorsValues = GetIndicatorsValues(bot.Config.Scenario.Indicators, candles);
@@ -256,7 +247,8 @@ namespace Managing.Application.Backtesting
Statistics = stats,
OptimizedMoneyManagement = optimizedMoneyManagement,
IndicatorsValues = AggregateValues(indicatorsValues, bot.IndicatorsValues),
Score = score
Score = score,
Id = Guid.NewGuid().ToString()
};
return result;

View File

@@ -38,27 +38,14 @@ namespace Managing.Application.Bots.Base
ITradingBot IBotFactory.CreateTradingBot(TradingBotConfig config)
{
return new TradingBot(
_exchangeService,
_tradingBotLogger,
_tradingService,
_accountService,
_messengerService,
_botService,
config);
// Delegate to BotService which handles scenario loading properly
return _botService.CreateTradingBot(config);
}
ITradingBot IBotFactory.CreateBacktestTradingBot(TradingBotConfig config)
{
config.IsForBacktest = true;
return new TradingBot(
_exchangeService,
_tradingBotLogger,
_tradingService,
_accountService,
_messengerService,
_botService,
config);
// Delegate to BotService which handles scenario loading properly
return _botService.CreateBacktestTradingBot(config);
}
}
}

View File

@@ -76,6 +76,16 @@ public class TradingBot : Bot, ITradingBot
WalletBalances = new Dictionary<DateTime, decimal>();
IndicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
// Load indicators if scenario is provided in config
if (Config.Scenario != null)
{
LoadIndicators(Config.Scenario);
}
else
{
throw new ArgumentException("Scenario object must be provided in TradingBotConfig. ScenarioName alone is not sufficient.");
}
if (!Config.IsForBacktest)
{
Interval = CandleExtensions.GetIntervalFromTimeframe(Config.Timeframe);
@@ -91,7 +101,13 @@ public class TradingBot : Bot, ITradingBot
if (!Config.IsForBacktest)
{
LoadScenario(Config.ScenarioName);
// Scenario and indicators should already be loaded in constructor
// This is just a safety check
if (Config.Scenario == null || !Indicators.Any())
{
throw new InvalidOperationException("Scenario or indicators not loaded properly in constructor. This indicates a configuration error.");
}
PreloadCandles().GetAwaiter().GetResult();
CancelAllOrders().GetAwaiter().GetResult();
@@ -127,33 +143,32 @@ public class TradingBot : Bot, ITradingBot
}
}
public void LoadScenario(string scenarioName)
{
if (Config.Scenario != null)
return;
var scenario = TradingService.GetScenarioByName(scenarioName);
if (scenario == null)
{
Logger.LogWarning("No scenario found for this scenario name");
Stop();
}
else
{
LoadIndicators(ScenarioHelpers.GetIndicatorsFromScenario(scenario));
}
}
public void LoadScenario(Scenario scenario)
{
if (scenario == null)
{
Logger.LogWarning("Null scenario provided");
Stop();
var errorMessage = "Null scenario provided";
Logger.LogWarning(errorMessage);
// If called during construction, throw exception instead of Stop()
if (Status == BotStatus.Down)
{
throw new ArgumentException(errorMessage);
}
else
{
Stop();
}
}
else
{
// Store the scenario in config and load indicators
Config.Scenario = scenario;
LoadIndicators(ScenarioHelpers.GetIndicatorsFromScenario(scenario));
Logger.LogInformation($"Loaded scenario '{scenario.Name}' with {Indicators.Count} indicators");
}
}
@@ -164,10 +179,15 @@ public class TradingBot : Bot, ITradingBot
public void LoadIndicators(IEnumerable<IIndicator> indicators)
{
foreach (var strategy in indicators)
// Clear existing indicators to prevent duplicates
Indicators.Clear();
foreach (var indicator in indicators)
{
Indicators.Add(strategy);
Indicators.Add(indicator);
}
Logger.LogInformation($"Loaded {Indicators.Count} indicators for bot '{Name}'");
}
public async Task Run()
@@ -1444,9 +1464,9 @@ public class TradingBot : Bot, ITradingBot
throw new ArgumentException("Account name cannot be null or empty");
}
if (string.IsNullOrEmpty(newConfig.ScenarioName))
if (newConfig.Scenario == null)
{
throw new ArgumentException("Scenario name cannot be null or empty");
throw new ArgumentException("Scenario object must be provided in configuration");
}
// Protect critical properties that shouldn't change for running bots
@@ -1487,9 +1507,17 @@ public class TradingBot : Bot, ITradingBot
// If scenario changed, reload it
var currentScenario = Config.Scenario?.Name;
if (Config.ScenarioName != currentScenario)
var newScenario = newConfig.Scenario?.Name;
if (newScenario != currentScenario)
{
LoadScenario(Config.ScenarioName);
if (newConfig.Scenario != null)
{
LoadScenario(newConfig.Scenario);
}
else
{
throw new ArgumentException("New scenario object must be provided when updating configuration.");
}
}
await LogInformation("✅ **Configuration Applied**\n" +

View File

@@ -134,6 +134,25 @@ namespace Managing.Application.ManageBot
}
}
// Ensure the scenario is properly loaded from database if needed
if (scalpingConfig.Scenario == null && !string.IsNullOrEmpty(scalpingConfig.ScenarioName))
{
var scenario = _tradingService.GetScenarioByName(scalpingConfig.ScenarioName);
if (scenario != null)
{
scalpingConfig.Scenario = scenario;
}
else
{
throw new ArgumentException($"Scenario '{scalpingConfig.ScenarioName}' not found in database when loading backup");
}
}
if (scalpingConfig.Scenario == null)
{
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid when loading backup");
}
// Ensure critical properties are set correctly for restored bots
scalpingConfig.IsForBacktest = false;
@@ -235,6 +254,25 @@ namespace Managing.Application.ManageBot
if (_botTasks.TryGetValue(identifier, out var botTaskWrapper) &&
botTaskWrapper.BotInstance is TradingBot tradingBot)
{
// Ensure the scenario is properly loaded from database if needed
if (newConfig.Scenario == null && !string.IsNullOrEmpty(newConfig.ScenarioName))
{
var scenario = _tradingService.GetScenarioByName(newConfig.ScenarioName);
if (scenario != null)
{
newConfig.Scenario = scenario;
}
else
{
throw new ArgumentException($"Scenario '{newConfig.ScenarioName}' not found in database when updating configuration");
}
}
if (newConfig.Scenario == null)
{
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid when updating configuration");
}
// Check if the bot name is changing
if (newConfig.Name != identifier && !string.IsNullOrEmpty(newConfig.Name))
{
@@ -279,6 +317,25 @@ namespace Managing.Application.ManageBot
public ITradingBot CreateTradingBot(TradingBotConfig config)
{
// Ensure the scenario is properly loaded from database if needed
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
{
var scenario = _tradingService.GetScenarioByName(config.ScenarioName);
if (scenario != null)
{
config.Scenario = scenario;
}
else
{
throw new ArgumentException($"Scenario '{config.ScenarioName}' not found in database");
}
}
if (config.Scenario == null)
{
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid");
}
return new TradingBot(
_exchangeService,
_tradingBotLogger,
@@ -291,6 +348,25 @@ namespace Managing.Application.ManageBot
public ITradingBot CreateBacktestTradingBot(TradingBotConfig config)
{
// Ensure the scenario is properly loaded from database if needed
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
{
var scenario = _tradingService.GetScenarioByName(config.ScenarioName);
if (scenario != null)
{
config.Scenario = scenario;
}
else
{
throw new ArgumentException($"Scenario '{config.ScenarioName}' not found in database");
}
}
if (config.Scenario == null)
{
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid");
}
config.IsForBacktest = true;
return new TradingBot(
_exchangeService,
@@ -304,6 +380,25 @@ namespace Managing.Application.ManageBot
public ITradingBot CreateScalpingBot(TradingBotConfig config)
{
// Ensure the scenario is properly loaded from database if needed
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
{
var scenario = _tradingService.GetScenarioByName(config.ScenarioName);
if (scenario != null)
{
config.Scenario = scenario;
}
else
{
throw new ArgumentException($"Scenario '{config.ScenarioName}' not found in database");
}
}
if (config.Scenario == null)
{
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid");
}
config.FlipPosition = false;
return new TradingBot(
_exchangeService,
@@ -317,6 +412,25 @@ namespace Managing.Application.ManageBot
public ITradingBot CreateBacktestScalpingBot(TradingBotConfig config)
{
// Ensure the scenario is properly loaded from database if needed
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
{
var scenario = _tradingService.GetScenarioByName(config.ScenarioName);
if (scenario != null)
{
config.Scenario = scenario;
}
else
{
throw new ArgumentException($"Scenario '{config.ScenarioName}' not found in database");
}
}
if (config.Scenario == null)
{
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid");
}
config.IsForBacktest = true;
config.FlipPosition = false;
return new TradingBot(
@@ -331,6 +445,25 @@ namespace Managing.Application.ManageBot
public ITradingBot CreateFlippingBot(TradingBotConfig config)
{
// Ensure the scenario is properly loaded from database if needed
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
{
var scenario = _tradingService.GetScenarioByName(config.ScenarioName);
if (scenario != null)
{
config.Scenario = scenario;
}
else
{
throw new ArgumentException($"Scenario '{config.ScenarioName}' not found in database");
}
}
if (config.Scenario == null)
{
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid");
}
config.FlipPosition = true;
return new TradingBot(
_exchangeService,
@@ -344,6 +477,25 @@ namespace Managing.Application.ManageBot
public ITradingBot CreateBacktestFlippingBot(TradingBotConfig config)
{
// Ensure the scenario is properly loaded from database if needed
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
{
var scenario = _tradingService.GetScenarioByName(config.ScenarioName);
if (scenario != null)
{
config.Scenario = scenario;
}
else
{
throw new ArgumentException($"Scenario '{config.ScenarioName}' not found in database");
}
}
if (config.Scenario == null)
{
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid");
}
config.IsForBacktest = true;
config.FlipPosition = true;
return new TradingBot(

View File

@@ -0,0 +1,40 @@
import {create} from 'zustand'
import type {Backtest} from '../../generated/ManagingApi'
interface BacktestStore {
backtests: Backtest[]
isLoading: boolean
setBacktests: (backtests: Backtest[]) => void
addBacktest: (backtest: Backtest) => void
removeBacktest: (id: string) => void
setLoading: (loading: boolean) => void
clearBacktests: () => void
}
const useBacktestStore = create<BacktestStore>((set, get) => ({
backtests: [],
isLoading: false,
setBacktests: (backtests: Backtest[]) =>
set({ backtests }),
addBacktest: (backtest: Backtest) =>
set((state) => ({
backtests: [...state.backtests, backtest]
})),
removeBacktest: (id: string) =>
set((state) => ({
backtests: state.backtests.filter(backtest =>
String(backtest.id) !== String(id)
)
})),
setLoading: (loading: boolean) =>
set({ isLoading: loading }),
clearBacktests: () =>
set({ backtests: [] })
}))
export default useBacktestStore

View File

@@ -0,0 +1,117 @@
import React, {useState} from 'react'
import {Modal} from '../index'
import type {Backtest} from '../../../generated/ManagingApi'
interface ConfigDisplayModalProps {
showModal: boolean
onClose: () => void
backtest: Backtest | null
}
const ConfigDisplayModal: React.FC<ConfigDisplayModalProps> = ({
showModal,
onClose,
backtest,
}) => {
const [copied, setCopied] = useState(false)
const formatConfigForDisplay = (config: any) => {
// Create a clean config object without undefined values
const cleanConfig: Record<string, any> = {
accountName: config.accountName,
moneyManagement: config.moneyManagement,
ticker: config.ticker,
timeframe: config.timeframe,
isForWatchingOnly: config.isForWatchingOnly,
botTradingBalance: config.botTradingBalance,
isForBacktest: config.isForBacktest,
cooldownPeriod: config.cooldownPeriod,
maxLossStreak: config.maxLossStreak,
flipPosition: config.flipPosition,
name: config.name,
riskManagement: config.riskManagement,
scenario: config.scenario,
scenarioName: config.scenarioName,
maxPositionTimeHours: config.maxPositionTimeHours,
closeEarlyWhenProfitable: config.closeEarlyWhenProfitable,
flipOnlyWhenInProfit: config.flipOnlyWhenInProfit,
useSynthApi: config.useSynthApi,
useForPositionSizing: config.useForPositionSizing,
useForSignalFiltering: config.useForSignalFiltering,
useForDynamicStopLoss: config.useForDynamicStopLoss,
}
// Remove undefined values
Object.keys(cleanConfig).forEach(key => {
if (cleanConfig[key] === undefined || cleanConfig[key] === null) {
delete cleanConfig[key]
}
})
return JSON.stringify(cleanConfig, null, 2)
}
const handleCopyConfig = async () => {
if (!backtest?.config) return
try {
const configJson = formatConfigForDisplay(backtest.config)
await navigator.clipboard.writeText(configJson)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (error) {
console.error('Failed to copy config:', error)
}
}
if (!backtest) return null
const configJson = formatConfigForDisplay(backtest.config)
return (
<Modal
showModal={showModal}
onClose={onClose}
titleHeader="Backtest Configuration"
>
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">Configuration JSON</h3>
<button
type="button"
className={`btn btn-sm ${copied ? 'btn-success' : 'btn-primary'}`}
onClick={handleCopyConfig}
>
{copied ? (
<>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
Copied!
</>
) : (
<>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
</svg>
Copy Config
</>
)}
</button>
</div>
<div className="bg-base-200 rounded-lg p-4 max-h-96 overflow-auto">
<pre className="text-sm text-base-content whitespace-pre-wrap break-words">
{configJson}
</pre>
</div>
<div className="text-sm text-base-content/70">
<p>This configuration can be copied and imported into the backtest modal to replicate this exact setup.</p>
</div>
</div>
</Modal>
)
}
export default ConfigDisplayModal

View File

@@ -11,8 +11,8 @@ import {Loader} from '../../atoms'
const navigation = [
{ href: '/bots', name: 'Bots' },
{ href: '/scenarios', name: 'Scenarios' },
{ href: '/backtest', name: 'Backtest' },
{ href: '/scenarios', name: 'Scenarios' },
{ href: '/tools', name: 'Tools' },
{ href: '/settings', name: 'Settings' },
]

View File

@@ -12,3 +12,4 @@ export { default as LogIn } from './LogIn/LogIn'
export { default as GridTile } from './GridTile/GridTile'
export { default as SelectColumnFilter } from './Table/SelectColumnFilter'
export { default as Card } from './Card/Card'
export { default as ConfigDisplayModal } from './ConfigDisplayModal/ConfigDisplayModal'

View File

@@ -4,6 +4,7 @@ import React, {useEffect, useState} from 'react'
import {useQuery} from '@tanstack/react-query'
import useApiUrlStore from '../../../app/store/apiStore'
import useBacktestStore from '../../../app/store/backtestStore'
import type {
Backtest,
MoneyManagement,
@@ -13,7 +14,6 @@ import type {
TradingBotConfigRequest
} from '../../../generated/ManagingApi'
import {BacktestClient, BotClient, MoneyManagementClient} from '../../../generated/ManagingApi'
import type {IBacktestCards} from '../../../global/type.tsx'
import MoneyManagementModal from '../../../pages/settingsPage/moneymanagement/moneyManagementModal'
import {CardPosition, CardText, Toast} from '../../mollecules'
import CardPositionItem from '../Trading/CardPositionItem'
@@ -52,9 +52,14 @@ function daysBetween(date: Date) {
return diffDays
}
const BacktestCards: React.FC<IBacktestCards> = ({list, setBacktests}) => {
interface BacktestCardsProps {
list: Backtest[] | undefined
}
const BacktestCards: React.FC<BacktestCardsProps> = ({list}) => {
console.log(list)
const {apiUrl} = useApiUrlStore()
const {addBacktest, removeBacktest} = useBacktestStore()
const [showMoneyManagementModal, setShowMoneyManagementModal] =
React.useState(false)
const [selectedMoneyManagement, setSelectedMoneyManagement] =
@@ -173,13 +178,29 @@ const BacktestCards: React.FC<IBacktestCards> = ({list, setBacktests}) => {
.backtest_Run(request)
.then((backtest: Backtest) => {
t.update('success', `${backtest.config.ticker} Backtest Succeeded`)
setBacktests((arr: Backtest[]) => [...arr, backtest])
addBacktest(backtest)
})
.catch((err) => {
t.update('error', 'Error :' + err)
})
}
async function deleteBacktest(id: string) {
const t = new Toast('Deleting backtest')
const client = new BacktestClient({}, apiUrl)
await client
.backtest_DeleteBacktest(id)
.then(() => {
t.update('success', 'Backtest deleted')
// Remove the deleted backtest from the store
removeBacktest(id)
})
.catch((err) => {
t.update('error', err)
})
}
function saveMoneyManagement(moneyManagement: MoneyManagement) {
setSelectedMoneyManagement(moneyManagement)
setShowMoneyManagementModal(true)
@@ -194,9 +215,14 @@ const BacktestCards: React.FC<IBacktestCards> = ({list, setBacktests}) => {
>
<div className="indicator">
<div className="indicator-item indicator-top">
<button className="btn btn-primary h-5 min-h-0 px-2 mr-5 rounded-full">
<TrashIcon width={15}></TrashIcon>
</button>
<div className="tooltip" data-tip="Delete backtest">
<button
className="btn btn-primary h-5 min-h-0 px-2 mr-5 rounded-full"
onClick={() => deleteBacktest(backtest.id)}
>
<TrashIcon width={15}></TrashIcon>
</button>
</div>
</div>
<div className="card bg-base-300 shadow-xl">

View File

@@ -28,7 +28,6 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
showModal,
closeModal,
setBacktests,
showLoopSlider = false,
}) => {
// Get date 15 days ago for start date
const defaultStartDate = new Date();
@@ -77,9 +76,7 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
const [selectedAccount, setSelectedAccount] = useState<string>('')
const [selectedTimeframe, setSelectedTimeframe] = useState<Timeframe>(Timeframe.OneHour)
const [selectedLoopQuantity, setLoopQuantity] = React.useState<number>(
showLoopSlider ? 3 : 1
)
const [balance, setBalance] = useState<number>(10000)
const [customMoneyManagement, setCustomMoneyManagement] =
@@ -204,17 +201,7 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
t.update('success', `${backtest.config.ticker} Backtest Succeeded`)
setBacktests((arr) => [...arr, backtest])
if (showLoopSlider && selectedLoopQuantity > loopCount) {
const nextCount = loopCount + 1
const mm: MoneyManagement = {
leverage: backtest.optimizedMoneyManagement.leverage,
name: backtest.optimizedMoneyManagement.name + nextCount,
stopLoss: backtest.optimizedMoneyManagement.stopLoss,
takeProfit: backtest.optimizedMoneyManagement.takeProfit,
timeframe: backtest.optimizedMoneyManagement.timeframe,
}
await runBacktest(form, ticker, scenarioName, mm, nextCount)
}
} catch (err) {
t.update('error', 'Error: ' + err)
throw err; // Re-throw the error to be caught by the caller

View File

@@ -2,18 +2,24 @@ import {ChevronDownIcon, ChevronRightIcon, CogIcon, PlayIcon, TrashIcon} from '@
import React, {useEffect, useState} from 'react'
import useApiUrlStore from '../../../app/store/apiStore'
import useBacktestStore from '../../../app/store/backtestStore'
import type {Backtest} from '../../../generated/ManagingApi'
import {BacktestClient} from '../../../generated/ManagingApi'
import type {IBacktestCards} from '../../../global/type.tsx'
import {CardText, SelectColumnFilter, Table} from '../../mollecules'
import {CardText, ConfigDisplayModal, SelectColumnFilter, Table} from '../../mollecules'
import {UnifiedTradingModal} from '../index'
import Toast from '../../mollecules/Toast/Toast'
import BacktestRowDetails from './backtestRowDetails'
const BacktestTable: React.FC<IBacktestCards> = ({list, isFetching, setBacktests}) => {
interface BacktestTableProps {
list: Backtest[] | undefined
isFetching?: boolean
}
const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching}) => {
const [rows, setRows] = useState<Backtest[]>([])
const {apiUrl} = useApiUrlStore()
const {removeBacktest} = useBacktestStore()
const [optimizedMoneyManagement, setOptimizedMoneyManagement] = useState({
stopLoss: 0,
takeProfit: 0,
@@ -36,6 +42,10 @@ const BacktestTable: React.FC<IBacktestCards> = ({list, isFetching, setBacktests
const [showBacktestConfigModal, setShowBacktestConfigModal] = useState(false)
const [selectedBacktestForRerun, setSelectedBacktestForRerun] = useState<Backtest | null>(null)
// Config display modal state
const [showConfigDisplayModal, setShowConfigDisplayModal] = useState(false)
const [selectedBacktestForConfigView, setSelectedBacktestForConfigView] = useState<Backtest | null>(null)
const handleOpenBotConfigModal = (backtest: Backtest) => {
setSelectedBacktest(backtest)
setShowBotConfigModal(true)
@@ -56,6 +66,16 @@ const BacktestTable: React.FC<IBacktestCards> = ({list, isFetching, setBacktests
setSelectedBacktestForRerun(null)
}
const handleOpenConfigDisplayModal = (backtest: Backtest) => {
setSelectedBacktestForConfigView(backtest)
setShowConfigDisplayModal(true)
}
const handleCloseConfigDisplayModal = () => {
setShowConfigDisplayModal(false)
setSelectedBacktestForConfigView(null)
}
async function deleteBacktest(id: string) {
const t = new Toast('Deleting backtest')
const client = new BacktestClient({}, apiUrl)
@@ -64,11 +84,8 @@ const BacktestTable: React.FC<IBacktestCards> = ({list, isFetching, setBacktests
.backtest_DeleteBacktest(id)
.then(() => {
t.update('success', 'Backtest deleted')
// Remove the deleted backtest from the list
if (list) {
const updatedList = list.filter(backtest => backtest.id !== id);
setBacktests(updatedList);
}
// Remove the deleted backtest from the store
removeBacktest(id)
})
.catch((err) => {
t.update('error', err)
@@ -218,7 +235,7 @@ const BacktestTable: React.FC<IBacktestCards> = ({list, isFetching, setBacktests
data-value={cell.row.values.name}
onClick={() => deleteBacktest(cell.row.values.id)}
>
<TrashIcon className="text-accent w-4"></TrashIcon>
<TrashIcon className="text-error w-4"></TrashIcon>
</button>
</div>
</>
@@ -227,15 +244,35 @@ const BacktestTable: React.FC<IBacktestCards> = ({list, isFetching, setBacktests
accessor: 'id',
disableFilters: true,
},
{
Cell: ({cell}: any) => (
<>
<div className="tooltip" data-tip="Re-run backtest with same config">
<div className="tooltip" data-tip="Create bot from backtest">
<button
data-value={cell.row.values.name}
onClick={() => handleOpenBotConfigModal(cell.row.original as Backtest)}
>
<PlayIcon className="text-success w-4"></PlayIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'runner',
disableFilters: true,
},
{
Cell: ({cell}: any) => (
<>
<div className="tooltip" data-tip="Rerun backtest with same config">
<button
data-value={cell.row.values.name}
onClick={() => handleOpenBacktestConfigModal(cell.row.original as Backtest)}
>
<CogIcon className="text-info w-4"></CogIcon>
<svg className="w-4 h-4 text-warning" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</>
@@ -247,18 +284,18 @@ const BacktestTable: React.FC<IBacktestCards> = ({list, isFetching, setBacktests
{
Cell: ({cell}: any) => (
<>
<div className="tooltip" data-tip="Create bot from backtest">
<div className="tooltip" data-tip="View/Copy configuration">
<button
data-value={cell.row.values.name}
onClick={() => handleOpenBotConfigModal(cell.row.original as Backtest)}
onClick={() => handleOpenConfigDisplayModal(cell.row.original as Backtest)}
>
<PlayIcon className="text-primary w-4"></PlayIcon>
<CogIcon className="text-info w-4"></CogIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'runner',
accessor: 'config',
disableFilters: true,
}
],
@@ -481,10 +518,15 @@ const BacktestTable: React.FC<IBacktestCards> = ({list, isFetching, setBacktests
mode="backtest"
backtest={selectedBacktestForRerun}
closeModal={handleCloseBacktestConfigModal}
setBacktests={setBacktests}
showLoopSlider={true}
/>
)}
{/* Config Display Modal */}
<ConfigDisplayModal
showModal={showConfigDisplayModal}
onClose={handleCloseConfigDisplayModal}
backtest={selectedBacktestForConfigView}
/>
</>
)}
</>

View File

@@ -3,30 +3,31 @@ import React, {useEffect, useState} from 'react'
import {type SubmitHandler, useForm} from 'react-hook-form'
import useApiUrlStore from '../../../app/store/apiStore'
import useBacktestStore from '../../../app/store/backtestStore'
import {useCustomMoneyManagement} from '../../../app/store/customMoneyManagement'
import {useCustomScenario} from '../../../app/store/customScenario'
import {
AccountClient,
BacktestClient,
BotClient,
DataClient,
MoneyManagement,
MoneyManagementClient,
RiskManagement,
RiskToleranceLevel,
RunBacktestRequest,
Scenario,
ScenarioClient,
ScenarioRequest,
SignalType,
StartBotRequest,
Ticker,
Timeframe,
TradingBotConfigRequest,
UpdateBotConfigRequest,
AccountClient,
BacktestClient,
BotClient,
DataClient,
MoneyManagement,
MoneyManagementClient,
RiskManagement,
RiskToleranceLevel,
RunBacktestRequest,
Scenario,
ScenarioClient,
ScenarioRequest,
SignalType,
StartBotRequest,
Ticker,
Timeframe,
TradingBotConfigRequest,
UpdateBotConfigRequest,
} from '../../../generated/ManagingApi'
import type {IUnifiedTradingConfigInput, UnifiedTradingModalProps} from '../../../global/type'
import {Loader, Slider} from '../../atoms'
import {Loader} from '../../atoms'
import {Modal, Toast} from '../../mollecules'
import FormInput from '../../mollecules/FormInput/FormInput'
import CustomMoneyManagement from '../CustomMoneyManagement/CustomMoneyManagement'
@@ -36,7 +37,6 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
showModal,
closeModal,
mode,
showLoopSlider = false,
setBacktests,
backtest,
existingBot,
@@ -118,11 +118,9 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
// State for collapsible sections
const [showAdvancedParams, setShowAdvancedParams] = useState(false);
const [showRiskManagement, setShowRiskManagement] = useState(false);
const [showJsonImport, setShowJsonImport] = useState(false);
// State for loop slider (backtests only)
const [selectedLoopQuantity, setLoopQuantity] = React.useState<number>(
showLoopSlider ? 3 : 1
);
// Custom components state
const [customMoneyManagement, setCustomMoneyManagement] = useState<MoneyManagement | undefined>(undefined);
@@ -136,7 +134,12 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
// Selected ticker for bots (separate from form tickers for backtests)
const [selectedTicker, setSelectedTicker] = useState<Ticker | undefined>(undefined);
// JSON import state
const [jsonConfig, setJsonConfig] = useState('');
const [importError, setImportError] = useState('');
const { apiUrl } = useApiUrlStore();
const { addBacktest } = useBacktestStore();
const { setCustomMoneyManagement: setGlobalCustomMoneyManagement } = useCustomMoneyManagement();
const { setCustomScenario: setGlobalCustomScenario } = useCustomScenario();
@@ -436,6 +439,84 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
}
};
const handleImportJsonConfig = () => {
try {
setImportError('');
const config = JSON.parse(jsonConfig);
// Load the configuration into the form
if (config.accountName) setValue('accountName', config.accountName);
if (config.timeframe) setValue('timeframe', config.timeframe);
if (config.cooldownPeriod !== undefined) setValue('cooldownPeriod', config.cooldownPeriod);
if (config.maxLossStreak !== undefined) setValue('maxLossStreak', config.maxLossStreak);
if (config.maxPositionTimeHours !== undefined) setValue('maxPositionTimeHours', config.maxPositionTimeHours);
if (config.flipOnlyWhenInProfit !== undefined) setValue('flipOnlyWhenInProfit', config.flipOnlyWhenInProfit);
if (config.closeEarlyWhenProfitable !== undefined) setValue('closeEarlyWhenProfitable', config.closeEarlyWhenProfitable);
if (config.botTradingBalance !== undefined) setValue('balance', config.botTradingBalance);
if (config.useSynthApi !== undefined) setValue('useSynthApi', config.useSynthApi);
if (config.useForPositionSizing !== undefined) setValue('useForPositionSizing', config.useForPositionSizing);
if (config.useForSignalFiltering !== undefined) setValue('useForSignalFiltering', config.useForSignalFiltering);
if (config.useForDynamicStopLoss !== undefined) setValue('useForDynamicStopLoss', config.useForDynamicStopLoss);
// For bot modes, set the ticker
if (mode !== 'backtest' && config.ticker) {
setSelectedTicker(config.ticker);
}
// For backtest mode, set tickers array
if (mode === 'backtest' && config.ticker) {
setValue('tickers', [config.ticker]);
}
// Handle scenario
if (config.scenario) {
setShowCustomScenario(true);
setCustomScenario(config.scenario);
setGlobalCustomScenario(config.scenario);
setSelectedScenario('custom');
} else if (config.scenarioName) {
setSelectedScenario(config.scenarioName);
setValue('scenarioName', config.scenarioName);
setShowCustomScenario(false);
}
// Handle money management
if (config.moneyManagement) {
setShowCustomMoneyManagement(true);
setCustomMoneyManagement(config.moneyManagement);
// Convert decimal values to percentages for UI display
const formattedMoneyManagement = {
...config.moneyManagement,
stopLoss: config.moneyManagement.stopLoss * 100,
takeProfit: config.moneyManagement.takeProfit * 100,
};
setGlobalCustomMoneyManagement(formattedMoneyManagement);
setSelectedMoneyManagement('custom');
}
// Handle risk management
if (config.riskManagement) {
setValue('useCustomRiskManagement', true);
setValue('riskManagement', config.riskManagement);
}
// Bot-specific fields
if (mode !== 'backtest') {
if (config.name) setValue('name', config.name);
if (config.isForWatchingOnly !== undefined) setValue('isForWatchingOnly', config.isForWatchingOnly);
if (config.flipPosition !== undefined) setValue('flipPosition', config.flipPosition);
}
new Toast('Configuration imported successfully!', false);
setShowJsonImport(false);
setJsonConfig('');
} catch (error) {
setImportError('Invalid JSON format. Please check your configuration.');
console.error('JSON import error:', error);
}
};
const onMoneyManagementChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
if (e.target.value === 'custom') {
setShowCustomMoneyManagement(true);
@@ -632,21 +713,9 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
const backtest = await backtestClient.backtest_Run(request);
t.update('success', `${ticker} Backtest Succeeded`);
if (setBacktests) {
setBacktests((arr) => [...arr, backtest]);
}
addBacktest(backtest);
if (showLoopSlider && selectedLoopQuantity > loopCount) {
const nextCount = loopCount + 1;
const mm: MoneyManagement = {
leverage: backtest.optimizedMoneyManagement.leverage,
name: backtest.optimizedMoneyManagement.name + nextCount,
stopLoss: backtest.optimizedMoneyManagement.stopLoss,
takeProfit: backtest.optimizedMoneyManagement.takeProfit,
timeframe: backtest.optimizedMoneyManagement.timeframe,
};
await runBacktest(form, ticker, nextCount);
}
} catch (err: any) {
t.update('error', 'Error: ' + err);
throw err;
@@ -682,6 +751,81 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
</FormInput>
)}
{/* JSON Import Section */}
<div className="divider">
<button
type="button"
className="btn btn-outline btn-sm normal-case"
onClick={() => setShowJsonImport(!showJsonImport)}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
</svg>
Import Configuration from JSON
<svg
className={`w-4 h-4 ml-2 transition-transform duration-200 ${showJsonImport ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
{showJsonImport && (
<div className="space-y-4 border border-accent rounded-lg p-4 bg-base-100">
<div className="space-y-2">
<label className="label">
<span className="label-text">Paste Configuration JSON</span>
</label>
<textarea
className="textarea textarea-bordered w-full h-32"
placeholder='Paste your configuration JSON here...'
value={jsonConfig}
onChange={(e) => setJsonConfig(e.target.value)}
/>
{importError && (
<div className="alert alert-error">
<svg className="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{importError}</span>
</div>
)}
</div>
<div className="flex gap-2">
<button
type="button"
className="btn btn-primary btn-sm"
onClick={handleImportJsonConfig}
disabled={!jsonConfig.trim()}
>
Import Configuration
</button>
<button
type="button"
className="btn btn-ghost btn-sm"
onClick={() => {
setJsonConfig('');
setImportError('');
setShowJsonImport(false);
}}
>
Cancel
</button>
</div>
<div className="text-sm text-base-content/70">
<p>Copy a configuration from the backtest table and paste it here to quickly load all settings.</p>
</div>
</div>
)}
{/* First Row: Account & Timeframe */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormInput label="Account" htmlFor="accountName">
@@ -938,28 +1082,7 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
</div>
)}
{/* Loop Slider (if enabled for backtests) */}
{showLoopSlider && mode === 'backtest' && (
<FormInput
label={
<div className="flex items-center gap-2">
Loop
<div className="tooltip tooltip-top" data-tip="Number of optimization loops to run for money management. Each loop uses the optimized parameters from the previous iteration">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="loop"
>
<Slider
id="loopSlider"
min="1"
max="10"
value={selectedLoopQuantity.toString()}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setLoopQuantity(Number(e.target.value))}
/>
</FormInput>
)}
{/* Max Loss Streak & Max Position Time */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">

View File

@@ -36,7 +36,6 @@ export interface UnifiedTradingModalProps {
showModal: boolean
closeModal: () => void
mode: 'backtest' | 'createBot' | 'updateBot'
showLoopSlider?: boolean
// For backtests
setBacktests?: React.Dispatch<React.SetStateAction<Backtest[]>>

View File

@@ -86,7 +86,15 @@ export type BacktestModalProps = {
showModal: boolean
closeModal: () => void
setBacktests: React.Dispatch<React.SetStateAction<Backtest[]>>
showLoopSlider?: boolean
}
export type UnifiedTradingModalProps = {
showModal: boolean
closeModal: () => void
mode: 'backtest' | 'createBot' | 'updateBot'
setBacktests?: React.Dispatch<React.SetStateAction<Backtest[]>>
backtest?: Backtest
existingBot?: any // TradingBotResponse or whatever the bot type is
}
export type ISpotlightBadge = {
@@ -104,7 +112,6 @@ export type IBacktestsFormInput = {
save: boolean
balance: number
moneyManagement: MoneyManagement
loop: number
startDate: string
endDate: string
cooldownPeriod: number
@@ -122,7 +129,7 @@ export type IBacktestsFormInput = {
export type IBacktestCards = {
list: Backtest[] | undefined
isFetching?: boolean
setBacktests: React.Dispatch<React.SetStateAction<Backtest[]>>
setBacktests?: React.Dispatch<React.SetStateAction<Backtest[]>>
}
export type IFormInput = {

View File

@@ -2,12 +2,10 @@ import React, {useState} from 'react'
import 'react-toastify/dist/ReactToastify.css'
import {Tabs} from '../../components/mollecules'
import type {TabsType} from '../../global/type'
import BacktestLoop from './backtestLoop'
import BacktestPlayground from './backtestPlayground'
import BacktestScanner from './backtestScanner'
import BacktestUpload from './backtestUpload'
import type {TabsType} from '../../global/type.tsx'
// Tabs Array
const tabs: TabsType = [
@@ -21,14 +19,9 @@ const tabs: TabsType = [
index: 2,
label: 'Scanner',
},
{
Component: BacktestLoop,
index: 3,
label: 'Loop',
},
{
Component: BacktestUpload,
index: 4,
index: 3,
label: 'Upload',
},
]

View File

@@ -1,12 +1,11 @@
import React, {useState} from 'react'
import 'react-toastify/dist/ReactToastify.css'
import useBacktestStore from '../../app/store/backtestStore'
import {BacktestCards, UnifiedTradingModal} from '../../components/organism'
import type {Backtest} from '../../generated/ManagingApi'
const BacktestPlayground: React.FC = () => {
const [backtestingResult, setBacktest] = useState<Backtest[]>([])
const { backtests: backtestingResult } = useBacktestStore()
const [showModal, setShowModal] = useState(false)
function openModal() {
@@ -22,12 +21,11 @@ const BacktestPlayground: React.FC = () => {
<button className="btn" onClick={openModal}>
Run New Backtest
</button>
<BacktestCards list={backtestingResult} setBacktests={setBacktest} />
<BacktestCards list={backtestingResult} />
<UnifiedTradingModal
mode="backtest"
showModal={showModal}
closeModal={closeModal}
setBacktests={setBacktest}
/>
</div>
)

View File

@@ -4,17 +4,17 @@ import React, {useEffect, useState} from 'react'
import 'react-toastify/dist/ReactToastify.css'
import useApiUrlStore from '../../app/store/apiStore'
import useBacktestStore from '../../app/store/backtestStore'
import {Loader} from '../../components/atoms'
import {Modal, Toast} from '../../components/mollecules'
import {BacktestTable, UnifiedTradingModal} from '../../components/organism'
import type {Backtest} from '../../generated/ManagingApi'
import {BacktestClient} from '../../generated/ManagingApi'
const BacktestScanner: React.FC = () => {
const [backtestingResult, setBacktest] = useState<Backtest[]>([])
const [showModal, setShowModal] = useState(false)
const [showModalRemoveBacktest, setShowModalRemoveBacktest] = useState(false)
const { apiUrl } = useApiUrlStore()
const { backtests: backtestingResult, setBacktests, setLoading } = useBacktestStore()
const client = new BacktestClient({}, apiUrl)
const { isLoading, refetch, data: backtests } = useQuery({
@@ -24,9 +24,13 @@ const BacktestScanner: React.FC = () => {
useEffect(() => {
if (backtests) {
setBacktest(backtests)
setBacktests(backtests)
}
}, [backtests])
}, [backtests, setBacktests])
useEffect(() => {
setLoading(isLoading)
}, [isLoading, setLoading])
const openModalRemoveBacktests = () => {
setShowModalRemoveBacktest(true)
@@ -99,13 +103,12 @@ const BacktestScanner: React.FC = () => {
</button>
</div>
<BacktestTable list={backtestingResult} isFetching={isLoading} setBacktests={setBacktest} />
<BacktestTable list={backtestingResult} isFetching={isLoading} />
<UnifiedTradingModal
mode="backtest"
showModal={showModal}
closeModal={closeModal}
setBacktests={setBacktest}
/>
{/****************************/}