diff --git a/src/App.tsx b/src/App.tsx index a91d4d0..8925352 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,6 +25,7 @@ const SecuritySettings = lazy(() => import('./modules/auth/pages/SecuritySetting // Lazy load modules - Core const Dashboard = lazy(() => import('./modules/dashboard/pages/Dashboard')); const Trading = lazy(() => import('./modules/trading/pages/Trading')); +const TradingAgents = lazy(() => import('./modules/trading/pages/AgentsPage')); const MLDashboard = lazy(() => import('./modules/ml/pages/MLDashboard')); const BacktestingDashboard = lazy(() => import('./modules/backtesting/pages/BacktestingDashboard')); const Investment = lazy(() => import('./modules/investment/pages/Investment')); @@ -95,6 +96,7 @@ function App() { {/* Trading */} } /> + } /> } /> } /> } /> diff --git a/src/modules/trading/components/agents/AgentCard.tsx b/src/modules/trading/components/agents/AgentCard.tsx new file mode 100644 index 0000000..8870c22 --- /dev/null +++ b/src/modules/trading/components/agents/AgentCard.tsx @@ -0,0 +1,178 @@ +/** + * AgentCard Component + * Displays individual agent performance statistics and control buttons + */ + +import { + UserGroupIcon, + PlayIcon, + PauseIcon, + StopIcon, + ArrowTrendingUpIcon, + ArrowTrendingDownIcon, +} from '@heroicons/react/24/solid'; +import type { AgentPerformance } from '../../../../services/adminService'; + +interface AgentCardProps { + agent: AgentPerformance; + onStatusChange?: (agentId: string, newStatus: 'active' | 'paused' | 'stopped') => void; +} + +export function AgentCard({ agent, onStatusChange }: AgentCardProps) { + const getStatusColor = (status: string) => { + switch (status) { + case 'active': + return 'bg-green-900/50 text-green-400 border-green-700'; + case 'paused': + return 'bg-yellow-900/50 text-yellow-400 border-yellow-700'; + default: + return 'bg-red-900/50 text-red-400 border-red-700'; + } + }; + + const getAgentColor = (name: string) => { + const colors: Record = { + Atlas: 'bg-blue-600', + Orion: 'bg-purple-600', + Nova: 'bg-orange-600', + }; + return colors[name] || 'bg-gray-600'; + }; + + const getRiskBadge = (name: string) => { + const risks: Record = { + Atlas: { label: 'Conservative', color: 'text-green-400' }, + Orion: { label: 'Moderate', color: 'text-yellow-400' }, + Nova: { label: 'Aggressive', color: 'text-red-400' }, + }; + return risks[name] || { label: 'Unknown', color: 'text-gray-400' }; + }; + + const formatCurrency = (value: number) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + }).format(value); + }; + + const formatDate = (dateString: string) => { + if (!dateString) return 'N/A'; + return new Date(dateString).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }; + + const riskBadge = getRiskBadge(agent.name); + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

{agent.name}

+ {riskBadge.label} +
+
+
+ {agent.status.charAt(0).toUpperCase() + agent.status.slice(1)} +
+
+ + {/* Description */} + {agent.description && ( +

{agent.description}

+ )} + + {/* Main Stats */} +
+
+ Win Rate +

+ {((agent.win_rate || 0) * 100).toFixed(1)}% +

+
+
+ Total P&L +
+ {(agent.total_pnl || 0) >= 0 ? ( + + ) : ( + + )} +

= 0 ? 'text-green-400' : 'text-red-400'}`}> + {formatCurrency(agent.total_pnl || 0)} +

+
+
+
+ + {/* Secondary Stats */} +
+
+ Trades +

{agent.total_trades || 0}

+
+
+ Signals +

{agent.total_signals || 0}

+
+
+ Avg Profit +

= 0 ? 'text-green-400' : 'text-red-400'}`}> + {formatCurrency(agent.avg_profit_per_trade || 0)} +

+
+
+ + {/* Footer */} +
+ Confidence: {((agent.avg_confidence || 0) * 100).toFixed(0)}% + Last signal: {formatDate(agent.last_signal_at)} +
+ + {/* Control Buttons */} + {onStatusChange && ( +
+ {agent.status !== 'active' && ( + + )} + {agent.status === 'active' && ( + + )} + {agent.status !== 'stopped' && ( + + )} +
+ )} +
+ ); +} diff --git a/src/modules/trading/components/agents/AgentsList.tsx b/src/modules/trading/components/agents/AgentsList.tsx new file mode 100644 index 0000000..9db02ee --- /dev/null +++ b/src/modules/trading/components/agents/AgentsList.tsx @@ -0,0 +1,200 @@ +/** + * AgentsList Component + * Displays a grid of trading bots (Atlas, Orion, Nova) with status filters + */ + +import { useEffect, useState, useCallback } from 'react'; +import { useAgentsStore } from '../../../../stores/agentsStore'; +import { BotCard } from './BotCard'; +import { Loader2, RefreshCw, AlertCircle, Bot } from 'lucide-react'; +import type { BotStatus, TradingBot, TradingBotConfig } from '../../../../types/tradingAgents.types'; + +interface AgentsListProps { + statusFilter?: BotStatus; +} + +export function AgentsList({ statusFilter }: AgentsListProps) { + const { bots, isLoading, error, fetchBots, selectBot, startBot, stopBot } = useAgentsStore(); + const [filterStatus, setFilterStatus] = useState('all'); + + useEffect(() => { + fetchBots(); + }, [fetchBots]); + + // Handler for starting a bot with default config + const handleStartBot = useCallback(async (bot: TradingBot) => { + const defaultConfig: TradingBotConfig = { + initialEquity: 10000, + riskPerTrade: 2.0, + maxPositions: 3, + }; + await startBot(bot.name, defaultConfig); + }, [startBot]); + + // Handler for stopping a bot + const handleStopBot = useCallback(async (bot: TradingBot) => { + await stopBot(bot.name); + }, [stopBot]); + + // Apply status filter + const filteredBots = filterStatus === 'all' + ? bots + : bots.filter((bot) => bot.status === filterStatus); + + // Loading skeleton + if (isLoading) { + return ( +
+
+

Trading Bots

+ +
+ +
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+
+
+ ))} +
+
+ ); + } + + // Error state + if (error) { + return ( +
+
+

Trading Bots

+ +
+ +
+
+ +
+

Error Loading Bots

+

{error}

+ +
+
+
+
+ ); + } + + // Empty state + if (filteredBots.length === 0) { + return ( +
+
+

Trading Bots

+ +
+ +
+
+
+ +
+

No Bots Available

+

+ {filterStatus !== 'all' + ? `No bots with status "${filterStatus}" found.` + : 'Start configuring your trading bots to begin automated trading.'} +

+ +
+
+
+ ); + } + + // Main render + return ( +
+ {/* Header */} +
+
+

Trading Bots

+

+ {filteredBots.length} {filteredBots.length === 1 ? 'bot' : 'bots'} available +

+
+
+ {/* Status Filter */} + + + {/* Refresh Button */} + +
+
+ + {/* Bots Grid */} +
+ {filteredBots.map((bot) => ( + selectBot(bot.name)} + onStart={() => handleStartBot(bot)} + onStop={() => handleStopBot(bot)} + /> + ))} +
+
+ ); +} diff --git a/src/modules/trading/components/agents/BotCard.tsx b/src/modules/trading/components/agents/BotCard.tsx new file mode 100644 index 0000000..c37dad1 --- /dev/null +++ b/src/modules/trading/components/agents/BotCard.tsx @@ -0,0 +1,276 @@ +/** + * BotCard Component + * Displays trading bot information including status, metrics, and control actions + * Used to show individual bot cards in the trading agents dashboard + */ + +import React from 'react'; +import { + Shield, + Target, + Rocket, + TrendingUp, + TrendingDown, + Play, + Pause, + Square, + Eye, +} from 'lucide-react'; +import type { TradingBot, AgentType } from '../../../../types/tradingAgents.types'; + +/** + * Props for BotCard component + */ +interface BotCardProps { + /** Trading bot data to display */ + bot: TradingBot; + + /** Callback when user clicks to view bot details */ + onSelect?: (bot: TradingBot) => void; + + /** Callback when user starts the bot */ + onStart?: (botId: string) => void; + + /** Callback when user stops the bot */ + onStop?: (botId: string) => void; + + /** Whether actions are currently loading */ + loading?: boolean; +} + +/** + * BotCard Component + * Displays a trading bot with its current status, metrics, and action buttons + */ +export const BotCard: React.FC = ({ + bot, + onSelect, + onStart, + onStop, + loading = false, +}) => { + /** + * Get icon component based on agent type + */ + const getAgentIcon = (type: AgentType) => { + const iconProps = { className: 'w-5 h-5 text-white' }; + switch (type) { + case 'atlas': + return ; + case 'orion': + return ; + case 'nova': + return ; + default: + return ; + } + }; + + /** + * Get background color for agent icon based on type + */ + const getAgentColor = (type: AgentType): string => { + switch (type) { + case 'atlas': + return 'bg-blue-600'; + case 'orion': + return 'bg-purple-600'; + case 'nova': + return 'bg-orange-600'; + default: + return 'bg-gray-600'; + } + }; + + /** + * Get status badge styling based on bot status + */ + const getStatusStyle = (status: string) => { + switch (status) { + case 'running': + return 'bg-green-900/50 text-green-400 border-green-700'; + case 'stopped': + return 'bg-gray-900/50 text-gray-400 border-gray-700'; + case 'error': + return 'bg-red-900/50 text-red-400 border-red-700'; + case 'paused': + return 'bg-yellow-900/50 text-yellow-400 border-yellow-700'; + default: + return 'bg-gray-900/50 text-gray-400 border-gray-700'; + } + }; + + /** + * Get risk level text and color based on agent type + */ + const getRiskBadge = (type: AgentType) => { + switch (type) { + case 'atlas': + return { label: 'Conservative', color: 'text-green-400' }; + case 'orion': + return { label: 'Moderate', color: 'text-yellow-400' }; + case 'nova': + return { label: 'Aggressive', color: 'text-red-400' }; + default: + return { label: 'Unknown', color: 'text-gray-400' }; + } + }; + + /** + * Format currency values + */ + const formatCurrency = (value: number): string => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(value); + }; + + /** + * Format percentage values + */ + const formatPercentage = (value: number): string => { + return `${value.toFixed(1)}%`; + }; + + const riskBadge = getRiskBadge(bot.name); + const isProfitable = bot.todayPnl >= 0; + const winRate = bot.metrics?.win_rate ?? 0; + + return ( +
+ {/* Header Section */} +
+
+ {/* Agent Icon */} +
+ {getAgentIcon(bot.name)} +
+ + {/* Agent Name and Risk Level */} +
+

{bot.displayName}

+ {riskBadge.label} +
+
+ + {/* Status Badge */} +
+ {bot.status.charAt(0).toUpperCase() + bot.status.slice(1)} +
+
+ + {/* Description */} + {bot.description && ( +

{bot.description}

+ )} + + {/* Main Metrics Grid */} +
+ {/* P&L Card */} +
+ Today's P&L +
+ {isProfitable ? ( + + ) : ( + + )} +

+ {formatCurrency(bot.todayPnl)} +

+
+
+ + {/* Win Rate Card */} +
+ Win Rate +

{formatPercentage(winRate)}

+
+
+ + {/* Secondary Stats */} +
+ {/* Positions */} +
+ Positions +

{bot.positions}

+
+ + {/* Equity */} +
+ Equity +

{formatCurrency(bot.equity)}

+
+ + {/* Total Trades */} +
+ Trades +

{bot.metrics?.total_trades ?? 0}

+
+
+ + {/* Action Buttons */} +
+ {/* View Details Button */} + {onSelect && ( + + )} + + {/* Start/Stop Button */} + {bot.status === 'running' || bot.status === 'paused' ? ( + onStop && ( + + ) + ) : ( + onStart && ( + + ) + )} + + {/* Pause Button (only when running) */} + {bot.status === 'running' && onStop && ( + + )} +
+
+ ); +}; + +export default BotCard; diff --git a/src/modules/trading/components/agents/index.ts b/src/modules/trading/components/agents/index.ts new file mode 100644 index 0000000..4f526d7 --- /dev/null +++ b/src/modules/trading/components/agents/index.ts @@ -0,0 +1,7 @@ +/** + * Export all agent-related components + */ + +export { AgentsList } from './AgentsList'; +export { AgentCard } from './AgentCard'; +export { BotCard } from './BotCard'; diff --git a/src/modules/trading/components/index.ts b/src/modules/trading/components/index.ts index f426fa0..38e7642 100644 --- a/src/modules/trading/components/index.ts +++ b/src/modules/trading/components/index.ts @@ -66,3 +66,7 @@ export { default as PositionModifierDialog } from './PositionModifierDialog'; export { default as RiskBasedPositionSizer } from './RiskBasedPositionSizer'; export { default as TradeAlertsNotificationCenter } from './TradeAlertsNotificationCenter'; export type { TradeAlert, AlertType, AlertPriority } from './TradeAlertsNotificationCenter'; + +// Trading Agents Components (OQI-007) +export { AgentsList } from './agents/AgentsList'; +export { BotCard } from './agents/BotCard'; diff --git a/src/modules/trading/pages/AgentsPage.tsx b/src/modules/trading/pages/AgentsPage.tsx new file mode 100644 index 0000000..de19660 --- /dev/null +++ b/src/modules/trading/pages/AgentsPage.tsx @@ -0,0 +1,23 @@ +/** + * Trading Agents Page + * Manage and monitor trading agents within the trading module + */ + +import { AgentsList } from '../components/agents/AgentsList'; + +export default function AgentsPage() { + return ( +
+ {/* Header */} +
+

Trading Agents

+

+ Gestiona tus bots de trading automatizado: Atlas, Orion y Nova +

+
+ + {/* Main Content */} + +
+ ); +} diff --git a/src/services/agents.service.ts b/src/services/agents.service.ts new file mode 100644 index 0000000..3897010 --- /dev/null +++ b/src/services/agents.service.ts @@ -0,0 +1,242 @@ +/** + * Trading Agents Service + * Client for connecting to the Trading Agents API (Atlas, Orion, Nova) + */ + +import type { + AgentType, + AgentStatusResponse, + AgentMetrics, + AgentPosition, + AgentTrade, + TradingBotConfig, + SignalInput, + TradingAgentsHealthResponse, + AgentSummary, + CloseAllPositionsResponse, + SignalResponse, + BroadcastSignalResponse, +} from '../types/tradingAgents.types'; + +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3080'; + +// ============================================================================ +// Helper Function +// ============================================================================ + +/** + * Generic fetch wrapper with error handling + */ +async function fetchAPI( + url: string, + options?: RequestInit +): Promise { + const response = await fetch(url, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + ...options, + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ + error: { message: 'Failed to fetch data', code: 'FETCH_ERROR' }, + })); + throw new Error(error.error?.message || 'Request failed'); + } + + const data = await response.json(); + return data.data || data; +} + +// ============================================================================ +// Health & Status +// ============================================================================ + +/** + * Get Trading Agents service health + */ +export async function getAgentsHealth(): Promise { + return fetchAPI(`${API_URL}/api/v1/agents/health`); +} + +/** + * Check if Trading Agents service is available + */ +export async function checkAgentsConnection(): Promise<{ connected: boolean }> { + return fetchAPI<{ connected: boolean }>(`${API_URL}/api/v1/agents/connection`); +} + +/** + * Get summary of all trading agents + */ +export async function getAllAgentsSummary(): Promise { + return fetchAPI(`${API_URL}/api/v1/agents/summary`); +} + +// ============================================================================ +// Agent Lifecycle +// ============================================================================ + +/** + * Start a trading agent + */ +export async function startAgent( + agentType: AgentType, + config: TradingBotConfig +): Promise { + return fetchAPI( + `${API_URL}/api/v1/agents/${agentType}/start`, + { + method: 'POST', + body: JSON.stringify(config), + } + ); +} + +/** + * Stop a trading agent + */ +export async function stopAgent(agentType: AgentType): Promise { + return fetchAPI( + `${API_URL}/api/v1/agents/${agentType}/stop`, + { + method: 'POST', + } + ); +} + +/** + * Pause a trading agent + */ +export async function pauseAgent(agentType: AgentType): Promise { + return fetchAPI( + `${API_URL}/api/v1/agents/${agentType}/pause`, + { + method: 'POST', + } + ); +} + +/** + * Resume a paused trading agent + */ +export async function resumeAgent(agentType: AgentType): Promise { + return fetchAPI( + `${API_URL}/api/v1/agents/${agentType}/resume`, + { + method: 'POST', + } + ); +} + +// ============================================================================ +// Agent Status & Metrics +// ============================================================================ + +/** + * Get current status of a specific agent + */ +export async function getAgentStatus(agentType: AgentType): Promise { + return fetchAPI(`${API_URL}/api/v1/agents/${agentType}/status`); +} + +/** + * Get performance metrics of a specific agent + */ +export async function getAgentMetrics(agentType: AgentType): Promise { + return fetchAPI(`${API_URL}/api/v1/agents/${agentType}/metrics`); +} + +// ============================================================================ +// Positions & Trades +// ============================================================================ + +/** + * Get open positions for a specific agent + */ +export async function getAgentPositions(agentType: AgentType): Promise { + return fetchAPI(`${API_URL}/api/v1/agents/${agentType}/positions`); +} + +/** + * Get trade history for a specific agent + */ +export async function getAgentTrades( + agentType: AgentType, + options?: { limit?: number; offset?: number; symbol?: string } +): Promise { + const params = new URLSearchParams(); + if (options?.limit) params.append('limit', options.limit.toString()); + if (options?.offset) params.append('offset', options.offset.toString()); + if (options?.symbol) params.append('symbol', options.symbol); + + const url = `${API_URL}/api/v1/agents/${agentType}/trades${params.toString() ? `?${params.toString()}` : ''}`; + return fetchAPI(url); +} + +/** + * Close a specific position + */ +export async function closePosition( + agentType: AgentType, + positionId: string +): Promise { + return fetchAPI( + `${API_URL}/api/v1/agents/${agentType}/positions/${positionId}/close`, + { + method: 'POST', + } + ); +} + +/** + * Close all positions for a specific agent + */ +export async function closeAllPositions( + agentType: AgentType +): Promise { + return fetchAPI( + `${API_URL}/api/v1/agents/${agentType}/positions/close-all`, + { + method: 'POST', + } + ); +} + +// ============================================================================ +// Signals +// ============================================================================ + +/** + * Send a trading signal to a specific agent + */ +export async function sendSignal( + agentType: AgentType, + signal: SignalInput +): Promise { + return fetchAPI( + `${API_URL}/api/v1/agents/${agentType}/signal`, + { + method: 'POST', + body: JSON.stringify(signal), + } + ); +} + +/** + * Broadcast a signal to all running agents + */ +export async function broadcastSignal( + signal: SignalInput +): Promise { + return fetchAPI( + `${API_URL}/api/v1/signals/broadcast`, + { + method: 'POST', + body: JSON.stringify(signal), + } + ); +} diff --git a/src/stores/agentsStore.ts b/src/stores/agentsStore.ts new file mode 100644 index 0000000..bddb8b4 --- /dev/null +++ b/src/stores/agentsStore.ts @@ -0,0 +1,497 @@ +/** + * Trading Agents Store + * Zustand store for managing Trading Agents (Atlas, Orion, Nova) + */ + +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import type { + AgentType, + TradingBot, + BotStatus, + TradingBotConfig, + AgentPosition, + AgentTrade, + SignalInput, + AgentStatusResponse, + AgentMetrics, +} from '../types/tradingAgents.types'; +import { + getAllAgentsSummary, + getAgentStatus, + getAgentMetrics, + startAgent, + stopAgent, + pauseAgent, + resumeAgent, + getAgentPositions, + getAgentTrades, + closePosition, + closeAllPositions, + sendSignal, + getAgentsHealth, +} from '../services/agents.service'; +import { + getAgentDisplayName, + getAgentDescription, + statusToDisplayStatus, +} from '../types/tradingAgents.types'; + +// ============================================================================ +// State Interface +// ============================================================================ + +interface AgentsState { + // Bots data + bots: TradingBot[]; + selectedBot: TradingBot | null; + + // Positions & Trades + positions: AgentPosition[]; + trades: AgentTrade[]; + + // Loading states + isLoading: boolean; + loadingPositions: boolean; + loadingTrades: boolean; + loadingMetrics: boolean; + + // Error state + error: string | null; + + // Service status + serviceAvailable: boolean; + + // Actions - Bot Management + fetchBots: () => Promise; + selectBot: (botName: AgentType) => void; + refreshBot: (botName: AgentType) => Promise; + startBot: (botName: AgentType, config: TradingBotConfig) => Promise; + stopBot: (botName: AgentType) => Promise; + pauseBot: (botName: AgentType) => Promise; + resumeBot: (botName: AgentType) => Promise; + + // Actions - Metrics + updateBotMetrics: (botName: AgentType, metrics: Partial) => void; + fetchBotMetrics: (botName: AgentType) => Promise; + + // Actions - Positions & Trades + fetchPositions: (botName: AgentType) => Promise; + fetchTrades: (botName: AgentType, options?: { limit?: number; offset?: number; symbol?: string }) => Promise; + closePosition: (botName: AgentType, positionId: string) => Promise; + closeAllPositions: (botName: AgentType) => Promise; + + // Actions - Signals + sendSignal: (botName: AgentType, signal: SignalInput) => Promise; + + // Utility actions + clearError: () => void; + reset: () => void; +} + +// ============================================================================ +// Initial State +// ============================================================================ + +const AGENT_TYPES: AgentType[] = ['atlas', 'orion', 'nova']; + +const initialBots: TradingBot[] = AGENT_TYPES.map((name) => ({ + id: name, + name, + displayName: getAgentDisplayName(name), + description: getAgentDescription(name), + status: 'stopped' as BotStatus, + equity: 0, + positions: 0, + todayPnl: 0, + metrics: undefined, +})); + +const initialState = { + bots: initialBots, + selectedBot: null, + positions: [], + trades: [], + isLoading: false, + loadingPositions: false, + loadingTrades: false, + loadingMetrics: false, + error: null, + serviceAvailable: false, +}; + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Convert backend status response to frontend TradingBot format + */ +function mapStatusToBotData(status: AgentStatusResponse, existingBot?: TradingBot): TradingBot { + return { + id: status.agent_name, + name: status.agent_name, + displayName: existingBot?.displayName || getAgentDisplayName(status.agent_name), + description: existingBot?.description || getAgentDescription(status.agent_name), + status: statusToDisplayStatus(status.status), + equity: status.equity, + positions: status.positions, + todayPnl: status.today_pnl, + metrics: existingBot?.metrics, + }; +} + +/** + * Convert backend metrics response to frontend AgentMetrics format + */ +function mapMetricsResponse(metrics: AgentMetrics): AgentMetrics { + return { + total_trades: metrics.total_trades, + winning_trades: metrics.winning_trades, + losing_trades: metrics.losing_trades, + win_rate: metrics.win_rate, + total_profit: metrics.total_profit, + total_loss: metrics.total_loss, + net_pnl: metrics.net_pnl, + max_drawdown: metrics.max_drawdown, + current_drawdown: metrics.current_drawdown, + sharpe_ratio: metrics.sharpe_ratio, + sortino_ratio: metrics.sortino_ratio, + profit_factor: metrics.profit_factor, + }; +} + +// ============================================================================ +// Store +// ============================================================================ + +export const useAgentsStore = create()( + devtools( + (set, get) => ({ + ...initialState, + + // ====================================================================== + // Bot Management Actions + // ====================================================================== + + fetchBots: async () => { + set({ isLoading: true, error: null }); + + try { + // Check service health first + const health = await getAgentsHealth(); + const serviceAvailable = health.status === 'healthy'; + + if (!serviceAvailable) { + set({ + serviceAvailable: false, + isLoading: false, + bots: initialBots, + error: 'Trading Agents service is not available', + }); + return; + } + + // Fetch all agents summary + const summaries = await getAllAgentsSummary(); + + // Map summaries to bots + const bots = AGENT_TYPES.map((agentType) => { + const summary = summaries.find((s) => s.name === agentType); + const existingBot = get().bots.find((b) => b.name === agentType); + + if (!summary) { + return existingBot || initialBots.find((b) => b.name === agentType)!; + } + + return { + id: agentType, + name: agentType, + displayName: getAgentDisplayName(agentType), + description: getAgentDescription(agentType), + status: statusToDisplayStatus(summary.status), + equity: summary.equity, + positions: summary.positions, + todayPnl: summary.todayPnl, + metrics: existingBot?.metrics, + }; + }); + + set({ + bots, + serviceAvailable: true, + isLoading: false, + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to fetch bots'; + set({ + error: message, + isLoading: false, + serviceAvailable: false, + }); + console.error('Error fetching bots:', error); + } + }, + + selectBot: (botName: AgentType) => { + const bot = get().bots.find((b) => b.name === botName); + if (bot) { + set({ selectedBot: bot, positions: [], trades: [] }); + // Auto-fetch positions and trades for selected bot + get().fetchPositions(botName); + get().fetchTrades(botName); + get().fetchBotMetrics(botName); + } + }, + + refreshBot: async (botName: AgentType) => { + try { + const [status, metrics] = await Promise.all([ + getAgentStatus(botName), + getAgentMetrics(botName).catch(() => null), + ]); + + const existingBot = get().bots.find((b) => b.name === botName); + const updatedBot = mapStatusToBotData(status, existingBot); + + if (metrics) { + updatedBot.metrics = mapMetricsResponse(metrics); + } + + set((state) => ({ + bots: state.bots.map((b) => (b.name === botName ? updatedBot : b)), + selectedBot: state.selectedBot?.name === botName ? updatedBot : state.selectedBot, + })); + } catch (error) { + console.error(`Error refreshing bot ${botName}:`, error); + } + }, + + startBot: async (botName: AgentType, config: TradingBotConfig) => { + set({ error: null }); + + try { + const status = await startAgent(botName, config); + const existingBot = get().bots.find((b) => b.name === botName); + const updatedBot = mapStatusToBotData(status, existingBot); + + set((state) => ({ + bots: state.bots.map((b) => (b.name === botName ? updatedBot : b)), + selectedBot: state.selectedBot?.name === botName ? updatedBot : state.selectedBot, + })); + + // Refresh full data after starting + await get().refreshBot(botName); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to start bot'; + set({ error: message }); + throw error; + } + }, + + stopBot: async (botName: AgentType) => { + set({ error: null }); + + try { + const status = await stopAgent(botName); + const existingBot = get().bots.find((b) => b.name === botName); + const updatedBot = mapStatusToBotData(status, existingBot); + + set((state) => ({ + bots: state.bots.map((b) => (b.name === botName ? updatedBot : b)), + selectedBot: state.selectedBot?.name === botName ? updatedBot : state.selectedBot, + positions: state.selectedBot?.name === botName ? [] : state.positions, + })); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to stop bot'; + set({ error: message }); + throw error; + } + }, + + pauseBot: async (botName: AgentType) => { + set({ error: null }); + + try { + const status = await pauseAgent(botName); + const existingBot = get().bots.find((b) => b.name === botName); + const updatedBot = mapStatusToBotData(status, existingBot); + + set((state) => ({ + bots: state.bots.map((b) => (b.name === botName ? updatedBot : b)), + selectedBot: state.selectedBot?.name === botName ? updatedBot : state.selectedBot, + })); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to pause bot'; + set({ error: message }); + throw error; + } + }, + + resumeBot: async (botName: AgentType) => { + set({ error: null }); + + try { + const status = await resumeAgent(botName); + const existingBot = get().bots.find((b) => b.name === botName); + const updatedBot = mapStatusToBotData(status, existingBot); + + set((state) => ({ + bots: state.bots.map((b) => (b.name === botName ? updatedBot : b)), + selectedBot: state.selectedBot?.name === botName ? updatedBot : state.selectedBot, + })); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to resume bot'; + set({ error: message }); + throw error; + } + }, + + // ====================================================================== + // Metrics Actions + // ====================================================================== + + updateBotMetrics: (botName: AgentType, metricsUpdate: Partial) => { + set((state) => ({ + bots: state.bots.map((b) => + b.name === botName + ? { ...b, metrics: { ...b.metrics, ...metricsUpdate } as AgentMetrics } + : b + ), + selectedBot: + state.selectedBot?.name === botName + ? { ...state.selectedBot, metrics: { ...state.selectedBot.metrics, ...metricsUpdate } as AgentMetrics } + : state.selectedBot, + })); + }, + + fetchBotMetrics: async (botName: AgentType) => { + set({ loadingMetrics: true }); + + try { + const metrics = await getAgentMetrics(botName); + const mappedMetrics = mapMetricsResponse(metrics); + + get().updateBotMetrics(botName, mappedMetrics); + set({ loadingMetrics: false }); + } catch (error) { + console.error(`Error fetching metrics for ${botName}:`, error); + set({ loadingMetrics: false }); + } + }, + + // ====================================================================== + // Positions & Trades Actions + // ====================================================================== + + fetchPositions: async (botName: AgentType) => { + set({ loadingPositions: true, error: null }); + + try { + const positions = await getAgentPositions(botName); + set({ positions, loadingPositions: false }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to fetch positions'; + set({ error: message, loadingPositions: false, positions: [] }); + console.error('Error fetching positions:', error); + } + }, + + fetchTrades: async (botName: AgentType, options?) => { + set({ loadingTrades: true, error: null }); + + try { + const trades = await getAgentTrades(botName, options); + set({ trades, loadingTrades: false }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to fetch trades'; + set({ error: message, loadingTrades: false, trades: [] }); + console.error('Error fetching trades:', error); + } + }, + + closePosition: async (botName: AgentType, positionId: string) => { + set({ error: null }); + + try { + await closePosition(botName, positionId); + // Refresh positions and bot data after closing + await Promise.all([ + get().fetchPositions(botName), + get().refreshBot(botName), + ]); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to close position'; + set({ error: message }); + throw error; + } + }, + + closeAllPositions: async (botName: AgentType) => { + set({ error: null }); + + try { + await closeAllPositions(botName); + // Refresh positions and bot data after closing all + await Promise.all([ + get().fetchPositions(botName), + get().refreshBot(botName), + ]); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to close all positions'; + set({ error: message }); + throw error; + } + }, + + // ====================================================================== + // Signal Actions + // ====================================================================== + + sendSignal: async (botName: AgentType, signal: SignalInput) => { + set({ error: null }); + + try { + await sendSignal(botName, signal); + // Signal sent successfully - no state update needed + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to send signal'; + set({ error: message }); + throw error; + } + }, + + // ====================================================================== + // Utility Actions + // ====================================================================== + + clearError: () => { + set({ error: null }); + }, + + reset: () => { + set(initialState); + }, + }), + { + name: 'agents-store', + } + ) +); + +// ============================================================================ +// Selectors (for performance optimization) +// ============================================================================ + +export const useBots = () => useAgentsStore((state) => state.bots); +export const useSelectedBot = () => useAgentsStore((state) => state.selectedBot); +export const useBotPositions = () => useAgentsStore((state) => state.positions); +export const useBotTrades = () => useAgentsStore((state) => state.trades); +export const useAgentsLoading = () => useAgentsStore((state) => state.isLoading); +export const useAgentsError = () => useAgentsStore((state) => state.error); +export const useServiceAvailable = () => useAgentsStore((state) => state.serviceAvailable); +export const useLoadingPositions = () => useAgentsStore((state) => state.loadingPositions); +export const useLoadingTrades = () => useAgentsStore((state) => state.loadingTrades); +export const useLoadingMetrics = () => useAgentsStore((state) => state.loadingMetrics); + +export default useAgentsStore; diff --git a/src/types/index.ts b/src/types/index.ts index c4d3f15..370db9b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -141,6 +141,9 @@ export { // Chat types - Messages, Conversations export * from './chat.types'; +// Trading Agents types - Atlas, Orion, Nova agents +export * from './tradingAgents.types'; + // Export a unified ApiResponse type (using the most complete version from payment.types) // Also export module-specific aliases for backward compatibility export { type ApiResponse } from './payment.types'; diff --git a/src/types/tradingAgents.types.ts b/src/types/tradingAgents.types.ts new file mode 100644 index 0000000..3858976 --- /dev/null +++ b/src/types/tradingAgents.types.ts @@ -0,0 +1,582 @@ +/** + * Trading Agents Types + * Type definitions for Trading Agents (Atlas, Orion, Nova) integration + * Aligned with backend trading-agents.client.ts and agents.service.ts + * Updated: 2026-01-27 + */ + +// ============================================================================ +// Agent Type Definitions +// ============================================================================ + +/** + * Supported trading agent types + * - atlas: Conservative trading strategy (long-term, low risk) + * - orion: Balanced/moderate trading strategy + * - nova: Aggressive trading strategy (high risk/reward) + */ +export type AgentType = 'atlas' | 'orion' | 'nova'; + +/** + * Agent lifecycle status states + */ +export type AgentStatus = 'stopped' | 'starting' | 'running' | 'paused' | 'error'; + +/** + * Display-friendly bot status for UI + */ +export type BotStatus = 'idle' | 'running' | 'paused' | 'stopped' | 'error' | 'unavailable'; + +/** + * Trade side direction + */ +export type TradeSide = 'long' | 'short'; + +/** + * Signal action direction + */ +export type SignalAction = 'buy' | 'sell' | 'hold'; + +// ============================================================================ +// Agent Configuration +// ============================================================================ + +/** + * Configuration for starting a trading agent + */ +export interface TradingBotConfig { + /** Initial capital/equity for the agent */ + initialEquity: number; + + /** List of trading symbols (e.g., 'BTCUSDT', 'EURUSD') */ + symbols?: string[]; + + /** Risk per trade as percentage (e.g., 2.0 = 2%) */ + riskPerTrade?: number; + + /** Maximum number of concurrent positions */ + maxPositions?: number; +} + +// ============================================================================ +// Agent Status & Metrics +// ============================================================================ + +/** + * Current status of a trading agent + */ +export interface AgentStatusResponse { + /** Agent identifier (atlas, orion, nova) */ + agent_name: AgentType; + + /** Current status of the agent */ + status: AgentStatus; + + /** Current equity/account balance */ + equity: number; + + /** Number of open positions */ + positions: number; + + /** Profit/Loss for the current day */ + today_pnl: number; + + /** Agent uptime in seconds */ + uptime_seconds: number; + + /** Timestamp of the last trade executed */ + last_trade_at?: string; + + /** Error message if status is 'error' */ + error_message?: string; +} + +/** + * Performance metrics for a trading agent + */ +export interface AgentMetrics { + /** Total number of trades executed */ + total_trades: number; + + /** Number of profitable trades */ + winning_trades: number; + + /** Number of losing trades */ + losing_trades: number; + + /** Win rate as percentage (0-100) */ + win_rate: number; + + /** Total profit from winning trades */ + total_profit: number; + + /** Total loss from losing trades */ + total_loss: number; + + /** Net profit/loss */ + net_pnl: number; + + /** Maximum drawdown percentage */ + max_drawdown: number; + + /** Current drawdown percentage */ + current_drawdown: number; + + /** Sharpe ratio (risk-adjusted return) */ + sharpe_ratio?: number; + + /** Sortino ratio (downside risk-adjusted return) */ + sortino_ratio?: number; + + /** Profit factor (total profit / total loss) */ + profit_factor?: number; +} + +/** + * Frontend representation of a trading bot + * Combines status and basic metrics for dashboard display + */ +export interface TradingBot { + /** Unique identifier for the bot */ + id: string; + + /** Agent type name */ + name: AgentType; + + /** Display name for UI */ + displayName: string; + + /** Agent description/strategy type */ + description: string; + + /** Current bot status */ + status: BotStatus; + + /** Current account equity */ + equity: number; + + /** Number of open positions */ + positions: number; + + /** P&L for today */ + todayPnl: number; + + /** Extended metrics (optional) */ + metrics?: AgentMetrics; +} + +// ============================================================================ +// Positions +// ============================================================================ + +/** + * Open trading position for an agent + */ +export interface AgentPosition { + /** Position unique identifier */ + id: string; + + /** Trading symbol (e.g., 'BTCUSDT') */ + symbol: string; + + /** Position direction (long/short) */ + side: TradeSide; + + /** Position size/quantity */ + quantity: number; + + /** Entry price */ + entry_price: number; + + /** Current market price */ + current_price: number; + + /** Unrealized P&L in currency */ + unrealized_pnl: number; + + /** Unrealized P&L percentage */ + unrealized_pnl_percent: number; + + /** Stop loss price */ + stop_loss?: number; + + /** Take profit price */ + take_profit?: number; + + /** Position open timestamp (ISO 8601) */ + opened_at: string; +} + +/** + * Frontend-friendly position display + */ +export interface BotPosition extends AgentPosition { + /** Display-friendly P&L string */ + pnlDisplay?: string; + + /** Position status color (for UI styling) */ + statusColor?: 'green' | 'red' | 'neutral'; +} + +// ============================================================================ +// Trades & History +// ============================================================================ + +/** + * Closed trade from agent history + */ +export interface AgentTrade { + /** Trade unique identifier */ + id: string; + + /** Trading symbol (e.g., 'BTCUSDT') */ + symbol: string; + + /** Trade direction (long/short) */ + side: TradeSide; + + /** Quantity traded */ + quantity: number; + + /** Entry price */ + entry_price: number; + + /** Exit price */ + exit_price: number; + + /** Realized P&L in currency */ + pnl: number; + + /** Realized P&L percentage */ + pnl_percent: number; + + /** Trade entry timestamp (ISO 8601) */ + opened_at: string; + + /** Trade exit timestamp (ISO 8601) */ + closed_at: string; + + /** Reason for trade closure */ + close_reason: string; +} + +/** + * Frontend-friendly trade display + */ +export interface BotTrade extends AgentTrade { + /** Display-friendly profit/loss indicator */ + profitDisplay?: string; + + /** Trade result status for UI */ + result?: 'win' | 'loss'; +} + +// ============================================================================ +// Signals +// ============================================================================ + +/** + * Trading signal input for agents + */ +export interface SignalInput { + /** Trading symbol to apply signal to */ + symbol: string; + + /** Signal action (buy/sell/hold) */ + action: SignalAction; + + /** Confidence level (0-1) */ + confidence: number; + + /** Suggested entry price */ + price: number; + + /** Stop loss level */ + stop_loss?: number; + + /** Take profit level */ + take_profit?: number; + + /** Additional metadata */ + metadata?: Record; +} + +// ============================================================================ +// Health & System Status +// ============================================================================ + +/** + * Health status of Trading Agents service + */ +export interface TradingAgentsHealthResponse { + /** Overall service health status */ + status: 'healthy' | 'unhealthy'; + + /** Service version */ + version: string; + + /** Number of agents currently running */ + agents_running: number; + + /** Service uptime in seconds */ + uptime_seconds: number; +} + +// ============================================================================ +// Request/Response Types +// ============================================================================ + +/** + * Request to start a trading agent + */ +export interface StartAgentRequest { + /** Agent type to start */ + agentType: AgentType; + + /** Initial capital */ + initialEquity: number; + + /** Optional trading symbols */ + symbols?: string[]; + + /** Risk per trade percentage */ + riskPerTrade?: number; + + /** Max concurrent positions */ + maxPositions?: number; +} + +/** + * Summary of all trading agents + */ +export interface AgentSummary { + /** Agent type name */ + name: AgentType; + + /** Current agent status */ + status: AgentStatus; + + /** Current equity */ + equity: number; + + /** Open positions count */ + positions: number; + + /** Today's P&L */ + todayPnl: number; + + /** Win rate (optional, when available) */ + winRate?: number; + + /** Total trades executed (optional, when available) */ + totalTrades?: number; +} + +/** + * Response for position closure operation + */ +export interface ClosePositionResponse { + /** Whether position was successfully closed */ + closed: boolean; + + /** Trade result from position closure */ + trade?: AgentTrade; +} + +/** + * Response for closing all positions + */ +export interface CloseAllPositionsResponse { + /** Number of positions closed */ + closed: number; +} + +/** + * Response for signal operations + */ +export interface SignalResponse { + /** Whether signal was received and processed */ + received: boolean; +} + +/** + * Response for broadcast signal + */ +export interface BroadcastSignalResponse { + /** Number of agents notified */ + agents_notified: number; +} + +// ============================================================================ +// UI/Display Types +// ============================================================================ + +/** + * Chart data for bot performance visualization + */ +export interface BotEquityChartData { + /** Timestamp */ + time: string; + + /** Equity value at this point */ + equity: number; + + /** P&L at this point */ + pnl: number; +} + +/** + * Comparison data for multiple agents + */ +export interface BotComparison { + /** Agent type */ + agent: AgentType; + + /** Current equity */ + equity: number; + + /** Total return percentage */ + totalReturn: number; + + /** Win rate */ + winRate: number; + + /** Sharpe ratio */ + sharpeRatio: number; + + /** Status */ + status: BotStatus; +} + +/** + * Filter options for agent trades history + */ +export interface TradeFilterOptions { + /** Filter by symbol */ + symbol?: string; + + /** Filter by trade side */ + side?: TradeSide; + + /** Filter by result */ + result?: 'win' | 'loss'; + + /** Start date (ISO 8601) */ + startDate?: string; + + /** End date (ISO 8601) */ + endDate?: string; + + /** Number of results to return */ + limit?: number; + + /** Offset for pagination */ + offset?: number; +} + +// ============================================================================ +// Enums for common values +// ============================================================================ + +/** + * Agent type enum for dropdown/selection + */ +export enum AgentTypeEnum { + ATLAS = 'atlas', + ORION = 'orion', + NOVA = 'nova', +} + +/** + * Agent status enum + */ +export enum AgentStatusEnum { + STOPPED = 'stopped', + STARTING = 'starting', + RUNNING = 'running', + PAUSED = 'paused', + ERROR = 'error', +} + +/** + * Bot display status enum + */ +export enum BotStatusEnum { + IDLE = 'idle', + RUNNING = 'running', + PAUSED = 'paused', + STOPPED = 'stopped', + ERROR = 'error', + UNAVAILABLE = 'unavailable', +} + +/** + * Trade side enum + */ +export enum TradeSideEnum { + LONG = 'long', + SHORT = 'short', +} + +/** + * Signal action enum + */ +export enum SignalActionEnum { + BUY = 'buy', + SELL = 'sell', + HOLD = 'hold', +} + +// ============================================================================ +// Mapping utilities (for use in components) +// ============================================================================ + +/** + * Map internal agent status to user-friendly status + */ +export const statusToDisplayStatus = (status: AgentStatus): BotStatus => { + switch (status) { + case 'running': + return 'running'; + case 'paused': + return 'paused'; + case 'stopped': + return 'stopped'; + case 'error': + return 'error'; + default: + return 'unavailable'; + } +}; + +/** + * Get friendly display name for agent type + */ +export const getAgentDisplayName = (agentType: AgentType): string => { + switch (agentType) { + case 'atlas': + return 'Atlas (Conservative)'; + case 'orion': + return 'Orion (Balanced)'; + case 'nova': + return 'Nova (Aggressive)'; + default: + return agentType; + } +}; + +/** + * Get description for agent type + */ +export const getAgentDescription = (agentType: AgentType): string => { + switch (agentType) { + case 'atlas': + return 'Conservative long-term trading strategy with low risk'; + case 'orion': + return 'Balanced trading strategy with moderate risk/reward'; + case 'nova': + return 'Aggressive trading strategy with high risk/reward potential'; + default: + return 'Trading agent'; + } +};