//! Basic TLI Dashboard Example //! //! This example demonstrates how to create a basic terminal dashboard that //! connects to the Foxhunt trading services and displays real-time information //! including system status, active orders, positions, and performance metrics. use std::time::Duration; use tli::prelude::*; use tli::{ServiceEndpoints, TliClient}; use tokio::time::{interval, sleep}; use tracing::{error, info, warn}; /// Dashboard configuration #[derive(Debug, Clone)] struct DashboardConfig { refresh_interval: Duration, max_orders_display: usize, show_positions: bool, show_metrics: bool, auto_reconnect: bool, } impl Default for DashboardConfig { fn default() -> Self { Self { refresh_interval: Duration::from_secs(1), max_orders_display: 10, show_positions: true, show_metrics: true, auto_reconnect: true, } } } /// Simple dashboard state #[derive(Debug, Default)] struct DashboardState { connected: bool, last_update: Option, order_count: usize, position_count: usize, system_status: String, error_message: Option, } /// Basic dashboard implementation struct BasicDashboard { client: TliClient, config: DashboardConfig, state: DashboardState, } impl BasicDashboard { /// Create a new dashboard with default configuration fn new() -> Self { Self { client: TliClient::new(), config: DashboardConfig::default(), state: DashboardState::default(), } } /// Create a dashboard with custom endpoints fn with_endpoints(endpoints: ServiceEndpoints) -> Self { Self { client: TliClient::with_endpoints(endpoints), config: DashboardConfig::default(), state: DashboardState::default(), } } /// Connect to trading services async fn connect(&mut self) -> TliResult<()> { info!("Connecting to Foxhunt trading services..."); match self.client.connect().await { Ok(_) => { self.state.connected = true; self.state.error_message = None; info!("Successfully connected to trading services"); Ok(()) } Err(e) => { self.state.connected = false; self.state.error_message = Some(e.to_string()); warn!("Failed to connect to trading services: {}", e); Err(e) } } } /// Update dashboard data async fn update_data(&mut self) -> TliResult<()> { if !self.state.connected { return Err(TliError::NotConnected( "Dashboard not connected".to_string(), )); } // Update system status match self.client.check_health().await { Ok(health_status) => { if health_status.is_empty() { self.state.system_status = "Unknown".to_string(); } else { let healthy_services = health_status .iter() .filter(|s| s.status.contains("OK") || s.status.contains("SERVING")) .count(); self.state.system_status = format!( "{}/{} services healthy", healthy_services, health_status.len() ); } } Err(e) => { self.state.system_status = format!("Health check failed: {}", e); } } // Update order information if let Ok(trading_client) = self.client.trading() { let list_request = tli::proto::trading::ListOrdersRequest { symbol: "".to_string(), // All symbols limit: Some(self.config.max_orders_display as u32), }; match trading_client .list_orders(tonic::Request::new(list_request)) .await { Ok(response) => { let orders = response.into_inner().orders; self.state.order_count = orders.len(); } Err(e) => { warn!("Failed to retrieve orders: {}", e); self.state.order_count = 0; } } } // Update position information if self.config.show_positions { if let Ok(trading_client) = self.client.trading() { let positions_request = tli::proto::trading::GetPositionsRequest {}; match trading_client .get_positions(tonic::Request::new(positions_request)) .await { Ok(response) => { let positions = response.into_inner().positions; self.state.position_count = positions.len(); } Err(e) => { warn!("Failed to retrieve positions: {}", e); self.state.position_count = 0; } } } } self.state.last_update = Some(std::time::SystemTime::now()); self.state.error_message = None; Ok(()) } /// Display dashboard fn display(&self) { // Clear screen (simple ANSI escape sequence) print!("\x1B[2J\x1B[1;1H"); println!("╔══════════════════════════════════════════════════════════════╗"); println!("║ Foxhunt Trading Dashboard ║"); println!("╠══════════════════════════════════════════════════════════════╣"); // Connection status let connection_status = if self.state.connected { "🟢 CONNECTED" } else { "🔴 DISCONNECTED" }; println!("║ Status: {:<50} ║", connection_status); // System health println!("║ System: {:<50} ║", self.state.system_status); // Last update if let Some(last_update) = self.state.last_update { let elapsed = last_update .elapsed() .map(|d| format!("{:.1}s ago", d.as_secs_f64())) .unwrap_or_else(|_| "Unknown".to_string()); println!("║ Updated: {:<49} ║", elapsed); } else { println!("║ Updated: {:<49} ║", "Never"); } println!("╠══════════════════════════════════════════════════════════════╣"); // Trading information println!("║ Active Orders: {:<45} ║", self.state.order_count); if self.config.show_positions { println!("║ Open Positions: {:<44} ║", self.state.position_count); } println!("╠══════════════════════════════════════════════════════════════╣"); // Error message if let Some(ref error) = self.state.error_message { println!( "║ Error: {:<52} ║", if error.len() > 52 { &error[..52] } else { error } ); } else { println!("║ {:<60} ║", "All systems operational"); } println!("╚══════════════════════════════════════════════════════════════╝"); // Instructions println!("\nPress Ctrl+C to exit"); if !self.state.connected && self.config.auto_reconnect { println!("Attempting to reconnect..."); } } /// Run the dashboard async fn run(&mut self) -> TliResult<()> { info!("Starting basic dashboard..."); // Initial connection if let Err(e) = self.connect().await { error!("Failed initial connection: {}", e); if !self.config.auto_reconnect { return Err(e); } } // Set up refresh interval let mut refresh_timer = interval(self.config.refresh_interval); // Set up reconnection timer (every 5 seconds when disconnected) let mut reconnect_timer = interval(Duration::from_secs(5)); loop { tokio::select! { _ = refresh_timer.tick() => { if self.state.connected { if let Err(e) = self.update_data().await { warn!("Failed to update dashboard data: {}", e); self.state.error_message = Some(e.to_string()); // Mark as disconnected if it's a connection error if matches!(e, TliError::Connection(_) | TliError::NotConnected(_)) { self.state.connected = false; } } } self.display(); } _ = reconnect_timer.tick() => { if !self.state.connected && self.config.auto_reconnect { info!("Attempting to reconnect..."); let _ = self.connect().await; // Ignore errors, will retry } } _ = tokio::signal::ctrl_c() => { info!("Received shutdown signal"); break; } } } info!("Dashboard shutting down..."); self.client.disconnect().await; Ok(()) } } /// Demonstrate basic dashboard usage async fn demo_basic_dashboard() -> TliResult<()> { println!("=== Basic Dashboard Demo ==="); // Create and run dashboard with default settings let mut dashboard = BasicDashboard::new(); dashboard.run().await } /// Demonstrate dashboard with custom configuration async fn demo_custom_dashboard() -> TliResult<()> { println!("=== Custom Dashboard Demo ==="); // Custom endpoints (useful for testing with mock servers) let endpoints = ServiceEndpoints { trading_engine: "http://localhost:51000".to_string(), risk_management: "http://localhost:51001".to_string(), ml_signals: "http://localhost:51002".to_string(), market_data: "http://localhost:51003".to_string(), health_check: "http://localhost:51004".to_string(), }; let mut dashboard = BasicDashboard::with_endpoints(endpoints); // Customize configuration dashboard.config.refresh_interval = Duration::from_millis(500); // Faster refresh dashboard.config.max_orders_display = 20; // Show more orders dashboard.config.show_positions = true; dashboard.config.show_metrics = false; // Disable metrics for simplicity dashboard.config.auto_reconnect = true; dashboard.run().await } /// Simple connection test async fn demo_connection_test() -> TliResult<()> { println!("=== Connection Test Demo ==="); let mut client = TliClient::new(); println!("Attempting to connect to default endpoints..."); match client.connect().await { Ok(_) => { println!("✅ Successfully connected to trading services"); // Test health check match client.check_health().await { Ok(health_status) => { println!("🏥 Health check results:"); for status in health_status { println!( " - {}: {} ({})", status.service, status.status, status.endpoint ); } } Err(e) => { println!("⚠️ Health check failed: {}", e); } } client.disconnect().await; } Err(e) => { println!("❌ Failed to connect: {}", e); println!("💡 Make sure the Foxhunt services are running"); return Err(e); } } Ok(()) } #[tokio::main] async fn main() -> Result<(), Box> { // Initialize logging tracing_subscriber::fmt() .with_max_level(tracing::Level::INFO) .init(); // Parse command line arguments let args: Vec = std::env::args().collect(); let demo_mode = args.get(1).map(|s| s.as_str()).unwrap_or("dashboard"); let result = match demo_mode { "dashboard" | "basic" => demo_basic_dashboard().await, "custom" => demo_custom_dashboard().await, "test" | "connection" => demo_connection_test().await, "help" | "--help" | "-h" => { println!("Basic Dashboard Example"); println!(); println!("Usage: cargo run --example basic_dashboard [MODE]"); println!(); println!("Modes:"); println!(" dashboard, basic - Run basic dashboard (default)"); println!(" custom - Run dashboard with custom configuration"); println!(" test, connection - Test connection to services"); println!(" help - Show this help message"); println!(); println!("Environment Variables:"); println!(" FOXHUNT_TRADING_ENGINE_URL - Trading engine endpoint"); println!(" FOXHUNT_RISK_MANAGEMENT_URL - Risk management endpoint"); println!(" FOXHUNT_ML_SIGNALS_URL - ML signals endpoint"); println!(" FOXHUNT_MARKET_DATA_URL - Market data endpoint"); println!(" FOXHUNT_HEALTH_CHECK_URL - Health check endpoint"); println!(" RUST_LOG - Log level (info, debug, warn, error)"); Ok(()) } _ => { eprintln!( "Unknown mode: {}. Use 'help' for usage information.", demo_mode ); std::process::exit(1); } }; if let Err(e) = result { eprintln!("Demo failed: {}", e); std::process::exit(1); } Ok(()) } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_dashboard_creation() { let dashboard = BasicDashboard::new(); assert!(!dashboard.state.connected); assert_eq!(dashboard.state.order_count, 0); assert_eq!(dashboard.state.position_count, 0); } #[tokio::test] async fn test_custom_endpoints() { let endpoints = ServiceEndpoints { trading_engine: "http://test:8080".to_string(), risk_management: "http://test:8081".to_string(), ml_signals: "http://test:8082".to_string(), market_data: "http://test:8083".to_string(), health_check: "http://test:8084".to_string(), }; let dashboard = BasicDashboard::with_endpoints(endpoints.clone()); assert_eq!( dashboard.client.endpoints.trading_engine, endpoints.trading_engine ); } #[test] fn test_dashboard_config() { let config = DashboardConfig::default(); assert_eq!(config.refresh_interval, Duration::from_secs(1)); assert_eq!(config.max_orders_display, 10); assert!(config.show_positions); assert!(config.show_metrics); assert!(config.auto_reconnect); } #[test] fn test_dashboard_state() { let state = DashboardState::default(); assert!(!state.connected); assert!(state.last_update.is_none()); assert_eq!(state.order_count, 0); assert_eq!(state.position_count, 0); assert_eq!(state.system_status, ""); assert!(state.error_message.is_none()); } }