Fix stop/restart

This commit is contained in:
2025-07-10 12:53:59 +07:00
parent 0c1184a22d
commit 14b3a3c39a
9 changed files with 164 additions and 271 deletions

View File

@@ -20,7 +20,7 @@ namespace Managing.Application.Abstractions
List<Position> Positions { get; set; } List<Position> Positions { get; set; }
Dictionary<DateTime, decimal> WalletBalances { get; set; } Dictionary<DateTime, decimal> WalletBalances { get; set; }
Dictionary<IndicatorType, IndicatorsResultBase> IndicatorsValues { get; set; } Dictionary<IndicatorType, IndicatorsResultBase> IndicatorsValues { get; set; }
DateTime StartupTime { get; set; } DateTime StartupTime { get; }
DateTime CreateDate { get; } DateTime CreateDate { get; }
DateTime PreloadSince { get; set; } DateTime PreloadSince { get; set; }
int PreloadedCandlesCount { get; set; } int PreloadedCandlesCount { get; set; }

View File

@@ -116,14 +116,15 @@ public class TradingBot : Bot, ITradingBot
// Send startup message only for fresh starts (not reboots) // Send startup message only for fresh starts (not reboots)
// Consider it a reboot if the bot was created more than 5 minutes ago // Consider it a reboot if the bot was created more than 5 minutes ago
var timeSinceCreation = DateTime.UtcNow - CreateDate; var timeSinceCreation = DateTime.UtcNow - CreateDate;
var isReboot = timeSinceCreation.TotalMinutes > 5; var isReboot = timeSinceCreation.TotalMinutes > 3;
StartupTime = DateTime.UtcNow;
if (!isReboot) if (!isReboot)
{ {
try try
{ {
StartupTime = DateTime.UtcNow;
var indicatorNames = Indicators.Select(i => i.Type.ToString()).ToList(); var indicatorNames = Indicators.Select(i => i.Type.ToString()).ToList();
var startupMessage = $"🚀 **Bot Started Successfully!**\n\n" + var startupMessage = $"🚀 **Bot Started Successfully!**\n\n" +
$"📊 **Trading Setup:**\n" + $"📊 **Trading Setup:**\n" +
@@ -157,6 +158,7 @@ public class TradingBot : Bot, ITradingBot
} }
} }
InitWorker(Run).GetAwaiter().GetResult(); InitWorker(Run).GetAwaiter().GetResult();
} }
@@ -1458,6 +1460,7 @@ public class TradingBot : Bot, ITradingBot
Identifier = backup.Identifier; Identifier = backup.Identifier;
User = backup.User; User = backup.User;
Status = backup.LastStatus; Status = backup.LastStatus;
StartupTime = data.StartupTime;
} }
/// <summary> /// <summary>

View File

@@ -174,7 +174,18 @@ namespace Managing.Application.ManageBot
bot.User = user; bot.User = user;
// Config is already set correctly from backup data, so we only need to restore signals, positions, etc. // Config is already set correctly from backup data, so we only need to restore signals, positions, etc.
bot.LoadBackup(backupBot); bot.LoadBackup(backupBot);
bot.Start();
// Only start the bot if the backup status is Up
if (backupBot.LastStatus == BotStatus.Up)
{
// Start the bot asynchronously without waiting for completion
_ = Task.Run(() => bot.Start());
}
else
{
// Keep the bot in Down status if it was originally Down
bot.Stop();
}
} }
public IBot CreateSimpleBot(string botName, Workflow workflow) public IBot CreateSimpleBot(string botName, Workflow workflow)
@@ -246,12 +257,15 @@ namespace Managing.Application.ManageBot
{ {
// Stop the bot first to ensure clean state // Stop the bot first to ensure clean state
bot.Stop(); bot.Stop();
// Small delay to ensure stop is complete // Small delay to ensure stop is complete
await Task.Delay(100); await Task.Delay(100);
// Restart the bot (this will update StartupTime) // Restart the bot (this will update StartupTime)
bot.Restart(); bot.Restart();
// Start the bot asynchronously without waiting for completion
_ = Task.Run(() => bot.Start());
var restartMessage = $"🔄 **Bot Restarted**\n\n" + var restartMessage = $"🔄 **Bot Restarted**\n\n" +
$"🎯 **Agent:** {bot.User.AgentName}\n" + $"🎯 **Agent:** {bot.User.AgentName}\n" +

View File

@@ -1,13 +1,16 @@
using MediatR; using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot.Commands namespace Managing.Application.ManageBot.Commands
{ {
public class ToggleIsForWatchingCommand : IRequest<string> public class RestartBotCommand : IRequest<string>
{ {
public string Name { get; } public string Name { get; }
public BotType BotType { get; }
public ToggleIsForWatchingCommand(string name) public RestartBotCommand(BotType botType, string name)
{ {
BotType = botType;
Name = name; Name = name;
} }
} }

View File

@@ -1,16 +1,13 @@
using MediatR; using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot.Commands namespace Managing.Application.ManageBot.Commands
{ {
public class RestartBotCommand : IRequest<string> public class ToggleIsForWatchingCommand : IRequest<string>
{ {
public string Name { get; } public string Name { get; }
public BotType BotType { get; }
public RestartBotCommand(BotType botType, string name) public ToggleIsForWatchingCommand(string name)
{ {
BotType = botType;
Name = name; Name = name;
} }
} }

View File

@@ -31,13 +31,6 @@ public class LoadBackupBotCommandHandler : IRequestHandler<LoadBackupBotCommand,
{ {
try try
{ {
if (backupBot.LastStatus == BotStatus.Down)
{
_logger.LogInformation("Skipping backup bot {Identifier} as it is marked as Down.",
backupBot.Identifier);
continue;
}
var activeBot = _botService.GetActiveBots().FirstOrDefault(b => b.Identifier == backupBot.Identifier); var activeBot = _botService.GetActiveBots().FirstOrDefault(b => b.Identifier == backupBot.Identifier);
if (activeBot == null) if (activeBot == null)
@@ -61,10 +54,20 @@ public class LoadBackupBotCommandHandler : IRequestHandler<LoadBackupBotCommand,
.FirstOrDefault(b => b.Identifier == backupBot.Identifier); .FirstOrDefault(b => b.Identifier == backupBot.Identifier);
if (activeBot != null) if (activeBot != null)
{ {
result[activeBot.Identifier] = BotStatus.Up; // Check if the bot was originally Down
anyBackupStarted = true; if (backupBot.LastStatus == BotStatus.Down)
_logger.LogInformation("Backup bot {Identifier} started successfully.", {
backupBot.Identifier); result[activeBot.Identifier] = BotStatus.Down;
_logger.LogInformation("Backup bot {Identifier} loaded but kept in Down status as it was originally Down.",
backupBot.Identifier);
}
else
{
result[activeBot.Identifier] = BotStatus.Up;
anyBackupStarted = true;
_logger.LogInformation("Backup bot {Identifier} started successfully.",
backupBot.Identifier);
}
break; break;
} }

View File

@@ -85,13 +85,12 @@ namespace Managing.Domain.Bots
{ {
Status = BotStatus.Down; Status = BotStatus.Down;
SaveBackup(); SaveBackup();
CancellationToken.Cancel(); // CancellationToken.Cancel();
} }
public void Restart() public void Restart()
{ {
Status = BotStatus.Up; Status = BotStatus.Up;
SaveBackup();
StartupTime = DateTime.UtcNow; // Update the startup time when the bot is restarted StartupTime = DateTime.UtcNow; // Update the startup time when the bot is restarted
} }

View File

@@ -6,9 +6,17 @@ import {CardPosition, CardSignal, CardText, Toast,} from '../../components/molle
import ManualPositionModal from '../../components/mollecules/ManualPositionModal' import ManualPositionModal from '../../components/mollecules/ManualPositionModal'
import TradesModal from '../../components/mollecules/TradesModal/TradesModal' import TradesModal from '../../components/mollecules/TradesModal/TradesModal'
import {TradeChart, UnifiedTradingModal} from '../../components/organism' import {TradeChart, UnifiedTradingModal} from '../../components/organism'
import {BotClient, BotType, MoneyManagement, Position, TradingBotResponse} from '../../generated/ManagingApi' import {
BotClient,
BotType,
MoneyManagement,
Position,
TradingBotResponse,
UserClient
} from '../../generated/ManagingApi'
import type {IBotList} from '../../global/type.tsx' import type {IBotList} from '../../global/type.tsx'
import MoneyManagementModal from '../settingsPage/moneymanagement/moneyManagementModal' import MoneyManagementModal from '../settingsPage/moneymanagement/moneyManagementModal'
import {useQuery} from '@tanstack/react-query'
function baseBadgeClass(isOutlined = false) { function baseBadgeClass(isOutlined = false) {
let classes = 'text-xs badge badge-sm transition-all duration-200 hover:scale-105 ' let classes = 'text-xs badge badge-sm transition-all duration-200 hover:scale-105 '
@@ -29,6 +37,13 @@ function cardClasses(botStatus: string) {
const BotList: React.FC<IBotList> = ({ list }) => { const BotList: React.FC<IBotList> = ({ list }) => {
const { apiUrl } = useApiUrlStore() const { apiUrl } = useApiUrlStore()
const client = new BotClient({}, apiUrl) const client = new BotClient({}, apiUrl)
const userClient = new UserClient({}, apiUrl)
const { data: currentUser } = useQuery({
queryFn: () => userClient.user_GetCurrentUser(),
queryKey: ['currentUser'],
})
const [showMoneyManagementModal, setShowMoneyManagementModal] = const [showMoneyManagementModal, setShowMoneyManagementModal] =
useState(false) useState(false)
const [selectedMoneyManagement, setSelectedMoneyManagement] = const [selectedMoneyManagement, setSelectedMoneyManagement] =
@@ -38,12 +53,16 @@ const BotList: React.FC<IBotList> = ({ list }) => {
const [showTradesModal, setShowTradesModal] = useState(false) const [showTradesModal, setShowTradesModal] = useState(false)
const [selectedBotForTrades, setSelectedBotForTrades] = useState<{ identifier: string; agentName: string } | null>(null) const [selectedBotForTrades, setSelectedBotForTrades] = useState<{ identifier: string; agentName: string } | null>(null)
const [showBotConfigModal, setShowBotConfigModal] = useState(false) const [showBotConfigModal, setShowBotConfigModal] = useState(false)
const [botConfigModalMode, setBotConfigModalMode] = useState<'createBot' | 'updateBot'>('createBot')
const [selectedBotForUpdate, setSelectedBotForUpdate] = useState<{ const [selectedBotForUpdate, setSelectedBotForUpdate] = useState<{
identifier: string identifier: string
config: any config: any
} | null>(null) } | null>(null)
// Helper function to check if current user owns the bot
const isBotOwner = (botAgentName: string): boolean => {
return currentUser?.agentName === botAgentName
}
function getIsForWatchingBadge(isForWatchingOnly: boolean, identifier: string) { function getIsForWatchingBadge(isForWatchingOnly: boolean, identifier: string) {
const classes = const classes =
baseBadgeClass() + (isForWatchingOnly ? ' bg-accent' : ' bg-primary') baseBadgeClass() + (isForWatchingOnly ? ' bg-accent' : ' bg-primary')
@@ -204,26 +223,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
) )
} }
function getCreateBotBadge() {
const classes = baseBadgeClass() + ' bg-success'
return (
<button className={classes} onClick={() => openCreateBotModal()}>
<p className="text-primary-content flex">
<PlusCircleIcon width={15}></PlusCircleIcon>
Create Bot
</p>
</button>
)
}
function openCreateBotModal() {
setBotConfigModalMode('createBot')
setSelectedBotForUpdate(null)
setShowBotConfigModal(true)
}
function openUpdateBotModal(bot: TradingBotResponse) { function openUpdateBotModal(bot: TradingBotResponse) {
setBotConfigModalMode('updateBot')
setSelectedBotForUpdate({ setSelectedBotForUpdate({
identifier: bot.identifier, identifier: bot.identifier,
config: bot.config config: bot.config
@@ -233,12 +233,6 @@ const BotList: React.FC<IBotList> = ({ list }) => {
return ( return (
<div className="flex flex-wrap m-4 -mx-4"> <div className="flex flex-wrap m-4 -mx-4">
<div className="w-full p-2 mb-4">
<div className="flex justify-end">
{getCreateBotBadge()}
</div>
</div>
{list.map((bot: TradingBotResponse, index: number) => ( {list.map((bot: TradingBotResponse, index: number) => (
<div <div
key={index.toString()} key={index.toString()}
@@ -246,13 +240,13 @@ const BotList: React.FC<IBotList> = ({ list }) => {
> >
<div className={cardClasses(bot.status)}> <div className={cardClasses(bot.status)}>
<figure className="w-full"> <figure className="w-full">
{ {bot.candles && bot.candles.length > 0 ? (
<TradeChart <TradeChart
candles={bot.candles} candles={bot.candles}
positions={bot.positions} positions={bot.positions}
signals={bot.signals} signals={bot.signals}
></TradeChart> ></TradeChart>
} ) : null}
</figure> </figure>
<div className="card-body"> <div className="card-body">
<div className="mb-4"> <div className="mb-4">
@@ -266,16 +260,18 @@ const BotList: React.FC<IBotList> = ({ list }) => {
{/* Info Badges */} {/* Info Badges */}
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{getMoneyManagementBadge(bot.config.moneyManagement)} {getMoneyManagementBadge(bot.config.moneyManagement)}
{getIsForWatchingBadge(bot.config.isForWatchingOnly, bot.identifier)}
</div> </div>
{/* Action Badges */} {/* Action Badges - Only show for bot owners */}
<div className="flex flex-wrap gap-1"> {isBotOwner(bot.agentName) && (
{getToggleBotStatusBadge(bot.status, bot.identifier, bot.config.flipPosition ? BotType.FlippingBot : BotType.SimpleBot)} <div className="flex flex-wrap gap-1">
{getUpdateBotBadge(bot)} {getIsForWatchingBadge(bot.config.isForWatchingOnly, bot.identifier)}
{getManualPositionBadge(bot.identifier)} {getToggleBotStatusBadge(bot.status, bot.identifier, bot.config.flipPosition ? BotType.FlippingBot : BotType.SimpleBot)}
{getDeleteBadge(bot.identifier)} {getUpdateBotBadge(bot)}
</div> {getManualPositionBadge(bot.identifier)}
{getDeleteBadge(bot.identifier)}
</div>
)}
</div> </div>
</div> </div>
@@ -356,7 +352,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
}} }}
/> />
<UnifiedTradingModal <UnifiedTradingModal
mode={botConfigModalMode} mode="updateBot"
showModal={showBotConfigModal} showModal={showBotConfigModal}
closeModal={() => { closeModal={() => {
setShowBotConfigModal(false) setShowBotConfigModal(false)

View File

@@ -1,70 +1,58 @@
import { PlayIcon, StopIcon, ViewGridAddIcon } from '@heroicons/react/solid' import {PlayIcon, StopIcon, ViewGridAddIcon} from '@heroicons/react/solid'
import React, { useEffect, useState } from 'react' import React, {useState} from 'react'
import type { SubmitHandler } from 'react-hook-form'
import { useForm } from 'react-hook-form'
import 'react-toastify/dist/ReactToastify.css' import 'react-toastify/dist/ReactToastify.css'
import { Hub } from '../../app/providers/Hubs'
import useApiUrlStore from '../../app/store/apiStore' import useApiUrlStore from '../../app/store/apiStore'
import { Toast } from '../../components/mollecules' import {Toast} from '../../components/mollecules'
import type { import {UnifiedTradingModal} from '../../components/organism'
Account, import type {TradingBotResponse,} from '../../generated/ManagingApi'
Scenario, import {BotClient, UserClient,} from '../../generated/ManagingApi'
StartBotRequest,
TradingBot,
} from '../../generated/ManagingApi'
import {
AccountClient,
ScenarioClient,
BotClient,
BotType,
Ticker,
Timeframe,
} from '../../generated/ManagingApi'
import BotList from './botList' import BotList from './botList'
import { useQuery } from '@tanstack/react-query' import {useQuery} from '@tanstack/react-query'
const Bots: React.FC = () => { const Bots: React.FC = () => {
const [showModal, setShowModal] = useState(false) const [activeTab, setActiveTab] = useState(0)
const { register, handleSubmit } = useForm<StartBotRequest>() const [showBotConfigModal, setShowBotConfigModal] = useState(false)
const { apiUrl } = useApiUrlStore() const { apiUrl } = useApiUrlStore()
const botClient = new BotClient({}, apiUrl) const botClient = new BotClient({}, apiUrl)
const userClient = new UserClient({}, apiUrl)
const onSubmit: SubmitHandler<StartBotRequest> = async (form) => {
closeModal()
const t = new Toast('Bot is starting')
await botClient
.bot_Start(form)
.then((status: string) => {
t.update('success', 'Bot status : ' + status)
})
.catch((err) => {
t.update('error', err)
})
}
const { data: bots } = useQuery({ const { data: bots } = useQuery({
queryFn: () => botClient.bot_GetActiveBots(), queryFn: () => botClient.bot_GetActiveBots(),
queryKey: ['bots'], queryKey: ['bots'],
}) })
const scenarioClient = new ScenarioClient({}, apiUrl) const { data: currentUser } = useQuery({
const { data: scenarios } = useQuery({ queryFn: () => userClient.user_GetCurrentUser(),
queryFn: () => scenarioClient.scenario_GetScenarios(), queryKey: ['currentUser'],
queryKey: ['scenarios'],
}) })
const accountClient = new AccountClient({}, apiUrl) // Filter bots based on active tab and current user
const { data: accounts } = useQuery({ const getFilteredBots = (): TradingBotResponse[] => {
queryFn: () => accountClient.account_GetAccounts(), if (!bots || !currentUser) return []
queryKey: ['accounts'],
}) switch (activeTab) {
case 0: // All Active Bots
return bots.filter(bot => bot.status === 'Up')
case 1: // My Active Bots
return bots.filter(bot => bot.status === 'Up' && bot.agentName === currentUser.agentName)
case 2: // My Down Bots
return bots.filter(bot => bot.status === 'Down' && bot.agentName === currentUser.agentName)
default:
return bots
}
}
const filteredBots = getFilteredBots()
function openCreateBotModal() {
setShowBotConfigModal(true)
}
// const setupHubConnection = async () => { // const setupHubConnection = async () => {
// const hub = new Hub('bothub', apiUrl).hub // const hub = new Hub('bothub', apiUrl).hub
// hub.on('BotsSubscription', (bots: TradingBot[]) => { // hub.on('BotsSubscription', (bots: TradingBotResponse[]) => {
// // eslint-disable-next-line no-console // // eslint-disable-next-line no-console
// console.log( // console.log(
// 'bot List', // 'bot List',
@@ -78,14 +66,6 @@ const Bots: React.FC = () => {
// return hub // return hub
// } // }
function openModal() {
setShowModal(true)
}
function closeModal() {
setShowModal(false)
}
async function stopAllBots() { async function stopAllBots() {
const t = new Toast('Stoping all bots') const t = new Toast('Stoping all bots')
await botClient await botClient
@@ -110,163 +90,61 @@ const Bots: React.FC = () => {
}) })
} }
const tabs = [
{ label: 'All Active Bots', index: 0 },
{ label: 'My Active Bots', index: 1 },
{ label: 'My Down Bots', index: 2 },
]
return ( return (
<div> <div>
<div className="container mx-auto"> <div className="container mx-auto">
<div className="tooltip" data-tip="Run new bot"> {/* Action Buttons */}
<button className="btn btn-primary m-1 text-xs" onClick={openModal}> <div className="flex gap-2 mb-4">
<ViewGridAddIcon width="20"></ViewGridAddIcon> <div className="tooltip" data-tip="Create new bot">
</button> <button className="btn btn-primary m-1 text-xs" onClick={openCreateBotModal}>
<ViewGridAddIcon width="20"></ViewGridAddIcon>
</button>
</div>
<div className="tooltip" data-tip="Stop all bots">
<button className="btn btn-error m-1 text-xs" onClick={stopAllBots}>
<StopIcon width="20"></StopIcon>
</button>
</div>
<div className="tooltip" data-tip="Restart all bots">
<button
className="btn btn-success m-1 text-xs"
onClick={restartAllBots}
>
<PlayIcon width="20"></PlayIcon>
</button>
</div>
</div> </div>
<div className="tooltip" data-tip="Stop all bots">
<button className="btn btn-error m-1 text-xs" onClick={stopAllBots}> {/* Tabs */}
<StopIcon width="20"></StopIcon> <div className="tabs tabs-boxed mb-4">
</button> {tabs.map((tab) => (
<button
key={tab.index}
className={`tab ${activeTab === tab.index ? 'tab-active' : ''}`}
onClick={() => setActiveTab(tab.index)}
>
{tab.label}
</button>
))}
</div> </div>
<div className="tooltip" data-tip="Restart all bots">
<button {/* Bot List */}
className="btn btn-success m-1 text-xs" <BotList list={filteredBots} />
onClick={restartAllBots}
> {/* Unified Trading Modal */}
<PlayIcon width="20"></PlayIcon> <UnifiedTradingModal
</button> mode="createBot"
</div> showModal={showBotConfigModal}
<BotList list={bots || []} /> closeModal={() => {
{showModal ? ( setShowBotConfigModal(false)
<> }}
<div> />
<form onSubmit={handleSubmit(onSubmit)}>
<div className="modal modal-bottom sm:modal-middle modal-open">
<div className="modal-box">
<button
onClick={closeModal}
className="btn btn-sm btn-circle right-2 top-2 absolute"
>
</button>
<div className="text-primary mb-3 text-xl">Run a bot</div>
<div className="form-control">
<div className="input-group">
<label htmlFor="accountName" className="label mr-6">
Account
</label>
<select
className="select select-bordered w-full h-auto max-w-xs"
{...register('accountName')}
>
{accounts.map((item) => (
<option
style={{ color: 'black' }}
key={item.name}
value={item.name}
>
{item.name}
</option>
))}
</select>
</div>
</div>
<div className="form-control">
<div className="input-group">
<label htmlFor="botName" className="label mr-6">
Name
</label>
<input
className="w-full max-w-xs"
{...register('botName')}
></input>
</div>
</div>
<div className="form-control">
<div className="input-group">
<label htmlFor="botType" className="label mr-6">
Type
</label>
<select
className="select w-full max-w-xs"
{...register('botType')}
>
{Object.keys(BotType).map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</div>
</div>
<div className="form-control">
<div className="input-group">
<label htmlFor="ticker" className="label mr-6">
Ticker
</label>
<select
className="select w-full max-w-xs"
{...register('ticker')}
>
{Object.keys(Ticker).map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</div>
</div>
<div className="form-control">
<div className="input-group">
<label htmlFor="timeframe" className="label mr-6">
Timeframes
</label>
<select
className="select w-full max-w-xs"
{...register('timeframe')}
>
{Object.keys(Timeframe).map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</div>
</div>
<div className="form-control">
<div className="input-group">
<label htmlFor="scenario" className="label mr-6">
Scenario
</label>
<select
className="select select-bordered w-full h-auto max-w-xs"
{...register('scenario')}
>
{scenarios.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
</select>
</div>
</div>
<div className="form-control">
<div className="input-group">
<label htmlFor="isForWatchOnly" className="label mr-6">
Watch Only
</label>
<input
type="checkbox"
{...register('isForWatchOnly')}
></input>
</div>
</div>
<div className="modal-action">
<button type="submit" className="btn">
Run
</button>
</div>
</div>
</div>
</form>
</div>
</>
) : null}
</div> </div>
</div> </div>
) )