feat: Add Trading Agents UI components (GAP-P0-003)
- Add tradingAgents.types.ts with complete type definitions - Add agents.service.ts for Trading Agents API integration - Add agentsStore.ts (Zustand) for state management - Add BotCard.tsx, AgentCard.tsx, AgentsList.tsx components - Add AgentsPage.tsx for /trading/agents route - Fix TypeScript errors in AgentsList.tsx: - Add handlers for startBot with default config - Use correct AgentType for selectBot Total: 8 new files, ~3,000 LOC Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0f20468381
commit
5779c9a5cf
@ -25,6 +25,7 @@ const SecuritySettings = lazy(() => import('./modules/auth/pages/SecuritySetting
|
|||||||
// Lazy load modules - Core
|
// Lazy load modules - Core
|
||||||
const Dashboard = lazy(() => import('./modules/dashboard/pages/Dashboard'));
|
const Dashboard = lazy(() => import('./modules/dashboard/pages/Dashboard'));
|
||||||
const Trading = lazy(() => import('./modules/trading/pages/Trading'));
|
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 MLDashboard = lazy(() => import('./modules/ml/pages/MLDashboard'));
|
||||||
const BacktestingDashboard = lazy(() => import('./modules/backtesting/pages/BacktestingDashboard'));
|
const BacktestingDashboard = lazy(() => import('./modules/backtesting/pages/BacktestingDashboard'));
|
||||||
const Investment = lazy(() => import('./modules/investment/pages/Investment'));
|
const Investment = lazy(() => import('./modules/investment/pages/Investment'));
|
||||||
@ -95,6 +96,7 @@ function App() {
|
|||||||
|
|
||||||
{/* Trading */}
|
{/* Trading */}
|
||||||
<Route path="/trading" element={<Trading />} />
|
<Route path="/trading" element={<Trading />} />
|
||||||
|
<Route path="/trading/agents" element={<TradingAgents />} />
|
||||||
<Route path="/ml-dashboard" element={<MLDashboard />} />
|
<Route path="/ml-dashboard" element={<MLDashboard />} />
|
||||||
<Route path="/backtesting" element={<BacktestingDashboard />} />
|
<Route path="/backtesting" element={<BacktestingDashboard />} />
|
||||||
<Route path="/investment" element={<Investment />} />
|
<Route path="/investment" element={<Investment />} />
|
||||||
|
|||||||
178
src/modules/trading/components/agents/AgentCard.tsx
Normal file
178
src/modules/trading/components/agents/AgentCard.tsx
Normal file
@ -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<string, string> = {
|
||||||
|
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<string, { label: string; color: string }> = {
|
||||||
|
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 (
|
||||||
|
<div className="bg-gray-900 rounded-lg p-4 border border-gray-700 hover:border-gray-600 transition-colors">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`p-2 rounded-lg ${getAgentColor(agent.name)}`}>
|
||||||
|
<UserGroupIcon className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-white font-bold">{agent.name}</h3>
|
||||||
|
<span className={`text-xs ${riskBadge.color}`}>{riskBadge.label}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`px-2.5 py-1 rounded-full text-xs font-semibold border ${getStatusColor(agent.status)}`}>
|
||||||
|
{agent.status.charAt(0).toUpperCase() + agent.status.slice(1)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
{agent.description && (
|
||||||
|
<p className="text-gray-400 text-xs mb-4 line-clamp-2">{agent.description}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main Stats */}
|
||||||
|
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||||
|
<div className="bg-gray-800 rounded-lg p-3">
|
||||||
|
<span className="text-gray-500 text-xs">Win Rate</span>
|
||||||
|
<p className="text-xl font-bold text-green-400">
|
||||||
|
{((agent.win_rate || 0) * 100).toFixed(1)}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-800 rounded-lg p-3">
|
||||||
|
<span className="text-gray-500 text-xs">Total P&L</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{(agent.total_pnl || 0) >= 0 ? (
|
||||||
|
<ArrowTrendingUpIcon className="w-4 h-4 text-green-400" />
|
||||||
|
) : (
|
||||||
|
<ArrowTrendingDownIcon className="w-4 h-4 text-red-400" />
|
||||||
|
)}
|
||||||
|
<p className={`text-xl font-bold ${(agent.total_pnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{formatCurrency(agent.total_pnl || 0)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Secondary Stats */}
|
||||||
|
<div className="grid grid-cols-3 gap-2 mb-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="text-gray-500 text-xs">Trades</span>
|
||||||
|
<p className="text-white font-semibold">{agent.total_trades || 0}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="text-gray-500 text-xs">Signals</span>
|
||||||
|
<p className="text-white font-semibold">{agent.total_signals || 0}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="text-gray-500 text-xs">Avg Profit</span>
|
||||||
|
<p className={`font-semibold ${(agent.avg_profit_per_trade || 0) >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{formatCurrency(agent.avg_profit_per_trade || 0)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="flex items-center justify-between text-xs text-gray-500 mb-3 border-t border-gray-700 pt-3">
|
||||||
|
<span>Confidence: {((agent.avg_confidence || 0) * 100).toFixed(0)}%</span>
|
||||||
|
<span>Last signal: {formatDate(agent.last_signal_at)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Control Buttons */}
|
||||||
|
{onStatusChange && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{agent.status !== 'active' && (
|
||||||
|
<button
|
||||||
|
onClick={() => onStatusChange(agent.agent_id, 'active')}
|
||||||
|
className="flex-1 flex items-center justify-center gap-1 py-2 bg-green-700 hover:bg-green-600 text-white rounded text-xs font-semibold transition-colors"
|
||||||
|
title="Start agent"
|
||||||
|
>
|
||||||
|
<PlayIcon className="w-3 h-3" />
|
||||||
|
Start
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{agent.status === 'active' && (
|
||||||
|
<button
|
||||||
|
onClick={() => onStatusChange(agent.agent_id, 'paused')}
|
||||||
|
className="flex-1 flex items-center justify-center gap-1 py-2 bg-yellow-700 hover:bg-yellow-600 text-white rounded text-xs font-semibold transition-colors"
|
||||||
|
title="Pause agent"
|
||||||
|
>
|
||||||
|
<PauseIcon className="w-3 h-3" />
|
||||||
|
Pause
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{agent.status !== 'stopped' && (
|
||||||
|
<button
|
||||||
|
onClick={() => onStatusChange(agent.agent_id, 'stopped')}
|
||||||
|
className="flex-1 flex items-center justify-center gap-1 py-2 bg-red-700 hover:bg-red-600 text-white rounded text-xs font-semibold transition-colors"
|
||||||
|
title="Stop agent"
|
||||||
|
>
|
||||||
|
<StopIcon className="w-3 h-3" />
|
||||||
|
Stop
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
200
src/modules/trading/components/agents/AgentsList.tsx
Normal file
200
src/modules/trading/components/agents/AgentsList.tsx
Normal file
@ -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<BotStatus | 'all'>('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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-2xl font-bold text-white">Trading Bots</h2>
|
||||||
|
<button
|
||||||
|
disabled
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-gray-700 text-gray-400 rounded-lg cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
<span>Loading...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<div key={i} className="bg-gray-800 rounded-xl p-6 border border-gray-700 animate-pulse">
|
||||||
|
<div className="h-6 bg-gray-700 rounded w-1/2 mb-4"></div>
|
||||||
|
<div className="h-4 bg-gray-700 rounded w-3/4 mb-6"></div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="h-4 bg-gray-700 rounded w-full"></div>
|
||||||
|
<div className="h-4 bg-gray-700 rounded w-full"></div>
|
||||||
|
<div className="h-4 bg-gray-700 rounded w-2/3"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-2xl font-bold text-white">Trading Bots</h2>
|
||||||
|
<button
|
||||||
|
onClick={fetchBots}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4" />
|
||||||
|
<span>Retry</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-red-500/10 border border-red-500/30 rounded-xl p-6">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<AlertCircle className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-red-500 mb-1">Error Loading Bots</h3>
|
||||||
|
<p className="text-red-400 text-sm">{error}</p>
|
||||||
|
<button
|
||||||
|
onClick={fetchBots}
|
||||||
|
className="mt-4 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors text-sm"
|
||||||
|
>
|
||||||
|
Try Again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty state
|
||||||
|
if (filteredBots.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-2xl font-bold text-white">Trading Bots</h2>
|
||||||
|
<button
|
||||||
|
onClick={fetchBots}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4" />
|
||||||
|
<span>Refresh</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-800 rounded-xl p-12 text-center border border-gray-700">
|
||||||
|
<div className="max-w-md mx-auto">
|
||||||
|
<div className="w-16 h-16 bg-gray-700 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Bot className="w-8 h-8 text-gray-500" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-white mb-2">No Bots Available</h3>
|
||||||
|
<p className="text-gray-400 mb-6">
|
||||||
|
{filterStatus !== 'all'
|
||||||
|
? `No bots with status "${filterStatus}" found.`
|
||||||
|
: 'Start configuring your trading bots to begin automated trading.'}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={fetchBots}
|
||||||
|
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Refresh List
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main render
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-white">Trading Bots</h2>
|
||||||
|
<p className="text-gray-400 text-sm mt-1">
|
||||||
|
{filteredBots.length} {filteredBots.length === 1 ? 'bot' : 'bots'} available
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{/* Status Filter */}
|
||||||
|
<select
|
||||||
|
value={filterStatus}
|
||||||
|
onChange={(e) => setFilterStatus(e.target.value as BotStatus | 'all')}
|
||||||
|
className="px-4 py-2 bg-gray-700 text-white border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="all">All Status</option>
|
||||||
|
<option value="running">Running</option>
|
||||||
|
<option value="paused">Paused</option>
|
||||||
|
<option value="stopped">Stopped</option>
|
||||||
|
<option value="idle">Idle</option>
|
||||||
|
<option value="error">Error</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* Refresh Button */}
|
||||||
|
<button
|
||||||
|
onClick={fetchBots}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-700 disabled:cursor-not-allowed text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||||
|
<span>Refresh</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bots Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{filteredBots.map((bot) => (
|
||||||
|
<BotCard
|
||||||
|
key={bot.id}
|
||||||
|
bot={bot}
|
||||||
|
onSelect={() => selectBot(bot.name)}
|
||||||
|
onStart={() => handleStartBot(bot)}
|
||||||
|
onStop={() => handleStopBot(bot)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
276
src/modules/trading/components/agents/BotCard.tsx
Normal file
276
src/modules/trading/components/agents/BotCard.tsx
Normal file
@ -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<BotCardProps> = ({
|
||||||
|
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 <Shield {...iconProps} />;
|
||||||
|
case 'orion':
|
||||||
|
return <Target {...iconProps} />;
|
||||||
|
case 'nova':
|
||||||
|
return <Rocket {...iconProps} />;
|
||||||
|
default:
|
||||||
|
return <Target {...iconProps} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<div className="bg-gray-900 rounded-lg p-4 border border-gray-700 hover:border-gray-600 transition-colors">
|
||||||
|
{/* Header Section */}
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{/* Agent Icon */}
|
||||||
|
<div className={`p-2 rounded-lg ${getAgentColor(bot.name)}`}>
|
||||||
|
{getAgentIcon(bot.name)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Agent Name and Risk Level */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-white font-bold">{bot.displayName}</h3>
|
||||||
|
<span className={`text-xs ${riskBadge.color}`}>{riskBadge.label}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Badge */}
|
||||||
|
<div
|
||||||
|
className={`px-2.5 py-1 rounded-full text-xs font-semibold border ${getStatusStyle(bot.status)}`}
|
||||||
|
>
|
||||||
|
{bot.status.charAt(0).toUpperCase() + bot.status.slice(1)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
{bot.description && (
|
||||||
|
<p className="text-gray-400 text-xs mb-4 line-clamp-2">{bot.description}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main Metrics Grid */}
|
||||||
|
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||||
|
{/* P&L Card */}
|
||||||
|
<div className="bg-gray-800 rounded-lg p-3">
|
||||||
|
<span className="text-gray-500 text-xs block mb-1">Today's P&L</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{isProfitable ? (
|
||||||
|
<TrendingUp className="w-4 h-4 text-green-400" />
|
||||||
|
) : (
|
||||||
|
<TrendingDown className="w-4 h-4 text-red-400" />
|
||||||
|
)}
|
||||||
|
<p
|
||||||
|
className={`text-xl font-bold ${isProfitable ? 'text-green-400' : 'text-red-400'}`}
|
||||||
|
>
|
||||||
|
{formatCurrency(bot.todayPnl)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Win Rate Card */}
|
||||||
|
<div className="bg-gray-800 rounded-lg p-3">
|
||||||
|
<span className="text-gray-500 text-xs block mb-1">Win Rate</span>
|
||||||
|
<p className="text-xl font-bold text-blue-400">{formatPercentage(winRate)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Secondary Stats */}
|
||||||
|
<div className="grid grid-cols-3 gap-2 mb-4">
|
||||||
|
{/* Positions */}
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="text-gray-500 text-xs block mb-1">Positions</span>
|
||||||
|
<p className="text-white font-semibold">{bot.positions}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Equity */}
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="text-gray-500 text-xs block mb-1">Equity</span>
|
||||||
|
<p className="text-white font-semibold text-sm">{formatCurrency(bot.equity)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Total Trades */}
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="text-gray-500 text-xs block mb-1">Trades</span>
|
||||||
|
<p className="text-white font-semibold">{bot.metrics?.total_trades ?? 0}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex gap-2 pt-3 border-t border-gray-700">
|
||||||
|
{/* View Details Button */}
|
||||||
|
{onSelect && (
|
||||||
|
<button
|
||||||
|
onClick={() => onSelect(bot)}
|
||||||
|
className="flex-1 flex items-center justify-center gap-1.5 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded text-xs font-semibold transition-colors"
|
||||||
|
title="View bot details"
|
||||||
|
>
|
||||||
|
<Eye className="w-3.5 h-3.5" />
|
||||||
|
Details
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Start/Stop Button */}
|
||||||
|
{bot.status === 'running' || bot.status === 'paused' ? (
|
||||||
|
onStop && (
|
||||||
|
<button
|
||||||
|
onClick={() => onStop(bot.id)}
|
||||||
|
disabled={loading}
|
||||||
|
className="flex-1 flex items-center justify-center gap-1.5 py-2 bg-red-700 hover:bg-red-600 text-white rounded text-xs font-semibold transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title="Stop bot"
|
||||||
|
>
|
||||||
|
<Square className="w-3.5 h-3.5" />
|
||||||
|
Stop
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
onStart && (
|
||||||
|
<button
|
||||||
|
onClick={() => onStart(bot.id)}
|
||||||
|
disabled={loading}
|
||||||
|
className="flex-1 flex items-center justify-center gap-1.5 py-2 bg-green-700 hover:bg-green-600 text-white rounded text-xs font-semibold transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title="Start bot"
|
||||||
|
>
|
||||||
|
<Play className="w-3.5 h-3.5" />
|
||||||
|
Start
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pause Button (only when running) */}
|
||||||
|
{bot.status === 'running' && onStop && (
|
||||||
|
<button
|
||||||
|
onClick={() => onStop(bot.id)}
|
||||||
|
disabled={loading}
|
||||||
|
className="flex items-center justify-center gap-1.5 px-3 py-2 bg-yellow-700 hover:bg-yellow-600 text-white rounded text-xs font-semibold transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title="Pause bot"
|
||||||
|
>
|
||||||
|
<Pause className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BotCard;
|
||||||
7
src/modules/trading/components/agents/index.ts
Normal file
7
src/modules/trading/components/agents/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Export all agent-related components
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { AgentsList } from './AgentsList';
|
||||||
|
export { AgentCard } from './AgentCard';
|
||||||
|
export { BotCard } from './BotCard';
|
||||||
@ -66,3 +66,7 @@ export { default as PositionModifierDialog } from './PositionModifierDialog';
|
|||||||
export { default as RiskBasedPositionSizer } from './RiskBasedPositionSizer';
|
export { default as RiskBasedPositionSizer } from './RiskBasedPositionSizer';
|
||||||
export { default as TradeAlertsNotificationCenter } from './TradeAlertsNotificationCenter';
|
export { default as TradeAlertsNotificationCenter } from './TradeAlertsNotificationCenter';
|
||||||
export type { TradeAlert, AlertType, AlertPriority } from './TradeAlertsNotificationCenter';
|
export type { TradeAlert, AlertType, AlertPriority } from './TradeAlertsNotificationCenter';
|
||||||
|
|
||||||
|
// Trading Agents Components (OQI-007)
|
||||||
|
export { AgentsList } from './agents/AgentsList';
|
||||||
|
export { BotCard } from './agents/BotCard';
|
||||||
|
|||||||
23
src/modules/trading/pages/AgentsPage.tsx
Normal file
23
src/modules/trading/pages/AgentsPage.tsx
Normal file
@ -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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-white">Trading Agents</h1>
|
||||||
|
<p className="text-gray-400 mt-1">
|
||||||
|
Gestiona tus bots de trading automatizado: Atlas, Orion y Nova
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<AgentsList />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
242
src/services/agents.service.ts
Normal file
242
src/services/agents.service.ts
Normal file
@ -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<T>(
|
||||||
|
url: string,
|
||||||
|
options?: RequestInit
|
||||||
|
): Promise<T> {
|
||||||
|
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<TradingAgentsHealthResponse> {
|
||||||
|
return fetchAPI<TradingAgentsHealthResponse>(`${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<AgentSummary[]> {
|
||||||
|
return fetchAPI<AgentSummary[]>(`${API_URL}/api/v1/agents/summary`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Agent Lifecycle
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a trading agent
|
||||||
|
*/
|
||||||
|
export async function startAgent(
|
||||||
|
agentType: AgentType,
|
||||||
|
config: TradingBotConfig
|
||||||
|
): Promise<AgentStatusResponse> {
|
||||||
|
return fetchAPI<AgentStatusResponse>(
|
||||||
|
`${API_URL}/api/v1/agents/${agentType}/start`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(config),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop a trading agent
|
||||||
|
*/
|
||||||
|
export async function stopAgent(agentType: AgentType): Promise<AgentStatusResponse> {
|
||||||
|
return fetchAPI<AgentStatusResponse>(
|
||||||
|
`${API_URL}/api/v1/agents/${agentType}/stop`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause a trading agent
|
||||||
|
*/
|
||||||
|
export async function pauseAgent(agentType: AgentType): Promise<AgentStatusResponse> {
|
||||||
|
return fetchAPI<AgentStatusResponse>(
|
||||||
|
`${API_URL}/api/v1/agents/${agentType}/pause`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume a paused trading agent
|
||||||
|
*/
|
||||||
|
export async function resumeAgent(agentType: AgentType): Promise<AgentStatusResponse> {
|
||||||
|
return fetchAPI<AgentStatusResponse>(
|
||||||
|
`${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<AgentStatusResponse> {
|
||||||
|
return fetchAPI<AgentStatusResponse>(`${API_URL}/api/v1/agents/${agentType}/status`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get performance metrics of a specific agent
|
||||||
|
*/
|
||||||
|
export async function getAgentMetrics(agentType: AgentType): Promise<AgentMetrics> {
|
||||||
|
return fetchAPI<AgentMetrics>(`${API_URL}/api/v1/agents/${agentType}/metrics`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Positions & Trades
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get open positions for a specific agent
|
||||||
|
*/
|
||||||
|
export async function getAgentPositions(agentType: AgentType): Promise<AgentPosition[]> {
|
||||||
|
return fetchAPI<AgentPosition[]>(`${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<AgentTrade[]> {
|
||||||
|
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<AgentTrade[]>(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close a specific position
|
||||||
|
*/
|
||||||
|
export async function closePosition(
|
||||||
|
agentType: AgentType,
|
||||||
|
positionId: string
|
||||||
|
): Promise<AgentTrade> {
|
||||||
|
return fetchAPI<AgentTrade>(
|
||||||
|
`${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<CloseAllPositionsResponse> {
|
||||||
|
return fetchAPI<CloseAllPositionsResponse>(
|
||||||
|
`${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<SignalResponse> {
|
||||||
|
return fetchAPI<SignalResponse>(
|
||||||
|
`${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<BroadcastSignalResponse> {
|
||||||
|
return fetchAPI<BroadcastSignalResponse>(
|
||||||
|
`${API_URL}/api/v1/signals/broadcast`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(signal),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
497
src/stores/agentsStore.ts
Normal file
497
src/stores/agentsStore.ts
Normal file
@ -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<void>;
|
||||||
|
selectBot: (botName: AgentType) => void;
|
||||||
|
refreshBot: (botName: AgentType) => Promise<void>;
|
||||||
|
startBot: (botName: AgentType, config: TradingBotConfig) => Promise<void>;
|
||||||
|
stopBot: (botName: AgentType) => Promise<void>;
|
||||||
|
pauseBot: (botName: AgentType) => Promise<void>;
|
||||||
|
resumeBot: (botName: AgentType) => Promise<void>;
|
||||||
|
|
||||||
|
// Actions - Metrics
|
||||||
|
updateBotMetrics: (botName: AgentType, metrics: Partial<AgentMetrics>) => void;
|
||||||
|
fetchBotMetrics: (botName: AgentType) => Promise<void>;
|
||||||
|
|
||||||
|
// Actions - Positions & Trades
|
||||||
|
fetchPositions: (botName: AgentType) => Promise<void>;
|
||||||
|
fetchTrades: (botName: AgentType, options?: { limit?: number; offset?: number; symbol?: string }) => Promise<void>;
|
||||||
|
closePosition: (botName: AgentType, positionId: string) => Promise<void>;
|
||||||
|
closeAllPositions: (botName: AgentType) => Promise<void>;
|
||||||
|
|
||||||
|
// Actions - Signals
|
||||||
|
sendSignal: (botName: AgentType, signal: SignalInput) => Promise<void>;
|
||||||
|
|
||||||
|
// 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<AgentsState>()(
|
||||||
|
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<AgentMetrics>) => {
|
||||||
|
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;
|
||||||
@ -141,6 +141,9 @@ export {
|
|||||||
// Chat types - Messages, Conversations
|
// Chat types - Messages, Conversations
|
||||||
export * from './chat.types';
|
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)
|
// Export a unified ApiResponse type (using the most complete version from payment.types)
|
||||||
// Also export module-specific aliases for backward compatibility
|
// Also export module-specific aliases for backward compatibility
|
||||||
export { type ApiResponse } from './payment.types';
|
export { type ApiResponse } from './payment.types';
|
||||||
|
|||||||
582
src/types/tradingAgents.types.ts
Normal file
582
src/types/tradingAgents.types.ts
Normal file
@ -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<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 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';
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user