#
tokens: 49002/50000 19/101 files (page 2/8)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 8. Use http://codebase.md/sammcj/bybit-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .env.example
├── .gitignore
├── client
│   ├── .env.example
│   ├── .gitignore
│   ├── package.json
│   ├── pnpm-lock.yaml
│   ├── README.md
│   ├── src
│   │   ├── cli.ts
│   │   ├── client.ts
│   │   ├── config.ts
│   │   ├── env.ts
│   │   ├── index.ts
│   │   └── launch.ts
│   └── tsconfig.json
├── DEV_PLAN.md
├── docs
│   └── HTTP_SERVER.md
├── eslint.config.js
├── jest.config.js
├── LICENSE
├── package-lock.json
├── package.json
├── pnpm-lock.yaml
├── README.md
├── specs
│   ├── bybit
│   │   ├── bybit-api-v5-openapi.yaml
│   │   └── bybit-api-v5-postman-collection.json
│   ├── mcp
│   │   ├── mcp-schema.json
│   │   └── mcp-schema.ts
│   └── README.md
├── src
│   ├── __tests__
│   │   ├── GetMLRSI.test.ts
│   │   ├── integration.test.ts
│   │   ├── test-setup.ts
│   │   └── tools.test.ts
│   ├── constants.ts
│   ├── env.ts
│   ├── httpServer.ts
│   ├── index.ts
│   ├── tools
│   │   ├── BaseTool.ts
│   │   ├── GetInstrumentInfo.ts
│   │   ├── GetKline.ts
│   │   ├── GetMarketInfo.ts
│   │   ├── GetMarketStructure.ts
│   │   ├── GetMLRSI.ts
│   │   ├── GetOrderBlocks.ts
│   │   ├── GetOrderbook.ts
│   │   ├── GetOrderHistory.ts
│   │   ├── GetPositions.ts
│   │   ├── GetTicker.ts
│   │   ├── GetTrades.ts
│   │   └── GetWalletBalance.ts
│   └── utils
│       ├── knnAlgorithm.ts
│       ├── mathUtils.ts
│       ├── toolLoader.ts
│       └── volumeAnalysis.ts
├── tsconfig.json
└── webui
    ├── .dockerignore
    ├── .env.example
    ├── build-docker.sh
    ├── docker-compose.yml
    ├── docker-entrypoint.sh
    ├── docker-healthcheck.sh
    ├── DOCKER.md
    ├── Dockerfile
    ├── index.html
    ├── package.json
    ├── pnpm-lock.yaml
    ├── pnpm-workspace.yaml
    ├── public
    │   ├── favicon.svg
    │   └── inter.woff2
    ├── README.md
    ├── screenshot.png
    ├── src
    │   ├── assets
    │   │   └── fonts
    │   │       └── fonts.css
    │   ├── components
    │   │   ├── AgentDashboard.ts
    │   │   ├── chat
    │   │   │   ├── DataCard.ts
    │   │   │   └── MessageRenderer.ts
    │   │   ├── ChatApp.ts
    │   │   ├── DataVerificationPanel.ts
    │   │   ├── DebugConsole.ts
    │   │   └── ToolsManager.ts
    │   ├── main.ts
    │   ├── services
    │   │   ├── agentConfig.ts
    │   │   ├── agentMemory.ts
    │   │   ├── aiClient.ts
    │   │   ├── citationProcessor.ts
    │   │   ├── citationStore.ts
    │   │   ├── configService.ts
    │   │   ├── logService.ts
    │   │   ├── mcpClient.ts
    │   │   ├── multiStepAgent.ts
    │   │   ├── performanceOptimiser.ts
    │   │   └── systemPrompt.ts
    │   ├── styles
    │   │   ├── agent-dashboard.css
    │   │   ├── base.css
    │   │   ├── citations.css
    │   │   ├── components.css
    │   │   ├── data-cards.css
    │   │   ├── main.css
    │   │   ├── processing.css
    │   │   ├── variables.css
    │   │   └── verification-panel.css
    │   ├── types
    │   │   ├── agent.ts
    │   │   ├── ai.ts
    │   │   ├── citation.ts
    │   │   ├── mcp.ts
    │   │   └── workflow.ts
    │   └── utils
    │       ├── dataDetection.ts
    │       └── formatters.ts
    ├── tsconfig.json
    └── vite.config.ts
```

# Files

--------------------------------------------------------------------------------
/webui/src/types/mcp.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * TypeScript types for MCP (Model Context Protocol) server integration
  3 |  */
  4 | 
  5 | // Base MCP types
  6 | export interface MCPRequest {
  7 |   jsonrpc: '2.0';
  8 |   id: string | number;
  9 |   method: string;
 10 |   params?: Record<string, unknown>;
 11 | }
 12 | 
 13 | export interface MCPResponse<T = unknown> {
 14 |   jsonrpc: '2.0';
 15 |   id: string | number;
 16 |   result?: T;
 17 |   error?: MCPError;
 18 | }
 19 | 
 20 | export interface MCPError {
 21 |   code: number;
 22 |   message: string;
 23 |   data?: unknown;
 24 | }
 25 | 
 26 | // Tool definitions
 27 | export interface MCPTool {
 28 |   name: string;
 29 |   description: string;
 30 |   inputSchema: {
 31 |     type: 'object';
 32 |     properties?: Record<string, unknown>;
 33 |     required?: string[];
 34 |   };
 35 | }
 36 | 
 37 | export interface MCPToolCall {
 38 |   name: string;
 39 |   arguments: Record<string, unknown>;
 40 | }
 41 | 
 42 | export interface MCPToolResult {
 43 |   content: Array<{
 44 |     type: 'text';
 45 |     text: string;
 46 |   }>;
 47 |   isError?: boolean;
 48 | }
 49 | 
 50 | // Bybit-specific types
 51 | export interface BybitCategory {
 52 |   spot: 'spot';
 53 |   linear: 'linear';
 54 |   inverse: 'inverse';
 55 |   option: 'option';
 56 | }
 57 | 
 58 | export interface TickerData {
 59 |   symbol: string;
 60 |   category: string;
 61 |   lastPrice: string;
 62 |   price24hPcnt: string;
 63 |   highPrice24h: string;
 64 |   lowPrice24h: string;
 65 |   prevPrice24h: string;
 66 |   volume24h: string;
 67 |   turnover24h: string;
 68 |   bid1Price: string;
 69 |   bid1Size: string;
 70 |   ask1Price: string;
 71 |   ask1Size: string;
 72 |   usdIndexPrice?: string;
 73 |   timestamp: string;
 74 | }
 75 | 
 76 | export interface KlineData {
 77 |   symbol: string;
 78 |   category: string;
 79 |   interval: string;
 80 |   data: Array<{
 81 |     startTime: number;
 82 |     openPrice: string;
 83 |     highPrice: string;
 84 |     lowPrice: string;
 85 |     closePrice: string;
 86 |     volume: string;
 87 |     turnover: string;
 88 |   }>;
 89 | }
 90 | 
 91 | export interface OrderbookData {
 92 |   symbol: string;
 93 |   category: string;
 94 |   bids: Array<[string, string]>; // [price, size]
 95 |   asks: Array<[string, string]>; // [price, size]
 96 |   timestamp: string;
 97 | }
 98 | 
 99 | // Advanced analysis types
100 | export interface MLRSIData {
101 |   symbol: string;
102 |   interval: string;
103 |   data: Array<{
104 |     timestamp: number;
105 |     standardRsi: number;
106 |     mlRsi: number;
107 |     adaptiveOverbought: number;
108 |     adaptiveOversold: number;
109 |     knnDivergence: number;
110 |     effectiveNeighbors: number;
111 |     trend: 'bullish' | 'bearish' | 'neutral';
112 |   }>;
113 |   metadata: {
114 |     mlEnabled: boolean;
115 |     featuresUsed: string[];
116 |     smoothingApplied: string;
117 |     calculationTime: number;
118 |   };
119 | }
120 | 
121 | export interface OrderBlock {
122 |   id: string;
123 |   timestamp: number;
124 |   top: number;
125 |   bottom: number;
126 |   average: number;
127 |   volume: number;
128 |   mitigated: boolean;
129 |   mitigationTime?: number;
130 | }
131 | 
132 | export interface OrderBlocksData {
133 |   symbol: string;
134 |   interval: string;
135 |   bullishBlocks: OrderBlock[];
136 |   bearishBlocks: OrderBlock[];
137 |   currentSupport: number[];
138 |   currentResistance: number[];
139 |   metadata: {
140 |     volumePivotLength: number;
141 |     mitigationMethod: string;
142 |     blocksDetected: number;
143 |     activeBullishBlocks: number;
144 |     activeBearishBlocks: number;
145 |   };
146 | }
147 | 
148 | export interface MarketStructureData {
149 |   symbol: string;
150 |   interval: string;
151 |   marketRegime: 'trending_up' | 'trending_down' | 'ranging' | 'volatile';
152 |   trendStrength: number;
153 |   volatilityLevel: 'low' | 'medium' | 'high';
154 |   keyLevels: {
155 |     support: number[];
156 |     resistance: number[];
157 |     liquidityZones: Array<{
158 |       price: number;
159 |       strength: number;
160 |       type: 'support' | 'resistance';
161 |     }>;
162 |   };
163 |   orderBlocks?: OrderBlocksData;
164 |   mlRsi?: MLRSIData;
165 |   recommendations: string[];
166 |   metadata: {
167 |     analysisDepth: number;
168 |     calculationTime: number;
169 |     confidence: number;
170 |   };
171 | }
172 | 
173 | // Tool parameter types
174 | export interface GetTickerParams {
175 |   symbol: string;
176 |   category?: keyof BybitCategory;
177 | }
178 | 
179 | export interface GetKlineParams {
180 |   symbol: string;
181 |   category?: keyof BybitCategory;
182 |   interval?: string;
183 |   limit?: number;
184 | }
185 | 
186 | export interface GetOrderbookParams {
187 |   symbol: string;
188 |   category?: keyof BybitCategory;
189 |   limit?: number;
190 | }
191 | 
192 | export interface GetMLRSIParams {
193 |   symbol: string;
194 |   category: keyof BybitCategory;
195 |   interval: string;
196 |   rsiLength?: number;
197 |   knnNeighbors?: number;
198 |   knnLookback?: number;
199 |   mlWeight?: number;
200 |   featureCount?: number;
201 |   smoothingMethod?: string;
202 |   limit?: number;
203 | }
204 | 
205 | export interface GetOrderBlocksParams {
206 |   symbol: string;
207 |   category: keyof BybitCategory;
208 |   interval: string;
209 |   volumePivotLength?: number;
210 |   bullishBlocks?: number;
211 |   bearishBlocks?: number;
212 |   mitigationMethod?: string;
213 |   limit?: number;
214 | }
215 | 
216 | export interface GetMarketStructureParams {
217 |   symbol: string;
218 |   category: keyof BybitCategory;
219 |   interval: string;
220 |   analysisDepth?: number;
221 |   includeOrderBlocks?: boolean;
222 |   includeMLRSI?: boolean;
223 |   includeLiquidityZones?: boolean;
224 | }
225 | 
226 | // Available MCP tools
227 | export type MCPToolName =
228 |   | 'get_ticker'
229 |   | 'get_orderbook'
230 |   | 'get_kline'
231 |   | 'get_market_info'
232 |   | 'get_trades'
233 |   | 'get_instrument_info'
234 |   | 'get_wallet_balance'
235 |   | 'get_positions'
236 |   | 'get_order_history'
237 |   | 'get_ml_rsi'
238 |   | 'get_order_blocks'
239 |   | 'get_market_structure';
240 | 
241 | export type MCPToolParams<T extends MCPToolName> =
242 |   T extends 'get_ticker' ? GetTickerParams :
243 |   T extends 'get_kline' ? GetKlineParams :
244 |   T extends 'get_orderbook' ? GetOrderbookParams :
245 |   T extends 'get_ml_rsi' ? GetMLRSIParams :
246 |   T extends 'get_order_blocks' ? GetOrderBlocksParams :
247 |   T extends 'get_market_structure' ? GetMarketStructureParams :
248 |   Record<string, unknown>;
249 | 
250 | export type MCPToolResponse<T extends MCPToolName> =
251 |   T extends 'get_ticker' ? TickerData :
252 |   T extends 'get_kline' ? KlineData :
253 |   T extends 'get_orderbook' ? OrderbookData :
254 |   T extends 'get_ml_rsi' ? MLRSIData :
255 |   T extends 'get_order_blocks' ? OrderBlocksData :
256 |   T extends 'get_market_structure' ? MarketStructureData :
257 |   unknown;
258 | 
```

--------------------------------------------------------------------------------
/docs/HTTP_SERVER.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Bybit MCP HTTP Server
  2 | 
  3 | The Bybit MCP server now supports HTTP/SSE transport in addition to the standard stdio transport. This enables web applications and other HTTP clients to interact with the MCP server.
  4 | 
  5 | ## Features
  6 | 
  7 | - **Modern Streamable HTTP Transport**: Latest MCP protocol support with session management
  8 | - **Legacy SSE Transport**: Backwards compatibility with older MCP clients
  9 | - **Health Monitoring**: Built-in health check endpoint
 10 | - **CORS Support**: Configurable cross-origin resource sharing
 11 | - **Session Management**: Automatic session lifecycle management
 12 | - **Graceful Shutdown**: Proper cleanup on server termination
 13 | 
 14 | ## Configuration
 15 | 
 16 | ### Environment Variables
 17 | 
 18 | - `MCP_HTTP_PORT`: Server port (default: 8080)
 19 | - `MCP_HTTP_HOST`: Server host (default: localhost)
 20 | - `CORS_ORIGIN`: CORS origin policy (default: *)
 21 | 
 22 | ### Example Configuration
 23 | 
 24 | ```bash
 25 | export MCP_HTTP_PORT=8080
 26 | export MCP_HTTP_HOST=0.0.0.0
 27 | export CORS_ORIGIN="https://myapp.com"
 28 | ```
 29 | 
 30 | ## Endpoints
 31 | 
 32 | ### Health Check
 33 | - **URL**: `GET /health`
 34 | - **Description**: Server health and status information
 35 | - **Response**: JSON with server status, version, and active transport counts
 36 | 
 37 | ```json
 38 | {
 39 |   "status": "healthy",
 40 |   "name": "bybit-mcp",
 41 |   "version": "0.2.0",
 42 |   "timestamp": "2025-05-24T04:19:35.168Z",
 43 |   "transports": {
 44 |     "streamable": 0,
 45 |     "sse": 0
 46 |   }
 47 | }
 48 | ```
 49 | 
 50 | ### Modern Streamable HTTP Transport
 51 | - **URL**: `POST|GET|DELETE /mcp`
 52 | - **Description**: Modern MCP protocol endpoint with session management
 53 | - **Headers**: 
 54 |   - `Content-Type: application/json`
 55 |   - `mcp-session-id: <session-id>` (for existing sessions)
 56 | 
 57 | ### Legacy SSE Transport
 58 | - **URL**: `GET /sse`
 59 | - **Description**: Server-Sent Events endpoint for legacy clients
 60 | - **Response**: SSE stream with session ID
 61 | 
 62 | - **URL**: `POST /messages?sessionId=<session-id>`
 63 | - **Description**: Message endpoint for SSE clients
 64 | - **Headers**: `Content-Type: application/json`
 65 | 
 66 | ## Usage
 67 | 
 68 | ### Starting the HTTP Server
 69 | 
 70 | ```bash
 71 | # Build the project
 72 | pnpm build
 73 | 
 74 | # Start HTTP server
 75 | pnpm start:http
 76 | 
 77 | # Or run directly
 78 | node build/httpServer.js
 79 | ```
 80 | 
 81 | ### Client Connection Examples
 82 | 
 83 | #### Modern HTTP Client (Recommended)
 84 | 
 85 | ```typescript
 86 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 87 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
 88 | 
 89 | const client = new Client({
 90 |   name: 'my-client',
 91 |   version: '1.0.0'
 92 | });
 93 | 
 94 | const transport = new StreamableHTTPClientTransport(
 95 |   new URL('http://localhost:8080/mcp')
 96 | );
 97 | 
 98 | await client.connect(transport);
 99 | ```
100 | 
101 | #### Legacy SSE Client
102 | 
103 | ```typescript
104 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
105 | import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
106 | 
107 | const client = new Client({
108 |   name: 'legacy-client',
109 |   version: '1.0.0'
110 | });
111 | 
112 | const transport = new SSEClientTransport(
113 |   new URL('http://localhost:8080/sse')
114 | );
115 | 
116 | await client.connect(transport);
117 | ```
118 | 
119 | #### Backwards Compatible Client
120 | 
121 | ```typescript
122 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
123 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
124 | import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
125 | 
126 | const baseUrl = new URL('http://localhost:8080');
127 | let client: Client;
128 | 
129 | try {
130 |   // Try modern transport first
131 |   client = new Client({ name: 'client', version: '1.0.0' });
132 |   const transport = new StreamableHTTPClientTransport(new URL('/mcp', baseUrl));
133 |   await client.connect(transport);
134 |   console.log("Connected using Streamable HTTP transport");
135 | } catch (error) {
136 |   // Fall back to SSE transport
137 |   console.log("Falling back to SSE transport");
138 |   client = new Client({ name: 'client', version: '1.0.0' });
139 |   const sseTransport = new SSEClientTransport(new URL('/sse', baseUrl));
140 |   await client.connect(sseTransport);
141 |   console.log("Connected using SSE transport");
142 | }
143 | ```
144 | 
145 | ### Web Application Integration
146 | 
147 | The HTTP server is designed to work seamlessly with web applications. The included WebUI demonstrates how to integrate with the MCP server over HTTP.
148 | 
149 | #### Proxy Configuration (Vite)
150 | 
151 | ```typescript
152 | // vite.config.ts
153 | export default defineConfig({
154 |   server: {
155 |     proxy: {
156 |       '/api/mcp': {
157 |         target: 'http://localhost:8080',
158 |         changeOrigin: true,
159 |         rewrite: (path) => path.replace(/^\/api\/mcp/, ''),
160 |       },
161 |     },
162 |   },
163 | });
164 | ```
165 | 
166 | ## Available Tools
167 | 
168 | The HTTP server exposes all the same tools as the stdio version:
169 | 
170 | - `get_instrument_info` - Get trading instrument information
171 | - `get_kline` - Get candlestick/kline data
172 | - `get_ml_rsi` - Get ML-enhanced RSI indicator
173 | - `get_market_info` - Get market information
174 | - `get_market_structure` - Get market structure analysis
175 | - `get_order_blocks` - Get order block detection
176 | - `get_order_history` - Get order history
177 | - `get_orderbook` - Get order book data
178 | - `get_positions` - Get current positions
179 | - `get_ticker` - Get ticker information
180 | - `get_trades` - Get recent trades
181 | - `get_wallet_balance` - Get wallet balance
182 | 
183 | ## Security Considerations
184 | 
185 | - Configure CORS appropriately for production use
186 | - Use HTTPS in production environments
187 | - Consider rate limiting for public deployments
188 | - Validate all input parameters
189 | - Monitor session counts and cleanup
190 | 
191 | ## Troubleshooting
192 | 
193 | ### Common Issues
194 | 
195 | 1. **Port already in use**: Change the port using `MCP_HTTP_PORT` environment variable
196 | 2. **CORS errors**: Configure `CORS_ORIGIN` environment variable
197 | 3. **Connection refused**: Ensure the server is running and accessible
198 | 4. **Session errors**: Check that session IDs are properly managed
199 | 
200 | ### Debugging
201 | 
202 | Enable debug logging by setting the log level:
203 | 
204 | ```bash
205 | export LOG_LEVEL=debug
206 | node build/httpServer.js
207 | ```
208 | 
209 | ### Health Check
210 | 
211 | Always verify the server is healthy:
212 | 
213 | ```bash
214 | curl http://localhost:8080/health
215 | ```
216 | 
217 | ## Performance
218 | 
219 | - Session management is memory-based (consider Redis for production)
220 | - Automatic cleanup of closed sessions
221 | - Configurable timeouts and limits
222 | - Graceful shutdown handling
223 | 
224 | ## Development
225 | 
226 | For development, you can run both the HTTP server and WebUI simultaneously:
227 | 
228 | ```bash
229 | # Terminal 1: Start MCP HTTP server
230 | pnpm start:http
231 | 
232 | # Terminal 2: Start WebUI development server
233 | cd webui && pnpm dev
234 | ```
235 | 
236 | The WebUI will proxy MCP requests to the HTTP server automatically.
237 | 
```

--------------------------------------------------------------------------------
/webui/src/services/citationProcessor.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Citation processor for parsing AI responses and creating interactive citations
  3 |  */
  4 | 
  5 | import type { CitationReference, ProcessedMessage, CitationTooltipData } from '@/types/citation';
  6 | import { citationStore } from './citationStore';
  7 | 
  8 | export class CitationProcessor {
  9 |   // Regex pattern to match citation references like [REF001], [REF123], etc.
 10 |   private static readonly CITATION_PATTERN = /\[REF(\d{3})\]/g;
 11 | 
 12 |   /**
 13 |    * Process AI response content to extract and convert citations
 14 |    */
 15 |   processMessage(content: string): ProcessedMessage {
 16 |     const citations: CitationReference[] = [];
 17 |     let processedContent = content;
 18 |     let match;
 19 | 
 20 |     // Reset regex lastIndex to ensure we find all matches
 21 |     CitationProcessor.CITATION_PATTERN.lastIndex = 0;
 22 | 
 23 |     // Find all citation patterns in the content
 24 |     while ((match = CitationProcessor.CITATION_PATTERN.exec(content)) !== null) {
 25 |       const fullMatch = match[0]; // e.g., "[REF001]"
 26 |       const referenceId = fullMatch; // Keep the full format for consistency
 27 | 
 28 |       citations.push({
 29 |         referenceId,
 30 |         startIndex: match.index,
 31 |         endIndex: match.index + fullMatch.length,
 32 |         text: fullMatch
 33 |       });
 34 |     }
 35 | 
 36 |     // Convert citation patterns to interactive elements
 37 |     if (citations.length > 0) {
 38 |       processedContent = this.convertCitationsToInteractive(content, citations);
 39 |     }
 40 | 
 41 |     return {
 42 |       originalContent: content,
 43 |       processedContent,
 44 |       citations
 45 |     };
 46 |   }
 47 | 
 48 |   /**
 49 |    * Convert citation patterns to interactive HTML elements
 50 |    */
 51 |   private convertCitationsToInteractive(content: string, citations: CitationReference[]): string {
 52 |     let processedContent = content;
 53 | 
 54 |     // Process citations in reverse order to maintain correct indices
 55 |     const sortedCitations = [...citations].sort((a, b) => b.startIndex - a.startIndex);
 56 | 
 57 |     for (const citation of sortedCitations) {
 58 |       const citationData = citationStore.getCitation(citation.referenceId);
 59 |       let hasData = citationData !== undefined;
 60 | 
 61 |       // For testing: create mock data if no real data exists
 62 |       if (!hasData) {
 63 |         console.log(`🧪 Creating mock citation data for ${citation.referenceId}`);
 64 |         const mockData = {
 65 |           referenceId: citation.referenceId,
 66 |           timestamp: new Date().toISOString(),
 67 |           toolName: 'get_ticker',
 68 |           endpoint: '/v5/market/tickers',
 69 |           rawData: {
 70 |             symbol: 'BTCUSDT',
 71 |             lastPrice: '$103,411.53',
 72 |             price24hPcnt: '-0.73%',
 73 |             volume24h: '19.73 BTC'
 74 |           },
 75 |           extractedMetrics: [
 76 |             {
 77 |               type: 'price' as const,
 78 |               label: 'Last Price',
 79 |               value: '$103,411.53',
 80 |               unit: 'USD',
 81 |               significance: 'high' as const
 82 |             },
 83 |             {
 84 |               type: 'percentage' as const,
 85 |               label: '24h Change',
 86 |               value: '-0.73%',
 87 |               unit: '%',
 88 |               significance: 'high' as const
 89 |             }
 90 |           ]
 91 |         };
 92 |         citationStore.storeCitation(mockData);
 93 |         hasData = true;
 94 |       }
 95 | 
 96 |       // Create a more compact, single-line span element
 97 |       const interactiveElement = `<span class="citation-ref ${hasData ? 'has-data' : 'no-data'}" data-reference-id="${citation.referenceId}" data-has-data="${hasData}" title="${hasData ? 'Click to view data details' : 'Citation data not available'}" role="button" tabindex="0">${citation.text}</span>`;
 98 | 
 99 |       processedContent =
100 |         processedContent.slice(0, citation.startIndex) +
101 |         interactiveElement +
102 |         processedContent.slice(citation.endIndex);
103 |     }
104 | 
105 |     return processedContent;
106 |   }
107 | 
108 |   /**
109 |    * Get tooltip data for a citation reference
110 |    */
111 |   getCitationTooltipData(referenceId: string): CitationTooltipData | null {
112 |     const citationData = citationStore.getCitation(referenceId);
113 | 
114 |     if (!citationData) {
115 |       return null;
116 |     }
117 | 
118 |     return {
119 |       referenceId: citationData.referenceId,
120 |       toolName: citationData.toolName,
121 |       timestamp: citationData.timestamp,
122 |       endpoint: citationData.endpoint,
123 |       keyMetrics: citationData.extractedMetrics || [],
124 |       hasFullData: true
125 |     };
126 |   }
127 | 
128 |   /**
129 |    * Format timestamp for display
130 |    */
131 |   formatTimestamp(timestamp: string): string {
132 |     try {
133 |       const date = new Date(timestamp);
134 |       return date.toLocaleString();
135 |     } catch (error) {
136 |       return timestamp;
137 |     }
138 |   }
139 | 
140 |   /**
141 |    * Create tooltip HTML content
142 |    */
143 |   createTooltipContent(tooltipData: CitationTooltipData): string {
144 |     const formattedTime = this.formatTimestamp(tooltipData.timestamp);
145 | 
146 |     let metricsHtml = '';
147 |     if (tooltipData.keyMetrics.length > 0) {
148 |       metricsHtml = `
149 |         <div class="citation-metrics">
150 |           <h4>Key Data Points:</h4>
151 |           <ul>
152 |             ${tooltipData.keyMetrics.map(metric => `
153 |               <li class="metric-${metric.significance}">
154 |                 <span class="metric-label">${metric.label}:</span>
155 |                 <span class="metric-value">${metric.value}${metric.unit ? ' ' + metric.unit : ''}</span>
156 |               </li>
157 |             `).join('')}
158 |           </ul>
159 |         </div>
160 |       `;
161 |     }
162 | 
163 |     return `
164 |       <div class="citation-tooltip">
165 |         <div class="citation-header">
166 |           <span class="citation-id">${tooltipData.referenceId}</span>
167 |           <span class="citation-tool">${tooltipData.toolName}</span>
168 |         </div>
169 |         <div class="citation-time">${formattedTime}</div>
170 |         ${tooltipData.endpoint ? `<div class="citation-endpoint">${tooltipData.endpoint}</div>` : ''}
171 |         ${metricsHtml}
172 |         <div class="citation-actions">
173 |           <button class="btn-view-full" data-reference-id="${tooltipData.referenceId}">
174 |             View Full Data
175 |           </button>
176 |         </div>
177 |       </div>
178 |     `;
179 |   }
180 | 
181 |   /**
182 |    * Extract all citation references from content
183 |    */
184 |   extractCitationReferences(content: string): string[] {
185 |     const references: string[] = [];
186 |     let match;
187 | 
188 |     CitationProcessor.CITATION_PATTERN.lastIndex = 0;
189 |     while ((match = CitationProcessor.CITATION_PATTERN.exec(content)) !== null) {
190 |       references.push(match[0]);
191 |     }
192 | 
193 |     return references;
194 |   }
195 | 
196 |   /**
197 |    * Validate citation reference format
198 |    */
199 |   isValidCitationReference(reference: string): boolean {
200 |     return CitationProcessor.CITATION_PATTERN.test(reference);
201 |   }
202 | }
203 | 
204 | // Singleton instance
205 | export const citationProcessor = new CitationProcessor();
206 | 
```

--------------------------------------------------------------------------------
/webui/DOCKER.md:
--------------------------------------------------------------------------------

```markdown
  1 | # 🐳 Docker Deployment Guide
  2 | 
  3 | This guide covers how to build and deploy the Bybit MCP WebUI using Docker.
  4 | 
  5 | ## 📋 Prerequisites
  6 | 
  7 | - Docker Engine 20.10+
  8 | - Docker Compose 2.0+
  9 | - 2GB+ available RAM
 10 | - 1GB+ available disk space
 11 | 
 12 | ## 🚀 Quick Start
 13 | 
 14 | ### Option 1: Using Docker Compose (Recommended)
 15 | 
 16 | ```bash
 17 | # Clone the repository
 18 | git clone https://github.com/sammcj/bybit-mcp.git
 19 | cd bybit-mcp/webui
 20 | 
 21 | # Start the application
 22 | docker-compose up -d
 23 | 
 24 | # View logs
 25 | docker-compose logs -f
 26 | 
 27 | # Stop the application
 28 | docker-compose down
 29 | ```
 30 | 
 31 | ### Option 2: Using Docker Build
 32 | 
 33 | ```bash
 34 | # Build the image
 35 | docker build -t bybit-mcp-webui -f webui/Dockerfile .
 36 | 
 37 | # Run the container
 38 | docker run -d \
 39 |   --name bybit-mcp-webui \
 40 |   -p 8080:8080 \
 41 |   --restart unless-stopped \
 42 |   bybit-mcp-webui
 43 | ```
 44 | 
 45 | ## 🌐 Access the Application
 46 | 
 47 | Once running, access the WebUI at:
 48 | - **Local**: http://localhost:8080
 49 | - **Network**: http://YOUR_SERVER_IP:8080
 50 | 
 51 | ## ⚙️ Configuration
 52 | 
 53 | ### Environment Variables
 54 | 
 55 | | Variable | Default | Description |
 56 | |----------|---------|-------------|
 57 | | `NODE_ENV` | `production` | Application environment |
 58 | | `PORT` | `8080` | HTTP server port |
 59 | | `MCP_PORT` | `8080` | MCP server port |
 60 | | `HOST` | `0.0.0.0` | Bind address |
 61 | | `BYBIT_API_KEY` | - | Bybit API key (optional) |
 62 | | `BYBIT_API_SECRET` | - | Bybit API secret (optional) |
 63 | | `BYBIT_TESTNET` | `true` | Use Bybit testnet |
 64 | 
 65 | ### Custom Configuration
 66 | 
 67 | Create a `.env` file in the webui directory:
 68 | 
 69 | ```bash
 70 | # .env
 71 | BYBIT_API_KEY=your_api_key_here
 72 | BYBIT_API_SECRET=your_api_secret_here
 73 | BYBIT_TESTNET=false
 74 | PORT=3000
 75 | ```
 76 | 
 77 | Then update docker-compose.yml:
 78 | 
 79 | ```yaml
 80 | services:
 81 |   bybit-mcp-webui:
 82 |     env_file:
 83 |       - .env
 84 | ```
 85 | 
 86 | ## 🔧 Advanced Usage
 87 | 
 88 | ### Development Mode
 89 | 
 90 | For development with hot reload:
 91 | 
 92 | ```bash
 93 | # Build development image
 94 | docker build -t bybit-mcp-webui:dev --target deps -f webui/Dockerfile .
 95 | 
 96 | # Run with volume mounts
 97 | docker run -d \
 98 |   --name bybit-mcp-dev \
 99 |   -p 8080:8080 \
100 |   -v $(pwd):/app \
101 |   -v /app/node_modules \
102 |   -v /app/webui/node_modules \
103 |   bybit-mcp-webui:dev \
104 |   sh -c "cd /app && pnpm dev:full"
105 | ```
106 | 
107 | ### Production Deployment
108 | 
109 | For production with reverse proxy:
110 | 
111 | ```yaml
112 | # docker-compose.prod.yml
113 | services:
114 |   bybit-mcp-webui:
115 |     image: bybit-mcp-webui:latest
116 |     environment:
117 |       - NODE_ENV=production
118 |       - BYBIT_TESTNET=false
119 |     deploy:
120 |       replicas: 2
121 |       resources:
122 |         limits:
123 |           memory: 1G
124 |           cpus: '1.0'
125 |     networks:
126 |       - traefik
127 |     labels:
128 |       - "traefik.enable=true"
129 |       - "traefik.http.routers.bybit.rule=Host(`your-domain.com`)"
130 |       - "traefik.http.routers.bybit.tls.certresolver=letsencrypt"
131 | 
132 | networks:
133 |   traefik:
134 |     external: true
135 | ```
136 | 
137 | ## 🔍 Monitoring & Debugging
138 | 
139 | ### Health Checks
140 | 
141 | The container includes built-in health checks:
142 | 
143 | ```bash
144 | # Check container health
145 | docker ps
146 | docker inspect bybit-mcp-webui | grep -A 10 Health
147 | 
148 | # Manual health check
149 | docker exec bybit-mcp-webui /app/healthcheck.sh
150 | ```
151 | 
152 | ### Logs
153 | 
154 | ```bash
155 | # View application logs
156 | docker logs bybit-mcp-webui
157 | 
158 | # Follow logs in real-time
159 | docker logs -f bybit-mcp-webui
160 | 
161 | # View last 100 lines
162 | docker logs --tail 100 bybit-mcp-webui
163 | ```
164 | 
165 | ### Container Shell Access
166 | 
167 | ```bash
168 | # Access container shell
169 | docker exec -it bybit-mcp-webui sh
170 | 
171 | # Check running processes
172 | docker exec bybit-mcp-webui ps aux
173 | 
174 | # Check disk usage
175 | docker exec bybit-mcp-webui df -h
176 | ```
177 | 
178 | ## 🛠️ Troubleshooting
179 | 
180 | ### Common Issues
181 | 
182 | **Port Already in Use**
183 | ```bash
184 | # Find process using port 8080
185 | lsof -i :8080
186 | # or
187 | netstat -tulpn | grep 8080
188 | 
189 | # Kill the process or use different port
190 | docker run -p 3000:8080 bybit-mcp-webui
191 | ```
192 | 
193 | **Permission Denied**
194 | ```bash
195 | # Check if Docker daemon is running
196 | sudo systemctl status docker
197 | 
198 | # Add user to docker group
199 | sudo usermod -aG docker $USER
200 | newgrp docker
201 | ```
202 | 
203 | **Build Failures**
204 | ```bash
205 | # Clear Docker cache
206 | docker system prune -a
207 | 
208 | # Rebuild without cache
209 | docker build --no-cache -t bybit-mcp-webui -f webui/Dockerfile .
210 | ```
211 | 
212 | **Memory Issues**
213 | ```bash
214 | # Increase Docker memory limit
215 | # Docker Desktop: Settings > Resources > Memory
216 | 
217 | # Check container memory usage
218 | docker stats bybit-mcp-webui
219 | ```
220 | 
221 | ### Performance Optimization
222 | 
223 | **Two-stage Build Benefits:**
224 | - ✅ Smaller final image (~200MB vs ~1GB)
225 | - ✅ Faster deployments
226 | - ✅ Better security (no dev dependencies)
227 | - ✅ Optimized layer caching
228 | - ✅ Cleaner, more maintainable Dockerfile
229 | 
230 | **Resource Limits:**
231 | ```yaml
232 | deploy:
233 |   resources:
234 |     limits:
235 |       memory: 512M      # Adjust based on usage
236 |       cpus: '0.5'       # Adjust based on load
237 | ```
238 | 
239 | ## 🔒 Security
240 | 
241 | ### Best Practices
242 | 
243 | 1. **Non-root User**: Container runs as user `bybit` (UID 1001)
244 | 2. **Read-only Filesystem**: Where possible
245 | 3. **No New Privileges**: Security option enabled
246 | 4. **Minimal Base Image**: Alpine Linux
247 | 5. **Health Checks**: Built-in monitoring
248 | 6. **Resource Limits**: Prevent resource exhaustion
249 | 
250 | ### API Key Security
251 | 
252 | **Never commit API keys to version control!**
253 | 
254 | ```bash
255 | # Use environment variables
256 | export BYBIT_API_KEY="your_key"
257 | export BYBIT_API_SECRET="your_secret"
258 | 
259 | # Or use Docker secrets
260 | echo "your_key" | docker secret create bybit_api_key -
261 | echo "your_secret" | docker secret create bybit_api_secret -
262 | ```
263 | 
264 | ## 📊 Monitoring
265 | 
266 | ### Prometheus Metrics
267 | 
268 | Add monitoring with Prometheus:
269 | 
270 | ```yaml
271 | # docker-compose.monitoring.yml
272 | services:
273 |   prometheus:
274 |     image: prom/prometheus:latest
275 |     ports:
276 |       - "9090:9090"
277 |     volumes:
278 |       - ./prometheus.yml:/etc/prometheus/prometheus.yml
279 | 
280 |   grafana:
281 |     image: grafana/grafana:latest
282 |     ports:
283 |       - "3000:3000"
284 |     environment:
285 |       - GF_SECURITY_ADMIN_PASSWORD=admin
286 | ```
287 | 
288 | ## 🚀 Deployment Platforms
289 | 
290 | ### Docker Swarm
291 | ```bash
292 | docker stack deploy -c docker-compose.yml bybit-mcp
293 | ```
294 | 
295 | ### Kubernetes
296 | ```bash
297 | # Generate Kubernetes manifests
298 | kompose convert -f docker-compose.yml
299 | kubectl apply -f .
300 | ```
301 | 
302 | ### Cloud Platforms
303 | - **AWS ECS**: Use the provided Dockerfile
304 | - **Google Cloud Run**: Compatible with minimal changes
305 | - **Azure Container Instances**: Direct deployment support
306 | - **DigitalOcean App Platform**: Git-based deployment
307 | 
308 | ## 📚 Additional Resources
309 | 
310 | - [Docker Best Practices](https://docs.docker.com/develop/dev-best-practices/)
311 | - [Multi-stage Builds](https://docs.docker.com/develop/dev-best-practices/dockerfile_best-practices/#use-multi-stage-builds)
312 | - [Docker Compose Reference](https://docs.docker.com/compose/compose-file/)
313 | - [Container Security](https://docs.docker.com/engine/security/)
314 | 
315 | ---
316 | 
317 | **Need help?** Open an issue on [GitHub](https://github.com/sammcj/bybit-mcp/issues) 🚀
318 | 
```

--------------------------------------------------------------------------------
/webui/src/utils/formatters.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Utility functions for formatting data
  3 |  */
  4 | 
  5 | /**
  6 |  * Format a number as currency
  7 |  */
  8 | export function formatCurrency(value: number, currency: string = 'USD', decimals?: number): string {
  9 |   const options: Intl.NumberFormatOptions = {
 10 |     style: 'currency',
 11 |     currency,
 12 |   };
 13 | 
 14 |   if (decimals !== undefined) {
 15 |     options.minimumFractionDigits = decimals;
 16 |     options.maximumFractionDigits = decimals;
 17 |   }
 18 | 
 19 |   return new Intl.NumberFormat('en-US', options).format(value);
 20 | }
 21 | 
 22 | /**
 23 |  * Format a number with appropriate decimal places
 24 |  */
 25 | export function formatNumber(value: number, decimals: number = 2): string {
 26 |   return new Intl.NumberFormat('en-US', {
 27 |     minimumFractionDigits: decimals,
 28 |     maximumFractionDigits: decimals,
 29 |   }).format(value);
 30 | }
 31 | 
 32 | /**
 33 |  * Format a large number with K, M, B suffixes
 34 |  */
 35 | export function formatLargeNumber(value: number): string {
 36 |   const absValue = Math.abs(value);
 37 |   
 38 |   if (absValue >= 1e9) {
 39 |     return formatNumber(value / 1e9, 2) + 'B';
 40 |   } else if (absValue >= 1e6) {
 41 |     return formatNumber(value / 1e6, 2) + 'M';
 42 |   } else if (absValue >= 1e3) {
 43 |     return formatNumber(value / 1e3, 2) + 'K';
 44 |   }
 45 |   
 46 |   return formatNumber(value, 2);
 47 | }
 48 | 
 49 | /**
 50 |  * Format a percentage
 51 |  */
 52 | export function formatPercentage(value: number, decimals: number = 2): string {
 53 |   return new Intl.NumberFormat('en-US', {
 54 |     style: 'percent',
 55 |     minimumFractionDigits: decimals,
 56 |     maximumFractionDigits: decimals,
 57 |   }).format(value / 100);
 58 | }
 59 | 
 60 | /**
 61 |  * Format a timestamp
 62 |  */
 63 | export function formatTimestamp(timestamp: number, options?: Intl.DateTimeFormatOptions): string {
 64 |   const defaultOptions: Intl.DateTimeFormatOptions = {
 65 |     year: 'numeric',
 66 |     month: 'short',
 67 |     day: 'numeric',
 68 |     hour: '2-digit',
 69 |     minute: '2-digit',
 70 |     second: '2-digit',
 71 |   };
 72 | 
 73 |   return new Intl.DateTimeFormat('en-US', { ...defaultOptions, ...options }).format(new Date(timestamp));
 74 | }
 75 | 
 76 | /**
 77 |  * Format a relative time (e.g., "2 minutes ago")
 78 |  */
 79 | export function formatRelativeTime(timestamp: number): string {
 80 |   const now = Date.now();
 81 |   const diff = now - timestamp;
 82 |   
 83 |   const seconds = Math.floor(diff / 1000);
 84 |   const minutes = Math.floor(seconds / 60);
 85 |   const hours = Math.floor(minutes / 60);
 86 |   const days = Math.floor(hours / 24);
 87 |   
 88 |   if (days > 0) {
 89 |     return `${days} day${days > 1 ? 's' : ''} ago`;
 90 |   } else if (hours > 0) {
 91 |     return `${hours} hour${hours > 1 ? 's' : ''} ago`;
 92 |   } else if (minutes > 0) {
 93 |     return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
 94 |   } else {
 95 |     return 'Just now';
 96 |   }
 97 | }
 98 | 
 99 | /**
100 |  * Format a trading symbol for display
101 |  */
102 | export function formatSymbol(symbol: string): string {
103 |   // Convert BTCUSDT to BTC/USDT
104 |   const commonQuotes = ['USDT', 'USDC', 'BTC', 'ETH', 'BNB'];
105 |   
106 |   for (const quote of commonQuotes) {
107 |     if (symbol.endsWith(quote)) {
108 |       const base = symbol.slice(0, -quote.length);
109 |       return `${base}/${quote}`;
110 |     }
111 |   }
112 |   
113 |   return symbol;
114 | }
115 | 
116 | /**
117 |  * Format price change with color indication
118 |  */
119 | export function formatPriceChange(change: number, isPercentage: boolean = false): {
120 |   formatted: string;
121 |   className: string;
122 |   icon: string;
123 | } {
124 |   const isPositive = change > 0;
125 |   const isNegative = change < 0;
126 |   
127 |   let formatted: string;
128 |   if (isPercentage) {
129 |     formatted = formatPercentage(Math.abs(change));
130 |   } else {
131 |     formatted = formatNumber(Math.abs(change));
132 |   }
133 |   
134 |   const className = isPositive ? 'text-success' : isNegative ? 'text-danger' : 'text-secondary';
135 |   const icon = isPositive ? '↗' : isNegative ? '↘' : '→';
136 |   
137 |   return {
138 |     formatted: `${isPositive ? '+' : isNegative ? '-' : ''}${formatted}`,
139 |     className,
140 |     icon,
141 |   };
142 | }
143 | 
144 | /**
145 |  * Format volume with appropriate units
146 |  */
147 | export function formatVolume(volume: number): string {
148 |   return formatLargeNumber(volume);
149 | }
150 | 
151 | /**
152 |  * Format market cap
153 |  */
154 | export function formatMarketCap(marketCap: number): string {
155 |   return formatLargeNumber(marketCap);
156 | }
157 | 
158 | /**
159 |  * Format order book price levels
160 |  */
161 | export function formatOrderBookLevel(price: string, size: string): {
162 |   price: string;
163 |   size: string;
164 |   total: string;
165 | } {
166 |   const priceNum = parseFloat(price);
167 |   const sizeNum = parseFloat(size);
168 |   const total = priceNum * sizeNum;
169 |   
170 |   return {
171 |     price: formatNumber(priceNum, 4),
172 |     size: formatNumber(sizeNum, 6),
173 |     total: formatNumber(total, 2),
174 |   };
175 | }
176 | 
177 | /**
178 |  * Format RSI value with overbought/oversold indication
179 |  */
180 | export function formatRSI(rsi: number): {
181 |   formatted: string;
182 |   className: string;
183 |   status: 'overbought' | 'oversold' | 'neutral';
184 | } {
185 |   const formatted = formatNumber(rsi, 2);
186 |   
187 |   let className: string;
188 |   let status: 'overbought' | 'oversold' | 'neutral';
189 |   
190 |   if (rsi >= 70) {
191 |     className = 'text-danger';
192 |     status = 'overbought';
193 |   } else if (rsi <= 30) {
194 |     className = 'text-success';
195 |     status = 'oversold';
196 |   } else {
197 |     className = 'text-secondary';
198 |     status = 'neutral';
199 |   }
200 |   
201 |   return { formatted, className, status };
202 | }
203 | 
204 | /**
205 |  * Format file size
206 |  */
207 | export function formatFileSize(bytes: number): string {
208 |   const units = ['B', 'KB', 'MB', 'GB', 'TB'];
209 |   let size = bytes;
210 |   let unitIndex = 0;
211 |   
212 |   while (size >= 1024 && unitIndex < units.length - 1) {
213 |     size /= 1024;
214 |     unitIndex++;
215 |   }
216 |   
217 |   return `${formatNumber(size, unitIndex === 0 ? 0 : 2)} ${units[unitIndex]}`;
218 | }
219 | 
220 | /**
221 |  * Truncate text with ellipsis
222 |  */
223 | export function truncateText(text: string, maxLength: number): string {
224 |   if (text.length <= maxLength) {
225 |     return text;
226 |   }
227 |   
228 |   return text.slice(0, maxLength - 3) + '...';
229 | }
230 | 
231 | /**
232 |  * Format duration in milliseconds to human readable
233 |  */
234 | export function formatDuration(ms: number): string {
235 |   if (ms < 1000) {
236 |     return `${ms}ms`;
237 |   }
238 |   
239 |   const seconds = ms / 1000;
240 |   if (seconds < 60) {
241 |     return `${formatNumber(seconds, 1)}s`;
242 |   }
243 |   
244 |   const minutes = seconds / 60;
245 |   if (minutes < 60) {
246 |     return `${formatNumber(minutes, 1)}m`;
247 |   }
248 |   
249 |   const hours = minutes / 60;
250 |   return `${formatNumber(hours, 1)}h`;
251 | }
252 | 
253 | /**
254 |  * Format confidence score
255 |  */
256 | export function formatConfidence(confidence: number): {
257 |   formatted: string;
258 |   className: string;
259 |   level: 'high' | 'medium' | 'low';
260 | } {
261 |   const formatted = formatPercentage(confidence);
262 |   
263 |   let className: string;
264 |   let level: 'high' | 'medium' | 'low';
265 |   
266 |   if (confidence >= 80) {
267 |     className = 'text-success';
268 |     level = 'high';
269 |   } else if (confidence >= 60) {
270 |     className = 'text-warning';
271 |     level = 'medium';
272 |   } else {
273 |     className = 'text-danger';
274 |     level = 'low';
275 |   }
276 |   
277 |   return { formatted, className, level };
278 | }
279 | 
```

--------------------------------------------------------------------------------
/webui/src/services/configService.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Configuration service for managing application settings
  3 |  */
  4 | 
  5 | import type { ChatSettings, AIConfig } from '@/types/ai';
  6 | import { systemPromptService } from './systemPrompt';
  7 | 
  8 | const STORAGE_KEY = 'bybit-mcp-webui-settings';
  9 | 
 10 | // Get environment-based defaults
 11 | function getDefaultSettings(): ChatSettings {
 12 |   // Check for environment variables (available in build-time or runtime)
 13 |   const ollamaHost = (typeof window !== 'undefined' && (window as any).__OLLAMA_HOST__) ||
 14 |                     (typeof process !== 'undefined' && process.env?.OLLAMA_HOST) ||
 15 |                     'http://localhost:11434';
 16 | 
 17 |   const mcpEndpoint = (typeof window !== 'undefined' && (window as any).__MCP_ENDPOINT__) ||
 18 |                       (typeof process !== 'undefined' && process.env?.MCP_ENDPOINT) ||
 19 |                       ''; // Empty means use current origin in production
 20 | 
 21 |   return {
 22 |     ai: {
 23 |       endpoint: ollamaHost,
 24 |       model: 'qwen3-30b-a3b-ud-nothink-128k:q4_k_xl',
 25 |       temperature: 0.7,
 26 |       maxTokens: 2048,
 27 |       systemPrompt: systemPromptService.generateLegacySystemPrompt(),
 28 |     },
 29 |     mcp: {
 30 |       endpoint: mcpEndpoint,
 31 |       timeout: 30000,
 32 |     },
 33 |     ui: {
 34 |       theme: 'auto',
 35 |       fontSize: 'medium',
 36 |       showTimestamps: true,
 37 |       enableSounds: false,
 38 |     },
 39 |   };
 40 | }
 41 | 
 42 | const DEFAULT_SETTINGS: ChatSettings = getDefaultSettings();
 43 | 
 44 | export class ConfigService {
 45 |   private settings: ChatSettings;
 46 |   private listeners: Set<(settings: ChatSettings) => void> = new Set();
 47 | 
 48 |   constructor() {
 49 |     this.settings = this.loadSettings();
 50 |     this.applyTheme();
 51 |   }
 52 | 
 53 |   /**
 54 |    * Get current settings
 55 |    */
 56 |   getSettings(): ChatSettings {
 57 |     return { ...this.settings };
 58 |   }
 59 | 
 60 |   /**
 61 |    * Update settings
 62 |    */
 63 |   updateSettings(updates: Partial<ChatSettings>): void {
 64 |     this.settings = this.mergeSettings(this.settings, updates);
 65 |     this.saveSettings();
 66 |     this.applyTheme();
 67 |     this.notifyListeners();
 68 |   }
 69 | 
 70 |   /**
 71 |    * Reset settings to defaults
 72 |    */
 73 |   resetSettings(): void {
 74 |     this.settings = { ...DEFAULT_SETTINGS };
 75 |     this.saveSettings();
 76 |     this.applyTheme();
 77 |     this.notifyListeners();
 78 |   }
 79 | 
 80 |   /**
 81 |    * Get AI configuration
 82 |    */
 83 |   getAIConfig(): AIConfig {
 84 |     return { ...this.settings.ai };
 85 |   }
 86 | 
 87 |   /**
 88 |    * Update AI configuration
 89 |    */
 90 |   updateAIConfig(config: Partial<AIConfig>): void {
 91 |     this.updateSettings({
 92 |       ai: { ...this.settings.ai, ...config },
 93 |     });
 94 |   }
 95 | 
 96 |   /**
 97 |    * Get MCP configuration
 98 |    */
 99 |   getMCPConfig(): { endpoint: string; timeout: number } {
100 |     return { ...this.settings.mcp };
101 |   }
102 | 
103 |   /**
104 |    * Update MCP configuration
105 |    */
106 |   updateMCPConfig(config: Partial<{ endpoint: string; timeout: number }>): void {
107 |     this.updateSettings({
108 |       mcp: { ...this.settings.mcp, ...config },
109 |     });
110 |   }
111 | 
112 |   /**
113 |    * Subscribe to settings changes
114 |    */
115 |   subscribe(listener: (settings: ChatSettings) => void): () => void {
116 |     this.listeners.add(listener);
117 |     return () => this.listeners.delete(listener);
118 |   }
119 | 
120 |   /**
121 |    * Apply theme to document
122 |    */
123 |   private applyTheme(): void {
124 |     const { theme } = this.settings.ui;
125 |     const root = document.documentElement;
126 | 
127 |     if (theme === 'auto') {
128 |       // Use system preference
129 |       const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
130 |       root.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
131 |     } else {
132 |       root.setAttribute('data-theme', theme);
133 |     }
134 | 
135 |     // Apply font size
136 |     const { fontSize } = this.settings.ui;
137 |     root.setAttribute('data-font-size', fontSize);
138 |   }
139 | 
140 |   /**
141 |    * Load settings from localStorage
142 |    */
143 |   private loadSettings(): ChatSettings {
144 |     try {
145 |       const stored = localStorage.getItem(STORAGE_KEY);
146 |       if (stored) {
147 |         const parsed = JSON.parse(stored);
148 |         const merged = this.mergeSettings(DEFAULT_SETTINGS, parsed);
149 | 
150 |         // Fix legacy localhost URLs when running in production
151 |         if (typeof window !== 'undefined' && window.location.hostname !== 'localhost') {
152 |           if (merged.mcp.endpoint === 'http://localhost:8080' || merged.mcp.endpoint.includes('localhost')) {
153 |             console.log('🔧 Detected legacy localhost MCP endpoint, resetting to auto-detect');
154 |             merged.mcp.endpoint = ''; // Reset to auto-detect current origin
155 |           }
156 |         }
157 | 
158 |         return merged;
159 |       }
160 |     } catch (error) {
161 |       console.warn('Failed to load settings from localStorage:', error);
162 |     }
163 | 
164 |     return { ...DEFAULT_SETTINGS };
165 |   }
166 | 
167 |   /**
168 |    * Save settings to localStorage
169 |    */
170 |   private saveSettings(): void {
171 |     try {
172 |       localStorage.setItem(STORAGE_KEY, JSON.stringify(this.settings));
173 |     } catch (error) {
174 |       console.warn('Failed to save settings to localStorage:', error);
175 |     }
176 |   }
177 | 
178 |   /**
179 |    * Deep merge settings objects
180 |    */
181 |   private mergeSettings(base: ChatSettings, updates: Partial<ChatSettings>): ChatSettings {
182 |     return {
183 |       ai: { ...base.ai, ...updates.ai },
184 |       mcp: { ...base.mcp, ...updates.mcp },
185 |       ui: { ...base.ui, ...updates.ui },
186 |     };
187 |   }
188 | 
189 |   /**
190 |    * Notify all listeners of settings changes
191 |    */
192 |   private notifyListeners(): void {
193 |     this.listeners.forEach(listener => {
194 |       try {
195 |         listener(this.settings);
196 |       } catch (error) {
197 |         console.error('Error in settings listener:', error);
198 |       }
199 |     });
200 |   }
201 | }
202 | 
203 | // Singleton instance
204 | export const configService = new ConfigService();
205 | 
206 | // Listen for system theme changes
207 | if (typeof window !== 'undefined') {
208 |   window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
209 |     const settings = configService.getSettings();
210 |     if (settings.ui.theme === 'auto') {
211 |       configService.updateSettings({}); // Trigger theme reapplication
212 |     }
213 |   });
214 | }
215 | 
216 | // Export convenience functions
217 | export function getAIConfig(): AIConfig {
218 |   return configService.getAIConfig();
219 | }
220 | 
221 | export function getMCPConfig(): { endpoint: string; timeout: number } {
222 |   return configService.getMCPConfig();
223 | }
224 | 
225 | export function updateAIConfig(config: Partial<AIConfig>): void {
226 |   configService.updateAIConfig(config);
227 | }
228 | 
229 | export function updateMCPConfig(config: Partial<{ endpoint: string; timeout: number }>): void {
230 |   configService.updateMCPConfig(config);
231 | }
232 | 
233 | export function toggleTheme(): void {
234 |   const settings = configService.getSettings();
235 |   const currentTheme = settings.ui.theme;
236 | 
237 |   let newTheme: 'light' | 'dark' | 'auto';
238 |   if (currentTheme === 'light') {
239 |     newTheme = 'dark';
240 |   } else if (currentTheme === 'dark') {
241 |     newTheme = 'auto';
242 |   } else {
243 |     newTheme = 'light';
244 |   }
245 | 
246 |   configService.updateSettings({
247 |     ui: { ...settings.ui, theme: newTheme },
248 |   });
249 | }
250 | 
```

--------------------------------------------------------------------------------
/webui/src/components/DebugConsole.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Debug Console Component - Real-time streaming log viewer
  3 |  */
  4 | 
  5 | import { logService, type LogEntry } from '../services/logService';
  6 | 
  7 | export class DebugConsole {
  8 |   private container: HTMLElement;
  9 |   private isVisible: boolean = false;
 10 |   private autoScroll: boolean = true;
 11 |   private filterLevels: Set<LogEntry['level']> = new Set(['log', 'info', 'warn', 'error']);
 12 |   private unsubscribe?: () => void;
 13 | 
 14 |   constructor(container: HTMLElement) {
 15 |     this.container = container;
 16 |     this.render();
 17 |     this.setupEventListeners();
 18 |     
 19 |     // Subscribe to log updates
 20 |     this.unsubscribe = logService.subscribe((logs) => {
 21 |       this.updateLogs(logs);
 22 |     });
 23 |   }
 24 | 
 25 |   private render(): void {
 26 |     this.container.innerHTML = `
 27 |       <div class="debug-console ${this.isVisible ? 'visible' : 'hidden'}">
 28 |         <div class="debug-header">
 29 |           <div class="debug-title">
 30 |             <span class="debug-icon">🔍</span>
 31 |             <span>Debug Console</span>
 32 |             <span class="debug-count">(${logService.getLogs().length})</span>
 33 |           </div>
 34 |           <div class="debug-controls">
 35 |             <div class="debug-filters">
 36 |               <label><input type="checkbox" data-level="log" ${this.filterLevels.has('log') ? 'checked' : ''}> Log</label>
 37 |               <label><input type="checkbox" data-level="info" ${this.filterLevels.has('info') ? 'checked' : ''}> Info</label>
 38 |               <label><input type="checkbox" data-level="warn" ${this.filterLevels.has('warn') ? 'checked' : ''}> Warn</label>
 39 |               <label><input type="checkbox" data-level="error" ${this.filterLevels.has('error') ? 'checked' : ''}> Error</label>
 40 |             </div>
 41 |             <button class="debug-btn" data-action="clear">Clear</button>
 42 |             <button class="debug-btn" data-action="export">Export</button>
 43 |             <button class="debug-btn" data-action="scroll-toggle">
 44 |               ${this.autoScroll ? '📌' : '📌'}
 45 |             </button>
 46 |             <button class="debug-btn debug-toggle" data-action="toggle">
 47 |               ${this.isVisible ? '▼' : '▲'}
 48 |             </button>
 49 |           </div>
 50 |         </div>
 51 |         <div class="debug-content">
 52 |           <div class="debug-logs" id="debug-logs"></div>
 53 |         </div>
 54 |       </div>
 55 |     `;
 56 | 
 57 |     this.updateLogs(logService.getLogs());
 58 |   }
 59 | 
 60 |   private setupEventListeners(): void {
 61 |     this.container.addEventListener('click', (e) => {
 62 |       const target = e.target as HTMLElement;
 63 |       const action = target.getAttribute('data-action');
 64 | 
 65 |       switch (action) {
 66 |         case 'toggle':
 67 |           this.toggle();
 68 |           break;
 69 |         case 'clear':
 70 |           logService.clearLogs();
 71 |           break;
 72 |         case 'export':
 73 |           this.exportLogs();
 74 |           break;
 75 |         case 'scroll-toggle':
 76 |           this.autoScroll = !this.autoScroll;
 77 |           target.textContent = this.autoScroll ? '📌' : '📌';
 78 |           target.title = this.autoScroll ? 'Auto-scroll enabled' : 'Auto-scroll disabled';
 79 |           break;
 80 |       }
 81 |     });
 82 | 
 83 |     this.container.addEventListener('change', (e) => {
 84 |       const target = e.target as HTMLInputElement;
 85 |       const level = target.getAttribute('data-level') as LogEntry['level'];
 86 |       
 87 |       if (level) {
 88 |         if (target.checked) {
 89 |           this.filterLevels.add(level);
 90 |         } else {
 91 |           this.filterLevels.delete(level);
 92 |         }
 93 |         this.updateLogs(logService.getLogs());
 94 |       }
 95 |     });
 96 |   }
 97 | 
 98 |   private updateLogs(logs: LogEntry[]): void {
 99 |     const logsContainer = this.container.querySelector('#debug-logs') as HTMLElement;
100 |     if (!logsContainer) return;
101 | 
102 |     // Filter logs by selected levels
103 |     const filteredLogs = logs.filter(log => this.filterLevels.has(log.level));
104 | 
105 |     // Update count
106 |     const countElement = this.container.querySelector('.debug-count') as HTMLElement;
107 |     if (countElement) {
108 |       countElement.textContent = `(${filteredLogs.length}/${logs.length})`;
109 |     }
110 | 
111 |     // Render logs
112 |     logsContainer.innerHTML = filteredLogs.map(log => this.renderLogEntry(log)).join('');
113 | 
114 |     // Auto-scroll to bottom
115 |     if (this.autoScroll && this.isVisible) {
116 |       logsContainer.scrollTop = logsContainer.scrollHeight;
117 |     }
118 |   }
119 | 
120 |   private renderLogEntry(log: LogEntry): string {
121 |     const time = new Date(log.timestamp).toLocaleTimeString();
122 |     const levelClass = `debug-log-${log.level}`;
123 |     const source = log.source ? ` <span class="debug-source">[${log.source}]</span>` : '';
124 |     
125 |     let dataHtml = '';
126 |     if (log.data) {
127 |       const dataStr = typeof log.data === 'object' 
128 |         ? JSON.stringify(log.data, null, 2) 
129 |         : String(log.data);
130 |       dataHtml = `<div class="debug-data">${this.escapeHtml(dataStr)}</div>`;
131 |     }
132 | 
133 |     return `
134 |       <div class="debug-log-entry ${levelClass}">
135 |         <div class="debug-log-header">
136 |           <span class="debug-time">${time}</span>
137 |           <span class="debug-level">${log.level.toUpperCase()}</span>
138 |           ${source}
139 |         </div>
140 |         <div class="debug-message">${this.escapeHtml(log.message)}</div>
141 |         ${dataHtml}
142 |       </div>
143 |     `;
144 |   }
145 | 
146 |   private escapeHtml(text: string): string {
147 |     const div = document.createElement('div');
148 |     div.textContent = text;
149 |     return div.innerHTML;
150 |   }
151 | 
152 |   private exportLogs(): void {
153 |     const logs = logService.exportLogs();
154 |     const blob = new Blob([logs], { type: 'text/plain' });
155 |     const url = URL.createObjectURL(blob);
156 |     
157 |     const a = document.createElement('a');
158 |     a.href = url;
159 |     a.download = `debug-logs-${new Date().toISOString().slice(0, 19)}.txt`;
160 |     document.body.appendChild(a);
161 |     a.click();
162 |     document.body.removeChild(a);
163 |     URL.revokeObjectURL(url);
164 |   }
165 | 
166 |   public toggle(): void {
167 |     this.isVisible = !this.isVisible;
168 |     const debugConsole = this.container.querySelector('.debug-console') as HTMLElement;
169 |     const toggleBtn = this.container.querySelector('.debug-toggle') as HTMLElement;
170 |     
171 |     if (debugConsole) {
172 |       debugConsole.className = `debug-console ${this.isVisible ? 'visible' : 'hidden'}`;
173 |     }
174 |     
175 |     if (toggleBtn) {
176 |       toggleBtn.textContent = this.isVisible ? '▼' : '▲';
177 |     }
178 | 
179 |     // Auto-scroll when opening
180 |     if (this.isVisible && this.autoScroll) {
181 |       setTimeout(() => {
182 |         const logsContainer = this.container.querySelector('#debug-logs') as HTMLElement;
183 |         if (logsContainer) {
184 |           logsContainer.scrollTop = logsContainer.scrollHeight;
185 |         }
186 |       }, 100);
187 |     }
188 |   }
189 | 
190 |   public show(): void {
191 |     if (!this.isVisible) {
192 |       this.toggle();
193 |     }
194 |   }
195 | 
196 |   public hide(): void {
197 |     if (this.isVisible) {
198 |       this.toggle();
199 |     }
200 |   }
201 | 
202 |   public destroy(): void {
203 |     if (this.unsubscribe) {
204 |       this.unsubscribe();
205 |     }
206 |   }
207 | }
208 | 
```

--------------------------------------------------------------------------------
/webui/src/services/agentConfig.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Agent configuration service for managing agent settings
  3 |  */
  4 | 
  5 | import type {
  6 |   AgentConfig,
  7 |   AgentState
  8 | } from '@/types/agent';
  9 | import { DEFAULT_AGENT_CONFIG } from '@/types/agent';
 10 | 
 11 | export class AgentConfigService {
 12 |   private static readonly STORAGE_KEY = 'bybit-mcp-agent-config';
 13 |   private static readonly STATE_KEY = 'bybit-mcp-agent-state';
 14 | 
 15 |   private config: AgentConfig;
 16 |   private state: AgentState;
 17 |   private listeners: Set<(config: AgentConfig) => void> = new Set();
 18 | 
 19 |   constructor() {
 20 |     this.config = this.loadConfig();
 21 |     this.state = this.loadState();
 22 |   }
 23 | 
 24 |   /**
 25 |    * Get current agent configuration
 26 |    */
 27 |   getConfig(): AgentConfig {
 28 |     return { ...this.config };
 29 |   }
 30 | 
 31 |   /**
 32 |    * Update agent configuration
 33 |    */
 34 |   updateConfig(updates: Partial<AgentConfig>): void {
 35 |     this.config = { ...this.config, ...updates };
 36 |     this.saveConfig();
 37 |     this.notifyListeners();
 38 |   }
 39 | 
 40 |   /**
 41 |    * Reset configuration to defaults
 42 |    */
 43 |   resetConfig(): void {
 44 |     this.config = { ...DEFAULT_AGENT_CONFIG };
 45 |     this.saveConfig();
 46 |     this.notifyListeners();
 47 |   }
 48 | 
 49 |   /**
 50 |    * Apply a simple preset configuration
 51 |    */
 52 |   applyPreset(presetName: 'quick' | 'standard' | 'comprehensive'): void {
 53 |     const updates: Partial<AgentConfig> = {};
 54 | 
 55 |     switch (presetName) {
 56 |       case 'quick':
 57 |         updates.maxIterations = 2;
 58 |         updates.showWorkflowSteps = false;
 59 |         updates.showToolCalls = false;
 60 |         break;
 61 |       case 'standard':
 62 |         updates.maxIterations = 5;
 63 |         updates.showWorkflowSteps = false;
 64 |         updates.showToolCalls = false;
 65 |         break;
 66 |       case 'comprehensive':
 67 |         updates.maxIterations = 8;
 68 |         updates.showWorkflowSteps = true;
 69 |         updates.showToolCalls = true;
 70 |         break;
 71 |     }
 72 | 
 73 |     this.updateConfig(updates);
 74 |   }
 75 | 
 76 |   /**
 77 |    * Get current agent state
 78 |    */
 79 |   getState(): AgentState {
 80 |     return { ...this.state };
 81 |   }
 82 | 
 83 |   /**
 84 |    * Update agent state
 85 |    */
 86 |   updateState(updates: Partial<AgentState>): void {
 87 |     this.state = { ...this.state, ...updates };
 88 |     this.saveState();
 89 |   }
 90 | 
 91 |   /**
 92 |    * Record a successful query
 93 |    */
 94 |   recordQuery(responseTime: number, _toolCallsCount: number): void {
 95 |     const currentState = this.getState();
 96 |     const queryCount = currentState.queryCount + 1;
 97 |     const averageResponseTime =
 98 |       (currentState.averageResponseTime * currentState.queryCount + responseTime) / queryCount;
 99 | 
100 |     this.updateState({
101 |       queryCount,
102 |       averageResponseTime,
103 |       successRate: (currentState.successRate * currentState.queryCount + 1) / queryCount,
104 |       lastQuery: undefined,
105 |       lastResponse: undefined
106 |     });
107 |   }
108 | 
109 |   /**
110 |    * Record a failed query
111 |    */
112 |   recordFailure(): void {
113 |     const currentState = this.getState();
114 |     const queryCount = currentState.queryCount + 1;
115 | 
116 |     this.updateState({
117 |       queryCount,
118 |       successRate: (currentState.successRate * currentState.queryCount) / queryCount
119 |     });
120 |   }
121 | 
122 |   /**
123 |    * Subscribe to configuration changes
124 |    */
125 |   subscribe(listener: (config: AgentConfig) => void): () => void {
126 |     this.listeners.add(listener);
127 |     return () => this.listeners.delete(listener);
128 |   }
129 | 
130 |   /**
131 |    * Get configuration for specific analysis type
132 |    */
133 |   getConfigForAnalysis(analysisType: 'quick' | 'standard' | 'comprehensive'): AgentConfig {
134 |     const baseConfig = this.getConfig();
135 | 
136 |     switch (analysisType) {
137 |       case 'quick':
138 |         return {
139 |           ...baseConfig,
140 |           maxIterations: 2,
141 |           showWorkflowSteps: false,
142 |           showToolCalls: false
143 |         };
144 | 
145 |       case 'standard':
146 |         return {
147 |           ...baseConfig,
148 |           maxIterations: 5,
149 |           showWorkflowSteps: false,
150 |           showToolCalls: false
151 |         };
152 | 
153 |       case 'comprehensive':
154 |         return {
155 |           ...baseConfig,
156 |           maxIterations: 8,
157 |           showWorkflowSteps: true,
158 |           showToolCalls: true
159 |         };
160 | 
161 |       default:
162 |         return baseConfig;
163 |     }
164 |   }
165 | 
166 |   /**
167 |    * Validate configuration
168 |    */
169 |   validateConfig(config: Partial<AgentConfig>): string[] {
170 |     const errors: string[] = [];
171 | 
172 |     if (config.maxIterations !== undefined) {
173 |       if (config.maxIterations < 1 || config.maxIterations > 20) {
174 |         errors.push('Max iterations must be between 1 and 20');
175 |       }
176 |     }
177 | 
178 |     if (config.toolTimeout !== undefined) {
179 |       if (config.toolTimeout < 5000 || config.toolTimeout > 120000) {
180 |         errors.push('Tool timeout must be between 5 and 120 seconds');
181 |       }
182 |     }
183 | 
184 |     return errors;
185 |   }
186 | 
187 |   /**
188 |    * Export configuration
189 |    */
190 |   exportConfig(): string {
191 |     return JSON.stringify({
192 |       config: this.config,
193 |       state: this.state,
194 |       exportedAt: new Date().toISOString()
195 |     }, null, 2);
196 |   }
197 | 
198 |   /**
199 |    * Import configuration
200 |    */
201 |   importConfig(configJson: string): void {
202 |     try {
203 |       const imported = JSON.parse(configJson);
204 |       if (imported.config) {
205 |         const errors = this.validateConfig(imported.config);
206 |         if (errors.length > 0) {
207 |           throw new Error(`Invalid configuration: ${errors.join(', ')}`);
208 |         }
209 |         this.config = { ...DEFAULT_AGENT_CONFIG, ...imported.config };
210 |         this.saveConfig();
211 |         this.notifyListeners();
212 |       }
213 |     } catch (error) {
214 |       throw new Error(`Failed to import configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
215 |     }
216 |   }
217 | 
218 |   // Private methods
219 |   private loadConfig(): AgentConfig {
220 |     try {
221 |       const stored = localStorage.getItem(AgentConfigService.STORAGE_KEY);
222 |       if (stored) {
223 |         const parsed = JSON.parse(stored);
224 |         return { ...DEFAULT_AGENT_CONFIG, ...parsed };
225 |       }
226 |     } catch (error) {
227 |       console.warn('Failed to load agent config from localStorage:', error);
228 |     }
229 |     return { ...DEFAULT_AGENT_CONFIG };
230 |   }
231 | 
232 |   private saveConfig(): void {
233 |     try {
234 |       localStorage.setItem(AgentConfigService.STORAGE_KEY, JSON.stringify(this.config));
235 |     } catch (error) {
236 |       console.warn('Failed to save agent config to localStorage:', error);
237 |     }
238 |   }
239 | 
240 |   private loadState(): AgentState {
241 |     try {
242 |       const stored = localStorage.getItem(AgentConfigService.STATE_KEY);
243 |       if (stored) {
244 |         return JSON.parse(stored);
245 |       }
246 |     } catch (error) {
247 |       console.warn('Failed to load agent state from localStorage:', error);
248 |     }
249 |     return {
250 |       isProcessing: false,
251 |       queryCount: 0,
252 |       averageResponseTime: 0,
253 |       successRate: 0
254 |     };
255 |   }
256 | 
257 |   private saveState(): void {
258 |     try {
259 |       localStorage.setItem(AgentConfigService.STATE_KEY, JSON.stringify(this.state));
260 |     } catch (error) {
261 |       console.warn('Failed to save agent state to localStorage:', error);
262 |     }
263 |   }
264 | 
265 |   // Removed metrics-related methods - now using simplified state tracking
266 | 
267 |   private notifyListeners(): void {
268 |     this.listeners.forEach(listener => listener(this.config));
269 |   }
270 | }
271 | 
272 | // Singleton instance
273 | export const agentConfigService = new AgentConfigService();
274 | 
```

--------------------------------------------------------------------------------
/webui/build-docker.sh:
--------------------------------------------------------------------------------

```bash
  1 | #!/bin/bash
  2 | 
  3 | # ==============================================================================
  4 | # Bybit MCP WebUI - Docker Build Script
  5 | # ==============================================================================
  6 | # This script provides convenient commands for building and managing
  7 | # the Docker container for the Bybit MCP WebUI.
  8 | # ==============================================================================
  9 | 
 10 | set -e
 11 | 
 12 | # Colors for output
 13 | RED='\033[0;31m'
 14 | GREEN='\033[0;32m'
 15 | YELLOW='\033[1;33m'
 16 | BLUE='\033[0;34m'
 17 | NC='\033[0m' # No Color
 18 | 
 19 | # Configuration
 20 | IMAGE_NAME="bybit-mcp-webui"
 21 | CONTAINER_NAME="bybit-mcp-webui"
 22 | PORT="8080"
 23 | 
 24 | # Functions
 25 | print_header() {
 26 |     echo -e "${BLUE}==============================================================================${NC}"
 27 |     echo -e "${BLUE} $1${NC}"
 28 |     echo -e "${BLUE}==============================================================================${NC}"
 29 | }
 30 | 
 31 | print_success() {
 32 |     echo -e "${GREEN}✅ $1${NC}"
 33 | }
 34 | 
 35 | print_warning() {
 36 |     echo -e "${YELLOW}⚠️  $1${NC}"
 37 | }
 38 | 
 39 | print_error() {
 40 |     echo -e "${RED}❌ $1${NC}"
 41 | }
 42 | 
 43 | print_info() {
 44 |     echo -e "${BLUE}ℹ️  $1${NC}"
 45 | }
 46 | 
 47 | # Check if Docker is running
 48 | check_docker() {
 49 |     if ! docker info > /dev/null 2>&1; then
 50 |         print_error "Docker is not running. Please start Docker and try again."
 51 |         exit 1
 52 |     fi
 53 | }
 54 | 
 55 | # Build the Docker image
 56 | build_image() {
 57 |     print_header "Building Docker Image"
 58 | 
 59 |     print_info "Building $IMAGE_NAME with Node.js 22..."
 60 |     docker build -t "$IMAGE_NAME:latest" -f Dockerfile ..
 61 | 
 62 |     print_success "Docker image built successfully!"
 63 |     docker images | grep "$IMAGE_NAME"
 64 | 
 65 |     # Show image size
 66 |     print_info "Image size:"
 67 |     docker images "$IMAGE_NAME:latest" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
 68 | }
 69 | 
 70 | # Run the container
 71 | run_container() {
 72 |     print_header "Running Container"
 73 | 
 74 |     # Stop existing container if running
 75 |     if docker ps -q -f name="$CONTAINER_NAME" | grep -q .; then
 76 |         print_warning "Stopping existing container..."
 77 |         docker stop "$CONTAINER_NAME"
 78 |         docker rm "$CONTAINER_NAME"
 79 |     fi
 80 | 
 81 |     print_info "Starting new container..."
 82 |     docker run -d \
 83 |         --name "$CONTAINER_NAME" \
 84 |         -p "$PORT:8080" \
 85 |         --restart unless-stopped \
 86 |         "$IMAGE_NAME:latest"
 87 | 
 88 |     print_success "Container started successfully!"
 89 |     print_info "Access the WebUI at: http://localhost:$PORT"
 90 | }
 91 | 
 92 | # Stop the container
 93 | stop_container() {
 94 |     print_header "Stopping Container"
 95 | 
 96 |     if docker ps -q -f name="$CONTAINER_NAME" | grep -q .; then
 97 |         docker stop "$CONTAINER_NAME"
 98 |         docker rm "$CONTAINER_NAME"
 99 |         print_success "Container stopped and removed."
100 |     else
101 |         print_warning "No running container found."
102 |     fi
103 | }
104 | 
105 | # Show container logs
106 | show_logs() {
107 |     print_header "Container Logs"
108 | 
109 |     if docker ps -q -f name="$CONTAINER_NAME" | grep -q .; then
110 |         docker logs -f "$CONTAINER_NAME"
111 |     else
112 |         print_error "Container is not running."
113 |         exit 1
114 |     fi
115 | }
116 | 
117 | # Show container status
118 | show_status() {
119 |     print_header "Container Status"
120 | 
121 |     echo "Docker Images:"
122 |     docker images | grep "$IMAGE_NAME" || echo "No images found."
123 | 
124 |     echo ""
125 |     echo "Running Containers:"
126 |     docker ps | grep "$CONTAINER_NAME" || echo "No running containers found."
127 | 
128 |     echo ""
129 |     echo "All Containers:"
130 |     docker ps -a | grep "$CONTAINER_NAME" || echo "No containers found."
131 | }
132 | 
133 | # Clean up Docker resources
134 | cleanup() {
135 |     print_header "Cleaning Up"
136 | 
137 |     print_info "Stopping and removing containers..."
138 |     docker ps -a -q -f name="$CONTAINER_NAME" | xargs -r docker rm -f
139 | 
140 |     print_info "Removing images..."
141 |     docker images -q "$IMAGE_NAME" | xargs -r docker rmi -f
142 | 
143 |     print_info "Cleaning up unused Docker resources..."
144 |     docker system prune -f
145 | 
146 |     print_success "Cleanup completed!"
147 | }
148 | 
149 | # Development mode with volume mounts
150 | dev_mode() {
151 |     print_header "Development Mode"
152 | 
153 |     # Stop existing container
154 |     docker ps -q -f name="$CONTAINER_NAME-dev" | xargs -r docker rm -f
155 | 
156 |     print_info "Starting development container with volume mounts..."
157 |     docker run -d \
158 |         --name "$CONTAINER_NAME-dev" \
159 |         -p "$PORT:8080" \
160 |         -v "$(pwd)/..:/app" \
161 |         -v "/app/node_modules" \
162 |         -v "/app/webui/node_modules" \
163 |         --restart unless-stopped \
164 |         "$IMAGE_NAME:latest" \
165 |         sh -c "cd /app && pnpm dev:full"
166 | 
167 |     print_success "Development container started!"
168 |     print_info "Access the WebUI at: http://localhost:$PORT"
169 |     print_warning "Note: Changes to source files will trigger rebuilds."
170 | }
171 | 
172 | # Show help
173 | show_help() {
174 |     echo "Bybit MCP WebUI - Docker Build Script"
175 |     echo ""
176 |     echo "Usage: $0 [COMMAND]"
177 |     echo ""
178 |     echo "Commands:"
179 |     echo "  build     Build the Docker image"
180 |     echo "  run       Run the container"
181 |     echo "  stop      Stop and remove the container"
182 |     echo "  restart   Stop and start the container"
183 |     echo "  logs      Show container logs"
184 |     echo "  status    Show container and image status"
185 |     echo "  cleanup   Remove all containers and images"
186 |     echo "  dev       Run in development mode with volume mounts"
187 |     echo "  compose   Use Docker Compose (up/down/logs)"
188 |     echo "  help      Show this help message"
189 |     echo ""
190 |     echo "Examples:"
191 |     echo "  $0 build && $0 run    # Build and run"
192 |     echo "  $0 restart            # Restart container"
193 |     echo "  $0 logs               # Follow logs"
194 |     echo "  $0 compose up         # Use Docker Compose"
195 | }
196 | 
197 | # Docker Compose commands
198 | compose_command() {
199 |     case "$1" in
200 |         up)
201 |             print_header "Starting with Docker Compose"
202 |             docker-compose up -d
203 |             print_success "Services started with Docker Compose!"
204 |             ;;
205 |         down)
206 |             print_header "Stopping Docker Compose Services"
207 |             docker-compose down
208 |             print_success "Services stopped!"
209 |             ;;
210 |         logs)
211 |             print_header "Docker Compose Logs"
212 |             docker-compose logs -f
213 |             ;;
214 |         *)
215 |             print_error "Unknown compose command: $1"
216 |             echo "Available: up, down, logs"
217 |             exit 1
218 |             ;;
219 |     esac
220 | }
221 | 
222 | # Main script logic
223 | main() {
224 |     check_docker
225 | 
226 |     case "$1" in
227 |         build)
228 |             build_image
229 |             ;;
230 |         run)
231 |             run_container
232 |             ;;
233 |         stop)
234 |             stop_container
235 |             ;;
236 |         restart)
237 |             stop_container
238 |             sleep 2
239 |             run_container
240 |             ;;
241 |         logs)
242 |             show_logs
243 |             ;;
244 |         status)
245 |             show_status
246 |             ;;
247 |         cleanup)
248 |             cleanup
249 |             ;;
250 |         dev)
251 |             dev_mode
252 |             ;;
253 |         compose)
254 |             compose_command "$2"
255 |             ;;
256 |         help|--help|-h)
257 |             show_help
258 |             ;;
259 |         "")
260 |             print_warning "No command specified."
261 |             show_help
262 |             ;;
263 |         *)
264 |             print_error "Unknown command: $1"
265 |             show_help
266 |             exit 1
267 |             ;;
268 |     esac
269 | }
270 | 
271 | # Run the script
272 | main "$@"
273 | 
```

--------------------------------------------------------------------------------
/webui/src/styles/agent-dashboard.css:
--------------------------------------------------------------------------------

```css
  1 | /**
  2 |  * Agent Dashboard Styles
  3 |  */
  4 | 
  5 | /* Dashboard container */
  6 | .agent-dashboard {
  7 |   position: fixed;
  8 |   top: 0;
  9 |   left: 0;
 10 |   width: 350px;
 11 |   height: 100vh;
 12 |   background: var(--dashboard-bg, #ffffff);
 13 |   border-right: 1px solid var(--dashboard-border, #e0e0e0);
 14 |   box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
 15 |   z-index: 1000;
 16 |   transform: translateX(-100%);
 17 |   transition: transform 0.3s ease;
 18 |   display: flex;
 19 |   flex-direction: column;
 20 |   overflow: hidden;
 21 | }
 22 | 
 23 | .agent-dashboard.visible {
 24 |   transform: translateX(0);
 25 | }
 26 | 
 27 | .agent-dashboard.hidden {
 28 |   transform: translateX(-100%);
 29 | }
 30 | 
 31 | /* Dashboard header */
 32 | .dashboard-header {
 33 |   display: flex;
 34 |   justify-content: space-between;
 35 |   align-items: center;
 36 |   padding: 16px 20px;
 37 |   border-bottom: 1px solid var(--dashboard-border, #e0e0e0);
 38 |   background: var(--dashboard-header-bg, #f8f9fa);
 39 |   flex-shrink: 0;
 40 | }
 41 | 
 42 | .dashboard-header h3 {
 43 |   margin: 0;
 44 |   font-size: 1.1em;
 45 |   font-weight: 600;
 46 |   color: var(--dashboard-title, #333);
 47 | }
 48 | 
 49 | .dashboard-controls {
 50 |   display: flex;
 51 |   align-items: center;
 52 |   gap: 8px;
 53 | }
 54 | 
 55 | .refresh-btn,
 56 | .toggle-btn {
 57 |   background: var(--accent-color, #007acc);
 58 |   color: white;
 59 |   border: none;
 60 |   padding: 6px 10px;
 61 |   border-radius: 4px;
 62 |   cursor: pointer;
 63 |   transition: background 0.2s ease;
 64 |   font-size: 0.9em;
 65 | }
 66 | 
 67 | .refresh-btn:hover,
 68 | .toggle-btn:hover {
 69 |   background: var(--accent-hover, #005a9e);
 70 | }
 71 | 
 72 | /* Dashboard content */
 73 | .dashboard-content {
 74 |   flex: 1;
 75 |   overflow-y: auto;
 76 |   padding: 16px 20px;
 77 | }
 78 | 
 79 | /* Dashboard sections */
 80 | .dashboard-section {
 81 |   margin-bottom: 24px;
 82 | }
 83 | 
 84 | .dashboard-section h4 {
 85 |   margin: 0 0 12px 0;
 86 |   font-size: 1em;
 87 |   font-weight: 600;
 88 |   color: var(--section-title, #333);
 89 |   border-bottom: 1px solid var(--section-border, #eee);
 90 |   padding-bottom: 6px;
 91 | }
 92 | 
 93 | /* Statistics grid */
 94 | .stats-grid {
 95 |   display: grid;
 96 |   grid-template-columns: 1fr 1fr;
 97 |   gap: 12px;
 98 | }
 99 | 
100 | .stat-item {
101 |   background: var(--stat-bg, #f8f9fa);
102 |   border: 1px solid var(--stat-border, #e9ecef);
103 |   border-radius: 6px;
104 |   padding: 12px;
105 |   text-align: center;
106 | }
107 | 
108 | .stat-label {
109 |   display: block;
110 |   font-size: 0.8em;
111 |   color: var(--text-muted, #666);
112 |   margin-bottom: 4px;
113 | }
114 | 
115 | .stat-value {
116 |   display: block;
117 |   font-size: 1.2em;
118 |   font-weight: 600;
119 |   color: var(--accent-color, #007acc);
120 | }
121 | 
122 | /* Analysis list */
123 | .analysis-list {
124 |   display: flex;
125 |   flex-direction: column;
126 |   gap: 12px;
127 |   max-height: 300px;
128 |   overflow-y: auto;
129 | }
130 | 
131 | .analysis-item {
132 |   background: var(--analysis-bg, #fff);
133 |   border: 1px solid var(--analysis-border, #e0e0e0);
134 |   border-radius: 6px;
135 |   padding: 12px;
136 |   transition: all 0.2s ease;
137 | }
138 | 
139 | .analysis-item:hover {
140 |   border-color: var(--accent-color, #007acc);
141 |   box-shadow: 0 2px 4px rgba(0, 122, 204, 0.1);
142 | }
143 | 
144 | .analysis-header {
145 |   display: flex;
146 |   justify-content: space-between;
147 |   align-items: center;
148 |   margin-bottom: 6px;
149 |   flex-wrap: wrap;
150 |   gap: 6px;
151 | }
152 | 
153 | .analysis-symbol {
154 |   background: var(--accent-color, #007acc);
155 |   color: white;
156 |   padding: 2px 6px;
157 |   border-radius: 3px;
158 |   font-size: 0.8em;
159 |   font-weight: 500;
160 | }
161 | 
162 | .analysis-type {
163 |   background: var(--type-bg, #e9ecef);
164 |   color: var(--type-text, #495057);
165 |   padding: 2px 6px;
166 |   border-radius: 3px;
167 |   font-size: 0.8em;
168 |   text-transform: capitalize;
169 | }
170 | 
171 | .analysis-time {
172 |   font-size: 0.8em;
173 |   color: var(--text-muted, #666);
174 | }
175 | 
176 | .analysis-query {
177 |   font-size: 0.85em;
178 |   color: var(--text-primary, #333);
179 |   margin-bottom: 6px;
180 |   line-height: 1.3;
181 | }
182 | 
183 | .analysis-metrics {
184 |   display: flex;
185 |   gap: 8px;
186 |   flex-wrap: wrap;
187 | }
188 | 
189 | .analysis-metrics .metric {
190 |   font-size: 0.75em;
191 |   color: var(--text-muted, #666);
192 |   background: var(--metric-bg, #f1f3f4);
193 |   padding: 2px 6px;
194 |   border-radius: 3px;
195 | }
196 | 
197 | /* Action buttons */
198 | .action-buttons {
199 |   display: flex;
200 |   flex-direction: column;
201 |   gap: 8px;
202 | }
203 | 
204 | .action-btn {
205 |   background: var(--button-bg, #fff);
206 |   color: var(--button-text, #333);
207 |   border: 1px solid var(--button-border, #ddd);
208 |   padding: 8px 12px;
209 |   border-radius: 4px;
210 |   cursor: pointer;
211 |   transition: all 0.2s ease;
212 |   font-size: 0.9em;
213 | }
214 | 
215 | .action-btn:hover {
216 |   background: var(--button-hover-bg, #f8f9fa);
217 |   border-color: var(--accent-color, #007acc);
218 | }
219 | 
220 | .action-btn:active {
221 |   background: var(--button-active-bg, #e9ecef);
222 | }
223 | 
224 | /* Empty state */
225 | .empty-state {
226 |   text-align: center;
227 |   padding: 20px;
228 |   color: var(--text-muted, #666);
229 | }
230 | 
231 | .empty-state p {
232 |   margin: 0;
233 |   font-style: italic;
234 |   font-size: 0.9em;
235 | }
236 | 
237 | /* Toast notifications */
238 | .dashboard-toast {
239 |   position: fixed;
240 |   top: 20px;
241 |   left: 370px; /* Position next to dashboard */
242 |   padding: 12px 20px;
243 |   border-radius: 6px;
244 |   color: white;
245 |   font-weight: 500;
246 |   z-index: 3000;
247 |   transform: translateX(-100%);
248 |   transition: transform 0.3s ease;
249 |   max-width: 300px;
250 | }
251 | 
252 | .dashboard-toast.show {
253 |   transform: translateX(0);
254 | }
255 | 
256 | .dashboard-toast.toast-success {
257 |   background: var(--success-color, #28a745);
258 | }
259 | 
260 | .dashboard-toast.toast-error {
261 |   background: var(--danger-color, #dc3545);
262 | }
263 | 
264 | /* Dark theme */
265 | [data-theme="dark"] .agent-dashboard {
266 |   --dashboard-bg: #2d2d2d;
267 |   --dashboard-border: #444;
268 |   --dashboard-header-bg: #333;
269 |   --dashboard-title: #fff;
270 |   --section-title: #fff;
271 |   --section-border: #444;
272 |   --stat-bg: #333;
273 |   --stat-border: #444;
274 |   --text-muted: #aaa;
275 |   --text-primary: #fff;
276 |   --analysis-bg: #333;
277 |   --analysis-border: #444;
278 |   --type-bg: #444;
279 |   --type-text: #ccc;
280 |   --metric-bg: #404040;
281 |   --button-bg: #444;
282 |   --button-text: #fff;
283 |   --button-border: #555;
284 |   --button-hover-bg: #505050;
285 |   --button-active-bg: #555;
286 | }
287 | 
288 | /* Responsive design */
289 | @media (max-width: 768px) {
290 |   .agent-dashboard {
291 |     width: 100vw;
292 |   }
293 |   
294 |   .dashboard-toast {
295 |     left: 20px;
296 |     right: 20px;
297 |     max-width: none;
298 |   }
299 |   
300 |   .stats-grid {
301 |     grid-template-columns: 1fr;
302 |   }
303 | }
304 | 
305 | /* Scrollbar styling */
306 | .dashboard-content::-webkit-scrollbar,
307 | .analysis-list::-webkit-scrollbar {
308 |   width: 6px;
309 | }
310 | 
311 | .dashboard-content::-webkit-scrollbar-track,
312 | .analysis-list::-webkit-scrollbar-track {
313 |   background: var(--scrollbar-track, #f1f1f1);
314 | }
315 | 
316 | .dashboard-content::-webkit-scrollbar-thumb,
317 | .analysis-list::-webkit-scrollbar-thumb {
318 |   background: var(--scrollbar-thumb, #c1c1c1);
319 |   border-radius: 3px;
320 | }
321 | 
322 | .dashboard-content::-webkit-scrollbar-thumb:hover,
323 | .analysis-list::-webkit-scrollbar-thumb:hover {
324 |   background: var(--scrollbar-thumb-hover, #a8a8a8);
325 | }
326 | 
327 | /* Animation for stats updates */
328 | .stat-value {
329 |   transition: color 0.3s ease;
330 | }
331 | 
332 | .stat-value.updated {
333 |   color: var(--success-color, #28a745);
334 | }
335 | 
336 | /* Performance indicators */
337 | .stat-item.performance-good .stat-value {
338 |   color: var(--success-color, #28a745);
339 | }
340 | 
341 | .stat-item.performance-warning .stat-value {
342 |   color: var(--warning-color, #ffc107);
343 | }
344 | 
345 | .stat-item.performance-poor .stat-value {
346 |   color: var(--danger-color, #dc3545);
347 | }
348 | 
349 | /* Loading state */
350 | .dashboard-section.loading {
351 |   opacity: 0.6;
352 |   pointer-events: none;
353 | }
354 | 
355 | .dashboard-section.loading::after {
356 |   content: '';
357 |   position: absolute;
358 |   top: 50%;
359 |   left: 50%;
360 |   width: 20px;
361 |   height: 20px;
362 |   margin: -10px 0 0 -10px;
363 |   border: 2px solid var(--accent-color, #007acc);
364 |   border-top: 2px solid transparent;
365 |   border-radius: 50%;
366 |   animation: spin 1s linear infinite;
367 | }
368 | 
369 | @keyframes spin {
370 |   0% { transform: rotate(0deg); }
371 |   100% { transform: rotate(360deg); }
372 | }
373 | 
```

--------------------------------------------------------------------------------
/webui/src/types/workflow.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Workflow event types and utilities for agent workflows
  3 |  */
  4 | 
  5 | // Base workflow event interface
  6 | export interface BaseWorkflowEvent {
  7 |   id: string;
  8 |   timestamp: number;
  9 |   source: 'agent' | 'tool' | 'workflow' | 'user';
 10 | }
 11 | 
 12 | // Market analysis workflow events
 13 | export interface MarketAnalysisRequestEvent extends BaseWorkflowEvent {
 14 |   type: 'market_analysis_request';
 15 |   data: {
 16 |     symbol: string;
 17 |     analysisType: 'quick' | 'standard' | 'comprehensive';
 18 |     userQuery: string;
 19 |     preferences: {
 20 |       includeTechnical: boolean;
 21 |       includeStructure: boolean;
 22 |       includeRisk: boolean;
 23 |     };
 24 |   };
 25 | }
 26 | 
 27 | export interface TechnicalDataGatheredEvent extends BaseWorkflowEvent {
 28 |   type: 'technical_data_gathered';
 29 |   data: {
 30 |     symbol: string;
 31 |     priceData: any;
 32 |     indicators: any;
 33 |     volume: any;
 34 |     confidence: number;
 35 |   };
 36 | }
 37 | 
 38 | export interface StructureAnalysisCompleteEvent extends BaseWorkflowEvent {
 39 |   type: 'structure_analysis_complete';
 40 |   data: {
 41 |     symbol: string;
 42 |     orderBlocks: any;
 43 |     marketStructure: any;
 44 |     liquidityZones: any;
 45 |     confidence: number;
 46 |   };
 47 | }
 48 | 
 49 | export interface RiskAssessmentDoneEvent extends BaseWorkflowEvent {
 50 |   type: 'risk_assessment_done';
 51 |   data: {
 52 |     symbol: string;
 53 |     riskLevel: 'low' | 'medium' | 'high';
 54 |     positionSizing: any;
 55 |     stopLoss: number;
 56 |     takeProfit: number;
 57 |     confidence: number;
 58 |   };
 59 | }
 60 | 
 61 | export interface FinalRecommendationEvent extends BaseWorkflowEvent {
 62 |   type: 'final_recommendation';
 63 |   data: {
 64 |     symbol: string;
 65 |     action: 'buy' | 'sell' | 'hold' | 'wait';
 66 |     confidence: number;
 67 |     reasoning: string;
 68 |     technicalAnalysis?: any;
 69 |     structureAnalysis?: any;
 70 |     riskAssessment?: any;
 71 |     timeframe: string;
 72 |   };
 73 | }
 74 | 
 75 | // Agent communication events
 76 | export interface AgentHandoffEvent extends BaseWorkflowEvent {
 77 |   type: 'agent_handoff';
 78 |   data: {
 79 |     fromAgent: string;
 80 |     toAgent: string;
 81 |     context: any;
 82 |     reason: string;
 83 |   };
 84 | }
 85 | 
 86 | export interface AgentCollaborationEvent extends BaseWorkflowEvent {
 87 |   type: 'agent_collaboration';
 88 |   data: {
 89 |     participants: string[];
 90 |     topic: string;
 91 |     consensus?: any;
 92 |     disagreements?: any;
 93 |   };
 94 | }
 95 | 
 96 | // Tool execution events
 97 | export interface ToolExecutionStartEvent extends BaseWorkflowEvent {
 98 |   type: 'tool_execution_start';
 99 |   data: {
100 |     toolName: string;
101 |     parameters: Record<string, any>;
102 |     expectedDuration?: number;
103 |     agent: string;
104 |   };
105 | }
106 | 
107 | export interface ToolExecutionCompleteEvent extends BaseWorkflowEvent {
108 |   type: 'tool_execution_complete';
109 |   data: {
110 |     toolName: string;
111 |     parameters: Record<string, any>;
112 |     result: any;
113 |     duration: number;
114 |     success: boolean;
115 |     agent: string;
116 |   };
117 | }
118 | 
119 | export interface ToolExecutionErrorEvent extends BaseWorkflowEvent {
120 |   type: 'tool_execution_error';
121 |   data: {
122 |     toolName: string;
123 |     parameters: Record<string, any>;
124 |     error: string;
125 |     duration: number;
126 |     agent: string;
127 |     retryable: boolean;
128 |   };
129 | }
130 | 
131 | // Workflow control events
132 | export interface WorkflowStartEvent extends BaseWorkflowEvent {
133 |   type: 'workflow_start';
134 |   data: {
135 |     workflowName: string;
136 |     initialQuery: string;
137 |     configuration: any;
138 |   };
139 | }
140 | 
141 | export interface WorkflowStepEvent extends BaseWorkflowEvent {
142 |   type: 'workflow_step';
143 |   data: {
144 |     stepName: string;
145 |     stepDescription: string;
146 |     progress: number;
147 |     totalSteps: number;
148 |     currentAgent?: string;
149 |   };
150 | }
151 | 
152 | export interface WorkflowCompleteEvent extends BaseWorkflowEvent {
153 |   type: 'workflow_complete';
154 |   data: {
155 |     workflowName: string;
156 |     result: any;
157 |     duration: number;
158 |     stepsCompleted: number;
159 |     success: boolean;
160 |   };
161 | }
162 | 
163 | export interface WorkflowErrorEvent extends BaseWorkflowEvent {
164 |   type: 'workflow_error';
165 |   data: {
166 |     workflowName: string;
167 |     error: string;
168 |     step?: string;
169 |     agent?: string;
170 |     recoverable: boolean;
171 |   };
172 | }
173 | 
174 | // Agent thinking and reasoning events
175 | export interface AgentReasoningEvent extends BaseWorkflowEvent {
176 |   type: 'agent_reasoning';
177 |   data: {
178 |     agent: string;
179 |     thought: string;
180 |     nextAction: string;
181 |     confidence: number;
182 |     context: any;
183 |   };
184 | }
185 | 
186 | export interface AgentDecisionEvent extends BaseWorkflowEvent {
187 |   type: 'agent_decision';
188 |   data: {
189 |     agent: string;
190 |     decision: string;
191 |     reasoning: string;
192 |     alternatives: string[];
193 |     confidence: number;
194 |   };
195 | }
196 | 
197 | // Union type for all workflow events
198 | export type WorkflowEvent =
199 |   | MarketAnalysisRequestEvent
200 |   | TechnicalDataGatheredEvent
201 |   | StructureAnalysisCompleteEvent
202 |   | RiskAssessmentDoneEvent
203 |   | FinalRecommendationEvent
204 |   | AgentHandoffEvent
205 |   | AgentCollaborationEvent
206 |   | ToolExecutionStartEvent
207 |   | ToolExecutionCompleteEvent
208 |   | ToolExecutionErrorEvent
209 |   | WorkflowStartEvent
210 |   | WorkflowStepEvent
211 |   | WorkflowCompleteEvent
212 |   | WorkflowErrorEvent
213 |   | AgentReasoningEvent
214 |   | AgentDecisionEvent;
215 | 
216 | // Event utilities
217 | export class WorkflowEventEmitter {
218 |   private listeners: Map<string, ((event: WorkflowEvent) => void)[]> = new Map();
219 | 
220 |   on(eventType: string, listener: (event: WorkflowEvent) => void): void {
221 |     if (!this.listeners.has(eventType)) {
222 |       this.listeners.set(eventType, []);
223 |     }
224 |     this.listeners.get(eventType)!.push(listener);
225 |   }
226 | 
227 |   off(eventType: string, listener: (event: WorkflowEvent) => void): void {
228 |     const listeners = this.listeners.get(eventType);
229 |     if (listeners) {
230 |       const index = listeners.indexOf(listener);
231 |       if (index > -1) {
232 |         listeners.splice(index, 1);
233 |       }
234 |     }
235 |   }
236 | 
237 |   emit(event: WorkflowEvent): void {
238 |     const listeners = this.listeners.get(event.type);
239 |     if (listeners) {
240 |       listeners.forEach(listener => listener(event));
241 |     }
242 | 
243 |     // Also emit to 'all' listeners
244 |     const allListeners = this.listeners.get('all');
245 |     if (allListeners) {
246 |       allListeners.forEach(listener => listener(event));
247 |     }
248 |   }
249 | 
250 |   clear(): void {
251 |     this.listeners.clear();
252 |   }
253 | }
254 | 
255 | // Event factory functions
256 | export function createWorkflowEvent<T extends WorkflowEvent>(
257 |   type: T['type'],
258 |   data: T['data'],
259 |   source: BaseWorkflowEvent['source'] = 'workflow'
260 | ): T {
261 |   return {
262 |     id: `${type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
263 |     timestamp: Date.now(),
264 |     source,
265 |     type,
266 |     data
267 |   } as T;
268 | }
269 | 
270 | // Event type guards
271 | export function isToolEvent(event: WorkflowEvent): event is ToolExecutionStartEvent | ToolExecutionCompleteEvent | ToolExecutionErrorEvent {
272 |   return event.type.startsWith('tool_execution');
273 | }
274 | 
275 | export function isAgentEvent(event: WorkflowEvent): event is AgentHandoffEvent | AgentCollaborationEvent | AgentReasoningEvent | AgentDecisionEvent {
276 |   return event.type.startsWith('agent_');
277 | }
278 | 
279 | export function isWorkflowControlEvent(event: WorkflowEvent): event is WorkflowStartEvent | WorkflowStepEvent | WorkflowCompleteEvent | WorkflowErrorEvent {
280 |   return event.type.startsWith('workflow_');
281 | }
282 | 
283 | export function isAnalysisEvent(event: WorkflowEvent): event is MarketAnalysisRequestEvent | TechnicalDataGatheredEvent | StructureAnalysisCompleteEvent | RiskAssessmentDoneEvent | FinalRecommendationEvent {
284 |   return ['market_analysis_request', 'technical_data_gathered', 'structure_analysis_complete', 'risk_assessment_done', 'final_recommendation'].includes(event.type);
285 | }
286 | 
```

--------------------------------------------------------------------------------
/webui/src/services/citationStore.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Citation store for managing tool response data and references
  3 |  */
  4 | 
  5 | import type { CitationData, ExtractedMetric } from '@/types/citation';
  6 | 
  7 | export class CitationStore {
  8 |   private citations: Map<string, CitationData> = new Map();
  9 |   private maxCitations = 100; // Limit to prevent memory issues
 10 |   private cleanupThreshold = 120; // Clean up citations older than 2 hours
 11 | 
 12 |   /**
 13 |    * Store tool response data with citation metadata
 14 |    */
 15 |   storeCitation(data: CitationData): void {
 16 |     this.citations.set(data.referenceId, data);
 17 | 
 18 |     // Clean up old citations if we exceed the limit
 19 |     if (this.citations.size > this.maxCitations) {
 20 |       this.cleanupOldCitations();
 21 |     }
 22 |   }
 23 | 
 24 |   /**
 25 |    * Retrieve citation data by reference ID
 26 |    */
 27 |   getCitation(referenceId: string): CitationData | undefined {
 28 |     return this.citations.get(referenceId);
 29 |   }
 30 | 
 31 |   /**
 32 |    * Get all citations sorted by timestamp (newest first)
 33 |    */
 34 |   getAllCitations(): CitationData[] {
 35 |     return Array.from(this.citations.values())
 36 |       .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
 37 |   }
 38 | 
 39 |   /**
 40 |    * Get recent citations (last N citations)
 41 |    */
 42 |   getRecentCitations(limit: number = 10): CitationData[] {
 43 |     return this.getAllCitations().slice(0, limit);
 44 |   }
 45 | 
 46 |   /**
 47 |    * Extract key metrics from tool response data
 48 |    */
 49 |   extractMetrics(toolName: string, rawData: any): ExtractedMetric[] {
 50 |     const metrics: ExtractedMetric[] = [];
 51 | 
 52 |     try {
 53 |       switch (toolName) {
 54 |         case 'get_ticker':
 55 |           if (rawData.lastPrice) {
 56 |             metrics.push({
 57 |               type: 'price',
 58 |               label: 'Last Price',
 59 |               value: rawData.lastPrice,
 60 |               unit: 'USD',
 61 |               significance: 'high'
 62 |             });
 63 |           }
 64 |           if (rawData.price24hPcnt) {
 65 |             metrics.push({
 66 |               type: 'percentage',
 67 |               label: '24h Change',
 68 |               value: rawData.price24hPcnt,
 69 |               unit: '%',
 70 |               significance: 'high'
 71 |             });
 72 |           }
 73 |           if (rawData.volume24h) {
 74 |             metrics.push({
 75 |               type: 'volume',
 76 |               label: '24h Volume',
 77 |               value: rawData.volume24h,
 78 |               significance: 'medium'
 79 |             });
 80 |           }
 81 |           break;
 82 | 
 83 |         case 'get_kline':
 84 |           if (rawData.data && Array.isArray(rawData.data) && rawData.data.length > 0) {
 85 |             const latestCandle = rawData.data[0];
 86 |             if (latestCandle.close) {
 87 |               metrics.push({
 88 |                 type: 'price',
 89 |                 label: 'Close Price',
 90 |                 value: latestCandle.close,
 91 |                 unit: 'USD',
 92 |                 significance: 'high'
 93 |               });
 94 |             }
 95 |           }
 96 |           break;
 97 | 
 98 |         case 'get_ml_rsi':
 99 |           if (rawData.data && Array.isArray(rawData.data) && rawData.data.length > 0) {
100 |             const latestRsi = rawData.data[0];
101 |             if (latestRsi.mlRsi !== undefined) {
102 |               metrics.push({
103 |                 type: 'indicator',
104 |                 label: 'ML RSI',
105 |                 value: latestRsi.mlRsi.toFixed(2),
106 |                 significance: 'high'
107 |               });
108 |             }
109 |             if (latestRsi.trend) {
110 |               metrics.push({
111 |                 type: 'other',
112 |                 label: 'Trend',
113 |                 value: latestRsi.trend,
114 |                 significance: 'medium'
115 |               });
116 |             }
117 |           }
118 |           break;
119 | 
120 |         case 'get_orderbook':
121 |           if (rawData.bids && rawData.bids.length > 0) {
122 |             metrics.push({
123 |               type: 'price',
124 |               label: 'Best Bid',
125 |               value: rawData.bids[0][0],
126 |               unit: 'USD',
127 |               significance: 'high'
128 |             });
129 |           }
130 |           if (rawData.asks && rawData.asks.length > 0) {
131 |             metrics.push({
132 |               type: 'price',
133 |               label: 'Best Ask',
134 |               value: rawData.asks[0][0],
135 |               unit: 'USD',
136 |               significance: 'high'
137 |             });
138 |           }
139 |           break;
140 | 
141 |         default:
142 |           // Generic extraction for unknown tools
143 |           if (typeof rawData === 'object' && rawData !== null) {
144 |             Object.entries(rawData).forEach(([key, value]) => {
145 |               if (typeof value === 'string' || typeof value === 'number') {
146 |                 metrics.push({
147 |                   type: 'other',
148 |                   label: key,
149 |                   value: value,
150 |                   significance: 'low'
151 |                 });
152 |               }
153 |             });
154 |           }
155 |           break;
156 |       }
157 |     } catch (error) {
158 |       console.warn('Error extracting metrics:', error);
159 |     }
160 | 
161 |     return metrics.slice(0, 5); // Limit to 5 key metrics
162 |   }
163 | 
164 |   /**
165 |    * Process tool response and store citation if it has reference metadata
166 |    */
167 |   processToolResponse(toolResponse: any): void {
168 |     console.log('🔍 Processing tool response for citations:', toolResponse);
169 | 
170 |     if (!toolResponse || typeof toolResponse !== 'object') {
171 |       console.log('❌ Invalid tool response format');
172 |       return;
173 |     }
174 | 
175 |     // MCP responses are wrapped in a content array, so we need to extract the actual data
176 |     let actualData = toolResponse;
177 | 
178 |     // Check if response has content array (MCP format)
179 |     if (toolResponse.content && Array.isArray(toolResponse.content) && toolResponse.content.length > 0) {
180 |       console.log('🔍 Found MCP content array, extracting data...');
181 |       const contentItem = toolResponse.content[0];
182 |       if (contentItem.type === 'text' && contentItem.text) {
183 |         try {
184 |           actualData = JSON.parse(contentItem.text);
185 |           console.log('🔍 Parsed content data:', actualData);
186 |         } catch (e) {
187 |           console.log('❌ Failed to parse content text as JSON');
188 |           return;
189 |         }
190 |       }
191 |     }
192 | 
193 |     // Check if response has reference metadata
194 |     if (actualData._referenceId && actualData._timestamp && actualData._toolName) {
195 |       console.log('✅ Found reference metadata:', {
196 |         referenceId: actualData._referenceId,
197 |         toolName: actualData._toolName,
198 |         timestamp: actualData._timestamp
199 |       });
200 | 
201 |       const extractedMetrics = this.extractMetrics(actualData._toolName, actualData);
202 | 
203 |       const citationData: CitationData = {
204 |         referenceId: actualData._referenceId,
205 |         timestamp: actualData._timestamp,
206 |         toolName: actualData._toolName,
207 |         endpoint: actualData._endpoint,
208 |         rawData: actualData,
209 |         extractedMetrics
210 |       };
211 | 
212 |       this.storeCitation(citationData);
213 |       console.log('📋 Stored citation data for', actualData._referenceId);
214 |     } else {
215 |       console.log('❌ No reference metadata found in tool response');
216 |       console.log('🔍 Available keys in actualData:', Object.keys(actualData));
217 |     }
218 |   }
219 | 
220 |   /**
221 |    * Clean up citations older than the threshold
222 |    */
223 |   private cleanupOldCitations(): void {
224 |     const now = Date.now();
225 |     const thresholdMs = this.cleanupThreshold * 60 * 1000; // Convert minutes to milliseconds
226 | 
227 |     for (const [referenceId, citation] of this.citations.entries()) {
228 |       const citationAge = now - new Date(citation.timestamp).getTime();
229 |       if (citationAge > thresholdMs) {
230 |         this.citations.delete(referenceId);
231 |       }
232 |     }
233 | 
234 |     // Cleanup completed silently
235 |   }
236 | 
237 |   /**
238 |    * Clear all citations
239 |    */
240 |   clear(): void {
241 |     this.citations.clear();
242 |   }
243 | 
244 |   /**
245 |    * Get citation count
246 |    */
247 |   getCount(): number {
248 |     return this.citations.size;
249 |   }
250 | }
251 | 
252 | // Singleton instance
253 | export const citationStore = new CitationStore();
254 | 
```

--------------------------------------------------------------------------------
/client/src/cli.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | import { Command } from 'commander'
  3 | import chalk from 'chalk'
  4 | import { BybitMcpClient, Message } from './client.js'
  5 | import { Config } from './config.js'
  6 | import { createInterface } from 'readline'
  7 | 
  8 | const program = new Command()
  9 | const config = new Config()
 10 | let client: BybitMcpClient | null = null
 11 | 
 12 | // Debug helper to log configuration
 13 | function logDebugInfo() {
 14 |   if (config.get('debug')) {
 15 |     console.log(chalk.yellow('Debug Info:'))
 16 |     console.log('Ollama Host:', config.get('ollamaHost'))
 17 |     console.log('Default Model:', config.get('defaultModel'))
 18 |     console.log('Debug Mode:', config.get('debug'))
 19 |   }
 20 | }
 21 | 
 22 | program
 23 |   .name('bybit-mcp-client')
 24 |   .description('CLI for interacting with Ollama LLMs and bybit-mcp server')
 25 |   .version('0.1.0')
 26 |   .option('-i, --integrated', 'Run in integrated mode with built-in server')
 27 |   .option('-d, --debug', 'Enable debug logging')
 28 | 
 29 | program
 30 |   .command('config')
 31 |   .description('Configure client settings')
 32 |   .option('-h, --ollama-host <url>', 'Set Ollama host URL')
 33 |   .option('-m, --default-model <model>', 'Set default Ollama model')
 34 |   .option('-d, --debug <boolean>', 'Enable/disable debug mode')
 35 |   .action((options: { ollamaHost?: string; defaultModel?: string; debug?: string }) => {
 36 |     if (options.ollamaHost) {
 37 |       config.set('ollamaHost', options.ollamaHost)
 38 |       console.log(chalk.green(`Ollama host set to: ${options.ollamaHost}`))
 39 |     }
 40 |     if (options.defaultModel) {
 41 |       config.set('defaultModel', options.defaultModel)
 42 |       console.log(chalk.green(`Default model set to: ${options.defaultModel}`))
 43 |     }
 44 |     if (options.debug !== undefined) {
 45 |       const debugEnabled = options.debug.toLowerCase() === 'true'
 46 |       config.set('debug', debugEnabled)
 47 |       console.log(chalk.green(`Debug mode ${debugEnabled ? 'enabled' : 'disabled'}`))
 48 |     }
 49 |     logDebugInfo()
 50 |   })
 51 | 
 52 | program
 53 |   .command('models')
 54 |   .description('List available Ollama models')
 55 |   .action(async () => {
 56 |     try {
 57 |       logDebugInfo()
 58 |       client = new BybitMcpClient(config)
 59 |       const models = await client.listModels()
 60 |       console.log(chalk.cyan('Available models:'))
 61 |       models.forEach(model => console.log(`  ${model}`))
 62 |     } catch (error) {
 63 |       console.error(chalk.red('Error listing models:'), error)
 64 |     } finally {
 65 |       await client?.close()
 66 |     }
 67 |   })
 68 | 
 69 | program
 70 |   .command('tools')
 71 |   .description('List available bybit-mcp tools')
 72 |   .argument('[server-command]', 'Command to start the bybit-mcp server (not needed in integrated mode)')
 73 |   .action(async (serverCommand?: string) => {
 74 |     try {
 75 |       logDebugInfo()
 76 |       client = new BybitMcpClient(config)
 77 | 
 78 |       if (program.opts().integrated) {
 79 |         if (program.opts().debug) {
 80 |           console.log(chalk.yellow('Starting integrated server...'))
 81 |         }
 82 |         await client.startIntegratedServer()
 83 |         if (program.opts().debug) {
 84 |           console.log(chalk.green('Started integrated server'))
 85 |         }
 86 |       } else if (serverCommand) {
 87 |         await client.connectToServer(serverCommand)
 88 |       } else {
 89 |         throw new Error('Either use --integrated or provide a server command')
 90 |       }
 91 | 
 92 |       const tools = await client.listTools()
 93 |       console.log(chalk.cyan('Available tools:'))
 94 |       tools.forEach(tool => {
 95 |         console.log(chalk.bold(`\n${tool.name}`))
 96 |         if (tool.description) console.log(`  Description: ${tool.description}`)
 97 |         if (tool.inputSchema) console.log(`  Input Schema: ${JSON.stringify(tool.inputSchema, null, 2)}`)
 98 |       })
 99 |     } catch (error) {
100 |       console.error(chalk.red('Error listing tools:'), error)
101 |     } finally {
102 |       await client?.close()
103 |     }
104 |   })
105 | 
106 | program
107 |   .command('chat')
108 |   .description('Chat with an Ollama model')
109 |   .argument('[model]', 'Model to use (defaults to config setting)')
110 |   .option('-s, --system <message>', 'System message to set context')
111 |   .action(async (modelArg: string | undefined, options: { system?: string }) => {
112 |     try {
113 |       // Enable debug mode for chat to help diagnose issues
114 |       config.set('debug', true)
115 |       logDebugInfo()
116 | 
117 |       client = new BybitMcpClient(config)
118 | 
119 |       // Always start in integrated mode for chat
120 |       if (program.opts().debug) {
121 |         console.log(chalk.yellow('Starting integrated server for chat...'))
122 |       }
123 |       await client.startIntegratedServer()
124 |       if (program.opts().debug) {
125 |         console.log(chalk.green('Started integrated server'))
126 |       }
127 | 
128 |       const model = modelArg || config.get('defaultModel')
129 |       if (!model) {
130 |         throw new Error('No model specified and no default model configured')
131 |       }
132 | 
133 |       const messages: Message[] = []
134 | 
135 |       if (options.system) {
136 |         messages.push({ role: 'system', content: options.system })
137 |       }
138 | 
139 |       console.log(chalk.cyan(`Chatting with ${model} (Ctrl+C to exit)`))
140 |       console.log(chalk.yellow('Tools are available - ask about cryptocurrency data!'))
141 | 
142 |       // Start chat loop
143 |       while (true) {
144 |         const userInput = await question(chalk.green('You: '))
145 |         if (!userInput) continue
146 | 
147 |         messages.push({ role: 'user', content: userInput })
148 | 
149 |         process.stdout.write(chalk.blue('Assistant: '))
150 |         await client.streamChat(model, messages, (token) => {
151 |           process.stdout.write(token)
152 |         })
153 |         process.stdout.write('\n')
154 | 
155 |         messages.push({ role: 'assistant', content: await client.chat(model, messages) })
156 |       }
157 |     } catch (error) {
158 |       console.error(chalk.red('Error in chat:'), error)
159 |       if (program.opts().debug) {
160 |         console.error('Full error:', error)
161 |       }
162 |     } finally {
163 |       await client?.close()
164 |     }
165 |   })
166 | 
167 | program
168 |   .command('tool')
169 |   .description('Call a bybit-mcp tool')
170 |   .argument('[server-command]', 'Command to start the bybit-mcp server (not needed in integrated mode)')
171 |   .argument('<tool-name>', 'Name of the tool to call')
172 |   .argument('[args...]', 'Tool arguments as key=value pairs')
173 |   .action(async (serverCommand: string | undefined, toolName: string, args: string[]) => {
174 |     try {
175 |       logDebugInfo()
176 |       client = new BybitMcpClient(config)
177 | 
178 |       if (program.opts().integrated) {
179 |         if (program.opts().debug) {
180 |           console.log(chalk.yellow('Starting integrated server...'))
181 |         }
182 |         await client.startIntegratedServer()
183 |         if (program.opts().debug) {
184 |           console.log(chalk.green('Started integrated server'))
185 |         }
186 |       } else if (serverCommand) {
187 |         await client.connectToServer(serverCommand)
188 |       } else {
189 |         throw new Error('Either use --integrated or provide a server command')
190 |       }
191 | 
192 |       // Parse arguments
193 |       const toolArgs: Record<string, unknown> = {}
194 |       args.forEach((arg: string) => {
195 |         const [key, value] = arg.split('=')
196 |         if (key && value) {
197 |           // Try to parse as number or boolean if possible
198 |           if (value === 'true') toolArgs[key] = true
199 |           else if (value === 'false') toolArgs[key] = false
200 |           else if (!isNaN(Number(value))) toolArgs[key] = Number(value)
201 |           else toolArgs[key] = value
202 |         }
203 |       })
204 | 
205 |       const result = await client.callTool(toolName, toolArgs)
206 |       console.log(result)
207 |     } catch (error) {
208 |       console.error(chalk.red('Error calling tool:'), error)
209 |     } finally {
210 |       await client?.close()
211 |     }
212 |   })
213 | 
214 | // Helper function to read user input
215 | function question(query: string): Promise<string> {
216 |   const readline = createInterface({
217 |     input: process.stdin,
218 |     output: process.stdout
219 |   })
220 | 
221 |   return new Promise(resolve => readline.question(query, (answer: string) => {
222 |     readline.close()
223 |     resolve(answer)
224 |   }))
225 | }
226 | 
227 | // Set debug mode from command line option
228 | if (program.opts().debug) {
229 |   config.set('debug', true)
230 | }
231 | 
232 | program.parse()
233 | 
```

--------------------------------------------------------------------------------
/webui/index.html:
--------------------------------------------------------------------------------

```html
  1 | <!DOCTYPE html>
  2 | <html lang="en">
  3 | <head>
  4 |   <meta charset="UTF-8" />
  5 |   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6 |   <title>Bybit MCP WebUI</title>
  7 |   <meta name="description" content="Modern web interface for Bybit MCP server with AI chat capabilities" />
  8 |   <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
  9 | 
 10 |   <!-- Preload critical CSS to prevent layout shift -->
 11 |   <link rel="preload" href="/src/styles/main.css" as="style" />
 12 | </head>
 13 | <body>
 14 |   <div id="app">
 15 |     <!-- Loading spinner -->
 16 |     <div id="loading" class="loading-container">
 17 |       <div class="loading-spinner"></div>
 18 |       <p>Loading Bybit MCP WebUI...</p>
 19 |     </div>
 20 | 
 21 |     <!-- Main application container -->
 22 |     <div id="main-container" class="main-container hidden">
 23 |       <!-- Header -->
 24 |       <header class="header">
 25 |         <div class="header-content">
 26 |           <div class="logo">
 27 |             <h1>Bybit MCP</h1>
 28 |             <span class="version">v1.0.0</span>
 29 |           </div>
 30 |           <div class="header-controls">
 31 |             <button id="agent-dashboard-btn" class="agent-dashboard-btn" aria-label="Agent Dashboard">
 32 |               <span class="dashboard-icon">🤖</span>
 33 |             </button>
 34 |             <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
 35 |               <span class="theme-icon">🌙</span>
 36 |             </button>
 37 |             <button id="settings-btn" class="settings-btn" aria-label="Settings">
 38 |               <span class="settings-icon">⚙️</span>
 39 |             </button>
 40 |           </div>
 41 |         </div>
 42 |       </header>
 43 | 
 44 |       <!-- Main content area -->
 45 |       <main class="main-content">
 46 |         <!-- Sidebar -->
 47 |         <aside class="sidebar">
 48 |           <nav class="nav-menu">
 49 |             <button class="nav-item active" data-view="chat">
 50 |               <span class="nav-icon">💬</span>
 51 |               <span class="nav-label">AI Chat</span>
 52 |             </button>
 53 |             <button class="nav-item" data-view="tools">
 54 |               <span class="nav-icon">🔧</span>
 55 |               <span class="nav-label">MCP Tools</span>
 56 |             </button>
 57 |             <button class="nav-item" data-view="dashboard">
 58 |               <span class="nav-icon">🤖</span>
 59 |               <span class="nav-label">Agent Dashboard</span>
 60 |             </button>
 61 |           </nav>
 62 |         </aside>
 63 | 
 64 |         <!-- Content area -->
 65 |         <section class="content-area">
 66 |           <!-- Chat View -->
 67 |           <div id="chat-view" class="view active">
 68 |             <div class="chat-container">
 69 |               <div class="chat-messages" id="chat-messages">
 70 |                 <div class="welcome-message">
 71 |                   <h2>Welcome to Bybit MCP AI Assistant</h2>
 72 |                   <p>Ask me anything about cryptocurrency markets, trading data, or technical analysis!</p>
 73 |                   <div class="example-queries">
 74 |                     <button class="example-query">What's the current BTC price?</button>
 75 |                     <button class="example-query">Check for XRPUSDT order blocks in the 30 minute window</button>
 76 |                     <button class="example-query">Review XRPUSDT over the past week, where is the price likely to go in the next day?</button>
 77 |                     <button class="example-query">Analyse XRPUSDT with ML-RSI</button>
 78 |                     <button class="example-query">Compare XRPUSDT RSI with ML-RSI</button>
 79 |                     <button class="example-query">Show me the latest price candles for XRPUSDT</button>
 80 |                   </div>
 81 |                 </div>
 82 |               </div>
 83 |               <div class="chat-input-container">
 84 |                 <div class="chat-input-wrapper">
 85 |                   <textarea
 86 |                     id="chat-input"
 87 |                     class="chat-input"
 88 |                     placeholder="Ask about markets, trading data, or technical analysis..."
 89 |                     rows="1"
 90 |                   ></textarea>
 91 |                   <button id="send-btn" class="send-btn" disabled>
 92 |                     <span class="send-icon">➤</span>
 93 |                   </button>
 94 |                 </div>
 95 |                 <div class="input-status">
 96 |                   <span id="connection-status" class="connection-status">🔴 Disconnected</span>
 97 |                   <span id="typing-indicator" class="typing-indicator hidden">AI is typing...</span>
 98 |                 </div>
 99 |               </div>
100 |             </div>
101 |           </div>
102 | 
103 |           <!-- Tools View -->
104 |           <div id="tools-view" class="view">
105 |             <div class="tools-container">
106 |               <h2>MCP Tools</h2>
107 |               <div class="tools-grid" id="tools-grid">
108 |                 <!-- Tools will be populated dynamically -->
109 |               </div>
110 |             </div>
111 |           </div>
112 | 
113 |           <!-- Dashboard View -->
114 |           <div id="dashboard-view" class="view">
115 |             <div class="dashboard-container">
116 |               <h2>Agent Dashboard</h2>
117 |               <div id="dashboard-content-wrapper">
118 |                 <!-- Agent dashboard will be embedded here -->
119 |               </div>
120 |             </div>
121 |           </div>
122 |         </section>
123 |       </main>
124 |     </div>
125 | 
126 |     <!-- Settings Modal -->
127 |     <div id="settings-modal" class="modal hidden">
128 |       <div class="modal-content">
129 |         <div class="modal-header">
130 |           <h2>Settings</h2>
131 |           <button id="close-settings" class="close-btn">×</button>
132 |         </div>
133 |         <div class="modal-body">
134 |           <div class="settings-section">
135 |             <h3>AI Configuration</h3>
136 |             <label for="ai-endpoint">AI Endpoint:</label>
137 |             <input type="url" id="ai-endpoint" placeholder="https://ollama.example.com" />
138 |             <label for="ai-model">Model:</label>
139 |             <input type="text" id="ai-model" placeholder="qwen3-30b-a3b-ud-nothink-128k:q4_k_xl" />
140 |           </div>
141 |           <div class="settings-section">
142 |             <h3>MCP Server</h3>
143 |             <label for="mcp-endpoint">MCP Endpoint:</label>
144 |             <input type="url" id="mcp-endpoint" placeholder="(auto-detect current domain)" />
145 |           </div>
146 |           <div class="settings-section">
147 |             <h3>Agent Settings</h3>
148 |             <label class="checkbox-label">
149 |               <input type="checkbox" id="agent-mode-enabled" />
150 |               <span>Enable Agent Mode</span>
151 |               <small>Use multi-step reasoning agent instead of simple AI chat</small>
152 |             </label>
153 |             <label for="max-iterations">Max Iterations:</label>
154 |             <input type="number" id="max-iterations" min="1" max="20" value="5" />
155 |             <label for="tool-timeout">Tool Timeout (ms):</label>
156 |             <input type="number" id="tool-timeout" min="5000" max="120000" step="1000" value="30000" />
157 |             <label class="checkbox-label">
158 |               <input type="checkbox" id="show-workflow-steps" />
159 |               <span>Show Workflow Steps</span>
160 |               <small>Display agent reasoning process</small>
161 |             </label>
162 |             <label class="checkbox-label">
163 |               <input type="checkbox" id="show-tool-calls" />
164 |               <span>Show Tool Calls</span>
165 |               <small>Display tool execution details</small>
166 |             </label>
167 |             <label class="checkbox-label">
168 |               <input type="checkbox" id="enable-debug-mode" />
169 |               <span>Debug Mode</span>
170 |               <small>Enable verbose logging</small>
171 |             </label>
172 |           </div>
173 |         </div>
174 |         <div class="modal-footer">
175 |           <button id="save-settings" class="save-btn">Save Settings</button>
176 |         </div>
177 |       </div>
178 |     </div>
179 | 
180 |     <!-- Agent Dashboard -->
181 |     <div id="agent-dashboard-container"></div>
182 | 
183 |     <!-- Data Verification Panel -->
184 |     <div id="verification-panel-container"></div>
185 |   </div>
186 | 
187 |   <script type="module" src="/src/main.ts"></script>
188 | </body>
189 | </html>
190 | 
```

--------------------------------------------------------------------------------
/src/tools/GetOrderBlocks.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Tool, CallToolResult } from "@modelcontextprotocol/sdk/types.js"
  2 | import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js"
  3 | import { z } from "zod"
  4 | import { BaseToolImplementation } from "./BaseTool.js"
  5 | import { KlineData } from "../utils/mathUtils.js"
  6 | import { 
  7 |   detectOrderBlocks,
  8 |   getActiveLevels,
  9 |   calculateOrderBlockStats,
 10 |   OrderBlock,
 11 |   VolumeAnalysisConfig
 12 | } from "../utils/volumeAnalysis.js"
 13 | import { GetKlineParamsV5, KlineIntervalV3 } from "bybit-api"
 14 | 
 15 | // Zod schema for input validation
 16 | const inputSchema = z.object({
 17 |   symbol: z.string()
 18 |     .min(1, "Symbol is required")
 19 |     .regex(/^[A-Z0-9]+$/, "Symbol must contain only uppercase letters and numbers"),
 20 |   category: z.enum(["spot", "linear", "inverse"]),
 21 |   interval: z.enum(["1", "3", "5", "15", "30", "60", "120", "240", "360", "720", "D", "W", "M"]),
 22 |   volumePivotLength: z.number().min(1).max(20).optional().default(5),
 23 |   bullishBlocks: z.number().min(1).max(10).optional().default(3),
 24 |   bearishBlocks: z.number().min(1).max(10).optional().default(3),
 25 |   mitigationMethod: z.enum(["wick", "close"]).optional().default("wick"),
 26 |   limit: z.number().min(100).max(1000).optional().default(200)
 27 | })
 28 | 
 29 | type ToolArguments = z.infer<typeof inputSchema>
 30 | 
 31 | interface OrderBlockResponse {
 32 |   symbol: string;
 33 |   interval: string;
 34 |   bullishBlocks: Array<{
 35 |     id: string;
 36 |     timestamp: number;
 37 |     top: number;
 38 |     bottom: number;
 39 |     average: number;
 40 |     volume: number;
 41 |     mitigated: boolean;
 42 |     mitigationTime?: number;
 43 |   }>;
 44 |   bearishBlocks: Array<{
 45 |     id: string;
 46 |     timestamp: number;
 47 |     top: number;
 48 |     bottom: number;
 49 |     average: number;
 50 |     volume: number;
 51 |     mitigated: boolean;
 52 |     mitigationTime?: number;
 53 |   }>;
 54 |   currentSupport: number[];
 55 |   currentResistance: number[];
 56 |   metadata: {
 57 |     volumePivotLength: number;
 58 |     mitigationMethod: string;
 59 |     blocksDetected: number;
 60 |     activeBullishBlocks: number;
 61 |     activeBearishBlocks: number;
 62 |     averageVolume: number;
 63 |     calculationTime: number;
 64 |   };
 65 | }
 66 | 
 67 | class GetOrderBlocks extends BaseToolImplementation {
 68 |   name = "get_order_blocks"
 69 |   toolDefinition: Tool = {
 70 |     name: this.name,
 71 |     description: "Detect institutional order accumulation zones based on volume analysis. Identifies bullish and bearish order blocks using volume peaks and tracks their mitigation status.",
 72 |     inputSchema: {
 73 |       type: "object",
 74 |       properties: {
 75 |         symbol: {
 76 |           type: "string",
 77 |           description: "Trading pair symbol (e.g., 'BTCUSDT')",
 78 |           pattern: "^[A-Z0-9]+$"
 79 |         },
 80 |         category: {
 81 |           type: "string",
 82 |           description: "Category of the instrument",
 83 |           enum: ["spot", "linear", "inverse"]
 84 |         },
 85 |         interval: {
 86 |           type: "string",
 87 |           description: "Kline interval",
 88 |           enum: ["1", "3", "5", "15", "30", "60", "120", "240", "360", "720", "D", "W", "M"]
 89 |         },
 90 |         volumePivotLength: {
 91 |           type: "number",
 92 |           description: "Volume pivot detection period (default: 5)",
 93 |           minimum: 1,
 94 |           maximum: 20
 95 |         },
 96 |         bullishBlocks: {
 97 |           type: "number",
 98 |           description: "Number of bullish blocks to track (default: 3)",
 99 |           minimum: 1,
100 |           maximum: 10
101 |         },
102 |         bearishBlocks: {
103 |           type: "number",
104 |           description: "Number of bearish blocks to track (default: 3)",
105 |           minimum: 1,
106 |           maximum: 10
107 |         },
108 |         mitigationMethod: {
109 |           type: "string",
110 |           description: "Mitigation detection method (default: wick)",
111 |           enum: ["wick", "close"]
112 |         },
113 |         limit: {
114 |           type: "number",
115 |           description: "Historical data points to analyse (default: 200)",
116 |           minimum: 100,
117 |           maximum: 1000
118 |         }
119 |       },
120 |       required: ["symbol", "category", "interval"]
121 |     }
122 |   }
123 | 
124 |   async toolCall(request: z.infer<typeof CallToolRequestSchema>): Promise<CallToolResult> {
125 |     const startTime = Date.now()
126 |     
127 |     try {
128 |       this.logInfo("Starting get_order_blocks tool call")
129 | 
130 |       // Parse and validate input
131 |       const validationResult = inputSchema.safeParse(request.params.arguments)
132 |       if (!validationResult.success) {
133 |         const errorDetails = validationResult.error.errors.map(err => ({
134 |           field: err.path.join('.'),
135 |           message: err.message,
136 |           code: err.code
137 |         }))
138 |         throw new Error(`Invalid input: ${JSON.stringify(errorDetails)}`)
139 |       }
140 | 
141 |       const args = validationResult.data
142 | 
143 |       // Fetch kline data
144 |       const klineData = await this.fetchKlineData(args)
145 |       
146 |       if (klineData.length < args.volumePivotLength * 2 + 10) {
147 |         throw new Error(`Insufficient data. Need at least ${args.volumePivotLength * 2 + 10} data points, got ${klineData.length}`)
148 |       }
149 | 
150 |       // Configure volume analysis
151 |       const config: VolumeAnalysisConfig = {
152 |         volumePivotLength: args.volumePivotLength,
153 |         bullishBlocks: args.bullishBlocks,
154 |         bearishBlocks: args.bearishBlocks,
155 |         mitigationMethod: args.mitigationMethod
156 |       }
157 | 
158 |       // Detect order blocks
159 |       const { bullishBlocks, bearishBlocks } = detectOrderBlocks(klineData, config)
160 | 
161 |       // Get active support and resistance levels
162 |       const { support, resistance } = getActiveLevels([...bullishBlocks, ...bearishBlocks])
163 | 
164 |       // Calculate statistics
165 |       const stats = calculateOrderBlockStats(bullishBlocks, bearishBlocks)
166 | 
167 |       const calculationTime = Date.now() - startTime
168 | 
169 |       const response: OrderBlockResponse = {
170 |         symbol: args.symbol,
171 |         interval: args.interval,
172 |         bullishBlocks: bullishBlocks.map(block => ({
173 |           id: block.id,
174 |           timestamp: block.timestamp,
175 |           top: block.top,
176 |           bottom: block.bottom,
177 |           average: block.average,
178 |           volume: block.volume,
179 |           mitigated: block.mitigated,
180 |           mitigationTime: block.mitigationTime
181 |         })),
182 |         bearishBlocks: bearishBlocks.map(block => ({
183 |           id: block.id,
184 |           timestamp: block.timestamp,
185 |           top: block.top,
186 |           bottom: block.bottom,
187 |           average: block.average,
188 |           volume: block.volume,
189 |           mitigated: block.mitigated,
190 |           mitigationTime: block.mitigationTime
191 |         })),
192 |         currentSupport: support.slice(0, 5), // Top 5 support levels
193 |         currentResistance: resistance.slice(0, 5), // Top 5 resistance levels
194 |         metadata: {
195 |           volumePivotLength: args.volumePivotLength,
196 |           mitigationMethod: args.mitigationMethod,
197 |           blocksDetected: stats.totalBlocks,
198 |           activeBullishBlocks: stats.activeBullishBlocks,
199 |           activeBearishBlocks: stats.activeBearishBlocks,
200 |           averageVolume: stats.averageVolume,
201 |           calculationTime
202 |         }
203 |       }
204 | 
205 |       this.logInfo(`Order block detection completed in ${calculationTime}ms. Found ${stats.totalBlocks} blocks (${stats.activeBullishBlocks} bullish, ${stats.activeBearishBlocks} bearish active)`)
206 |       return this.formatResponse(response)
207 | 
208 |     } catch (error) {
209 |       this.logInfo(`Order block detection failed: ${error instanceof Error ? error.message : String(error)}`)
210 |       return this.handleError(error)
211 |     }
212 |   }
213 | 
214 |   private async fetchKlineData(args: ToolArguments): Promise<KlineData[]> {
215 |     const params: GetKlineParamsV5 = {
216 |       category: args.category,
217 |       symbol: args.symbol,
218 |       interval: args.interval as KlineIntervalV3,
219 |       limit: args.limit
220 |     }
221 | 
222 |     const response = await this.executeRequest(() => this.client.getKline(params))
223 |     
224 |     if (!response.list || response.list.length === 0) {
225 |       throw new Error("No kline data received from API")
226 |     }
227 | 
228 |     // Convert API response to KlineData format
229 |     return response.list.map(kline => ({
230 |       timestamp: parseInt(kline[0]),
231 |       open: parseFloat(kline[1]),
232 |       high: parseFloat(kline[2]),
233 |       low: parseFloat(kline[3]),
234 |       close: parseFloat(kline[4]),
235 |       volume: parseFloat(kline[5])
236 |     })).reverse() // Reverse to get chronological order
237 |   }
238 | }
239 | 
240 | export default GetOrderBlocks
241 | 
```

--------------------------------------------------------------------------------
/src/utils/mathUtils.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Mathematical utility functions for technical analysis
  3 |  * Inspired by pinescript mathematical operations
  4 |  */
  5 | 
  6 | export interface KlineData {
  7 |   timestamp: number;
  8 |   open: number;
  9 |   high: number;
 10 |   low: number;
 11 |   close: number;
 12 |   volume: number;
 13 | }
 14 | 
 15 | /**
 16 |  * Calculate RSI (Relative Strength Index)
 17 |  */
 18 | export function calculateRSI(prices: number[], period: number = 14): number[] {
 19 |   if (prices.length < period + 1) {
 20 |     return []
 21 |   }
 22 | 
 23 |   const rsiValues: number[] = []
 24 |   const gains: number[] = []
 25 |   const losses: number[] = []
 26 | 
 27 |   // Calculate initial gains and losses
 28 |   for (let i = 1; i < prices.length; i++) {
 29 |     const change = prices[i] - prices[i - 1]
 30 |     gains.push(change > 0 ? change : 0)
 31 |     losses.push(change < 0 ? Math.abs(change) : 0)
 32 |   }
 33 | 
 34 |   // Calculate initial average gain and loss
 35 |   let avgGain = gains.slice(0, period).reduce((sum, gain) => sum + gain, 0) / period
 36 |   let avgLoss = losses.slice(0, period).reduce((sum, loss) => sum + loss, 0) / period
 37 | 
 38 |   // Calculate first RSI value
 39 |   const rs = avgGain / (avgLoss || 0.0001) // Avoid division by zero
 40 |   rsiValues.push(100 - (100 / (1 + rs)))
 41 | 
 42 |   // Calculate subsequent RSI values using smoothed averages
 43 |   for (let i = period; i < gains.length; i++) {
 44 |     avgGain = (avgGain * (period - 1) + gains[i]) / period
 45 |     avgLoss = (avgLoss * (period - 1) + losses[i]) / period
 46 |     
 47 |     const rs = avgGain / (avgLoss || 0.0001)
 48 |     rsiValues.push(100 - (100 / (1 + rs)))
 49 |   }
 50 | 
 51 |   return rsiValues
 52 | }
 53 | 
 54 | /**
 55 |  * Calculate momentum (rate of change)
 56 |  */
 57 | export function calculateMomentum(values: number[], period: number = 1): number[] {
 58 |   const momentum: number[] = []
 59 |   
 60 |   for (let i = period; i < values.length; i++) {
 61 |     momentum.push(values[i] - values[i - period])
 62 |   }
 63 |   
 64 |   return momentum
 65 | }
 66 | 
 67 | /**
 68 |  * Calculate volatility using standard deviation
 69 |  */
 70 | export function calculateVolatility(values: number[], period: number = 10): number[] {
 71 |   const volatility: number[] = []
 72 |   
 73 |   for (let i = period - 1; i < values.length; i++) {
 74 |     const slice = values.slice(i - period + 1, i + 1)
 75 |     const mean = slice.reduce((sum, val) => sum + val, 0) / slice.length
 76 |     const variance = slice.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / slice.length
 77 |     volatility.push(Math.sqrt(variance))
 78 |   }
 79 |   
 80 |   return volatility
 81 | }
 82 | 
 83 | /**
 84 |  * Calculate linear regression slope
 85 |  */
 86 | export function calculateSlope(values: number[], period: number = 5): number[] {
 87 |   const slopes: number[] = []
 88 |   
 89 |   for (let i = period - 1; i < values.length; i++) {
 90 |     const slice = values.slice(i - period + 1, i + 1)
 91 |     const n = slice.length
 92 |     const x = Array.from({ length: n }, (_, idx) => idx)
 93 |     
 94 |     const sumX = x.reduce((sum, val) => sum + val, 0)
 95 |     const sumY = slice.reduce((sum, val) => sum + val, 0)
 96 |     const sumXY = x.reduce((sum, val, idx) => sum + val * slice[idx], 0)
 97 |     const sumXX = x.reduce((sum, val) => sum + val * val, 0)
 98 |     
 99 |     const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX)
100 |     slopes.push(slope)
101 |   }
102 |   
103 |   return slopes
104 | }
105 | 
106 | /**
107 |  * Min-max normalisation
108 |  */
109 | export function normalize(values: number[], period: number): number[] {
110 |   const normalized: number[] = []
111 |   
112 |   for (let i = period - 1; i < values.length; i++) {
113 |     const slice = values.slice(i - period + 1, i + 1)
114 |     const min = Math.min(...slice)
115 |     const max = Math.max(...slice)
116 |     const range = max - min
117 |     
118 |     if (range === 0) {
119 |       normalized.push(0.5) // Middle value when no variation
120 |     } else {
121 |       normalized.push((values[i] - min) / range)
122 |     }
123 |   }
124 |   
125 |   return normalized
126 | }
127 | 
128 | /**
129 |  * Calculate Euclidean distance between two feature vectors
130 |  */
131 | export function euclideanDistance(vector1: number[], vector2: number[]): number {
132 |   if (vector1.length !== vector2.length) {
133 |     throw new Error("Vectors must have the same length")
134 |   }
135 |   
136 |   const sumSquares = vector1.reduce((sum, val, idx) => {
137 |     return sum + Math.pow(val - vector2[idx], 2)
138 |   }, 0)
139 |   
140 |   return Math.sqrt(sumSquares)
141 | }
142 | 
143 | /**
144 |  * Simple Moving Average
145 |  */
146 | export function sma(values: number[], period: number): number[] {
147 |   const smaValues: number[] = []
148 |   
149 |   for (let i = period - 1; i < values.length; i++) {
150 |     const slice = values.slice(i - period + 1, i + 1)
151 |     const average = slice.reduce((sum, val) => sum + val, 0) / slice.length
152 |     smaValues.push(average)
153 |   }
154 |   
155 |   return smaValues
156 | }
157 | 
158 | /**
159 |  * Exponential Moving Average
160 |  */
161 | export function ema(values: number[], period: number): number[] {
162 |   if (values.length === 0) return []
163 |   
164 |   const emaValues: number[] = []
165 |   const multiplier = 2 / (period + 1)
166 |   
167 |   // First EMA value is the first price
168 |   emaValues.push(values[0])
169 |   
170 |   for (let i = 1; i < values.length; i++) {
171 |     const emaValue = (values[i] * multiplier) + (emaValues[i - 1] * (1 - multiplier))
172 |     emaValues.push(emaValue)
173 |   }
174 |   
175 |   return emaValues
176 | }
177 | 
178 | /**
179 |  * Kalman Filter implementation for smoothing
180 |  */
181 | export function kalmanFilter(values: number[], processNoise: number = 0.01, measurementNoise: number = 0.1): number[] {
182 |   if (values.length === 0) return []
183 |   
184 |   const filtered: number[] = []
185 |   let estimate = values[0]
186 |   let errorEstimate = 1.0
187 |   
188 |   filtered.push(estimate)
189 |   
190 |   for (let i = 1; i < values.length; i++) {
191 |     // Prediction step
192 |     const predictedEstimate = estimate
193 |     const predictedError = errorEstimate + processNoise
194 |     
195 |     // Update step
196 |     const kalmanGain = predictedError / (predictedError + measurementNoise)
197 |     estimate = predictedEstimate + kalmanGain * (values[i] - predictedEstimate)
198 |     errorEstimate = (1 - kalmanGain) * predictedError
199 |     
200 |     filtered.push(estimate)
201 |   }
202 |   
203 |   return filtered
204 | }
205 | 
206 | /**
207 |  * ALMA (Arnaud Legoux Moving Average) implementation
208 |  */
209 | export function alma(values: number[], period: number, offset: number = 0.85, sigma: number = 6): number[] {
210 |   if (values.length < period) return []
211 |   
212 |   const almaValues: number[] = []
213 |   const m = Math.floor(offset * (period - 1))
214 |   const s = period / sigma
215 |   
216 |   for (let i = period - 1; i < values.length; i++) {
217 |     let weightedSum = 0
218 |     let weightSum = 0
219 |     
220 |     for (let j = 0; j < period; j++) {
221 |       const weight = Math.exp(-Math.pow(j - m, 2) / (2 * Math.pow(s, 2)))
222 |       weightedSum += values[i - period + 1 + j] * weight
223 |       weightSum += weight
224 |     }
225 |     
226 |     almaValues.push(weightedSum / weightSum)
227 |   }
228 |   
229 |   return almaValues
230 | }
231 | 
232 | /**
233 |  * Double EMA implementation
234 |  */
235 | export function doubleEma(values: number[], period: number): number[] {
236 |   const firstEma = ema(values, period)
237 |   const secondEma = ema(firstEma, period)
238 |   
239 |   return firstEma.map((val, idx) => {
240 |     if (idx < secondEma.length) {
241 |       return 2 * val - secondEma[idx]
242 |     }
243 |     return val
244 |   }).slice(period - 1) // Remove initial values that don't have corresponding second EMA
245 | }
246 | 
247 | /**
248 |  * Extract features for KNN analysis
249 |  */
250 | export interface FeatureVector {
251 |   rsi: number;
252 |   momentum?: number;
253 |   volatility?: number;
254 |   slope?: number;
255 |   priceMomentum?: number;
256 | }
257 | 
258 | export function extractFeatures(
259 |   klineData: KlineData[],
260 |   index: number,
261 |   rsiValues: number[],
262 |   featureCount: number,
263 |   lookbackPeriod: number
264 | ): FeatureVector | null {
265 |   if (index < lookbackPeriod || index >= rsiValues.length) {
266 |     return null
267 |   }
268 |   
269 |   const features: FeatureVector = {
270 |     rsi: rsiValues[index]
271 |   }
272 |   
273 |   if (featureCount >= 2) {
274 |     const rsiMomentum = calculateMomentum(rsiValues.slice(0, index + 1), 3)
275 |     if (rsiMomentum.length > 0) {
276 |       features.momentum = rsiMomentum[rsiMomentum.length - 1]
277 |     }
278 |   }
279 |   
280 |   if (featureCount >= 3) {
281 |     const rsiVolatility = calculateVolatility(rsiValues.slice(0, index + 1), 10)
282 |     if (rsiVolatility.length > 0) {
283 |       features.volatility = rsiVolatility[rsiVolatility.length - 1]
284 |     }
285 |   }
286 |   
287 |   if (featureCount >= 4) {
288 |     const rsiSlope = calculateSlope(rsiValues.slice(0, index + 1), 5)
289 |     if (rsiSlope.length > 0) {
290 |       features.slope = rsiSlope[rsiSlope.length - 1]
291 |     }
292 |   }
293 |   
294 |   if (featureCount >= 5) {
295 |     const closePrices = klineData.slice(0, index + 1).map(k => k.close)
296 |     const priceMomentum = calculateMomentum(closePrices, 5)
297 |     if (priceMomentum.length > 0) {
298 |       features.priceMomentum = priceMomentum[priceMomentum.length - 1]
299 |     }
300 |   }
301 |   
302 |   return features
303 | }
304 | 
```

--------------------------------------------------------------------------------
/src/utils/volumeAnalysis.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Volume analysis utilities for Order Block Detection
  3 |  * Based on the pinescript order-block-detector implementation
  4 |  */
  5 | 
  6 | import { KlineData } from './mathUtils.js'
  7 | 
  8 | export interface OrderBlock {
  9 |   id: string;
 10 |   timestamp: number;
 11 |   top: number;
 12 |   bottom: number;
 13 |   average: number;
 14 |   volume: number;
 15 |   mitigated: boolean;
 16 |   mitigationTime?: number;
 17 |   type: 'bullish' | 'bearish';
 18 | }
 19 | 
 20 | export interface VolumeAnalysisConfig {
 21 |   volumePivotLength: number;
 22 |   bullishBlocks: number;
 23 |   bearishBlocks: number;
 24 |   mitigationMethod: 'wick' | 'close';
 25 | }
 26 | 
 27 | /**
 28 |  * Detect volume pivots (peaks) in the data
 29 |  */
 30 | export function detectVolumePivots(klineData: KlineData[], pivotLength: number): number[] {
 31 |   const pivotIndices: number[] = []
 32 |   
 33 |   for (let i = pivotLength; i < klineData.length - pivotLength; i++) {
 34 |     const currentVolume = klineData[i].volume
 35 |     let isPivot = true
 36 |     
 37 |     // Check if current volume is higher than surrounding volumes
 38 |     for (let j = i - pivotLength; j <= i + pivotLength; j++) {
 39 |       if (j !== i && klineData[j].volume >= currentVolume) {
 40 |         isPivot = false
 41 |         break
 42 |       }
 43 |     }
 44 |     
 45 |     if (isPivot) {
 46 |       pivotIndices.push(i)
 47 |     }
 48 |   }
 49 |   
 50 |   return pivotIndices
 51 | }
 52 | 
 53 | /**
 54 |  * Determine market structure (uptrend/downtrend) at a given point
 55 |  */
 56 | export function getMarketStructure(klineData: KlineData[], index: number, lookback: number): 'uptrend' | 'downtrend' {
 57 |   const startIndex = Math.max(0, index - lookback)
 58 |   const slice = klineData.slice(startIndex, index + 1)
 59 |   
 60 |   if (slice.length < 2) return 'uptrend'
 61 |   
 62 |   const highs = slice.map(k => k.high)
 63 |   const lows = slice.map(k => k.low)
 64 |   
 65 |   const currentHigh = highs[highs.length - 1]
 66 |   const currentLow = lows[lows.length - 1]
 67 |   const previousHigh = Math.max(...highs.slice(0, -1))
 68 |   const previousLow = Math.min(...lows.slice(0, -1))
 69 |   
 70 |   // Simple trend detection based on higher highs/lower lows
 71 |   if (currentHigh > previousHigh && currentLow > previousLow) {
 72 |     return 'uptrend'
 73 |   } else if (currentHigh < previousHigh && currentLow < previousLow) {
 74 |     return 'downtrend'
 75 |   }
 76 |   
 77 |   // Default to uptrend if unclear
 78 |   return 'uptrend'
 79 | }
 80 | 
 81 | /**
 82 |  * Create order block from volume pivot
 83 |  */
 84 | export function createOrderBlock(
 85 |   klineData: KlineData[],
 86 |   pivotIndex: number,
 87 |   pivotLength: number,
 88 |   type: 'bullish' | 'bearish'
 89 | ): OrderBlock {
 90 |   const kline = klineData[pivotIndex]
 91 |   const { high, low, close, volume, timestamp } = kline
 92 |   
 93 |   let top: number, bottom: number
 94 |   
 95 |   if (type === 'bullish') {
 96 |     // Bullish order block: from low to median (hl2)
 97 |     bottom = low
 98 |     top = (high + low) / 2
 99 |   } else {
100 |     // Bearish order block: from median (hl2) to high
101 |     bottom = (high + low) / 2
102 |     top = high
103 |   }
104 |   
105 |   const average = (top + bottom) / 2
106 |   
107 |   return {
108 |     id: `${type}_${timestamp}_${pivotIndex}`,
109 |     timestamp,
110 |     top,
111 |     bottom,
112 |     average,
113 |     volume,
114 |     mitigated: false,
115 |     type
116 |   }
117 | }
118 | 
119 | /**
120 |  * Check if an order block has been mitigated
121 |  */
122 | export function checkMitigation(
123 |   orderBlock: OrderBlock,
124 |   klineData: KlineData[],
125 |   currentIndex: number,
126 |   method: 'wick' | 'close'
127 | ): boolean {
128 |   if (orderBlock.mitigated) return true
129 |   
130 |   const currentKline = klineData[currentIndex]
131 |   
132 |   if (orderBlock.type === 'bullish') {
133 |     // Bullish order block is mitigated when price goes below the bottom
134 |     if (method === 'wick') {
135 |       return currentKline.low < orderBlock.bottom
136 |     } else {
137 |       return currentKline.close < orderBlock.bottom
138 |     }
139 |   } else {
140 |     // Bearish order block is mitigated when price goes above the top
141 |     if (method === 'wick') {
142 |       return currentKline.high > orderBlock.top
143 |     } else {
144 |       return currentKline.close > orderBlock.top
145 |     }
146 |   }
147 | }
148 | 
149 | /**
150 |  * Update order block mitigation status
151 |  */
152 | export function updateOrderBlockMitigation(
153 |   orderBlocks: OrderBlock[],
154 |   klineData: KlineData[],
155 |   currentIndex: number,
156 |   method: 'wick' | 'close'
157 | ): { mitigatedBullish: boolean; mitigatedBearish: boolean } {
158 |   let mitigatedBullish = false
159 |   let mitigatedBearish = false
160 |   
161 |   for (const block of orderBlocks) {
162 |     if (!block.mitigated && checkMitigation(block, klineData, currentIndex, method)) {
163 |       block.mitigated = true
164 |       block.mitigationTime = klineData[currentIndex].timestamp
165 |       
166 |       if (block.type === 'bullish') {
167 |         mitigatedBullish = true
168 |       } else {
169 |         mitigatedBearish = true
170 |       }
171 |     }
172 |   }
173 |   
174 |   return { mitigatedBullish, mitigatedBearish }
175 | }
176 | 
177 | /**
178 |  * Remove mitigated order blocks from arrays
179 |  */
180 | export function removeMitigatedBlocks(orderBlocks: OrderBlock[]): OrderBlock[] {
181 |   return orderBlocks.filter(block => !block.mitigated)
182 | }
183 | 
184 | /**
185 |  * Get active support and resistance levels from order blocks
186 |  */
187 | export function getActiveLevels(orderBlocks: OrderBlock[]): {
188 |   support: number[];
189 |   resistance: number[];
190 | } {
191 |   const activeBlocks = orderBlocks.filter(block => !block.mitigated)
192 |   
193 |   const support = activeBlocks
194 |     .filter(block => block.type === 'bullish')
195 |     .map(block => block.average)
196 |     .sort((a, b) => b - a) // Descending order
197 |   
198 |   const resistance = activeBlocks
199 |     .filter(block => block.type === 'bearish')
200 |     .map(block => block.average)
201 |     .sort((a, b) => a - b) // Ascending order
202 |   
203 |   return { support, resistance }
204 | }
205 | 
206 | /**
207 |  * Detect order blocks from kline data
208 |  */
209 | export function detectOrderBlocks(
210 |   klineData: KlineData[],
211 |   config: VolumeAnalysisConfig
212 | ): {
213 |   bullishBlocks: OrderBlock[];
214 |   bearishBlocks: OrderBlock[];
215 |   volumePivots: number[];
216 | } {
217 |   const volumePivots = detectVolumePivots(klineData, config.volumePivotLength)
218 |   const bullishBlocks: OrderBlock[] = []
219 |   const bearishBlocks: OrderBlock[] = []
220 |   
221 |   for (const pivotIndex of volumePivots) {
222 |     // Determine market structure at pivot point
223 |     const marketStructure = getMarketStructure(klineData, pivotIndex, config.volumePivotLength)
224 |     
225 |     if (marketStructure === 'uptrend') {
226 |       // In uptrend, create bullish order block
227 |       const block = createOrderBlock(klineData, pivotIndex, config.volumePivotLength, 'bullish')
228 |       bullishBlocks.push(block)
229 |     } else {
230 |       // In downtrend, create bearish order block
231 |       const block = createOrderBlock(klineData, pivotIndex, config.volumePivotLength, 'bearish')
232 |       bearishBlocks.push(block)
233 |     }
234 |   }
235 |   
236 |   // Process mitigation for all blocks
237 |   for (let i = 0; i < klineData.length; i++) {
238 |     updateOrderBlockMitigation([...bullishBlocks, ...bearishBlocks], klineData, i, config.mitigationMethod)
239 |   }
240 |   
241 |   // Keep only the most recent unmitigated blocks
242 |   const activeBullishBlocks = removeMitigatedBlocks(bullishBlocks)
243 |     .slice(-config.bullishBlocks)
244 |   
245 |   const activeBearishBlocks = removeMitigatedBlocks(bearishBlocks)
246 |     .slice(-config.bearishBlocks)
247 |   
248 |   return {
249 |     bullishBlocks: activeBullishBlocks,
250 |     bearishBlocks: activeBearishBlocks,
251 |     volumePivots
252 |   }
253 | }
254 | 
255 | /**
256 |  * Calculate order block statistics
257 |  */
258 | export function calculateOrderBlockStats(
259 |   bullishBlocks: OrderBlock[],
260 |   bearishBlocks: OrderBlock[]
261 | ): {
262 |   totalBlocks: number;
263 |   activeBullishBlocks: number;
264 |   activeBearishBlocks: number;
265 |   mitigatedBlocks: number;
266 |   averageVolume: number;
267 | } {
268 |   const allBlocks = [...bullishBlocks, ...bearishBlocks]
269 |   const activeBlocks = allBlocks.filter(block => !block.mitigated)
270 |   const mitigatedBlocks = allBlocks.filter(block => block.mitigated)
271 |   
272 |   const activeBullishBlocks = bullishBlocks.filter(block => !block.mitigated).length
273 |   const activeBearishBlocks = bearishBlocks.filter(block => !block.mitigated).length
274 |   
275 |   const averageVolume = allBlocks.length > 0
276 |     ? allBlocks.reduce((sum, block) => sum + block.volume, 0) / allBlocks.length
277 |     : 0
278 |   
279 |   return {
280 |     totalBlocks: allBlocks.length,
281 |     activeBullishBlocks,
282 |     activeBearishBlocks,
283 |     mitigatedBlocks: mitigatedBlocks.length,
284 |     averageVolume
285 |   }
286 | }
287 | 
288 | /**
289 |  * Find nearest order blocks to current price
290 |  */
291 | export function findNearestOrderBlocks(
292 |   orderBlocks: OrderBlock[],
293 |   currentPrice: number,
294 |   maxDistance: number = 0.05 // 5% price distance
295 | ): OrderBlock[] {
296 |   return orderBlocks
297 |     .filter(block => !block.mitigated)
298 |     .filter(block => {
299 |       const distance = Math.abs(block.average - currentPrice) / currentPrice
300 |       return distance <= maxDistance
301 |     })
302 |     .sort((a, b) => {
303 |       const distanceA = Math.abs(a.average - currentPrice)
304 |       const distanceB = Math.abs(b.average - currentPrice)
305 |       return distanceA - distanceB
306 |     })
307 | }
308 | 
```

--------------------------------------------------------------------------------
/webui/src/styles/citations.css:
--------------------------------------------------------------------------------

```css
  1 | /**
  2 |  * Citation system styles
  3 |  */
  4 | 
  5 | /* Citation reference styling */
  6 | .citation-ref {
  7 |   display: inline-block;
  8 |   background: var(--accent-color, #007acc);
  9 |   color: white;
 10 |   padding: 2px 6px;
 11 |   border-radius: 4px;
 12 |   font-size: 0.8em;
 13 |   font-weight: 500;
 14 |   cursor: pointer;
 15 |   transition: all 0.2s ease;
 16 |   text-decoration: none;
 17 |   margin: 0 2px;
 18 |   vertical-align: baseline;
 19 | }
 20 | 
 21 | .citation-ref:hover {
 22 |   background: var(--accent-hover, #005a9e);
 23 |   transform: translateY(-1px);
 24 |   box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
 25 | }
 26 | 
 27 | .citation-ref:focus {
 28 |   outline: 2px solid var(--focus-color, #4a90e2);
 29 |   outline-offset: 2px;
 30 | }
 31 | 
 32 | .citation-ref.no-data {
 33 |   background: var(--warning-color, #ff9800);
 34 |   cursor: not-allowed;
 35 | }
 36 | 
 37 | .citation-ref.no-data:hover {
 38 |   background: var(--warning-hover, #f57c00);
 39 |   transform: none;
 40 | }
 41 | 
 42 | /* Citation tooltip */
 43 | .citation-tooltip-container {
 44 |   background: var(--tooltip-bg, #ffffff);
 45 |   color: var(--tooltip-text, #333333);
 46 |   border: 1px solid var(--tooltip-border, #e0e0e0);
 47 |   border-radius: 12px;
 48 |   padding: 16px;
 49 |   box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
 50 |   max-width: 320px;
 51 |   font-size: 0.9em;
 52 |   z-index: 1000;
 53 |   animation: tooltipFadeIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
 54 |   backdrop-filter: blur(8px);
 55 |   position: relative;
 56 | }
 57 | 
 58 | /* Tooltip arrow */
 59 | .citation-tooltip-container::before {
 60 |   content: '';
 61 |   position: absolute;
 62 |   top: -6px;
 63 |   left: 50%;
 64 |   transform: translateX(-50%);
 65 |   width: 12px;
 66 |   height: 12px;
 67 |   background: var(--tooltip-bg, #ffffff);
 68 |   border: 1px solid var(--tooltip-border, #e0e0e0);
 69 |   border-bottom: none;
 70 |   border-right: none;
 71 |   transform: translateX(-50%) rotate(45deg);
 72 |   z-index: -1;
 73 | }
 74 | 
 75 | .citation-tooltip {
 76 |   display: flex;
 77 |   flex-direction: column;
 78 |   gap: 8px;
 79 | }
 80 | 
 81 | .citation-header {
 82 |   display: flex;
 83 |   justify-content: space-between;
 84 |   align-items: center;
 85 |   border-bottom: 1px solid var(--tooltip-border, #f0f0f0);
 86 |   padding-bottom: 8px;
 87 |   margin-bottom: 8px;
 88 | }
 89 | 
 90 | .citation-id {
 91 |   font-weight: 600;
 92 |   color: var(--accent-color, #007acc);
 93 |   font-size: 0.9em;
 94 |   background: var(--accent-bg, #f0f8ff);
 95 |   padding: 2px 8px;
 96 |   border-radius: 6px;
 97 |   border: 1px solid var(--accent-border, #b3d9ff);
 98 | }
 99 | 
100 | .citation-tool {
101 |   font-size: 0.8em;
102 |   color: var(--text-muted, #666666);
103 |   font-weight: 500;
104 |   background: var(--tool-bg, #f8f9fa);
105 |   padding: 2px 6px;
106 |   border-radius: 4px;
107 | }
108 | 
109 | .citation-time {
110 |   font-size: 0.8em;
111 |   color: var(--text-muted, #666666);
112 |   display: flex;
113 |   align-items: center;
114 |   gap: 4px;
115 | }
116 | 
117 | .citation-time::before {
118 |   content: "🕒";
119 |   font-size: 0.9em;
120 | }
121 | 
122 | .citation-endpoint {
123 |   font-size: 0.8em;
124 |   color: var(--text-muted, #666666);
125 |   font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
126 |   background: var(--code-bg, #f6f8fa);
127 |   padding: 4px 6px;
128 |   border-radius: 4px;
129 |   border: 1px solid var(--code-border, #e1e4e8);
130 |   margin-top: 4px;
131 | }
132 | 
133 | .citation-metrics {
134 |   margin-top: 12px;
135 |   background: var(--metrics-bg, #f8f9fa);
136 |   border-radius: 8px;
137 |   padding: 12px;
138 |   border: 1px solid var(--metrics-border, #e9ecef);
139 | }
140 | 
141 | .citation-metrics h4 {
142 |   margin: 0 0 8px 0;
143 |   font-size: 0.85em;
144 |   color: var(--text-primary, #333333);
145 |   font-weight: 600;
146 |   text-transform: uppercase;
147 |   letter-spacing: 0.5px;
148 |   display: flex;
149 |   align-items: center;
150 |   gap: 4px;
151 | }
152 | 
153 | .citation-metrics h4::before {
154 |   content: "📊";
155 |   font-size: 1em;
156 | }
157 | 
158 | .citation-metrics ul {
159 |   margin: 0;
160 |   padding: 0;
161 |   list-style: none;
162 |   display: flex;
163 |   flex-direction: column;
164 |   gap: 6px;
165 | }
166 | 
167 | .citation-metrics li {
168 |   display: flex;
169 |   justify-content: space-between;
170 |   align-items: center;
171 |   padding: 6px 8px;
172 |   font-size: 0.8em;
173 |   background: var(--metric-item-bg, #ffffff);
174 |   border-radius: 6px;
175 |   border: 1px solid var(--metric-item-border, #e9ecef);
176 | }
177 | 
178 | .citation-metrics .metric-high {
179 |   color: var(--success-color, #22c55e);
180 |   font-weight: 600;
181 | }
182 | 
183 | .citation-metrics .metric-medium {
184 |   color: var(--warning-color, #f59e0b);
185 |   font-weight: 600;
186 | }
187 | 
188 | .citation-metrics .metric-low {
189 |   color: var(--text-muted, #6b7280);
190 |   font-weight: 500;
191 | }
192 | 
193 | .metric-label {
194 |   font-weight: 500;
195 |   color: var(--text-secondary, #4b5563);
196 | }
197 | 
198 | .metric-value {
199 |   font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
200 |   font-weight: 600;
201 |   font-size: 0.9em;
202 | }
203 | 
204 | .citation-actions {
205 |   margin-top: 12px;
206 |   padding-top: 12px;
207 |   border-top: 1px solid var(--tooltip-border, #f0f0f0);
208 |   display: flex;
209 |   justify-content: center;
210 | }
211 | 
212 | .btn-view-full {
213 |   background: linear-gradient(135deg, var(--accent-color, #007acc) 0%, var(--accent-secondary, #0066cc) 100%);
214 |   color: white;
215 |   border: none;
216 |   padding: 8px 16px;
217 |   border-radius: 8px;
218 |   font-size: 0.8em;
219 |   font-weight: 600;
220 |   cursor: pointer;
221 |   transition: all 0.2s ease;
222 |   box-shadow: 0 2px 4px rgba(0, 122, 204, 0.2);
223 |   display: flex;
224 |   align-items: center;
225 |   gap: 6px;
226 | }
227 | 
228 | .btn-view-full::before {
229 |   content: "👁️";
230 |   font-size: 0.9em;
231 | }
232 | 
233 | .btn-view-full:hover {
234 |   background: linear-gradient(135deg, var(--accent-hover, #005a9e) 0%, var(--accent-secondary-hover, #0052a3) 100%);
235 |   transform: translateY(-1px);
236 |   box-shadow: 0 4px 8px rgba(0, 122, 204, 0.3);
237 | }
238 | 
239 | .btn-view-full:active {
240 |   transform: translateY(0);
241 |   box-shadow: 0 2px 4px rgba(0, 122, 204, 0.2);
242 | }
243 | 
244 | /* Citation modal */
245 | .citation-modal-overlay {
246 |   position: fixed;
247 |   top: 0;
248 |   left: 0;
249 |   right: 0;
250 |   bottom: 0;
251 |   background: rgba(0, 0, 0, 0.7);
252 |   display: flex;
253 |   align-items: center;
254 |   justify-content: center;
255 |   z-index: 2000;
256 |   animation: fadeIn 0.3s ease;
257 | }
258 | 
259 | .citation-modal {
260 |   background: var(--modal-bg, #fff);
261 |   color: var(--modal-text, #333);
262 |   border-radius: 12px;
263 |   max-width: 80vw;
264 |   max-height: 80vh;
265 |   overflow: hidden;
266 |   box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
267 |   animation: slideIn 0.3s ease;
268 | }
269 | 
270 | .citation-modal-header {
271 |   display: flex;
272 |   justify-content: space-between;
273 |   align-items: center;
274 |   padding: 16px 20px;
275 |   border-bottom: 1px solid var(--modal-border, #eee);
276 |   background: var(--modal-header-bg, #f8f9fa);
277 | }
278 | 
279 | .citation-modal-header h3 {
280 |   margin: 0;
281 |   font-size: 1.2em;
282 |   color: var(--modal-title, #333);
283 | }
284 | 
285 | .citation-modal-close {
286 |   background: none;
287 |   border: none;
288 |   font-size: 1.5em;
289 |   cursor: pointer;
290 |   color: var(--text-muted, #666);
291 |   padding: 0;
292 |   width: 30px;
293 |   height: 30px;
294 |   display: flex;
295 |   align-items: center;
296 |   justify-content: center;
297 |   border-radius: 50%;
298 |   transition: background 0.2s ease;
299 | }
300 | 
301 | .citation-modal-close:hover {
302 |   background: var(--hover-bg, #f0f0f0);
303 | }
304 | 
305 | .citation-modal-content {
306 |   padding: 20px;
307 |   overflow-y: auto;
308 |   max-height: calc(80vh - 80px);
309 | }
310 | 
311 | .citation-info {
312 |   margin-bottom: 20px;
313 | }
314 | 
315 | .citation-info p {
316 |   margin: 8px 0;
317 |   font-size: 0.9em;
318 | }
319 | 
320 | .citation-raw-data {
321 |   margin-top: 20px;
322 | }
323 | 
324 | .citation-raw-data h4 {
325 |   margin: 0 0 12px 0;
326 |   font-size: 1em;
327 |   color: var(--modal-title, #333);
328 | }
329 | 
330 | .citation-raw-data pre {
331 |   background: var(--code-bg, #f5f5f5);
332 |   border: 1px solid var(--code-border, #ddd);
333 |   border-radius: 6px;
334 |   padding: 12px;
335 |   overflow-x: auto;
336 |   font-size: 0.8em;
337 |   line-height: 1.4;
338 |   max-height: 300px;
339 | }
340 | 
341 | .citation-raw-data code {
342 |   color: var(--code-text, #333);
343 |   font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
344 | }
345 | 
346 | /* Animations */
347 | @keyframes fadeIn {
348 |   from {
349 |     opacity: 0;
350 |   }
351 |   to {
352 |     opacity: 1;
353 |   }
354 | }
355 | 
356 | @keyframes slideIn {
357 |   from {
358 |     opacity: 0;
359 |     transform: translateY(-20px) scale(0.95);
360 |   }
361 |   to {
362 |     opacity: 1;
363 |     transform: translateY(0) scale(1);
364 |   }
365 | }
366 | 
367 | @keyframes tooltipFadeIn {
368 |   from {
369 |     opacity: 0;
370 |     transform: translateY(-8px) scale(0.95);
371 |   }
372 |   to {
373 |     opacity: 1;
374 |     transform: translateY(0) scale(1);
375 |   }
376 | }
377 | 
378 | /* Dark theme adjustments */
379 | [data-theme="dark"] .citation-modal {
380 |   --modal-bg: #2d2d2d;
381 |   --modal-text: #e0e0e0;
382 |   --modal-border: #444;
383 |   --modal-header-bg: #333;
384 |   --modal-title: #fff;
385 |   --code-bg: #1e1e1e;
386 |   --code-border: #444;
387 |   --code-text: #e0e0e0;
388 |   --hover-bg: #404040;
389 | }
390 | 
391 | [data-theme="dark"] .citation-tooltip-container {
392 |   --tooltip-bg: #1f2937;
393 |   --tooltip-text: #f9fafb;
394 |   --tooltip-border: #374151;
395 |   --accent-bg: #1e3a8a;
396 |   --accent-border: #3b82f6;
397 |   --tool-bg: #374151;
398 |   --code-bg: #111827;
399 |   --code-border: #374151;
400 |   --metrics-bg: #374151;
401 |   --metrics-border: #4b5563;
402 |   --metric-item-bg: #1f2937;
403 |   --metric-item-border: #4b5563;
404 |   --text-primary: #f9fafb;
405 |   --text-secondary: #d1d5db;
406 |   --text-muted: #9ca3af;
407 | }
408 | 
409 | /* Responsive design */
410 | @media (max-width: 768px) {
411 |   .citation-modal {
412 |     max-width: 95vw;
413 |     max-height: 90vh;
414 |     margin: 20px;
415 |   }
416 | 
417 |   .citation-tooltip-container {
418 |     max-width: 250px;
419 |     font-size: 0.8em;
420 |   }
421 | }
422 | 
```

--------------------------------------------------------------------------------
/webui/src/services/systemPrompt.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Centralised system prompt service for consistent AI agent behavior
  3 |  */
  4 | 
  5 | import { mcpClient } from './mcpClient';
  6 | import type { MCPTool } from '@/types/mcp';
  7 | 
  8 | export interface SystemPromptConfig {
  9 |   includeTimestamp?: boolean;
 10 |   includeTools?: boolean;
 11 |   includeMemoryContext?: boolean;
 12 |   customInstructions?: string;
 13 | }
 14 | 
 15 | export class SystemPromptService {
 16 |   private static instance: SystemPromptService;
 17 |   private cachedTools: MCPTool[] = [];
 18 |   private lastToolsUpdate: number = 0;
 19 |   private readonly TOOLS_CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
 20 | 
 21 |   private constructor() {}
 22 | 
 23 |   public static getInstance(): SystemPromptService {
 24 |     if (!SystemPromptService.instance) {
 25 |       SystemPromptService.instance = new SystemPromptService();
 26 |     }
 27 |     return SystemPromptService.instance;
 28 |   }
 29 | 
 30 |   /**
 31 |    * Generate the complete system prompt with all components
 32 |    */
 33 |   public async generateSystemPrompt(config: SystemPromptConfig = {}): Promise<string> {
 34 |     console.log('🎯 Generating system prompt with dynamic tools...');
 35 |     const {
 36 |       includeTimestamp = true,
 37 |       includeTools = true,
 38 |       includeMemoryContext = false, // Reserved for future use
 39 |       customInstructions
 40 |     } = config;
 41 | 
 42 |     let prompt = this.getBasePrompt();
 43 | 
 44 |     // Add current timestamp
 45 |     if (includeTimestamp) {
 46 |       prompt += `\n\nCurrent date and time: ${this.getCurrentTimestamp()}`;
 47 |     }
 48 | 
 49 |     // Add dynamic tools list
 50 |     if (includeTools) {
 51 |       const toolsSection = await this.generateToolsSection();
 52 |       if (toolsSection) {
 53 |         prompt += `\n\n${toolsSection}`;
 54 |       }
 55 |     }
 56 | 
 57 |     // Add guidelines and instructions
 58 |     prompt += `\n\n${this.getGuidelines()}`;
 59 | 
 60 |     // Add custom instructions if provided
 61 |     if (customInstructions) {
 62 |       prompt += `\n\nAdditional Instructions:\n${customInstructions}`;
 63 |     }
 64 | 
 65 |     // Note: includeMemoryContext is reserved for future memory integration
 66 |     if (includeMemoryContext) {
 67 |       // Future: Add memory context here
 68 |       console.log('Memory context integration is planned for future releases');
 69 |     }
 70 | 
 71 |     return prompt;
 72 |   }
 73 | 
 74 |   /**
 75 |    * Get the base system prompt without dynamic content
 76 |    */
 77 |   private getBasePrompt(): string {
 78 |     return `You are an expert cryptocurrency trading assistant with access to real-time market data and advanced analysis tools through the Bybit MCP server.
 79 | 
 80 | You specialise in:
 81 | - Real-time market analysis and price monitoring
 82 | - Technical analysis using advanced indicators (RSI, MACD, Order Blocks, ML-RSI)
 83 | - Risk assessment and trading recommendations
 84 | - Market structure analysis and trend identification
 85 | - Portfolio analysis and position management
 86 | 
 87 | Your responses should be:
 88 | - Data-driven and based on current market conditions
 89 | - Clear and actionable with proper risk warnings
 90 | - Professional yet accessible to traders of all levels
 91 | - Comprehensive when needed, concise when appropriate
 92 | - Formatted in markdown for better readability`;
 93 |   }
 94 | 
 95 |   /**
 96 |    * Generate the tools section with dynamic tool discovery
 97 |    */
 98 |   private async generateToolsSection(): Promise<string> {
 99 |     try {
100 |       const tools = await this.getAvailableTools();
101 | 
102 |       if (tools.length === 0) {
103 |         return 'No tools are currently available.';
104 |       }
105 | 
106 |       let toolsSection = 'Available tools:\n\n';
107 | 
108 |       for (const tool of tools) {
109 |         toolsSection += `**${tool.name}**\n`;
110 |         toolsSection += `${tool.description || 'No description available'}\n`;
111 | 
112 |         // Add parameter information if available
113 |         if (tool.inputSchema?.properties) {
114 |           const params = Object.entries(tool.inputSchema.properties);
115 |           if (params.length > 0) {
116 |             toolsSection += 'Parameters:\n';
117 |             for (const [paramName, paramDef] of params) {
118 |               const isRequired = tool.inputSchema.required?.includes(paramName) || false;
119 |               const description = (paramDef as any)?.description || 'No description';
120 |               toolsSection += `- ${paramName}${isRequired ? ' (required)' : ''}: ${description}\n`;
121 |             }
122 |           }
123 |         }
124 | 
125 |         toolsSection += '\n';
126 |       }
127 | 
128 |       return toolsSection.trim();
129 |     } catch (error) {
130 |       console.warn('Failed to generate tools section:', error);
131 |       return 'Tools are currently unavailable due to connection issues.';
132 |     }
133 |   }
134 | 
135 |   /**
136 |    * Get available tools with caching
137 |    */
138 |   private async getAvailableTools(): Promise<MCPTool[]> {
139 |     const now = Date.now();
140 | 
141 |     // Return cached tools if still fresh
142 |     if (this.cachedTools.length > 0 && (now - this.lastToolsUpdate) < this.TOOLS_CACHE_DURATION) {
143 |       return this.cachedTools;
144 |     }
145 | 
146 |     try {
147 |       // Fetch fresh tools from MCP client
148 |       const tools = await mcpClient.listTools();
149 |       this.cachedTools = tools;
150 |       this.lastToolsUpdate = now;
151 |       return tools;
152 |     } catch (error) {
153 |       console.warn('Failed to fetch tools, using cached version:', error);
154 |       return this.cachedTools;
155 |     }
156 |   }
157 | 
158 |   /**
159 |    * Get current timestamp in consistent format
160 |    */
161 |   private getCurrentTimestamp(): string {
162 |     const now = new Date();
163 |     return now.getFullYear() + '-' +
164 |       String(now.getMonth() + 1).padStart(2, '0') + '-' +
165 |       String(now.getDate()).padStart(2, '0') + ' ' +
166 |       String(now.getHours()).padStart(2, '0') + ':' +
167 |       String(now.getMinutes()).padStart(2, '0') + ':' +
168 |       String(now.getSeconds()).padStart(2, '0') + ' UTC';
169 |   }
170 | 
171 |   /**
172 |    * Get standard guidelines and instructions
173 |    */
174 |   private getGuidelines(): string {
175 |     return `Guidelines:
176 | 1. **Always use relevant tools** to gather current data before making recommendations
177 | 2. **Include reference IDs** - For all Bybit tool calls, always include the parameter "includeReferenceId": true to enable data verification
178 | 3. **Cite your sources** - When citing specific data from tool responses, include the reference ID in square brackets like [REF001]
179 | 4. **Provide clear insights** with proper risk warnings and confidence levels
180 | 5. **Explain your reasoning** and methodology behind analysis
181 | 6. **Consider multiple timeframes** when relevant for comprehensive analysis
182 | 7. **Use tools intelligently** - Don't call unnecessary tools, focus on what's needed for the user's question
183 | 8. **Be comprehensive when appropriate** but concise when a simple answer suffices
184 | 9. **Include confidence levels** in your analysis and recommendations
185 | 10. **Always include risk warnings** for trading recommendations and emphasize that this is not financial advice
186 | 
187 | Remember: You are providing analysis and insights, not financial advice. Users should always do their own research, consider their risk tolerance, and consult with qualified financial advisors before making trading decisions.`;
188 |   }
189 | 
190 |   /**
191 |    * Generate a simplified system prompt for legacy compatibility
192 |    */
193 |   public generateLegacySystemPrompt(): string {
194 |     const timestamp = this.getCurrentTimestamp();
195 | 
196 |     return `You are an AI assistant specialised in cryptocurrency trading and market analysis. You have access to the Bybit MCP server which provides real-time market data and advanced technical analysis tools.
197 | 
198 | Current date and time: ${timestamp}
199 | 
200 | Available tools include:
201 | - get_ticker: Get current price and 24h statistics for any trading pair
202 | - get_orderbook: View current buy/sell orders and market depth
203 | - get_kline: Retrieve historical price data (candlestick charts)
204 | - get_trades: See recent trade history and market activity
205 | - get_ml_rsi: Advanced ML-enhanced RSI indicator for trend analysis
206 | - get_order_blocks: Detect institutional order blocks and liquidity zones
207 | - get_market_structure: Comprehensive market structure analysis
208 | 
209 | Guidelines:
210 | - Always use relevant tools to gather current data before making recommendations
211 | - Provide clear, actionable insights with proper risk warnings
212 | - Explain your reasoning and methodology
213 | - Include confidence levels in your analysis
214 | - Consider multiple timeframes when relevant
215 | - Use tools intelligently based on the user's question - don't call unnecessary tools
216 | - Provide comprehensive analysis when appropriate, but be concise when a simple answer suffices
217 | - IMPORTANT: For all Bybit tool calls, always include the parameter "includeReferenceId": true to enable data verification. When citing specific data from tool responses, include the reference ID in square brackets like [REF001].
218 | 
219 | Be helpful, accurate, and focused on providing valuable trading insights.`;
220 |   }
221 | 
222 |   /**
223 |    * Clear the tools cache to force refresh
224 |    */
225 |   public clearToolsCache(): void {
226 |     this.cachedTools = [];
227 |     this.lastToolsUpdate = 0;
228 |   }
229 | 
230 |   /**
231 |    * Get tools cache status
232 |    */
233 |   public getToolsCacheStatus(): { count: number; lastUpdate: number; isStale: boolean } {
234 |     const now = Date.now();
235 |     const isStale = (now - this.lastToolsUpdate) > this.TOOLS_CACHE_DURATION;
236 | 
237 |     return {
238 |       count: this.cachedTools.length,
239 |       lastUpdate: this.lastToolsUpdate,
240 |       isStale
241 |     };
242 |   }
243 | }
244 | 
245 | // Export singleton instance
246 | export const systemPromptService = SystemPromptService.getInstance();
247 | 
```
Page 2/8FirstPrevNextLast