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; }
Dictionary<DateTime, decimal> WalletBalances { get; set; }
Dictionary<IndicatorType, IndicatorsResultBase> IndicatorsValues { get; set; }
DateTime StartupTime { get; set; }
DateTime StartupTime { get; }
DateTime CreateDate { get; }
DateTime PreloadSince { 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)
// Consider it a reboot if the bot was created more than 5 minutes ago
var timeSinceCreation = DateTime.UtcNow - CreateDate;
var isReboot = timeSinceCreation.TotalMinutes > 5;
var isReboot = timeSinceCreation.TotalMinutes > 3;
StartupTime = DateTime.UtcNow;
if (!isReboot)
{
try
{
StartupTime = DateTime.UtcNow;
var indicatorNames = Indicators.Select(i => i.Type.ToString()).ToList();
var startupMessage = $"🚀 **Bot Started Successfully!**\n\n" +
$"📊 **Trading Setup:**\n" +
@@ -157,6 +158,7 @@ public class TradingBot : Bot, ITradingBot
}
}
InitWorker(Run).GetAwaiter().GetResult();
}
@@ -1458,6 +1460,7 @@ public class TradingBot : Bot, ITradingBot
Identifier = backup.Identifier;
User = backup.User;
Status = backup.LastStatus;
StartupTime = data.StartupTime;
}
/// <summary>

View File

@@ -174,7 +174,18 @@ namespace Managing.Application.ManageBot
bot.User = user;
// Config is already set correctly from backup data, so we only need to restore signals, positions, etc.
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)
@@ -253,6 +264,9 @@ namespace Managing.Application.ManageBot
// Restart the bot (this will update StartupTime)
bot.Restart();
// Start the bot asynchronously without waiting for completion
_ = Task.Run(() => bot.Start());
var restartMessage = $"🔄 **Bot Restarted**\n\n" +
$"🎯 **Agent:** {bot.User.AgentName}\n" +
$"🤖 **Bot Name:** {bot.Name}\n" +

View File

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

View File

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

View File

@@ -31,13 +31,6 @@ public class LoadBackupBotCommandHandler : IRequestHandler<LoadBackupBotCommand,
{
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);
if (activeBot == null)
@@ -60,11 +53,21 @@ public class LoadBackupBotCommandHandler : IRequestHandler<LoadBackupBotCommand,
activeBot = _botService.GetActiveBots()
.FirstOrDefault(b => b.Identifier == backupBot.Identifier);
if (activeBot != null)
{
// Check if the bot was originally Down
if (backupBot.LastStatus == BotStatus.Down)
{
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;
}

View File

@@ -85,13 +85,12 @@ namespace Managing.Domain.Bots
{
Status = BotStatus.Down;
SaveBackup();
CancellationToken.Cancel();
// CancellationToken.Cancel();
}
public void Restart()
{
Status = BotStatus.Up;
SaveBackup();
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 TradesModal from '../../components/mollecules/TradesModal/TradesModal'
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 MoneyManagementModal from '../settingsPage/moneymanagement/moneyManagementModal'
import {useQuery} from '@tanstack/react-query'
function baseBadgeClass(isOutlined = false) {
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 { apiUrl } = useApiUrlStore()
const client = new BotClient({}, apiUrl)
const userClient = new UserClient({}, apiUrl)
const { data: currentUser } = useQuery({
queryFn: () => userClient.user_GetCurrentUser(),
queryKey: ['currentUser'],
})
const [showMoneyManagementModal, setShowMoneyManagementModal] =
useState(false)
const [selectedMoneyManagement, setSelectedMoneyManagement] =
@@ -38,12 +53,16 @@ const BotList: React.FC<IBotList> = ({ list }) => {
const [showTradesModal, setShowTradesModal] = useState(false)
const [selectedBotForTrades, setSelectedBotForTrades] = useState<{ identifier: string; agentName: string } | null>(null)
const [showBotConfigModal, setShowBotConfigModal] = useState(false)
const [botConfigModalMode, setBotConfigModalMode] = useState<'createBot' | 'updateBot'>('createBot')
const [selectedBotForUpdate, setSelectedBotForUpdate] = useState<{
identifier: string
config: any
} | 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) {
const classes =
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) {
setBotConfigModalMode('updateBot')
setSelectedBotForUpdate({
identifier: bot.identifier,
config: bot.config
@@ -233,12 +233,6 @@ const BotList: React.FC<IBotList> = ({ list }) => {
return (
<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) => (
<div
key={index.toString()}
@@ -246,13 +240,13 @@ const BotList: React.FC<IBotList> = ({ list }) => {
>
<div className={cardClasses(bot.status)}>
<figure className="w-full">
{
{bot.candles && bot.candles.length > 0 ? (
<TradeChart
candles={bot.candles}
positions={bot.positions}
signals={bot.signals}
></TradeChart>
}
) : null}
</figure>
<div className="card-body">
<div className="mb-4">
@@ -266,16 +260,18 @@ const BotList: React.FC<IBotList> = ({ list }) => {
{/* Info Badges */}
<div className="flex flex-wrap gap-1">
{getMoneyManagementBadge(bot.config.moneyManagement)}
{getIsForWatchingBadge(bot.config.isForWatchingOnly, bot.identifier)}
</div>
{/* Action Badges */}
{/* Action Badges - Only show for bot owners */}
{isBotOwner(bot.agentName) && (
<div className="flex flex-wrap gap-1">
{getIsForWatchingBadge(bot.config.isForWatchingOnly, bot.identifier)}
{getToggleBotStatusBadge(bot.status, bot.identifier, bot.config.flipPosition ? BotType.FlippingBot : BotType.SimpleBot)}
{getUpdateBotBadge(bot)}
{getManualPositionBadge(bot.identifier)}
{getDeleteBadge(bot.identifier)}
</div>
)}
</div>
</div>
@@ -356,7 +352,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
}}
/>
<UnifiedTradingModal
mode={botConfigModalMode}
mode="updateBot"
showModal={showBotConfigModal}
closeModal={() => {
setShowBotConfigModal(false)

View File

@@ -1,70 +1,58 @@
import { PlayIcon, StopIcon, ViewGridAddIcon } from '@heroicons/react/solid'
import React, { useEffect, useState } from 'react'
import type { SubmitHandler } from 'react-hook-form'
import { useForm } from 'react-hook-form'
import {PlayIcon, StopIcon, ViewGridAddIcon} from '@heroicons/react/solid'
import React, {useState} from 'react'
import 'react-toastify/dist/ReactToastify.css'
import { Hub } from '../../app/providers/Hubs'
import useApiUrlStore from '../../app/store/apiStore'
import { Toast } from '../../components/mollecules'
import type {
Account,
Scenario,
StartBotRequest,
TradingBot,
} from '../../generated/ManagingApi'
import {
AccountClient,
ScenarioClient,
BotClient,
BotType,
Ticker,
Timeframe,
} from '../../generated/ManagingApi'
import {Toast} from '../../components/mollecules'
import {UnifiedTradingModal} from '../../components/organism'
import type {TradingBotResponse,} from '../../generated/ManagingApi'
import {BotClient, UserClient,} from '../../generated/ManagingApi'
import BotList from './botList'
import { useQuery } from '@tanstack/react-query'
import {useQuery} from '@tanstack/react-query'
const Bots: React.FC = () => {
const [showModal, setShowModal] = useState(false)
const { register, handleSubmit } = useForm<StartBotRequest>()
const [activeTab, setActiveTab] = useState(0)
const [showBotConfigModal, setShowBotConfigModal] = useState(false)
const { apiUrl } = useApiUrlStore()
const botClient = new BotClient({}, 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 userClient = new UserClient({}, apiUrl)
const { data: bots } = useQuery({
queryFn: () => botClient.bot_GetActiveBots(),
queryKey: ['bots'],
})
const scenarioClient = new ScenarioClient({}, apiUrl)
const { data: scenarios } = useQuery({
queryFn: () => scenarioClient.scenario_GetScenarios(),
queryKey: ['scenarios'],
const { data: currentUser } = useQuery({
queryFn: () => userClient.user_GetCurrentUser(),
queryKey: ['currentUser'],
})
const accountClient = new AccountClient({}, apiUrl)
const { data: accounts } = useQuery({
queryFn: () => accountClient.account_GetAccounts(),
queryKey: ['accounts'],
})
// Filter bots based on active tab and current user
const getFilteredBots = (): TradingBotResponse[] => {
if (!bots || !currentUser) return []
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 hub = new Hub('bothub', apiUrl).hub
// hub.on('BotsSubscription', (bots: TradingBot[]) => {
// hub.on('BotsSubscription', (bots: TradingBotResponse[]) => {
// // eslint-disable-next-line no-console
// console.log(
// 'bot List',
@@ -78,14 +66,6 @@ const Bots: React.FC = () => {
// return hub
// }
function openModal() {
setShowModal(true)
}
function closeModal() {
setShowModal(false)
}
async function stopAllBots() {
const t = new Toast('Stoping all bots')
await botClient
@@ -110,11 +90,19 @@ 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 (
<div>
<div className="container mx-auto">
<div className="tooltip" data-tip="Run new bot">
<button className="btn btn-primary m-1 text-xs" onClick={openModal}>
{/* Action Buttons */}
<div className="flex gap-2 mb-4">
<div className="tooltip" data-tip="Create new bot">
<button className="btn btn-primary m-1 text-xs" onClick={openCreateBotModal}>
<ViewGridAddIcon width="20"></ViewGridAddIcon>
</button>
</div>
@@ -131,142 +119,32 @@ const Bots: React.FC = () => {
<PlayIcon width="20"></PlayIcon>
</button>
</div>
<BotList list={bots || []} />
{showModal ? (
<>
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="modal modal-bottom sm:modal-middle modal-open">
<div className="modal-box">
</div>
{/* Tabs */}
<div className="tabs tabs-boxed mb-4">
{tabs.map((tab) => (
<button
onClick={closeModal}
className="btn btn-sm btn-circle right-2 top-2 absolute"
key={tab.index}
className={`tab ${activeTab === tab.index ? 'tab-active' : ''}`}
onClick={() => setActiveTab(tab.index)}
>
{tab.label}
</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}
{/* Bot List */}
<BotList list={filteredBots} />
{/* Unified Trading Modal */}
<UnifiedTradingModal
mode="createBot"
showModal={showBotConfigModal}
closeModal={() => {
setShowBotConfigModal(false)
}}
/>
</div>
</div>
)