diff --git a/src/Managing.Api/Controllers/BotController.cs b/src/Managing.Api/Controllers/BotController.cs index 0dc0ba70..48434065 100644 --- a/src/Managing.Api/Controllers/BotController.cs +++ b/src/Managing.Api/Controllers/BotController.cs @@ -509,6 +509,7 @@ public class BotController : BaseController Roi = item.Roi, Identifier = item.Identifier.ToString(), AgentName = item.User.AgentName, + MasterAgentName = item.MasterBotUser?.AgentName ?? item.User.AgentName, CreateDate = item.CreateDate, StartupTime = item.StartupTime, Name = item.Name, diff --git a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs index 3ec83f01..9de6b1dc 100644 --- a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs +++ b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs @@ -407,7 +407,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable _copyTradingStreamHandle = await streamProvider.GetStream(streamId) .SubscribeAsync(OnCopyTradingPositionReceivedAsync); - _logger.LogInformation("LiveTradingBotGrain {GrainId} subscribed to copy trading stream for master bot {MasterBotId}", + _logger.LogInformation( + "LiveTradingBotGrain {GrainId} subscribed to copy trading stream for master bot {MasterBotId}", this.GetPrimaryKey(), _state.State.Config.MasterBotIdentifier.Value); } catch (Exception ex) @@ -425,7 +426,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable { await _copyTradingStreamHandle.UnsubscribeAsync(); _copyTradingStreamHandle = null; - _logger.LogInformation("LiveTradingBotGrain {GrainId} unsubscribed from copy trading stream", this.GetPrimaryKey()); + _logger.LogInformation("LiveTradingBotGrain {GrainId} unsubscribed from copy trading stream", + this.GetPrimaryKey()); } } @@ -438,7 +440,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable { if (_tradingBot == null) { - _logger.LogWarning("Received copy trading position {PositionId} but trading bot is not running for bot {GrainId}", + _logger.LogWarning( + "Received copy trading position {PositionId} but trading bot is not running for bot {GrainId}", masterPosition.Identifier, this.GetPrimaryKey()); return; } @@ -511,41 +514,52 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable // Check if copy trading authorization is still valid if (_state.State.Config.IsForCopyTrading && _state.State.Config.MasterBotIdentifier.HasValue) { - try + // Check if copy trading validation should be bypassed (for testing) + var enableValidation = Environment.GetEnvironmentVariable("ENABLE_COPY_TRADING_VALIDATION")? + .Equals("true", StringComparison.OrdinalIgnoreCase) ?? true; + + if (enableValidation) { - var ownedKeys = await _kaigenService.GetOwnedKeysAsync(_state.State.User); - - var masterStrategy = await ServiceScopeHelpers.WithScopedService( - _scopeFactory, - async botService => await botService.GetBotByIdentifier(_state.State.Config.MasterBotIdentifier.Value)); - - if (masterStrategy == null) + try { - _logger.LogWarning("Master strategy {MasterBotId} not found", _state.State.Config.MasterBotIdentifier.Value); - return; + var ownedKeys = await _kaigenService.GetOwnedKeysAsync(_state.State.User); + + var masterStrategy = await ServiceScopeHelpers.WithScopedService( + _scopeFactory, + async botService => + await botService.GetBotByIdentifier(_state.State.Config.MasterBotIdentifier.Value)); + + if (masterStrategy == null) + { + _logger.LogWarning("Master strategy {MasterBotId} not found", + _state.State.Config.MasterBotIdentifier.Value); + return; + } + + var hasMasterStrategyKey = ownedKeys.Items.Any(key => + string.Equals(key.AgentName, masterStrategy.User.AgentName, + StringComparison.OrdinalIgnoreCase) && + key.Owned >= 1); + + if (!hasMasterStrategyKey) + { + _logger.LogWarning( + "Copy trading bot {GrainId} no longer has authorization for master strategy {MasterBotId}. Stopping bot.", + this.GetPrimaryKey(), _state.State.Config.MasterBotIdentifier.Value); + + await StopAsync( + "Copy trading authorization revoked - user no longer owns keys for master strategy"); + return; + } } - - var hasMasterStrategyKey = ownedKeys.Items.Any(key => - string.Equals(key.AgentName, masterStrategy.User.AgentName, StringComparison.OrdinalIgnoreCase) && - key.Owned >= 1); - - if (!hasMasterStrategyKey) + catch (Exception ex) { - _logger.LogWarning( - "Copy trading bot {GrainId} no longer has authorization for master strategy {MasterBotId}. Stopping bot.", + _logger.LogError(ex, + "Failed to verify copy trading authorization for bot {GrainId} with master strategy {MasterBotId}. Continuing execution.", this.GetPrimaryKey(), _state.State.Config.MasterBotIdentifier.Value); - - await StopAsync("Copy trading authorization revoked - user no longer owns keys for master strategy"); - return; + SentrySdk.CaptureException(ex); } } - catch (Exception ex) - { - _logger.LogError(ex, - "Failed to verify copy trading authorization for bot {GrainId} with master strategy {MasterBotId}. Continuing execution.", - this.GetPrimaryKey(), _state.State.Config.MasterBotIdentifier.Value); - SentrySdk.CaptureException(ex); - } } if (_tradingBot.Positions.Any(p => p.Value.IsOpen() || p.Value.Status.Equals(PositionStatus.New))) @@ -1128,7 +1142,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable _scopeFactory, async tradingService => await tradingService.GetPositionsByInitiatorIdentifierAsync(botId)); - var openPositions = positions?.Where(p => p.IsOpen() || p.Status.Equals(PositionStatus.New)).ToList() ?? new List(); + var openPositions = positions?.Where(p => p.IsOpen() || p.Status.Equals(PositionStatus.New)).ToList() ?? + new List(); if (openPositions.Any()) { @@ -1140,13 +1155,16 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable { try { - _logger.LogInformation("Closing position {PositionId} for bot {GrainId}", position.Identifier, botId); + _logger.LogInformation("Closing position {PositionId} for bot {GrainId}", position.Identifier, + botId); await ClosePositionAsync(position.Identifier); - _logger.LogInformation("Successfully closed position {PositionId} for bot {GrainId}", position.Identifier, botId); + _logger.LogInformation("Successfully closed position {PositionId} for bot {GrainId}", + position.Identifier, botId); } catch (Exception ex) { - _logger.LogError(ex, "Failed to close position {PositionId} for bot {GrainId}", position.Identifier, botId); + _logger.LogError(ex, "Failed to close position {PositionId} for bot {GrainId}", + position.Identifier, botId); // Continue with other positions even if one fails } } diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs index bb58a788..0165e1ed 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -93,13 +93,16 @@ public class TradingBotBase : ITradingBot { case BotStatus.Saved: var indicatorNames = Config.Scenario.Indicators.Select(i => i.Type.ToString()).ToList(); + var modeText = Config.IsForWatchingOnly ? "Watch Only" : + Config.IsForCopyTrading ? "Copy Trading" : "Live Trading"; + var startupMessage = $"🚀 Bot Started Successfully\n\n" + $"📊 Trading Setup:\n" + $"🎯 Ticker: `{Config.Ticker}`\n" + $"⏰ Timeframe: `{Config.Timeframe}`\n" + $"🎮 Scenario: `{Config.Scenario?.Name ?? "Unknown"}`\n" + $"💰 Balance: `${Config.BotTradingBalance:F2}`\n" + - $"👀 Mode: `{(Config.IsForWatchingOnly ? "Watch Only" : "Live Trading")}`\n\n" + + $"👀 Mode: `{modeText}`\n\n" + $"📈 Active Indicators: `{string.Join(", ", indicatorNames)}`\n\n" + $"✅ Ready to monitor signals and execute trades\n" + $"📢 Notifications will be sent when positions are triggered"; diff --git a/src/Managing.Application/ManageBot/BotService.cs b/src/Managing.Application/ManageBot/BotService.cs index d4bd3294..2593dfe2 100644 --- a/src/Managing.Application/ManageBot/BotService.cs +++ b/src/Managing.Application/ManageBot/BotService.cs @@ -71,7 +71,9 @@ namespace Managing.Application.ManageBot try { var config = await grain.GetConfiguration(); - var account = await grain.GetAccount(); + var account = await ServiceScopeHelpers.WithScopedService( + _scopeFactory, + async accountService => await accountService.GetAccount(config.AccountName, true, false)); await grain.StopAsync("Deleting bot"); await _botRepository.DeleteBot(identifier); await grain.DeleteAsync(); diff --git a/src/Managing.Application/ManageBot/StartCopyTradingCommandHandler.cs b/src/Managing.Application/ManageBot/StartCopyTradingCommandHandler.cs index 9b4f95e3..35caac19 100644 --- a/src/Managing.Application/ManageBot/StartCopyTradingCommandHandler.cs +++ b/src/Managing.Application/ManageBot/StartCopyTradingCommandHandler.cs @@ -8,7 +8,6 @@ using Managing.Domain.Accounts; using Managing.Domain.Bots; using Managing.Domain.Users; using MediatR; -using System; using static Managing.Common.Enums; namespace Managing.Application.ManageBot @@ -50,7 +49,7 @@ namespace Managing.Application.ManageBot // Check if copy trading validation should be bypassed (for testing) var enableValidation = Environment.GetEnvironmentVariable("ENABLE_COPY_TRADING_VALIDATION")? - .Equals("true", StringComparison.OrdinalIgnoreCase) == true; + .Equals("true", StringComparison.OrdinalIgnoreCase) ?? true; if (enableValidation) { diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBotRepository.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBotRepository.cs index 79cc0d79..5908d23a 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBotRepository.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBotRepository.cs @@ -194,6 +194,7 @@ public class PostgreSqlBotRepository : IBotRepository var query = _context.Bots .AsNoTracking() .Include(m => m.User) + .Include(m => m.MasterBotUser) .AsQueryable(); // Apply filters diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs index 85932b03..4f94388c 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs @@ -754,7 +754,8 @@ public static class PostgreSqlMappers Fees = entity.Fees, LongPositionCount = entity.LongPositionCount, ShortPositionCount = entity.ShortPositionCount, - MasterBotUserId = entity.MasterBotUserId + MasterBotUserId = entity.MasterBotUserId, + MasterBotUser = entity.MasterBotUser != null ? Map(entity.MasterBotUser) : null }; return bot; diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index 3a9ded08..afa818fe 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -4866,7 +4866,6 @@ export interface Backtest { endDate: Date; statistics: PerformanceMetrics; fees: number; - walletBalances: KeyValuePairOfDateTimeAndDecimal[]; user: User; score: number; requestId?: string; @@ -4874,6 +4873,7 @@ export interface Backtest { scoreMessage?: string; initialBalance: number; netPnl: number; + positionCount: number; } export interface TradingBotConfig { @@ -4900,6 +4900,7 @@ export interface TradingBotConfig { useForDynamicStopLoss?: boolean; isForCopyTrading?: boolean; masterBotIdentifier?: string | null; + masterBotUserId?: number | null; } export interface LightMoneyManagement { @@ -5007,6 +5008,7 @@ export interface Position { initiator: PositionInitiator; user: User; initiatorIdentifier: string; + recoveryAttempted?: boolean; } export enum TradeDirection { @@ -5128,11 +5130,6 @@ export interface PerformanceMetrics { totalPnL?: number; } -export interface KeyValuePairOfDateTimeAndDecimal { - key?: Date; - value?: number; -} - export interface DeleteBacktestsRequest { backtestIds: string[]; } @@ -5153,6 +5150,7 @@ export interface LightBacktestResponse { scoreMessage: string; initialBalance: number; netPnl: number; + positionCount: number; } export interface PaginatedBacktestsResponse { @@ -5200,6 +5198,7 @@ export interface LightBacktest { ticker?: string | null; initialBalance?: number; netPnl?: number; + positionCount?: number; } export interface RunBacktestRequest { @@ -5490,6 +5489,7 @@ export interface TradingBotResponse { startupTime: Date; name: string; ticker: Ticker; + masterAgentName?: string | null; } export interface PaginatedResponseOfTradingBotResponse { diff --git a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts index beba62d9..e2ed5656 100644 --- a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts +++ b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts @@ -332,7 +332,6 @@ export interface Backtest { endDate: Date; statistics: PerformanceMetrics; fees: number; - walletBalances: KeyValuePairOfDateTimeAndDecimal[]; user: User; score: number; requestId?: string; @@ -340,6 +339,7 @@ export interface Backtest { scoreMessage?: string; initialBalance: number; netPnl: number; + positionCount: number; } export interface TradingBotConfig { @@ -366,6 +366,7 @@ export interface TradingBotConfig { useForDynamicStopLoss?: boolean; isForCopyTrading?: boolean; masterBotIdentifier?: string | null; + masterBotUserId?: number | null; } export interface LightMoneyManagement { @@ -473,6 +474,7 @@ export interface Position { initiator: PositionInitiator; user: User; initiatorIdentifier: string; + recoveryAttempted?: boolean; } export enum TradeDirection { @@ -594,11 +596,6 @@ export interface PerformanceMetrics { totalPnL?: number; } -export interface KeyValuePairOfDateTimeAndDecimal { - key?: Date; - value?: number; -} - export interface DeleteBacktestsRequest { backtestIds: string[]; } @@ -619,6 +616,7 @@ export interface LightBacktestResponse { scoreMessage: string; initialBalance: number; netPnl: number; + positionCount: number; } export interface PaginatedBacktestsResponse { @@ -666,6 +664,7 @@ export interface LightBacktest { ticker?: string | null; initialBalance?: number; netPnl?: number; + positionCount?: number; } export interface RunBacktestRequest { @@ -956,6 +955,7 @@ export interface TradingBotResponse { startupTime: Date; name: string; ticker: Ticker; + masterAgentName?: string | null; } export interface PaginatedResponseOfTradingBotResponse { diff --git a/src/Managing.WebApp/src/pages/botsPage/botList.tsx b/src/Managing.WebApp/src/pages/botsPage/botList.tsx index d116c5cc..436ca60f 100644 --- a/src/Managing.WebApp/src/pages/botsPage/botList.tsx +++ b/src/Managing.WebApp/src/pages/botsPage/botList.tsx @@ -328,6 +328,22 @@ const BotList: React.FC = ({ list }) => { { Header: 'Agent', accessor: 'agentName', + Cell: ({ row }: any) => { + const bot = row.original + const hasMasterAgent = bot.masterAgentName && bot.masterAgentName !== bot.agentName + + if (hasMasterAgent) { + return ( +
+ + {bot.agentName} + +
+ ) + } + + return {bot.agentName} + }, }, { Header: 'Win Rate %', diff --git a/src/Managing.WebApp/src/pages/dashboardPage/analytics/bestAgents.tsx b/src/Managing.WebApp/src/pages/dashboardPage/analytics/bestAgents.tsx deleted file mode 100644 index 93df3411..00000000 --- a/src/Managing.WebApp/src/pages/dashboardPage/analytics/bestAgents.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import React, {useEffect, useState} from 'react' -import {GridTile, Table} from '../../../components/mollecules' -import useApiUrlStore from '../../../app/store/apiStore' -import {type AgentBalanceHistory, type BestAgentsResponse, DataClient} from '../../../generated/ManagingApi' - -// Extend the type to include agentBalances for runtime use -export interface AgentBalanceWithBalances extends AgentBalanceHistory { - agentBalances?: Array<{ - totalValue?: number - totalAccountUsdValue?: number - botsAllocationUsdValue?: number - pnL?: number - time?: string - }>; -} - -const FILTERS = [ - { label: '24H', value: '24H', days: 1 }, - { label: '3D', value: '3D', days: 3 }, - { label: '1W', value: '1W', days: 7 }, - { label: '1M', value: '1M', days: 30 }, - { label: '1Y', value: '1Y', days: 365 }, - { label: 'Total', value: 'Total', days: null }, -] - -function BestAgents({ index }: { index: number }) { - const { apiUrl } = useApiUrlStore() - const [data, setData] = useState([]) - const [isLoading, setIsLoading] = useState(true) - const [page, setPage] = useState(1) - const [pageSize, setPageSize] = useState(10) - const [totalPages, setTotalPages] = useState(1) - const [selectedFilter, setSelectedFilter] = useState('Total') - - useEffect(() => { - setIsLoading(true) - const client = new DataClient({}, apiUrl) - const now = new Date() - - // Calculate start date based on selected filter - const filterObj = FILTERS.find(f => f.value === selectedFilter) - let startDate: Date - if (filterObj?.days) { - // Use the filter's days value to calculate start date - startDate = new Date(now.getTime() - filterObj.days * 24 * 60 * 60 * 1000) - } else { - // For 'Total', fetch from a far past date (e.g., 5 years ago) - startDate = new Date(now.getFullYear() - 5, now.getMonth(), now.getDate()) - } - - client.data_GetBestAgents(startDate, now, page, pageSize).then((res: BestAgentsResponse) => { - setData(res.agents as AgentBalanceWithBalances[] ?? []) - setTotalPages(res.totalPages ?? 1) - console.log(res) - }).finally(() => setIsLoading(false)) - }, [apiUrl, page, pageSize, selectedFilter]) - - function filterBalancesByRange(agent: AgentBalanceWithBalances) { - if (!agent.agentBalances || selectedFilter === 'Total') return agent.agentBalances ?? [] - const days = FILTERS.find(f => f.value === selectedFilter)?.days - if (!days) return agent.agentBalances ?? [] - const now = new Date() - const cutoff = new Date(now.getTime() - days * 24 * 60 * 60 * 1000) - return agent.agentBalances.filter(b => b.time && new Date(b.time) >= cutoff) - } - - // Get the latest balance for each agent - const latestBalances = data.map(agent => { - const filteredBalances = filterBalancesByRange(agent) - if (filteredBalances.length > 0) { - const lastBalance = filteredBalances[filteredBalances.length - 1] - return { - agentName: agent.agentName, - originalAgent: { ...agent, agentBalances: filteredBalances }, - ...lastBalance - } - } - return { agentName: agent.agentName, originalAgent: { ...agent, agentBalances: filteredBalances } } - }) - - const columns = [ - { Header: 'Agent', accessor: 'agentName' }, - { Header: 'Total Value (USD)', accessor: 'totalValue', Cell: ({ value }: any) => value?.toLocaleString(undefined, { maximumFractionDigits: 2 }) }, - { Header: 'Account Value (USD)', accessor: 'totalAccountUsdValue', Cell: ({ value }: any) => value?.toLocaleString(undefined, { maximumFractionDigits: 2 }) }, - { Header: 'Bots Allocation (USD)', accessor: 'botsAllocationUsdValue', Cell: ({ value }: any) => value?.toLocaleString(undefined, { maximumFractionDigits: 2 }) }, - { Header: 'PnL (USD)', accessor: 'pnL', Cell: ({ value }: any) => value?.toLocaleString(undefined, { maximumFractionDigits: 2 }) }, - { Header: 'Last Update', accessor: 'time', Cell: ({ value }: any) => value ? new Date(value).toLocaleString() : '' }, - ] - - return ( -
- -
- {FILTERS.map(f => ( - - ))} -
- {isLoading ? ( - - ) : ( - - )} -
- - Page {page} of {totalPages} - - -
- - - ) -} - -export default BestAgents \ No newline at end of file diff --git a/src/Managing.WebApp/src/pages/dashboardPage/dashboard.tsx b/src/Managing.WebApp/src/pages/dashboardPage/dashboard.tsx index e9e47066..02922786 100644 --- a/src/Managing.WebApp/src/pages/dashboardPage/dashboard.tsx +++ b/src/Managing.WebApp/src/pages/dashboardPage/dashboard.tsx @@ -5,7 +5,6 @@ import type {ITabsType} from '../../global/type.tsx' import Analytics from './analytics/analytics' import Monitoring from './monitoring' -import BestAgents from './analytics/bestAgents' import AgentSearch from './agentSearch' import AgentIndex from './agentIndex' import AgentStrategy from './agentStrategy' @@ -27,24 +26,19 @@ const tabs: ITabsType = [ index: 3, label: 'Analytics', }, - { - Component: BestAgents, - index: 4, - label: 'Best Agents', - }, { Component: AgentSearch, - index: 5, + index: 4, label: 'Agent Search', }, { Component: AgentIndex, - index: 6, + index: 5, label: 'Agent Index', }, { Component: AgentStrategy, - index: 7, + index: 6, label: 'Agent Strategy', }, ] diff --git a/src/Managing.WebApp/src/pages/dashboardPage/platformSummary.tsx b/src/Managing.WebApp/src/pages/dashboardPage/platformSummary.tsx index 9111a8ac..bf299931 100644 --- a/src/Managing.WebApp/src/pages/dashboardPage/platformSummary.tsx +++ b/src/Managing.WebApp/src/pages/dashboardPage/platformSummary.tsx @@ -198,13 +198,6 @@ function PlatformSummary({index}: { index: number }) { {topStrategies?.topStrategies?.slice(0, 3).map((strategy, index) => (
-
-
- - {strategy.strategyName?.charAt(0) || 'S'} - -
-
{strategy.strategyName || '[Strategy Name]'} @@ -235,13 +228,6 @@ function PlatformSummary({index}: { index: number }) { {topStrategiesByRoi?.topStrategiesByRoi?.slice(0, 3).map((strategy, index) => (
-
-
- - {strategy.strategyName?.charAt(0) || 'S'} - -
-
{strategy.strategyName || '[Strategy Name]'} @@ -280,13 +266,6 @@ function PlatformSummary({index}: { index: number }) { {topAgentsByPnL?.slice(0, 3).map((agent, index) => (
-
-
- - {agent.agentName?.charAt(0) || 'A'} - -
-
{agent.agentName || '[Agent Name]'} @@ -485,13 +464,6 @@ function PlatformSummary({index}: { index: number }) { .map(([asset, volume]) => (
-
-
- - {asset.substring(0, 2)} - -
-
{asset}
@@ -520,13 +492,6 @@ function PlatformSummary({index}: { index: number }) { .map(([asset, count]) => (
-
-
- - {asset.substring(0, 2)} - -
-
{asset}