diff --git a/Cargo.lock b/Cargo.lock index 67876236f..8dff7f422 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1963,27 +1963,12 @@ dependencies = [ "log", ] -[[package]] -name = "cassowary" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" - [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - [[package]] name = "cc" version = "1.2.40" @@ -2304,20 +2289,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "compact_str" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - [[package]] name = "compression-codecs" version = "0.4.31" @@ -2639,10 +2610,7 @@ dependencies = [ "bitflags 2.9.4", "crossterm_winapi", "libc", - "mio 0.8.11", "parking_lot 0.12.5", - "signal-hook", - "signal-hook-mio", "winapi", ] @@ -2653,13 +2621,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags 2.9.4", - "crossterm_winapi", - "mio 1.0.4", "parking_lot 0.12.5", "rustix 0.38.44", - "signal-hook", - "signal-hook-mio", - "winapi", ] [[package]] @@ -2767,18 +2730,8 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" dependencies = [ - "darling_core 0.14.4", - "darling_macro 0.14.4", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", + "darling_core", + "darling_macro", ] [[package]] @@ -2795,42 +2748,17 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.106", -] - [[package]] name = "darling_macro" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ - "darling_core 0.14.4", + "darling_core", "quote", "syn 1.0.109", ] -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn 2.0.106", -] - [[package]] name = "dashmap" version = "5.5.3" @@ -3174,7 +3102,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" dependencies = [ - "darling 0.14.4", + "darling", "proc-macro2", "quote", "syn 1.0.109", @@ -4873,12 +4801,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "indoc" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" - [[package]] name = "influxdb" version = "0.7.2" @@ -4965,19 +4887,6 @@ dependencies = [ "similar", ] -[[package]] -name = "instability" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" -dependencies = [ - "darling 0.20.11", - "indoc", - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "instant" version = "0.1.13" @@ -5612,18 +5521,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536" -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.48.0", -] - [[package]] name = "mio" version = "1.0.4" @@ -5631,7 +5528,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -7546,27 +7442,6 @@ dependencies = [ "serde", ] -[[package]] -name = "ratatui" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d" -dependencies = [ - "bitflags 2.9.4", - "cassowary", - "compact_str", - "crossterm 0.28.1", - "instability", - "itertools 0.13.0", - "lru", - "paste", - "strum", - "strum_macros", - "unicode-segmentation", - "unicode-truncate", - "unicode-width 0.1.14", -] - [[package]] name = "rav1e" version = "0.7.1" @@ -8707,28 +8582,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-mio" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" -dependencies = [ - "libc", - "mio 0.8.11", - "mio 1.0.4", - "signal-hook", -] - [[package]] name = "signal-hook-registry" version = "1.4.6" @@ -9329,9 +9182,6 @@ name = "strum" version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] [[package]] name = "strum_macros" @@ -9883,7 +9733,6 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" name = "tli" version = "1.0.0" dependencies = [ - "adaptive-strategy", "aes-gcm", "anyhow", "argon2", @@ -9897,7 +9746,6 @@ dependencies = [ "common", "console", "criterion", - "crossterm 0.27.0", "dirs 5.0.1", "futures", "futures-util", @@ -9912,7 +9760,6 @@ dependencies = [ "proptest", "prost 0.14.1", "rand 0.8.5", - "ratatui", "rpassword", "rust_decimal", "serde", @@ -9945,7 +9792,7 @@ dependencies = [ "bytes", "io-uring", "libc", - "mio 1.0.4", + "mio", "parking_lot 0.12.5", "pin-project-lite", "signal-hook-registry", @@ -10876,17 +10723,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" -[[package]] -name = "unicode-truncate" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" -dependencies = [ - "itertools 0.13.0", - "unicode-segmentation", - "unicode-width 0.1.14", -] - [[package]] name = "unicode-width" version = "0.1.14" diff --git a/tests/fixtures/mod.rs b/tests/fixtures/mod.rs index 6b2c7e900..748cf2fd6 100644 --- a/tests/fixtures/mod.rs +++ b/tests/fixtures/mod.rs @@ -27,7 +27,7 @@ use std::collections::HashMap; use std::sync::{ - atomic::{AtomicU16, AtomicU64, Ordering}, + atomic::{AtomicU16, Ordering}, Arc, }; use std::time::Duration; @@ -35,12 +35,11 @@ use std::time::Duration; use chrono::{DateTime, Utc}; use rust_decimal::Decimal; use serde_json::json; -use tokio::sync::{mpsc, Mutex, RwLock}; +use tokio::sync::RwLock; use uuid::Uuid; // Import TLI types explicitly (tli crate is available as dependency) -use tli::error::{TliError, TliResult}; -use tli::events::{Event, EventSeverity, EventType}; +use tli::error::TliResult; // Re-export sub-modules for easy access pub mod builders; @@ -628,100 +627,7 @@ lazy_static::lazy_static! { pub static ref TEST_PORT_MANAGER: TestPortManager = TestPortManager::new(); } -/// Test event publisher for streaming tests -#[derive(Debug)] -pub struct TestEventPublisher { - _event_sender: mpsc::UnboundedSender, - event_receiver: Arc>>, - published_events: AtomicU64, -} - -impl TestEventPublisher { - pub async fn new() -> TliResult { - let (sender, receiver) = mpsc::unbounded_channel(); - - Ok(Self { - _event_sender: sender, - event_receiver: Arc::new(Mutex::new(receiver)), - published_events: AtomicU64::new(0), - }) - } - - pub async fn publish_event(&self, event: Event) -> TliResult<()> { - self._event_sender - .send(event) - .map_err(|e| TliError::Other(format!("Failed to publish event: {}", e)))?; - - self.published_events.fetch_add(1, Ordering::Relaxed); - Ok(()) - } - - pub async fn publish_market_data_burst(&self, symbol: &str, count: usize) -> TliResult<()> { - for i in 0..count { - let event = Event { - id: Uuid::new_v4(), - event_type: EventType::MarketData, - severity: EventSeverity::Info, - source: "test_publisher".to_string(), - timestamp_nanos: Utc::now().timestamp_nanos_opt().unwrap_or(0), - sequence: i as u64, - payload: json!({ - "symbol": symbol, - "price": 150.0 + (i as f64 * 0.01), - "volume": 100 + i, - "sequence": i - }), - correlation_id: None, - metadata: HashMap::new(), - ttl_seconds: 0, - }; - - self.publish_event(event).await?; - } - - Ok(()) - } - - pub async fn publish_order_lifecycle(&self, order_id: &str) -> TliResult<()> { - let states = vec!["pending", "partially_filled", "filled"]; - - for (i, state) in states.into_iter().enumerate() { - let event = Event { - id: Uuid::new_v4(), - event_type: EventType::Trading, - severity: EventSeverity::Info, - source: "test_lifecycle".to_string(), - timestamp_nanos: Utc::now().timestamp_nanos_opt().unwrap_or(0), - sequence: i as u64, - payload: json!({ - "order_id": order_id, - "status": state, - "filled_quantity": (i + 1) * 50, - "remaining_quantity": 100 - ((i + 1) * 50) - }), - correlation_id: None, - metadata: HashMap::new(), - ttl_seconds: 0, - }; - - self.publish_event(event).await?; - - // Small delay between state changes - tokio::time::sleep(Duration::from_millis(100)).await; - } - - Ok(()) - } - - pub fn get_published_count(&self) -> u64 { - self.published_events.load(Ordering::Relaxed) - } - - pub async fn receive_event(&self) -> Option { - let mut receiver = self.event_receiver.lock().await; - receiver.recv().await - } -} +// TestEventPublisher removed — event streaming moved to web-gateway WebSocket infrastructure /// Performance metrics collector for tests #[derive(Debug, Default)] @@ -858,7 +764,6 @@ pub struct TestEnvironment { pub config: IntegrationTestConfig, pub metrics: Arc, pub port_manager: Arc, - pub event_publisher: Arc, cleanup_tasks: Vec< Box< dyn Fn() -> std::pin::Pin + Send>> @@ -874,7 +779,6 @@ impl std::fmt::Debug for TestEnvironment { .field("config", &self.config) .field("metrics", &self.metrics) .field("port_manager", &self.port_manager) - .field("event_publisher", &self.event_publisher) .field( "cleanup_tasks", &format!("<{} cleanup tasks>", self.cleanup_tasks.len()), @@ -887,13 +791,11 @@ impl TestEnvironment { pub async fn new(config: IntegrationTestConfig) -> TliResult { let metrics = Arc::new(TestMetricsCollector::new()); let port_manager = Arc::new(TestPortManager::new()); - let event_publisher = Arc::new(TestEventPublisher::new().await?); Ok(Self { config, metrics, port_manager, - event_publisher, cleanup_tasks: Vec::new(), }) } diff --git a/tests/integration/tli_client_tests.rs b/tests/integration/tli_client_tests.rs index 495e341e1..58e237acd 100644 --- a/tests/integration/tli_client_tests.rs +++ b/tests/integration/tli_client_tests.rs @@ -8,9 +8,10 @@ use crate::framework::{TestOrchestrator, TestFrameworkConfig, PerformanceThresho use crate::framework::mocks::MockServiceRegistry; use config::{ConfigManager, TLIConfig}; -use tli::client::{TLIClient, ConnectionManager, ServiceConnection}; -use tli::ui::{Dashboard, Terminal, CommandInterface}; -use tli::commands::{Command, CommandResult, CommandType}; +// Dashboard types removed — TUI replaced by web-dashboard +// use tli::client::{TLIClient, ConnectionManager, ServiceConnection}; +// use tli::ui::{Dashboard, Terminal, CommandInterface}; +// use tli::commands::{Command, CommandResult, CommandType}; /// TLI Client Integration Tests /// diff --git a/tli/Cargo.toml b/tli/Cargo.toml index 85e3ad075..c5934b677 100644 --- a/tli/Cargo.toml +++ b/tli/Cargo.toml @@ -45,17 +45,10 @@ tracing-subscriber.workspace = true common.workspace = true # REMOVED trading_engine dependency - violates pure client architecture -# Terminal UI framework dependencies (essential for TLI) -ratatui.workspace = true -crossterm.workspace = true - -# Time and financial types for UI widgets +# Time and financial types chrono.workspace = true rust_decimal.workspace = true -# Import adaptive-strategy for UI widget types only (microstructure module) -adaptive-strategy.workspace = true - # Authentication dependencies keyring = "3.6" # OS keyring integration for secure token storage rpassword = "7.3" # Secure password input diff --git a/tli/examples/basic_dashboard.rs b/tli/examples/basic_dashboard.rs deleted file mode 100644 index 94419da3b..000000000 --- a/tli/examples/basic_dashboard.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Basic dashboard disabled - needs refactoring after client architecture changes -//! -//! This example referenced old client types that were removed when TLI was refactored. -//! -//! To re-enable: Update to use current client API and event types -#![allow(unused_crate_dependencies)] - -fn main() { - println!("Basic dashboard disabled - needs refactoring for current client API"); -} diff --git a/tli/examples/config_dashboard_demo.rs b/tli/examples/config_dashboard_demo.rs deleted file mode 100644 index e7b5f1f4a..000000000 --- a/tli/examples/config_dashboard_demo.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Configuration Dashboard Demo -//! -//! This example demonstrates the TLI Configuration Dashboard functionality. -//! Note: Configuration management is now handled by the shared config crate, -//! not directly by TLI which is a pure client. -#![allow(unused_crate_dependencies)] - -use tli::prelude::*; -use tokio::sync::mpsc; - -#[tokio::main] -async fn main() -> TliResult<()> { - println!("🚀 TLI Configuration Dashboard Demo"); - println!("====================================="); - - // Create a mock event channel - let (_event_sender, _event_receiver) = mpsc::channel(100); - - // Create the configuration dashboard - let config_dashboard = tli::dashboards::ConfigurationDashboard::new(_event_sender); - - println!("✅ Configuration Dashboard created successfully!"); - - // Note: Actual configuration management is done through the config crate - // which TLI accesses via gRPC services, not directly - println!( - " -Note: Configuration is managed by the config crate and accessed via gRPC. -TLI is a pure client that doesn't directly manage configuration." - ); - - let _ = config_dashboard; // Use the dashboard to avoid unused warning - - println!("\n📋 Configuration Dashboard Features:"); - println!(" 🌳 Hierarchical category tree navigation"); - println!(" ⚙️ Real-time configuration editing"); - println!(" ✅ Live validation with error reporting"); - println!(" 📜 Change history with rollback capability"); - println!(" 🔍 Search functionality across settings"); - println!(" 🔥 Hot-reload indicator for immediate changes"); - println!(" 🔐 Sensitive value masking"); - println!(" 🎯 Environment-specific configurations"); - - println!("\n🎮 Keyboard Controls:"); - println!(" [Tab] - Switch between panels"); - println!(" [↑↓] - Navigate lists"); - println!(" [Enter] - Select/Edit"); - println!(" [Space] - Toggle category expansion"); - println!(" [E] - Edit current setting"); - println!(" [S] - Search settings"); - println!(" [R] - Reset to default"); - println!(" [F5] - Refresh data"); - println!(" [Esc] - Cancel/Exit"); - - println!("\n🏗️ Architecture:"); - println!(" Client: TLI Terminal Application"); - println!(" Database: PostgreSQL with config schema"); - println!(" UI: Ratatui terminal interface"); - println!(" Validation: Real-time with custom rules"); - - println!("\n🎯 Dashboard Layout:"); - println!("┌─ CONFIGURATION DASHBOARD ────────────────────────────────────────┐"); - println!("│ Category Tree │ Settings Editor │ Validation & History │"); - println!("│ ▼ System │ Key: log_level │ Status: ✓ VALID │"); - println!("│ ├─ Logging │ Value: [info ▼] │ Type: string │"); - println!("│ ├─ Database │ Description: │ Required: Yes │"); - println!("│ └─ gRPC │ Global log level │ Hot Reload: Yes │"); - println!("│ ▼ Trading │ │ │"); - println!("│ ├─ Execution │ [SAVE CHANGES] │ Recent Changes: │"); - println!("│ ├─ Strategies │ [RESET] │ 14:30 - risk.var_conf │"); - println!("│ └─ Position │ [VALIDATE] │ 14:25 - ml.model_thresh │"); - println!("└──────────────────┴──────────────────┴──────────────────────────┘"); - - println!("\n✨ Demo completed successfully!"); - println!("📌 To run with real database: Set DATABASE_URL environment variable"); - - Ok(()) -} diff --git a/tli/examples/event_streaming_demo.rs b/tli/examples/event_streaming_demo.rs deleted file mode 100644 index 6678cd16e..000000000 --- a/tli/examples/event_streaming_demo.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Event streaming demo disabled - needs refactoring after client architecture changes -//! -//! This example referenced old event types that were removed when TLI was refactored. -//! -//! To re-enable: Update to use current EventType variants and streaming APIs -#![allow(unused_crate_dependencies)] - -fn main() { - println!("Event streaming demo disabled - needs refactoring for current event system"); -} diff --git a/tli/examples/real_time_streaming.rs b/tli/examples/real_time_streaming.rs deleted file mode 100644 index 446816f52..000000000 --- a/tli/examples/real_time_streaming.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Real-time streaming example disabled - needs refactoring -//! -//! This example referenced proto types (Order, OrderUpdate, MetricValue, StreamOrderUpdatesRequest) -//! and TliClient that are not available in current implementation. -//! -//! To re-enable: -//! 1. Check tli::proto::trading for actual available types -//! 2. Use correct client types from tli::client modules -//! 3. Update streaming API calls to match current implementation -//! 4. Add missing std::time::UNIX_EPOCH import -#![allow(unused_crate_dependencies)] - -fn main() { - println!( - "Real-time streaming example disabled - needs refactoring for current proto definitions" - ); -} diff --git a/tli/src/client/data_stream.rs b/tli/src/client/data_stream.rs deleted file mode 100644 index 81896fcba..000000000 --- a/tli/src/client/data_stream.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! Data streaming manager for high-performance market data processing -//! -//! This module provides low-latency data streaming capabilities for processing -//! market data feeds, order book updates, and trade executions with configurable -//! buffering and latency optimization. - -use serde::{Deserialize, Serialize}; -use tokio::sync::mpsc; - -/// Configuration parameters for data stream management -/// -/// Controls performance characteristics of data streams including buffer sizes, -/// latency requirements, and processing options for optimal throughput. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DataStreamConfig { - /// Size of the internal data buffer (number of messages) - pub buffer_size: usize, - /// Maximum acceptable latency in milliseconds for data processing - pub max_latency_ms: u64, -} - -/// High-performance data stream manager -/// -/// Manages multiple concurrent data streams with optimized buffering and -/// low-latency processing. Handles market data feeds, order updates, and -/// real-time trading events with microsecond precision timing. -pub struct DataStreamManager { - /// Stream configuration parameters - #[allow(dead_code)] - config: DataStreamConfig, - /// Channel sender for outgoing data - #[allow(dead_code)] - sender: mpsc::Sender>, - /// Channel receiver for incoming data - #[allow(dead_code)] - receiver: mpsc::Receiver>, -} - -impl DataStreamManager { - /// Create a new data stream manager with the specified configuration - /// - /// # Arguments - /// * `config` - Stream configuration including buffer size and latency requirements - /// - /// # Returns - /// - /// New `DataStreamManager` instance ready for high-performance data processing - /// - /// # Example - /// ```rust,no_run - /// use tli::client::data_stream::{DataStreamManager, DataStreamConfig}; - /// - /// let config = DataStreamConfig { - /// buffer_size: 10000, - /// max_latency_ms: 1, - /// }; - /// let stream_manager = DataStreamManager::new(config); - /// ``` - pub fn new(config: DataStreamConfig) -> Self { - let (sender, receiver) = mpsc::channel(config.buffer_size); - Self { - config, - sender, - receiver, - } - } - - /// Start the data stream processing - /// - /// Initializes all data streams and begins processing incoming market data. - /// - /// Must be called before any data can be processed through the streams. - /// - /// # Returns - /// `Result<(), String>` - Ok if streams start successfully - /// - /// # Errors - /// - /// Returns error message if stream initialization fails - pub async fn start(&mut self) -> Result<(), String> { - Ok(()) - } - - /// Stop all data stream processing - /// - /// Gracefully shuts down all active data streams and flushes any - /// remaining data in buffers. Ensures no data loss during shutdown. - /// - /// # Returns - /// `Result<(), String>` - Ok if streams stop cleanly - /// - /// # Errors - /// - /// Returns error message if shutdown encounters issues - pub async fn stop(&mut self) -> Result<(), String> { - Ok(()) - } - - /// Start all configured data streams - /// - /// Convenience method that starts all data streams in the correct order. - /// - /// Equivalent to calling `start()` but provides more explicit naming. - /// - /// # Returns - /// `Result<(), String>` - Ok if all streams start successfully - /// - /// # Errors - /// - /// Returns error message if any stream fails to start - pub async fn start_streams(&mut self) -> Result<(), String> { - self.start().await - } -} diff --git a/tli/src/client/event_stream.rs b/tli/src/client/event_stream.rs deleted file mode 100644 index ade5150af..000000000 --- a/tli/src/client/event_stream.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Event streaming client for real-time data processing -//! -//! This module provides event streaming capabilities for the TLI client, -//! enabling real-time consumption of market data, order updates, and -//! system events from backend services via streaming gRPC connections. - -use tokio::sync::mpsc; - -/// Event stream manager for handling real-time data streams -/// -/// Manages streaming connections to backend services and provides -/// asynchronous event processing capabilities with configurable buffering. -/// -/// Handles automatic reconnection and stream lifecycle management. -pub struct EventStreamManager { - /// Channel receiver for incoming event data - #[allow(dead_code)] - receiver: mpsc::Receiver>, -} - -/// Configuration parameters for event stream management -/// -/// Controls buffering behavior, connection parameters, and stream -/// processing settings for optimal performance and reliability. -pub struct EventStreamConfig { - /// Size of the internal event buffer (number of events) - pub buffer_size: usize, -} - -impl EventStreamManager { - /// Create a new event stream manager with the specified configuration - /// - /// # Arguments - /// * `config` - Stream configuration including buffer size and connection settings - /// - /// # Returns - /// - /// New `EventStreamManager` instance ready to handle event streams - /// - /// # Example - /// ```rust,no_run - /// use tli::client::event_stream::{EventStreamManager, EventStreamConfig}; - /// - /// let config = EventStreamConfig { buffer_size: 1000 }; - /// let stream_manager = EventStreamManager::new(config); - /// ``` - pub fn new(_config: EventStreamConfig) -> Self { - let (_sender, receiver) = mpsc::channel(100); - Self { receiver } - } -} diff --git a/tli/src/client/mod.rs b/tli/src/client/mod.rs index 934d1b41b..30ddf6b9d 100644 --- a/tli/src/client/mod.rs +++ b/tli/src/client/mod.rs @@ -10,20 +10,9 @@ pub mod backtesting_client; pub mod connection_manager; -pub mod data_stream; -pub mod event_stream; pub mod ml_training_client; pub mod trading_client; -// NO RE-EXPORTS: Import directly from submodules -// Use tli::client::connection_manager::{ConnectionManager, ConnectionConfig, etc.} instead -// Use tli::client::trading_client::{TradingClient, TradingClientConfig} instead -// Use tli::client::backtesting_client::{BacktestingClient, BacktestingClientConfig} instead -// Use tli::client::ml_training_client::{MLTrainingClient, MLTrainingClientConfig, etc.} instead -// Use tli::client::event_stream::{EventStreamManager, EventStreamConfig} instead -// Use tli::client::stream_manager::{DataStreamManager} instead -// Use tli::client::data_stream::{DataStreamManager, DataStreamConfig} instead - /// Service endpoints configuration #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] pub struct ServiceEndpoints { diff --git a/tli/src/client/stream_manager.rs b/tli/src/client/stream_manager.rs deleted file mode 100644 index c5ea85827..000000000 --- a/tli/src/client/stream_manager.rs +++ /dev/null @@ -1,238 +0,0 @@ -//! Data Stream Manager for Real-time Dashboard Updates -//! -//! Manages real-time data streams from gRPC services to dashboard components - -use crate::dashboard::events::{ - DashboardEvent, MLPredictionEvent, MarketDataDisplayEvent, PredictionType, RiskMetricsEvent, - SystemStatusEvent, -}; -use anyhow::Result; -use rand::Rng; -use std::collections::HashMap; -use tokio::sync::mpsc; -use tokio::time::{interval, Duration}; - -pub struct DataStreamManager { - _event_sender: mpsc::Sender, - is_running: bool, -} - -impl DataStreamManager { - pub const fn new(_event_sender: mpsc::Sender) -> Self { - Self { - _event_sender, - is_running: false, - } - } - - /// Start all data streams for real-time dashboard updates - pub async fn start_streams(&mut self) -> Result<()> { - if self.is_running { - return Ok(()); - } - - self.is_running = true; - - // Spawn individual stream tasks - let market_data_task = self.spawn_market_data_stream(); - let risk_metrics_task = self.spawn_risk_metrics_stream(); - let ml_predictions_task = self.spawn_ml_predictions_stream(); - let system_status_task = self.spawn_system_status_stream(); - - // Start all streams concurrently - tokio::try_join!( - market_data_task, - risk_metrics_task, - ml_predictions_task, - system_status_task - )?; - - Ok(()) - } - - /// Generate mock market data stream for demo purposes - async fn spawn_market_data_stream(&self) -> Result<()> { - let mut ticker = interval(Duration::from_millis(1000)); - let sender = self._event_sender.clone(); - let symbols = vec!["AAPL", "TSLA", "SPY", "QQQ", "NVDA"]; - let mut prices: HashMap<&str, f64> = HashMap::new(); - - // Initialize prices - prices.insert("AAPL", 150.25); - prices.insert("TSLA", 800.50); - prices.insert("SPY", 420.10); - prices.insert("QQQ", 350.75); - prices.insert("NVDA", 450.30); - - tokio::spawn(async move { - loop { - ticker.tick().await; - - // Generate all updates in a single scope without holding RNG across await - let updates: Vec = { - let mut rng = rand::thread_rng(); - let mut updates = Vec::new(); - - for symbol in &symbols { - if let Some(current_price) = prices.get_mut(symbol) { - // Simulate price movement (±0.5%) - let change_pct = (rng.gen::() - 0.5) * 0.01; - *current_price *= 1.0 + change_pct; - - let market_data = MarketDataDisplayEvent { - symbol: symbol.to_string(), - price: *current_price, - volume: rng.gen_range(500_000..1_500_000), - timestamp: chrono::Utc::now().timestamp(), - bid: Some(*current_price - 0.01), - ask: Some(*current_price + 0.01), - change: Some(change_pct * *current_price), - change_percent: Some(change_pct * 100.0), - }; - - updates.push(market_data); - } - } - updates - }; - - // Send all updates - for market_data in updates { - let _ = sender - .send(DashboardEvent::MarketDataUpdate(market_data)) - .await; - } - } - }); - Ok(()) - } - - /// Generate mock risk metrics stream - async fn spawn_risk_metrics_stream(&self) -> Result<()> { - let mut ticker = interval(Duration::from_millis(5000)); - let sender = self._event_sender.clone(); - - tokio::spawn(async move { - let mut portfolio_value = 1_000_000.0; - - loop { - ticker.tick().await; - - // Generate risk metrics in a single scope - let risk_metrics = { - let mut rng = rand::thread_rng(); - let pnl_change = (rng.gen::() - 0.5) * 5000.0; - let var_1d_rand = rng.gen::() * 1000.0; - let var_5d_rand = rng.gen::() * 1500.0; - let dd_rand = (rng.gen::() - 0.5) * 0.02; - let risk_rand = rng.gen::() * 0.4; - - portfolio_value += pnl_change; - - RiskMetricsEvent { - portfolio_value, - daily_pnl: pnl_change, - total_pnl: portfolio_value - 1_000_000.0, - var_1d: 5000.0 + var_1d_rand, - var_5d: 8000.0 + var_5d_rand, - max_drawdown: -0.15, - current_drawdown: -0.025 + dd_rand, - risk_score: 0.3 + risk_rand, - timestamp: chrono::Utc::now().timestamp(), - } - }; - - let _ = sender - .send(DashboardEvent::RiskMetricsUpdate(risk_metrics)) - .await; - } - }); - Ok(()) - } - - /// Generate mock ML predictions stream - async fn spawn_ml_predictions_stream(&self) -> Result<()> { - let mut ticker = interval(Duration::from_millis(3000)); - let sender = self._event_sender.clone(); - let models = vec!["DQN", "MAMBA", "TFT", "LIQUID", "TLOB", "PPO"]; - let symbols = vec!["AAPL", "TSLA", "SPY"]; - - tokio::spawn(async move { - loop { - ticker.tick().await; - - // Generate all predictions in a single scope - let predictions: Vec = { - let mut rng = rand::thread_rng(); - let mut predictions = Vec::new(); - - for model in &models { - for symbol in &symbols { - let prediction_type = match rng.gen_range(0..3) { - 0 => PredictionType::Buy, - 1 => PredictionType::Sell, - _ => PredictionType::Hold, - }; - - let ml_prediction = MLPredictionEvent { - model_name: model.to_string(), - symbol: symbol.to_string(), - prediction: prediction_type, - confidence: 0.5 + (rng.gen::() * 0.4), - signal_strength: rng.gen::(), - features: (0..10).map(|_| rng.gen::()).collect(), - timestamp: chrono::Utc::now().timestamp(), - }; - - predictions.push(ml_prediction); - } - } - predictions - }; - - // Send all predictions - for ml_prediction in predictions { - let _ = sender - .send(DashboardEvent::MLPredictionUpdate(ml_prediction)) - .await; - } - } - }); - Ok(()) - } - - /// Generate mock system status updates - async fn spawn_system_status_stream(&self) -> Result<()> { - let mut ticker = interval(Duration::from_millis(2000)); - let sender = self._event_sender.clone(); - - tokio::spawn(async move { - loop { - ticker.tick().await; - - // Generate system status in a single scope - let system_status = { - let mut rng = rand::thread_rng(); - SystemStatusEvent { - trading_enabled: true, - risk_controls_active: true, - ml_models_online: 6, - total_ml_models: 6, - active_positions: rng.gen_range(1..11), - pending_orders: rng.gen_range(0..5), - timestamp: chrono::Utc::now().timestamp(), - } - }; - - let _ = sender - .send(DashboardEvent::SystemStatus(system_status)) - .await; - } - }); - Ok(()) - } - - pub fn stop(&mut self) { - self.is_running = false; - } -} diff --git a/tli/src/dashboard/backtesting.rs b/tli/src/dashboard/backtesting.rs deleted file mode 100644 index 016fcbd0f..000000000 --- a/tli/src/dashboard/backtesting.rs +++ /dev/null @@ -1,564 +0,0 @@ -//! Backtesting Dashboard - Strategy Testing and Historical Analysis -//! -//! This dashboard provides comprehensive backtesting functionality including: -//! - Active backtest monitoring with real-time progress -//! - Historical backtest results and performance analysis -//! - Strategy configuration and parameter management -//! - Performance metrics visualization (returns, Sharpe ratio, drawdown) -//! - Trade execution analysis and order flow - -use super::Dashboard; -use crate::dashboard::events::DashboardEvent; -use anyhow::Result; -use crossterm::event::KeyEvent; -use ratatui::{ - layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - widgets::{Block, Borders, Cell, List, ListItem, ListState, Paragraph, Row, Table}, - Frame, -}; -use std::collections::HashMap; -use tokio::sync::mpsc; - -/// Backtesting Dashboard for strategy testing and historical analysis -pub struct BacktestingDashboard { - /// Event sender for dashboard communications - _event_sender: mpsc::Sender, - /// Active backtest status - active_backtests: Vec, - /// Historical backtest results - historical_results: Vec, - /// Selected backtest in the list - selected_backtest: ListState, - /// Current view mode - view_mode: BacktestViewMode, - /// Performance metrics cache - #[allow(dead_code)] - metrics_cache: HashMap, - /// Redraw flag - needs_redraw: bool, -} - -/// View modes for the backtesting dashboard -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum BacktestViewMode { - /// Show active running backtests - ActiveBacktests, - /// Show historical backtest results - HistoricalResults, - /// Show detailed performance analysis - PerformanceAnalysis, - /// Show strategy configuration - StrategyConfig, -} - -/// Active backtest entry -#[derive(Debug, Clone)] -pub struct BacktestEntry { - /// Backtest ID - pub id: String, - /// Strategy name - pub strategy: String, - /// Symbols being tested - pub symbols: Vec, - /// Progress percentage - pub progress: f64, - /// Current status - pub status: String, - /// Start time - pub started_at: String, - /// Estimated completion time - pub eta: Option, - /// Current `PnL` - pub current_pnl: f64, - /// Trade count - pub trade_count: u64, -} - -/// Historical backtest result -#[derive(Debug, Clone)] -pub struct BacktestResult { - /// Backtest ID - pub id: String, - /// Strategy name - pub strategy: String, - /// Symbols tested - pub symbols: Vec, - /// Test period - pub period: String, - /// Final return - pub total_return: f64, - /// Sharpe ratio - pub sharpe_ratio: f64, - /// Maximum drawdown - pub max_drawdown: f64, - /// Win rate - pub win_rate: f64, - /// Total trades - pub total_trades: u64, - /// Completion date - pub completed_at: String, -} - -/// Performance metrics for detailed analysis -#[derive(Debug, Clone)] -pub struct PerformanceMetrics { - /// Daily returns - pub daily_returns: Vec, - /// Cumulative returns - pub cumulative_returns: Vec, - /// Rolling Sharpe ratio - pub rolling_sharpe: Vec, - /// Drawdown series - pub drawdown_series: Vec, - /// Trade analysis - pub trade_metrics: TradeMetrics, -} - -/// Trade execution metrics -#[derive(Debug, Clone)] -pub struct TradeMetrics { - /// Average trade duration (hours) - pub avg_duration: f64, - /// Average win amount - pub avg_win: f64, - /// Average loss amount - pub avg_loss: f64, - /// Profit factor - pub profit_factor: f64, - /// Maximum consecutive wins - pub max_consecutive_wins: u32, - /// Maximum consecutive losses - pub max_consecutive_losses: u32, -} - -impl BacktestingDashboard { - /// Create a new backtesting dashboard - pub fn new(_event_sender: mpsc::Sender) -> Self { - let mut dashboard = Self { - _event_sender, - active_backtests: Vec::new(), - historical_results: Vec::new(), - selected_backtest: ListState::default(), - view_mode: BacktestViewMode::ActiveBacktests, - metrics_cache: HashMap::new(), - needs_redraw: true, - }; - - // Initialize with sample data - dashboard.load_sample_data(); - dashboard - } - - /// Load sample data for demonstration - fn load_sample_data(&mut self) { - // Sample active backtests - self.active_backtests = vec![ - BacktestEntry { - id: "bt_001".to_owned(), - strategy: "MeanReversion_v2.1".to_owned(), - symbols: vec!["SPY".to_owned(), "QQQ".to_owned()], - progress: 73.5, - status: "Running".to_owned(), - started_at: "2025-01-23 14:30:15".to_owned(), - eta: Some("2025-01-23 16:45:00".to_owned()), - current_pnl: 12_450.75, - trade_count: 127, - }, - BacktestEntry { - id: "bt_002".to_owned(), - strategy: "Momentum_ML_v1.3".to_owned(), - symbols: vec!["AAPL".to_owned(), "MSFT".to_owned(), "GOOGL".to_owned()], - progress: 28.2, - status: "Running".to_owned(), - started_at: "2025-01-23 15:15:30".to_owned(), - eta: Some("2025-01-23 18:20:00".to_owned()), - current_pnl: -2_100.25, - trade_count: 43, - }, - ]; - - // Sample historical results - self.historical_results = vec![ - BacktestResult { - id: "bt_hist_001".to_owned(), - strategy: "MeanReversion_v2.0".to_owned(), - symbols: vec!["SPY".to_owned(), "QQQ".to_owned(), "IWM".to_owned()], - period: "2024-01-01 to 2024-12-31".to_owned(), - total_return: 18.75, - sharpe_ratio: 1.42, - max_drawdown: -8.3, - win_rate: 64.2, - total_trades: 284, - completed_at: "2025-01-22 18:45:12".to_owned(), - }, - BacktestResult { - id: "bt_hist_002".to_owned(), - strategy: "Arbitrage_v3.1".to_owned(), - symbols: vec!["AAPL".to_owned(), "MSFT".to_owned()], - period: "2024-06-01 to 2024-12-31".to_owned(), - total_return: 12.34, - sharpe_ratio: 2.18, - max_drawdown: -3.7, - win_rate: 71.8, - total_trades: 156, - completed_at: "2025-01-21 22:15:45".to_owned(), - }, - BacktestResult { - id: "bt_hist_003".to_owned(), - strategy: "Momentum_ML_v1.2".to_owned(), - symbols: vec!["QQQ".to_owned(), "XLK".to_owned(), "TQQQ".to_owned()], - period: "2024-03-01 to 2024-09-30".to_owned(), - total_return: 24.67, - sharpe_ratio: 1.89, - max_drawdown: -12.1, - win_rate: 58.9, - total_trades: 412, - completed_at: "2025-01-20 16:30:22".to_owned(), - }, - ]; - - // Select first item by default - self.selected_backtest.select(Some(0)); - } - - /// Render active backtests view - fn render_active_backtests(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Min(10)].as_ref()) - .split(area); - - // Header with summary - let summary = format!( - "Active Backtests: {} | Total Progress: {:.1}%", - self.active_backtests.len(), - self.active_backtests - .iter() - .map(|bt| bt.progress) - .sum::() - / self.active_backtests.len() as f64 - ); - let header = Paragraph::new(summary).block( - Block::default() - .borders(Borders::ALL) - .title("Active Backtests Overview") - .style(Style::default().fg(Color::Green)), - ); - frame.render_widget(header, chunks[0]); - - // Active backtests list with progress bars - let items: Vec = self - .active_backtests - .iter() - .map(|bt| { - let pnl_color = if bt.current_pnl >= 0.0 { - Color::Green - } else { - Color::Red - }; - let content = format!( - "{} | {} | {:.1}% | PnL: ${:.2} | Trades: {}", - bt.strategy, - bt.symbols.join(","), - bt.progress, - bt.current_pnl, - bt.trade_count - ); - ListItem::new(content).style(Style::default().fg(pnl_color)) - }) - .collect(); - - let list = List::new(items) - .block( - Block::default() - .borders(Borders::ALL) - .title("Running Backtests (\u{2191}\u{2193} to navigate, Enter for details)") - .style(Style::default().fg(Color::White)), - ) - .highlight_style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ) - .highlight_symbol("\u{25ba} "); - - frame.render_stateful_widget(list, chunks[1], &mut self.selected_backtest); - - Ok(()) - } - - /// Render historical results view - fn render_historical_results(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Min(10)].as_ref()) - .split(area); - - // Summary stats - let avg_return = self - .historical_results - .iter() - .map(|r| r.total_return) - .sum::() - / self.historical_results.len() as f64; - let avg_sharpe = self - .historical_results - .iter() - .map(|r| r.sharpe_ratio) - .sum::() - / self.historical_results.len() as f64; - - let summary = format!( - "Historical Results: {} | Avg Return: {:.2}% | Avg Sharpe: {:.2}", - self.historical_results.len(), - avg_return, - avg_sharpe - ); - let header = Paragraph::new(summary).block( - Block::default() - .borders(Borders::ALL) - .title("Historical Performance Summary") - .style(Style::default().fg(Color::Cyan)), - ); - frame.render_widget(header, chunks[0]); - - // Results table - let headers = [ - "Strategy", "Period", "Return%", "Sharpe", "MaxDD%", "WinRate%", "Trades", - ]; - let header_cells = headers - .iter() - .map(|h| Cell::from(*h).style(Style::default().fg(Color::Yellow))); - let header_row = Row::new(header_cells).style(Style::default().bg(Color::DarkGray)); - - let rows: Vec = self - .historical_results - .iter() - .map(|result| { - let return_color = if result.total_return >= 0.0 { - Color::Green - } else { - Color::Red - }; - Row::new(vec![ - Cell::from(result.strategy.as_str()), - Cell::from(result.period.as_str()), - Cell::from(format!("{:.2}", result.total_return)) - .style(Style::default().fg(return_color)), - Cell::from(format!("{:.2}", result.sharpe_ratio)), - Cell::from(format!("{:.1}", result.max_drawdown)) - .style(Style::default().fg(Color::Red)), - Cell::from(format!("{:.1}", result.win_rate)), - Cell::from(format!("{}", result.total_trades)), - ]) - }) - .collect(); - - let table = Table::new( - rows, - [ - Constraint::Length(18), // Strategy - Constraint::Length(22), // Period - Constraint::Length(8), // Return% - Constraint::Length(7), // Sharpe - Constraint::Length(8), // MaxDD% - Constraint::Length(9), // WinRate% - Constraint::Length(7), // Trades - ], - ) - .header(header_row) - .block( - Block::default() - .borders(Borders::ALL) - .title("Historical Backtest Results") - .style(Style::default().fg(Color::White)), - ) - .column_spacing(1); - - frame.render_widget(table, chunks[1]); - - Ok(()) - } - - /// Switch to next view mode - fn next_view_mode(&mut self) { - self.view_mode = match self.view_mode { - BacktestViewMode::ActiveBacktests => BacktestViewMode::HistoricalResults, - BacktestViewMode::HistoricalResults => BacktestViewMode::PerformanceAnalysis, - BacktestViewMode::PerformanceAnalysis => BacktestViewMode::StrategyConfig, - BacktestViewMode::StrategyConfig => BacktestViewMode::ActiveBacktests, - }; - self.needs_redraw = true; - } - - /// Switch to previous view mode - fn previous_view_mode(&mut self) { - self.view_mode = match self.view_mode { - BacktestViewMode::ActiveBacktests => BacktestViewMode::StrategyConfig, - BacktestViewMode::HistoricalResults => BacktestViewMode::ActiveBacktests, - BacktestViewMode::PerformanceAnalysis => BacktestViewMode::HistoricalResults, - BacktestViewMode::StrategyConfig => BacktestViewMode::PerformanceAnalysis, - }; - self.needs_redraw = true; - } -} - -impl Dashboard for BacktestingDashboard { - fn render(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - // Create main layout with tabs - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Min(10)].as_ref()) - .split(area); - - // Render view mode tabs - let tab_title = match self.view_mode { - BacktestViewMode::ActiveBacktests => "Active Backtests [Tab: Historical]", - BacktestViewMode::HistoricalResults => "Historical Results [Tab: Performance]", - BacktestViewMode::PerformanceAnalysis => "Performance Analysis [Tab: Strategy Config]", - BacktestViewMode::StrategyConfig => "Strategy Configuration [Tab: Active]", - }; - - let tab_block = Block::default() - .borders(Borders::ALL) - .title(format!("Backtesting Dashboard - {}", tab_title)) - .style(Style::default().fg(Color::Magenta)); - frame.render_widget(tab_block, chunks[0]); - - // Render current view - match self.view_mode { - BacktestViewMode::ActiveBacktests => self.render_active_backtests(frame, chunks[1])?, - BacktestViewMode::HistoricalResults => { - self.render_historical_results(frame, chunks[1])? - }, - BacktestViewMode::PerformanceAnalysis => { - // Placeholder for performance analysis view - let content = Paragraph::new( - "Performance Analysis View\n\n\ - \u{2022} Cumulative returns chart\n\ - \u{2022} Rolling Sharpe ratio\n\ - \u{2022} Drawdown analysis\n\ - \u{2022} Trade distribution metrics\n\ - \u{2022} Risk-adjusted returns\n\n\ - [Implementation in progress...]", - ) - .block( - Block::default() - .borders(Borders::ALL) - .title("Performance Analysis") - .style(Style::default().fg(Color::Green)), - ); - frame.render_widget(content, chunks[1]); - }, - BacktestViewMode::StrategyConfig => { - // Placeholder for strategy configuration view - let content = Paragraph::new( - "Strategy Configuration View\n\n\ - \u{2022} Parameter settings\n\ - \u{2022} Optimization ranges\n\ - \u{2022} Risk constraints\n\ - \u{2022} Market data settings\n\ - \u{2022} Execution parameters\n\n\ - [Implementation in progress...]", - ) - .block( - Block::default() - .borders(Borders::ALL) - .title("Strategy Configuration") - .style(Style::default().fg(Color::Yellow)), - ); - frame.render_widget(content, chunks[1]); - }, - } - - self.needs_redraw = false; - Ok(()) - } - - fn handle_input(&mut self, key: KeyEvent) -> Result> { - use crossterm::event::KeyCode; - - match key.code { - KeyCode::Tab => { - self.next_view_mode(); - Ok(None) - }, - KeyCode::BackTab => { - self.previous_view_mode(); - Ok(None) - }, - KeyCode::Up => { - if let Some(selected) = self.selected_backtest.selected() { - let max_items = match self.view_mode { - BacktestViewMode::ActiveBacktests => self.active_backtests.len(), - BacktestViewMode::HistoricalResults => self.historical_results.len(), - _ => 0, - }; - if max_items > 0 { - let next = if selected > 0 { - selected - 1 - } else { - max_items - 1 - }; - self.selected_backtest.select(Some(next)); - self.needs_redraw = true; - } - } - Ok(None) - }, - KeyCode::Down => { - let max_items = match self.view_mode { - BacktestViewMode::ActiveBacktests => self.active_backtests.len(), - BacktestViewMode::HistoricalResults => self.historical_results.len(), - _ => 0, - }; - if max_items > 0 { - let selected = self.selected_backtest.selected().unwrap_or(0); - let next = if selected >= max_items - 1 { - 0 - } else { - selected + 1 - }; - self.selected_backtest.select(Some(next)); - self.needs_redraw = true; - } - Ok(None) - }, - KeyCode::Enter => { - // Handle selection - would show details or start actions - self.needs_redraw = true; - Ok(None) - }, - KeyCode::Char('r') => { - // Refresh data - self.load_sample_data(); - self.needs_redraw = true; - Ok(None) - }, - _ => Ok(None), - } - } - - fn update(&mut self, _event: DashboardEvent) -> Result<()> { - // Handle backtest-related events - self.needs_redraw = true; - Ok(()) - } - - fn title(&self) -> &str { - "Backtesting" - } - - fn shortcut_key(&self) -> char { - 'b' - } - - fn needs_redraw(&self) -> bool { - self.needs_redraw - } - - fn mark_drawn(&mut self) { - self.needs_redraw = false; - } -} diff --git a/tli/src/dashboard/config.rs b/tli/src/dashboard/config.rs deleted file mode 100644 index 21fe7e427..000000000 --- a/tli/src/dashboard/config.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Configuration Dashboard Implementation -//! -//! Re-exports the comprehensive `ConfigurationDashboard` from dashboards/configuration.rs - -// REMOVED: All pub use statements eliminated per cleanup requirements -// Use direct import: crate::dashboards::configuration::ConfigurationDashboard - -// Legacy compatibility - keeping the same interface -use super::Dashboard; -use crate::dashboard::events::DashboardEvent; -use tokio::sync::mpsc; - -/// Create a new configuration dashboard -pub fn create_config_dashboard(_event_sender: mpsc::Sender) -> Box { - Box::new(crate::dashboards::configuration::ConfigurationDashboard::new(_event_sender)) -} diff --git a/tli/src/dashboard/events.rs b/tli/src/dashboard/events.rs deleted file mode 100644 index 7b18ab16a..000000000 --- a/tli/src/dashboard/events.rs +++ /dev/null @@ -1,227 +0,0 @@ -//! Dashboard Event System -//! -//! Defines all events that can be exchanged between dashboards, gRPC clients, -//! and the main application loop. - -use crate::dashboard::DashboardType; -use serde::{Deserialize, Serialize}; -// Use canonical types from common crate - TLI is a pure client -use common::{Order as OrderRequest, OrderEvent, OrderSide}; - -/// Main event type for dashboard communication -#[derive(Debug, Clone)] -pub enum DashboardEvent { - // Navigation events - SwitchDashboard(DashboardType), - Exit, - - // Real-time data updates - MarketDataUpdate(MarketDataDisplayEvent), - PositionUpdate(PositionEvent), - OrderUpdate(OrderEvent), - ExecutionUpdate(ExecutionEvent), - RiskMetricsUpdate(RiskMetricsEvent), - MLPredictionUpdate(MLPredictionEvent), - ConfigurationUpdate(ConfigurationEvent), - VaultStatusUpdate(crate::dashboard::vault_status::VaultStats), - - // User action events - PlaceOrder(OrderRequest), - CancelOrder(String), // Order ID - UpdateConfiguration(ConfigUpdate), - TriggerEmergencyStop, - RefreshData, - RefreshConfig, - ShowHelp(String), - - // Configuration events - ConfigChanged { - category: String, - key: String, - }, - ConfigReloaded, - ConfigUpdateRequest(ConfigUpdateRequest), - ConfigUpdate { - category_id: i32, - settings: Vec, - }, - ConfigSearchResults { - results: Vec, - }, - - // System events - ConnectionStatus(ConnectionEvent), - Error(String), - SystemStatus(SystemStatusEvent), -} - -// Market Data Display Events (TLI-specific UI representation) -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MarketDataDisplayEvent { - pub symbol: String, - pub price: f64, - pub volume: u64, - pub timestamp: i64, - pub bid: Option, - pub ask: Option, - pub change: Option, - pub change_percent: Option, -} - -// Position Events -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PositionEvent { - pub symbol: String, - pub quantity: f64, - pub avg_price: f64, - pub current_price: f64, - pub unrealized_pnl: f64, - pub realized_pnl: f64, - pub timestamp: i64, -} - -// Order Events - using canonical OrderEvent from trading_engine - -// Execution Events -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ExecutionEvent { - pub execution_id: String, - pub order_id: String, - pub symbol: String, - pub side: OrderSide, - pub quantity: f64, - pub price: f64, - pub timestamp: i64, - pub commission: Option, -} - -// Risk Metrics Events -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RiskMetricsEvent { - pub portfolio_value: f64, - pub daily_pnl: f64, - pub total_pnl: f64, - pub var_1d: f64, - pub var_5d: f64, - pub max_drawdown: f64, - pub current_drawdown: f64, - pub risk_score: f64, - pub timestamp: i64, -} - -// ML Prediction Events -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MLPredictionEvent { - pub model_name: String, - pub symbol: String, - pub prediction: PredictionType, - pub confidence: f64, - pub signal_strength: f64, - pub features: Vec, - pub timestamp: i64, -} - -// Configuration Events -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ConfigurationEvent { - pub category: String, - pub key: String, - pub old_value: Option, - pub new_value: String, - pub timestamp: i64, - pub changed_by: String, -} - -// OrderRequest is now imported at the top - -// Configuration Update -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ConfigUpdate { - pub category: String, - pub key: String, - pub value: serde_json::Value, - pub changed_by: String, -} - -// Configuration Update Request for gRPC communication - PURE CLIENT ARCHITECTURE -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ConfigUpdateRequest { - pub category: String, - pub key: String, - pub value: serde_json::Value, - pub reason: String, -} - -// Connection Events -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ConnectionEvent { - pub service_name: String, - pub status: ConnectionStatus, - pub endpoint: String, - pub latency_ms: Option, - pub last_seen: i64, -} - -// System Status Events -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SystemStatusEvent { - pub trading_enabled: bool, - pub risk_controls_active: bool, - pub ml_models_online: u32, - pub total_ml_models: u32, - pub active_positions: u32, - pub pending_orders: u32, - pub timestamp: i64, -} - -// OrderSide is now imported at the top - -// OrderType and OrderStatus now imported from canonical source via common::types - -// REMOVED: TimeInForce duplicate - use common::TimeInForce - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -pub enum PredictionType { - Buy, - Sell, - Hold, - StrongBuy, - StrongSell, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -pub enum ConnectionStatus { - Connected, - Disconnected, - Connecting, - Error, -} - -// OrderSide Display implementation now provided by canonical source - -// OrderType Display implementation now provided by canonical source - -// OrderStatus Display implementation now provided by canonical source - -impl std::fmt::Display for PredictionType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PredictionType::Buy => write!(f, "BUY"), - PredictionType::Sell => write!(f, "SELL"), - PredictionType::Hold => write!(f, "HOLD"), - PredictionType::StrongBuy => write!(f, "STRONG_BUY"), - PredictionType::StrongSell => write!(f, "STRONG_SELL"), - } - } -} - -impl std::fmt::Display for ConnectionStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ConnectionStatus::Connected => write!(f, "\u{25cf}\u{25cf}\u{25cf}"), - ConnectionStatus::Disconnected => write!(f, "\u{25cb}\u{25cb}\u{25cb}"), - ConnectionStatus::Connecting => write!(f, "\u{25cf}\u{25cb}\u{25cb}"), - ConnectionStatus::Error => write!(f, "\u{2717}\u{2717}\u{2717}"), - } - } -} diff --git a/tli/src/dashboard/layout.rs b/tli/src/dashboard/layout.rs deleted file mode 100644 index 1ab9fe325..000000000 --- a/tli/src/dashboard/layout.rs +++ /dev/null @@ -1,57 +0,0 @@ -//! Layout management for TLI dashboards - -use ratatui::prelude::*; - -/// Layout manager for consistent UI layouts -pub struct LayoutManager { - /// Current layout configuration - pub layout_type: LayoutType, -} - -#[derive(Debug, Clone, Copy)] -pub enum LayoutType { - Standard, - Compact, - Extended, -} - -impl Default for LayoutManager { - fn default() -> Self { - Self::new() - } -} - -impl LayoutManager { - pub const fn new() -> Self { - Self { - layout_type: LayoutType::Standard, - } - } - - pub fn create_layout(&self, area: Rect) -> (Rect, Rect, Rect, Rect) { - // Create header, content, sidebar, footer areas - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), // Header - Constraint::Min(10), // Content - Constraint::Length(3), // Footer - ]) - .split(area); - - let middle_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Min(80), // Main content - Constraint::Length(25), // Sidebar - ]) - .split(chunks[1]); - - ( - chunks[0_usize], - middle_chunks[0_usize], - middle_chunks[1_usize], - chunks[2_usize], - ) - } -} diff --git a/tli/src/dashboard/ml.rs b/tli/src/dashboard/ml.rs deleted file mode 100644 index a6b91f1f6..000000000 --- a/tli/src/dashboard/ml.rs +++ /dev/null @@ -1,604 +0,0 @@ -//! ML Training Dashboard Implementation -//! -//! Comprehensive ML training management dashboard with: -//! - Real-time training progress monitoring -//! - Model performance metrics visualization -//! - Training data quality indicators -//! - Resource utilization tracking (GPU/CPU) -//! - Training job lifecycle management - -use super::Dashboard; -use crate::client::ml_training_client::{ - MLTrainingClient, ResourceMonitoringEvent, TrainingJobContext, TrainingProgressEvent, -}; -use crate::dashboard::events::DashboardEvent; -use crate::proto::ml::{TrainingJob, TrainingMetrics, TrainingStatus}; -use anyhow::Result; -use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::{ - prelude::*, - widgets::{Block, Borders, Cell, Paragraph, Row, Table, TableState, Wrap}, -}; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::mpsc; -use tokio::time::Instant; - -/// Training job display information -#[derive(Debug, Clone)] -pub struct TrainingJobDisplay { - pub job: TrainingJob, - pub context: TrainingJobContext, - pub last_update: Instant, - pub progress_history: Vec<(Instant, f64)>, // Time, progress percentage - pub metrics_history: Vec<(Instant, TrainingMetrics)>, // Time, metrics - pub is_selected: bool, -} - -/// Resource utilization display -#[derive(Debug, Clone)] -pub struct ResourceDisplay { - pub gpu_utilization: f64, - pub gpu_memory_used: f64, - pub cpu_utilization: f64, - pub memory_used: f64, - pub available_gpus: i32, - pub total_gpus: i32, - pub last_update: Instant, - pub history: Vec<(Instant, f64, f64)>, // Time, GPU util, CPU util -} - -/// Dashboard state for ML training management -#[derive(Debug, Clone)] -pub enum MLDashboardState { - JobList, // Main job list view - JobDetail, // Detailed view of selected job - StartJob, // Job creation form - ResourceView, // Resource monitoring view -} - -/// ML Training Dashboard with comprehensive management features -pub struct MLDashboard { - _event_sender: mpsc::Sender, - needs_redraw: bool, - state: MLDashboardState, - - // Training job management - training_jobs: HashMap, - selected_job_id: Option, - job_list_scroll: usize, - - // Resource monitoring - resource_display: Option, - - // ML Training client integration - ml_client: Option>, - #[allow(dead_code)] - progress_receivers: HashMap>, - #[allow(dead_code)] - resource_receiver: Option>, - - // UI state - #[allow(dead_code)] - show_logs: bool, - auto_refresh: bool, - refresh_interval: std::time::Duration, - last_refresh: Instant, - - // Form state for job creation - form_model_name: String, - form_dataset_id: String, - form_learning_rate: String, - form_batch_size: String, - form_epochs: String, - form_field_index: usize, -} - -impl MLDashboard { - pub fn new(_event_sender: mpsc::Sender) -> Self { - Self { - _event_sender, - needs_redraw: true, - state: MLDashboardState::JobList, - - training_jobs: HashMap::new(), - selected_job_id: None, - job_list_scroll: 0_usize, - - resource_display: None, - - ml_client: None, - progress_receivers: HashMap::new(), - resource_receiver: None, - - show_logs: false, - auto_refresh: true, - refresh_interval: std::time::Duration::from_secs(5), - last_refresh: Instant::now(), - - form_model_name: String::new(), - form_dataset_id: String::new(), - form_learning_rate: "0.001".to_owned(), - form_batch_size: "32".to_owned(), - form_epochs: "100".to_owned(), - form_field_index: 0_usize, - } - } - - /// Set the ML training client for dashboard operations - pub fn set_ml_client(&mut self, client: Arc) { - self.ml_client = Some(client); - self.needs_redraw = true; - } - - /// Add or update a training job - pub fn update_training_job(&mut self, job: TrainingJob, context: TrainingJobContext) { - let job_id = job.job_id.clone(); - - if let Some(display) = self.training_jobs.get_mut(&job_id) { - // Update existing job - display.job = job; - display.context = context; - display.last_update = Instant::now(); - - // Add progress point to history - display - .progress_history - .push((Instant::now(), display.job.progress_percentage)); - - // Keep only last 100 data points - if display.progress_history.len() > 100 { - display.progress_history.remove(0); - } - } else { - // New job - let display = TrainingJobDisplay { - job: job.clone(), - context, - last_update: Instant::now(), - progress_history: vec![(Instant::now(), job.progress_percentage)], - metrics_history: Vec::new(), - is_selected: self.selected_job_id.as_ref() == Some(&job_id), - }; - - self.training_jobs.insert(job_id, display); - } - - self.needs_redraw = true; - } - - /// Update resource utilization - pub fn update_resource_utilization( - &mut self, - gpu_util: f64, - gpu_memory: f64, - cpu_util: f64, - memory: f64, - available_gpus: i32, - total_gpus: i32, - ) { - if let Some(resource) = &mut self.resource_display { - resource.gpu_utilization = gpu_util; - resource.gpu_memory_used = gpu_memory; - resource.cpu_utilization = cpu_util; - resource.memory_used = memory; - resource.available_gpus = available_gpus; - resource.total_gpus = total_gpus; - resource.last_update = Instant::now(); - - // Add to history - resource.history.push((Instant::now(), gpu_util, cpu_util)); - if resource.history.len() > 60 { - // Keep 1 minute of data - resource.history.remove(0); - } - } else { - self.resource_display = Some(ResourceDisplay { - gpu_utilization: gpu_util, - gpu_memory_used: gpu_memory, - cpu_utilization: cpu_util, - memory_used: memory, - available_gpus, - total_gpus, - last_update: Instant::now(), - history: vec![(Instant::now(), gpu_util, cpu_util)], - }); - } - - self.needs_redraw = true; - } - - /// Render the job list view - fn render_job_list(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), // Header - Constraint::Min(10), // Job table - Constraint::Length(4), // Resource summary - ]) - .split(area); - - // Header with controls - let header = Paragraph::new( - "ML Training Dashboard | [s] Start Job | [Enter] Job Details | [r] Resources | [q] Quit" - ) - .block(Block::default().borders(Borders::ALL).title("Training Jobs")) - .style(Style::default().fg(Color::Cyan)); - frame.render_widget(header, chunks[0_usize]); - - // Job table - let jobs: Vec<_> = self.training_jobs.values().collect(); - let rows: Vec = jobs - .iter() - .map(|job_display| { - let status_style = match job_display.job.status() { - TrainingStatus::Running => Style::default().fg(Color::Green), - TrainingStatus::Completed => Style::default().fg(Color::Blue), - TrainingStatus::Failed => Style::default().fg(Color::Red), - TrainingStatus::Queued => Style::default().fg(Color::Yellow), - _ => Style::default().fg(Color::Gray), - }; - - let progress_bar = format!("{:>6.1}%", job_display.job.progress_percentage); - - let status_text = format!("{:?}", job_display.job.status()); - let model_name = job_display.job.model_name.clone(); - let job_id_short = job_display.job.job_id.chars().take(8).collect::(); - - Row::new(vec![ - Cell::from(job_id_short), - Cell::from(model_name), - Cell::from(status_text).style(status_style), - Cell::from(progress_bar), - Cell::from(format!( - "{:.3}", - job_display - .job - .current_metrics - .as_ref() - .map(|m| m.loss) - .unwrap_or(0.0) - )), - Cell::from(format!( - "{:.1}%", - job_display - .job - .current_metrics - .as_ref() - .map(|m| m.accuracy * 100.0) - .unwrap_or(0.0) - )), - ]) - }) - .collect(); - - let table = Table::new( - rows, - [ - Constraint::Length(8), // Job ID - Constraint::Length(15), // Model - Constraint::Length(12), // Status - Constraint::Length(8), // Progress - Constraint::Length(8), // Loss - Constraint::Length(8), // Accuracy - ], - ) - .header( - Row::new(vec![ - "Job ID", "Model", "Status", "Progress", "Loss", "Accuracy", - ]) - .style(Style::default().fg(Color::Yellow)), - ) - .block( - Block::default() - .borders(Borders::ALL) - .title("Active Training Jobs"), - ) - .highlight_style(Style::default().add_modifier(Modifier::REVERSED)); - - frame.render_stateful_widget(table, chunks[1_usize], &mut TableState::default()); - - // Resource summary - if let Some(resource) = &self.resource_display { - let resource_info = format!( - "GPU: {:.1}% ({}/{} available) | CPU: {:.1}% | Memory: {:.1}%", - resource.gpu_utilization * 100.0, - resource.available_gpus, - resource.total_gpus, - resource.cpu_utilization * 100.0, - resource.memory_used * 100.0 - ); - - let resource_widget = Paragraph::new(resource_info) - .block( - Block::default() - .borders(Borders::ALL) - .title("Resource Utilization"), - ) - .style(Style::default().fg(Color::Green)); - frame.render_widget(resource_widget, chunks[2_usize]); - } - - Ok(()) - } - - /// Render the job creation form - fn render_start_job_form(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), // Title - Constraint::Length(3), // Model name - Constraint::Length(3), // Dataset ID - Constraint::Length(3), // Learning rate - Constraint::Length(3), // Batch size - Constraint::Length(3), // Epochs - Constraint::Length(3), // Actions - Constraint::Min(1), // Spacer - ]) - .split(area); - - // Title - let title = Paragraph::new("Start New Training Job") - .block( - Block::default() - .borders(Borders::ALL) - .title("Create Training Job"), - ) - .style(Style::default().fg(Color::Cyan)); - frame.render_widget(title, chunks[0_usize]); - - // Form fields - let fields = [ - ("Model Name", &self.form_model_name), - ("Dataset ID", &self.form_dataset_id), - ("Learning Rate", &self.form_learning_rate), - ("Batch Size", &self.form_batch_size), - ("Epochs", &self.form_epochs), - ]; - - for (i, (label, value)) in fields.into_iter().enumerate() { - let style = if i == self.form_field_index { - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD) - } else { - Style::default() - }; - - let field = Paragraph::new(format!("{}: {}", label, value)) - .block(Block::default().borders(Borders::ALL)) - .style(style); - frame.render_widget(field, chunks[i + 1_usize]); - } - - // Actions - let actions = Paragraph::new("[Enter] Start Job | [Esc] Cancel | [Tab] Next Field") - .block(Block::default().borders(Borders::ALL).title("Actions")) - .style(Style::default().fg(Color::Green)); - frame.render_widget(actions, chunks[6_usize]); - - Ok(()) - } - - /// Render resource monitoring view - fn render_resource_view(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), // Header - Constraint::Length(6), // GPU info - Constraint::Length(6), // CPU/Memory info - Constraint::Min(8), // Usage chart - ]) - .split(area); - - // Header - let header = Paragraph::new("Resource Monitoring | [Esc] Back to Jobs") - .block( - Block::default() - .borders(Borders::ALL) - .title("System Resources"), - ) - .style(Style::default().fg(Color::Cyan)); - frame.render_widget(header, chunks[0_usize]); - - if let Some(resource) = &self.resource_display { - // GPU information - let gpu_info = format!( - "GPU Utilization: {:.1}%\nGPU Memory: {:.1}%\nAvailable GPUs: {}/{}\nGPU Type: V100/A100", - resource.gpu_utilization * 100.0, - resource.gpu_memory_used * 100.0, - resource.available_gpus, - resource.total_gpus - ); - - let gpu_widget = Paragraph::new(gpu_info) - .block(Block::default().borders(Borders::ALL).title("GPU Status")) - .style(Style::default().fg(Color::Green)); - frame.render_widget(gpu_widget, chunks[1_usize]); - - // CPU/Memory information - let cpu_info = format!( - "CPU Utilization: {:.1}%\nMemory Usage: {:.1}%\nActive Training Jobs: {}\nLast Update: {:?} ago", - resource.cpu_utilization * 100.0, - resource.memory_used * 100.0, - self.training_jobs.len(), - resource.last_update.elapsed() - ); - - let cpu_widget = Paragraph::new(cpu_info) - .block( - Block::default() - .borders(Borders::ALL) - .title("CPU/Memory Status"), - ) - .style(Style::default().fg(Color::Blue)); - frame.render_widget(cpu_widget, chunks[2_usize]); - - // Usage chart (simplified representation) - let chart_data = if resource.history.len() > 1 { - resource - .history - .iter() - .enumerate() - .map(|(i, (_, gpu, cpu))| { - format!("{:2}: GPU {:3.0}% CPU {:3.0}%", i, gpu * 100.0, cpu * 100.0) - }) - .collect::>() - .join("\n") - } else { - "Collecting data...".to_owned() - }; - - let chart_widget = Paragraph::new(chart_data) - .block( - Block::default() - .borders(Borders::ALL) - .title("Usage History"), - ) - .wrap(Wrap { trim: true }); - frame.render_widget(chart_widget, chunks[3_usize]); - } else { - let no_data = Paragraph::new("No resource data available") - .block( - Block::default() - .borders(Borders::ALL) - .title("Resource Monitor"), - ) - .style(Style::default().fg(Color::Red)); - frame.render_widget(no_data, chunks[1_usize]); - } - - Ok(()) - } -} - -impl Dashboard for MLDashboard { - fn render(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - match self.state { - MLDashboardState::JobList => self.render_job_list(frame, area), - MLDashboardState::JobDetail => { - // TODO: Implement detailed job view - self.render_job_list(frame, area) - }, - MLDashboardState::StartJob => self.render_start_job_form(frame, area), - MLDashboardState::ResourceView => self.render_resource_view(frame, area), - } - } - - fn handle_input(&mut self, key: KeyEvent) -> Result> { - match self.state { - MLDashboardState::JobList => match key.code { - KeyCode::Char('s') => { - self.state = MLDashboardState::StartJob; - self.needs_redraw = true; - }, - KeyCode::Char('r') => { - self.state = MLDashboardState::ResourceView; - self.needs_redraw = true; - }, - KeyCode::Enter => { - self.state = MLDashboardState::JobDetail; - self.needs_redraw = true; - }, - KeyCode::Up => { - self.job_list_scroll = self.job_list_scroll.saturating_sub(1); - self.needs_redraw = true; - }, - KeyCode::Down => { - if self.job_list_scroll < self.training_jobs.len().saturating_sub(1) { - self.job_list_scroll += 1; - } - self.needs_redraw = true; - }, - _ => {}, - }, - MLDashboardState::StartJob => { - match key.code { - KeyCode::Esc => { - self.state = MLDashboardState::JobList; - self.needs_redraw = true; - }, - KeyCode::Tab => { - self.form_field_index = (self.form_field_index + 1) % 5; - self.needs_redraw = true; - }, - KeyCode::Enter => { - // TODO: Start training job - self.state = MLDashboardState::JobList; - self.needs_redraw = true; - }, - KeyCode::Char(c) => { - match self.form_field_index { - 0 => self.form_model_name.push(c), - 1 => self.form_dataset_id.push(c), - 2 => self.form_learning_rate.push(c), - 3 => self.form_batch_size.push(c), - 4 => self.form_epochs.push(c), - _ => {}, - } - self.needs_redraw = true; - }, - KeyCode::Backspace => { - match self.form_field_index { - 0 => { - self.form_model_name.pop(); - }, - 1 => { - self.form_dataset_id.pop(); - }, - 2 => { - self.form_learning_rate.pop(); - }, - 3 => { - self.form_batch_size.pop(); - }, - 4 => { - self.form_epochs.pop(); - }, - _ => {}, - } - self.needs_redraw = true; - }, - _ => {}, - } - }, - MLDashboardState::ResourceView | MLDashboardState::JobDetail => { - if key.code == KeyCode::Esc { - self.state = MLDashboardState::JobList; - self.needs_redraw = true; - } - }, - } - - Ok(None) - } - - fn update(&mut self, _event: DashboardEvent) -> Result<()> { - // Auto-refresh logic - if self.auto_refresh && self.last_refresh.elapsed() > self.refresh_interval { - self.needs_redraw = true; - self.last_refresh = Instant::now(); - } - - Ok(()) - } - - fn title(&self) -> &str { - "ML Training" - } - - fn shortcut_key(&self) -> char { - 'm' - } - - fn needs_redraw(&self) -> bool { - self.needs_redraw - } - - fn mark_drawn(&mut self) { - self.needs_redraw = false; - } -} diff --git a/tli/src/dashboard/mod.rs b/tli/src/dashboard/mod.rs deleted file mode 100644 index 5b55d40df..000000000 --- a/tli/src/dashboard/mod.rs +++ /dev/null @@ -1,322 +0,0 @@ -//! Dashboard Framework for TLI Terminal Interface -//! -//! This module provides a comprehensive dashboard system for the Foxhunt HFT trading system. -//! It implements a multi-dashboard architecture with real-time data streaming and interactive -//! controls using Ratatui for terminal-based visualization. -//! -//! ## Architecture -//! - **`DashboardManager`**: Central coordinator for all dashboards -//! - **Dashboard Trait**: Common interface for all dashboard implementations -//! - **Real-time Updates**: Event-driven data streaming from gRPC services -//! - **Navigation**: Keyboard shortcuts for dashboard switching -//! - **Layout Management**: Consistent UI layout across all dashboards - -use anyhow::Result; -use crossterm::event::KeyEvent; -use ratatui::prelude::*; -use std::collections::HashMap; -use tokio::sync::mpsc; - -// Import from events module -use crate::dashboard::events::DashboardEvent; -use crate::dashboard::layout::LayoutManager; - -pub mod backtesting; -pub mod config; -pub mod events; -pub mod layout; -pub mod ml; -pub mod performance; -pub mod risk; -pub mod trading; -pub mod vault_status; - -// Import dashboard implementations after module declarations -use backtesting::BacktestingDashboard; -use config::create_config_dashboard; -use ml::MLDashboard; -use performance::PerformanceDashboard; -use risk::RiskDashboard; -use trading::TradingDashboard; -use vault_status::VaultStatusWidget; - -// NO RE-EXPORTS: Import directly from submodules -// Use tli::dashboard::events::DashboardEvent instead -// Use tli::dashboard::layout::LayoutManager instead - -/// Main dashboard manager that coordinates all dashboards -pub struct DashboardManager { - pub active_dashboard: DashboardType, - pub dashboards: HashMap>, - pub layout_manager: LayoutManager, - pub event_receiver: mpsc::Receiver, - pub _event_sender: mpsc::Sender, - // Vault service removed - TLI is pure client, uses shared config crate -} - -/// Available dashboard types -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum DashboardType { - Trading, // Live positions, orders, executions, market data - Risk, // VaR, drawdown, position limits, safety controls - ML, // Model predictions, signal strength, confidence - Performance, // PnL, Sharpe ratios, strategy performance - Config, // System configuration management - Backtesting, // Strategy testing, historical analysis, results - Vault, // Vault status, credentials, service discovery -} - -impl DashboardType { - pub fn all() -> Vec { - vec![ - DashboardType::Trading, - DashboardType::Risk, - DashboardType::ML, - DashboardType::Performance, - DashboardType::Config, - DashboardType::Backtesting, - DashboardType::Vault, - ] - } - - pub const fn shortcut_key(&self) -> char { - match self { - DashboardType::Trading => 't', - DashboardType::Risk => 'r', - DashboardType::ML => 'm', - DashboardType::Performance => 'p', - DashboardType::Config => 'c', - DashboardType::Backtesting => 'b', - DashboardType::Vault => 'v', - } - } - - pub const fn title(&self) -> &'static str { - match self { - DashboardType::Trading => "Trading", - DashboardType::Risk => "Risk", - DashboardType::ML => "ML", - DashboardType::Performance => "Performance", - DashboardType::Config => "Configuration", - DashboardType::Backtesting => "Backtesting", - DashboardType::Vault => "Vault Status", - } - } -} - -/// Common interface for all dashboard implementations -pub trait Dashboard: Send + Sync { - /// Render the dashboard to the given frame area - fn render(&mut self, frame: &mut Frame, area: Rect) -> Result<()>; - - /// Handle keyboard input and return optional dashboard events - fn handle_input(&mut self, key: KeyEvent) -> Result>; - - /// Update dashboard with new data/events - fn update(&mut self, event: DashboardEvent) -> Result<()>; - - /// Get dashboard title for display - fn title(&self) -> &str; - - /// Get keyboard shortcut for this dashboard - fn shortcut_key(&self) -> char; - - /// Check if dashboard needs redraw - fn needs_redraw(&self) -> bool; - - /// Mark dashboard as drawn - fn mark_drawn(&mut self); -} - -impl DashboardManager { - pub fn new() -> (Self, mpsc::Sender) { - let (_event_sender, event_receiver) = mpsc::channel(1000); - - let mut dashboards: HashMap> = HashMap::new(); - - // Initialize all dashboards - dashboards.insert( - DashboardType::Trading, - Box::new(TradingDashboard::new(_event_sender.clone())), - ); - dashboards.insert( - DashboardType::Risk, - Box::new(RiskDashboard::new(_event_sender.clone())), - ); - dashboards.insert( - DashboardType::ML, - Box::new(MLDashboard::new(_event_sender.clone())), - ); - dashboards.insert( - DashboardType::Performance, - Box::new(PerformanceDashboard::new(_event_sender.clone())), - ); - dashboards.insert( - DashboardType::Config, - create_config_dashboard(_event_sender.clone()), - ); - dashboards.insert( - DashboardType::Backtesting, - Box::new(BacktestingDashboard::new(_event_sender.clone())), - ); - dashboards.insert( - DashboardType::Vault, - Box::new(VaultStatusWidget::new(_event_sender.clone())), - ); - - let manager = Self { - active_dashboard: DashboardType::Trading, - dashboards, - layout_manager: LayoutManager::new(), - event_receiver, - _event_sender: _event_sender.clone(), - }; - - (manager, _event_sender) - } - - pub fn render(&mut self, frame: &mut Frame) -> Result<()> { - let area = frame.area(); - - // Create main layout - let (header_area, content_area, sidebar_area, footer_area) = - self.layout_manager.create_layout(area); - - // Render header with navigation tabs - self.render_header(frame, header_area)?; - - // Render active dashboard - if let Some(dashboard) = self.dashboards.get_mut(&self.active_dashboard) { - dashboard.render(frame, content_area)?; - } - - // Render sidebar with quick stats - self.render_sidebar(frame, sidebar_area)?; - - // Render footer with help and status - self.render_footer(frame, footer_area)?; - - Ok(()) - } - - pub fn handle_input(&mut self, key: KeyEvent) -> Result> { - // Check for dashboard switching shortcuts first - for dashboard_type in DashboardType::all() { - if key.code == crossterm::event::KeyCode::Char(dashboard_type.shortcut_key()) { - self.active_dashboard = dashboard_type; - return Ok(Some(DashboardEvent::SwitchDashboard(dashboard_type))); - } - } - - // Handle ESC for exit - if key.code == crossterm::event::KeyCode::Esc { - return Ok(Some(DashboardEvent::Exit)); - } - - // Pass input to active dashboard - if let Some(dashboard) = self.dashboards.get_mut(&self.active_dashboard) { - dashboard.handle_input(key) - } else { - Ok(None) - } - } - - pub async fn handle_event(&mut self, event: DashboardEvent) -> Result { - match event { - DashboardEvent::SwitchDashboard(dashboard_type) => { - self.active_dashboard = dashboard_type; - Ok(false) - }, - DashboardEvent::Exit => { - Ok(true) // Signal to exit - }, - _ => { - // Forward event to all dashboards that might be interested - for dashboard in self.dashboards.values_mut() { - let _ = dashboard.update(event.clone()); - } - Ok(false) - }, - } - } - - // Vault service functionality removed - TLI is pure client, uses shared config crate - - /// All vault-related methods removed - TLI uses shared config crate instead - // Vault service functionality completely removed from TLI - // TLI is a pure client - no vault service management - - fn render_header(&self, frame: &mut Frame, area: Rect) -> Result<()> { - let titles: Vec = DashboardType::all() - .iter() - .map(|dt| { - let _prefix = if *dt == self.active_dashboard { - "\u{25cf}" - } else { - "\u{25cb}" - }; - format!("[{}]{}", dt.shortcut_key().to_uppercase(), dt.title()) - }) - .collect(); - - let tabs = ratatui::widgets::Tabs::new(titles) - .block( - ratatui::widgets::Block::default() - .borders(ratatui::widgets::Borders::ALL) - .title("Foxhunt HFT Trading System - TLI Terminal"), - ) - .style(Style::default().fg(Color::White)) - .highlight_style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ) - .select(self.active_dashboard as usize); - - frame.render_widget(tabs, area); - Ok(()) - } - - fn render_sidebar(&self, frame: &mut Frame, area: Rect) -> Result<()> { - let block = ratatui::widgets::Block::default() - .borders(ratatui::widgets::Borders::ALL) - .title("Quick Stats"); - - // Get actual Vault status (placeholder - TLI uses shared config crate) - let vault_status = "\u{25cb}"; // Empty circle for not available - use config crate integration - - let content = ratatui::widgets::Paragraph::new( - format!( - "Connection: \u{25cf}\u{25cf}\u{25cf}\nVault: {}\nLatency: 12ms\nOrders: 15\nPositions: 5\nPnL: +$2,500", - vault_status - ), - ) - .block(block) - .wrap(ratatui::widgets::Wrap { trim: true }); - - frame.render_widget(content, area); - Ok(()) - } - - fn render_footer(&self, frame: &mut Frame, area: Rect) -> Result<()> { - let help_text = format!( - "[{}] Dashboards | [ESC] Exit | Status: Connected", - DashboardType::all() - .iter() - .map(|dt| format!( - "[{}]{}", - dt.shortcut_key().to_uppercase(), - dt.title().chars().next().unwrap_or(' ') - )) - .collect::>() - .join(" ") - ); - - let footer = ratatui::widgets::Paragraph::new(help_text) - .block(ratatui::widgets::Block::default().borders(ratatui::widgets::Borders::ALL)) - .style(Style::default().fg(Color::Gray)); - - frame.render_widget(footer, area); - Ok(()) - } -} diff --git a/tli/src/dashboard/observability.rs b/tli/src/dashboard/observability.rs deleted file mode 100644 index 9bc7e412f..000000000 --- a/tli/src/dashboard/observability.rs +++ /dev/null @@ -1,597 +0,0 @@ -//! # Enhanced Observability Dashboard -//! -//! Comprehensive observability dashboard for the Foxhunt HFT system featuring: -//! - OpenTelemetry/OTLP distributed tracing visualization -//! - P50/P95/P99 order acknowledgment latency histograms -//! - Real-time Parquet market data persistence monitoring -//! - System-wide metrics across all critical paths - -use crate::error::TliResult; -// All types from common crate - TLI is a pure client -use common::{ - get_order_ack_percentiles, LatencyPercentiles, MarketDataEvent, - MARKET_DATA_BUFFER, TELEMETRY_TRACER, ORDER_ACK_LATENCY, - HardwareTimestamp, LatencyStats, HftLatencyTracker -}; -use ratatui::{ - backend::Backend, - layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - symbols, - text::{Line, Span, Text}, - widgets::{ - Axis, BarChart, Block, Borders, Chart, Clear, Dataset, Gauge, List, ListItem, - Paragraph, Row, Sparkline, Table, Tabs, - }, - Frame, -}; -use std::collections::HashMap; -use tokio::sync::RwLock; -use tracing::{debug, info, warn}; - -/// Enhanced observability dashboard state -#[derive(Debug)] -pub struct ObservabilityDashboard { - pub selected_tab: usize, - pub latency_history: Vec, - pub throughput_history: Vec, - pub parquet_buffer_stats: BufferStats, - pub order_ack_stats: HashMap, - pub telemetry_spans: Vec, - pub system_metrics: SystemMetrics, - pub update_counter: u64, -} - -#[derive(Debug, Clone)] -pub struct BufferStats { - pub buffered_events: usize, - pub buffer_capacity: usize, - pub utilization_percent: f64, - pub events_per_second: f64, - pub last_flush_ago: u64, // seconds -} - -#[derive(Debug, Clone)] -pub struct SpanInfo { - pub operation: String, - pub venue: String, - pub duration_us: f64, - pub timestamp: u64, - pub trace_id: String, - pub span_id: String, -} - -#[derive(Debug, Clone)] -pub struct SystemMetrics { - pub cpu_usage: f64, - pub memory_usage: f64, - pub network_rx: u64, - pub network_tx: u64, - pub disk_io: u64, - pub active_connections: u32, -} - -impl Default for ObservabilityDashboard { - fn default() -> Self { - Self { - selected_tab: 0, - latency_history: Vec::with_capacity(100), - throughput_history: Vec::with_capacity(100), - parquet_buffer_stats: BufferStats { - buffered_events: 0, - buffer_capacity: 10000, - utilization_percent: 0.0, - events_per_second: 0.0, - last_flush_ago: 0, - }, - order_ack_stats: HashMap::new(), - telemetry_spans: Vec::new(), - system_metrics: SystemMetrics { - cpu_usage: 0.0, - memory_usage: 0.0, - network_rx: 0, - network_tx: 0, - disk_io: 0, - active_connections: 0, - }, - update_counter: 0, - } - } -} - -impl ObservabilityDashboard { - pub fn new() -> Self { - Self::default() - } - - /// Update dashboard with latest metrics - pub async fn update(&mut self) -> TliResult<()> { - self.update_counter += 1; - - // Update order acknowledgment latency stats - self.update_order_ack_stats().await; - - // Update Parquet buffer stats - self.update_parquet_buffer_stats().await; - - // Update telemetry spans - self.update_telemetry_spans().await; - - // Update system metrics - self.update_system_metrics().await; - - // Update latency history (simulated for now) - if self.latency_history.len() >= 100 { - self.latency_history.remove(0); - } - self.latency_history.push(self.get_current_latency_us()); - - // Update throughput history - if self.throughput_history.len() >= 100 { - self.throughput_history.remove(0); - } - self.throughput_history.push(self.get_current_throughput()); - - Ok(()) - } - - /// Render the enhanced observability dashboard - pub fn render(&mut self, frame: &mut Frame, area: Rect) { - let tabs = vec!["Latency", "Throughput", "Parquet", "Telemetry", "System"]; - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Min(0)]) - .split(area); - - // Render tabs - let tabs_widget = Tabs::new(tabs) - .block(Block::default().borders(Borders::ALL).title("Observability Dashboard")) - .highlight_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)) - .select(self.selected_tab); - frame.render_widget(tabs_widget, chunks[0]); - - // Render selected tab content - match self.selected_tab { - 0 => self.render_latency_tab(frame, chunks[1]), - 1 => self.render_throughput_tab(frame, chunks[1]), - 2 => self.render_parquet_tab(frame, chunks[1]), - 3 => self.render_telemetry_tab(frame, chunks[1]), - 4 => self.render_system_tab(frame, chunks[1]), - _ => {} - } - } - - /// Render latency analysis tab with P50/P95/P99 histograms - fn render_latency_tab(&self, frame: &mut Frame, area: Rect) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(60), Constraint::Percentage(40)]) - .split(area); - - // Top section: Latency chart - let latency_chart = Chart::new(vec![ - Dataset::default() - .name("Order Latency (μs)") - .marker(symbols::Marker::Braille) - .style(Style::default().fg(Color::Cyan)) - .data(&self.latency_history.iter().enumerate().map(|(i, &y)| (i as f64, y)).collect::>()), - ]) - .block( - Block::default() - .title("Real-time Order Latency") - .borders(Borders::ALL) - ) - .x_axis( - Axis::default() - .title("Time") - .bounds([0.0, 100.0]) - .style(Style::default().fg(Color::Gray)) - ) - .y_axis( - Axis::default() - .title("Latency (μs)") - .bounds([0.0, 1000.0]) - .style(Style::default().fg(Color::Gray)) - ); - frame.render_widget(latency_chart, chunks[0]); - - // Bottom section: P50/P95/P99 statistics table - let rows: Vec = self.order_ack_stats - .iter() - .map(|(venue, stats)| { - Row::new(vec![ - venue.clone(), - format!("{:.1}", stats.p50_us), - format!("{:.1}", stats.p95_us), - format!("{:.1}", stats.p99_us), - format!("{:.1}", stats.max_us), - stats.count.to_string(), - ]) - }) - .collect(); - - let latency_table = Table::new(rows) - .header( - Row::new(vec!["Venue", "P50 (μs)", "P95 (μs)", "P99 (μs)", "Max (μs)", "Count"]) - .style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) - ) - .block( - Block::default() - .title("Order Acknowledgment Latency Statistics") - .borders(Borders::ALL) - ) - .widths(&[ - Constraint::Percentage(20), - Constraint::Percentage(16), - Constraint::Percentage(16), - Constraint::Percentage(16), - Constraint::Percentage(16), - Constraint::Percentage(16), - ]); - frame.render_widget(latency_table, chunks[1]); - } - - /// Render throughput analysis tab - fn render_throughput_tab(&self, frame: &mut Frame, area: Rect) { - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(70), Constraint::Percentage(30)]) - .split(area); - - // Left: Throughput sparkline - let sparkline = Sparkline::default() - .block( - Block::default() - .title("Message Throughput (msgs/sec)") - .borders(Borders::ALL) - ) - .data(&self.throughput_history) - .style(Style::default().fg(Color::Green)); - frame.render_widget(sparkline, chunks[0]); - - // Right: Current stats - let current_throughput = self.throughput_history.last().copied().unwrap_or(0); - let avg_throughput = if !self.throughput_history.is_empty() { - self.throughput_history.iter().sum::() / self.throughput_history.len() as u64 - } else { - 0 - }; - - let stats_text = vec![ - Line::from(vec![ - Span::styled("Current: ", Style::default().fg(Color::Yellow)), - Span::styled( - format!("{} msgs/sec", current_throughput), - Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD) - ), - ]), - Line::from(vec![ - Span::styled("Average: ", Style::default().fg(Color::Yellow)), - Span::styled( - format!("{} msgs/sec", avg_throughput), - Style::default().fg(Color::Green).add_modifier(Modifier::BOLD) - ), - ]), - Line::from(""), - Line::from(vec![ - Span::styled("Peak: ", Style::default().fg(Color::Yellow)), - Span::styled( - format!("{} msgs/sec", self.throughput_history.iter().max().copied().unwrap_or(0)), - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD) - ), - ]), - ]; - - let stats_paragraph = Paragraph::new(stats_text) - .block( - Block::default() - .title("Throughput Statistics") - .borders(Borders::ALL) - ); - frame.render_widget(stats_paragraph, chunks[1]); - } - - /// Render Parquet persistence monitoring tab - fn render_parquet_tab(&self, frame: &mut Frame, area: Rect) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(40), Constraint::Percentage(60)]) - .split(area); - - // Top: Buffer utilization gauge - let buffer_gauge = Gauge::default() - .block( - Block::default() - .title("Parquet Buffer Utilization") - .borders(Borders::ALL) - ) - .gauge_style(Style::default().fg(Color::Cyan)) - .percent(self.parquet_buffer_stats.utilization_percent as u16) - .label(format!( - "{}/{} events ({:.1}%)", - self.parquet_buffer_stats.buffered_events, - self.parquet_buffer_stats.buffer_capacity, - self.parquet_buffer_stats.utilization_percent - )); - frame.render_widget(buffer_gauge, chunks[0]); - - // Bottom: Detailed statistics - let parquet_stats = vec![ - Line::from(vec![ - Span::styled("Buffered Events: ", Style::default().fg(Color::Yellow)), - Span::styled( - self.parquet_buffer_stats.buffered_events.to_string(), - Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD) - ), - ]), - Line::from(vec![ - Span::styled("Buffer Capacity: ", Style::default().fg(Color::Yellow)), - Span::styled( - self.parquet_buffer_stats.buffer_capacity.to_string(), - Style::default().fg(Color::Green).add_modifier(Modifier::BOLD) - ), - ]), - Line::from(vec![ - Span::styled("Events/Second: ", Style::default().fg(Color::Yellow)), - Span::styled( - format!("{:.1}", self.parquet_buffer_stats.events_per_second), - Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD) - ), - ]), - Line::from(vec![ - Span::styled("Last Flush: ", Style::default().fg(Color::Yellow)), - Span::styled( - format!("{}s ago", self.parquet_buffer_stats.last_flush_ago), - Style::default().fg(Color::Gray) - ), - ]), - ]; - - let parquet_paragraph = Paragraph::new(parquet_stats) - .block( - Block::default() - .title("Parquet Persistence Statistics") - .borders(Borders::ALL) - ); - frame.render_widget(parquet_paragraph, chunks[1]); - } - - /// Render OpenTelemetry distributed tracing tab - fn render_telemetry_tab(&self, frame: &mut Frame, area: Rect) { - let items: Vec = self.telemetry_spans - .iter() - .take(10) // Show last 10 spans - .map(|span| { - ListItem::new(vec![ - Line::from(vec![ - Span::styled( - format!("{} @ {}", span.operation, span.venue), - Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD) - ), - Span::styled( - format!(" ({:.1}μs)", span.duration_us), - Style::default().fg(if span.duration_us > 100.0 { Color::Red } else { Color::Green }) - ), - ]), - Line::from(vec![ - Span::styled("Trace: ", Style::default().fg(Color::Gray)), - Span::styled(&span.trace_id, Style::default().fg(Color::Yellow)), - ]), - ]) - }) - .collect(); - - let telemetry_list = List::new(items) - .block( - Block::default() - .title("Recent OpenTelemetry Spans") - .borders(Borders::ALL) - ) - .highlight_style(Style::default().add_modifier(Modifier::BOLD)); - frame.render_widget(telemetry_list, area); - } - - /// Render system metrics tab - fn render_system_tab(&self, frame: &mut Frame, area: Rect) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(area); - - let top_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(chunks[0]); - - // CPU Usage Gauge - let cpu_gauge = Gauge::default() - .block( - Block::default() - .title("CPU Usage") - .borders(Borders::ALL) - ) - .gauge_style(Style::default().fg(Color::Red)) - .percent(self.system_metrics.cpu_usage as u16) - .label(format!("{:.1}%", self.system_metrics.cpu_usage)); - frame.render_widget(cpu_gauge, top_chunks[0]); - - // Memory Usage Gauge - let memory_gauge = Gauge::default() - .block( - Block::default() - .title("Memory Usage") - .borders(Borders::ALL) - ) - .gauge_style(Style::default().fg(Color::Blue)) - .percent(self.system_metrics.memory_usage as u16) - .label(format!("{:.1}%", self.system_metrics.memory_usage)); - frame.render_widget(memory_gauge, top_chunks[1]); - - // Network and connection stats - let system_stats = vec![ - Line::from(vec![ - Span::styled("Network RX: ", Style::default().fg(Color::Yellow)), - Span::styled( - format!("{} MB/s", self.system_metrics.network_rx / 1_000_000), - Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD) - ), - ]), - Line::from(vec![ - Span::styled("Network TX: ", Style::default().fg(Color::Yellow)), - Span::styled( - format!("{} MB/s", self.system_metrics.network_tx / 1_000_000), - Style::default().fg(Color::Green).add_modifier(Modifier::BOLD) - ), - ]), - Line::from(vec![ - Span::styled("Disk I/O: ", Style::default().fg(Color::Yellow)), - Span::styled( - format!("{} MB/s", self.system_metrics.disk_io / 1_000_000), - Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD) - ), - ]), - Line::from(vec![ - Span::styled("Active Connections: ", Style::default().fg(Color::Yellow)), - Span::styled( - self.system_metrics.active_connections.to_string(), - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD) - ), - ]), - ]; - - let system_paragraph = Paragraph::new(system_stats) - .block( - Block::default() - .title("System Statistics") - .borders(Borders::ALL) - ); - frame.render_widget(system_paragraph, chunks[1]); - } - - /// Handle tab navigation - pub fn next_tab(&mut self) { - self.selected_tab = (self.selected_tab + 1) % 5; - } - - pub fn previous_tab(&mut self) { - self.selected_tab = if self.selected_tab > 0 { self.selected_tab - 1 } else { 4 }; - } - - // Private update methods - - async fn update_order_ack_stats(&mut self) { - // Update with real data from the metrics system - for venue in &["binance", "coinbase", "kraken"] { - for order_type in &["market", "limit"] { - if let Some(stats) = get_order_ack_percentiles(venue, order_type) { - let key = format!("{}_{}", venue, order_type); - self.order_ack_stats.insert(key, stats); - } - } - } - } - - async fn update_parquet_buffer_stats(&mut self) { - let buffer = MARKET_DATA_BUFFER.read(); - self.parquet_buffer_stats.buffered_events = buffer.len(); - self.parquet_buffer_stats.utilization_percent = - (buffer.len() as f64 / buffer.capacity() as f64) * 100.0; - - // Simulate events per second (would be calculated from actual metrics) - self.parquet_buffer_stats.events_per_second = 1250.0 + (rand::random::() * 500.0); - self.parquet_buffer_stats.last_flush_ago = self.update_counter % 60; - } - - async fn update_telemetry_spans(&mut self) { - // In a real implementation, this would query the telemetry system - // For now, simulate some spans - if self.update_counter % 5 == 0 { - let span = SpanInfo { - operation: "submit_order".to_string(), - venue: "binance".to_string(), - duration_us: 45.0 + (rand::random::() * 100.0), - timestamp: std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() as u64, - trace_id: format!("trace_{}", self.update_counter), - span_id: format!("span_{}", self.update_counter), - }; - - self.telemetry_spans.insert(0, span); - if self.telemetry_spans.len() > 50 { - self.telemetry_spans.truncate(50); - } - } - } - - async fn update_system_metrics(&mut self) { - // Simulate system metrics (would be from actual system monitoring) - self.system_metrics.cpu_usage = 25.0 + (rand::random::() * 40.0); - self.system_metrics.memory_usage = 60.0 + (rand::random::() * 20.0); - self.system_metrics.network_rx = 10_000_000 + (rand::random::() % 5_000_000); - self.system_metrics.network_tx = 8_000_000 + (rand::random::() % 4_000_000); - self.system_metrics.disk_io = 2_000_000 + (rand::random::() % 1_000_000); - self.system_metrics.active_connections = 150 + (rand::random::() % 50); - } - - fn get_current_latency_us(&self) -> f64 { - // Get the most recent P95 latency from order ack stats - self.order_ack_stats - .values() - .map(|stats| stats.p95_us as f64) - .fold(0.0, f64::max) - .max(10.0 + (rand::random::() * 200.0)) - } - - fn get_current_throughput(&self) -> u64 { - // Simulate current throughput - 5000 + (rand::random::() % 3000) - } -} - -/// Integration with main TLI dashboard -pub fn integrate_observability_dashboard() -> ObservabilityDashboard { - info!("Initializing enhanced observability dashboard"); - - // Initialize telemetry if not already done - let _tracer = &*TELEMETRY_TRACER; - - ObservabilityDashboard::new() -} - -#[cfg(test)] -#[allow(clippy::unwrap_used, clippy::expect_used)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_observability_dashboard_creation() { - let mut dashboard = ObservabilityDashboard::new(); - assert_eq!(dashboard.selected_tab, 0); - assert!(dashboard.latency_history.is_empty()); - } - - #[tokio::test] - async fn test_dashboard_update() { - let mut dashboard = ObservabilityDashboard::new(); - let result = dashboard.update().await; - assert!(result.is_ok()); - assert!(dashboard.update_counter > 0); - } - - #[test] - fn test_tab_navigation() { - let mut dashboard = ObservabilityDashboard::new(); - - dashboard.next_tab(); - assert_eq!(dashboard.selected_tab, 1); - - dashboard.previous_tab(); - assert_eq!(dashboard.selected_tab, 0); - - dashboard.previous_tab(); - assert_eq!(dashboard.selected_tab, 4); // Wraps around - } -} \ No newline at end of file diff --git a/tli/src/dashboard/performance.rs b/tli/src/dashboard/performance.rs deleted file mode 100644 index 7e21c8c06..000000000 --- a/tli/src/dashboard/performance.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Performance Dashboard Implementation - -use super::Dashboard; -use crate::dashboard::events::DashboardEvent; -use anyhow::Result; -use crossterm::event::KeyEvent; -use ratatui::{ - prelude::*, - widgets::{Block, Borders, Paragraph}, -}; -use tokio::sync::mpsc; - -pub struct PerformanceDashboard { - _event_sender: mpsc::Sender, - needs_redraw: bool, -} - -impl PerformanceDashboard { - pub const fn new(_event_sender: mpsc::Sender) -> Self { - Self { - _event_sender, - needs_redraw: true, - } - } -} - -impl Dashboard for PerformanceDashboard { - fn render(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let paragraph = Paragraph::new("Performance Dashboard\n\nTotal Return: +15.67% YTD\nDaily PnL: +$2,500\nSharpe Ratio: 1.85\nWin Rate: 66.8%\nTotal Trades: 247") - .block(Block::default().borders(Borders::ALL).title("Performance Dashboard")); - frame.render_widget(paragraph, area); - self.needs_redraw = false; - Ok(()) - } - - fn handle_input(&mut self, _key: KeyEvent) -> Result> { - Ok(None) - } - fn update(&mut self, _event: DashboardEvent) -> Result<()> { - Ok(()) - } - fn title(&self) -> &str { - "Performance" - } - fn shortcut_key(&self) -> char { - 'p' - } - fn needs_redraw(&self) -> bool { - self.needs_redraw - } - fn mark_drawn(&mut self) { - self.needs_redraw = false; - } -} diff --git a/tli/src/dashboard/risk.rs b/tli/src/dashboard/risk.rs deleted file mode 100644 index b889e94ed..000000000 --- a/tli/src/dashboard/risk.rs +++ /dev/null @@ -1,187 +0,0 @@ -//! Risk Dashboard Implementation -//! -//! Real-time risk monitoring dashboard showing: -//! - `VaR` metrics -//! - Position limits -//! - Drawdown monitor -//! - Safety controls - -use super::Dashboard; -use crate::dashboard::events::DashboardEvent; -use crate::dashboard::events::RiskMetricsEvent; -use anyhow::Result; -use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::{ - prelude::*, - widgets::{Block, Borders, Gauge, Paragraph}, -}; -use tokio::sync::mpsc; - -pub struct RiskDashboard { - _event_sender: mpsc::Sender, - risk_metrics: Option, - needs_redraw: bool, - emergency_stop_armed: bool, -} - -impl RiskDashboard { - pub const fn new(_event_sender: mpsc::Sender) -> Self { - Self { - _event_sender, - risk_metrics: None, - needs_redraw: true, - emergency_stop_armed: false, - } - } - - fn render_var_metrics(&self, frame: &mut Frame, area: Rect) { - let text = if let Some(metrics) = &self.risk_metrics { - format!( - "VaR Metrics\n\n1-Day: ${:.0}\n5-Day: ${:.0}\n30-Day: ${:.0}\nConfidence: 95%\nMethod: Monte Carlo\nLast Calc: Now", - metrics.var_1d, - metrics.var_5d, - metrics.var_5d * 2.0, // Approximate 30-day - ) - } else { - "VaR Metrics\n\n1-Day: $5,000\n5-Day: $8,000\n30-Day: $12,000\nConfidence: 95%\nMethod: Monte Carlo\nLast Calc: 14:30".to_owned() - }; - - let paragraph = - Paragraph::new(text).block(Block::default().borders(Borders::ALL).title("VaR Metrics")); - - frame.render_widget(paragraph, area); - } - - fn render_position_limits(&self, frame: &mut Frame, area: Rect) { - let text = "Position Limits\n\nMax Per Symbol:\n$100K (50% used)\n\nTotal Exposure:\n$2.5M (80% used)\n\nConcentration:\n25% (limit 30%)"; - - let paragraph = Paragraph::new(text).block( - Block::default() - .borders(Borders::ALL) - .title("Position Limits"), - ); - - frame.render_widget(paragraph, area); - } - - fn render_drawdown_monitor(&self, frame: &mut Frame, area: Rect) { - let current_dd = if let Some(metrics) = &self.risk_metrics { - metrics.current_drawdown - } else { - -0.025 // -2.5% - }; - - let dd_percentage = (current_dd * 100.0).abs(); - let dd_ratio = (dd_percentage / 15.0).min(1.0); // Max 15% drawdown - - let gauge = Gauge::default() - .block( - Block::default() - .borders(Borders::ALL) - .title("Drawdown Monitor"), - ) - .gauge_style(if dd_ratio > 0.8 { - Style::default().fg(Color::Red) - } else if dd_ratio > 0.5 { - Style::default().fg(Color::Yellow) - } else { - Style::default().fg(Color::Green) - }) - .ratio(dd_ratio) - .label(format!("Current: -{:.1}%", dd_percentage)); - - frame.render_widget(gauge, area); - } - - fn render_safety_controls(&self, frame: &mut Frame, area: Rect) { - let status_color = if self.emergency_stop_armed { - Color::Red - } else { - Color::Green - }; - - let text = format!( - "Safety Controls\n\nKill Switch:\n\u{25cf}\u{25cf}\u{25cf}\u{25cf} {}\n\nAuto Recovery:\n\u{25cf}\u{25cf}\u{25cf}\u{25cf} ENABLED\n\nLast Test: 14:00\n\n[E] Emergency Stop\n[R] Reset Controls", - if self.emergency_stop_armed { "ARMED" } else { "ACTIVE" } - ); - - let paragraph = Paragraph::new(text).block( - Block::default() - .borders(Borders::ALL) - .title("Safety Controls") - .border_style(Style::default().fg(status_color)), - ); - - frame.render_widget(paragraph, area); - } -} - -impl Dashboard for RiskDashboard { - fn render(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - // Create a 2x2 grid layout - let rows = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(area); - - let top_cols = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(rows[0_usize]); - - let bottom_cols = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(rows[1_usize]); - - self.render_var_metrics(frame, top_cols[0_usize]); - self.render_position_limits(frame, top_cols[1_usize]); - self.render_drawdown_monitor(frame, bottom_cols[0_usize]); - self.render_safety_controls(frame, bottom_cols[1_usize]); - - self.needs_redraw = false; - Ok(()) - } - - fn handle_input(&mut self, key: KeyEvent) -> Result> { - match key.code { - KeyCode::Char('e') | KeyCode::Char('E') => { - self.emergency_stop_armed = !self.emergency_stop_armed; - self.needs_redraw = true; - if self.emergency_stop_armed { - return Ok(Some(DashboardEvent::TriggerEmergencyStop)); - } - }, - KeyCode::Char('r') | KeyCode::Char('R') => { - self.emergency_stop_armed = false; - self.needs_redraw = true; - }, - _ => {}, - } - Ok(None) - } - - fn update(&mut self, event: DashboardEvent) -> Result<()> { - if let DashboardEvent::RiskMetricsUpdate(metrics) = event { - self.risk_metrics = Some(metrics); - self.needs_redraw = true; - } - Ok(()) - } - - fn title(&self) -> &str { - "Risk" - } - - fn shortcut_key(&self) -> char { - 'r' - } - - fn needs_redraw(&self) -> bool { - self.needs_redraw - } - - fn mark_drawn(&mut self) { - self.needs_redraw = false; - } -} diff --git a/tli/src/dashboard/trading.rs b/tli/src/dashboard/trading.rs deleted file mode 100644 index e52259ee4..000000000 --- a/tli/src/dashboard/trading.rs +++ /dev/null @@ -1,376 +0,0 @@ -//! Trading Dashboard Implementation -//! -//! Real-time trading dashboard showing: -//! - Market data feeds -//! - Active positions -//! - Order book -//! - Recent executions -//! - Order entry interface - -use super::{Dashboard, DashboardEvent}; -use crate::dashboard::events::{ExecutionEvent, MarketDataDisplayEvent, PositionEvent}; -use anyhow::Result; -use common::{ - HftTimestamp, Order as OrderRequest, OrderEvent, OrderId, OrderSide, OrderStatus, OrderType, - Quantity, Symbol, TimeInForce, -}; -use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::{ - prelude::*, - widgets::{Block, Borders, Cell, Paragraph, Row, Table, TableState}, -}; -use std::collections::HashMap; -use tokio::sync::mpsc; - -pub struct TradingDashboard { - _event_sender: mpsc::Sender, - market_data: HashMap, - positions: HashMap, - recent_orders: Vec, - recent_executions: Vec, - table_state: TableState, - needs_redraw: bool, - selected_symbol: String, -} - -impl TradingDashboard { - pub fn new(_event_sender: mpsc::Sender) -> Self { - let mut state = TableState::default(); - state.select(Some(0)); - - Self { - _event_sender, - market_data: HashMap::new(), - positions: HashMap::new(), - recent_orders: Vec::new(), - recent_executions: Vec::new(), - table_state: state, - needs_redraw: true, - selected_symbol: "AAPL".to_owned(), - } - } - - fn render_market_data(&self, frame: &mut Frame, area: Rect) { - let mut rows = vec![]; - - // Add sample data if no real data available - if self.market_data.is_empty() { - rows.extend(vec![ - Row::new(vec![ - Cell::from("AAPL"), - Cell::from("$150.25"), - Cell::from("+1.25%"), - Cell::from("1_usize,250_usize,000"), - ]), - Row::new(vec![ - Cell::from("TSLA"), - Cell::from("$800.50"), - Cell::from("-0.75%"), - Cell::from("850_usize,000"), - ]), - Row::new(vec![ - Cell::from("SPY"), - Cell::from("$420.10"), - Cell::from("+0.45%"), - Cell::from("5_usize,500_usize,000"), - ]), - ]); - } else { - for (symbol, data) in &self.market_data { - rows.push(Row::new(vec![ - Cell::from(symbol.clone()), - Cell::from(format!("${:.2}", data.price)), - Cell::from(format!("{:.2}%", data.change_percent.unwrap_or(0.0))), - Cell::from(format!("{}", data.volume)), - ])); - } - } - - let table = Table::new( - rows, - [ - Constraint::Length(8), // Symbol - Constraint::Length(10), // Price - Constraint::Length(8), // Change - Constraint::Length(12), // Volume - ], - ) - .header( - Row::new(vec!["Symbol", "Price", "Change", "Volume"]).style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ), - ) - .block(Block::default().borders(Borders::ALL).title("Market Data")) - .highlight_style(Style::default().bg(Color::DarkGray)); - - frame.render_stateful_widget(table, area, &mut self.table_state.clone()); - } - - fn render_positions(&self, frame: &mut Frame, area: Rect) { - let mut rows = vec![]; - - // Add sample data if no real data available - if self.positions.is_empty() { - rows.extend(vec![ - Row::new(vec![ - Cell::from("AAPL"), - Cell::from("1000"), - Cell::from("$150.00"), - Cell::from("+$250.00"), - ]), - Row::new(vec![ - Cell::from("TSLA"), - Cell::from("-500"), - Cell::from("$800.00"), - Cell::from("-$375.00"), - ]), - ]); - } else { - for (symbol, position) in &self.positions { - rows.push(Row::new(vec![ - Cell::from(symbol.clone()), - Cell::from(format!("{:.0}", position.quantity)), - Cell::from(format!("${:.2}", position.avg_price)), - Cell::from(format!("${:.2}", position.unrealized_pnl)), - ])); - } - } - - let table = Table::new( - rows, - [ - Constraint::Length(8), // Symbol - Constraint::Length(8), // Quantity - Constraint::Length(10), // Avg Price - Constraint::Length(12), // PnL - ], - ) - .header( - Row::new(vec!["Symbol", "Qty", "Avg Price", "Unrealized PnL"]).style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ), - ) - .block(Block::default().borders(Borders::ALL).title("Positions")); - - frame.render_widget(table, area); - } - - fn render_order_entry(&self, frame: &mut Frame, area: Rect) { - let text = format!( - "Order Entry\n\nSymbol: {}\nSide: [BUY \u{25bc}]\nQty: [500 ]\nPrice: [MKT \u{25bc}]\n\n[F1] Submit Order\n[F2] Cancel\n\nLast Order: BUY 100 AAPL @MKT", - self.selected_symbol - ); - - let paragraph = Paragraph::new(text) - .block(Block::default().borders(Borders::ALL).title("Order Entry")) - .wrap(ratatui::widgets::Wrap { trim: true }); - - frame.render_widget(paragraph, area); - } - - fn render_recent_executions(&self, frame: &mut Frame, area: Rect) { - let mut rows = vec![]; - - // Add sample data if no real data available - if self.recent_executions.is_empty() { - rows.extend(vec![ - Row::new(vec![ - Cell::from("14:35:21"), - Cell::from("AAPL"), - Cell::from("BUY"), - Cell::from("500"), - Cell::from("$150.25"), - ]), - Row::new(vec![ - Cell::from("14:34:15"), - Cell::from("TSLA"), - Cell::from("SELL"), - Cell::from("200"), - Cell::from("$800.75"), - ]), - ]); - } else { - for execution in &self.recent_executions { - let time = chrono::DateTime::from_timestamp(execution.timestamp, 0) - .unwrap_or_default() - .format("%H:%M:%S") - .to_string(); - - rows.push(Row::new(vec![ - Cell::from(time), - Cell::from(execution.symbol.clone()), - Cell::from(execution.side.to_string()), - Cell::from(format!("{:.0}", execution.quantity)), - Cell::from(format!("${:.2}", execution.price)), - ])); - } - } - - let table = Table::new( - rows, - [ - Constraint::Length(8), // Time - Constraint::Length(8), // Symbol - Constraint::Length(6), // Side - Constraint::Length(8), // Quantity - Constraint::Length(10), // Price - ], - ) - .header( - Row::new(vec!["Time", "Symbol", "Side", "Qty", "Price"]).style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ), - ) - .block( - Block::default() - .borders(Borders::ALL) - .title("Recent Executions"), - ); - - frame.render_widget(table, area); - } -} - -impl Dashboard for TradingDashboard { - fn render(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - // Create a 2x2 grid layout for the trading dashboard - let rows = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(60), Constraint::Percentage(40)]) - .split(area); - - let top_cols = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(60), Constraint::Percentage(40)]) - .split(rows[0_usize]); - - let bottom_cols = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(rows[1_usize]); - - // Render each section - self.render_market_data(frame, top_cols[0_usize]); - self.render_positions(frame, top_cols[1_usize]); - self.render_order_entry(frame, bottom_cols[0_usize]); - self.render_recent_executions(frame, bottom_cols[1_usize]); - - self.needs_redraw = false; - Ok(()) - } - - fn handle_input(&mut self, key: KeyEvent) -> Result> { - match key.code { - KeyCode::Up => { - if let Some(selected) = self.table_state.selected() { - if selected > 0 { - self.table_state.select(Some(selected - 1)); - self.needs_redraw = true; - } - } - }, - KeyCode::Down => { - if let Some(selected) = self.table_state.selected() { - self.table_state.select(Some(selected + 1)); - self.needs_redraw = true; - } - }, - KeyCode::F(1) => { - // Submit order - let order_request = OrderRequest { - id: OrderId::new(), - client_order_id: Some(format!("tli-{}", chrono::Utc::now().timestamp())), - broker_order_id: None, - account_id: None, - symbol: Symbol::from(self.selected_symbol.clone()), - side: OrderSide::Buy, - order_type: OrderType::Market, - status: OrderStatus::Pending, - time_in_force: TimeInForce::Day, - quantity: Quantity::from_f64(500.0).unwrap_or(Quantity::ZERO), - filled_quantity: Quantity::ZERO, - remaining_quantity: Quantity::from_f64(500.0).unwrap_or(Quantity::ZERO), - price: None, - stop_price: None, - average_fill_price: None, - exchange_order_id: None, - average_price: None, - avg_fill_price: None, // Database compatibility alias - parent_id: None, - execution_algorithm: None, - execution_params: serde_json::json!({}), - stop_loss: None, - take_profit: None, - created_at: HftTimestamp::now_or_zero(), - updated_at: Some(HftTimestamp::now_or_zero()), - expires_at: None, - metadata: serde_json::json!({}), - }; - return Ok(Some(DashboardEvent::PlaceOrder(order_request))); - }, - KeyCode::Enter => { - // Switch selected symbol based on table selection - if let Some(selected) = self.table_state.selected() { - let symbols = ["AAPL", "TSLA", "SPY"]; - if selected < symbols.len() { - self.selected_symbol = symbols[selected].to_owned(); - self.needs_redraw = true; - } - } - }, - _ => {}, - } - Ok(None) - } - - fn update(&mut self, event: DashboardEvent) -> Result<()> { - match event { - DashboardEvent::MarketDataUpdate(data) => { - self.market_data.insert(data.symbol.clone(), data); - self.needs_redraw = true; - }, - DashboardEvent::PositionUpdate(position) => { - self.positions.insert(position.symbol.clone(), position); - self.needs_redraw = true; - }, - DashboardEvent::OrderUpdate(order) => { - self.recent_orders.push(order); - if self.recent_orders.len() > 10 { - self.recent_orders.remove(0); - } - self.needs_redraw = true; - }, - DashboardEvent::ExecutionUpdate(execution) => { - self.recent_executions.push(execution); - if self.recent_executions.len() > 10 { - self.recent_executions.remove(0); - } - self.needs_redraw = true; - }, - _ => {}, - } - Ok(()) - } - - fn title(&self) -> &str { - "Trading" - } - - fn shortcut_key(&self) -> char { - 't' - } - - fn needs_redraw(&self) -> bool { - self.needs_redraw - } - - fn mark_drawn(&mut self) { - self.needs_redraw = false; - } -} diff --git a/tli/src/dashboard/vault_status.rs b/tli/src/dashboard/vault_status.rs deleted file mode 100644 index 7d5b66691..000000000 --- a/tli/src/dashboard/vault_status.rs +++ /dev/null @@ -1,370 +0,0 @@ -//! Vault Status Dashboard Component - -use super::{Dashboard, DashboardEvent}; -use anyhow::Result; -use crossterm::event::KeyEvent; -use ratatui::prelude::*; -use ratatui::widgets::{Block, Borders, Gauge, List, ListItem, Paragraph, Wrap}; -use std::sync::Arc; -use tokio::sync::mpsc; -use tokio::sync::RwLock; - -/// Vault connection statistics -#[derive(Debug, Clone)] -pub struct VaultStats { - pub health_status: VaultHealthStatus, - pub connection_count: u32, - pub cache_hit_ratio: f64, - pub credentials_cached: u32, - pub services_discovered: u32, - pub last_health_check: Option>, - pub rotation_stats: RotationStats, -} - -#[derive(Debug, Clone, PartialEq)] -pub enum VaultHealthStatus { - Healthy, - Degraded, - Unhealthy, - Unknown, -} - -#[derive(Debug, Clone)] -pub struct RotationStats { - pub total_rotations: u32, - pub successful_rotations: u32, - pub failed_rotations: u32, - pub pending_rotations: u32, -} - -impl Default for VaultStats { - fn default() -> Self { - Self { - health_status: VaultHealthStatus::Unknown, - connection_count: 0, - cache_hit_ratio: 0.0, - credentials_cached: 0, - services_discovered: 0, - last_health_check: None, - rotation_stats: RotationStats { - total_rotations: 0, - successful_rotations: 0, - failed_rotations: 0, - pending_rotations: 0, - }, - } - } -} - -/// Vault status dashboard widget -pub struct VaultStatusWidget { - stats: Arc>, - _event_sender: mpsc::Sender, - needs_redraw: bool, -} - -impl VaultStatusWidget { - pub fn new(_event_sender: mpsc::Sender) -> Self { - Self { - stats: Arc::new(RwLock::new(VaultStats::default())), - _event_sender, - needs_redraw: true, - } - } - - /// Update vault statistics - pub async fn update_stats(&self, stats: VaultStats) { - let mut current_stats = self.stats.write().await; - *current_stats = stats; - } - - /// Get current vault statistics - pub async fn get_stats(&self) -> VaultStats { - self.stats.read().await.clone() - } - - /// Render vault status widget in a specific area - pub async fn render_widget(&self, frame: &mut Frame<'_>, area: Rect) -> Result<()> { - let stats = self.stats.read().await; - - // Create main layout - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), // Health status - Constraint::Length(7), // Connection stats - Constraint::Min(3), // Rotation status - ]) - .split(area); - - // Health Status - self.render_health_status(frame, chunks[0], &stats)?; - - // Connection Statistics - self.render_connection_stats(frame, chunks[1], &stats)?; - - // Rotation Statistics - self.render_rotation_stats(frame, chunks[2], &stats)?; - - Ok(()) - } - - fn render_health_status( - &self, - frame: &mut Frame, - area: Rect, - stats: &VaultStats, - ) -> Result<()> { - let (status_text, status_color) = match stats.health_status { - VaultHealthStatus::Healthy => ("HEALTHY", Color::Green), - VaultHealthStatus::Degraded => ("DEGRADED", Color::Yellow), - VaultHealthStatus::Unhealthy => ("UNHEALTHY", Color::Red), - VaultHealthStatus::Unknown => ("UNKNOWN", Color::Gray), - }; - - let last_check = if let Some(timestamp) = stats.last_health_check { - format!(" (Last: {})", timestamp.format("%H:%M:%S")) - } else { - " (Never checked)".to_owned() - }; - - let paragraph = Paragraph::new(format!("Status: {}{}", status_text, last_check)) - .block( - Block::default() - .borders(Borders::ALL) - .title("Vault Health") - .border_style(Style::default().fg(status_color)), - ) - .style(Style::default().fg(status_color)) - .wrap(Wrap { trim: true }); - - frame.render_widget(paragraph, area); - Ok(()) - } - - fn render_connection_stats( - &self, - frame: &mut Frame, - area: Rect, - stats: &VaultStats, - ) -> Result<()> { - let items = vec![ - ListItem::new(format!("Connections: {}", stats.connection_count)), - ListItem::new(format!( - "Cache Hit Ratio: {:.1}%", - stats.cache_hit_ratio * 100.0 - )), - ListItem::new(format!("Cached Credentials: {}", stats.credentials_cached)), - ListItem::new(format!( - "Services Discovered: {}", - stats.services_discovered - )), - ]; - - let list = List::new(items) - .block( - Block::default() - .borders(Borders::ALL) - .title("Connection Statistics"), - ) - .style(Style::default().fg(Color::White)); - - frame.render_widget(list, area); - Ok(()) - } - - fn render_rotation_stats( - &self, - frame: &mut Frame, - area: Rect, - stats: &VaultStats, - ) -> Result<()> { - let rotation_stats = &stats.rotation_stats; - - let success_rate = if rotation_stats.total_rotations > 0 { - rotation_stats.successful_rotations as f64 / rotation_stats.total_rotations as f64 - } else { - 0.0 - }; - - // Split area for gauge and list - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), // Success rate gauge - Constraint::Min(3), // Stats list - ]) - .split(area); - - // Success rate gauge - let gauge = Gauge::default() - .block( - Block::default() - .borders(Borders::ALL) - .title("Rotation Success Rate"), - ) - .gauge_style(if success_rate > 0.8 { - Style::default().fg(Color::Green) - } else if success_rate > 0.5 { - Style::default().fg(Color::Yellow) - } else { - Style::default().fg(Color::Red) - }) - .ratio(success_rate) - .label(format!("{:.1}%", success_rate * 100.0)); - - frame.render_widget(gauge, chunks[0]); - - // Rotation statistics list - let items = vec![ - ListItem::new(format!( - "Total Rotations: {}", - rotation_stats.total_rotations - )), - ListItem::new(format!( - "Successful: {}", - rotation_stats.successful_rotations - )) - .style(Style::default().fg(Color::Green)), - ListItem::new(format!("Failed: {}", rotation_stats.failed_rotations)) - .style(Style::default().fg(Color::Red)), - ListItem::new(format!("Pending: {}", rotation_stats.pending_rotations)) - .style(Style::default().fg(Color::Yellow)), - ]; - - let list = List::new(items) - .block( - Block::default() - .borders(Borders::ALL) - .title("Credential Rotations"), - ) - .style(Style::default().fg(Color::White)); - - frame.render_widget(list, chunks[1]); - Ok(()) - } -} - -impl Dashboard for VaultStatusWidget { - fn render(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - // This would be called for a full dashboard view - let block = Block::default() - .borders(Borders::ALL) - .title("Vault Status Dashboard"); - - let inner_area = block.inner(area); - frame.render_widget(block, area); - - // Use async runtime to render widget - let rt = tokio::runtime::Handle::current(); - rt.block_on(self.render_widget(frame, inner_area))?; - - self.needs_redraw = false; - Ok(()) - } - - fn handle_input(&mut self, key: KeyEvent) -> Result> { - use crossterm::event::KeyCode; - - match key.code { - KeyCode::Char('r') => { - // Refresh vault stats - self.needs_redraw = true; - Ok(Some(DashboardEvent::RefreshData)) - }, - KeyCode::Char('h') => { - // Show help - Ok(Some(DashboardEvent::ShowHelp("Vault Status".to_owned()))) - }, - _ => Ok(None), - } - } - - fn update(&mut self, event: DashboardEvent) -> Result<()> { - match event { - DashboardEvent::RefreshData => { - self.needs_redraw = true; - }, - DashboardEvent::VaultStatusUpdate(stats) => { - // Update stats in background since we can't use async in trait method - let stats_clone = self.stats.clone(); - tokio::spawn(async move { - let mut current_stats = stats_clone.write().await; - *current_stats = stats; - }); - self.needs_redraw = true; - }, - _ => {}, - } - Ok(()) - } - - fn title(&self) -> &str { - "Vault Status" - } - - fn shortcut_key(&self) -> char { - 'v' - } - - fn needs_redraw(&self) -> bool { - self.needs_redraw - } - - fn mark_drawn(&mut self) { - self.needs_redraw = false; - } -} - -/// Helper function to get status color for health status -pub const fn get_vault_status_color(status: &VaultHealthStatus) -> Color { - match status { - VaultHealthStatus::Healthy => Color::Green, - VaultHealthStatus::Degraded => Color::Yellow, - VaultHealthStatus::Unhealthy => Color::Red, - VaultHealthStatus::Unknown => Color::Gray, - } -} - -/// Helper function to get status symbol for health status -pub const fn get_vault_status_symbol(status: &VaultHealthStatus) -> &'static str { - match status { - VaultHealthStatus::Healthy => "\u{25cf}", // Green circle - VaultHealthStatus::Degraded => "\u{25d0}", // Half circle - VaultHealthStatus::Unhealthy => "\u{25cb}", // Empty circle - VaultHealthStatus::Unknown => "?", // Question mark - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_vault_stats_default() { - let stats = VaultStats::default(); - assert_eq!(stats.health_status, VaultHealthStatus::Unknown); - assert_eq!(stats.connection_count, 0); - assert_eq!(stats.cache_hit_ratio, 0.0); - } - - #[test] - fn test_vault_status_colors() { - assert_eq!( - get_vault_status_color(&VaultHealthStatus::Healthy), - Color::Green - ); - assert_eq!( - get_vault_status_color(&VaultHealthStatus::Degraded), - Color::Yellow - ); - assert_eq!( - get_vault_status_color(&VaultHealthStatus::Unhealthy), - Color::Red - ); - assert_eq!( - get_vault_status_color(&VaultHealthStatus::Unknown), - Color::Gray - ); - } -} diff --git a/tli/src/dashboards/config_manager.rs b/tli/src/dashboards/config_manager.rs deleted file mode 100644 index 6ebf1bfce..000000000 --- a/tli/src/dashboards/config_manager.rs +++ /dev/null @@ -1,1364 +0,0 @@ -//! Comprehensive Configuration Management Dashboards for TLI -//! -//! This module provides specialized configuration management dashboards for each configuration -//! category in the Foxhunt HFT system. Each dashboard connects to the centralized `ConfigManager` -//! for `PostgreSQL` hot-reload, Vault integration, and real-time configuration updates. -//! -//! ## Dashboards Provided: -//! - **Trading Config Dashboard**: Order sizes, position limits, execution parameters -//! - **Risk Config Dashboard**: `VaR` limits, circuit breakers, safety controls -//! - **ML Config Dashboard**: Model parameters, inference settings, training configs -//! - **Security Config Dashboard**: Authentication, encryption, access controls -//! - **Performance Config Dashboard**: System tuning, cache settings, optimization parameters -//! -//! ## Features: -//! - Real-time configuration viewing and editing -//! - `PostgreSQL` NOTIFY/LISTEN hot-reload integration -//! - Configuration history and audit trail -//! - Vault secret management integration -//! - Type-safe configuration validation -//! - Multi-environment support (dev, staging, production) - -use crate::dashboard::events::ConfigUpdateRequest; -use crate::dashboard::events::DashboardEvent; -use crate::dashboard::Dashboard; -use anyhow::Result; -// ARCHITECTURAL VIOLATION FIXED: TLI should NOT directly access ConfigManager -// TLI is pure client - all config access must be via gRPC ConfigurationService -// use config::{ConfigCategory, ConfigManager, ConfigValue, ConfigChange, ConfigHealth}; // REMOVED: Database access violation - -// Use gRPC proto types instead -use crate::proto::config::{ - configuration_service_client::ConfigurationServiceClient, ConfigChangeResponse, ConfigRequest, - ConfigResponse, Empty, -}; -use tonic::transport::Channel; - -// Define ConfigCategory enum for TLI - Pure client types (no database dependencies) -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum ConfigCategory { - Trading, - Risk, - MachineLearning, - Security, - Performance, -} - -// Define ConfigValue struct for TLI - Pure client types -#[derive(Debug, Clone)] -pub struct ConfigValue { - pub key: String, - pub value: serde_json::Value, - pub category: ConfigCategory, - pub sensitive: bool, -} -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use ratatui::{ - prelude::*, - widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Tabs, Wrap}, -}; -use serde_json::Value as JsonValue; -use std::collections::HashMap; -use tokio::sync::mpsc; -use tracing::{error, info, warn}; - -/// Main configuration management dashboard coordinator -pub struct ConfigManagerDashboard { - /// Event sender for dashboard communication - _event_sender: mpsc::Sender, - /// gRPC Configuration Service Client - PURE CLIENT ARCHITECTURE - config_client: Option>, - /// Current active category dashboard - active_category: ConfigCategory, - /// Specialized category dashboards - category_dashboards: HashMap>, - /// UI state - ui_state: ConfigManagerState, - /// Connection status - connection_status: ConnectionStatus, - /// Configuration change stream - #[allow(dead_code)] - change_receiver: Option>, - /// Recent configuration changes for audit trail - recent_changes: Vec, - /// Needs redraw flag - needs_redraw: bool, -} - -/// UI state for configuration management -#[derive(Debug, Clone)] -struct ConfigManagerState { - /// Current environment filter - environment: String, - /// Category tab selection - tab_state: usize, - /// Whether to show sensitive values - #[allow(dead_code)] - show_sensitive: bool, - /// Search mode state - search_active: bool, - /// Search query - search_query: String, - /// Error popup state - error_popup: Option, - /// Success popup state - success_popup: Option, -} - -/// Connection status -#[derive(Debug, Clone)] -enum ConnectionStatus { - Disconnected, - Connecting, - Connected { postgres: bool, vault: bool }, - Error(String), -} - -/// Trait for category-specific configuration dashboards - PURE CLIENT ARCHITECTURE -pub trait CategoryConfigDashboard: Send + Sync { - /// Render the category-specific dashboard - fn render(&mut self, frame: &mut Frame, area: Rect) -> Result<()>; - - /// Handle input specific to this category - returns config update requests - fn handle_input(&mut self, key: KeyEvent) -> Result>; - - /// Update with new configuration data - fn update(&mut self, configs: Vec) -> Result<()>; - - /// Get category name for display - fn category_name(&self) -> &str; - - /// Get category for configuration operations - fn category(&self) -> ConfigCategory; - - /// Validate configuration value before saving - fn validate_config(&self, key: &str, value: &JsonValue) -> Result>; -} - -impl Default for ConfigManagerState { - fn default() -> Self { - Self { - environment: std::env::var("FOXHUNT_ENV").unwrap_or_else(|_| "production".to_owned()), - tab_state: 0, - show_sensitive: false, - search_active: false, - search_query: String::new(), - error_popup: None, - success_popup: None, - } - } -} - -impl ConfigManagerDashboard { - /// Create new configuration manager dashboard - pub fn new(_event_sender: mpsc::Sender) -> Self { - let mut category_dashboards: HashMap> = - HashMap::new(); - - // Initialize specialized category dashboards - category_dashboards.insert( - ConfigCategory::Trading, - Box::new(TradingConfigDashboard::new()), - ); - category_dashboards.insert(ConfigCategory::Risk, Box::new(RiskConfigDashboard::new())); - category_dashboards.insert( - ConfigCategory::MachineLearning, - Box::new(MLConfigDashboard::new()), - ); - category_dashboards.insert( - ConfigCategory::Security, - Box::new(SecurityConfigDashboard::new()), - ); - category_dashboards.insert( - ConfigCategory::Performance, - Box::new(PerformanceConfigDashboard::new()), - ); - - Self { - _event_sender, - config_client: None, // gRPC client instead of ConfigManager - active_category: ConfigCategory::Trading, - category_dashboards, - ui_state: ConfigManagerState::default(), - connection_status: ConnectionStatus::Disconnected, - change_receiver: None, - recent_changes: Vec::new(), - needs_redraw: true, - } - } - - /// Initialize connection to gRPC Configuration Service - PURE CLIENT ARCHITECTURE - pub async fn initialize(&mut self, service_url: &str) -> Result<()> { - self.connection_status = ConnectionStatus::Connecting; - self.needs_redraw = true; - - info!( - "Initializing gRPC ConfigurationService connection to {}", - service_url - ); - - match ConfigurationServiceClient::connect(service_url.to_owned()).await { - Ok(client) => { - info!("Connected to ConfigurationService successfully"); - - // Test connection with a simple request - let mut test_client = client.clone(); - match test_client.list_categories(Empty {}).await { - Ok(_) => { - // Connection successful - self.config_client = Some(client); - self.connection_status = ConnectionStatus::Connected { - postgres: true, // Service handles database - vault: true, // Service handles vault - }; - - // Load initial configuration data for all categories - self.load_all_category_configs().await?; - - info!("ConfigurationService initialized successfully"); - }, - Err(e) => { - error!("ConfigurationService connection test failed: {}", e); - self.connection_status = - ConnectionStatus::Error(format!("Service unavailable: {}", e)); - }, - } - }, - Err(e) => { - error!("Failed to connect to ConfigurationService: {}", e); - self.connection_status = - ConnectionStatus::Error(format!("Connection failed: {}", e)); - }, - } - - self.needs_redraw = true; - Ok(()) - } - - /// Load configuration data for all categories via gRPC - async fn load_all_category_configs(&mut self) -> Result<()> { - if let Some(ref mut client) = self.config_client { - for category in [ - ConfigCategory::Trading, - ConfigCategory::Risk, - ConfigCategory::MachineLearning, - ConfigCategory::Security, - ConfigCategory::Performance, - ] { - // Convert our category to proto category string for gRPC request - let category_name = match category { - ConfigCategory::Trading => "trading", - ConfigCategory::Risk => "risk", - ConfigCategory::MachineLearning => "ml", - ConfigCategory::Security => "security", - ConfigCategory::Performance => "performance", - }; - - let request = ConfigRequest { - keys: vec![], // Empty means get all configs - category: Some(category_name.to_owned()), - environment: None, // Use default - include_sensitive: false, - }; - - match client.get_configuration(request).await { - Ok(response) => { - let configs = Self::convert_proto_to_config_values( - response.into_inner(), - category.clone(), - ); - if let Some(dashboard) = self.category_dashboards.get_mut(&category) { - dashboard.update(configs)?; - } - }, - Err(e) => { - warn!("Failed to load configs for category {:?}: {}", category, e); - }, - } - } - } - Ok(()) - } - - /// Convert proto `ConfigResponse` to our `ConfigValue` types - fn convert_proto_to_config_values( - response: ConfigResponse, - category: ConfigCategory, - ) -> Vec { - response - .settings - .into_iter() - .map(|setting| ConfigValue { - key: setting.key.clone(), - value: serde_json::from_str(&setting.value) - .unwrap_or(serde_json::Value::String(setting.value)), - category: category.clone(), - sensitive: setting.sensitive, - }) - .collect() - } - - /// Switch to a different category dashboard - fn switch_category(&mut self, category: ConfigCategory) { - self.active_category = category.clone(); - self.ui_state.tab_state = self.category_to_tab_index(category); - self.needs_redraw = true; - } - - /// Convert category to tab index - const fn category_to_tab_index(&self, category: ConfigCategory) -> usize { - match category { - ConfigCategory::Trading => 0, - ConfigCategory::Risk => 1, - ConfigCategory::MachineLearning => 2, - ConfigCategory::Security => 3, - ConfigCategory::Performance => 4, - } - } - - /// Convert tab index to category - const fn tab_index_to_category(&self, index: usize) -> ConfigCategory { - match index { - 0 => ConfigCategory::Trading, - 1 => ConfigCategory::Risk, - 2 => ConfigCategory::MachineLearning, - 3 => ConfigCategory::Security, - 4 => ConfigCategory::Performance, - _ => ConfigCategory::Trading, - } - } - - /// Show success popup - fn show_success(&mut self, message: String) { - self.ui_state.success_popup = Some(message); - self.needs_redraw = true; - } - - /// Show error popup - #[allow(dead_code)] - fn show_error(&mut self, message: String) { - self.ui_state.error_popup = Some(message); - self.needs_redraw = true; - } - - /// Close popups - fn close_popups(&mut self) { - self.ui_state.error_popup = None; - self.ui_state.success_popup = None; - self.needs_redraw = true; - } - - /// Process configuration changes from the stream - #[allow(dead_code)] - async fn process_config_changes(&mut self) -> Result<()> { - if let Some(ref mut change_rx) = self.change_receiver { - while let Ok(change) = change_rx.try_recv() { - if let Some(setting) = &change.setting { - info!( - "Configuration change received: {}.{}", - setting.category, setting.key - ); - - // Add to recent changes for audit trail - self.recent_changes.push(change.clone()); - if self.recent_changes.len() > 100 { - self.recent_changes.remove(0); - } - - // Determine category and reload - let category = match setting.category.as_str() { - "trading" => ConfigCategory::Trading, - "risk" => ConfigCategory::Risk, - "ml" => ConfigCategory::MachineLearning, - "security" => ConfigCategory::Security, - "performance" => ConfigCategory::Performance, - _ => ConfigCategory::Trading, // Default fallback - }; - - // Reload affected category via gRPC - if let Some(ref mut client) = self.config_client { - let request = ConfigRequest { - keys: vec![], - category: Some(setting.category.clone()), - environment: None, - include_sensitive: false, - }; - - match client.get_configuration(request).await { - Ok(response) => { - let configs = Self::convert_proto_to_config_values( - response.into_inner(), - category.clone(), - ); - if let Some(dashboard) = self.category_dashboards.get_mut(&category) - { - dashboard.update(configs)?; - } - }, - Err(e) => { - warn!( - "Failed to reload category {:?} after change: {}", - category, e - ); - }, - } - } - } - - self.needs_redraw = true; - } - } - Ok(()) - } -} - -impl Dashboard for ConfigManagerDashboard { - fn render(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - // Main layout: [Header][Tabs][Content][Footer] - let main_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), // Header - Constraint::Length(3), // Tabs - Constraint::Min(10), // Content - Constraint::Length(3), // Footer - ]) - .split(area); - - // Render header with connection status - self.render_header(frame, main_chunks[0])?; - - // Render category tabs - self.render_tabs(frame, main_chunks[1])?; - - // Render content based on connection status - match &self.connection_status { - ConnectionStatus::Connected { .. } => { - self.render_connected_content(frame, main_chunks[2])?; - }, - ConnectionStatus::Connecting => { - self.render_connecting_screen(frame, main_chunks[2])?; - }, - ConnectionStatus::Disconnected => { - self.render_disconnected_screen(frame, main_chunks[2])?; - }, - ConnectionStatus::Error(err) => { - self.render_error_screen(frame, main_chunks[2], err)?; - }, - } - - // Render footer - self.render_footer(frame, main_chunks[3])?; - - // Render popups if active - self.render_popups(frame, area)?; - - self.needs_redraw = false; - Ok(()) - } - - fn handle_input(&mut self, key: KeyEvent) -> Result> { - // Handle popups first - if self.ui_state.error_popup.is_some() || self.ui_state.success_popup.is_some() { - match key.code { - KeyCode::Enter | KeyCode::Esc => { - self.close_popups(); - return Ok(None); - }, - _ => return Ok(None), - } - } - - // Handle search mode - if self.ui_state.search_active { - return self.handle_search_input(key); - } - - // Global hotkeys - match key.code { - KeyCode::Tab => { - // Switch to next category - self.ui_state.tab_state = (self.ui_state.tab_state + 1) % 5; - self.active_category = self.tab_index_to_category(self.ui_state.tab_state); - self.needs_redraw = true; - Ok(None) - }, - KeyCode::Char('1') => { - self.switch_category(ConfigCategory::Trading); - Ok(None) - }, - KeyCode::Char('2') => { - self.switch_category(ConfigCategory::Risk); - Ok(None) - }, - KeyCode::Char('3') => { - self.switch_category(ConfigCategory::MachineLearning); - Ok(None) - }, - KeyCode::Char('4') => { - self.switch_category(ConfigCategory::Security); - Ok(None) - }, - KeyCode::Char('5') => { - self.switch_category(ConfigCategory::Performance); - Ok(None) - }, - KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::NONE) => { - self.ui_state.search_active = true; - self.ui_state.search_query.clear(); - self.needs_redraw = true; - Ok(None) - }, - KeyCode::F(5) => { - // Refresh all configurations via gRPC - if self.config_client.is_some() { - let _event_sender = self._event_sender.clone(); - tokio::spawn(async move { - let _ = _event_sender.send(DashboardEvent::RefreshConfig).await; - }); - } - Ok(None) - }, - KeyCode::Char('h') => { - // Show connection status - let status_msg = match &self.connection_status { - ConnectionStatus::Connected { postgres, vault } => { - format!("Connected - PostgreSQL: {}, Vault: {}", postgres, vault) - }, - status => format!("Status: {:?}", status), - }; - self.show_success(status_msg); - Ok(None) - }, - KeyCode::Esc | KeyCode::Char('q') => Ok(Some(DashboardEvent::Exit)), - _ => { - // Forward to active category dashboard - if let Some(dashboard) = self.category_dashboards.get_mut(&self.active_category) { - match dashboard.handle_input(key)? { - Some(config_update) => { - // Queue config update for async processing - let _event_sender = self._event_sender.clone(); - tokio::spawn(async move { - let _ = _event_sender - .send(DashboardEvent::ConfigUpdateRequest(config_update)) - .await; - }); - Ok(None) - }, - None => Ok(None), - } - } else { - Ok(None) - } - }, - } - } - - fn update(&mut self, event: DashboardEvent) -> Result<()> { - match event { - DashboardEvent::RefreshConfig => { - if self.config_client.is_some() { - let _event_sender = self._event_sender.clone(); - tokio::spawn(async move { - // Refresh configurations in background - // This would typically trigger a reload - let _ = _event_sender.send(DashboardEvent::ConfigReloaded).await; - }); - } - }, - DashboardEvent::ConfigChanged { category, key } => { - info!("Configuration changed: {}.{}", category, key); - // Mark for redraw - actual reload happens via gRPC streaming - self.needs_redraw = true; - }, - _ => {}, - } - self.needs_redraw = true; - Ok(()) - } - - fn title(&self) -> &str { - "Configuration Manager" - } - - fn shortcut_key(&self) -> char { - 'c' - } - - fn needs_redraw(&self) -> bool { - self.needs_redraw - } - - fn mark_drawn(&mut self) { - self.needs_redraw = false; - } -} - -// Implementation continues with render methods and specific category dashboards... -impl ConfigManagerDashboard { - fn render_header(&self, frame: &mut Frame, area: Rect) -> Result<()> { - let status_text = match &self.connection_status { - ConnectionStatus::Connected { postgres, vault } => { - format!( - "\u{1f7e2} Connected (PostgreSQL: {}, Vault: {})", - if *postgres { "\u{2713}" } else { "\u{2717}" }, - if *vault { "\u{2713}" } else { "\u{2717}" } - ) - }, - ConnectionStatus::Connecting => "\u{1f7e1} Connecting...".to_owned(), - ConnectionStatus::Disconnected => "\u{1f534} Disconnected".to_owned(), - ConnectionStatus::Error(_) => "\u{1f534} Error".to_owned(), - }; - - let header_text = format!( - "Configuration Manager | {} | Environment: {} | Changes: {}", - status_text, - self.ui_state.environment, - self.recent_changes.len() - ); - - let header = Paragraph::new(header_text) - .block( - Block::default() - .borders(Borders::ALL) - .title("Foxhunt Configuration Management"), - ) - .style(Style::default().fg(Color::White)) - .wrap(Wrap { trim: true }); - - frame.render_widget(header, area); - Ok(()) - } - - fn render_tabs(&self, frame: &mut Frame, area: Rect) -> Result<()> { - let tab_titles = vec![ - "[1] Trading", - "[2] Risk", - "[3] ML", - "[4] Security", - "[5] Performance", - ]; - - let tabs = Tabs::new(tab_titles) - .block(Block::default().borders(Borders::ALL)) - .style(Style::default().fg(Color::White)) - .highlight_style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ) - .select(self.ui_state.tab_state); - - frame.render_widget(tabs, area); - Ok(()) - } - - fn render_connected_content(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - if let Some(dashboard) = self.category_dashboards.get_mut(&self.active_category) { - dashboard.render(frame, area)? - } else { - let content = Paragraph::new("No configuration dashboard available") - .block(Block::default().borders(Borders::ALL).title("Content")) - .wrap(Wrap { trim: true }); - frame.render_widget(content, area); - } - Ok(()) - } - fn render_connecting_screen(&self, frame: &mut Frame, area: Rect) -> Result<()> { - let content = Paragraph::new("Connecting to configuration system...\n\nInitializing PostgreSQL and Vault connections...") - .block(Block::default().borders(Borders::ALL).title("Connecting")) - .style(Style::default().fg(Color::Yellow)) - .wrap(Wrap { trim: true }); - - frame.render_widget(content, area); - Ok(()) - } - - fn render_disconnected_screen(&self, frame: &mut Frame, area: Rect) -> Result<()> { - let content = Paragraph::new("Not connected to configuration system.\n\nPress 'i' to initialize connection or check your configuration.") - .block(Block::default().borders(Borders::ALL).title("Disconnected")) - .style(Style::default().fg(Color::Red)) - .wrap(Wrap { trim: true }); - - frame.render_widget(content, area); - Ok(()) - } - - fn render_error_screen(&self, frame: &mut Frame, area: Rect, error: &str) -> Result<()> { - let error_text = format!( - "Configuration Error:\n\n{}\n\nPress 'r' to retry connection or 'q' to quit.", - error - ); - - let content = Paragraph::new(error_text) - .block(Block::default().borders(Borders::ALL).title("Error")) - .style(Style::default().fg(Color::Red)) - .wrap(Wrap { trim: true }); - - frame.render_widget(content, area); - Ok(()) - } - - fn render_footer(&self, frame: &mut Frame, area: Rect) -> Result<()> { - let help_text = if self.ui_state.search_active { - "[Enter] Search | [Esc] Cancel" - } else { - "[1-5] Categories | [Tab] Next | [S] Search | [F5] Refresh | [H] Health | [Q] Quit" - }; - - let footer = Paragraph::new(help_text) - .block(Block::default().borders(Borders::ALL)) - .style(Style::default().fg(Color::Gray)) - .wrap(Wrap { trim: true }); - - frame.render_widget(footer, area); - Ok(()) - } - - fn render_popups(&self, frame: &mut Frame, area: Rect) -> Result<()> { - // Render error popup - if let Some(ref error_msg) = self.ui_state.error_popup { - let popup_area = Self::centered_rect(60, 20, area); - frame.render_widget(Clear, popup_area); - - let popup = Paragraph::new(error_msg.as_str()) - .block( - Block::default() - .borders(Borders::ALL) - .title("Error") - .border_style(Style::default().fg(Color::Red)), - ) - .style(Style::default().fg(Color::Red)) - .wrap(Wrap { trim: true }); - - frame.render_widget(popup, popup_area); - } - - // Render success popup - if let Some(ref success_msg) = self.ui_state.success_popup { - let popup_area = Self::centered_rect(60, 20, area); - frame.render_widget(Clear, popup_area); - - let popup = Paragraph::new(success_msg.as_str()) - .block( - Block::default() - .borders(Borders::ALL) - .title("Success") - .border_style(Style::default().fg(Color::Green)), - ) - .style(Style::default().fg(Color::Green)) - .wrap(Wrap { trim: true }); - - frame.render_widget(popup, popup_area); - } - - Ok(()) - } - - fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { - let popup_layout = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage((100 - percent_y) / 2), - Constraint::Percentage(percent_y), - Constraint::Percentage((100 - percent_y) / 2), - ]) - .split(r); - - Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage((100 - percent_x) / 2), - Constraint::Percentage(percent_x), - Constraint::Percentage((100 - percent_x) / 2), - ]) - .split(popup_layout[1])[1] - } - - fn handle_search_input(&mut self, key: KeyEvent) -> Result> { - match key.code { - KeyCode::Char(c) => { - self.ui_state.search_query.push(c); - self.needs_redraw = true; - Ok(None) - }, - KeyCode::Backspace => { - self.ui_state.search_query.pop(); - self.needs_redraw = true; - Ok(None) - }, - KeyCode::Enter => { - // Perform search (implementation would depend on search functionality) - self.ui_state.search_active = false; - self.needs_redraw = true; - Ok(None) - }, - KeyCode::Esc => { - self.ui_state.search_active = false; - self.ui_state.search_query.clear(); - self.needs_redraw = true; - Ok(None) - }, - _ => Ok(None), - } - } -} - -// Trading Configuration Dashboard -pub struct TradingConfigDashboard { - configs: Vec, - list_state: ListState, - #[allow(dead_code)] - editing_key: Option, - #[allow(dead_code)] - edit_buffer: String, - needs_redraw: bool, -} - -impl TradingConfigDashboard { - fn new() -> Self { - Self { - configs: Vec::new(), - list_state: ListState::default(), - editing_key: None, - edit_buffer: String::new(), - needs_redraw: true, - } - } -} - -impl CategoryConfigDashboard for TradingConfigDashboard { - fn render(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(area); - - // Render configuration list - let items: Vec = self - .configs - .iter() - .map(|config| { - let display_value = if config.key.to_lowercase().contains("password") - || config.key.to_lowercase().contains("secret") - { - "********".to_owned() - } else { - config.value.to_string().chars().take(50).collect() - }; - - let text = format!("{}: {}", config.key, display_value); - ListItem::new(text) - }) - .collect(); - - let list = List::new(items) - .block( - Block::default() - .borders(Borders::ALL) - .title("Trading Configuration"), - ) - .highlight_style(Style::default().bg(Color::DarkGray)) - .highlight_symbol(">> "); - - frame.render_stateful_widget(list, chunks[0], &mut self.list_state); - - // Render editor panel - let editor_title = if self.editing_key.is_some() { - "Editor (Active)" - } else { - "Editor" - }; - - let editor_content = if self.editing_key.is_some() { - &self.edit_buffer - } else if let Some(selected) = self.list_state.selected() { - if let Some(config) = self.configs.get(selected) { - &config.value.to_string() - } else { - "No configuration selected" - } - } else { - "No configuration selected" - }; - - let editor = Paragraph::new(editor_content) - .block(Block::default().borders(Borders::ALL).title(editor_title)) - .wrap(Wrap { trim: true }); - - frame.render_widget(editor, chunks[1]); - - self.needs_redraw = false; - Ok(()) - } - - fn handle_input(&mut self, key: KeyEvent) -> Result> { - if self.editing_key.is_some() { - // Handle editing mode - match key.code { - KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => { - self.edit_buffer.push(c); - self.needs_redraw = true; - Ok(None) - }, - KeyCode::Backspace => { - self.edit_buffer.pop(); - self.needs_redraw = true; - Ok(None) - }, - KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Save configuration - return update request for gRPC processing - if let Some(key) = &self.editing_key { - let value: JsonValue = serde_json::from_str(&self.edit_buffer) - .unwrap_or_else(|_| JsonValue::String(self.edit_buffer.clone())); - - let update_request = ConfigUpdateRequest { - category: "trading".to_owned(), - key: key.clone(), - value, - reason: "Updated via TLI".to_owned(), - }; - - self.editing_key = None; - self.edit_buffer.clear(); - self.needs_redraw = true; - - return Ok(Some(update_request)); - } - Ok(None) - }, - KeyCode::Esc => { - self.editing_key = None; - self.edit_buffer.clear(); - self.needs_redraw = true; - Ok(None) - }, - _ => Ok(None), - } - } else { - // Handle navigation mode - match key.code { - KeyCode::Up => { - let current = self.list_state.selected().unwrap_or(0); - if current > 0 { - self.list_state.select(Some(current - 1)); - self.needs_redraw = true; - } - Ok(None) - }, - KeyCode::Down => { - let current = self.list_state.selected().unwrap_or(0); - if current < self.configs.len().saturating_sub(1) { - self.list_state.select(Some(current + 1)); - self.needs_redraw = true; - } - Ok(None) - }, - KeyCode::Enter | KeyCode::Char('e') => { - if let Some(selected) = self.list_state.selected() { - if let Some(config) = self.configs.get(selected) { - self.editing_key = Some(config.key.clone()); - self.edit_buffer = config.value.to_string(); - self.needs_redraw = true; - } - } - Ok(None) - }, - _ => Ok(None), - } - } - } - - fn update(&mut self, configs: Vec) -> Result<()> { - self.configs = configs; - self.needs_redraw = true; - Ok(()) - } - - fn category_name(&self) -> &str { - "Trading" - } - - fn category(&self) -> ConfigCategory { - ConfigCategory::Trading - } - - fn validate_config(&self, key: &str, value: &JsonValue) -> Result> { - let mut errors = Vec::new(); - - // Trading-specific validation - match key { - "max_order_size" => { - if let Some(size) = value.as_f64() { - if size <= 0.0 { - errors.push("Max order size must be positive".to_owned()); - } - if size > 10000000.0 { - errors.push("Max order size too large (>10M)".to_owned()); - } - } - }, - "max_position_size" => { - if let Some(size) = value.as_f64() { - if size <= 0.0 { - errors.push("Max position size must be positive".to_owned()); - } - } - }, - "tick_size" => { - if let Some(tick) = value.as_f64() { - if tick <= 0.0 { - errors.push("Tick size must be positive".to_owned()); - } - } - }, - _ => {}, - } - - Ok(errors) - } -} - -// Risk Configuration Dashboard -pub struct RiskConfigDashboard { - configs: Vec, - list_state: ListState, - #[allow(dead_code)] - editing_key: Option, - #[allow(dead_code)] - edit_buffer: String, - needs_redraw: bool, -} - -impl RiskConfigDashboard { - fn new() -> Self { - Self { - configs: Vec::new(), - list_state: ListState::default(), - editing_key: None, - edit_buffer: String::new(), - needs_redraw: true, - } - } -} - -impl CategoryConfigDashboard for RiskConfigDashboard { - fn render(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(60), Constraint::Percentage(40)]) - .split(area); - - // Render risk configuration list - let items: Vec = self - .configs - .iter() - .map(|config| { - let text = format!("{}: {}", config.key, config.value); - ListItem::new(text).style(Style::default().fg(Color::Red)) - }) - .collect(); - - let list = List::new(items) - .block( - Block::default() - .borders(Borders::ALL) - .title("Risk Configuration") - .border_style(Style::default().fg(Color::Red)), - ) - .highlight_style(Style::default().bg(Color::DarkGray)) - .highlight_symbol(">> "); - - frame.render_stateful_widget(list, chunks[0], &mut self.list_state); - - // Render risk summary - let risk_summary = self.generate_risk_summary(); - let summary = Paragraph::new(risk_summary) - .block(Block::default().borders(Borders::ALL).title("Risk Summary")) - .wrap(Wrap { trim: true }); - - frame.render_widget(summary, chunks[1]); - - self.needs_redraw = false; - Ok(()) - } - - fn handle_input(&mut self, key: KeyEvent) -> Result> { - match key.code { - KeyCode::Up => { - let current = self.list_state.selected().unwrap_or(0); - if current > 0 { - self.list_state.select(Some(current - 1)); - self.needs_redraw = true; - } - Ok(None) - }, - KeyCode::Down => { - let current = self.list_state.selected().unwrap_or(0); - if current < self.configs.len().saturating_sub(1) { - self.list_state.select(Some(current + 1)); - self.needs_redraw = true; - } - Ok(None) - }, - _ => Ok(None), - } - } - - fn update(&mut self, configs: Vec) -> Result<()> { - self.configs = configs; - self.needs_redraw = true; - Ok(()) - } - - fn category_name(&self) -> &str { - "Risk Management" - } - - fn category(&self) -> ConfigCategory { - ConfigCategory::Risk - } - - fn validate_config(&self, key: &str, value: &JsonValue) -> Result> { - let mut errors = Vec::new(); - - // Risk-specific validation - match key { - "var_limit" => { - if let Some(limit) = value.as_f64() { - if limit <= 0.0 { - errors.push("VaR limit must be positive".to_owned()); - } - if limit > 1.0 { - errors.push("VaR limit should typically be < 1.0".to_owned()); - } - } - }, - "max_drawdown" => { - if let Some(dd) = value.as_f64() { - if dd <= 0.0 || dd >= 1.0 { - errors.push("Max drawdown must be between 0 and 1".to_owned()); - } - } - }, - _ => {}, - } - - Ok(errors) - } -} - -impl RiskConfigDashboard { - fn generate_risk_summary(&self) -> String { - let mut summary = String::new(); - - // Extract key risk metrics - for config in &self.configs { - match config.key.as_str() { - "var_limit" => summary.push_str(&format!("VaR Limit: {}\n", config.value)), - "max_drawdown" => summary.push_str(&format!("Max Drawdown: {}\n", config.value)), - "position_limit" => { - summary.push_str(&format!("Position Limit: {}\n", config.value)) - }, - _ => {}, - } - } - - if summary.is_empty() { - summary = "No risk configurations found".to_owned(); - } - - summary - } -} - -// ML Configuration Dashboard (simplified implementation) -pub struct MLConfigDashboard { - configs: Vec, - #[allow(dead_code)] - list_state: ListState, - needs_redraw: bool, -} - -impl MLConfigDashboard { - fn new() -> Self { - Self { - configs: Vec::new(), - list_state: ListState::default(), - needs_redraw: true, - } - } -} - -impl CategoryConfigDashboard for MLConfigDashboard { - fn render(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let content = Paragraph::new("ML Configuration Dashboard\n\nThis dashboard manages machine learning model parameters,\ninference settings, and training configurations.") - .block( - Block::default() - .borders(Borders::ALL) - .title("ML Configuration") - .border_style(Style::default().fg(Color::Blue)) - ) - .wrap(Wrap { trim: true }); - - frame.render_widget(content, area); - self.needs_redraw = false; - Ok(()) - } - - fn handle_input(&mut self, _key: KeyEvent) -> Result> { - Ok(None) - } - - fn update(&mut self, configs: Vec) -> Result<()> { - self.configs = configs; - self.needs_redraw = true; - Ok(()) - } - - fn category_name(&self) -> &str { - "Machine Learning" - } - - fn category(&self) -> ConfigCategory { - ConfigCategory::MachineLearning - } - - fn validate_config(&self, _key: &str, _value: &JsonValue) -> Result> { - Ok(Vec::new()) - } -} - -// Security Configuration Dashboard (simplified implementation) -pub struct SecurityConfigDashboard { - configs: Vec, - #[allow(dead_code)] - list_state: ListState, - needs_redraw: bool, -} - -impl SecurityConfigDashboard { - fn new() -> Self { - Self { - configs: Vec::new(), - list_state: ListState::default(), - needs_redraw: true, - } - } -} - -impl CategoryConfigDashboard for SecurityConfigDashboard { - fn render(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let content = Paragraph::new("Security Configuration Dashboard\n\n\u{26a0}\u{fe0f} SENSITIVE CONFIGURATION AREA \u{26a0}\u{fe0f}\n\nThis dashboard manages authentication settings,\nencryption parameters, and access controls.") - .block( - Block::default() - .borders(Borders::ALL) - .title("Security Configuration") - .border_style(Style::default().fg(Color::Magenta)) - ) - .style(Style::default().fg(Color::Yellow)) - .wrap(Wrap { trim: true }); - - frame.render_widget(content, area); - self.needs_redraw = false; - Ok(()) - } - - fn handle_input(&mut self, _key: KeyEvent) -> Result> { - Ok(None) - } - - fn update(&mut self, configs: Vec) -> Result<()> { - self.configs = configs; - self.needs_redraw = true; - Ok(()) - } - - fn category_name(&self) -> &str { - "Security" - } - - fn category(&self) -> ConfigCategory { - ConfigCategory::Security - } - - fn validate_config(&self, _key: &str, _value: &JsonValue) -> Result> { - Ok(Vec::new()) - } -} - -// Performance Configuration Dashboard (simplified implementation) -pub struct PerformanceConfigDashboard { - configs: Vec, - #[allow(dead_code)] - list_state: ListState, - needs_redraw: bool, -} - -impl PerformanceConfigDashboard { - fn new() -> Self { - Self { - configs: Vec::new(), - list_state: ListState::default(), - needs_redraw: true, - } - } -} - -impl CategoryConfigDashboard for PerformanceConfigDashboard { - fn render(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let content = Paragraph::new("Performance Configuration Dashboard\n\nThis dashboard manages system tuning parameters,\ncache settings, and optimization configurations.") - .block( - Block::default() - .borders(Borders::ALL) - .title("Performance Configuration") - .border_style(Style::default().fg(Color::Green)) - ) - .wrap(Wrap { trim: true }); - - frame.render_widget(content, area); - self.needs_redraw = false; - Ok(()) - } - - fn handle_input(&mut self, _key: KeyEvent) -> Result> { - Ok(None) - } - - fn update(&mut self, configs: Vec) -> Result<()> { - self.configs = configs; - self.needs_redraw = true; - Ok(()) - } - - fn category_name(&self) -> &str { - "Performance" - } - - fn category(&self) -> ConfigCategory { - ConfigCategory::Performance - } - - fn validate_config(&self, _key: &str, _value: &JsonValue) -> Result> { - Ok(Vec::new()) - } -} - -// Additional dashboard event types for configuration management -#[derive(Debug, Clone)] -pub enum ConfigDashboardEvent { - ConfigChanged { - category: ConfigCategory, - key: String, - }, - RefreshConfig, - ConfigReloaded, - ValidationError { - key: String, - errors: Vec, - }, - ConfigSaved { - category: ConfigCategory, - key: String, - }, -} diff --git a/tli/src/dashboards/configuration.rs b/tli/src/dashboards/configuration.rs deleted file mode 100644 index 3dedfedcb..000000000 --- a/tli/src/dashboards/configuration.rs +++ /dev/null @@ -1,1537 +0,0 @@ -//! Configuration Dashboard Implementation -//! -//! This module provides a comprehensive terminal-based configuration management dashboard -//! for the TLI client. It displays configuration categories in a tree view, allows editing -//! of settings with real-time validation, and maintains change history with rollback capabilities. - -use crate::proto::config::{ - configuration_service_client::ConfigurationServiceClient, ConfigCategory, ConfigHistoryEntry, - ConfigSetting, UpdateConfigRequest as ConfigUpdateRequest, -}; -use tonic::transport::Channel; - -// Type alias for backwards compatibility -pub type ConfigHistory = ConfigHistoryEntry; - -#[derive(Debug, Clone)] -pub struct ValidationResult { - pub is_valid: bool, - pub valid: bool, // Alias for backwards compatibility - pub errors: Vec, - pub warnings: Vec, -} - -impl ValidationResult { - pub const fn new(is_valid: bool, errors: Vec, warnings: Vec) -> Self { - Self { - is_valid, - valid: is_valid, - errors, - warnings, - } - } -} -use crate::dashboard::events::DashboardEvent; -use crate::dashboard::Dashboard; -use anyhow::Result; -use chrono; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use ratatui::{ - prelude::*, - widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap}, -}; -use std::collections::HashMap; -use tokio::sync::mpsc; - -/// Configuration dashboard state -pub struct ConfigurationDashboard { - /// Event sender for dashboard communication - _event_sender: mpsc::Sender, - /// Configuration client for database operations - config_client: Option>, - /// Configuration tree data - config_tree: Vec, - /// Current UI state - ui_state: ConfigUiState, - /// Currently selected category/setting - selection: ConfigSelection, - /// Edit mode state - edit_state: EditState, - /// Validation results cache - validation_cache: HashMap, - /// History data for current setting - current_history: Vec, - /// Connection status - connection_status: ConnectionStatus, - /// Search state - search_state: SearchState, - /// Needs redraw flag - needs_redraw: bool, -} - -/// UI state for the configuration dashboard -#[derive(Debug, Clone)] -struct ConfigUiState { - /// Currently focused panel - focused_panel: Panel, - /// Category tree navigation state - category_list_state: ListState, - /// Settings list navigation state - settings_list_state: ListState, - /// History list navigation state - history_list_state: ListState, - /// Current environment filter - environment: String, - /// Show sensitive values (masked by default) - show_sensitive: bool, - /// Expanded categories in tree view - expanded_categories: std::collections::HashSet, -} - -/// Panel focus enumeration -#[derive(Debug, Clone, PartialEq)] -enum Panel { - CategoryTree, - SettingsList, - Editor, - History, - Validation, -} - -/// Configuration selection state -#[derive(Debug, Clone, Default)] -struct ConfigSelection { - /// Selected category ID - category_id: Option, - /// Selected setting key - setting_key: Option, - /// Flattened category tree for navigation - flat_categories: Vec, - /// Flattened settings list for current category - flat_settings: Vec, -} - -/// Flattened category for tree navigation -#[derive(Debug, Clone)] -struct FlatCategory { - id: i32, - name: String, - level: usize, - is_expanded: bool, - setting_count: usize, -} - -/// Edit mode state -#[derive(Debug, Clone, Default)] -struct EditState { - /// Whether we're currently editing - is_editing: bool, - /// Current edit buffer - edit_buffer: String, - /// Original value (for cancel) - original_value: String, - /// Cursor position in edit buffer - cursor_position: usize, - /// Whether changes are pending save - has_changes: bool, -} - -/// Connection status to `PostgreSQL` -#[derive(Debug, Clone)] -enum ConnectionStatus { - Disconnected, - Connecting, - Connected, - Error(String), -} - -/// Search functionality state -#[derive(Debug, Clone, Default)] -struct SearchState { - /// Whether search mode is active - is_searching: bool, - /// Search query buffer - query: String, - /// Search results - results: Vec, - /// Selected result index - selected_result: usize, -} - -impl Default for ConfigUiState { - fn default() -> Self { - Self { - focused_panel: Panel::CategoryTree, - category_list_state: ListState::default(), - settings_list_state: ListState::default(), - history_list_state: ListState::default(), - environment: "production".to_owned(), - show_sensitive: false, - expanded_categories: std::collections::HashSet::new(), - } - } -} - -impl ConfigurationDashboard { - pub fn new(_event_sender: mpsc::Sender) -> Self { - Self { - _event_sender, - config_client: None, - config_tree: Vec::new(), - ui_state: ConfigUiState::default(), - selection: ConfigSelection::default(), - edit_state: EditState::default(), - validation_cache: HashMap::new(), - current_history: Vec::new(), - connection_status: ConnectionStatus::Disconnected, - search_state: SearchState::default(), - needs_redraw: true, - } - } - - /// Initialize connection to `PostgreSQL` - pub async fn initialize_connection(&mut self, database_url: &str) -> Result<()> { - self.connection_status = ConnectionStatus::Connecting; - self.needs_redraw = true; - - // Create a tonic channel and client for gRPC service - match Channel::from_shared(database_url.to_owned()) { - Ok(channel_builder) => match channel_builder.connect().await { - Ok(channel) => { - let mut client = ConfigurationServiceClient::new(channel); - // Load initial configuration tree using ListCategories - let empty_request = crate::proto::config::Empty {}; - match client.list_categories(empty_request).await { - Ok(response) => { - self.config_tree = response.into_inner().categories; - self.flatten_categories(); - self.config_client = Some(client); - self.connection_status = ConnectionStatus::Connected; - self.needs_redraw = true; - Ok(()) - }, - Err(e) => { - self.connection_status = ConnectionStatus::Error(format!( - "Failed to load config tree: {}", - e - )); - self.needs_redraw = true; - Err(e.into()) - }, - } - }, - Err(e) => { - self.connection_status = - ConnectionStatus::Error(format!("Connection failed: {}", e)); - self.needs_redraw = true; - Err(e.into()) - }, - }, - Err(e) => { - self.connection_status = - ConnectionStatus::Error(format!("Failed to connect: {}", e)); - self.needs_redraw = true; - Err(e.into()) - }, - } - } - - /// Flatten the category tree for navigation - fn flatten_categories(&mut self) { - let expanded_categories = self.ui_state.expanded_categories.clone(); - self.selection.flat_categories.clear(); - for category in &self.config_tree { - Self::flatten_category_recursive_helper( - category, - 0_usize, - &mut self.selection.flat_categories, - &expanded_categories, - ); - } - } - - /// Helper method to flatten categories recursively - fn flatten_category_recursive_helper( - category: &ConfigCategory, - level: usize, - flat_categories: &mut Vec, - expanded_categories: &std::collections::HashSet, - ) { - let is_expanded = expanded_categories.contains(&(category.id as i32)); - - flat_categories.push(FlatCategory { - id: category.id as i32, - name: category.name.clone(), - level, - is_expanded, - setting_count: category.setting_count as usize, - }); - - if is_expanded { - for child in &category.children { - Self::flatten_category_recursive_helper( - child, - level + 1_usize, - flat_categories, - expanded_categories, - ); - } - } - } - - /// Load settings for selected category - fn load_category_settings(&mut self, category_id: i32) { - // Find the category and extract its settings - if let Some(category) = self.find_category_by_id(category_id) { - let category_name = category.name.clone(); - self.selection.category_id = Some(category_id); - - // Spawn async task to load settings via gRPC - if let Some(client) = self.config_client.clone() { - let event_sender = self._event_sender.clone(); - tokio::spawn(async move { - let mut client = client; - let config_request = crate::proto::config::ConfigRequest { - keys: vec![], - category: Some(category_name), - environment: None, - include_sensitive: false, - }; - - match client.get_configuration(config_request).await { - Ok(response) => { - let settings = response.into_inner().settings; - let _ = event_sender - .send(DashboardEvent::ConfigUpdate { - category_id, - settings, - }) - .await; - }, - Err(e) => { - tracing::error!("Failed to load category settings: {}", e); - }, - } - }); - } - - self.ui_state.settings_list_state.select(Some(0)); - } - } - - /// Find category by ID in the tree - fn find_category_by_id(&self, id: i32) -> Option<&ConfigCategory> { - for category in &self.config_tree { - if let Some(found) = self.find_category_recursive(category, id) { - return Some(found); - } - } - None - } - - /// Recursively search for category - fn find_category_recursive<'a>( - &self, - category: &'a ConfigCategory, - id: i32, - ) -> Option<&'a ConfigCategory> { - if category.id as i32 == id { - return Some(category); - } - for child in &category.children { - if let Some(found) = self.find_category_recursive(child, id) { - return Some(found); - } - } - None - } - - /// Start editing the currently selected setting - fn start_editing(&mut self) { - if let Some(setting_index) = self.ui_state.settings_list_state.selected() { - if let Some(setting) = self.selection.flat_settings.get(setting_index) { - self.edit_state.is_editing = true; - self.edit_state.edit_buffer = if setting.sensitive && !self.ui_state.show_sensitive - { - "\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}".to_owned() - } else { - setting.value.clone() - }; - self.edit_state.original_value = setting.value.clone(); - self.edit_state.cursor_position = self.edit_state.edit_buffer.len(); - self.edit_state.has_changes = false; - self.ui_state.focused_panel = Panel::Editor; - self.selection.setting_key = Some(setting.key.clone()); - self.needs_redraw = true; - } - } - } - - /// Cancel editing and revert changes - fn cancel_editing(&mut self) { - self.edit_state.is_editing = false; - self.edit_state.edit_buffer.clear(); - self.edit_state.cursor_position = 0; - self.edit_state.has_changes = false; - self.ui_state.focused_panel = Panel::SettingsList; - self.needs_redraw = true; - } - - /// Save the current edit - #[allow(dead_code)] - async fn save_edit(&mut self) -> Result<()> { - if let (Some(setting_key), Some(client)) = - (&self.selection.setting_key, &mut self.config_client) - { - let setting_key_clone = setting_key.clone(); - let config_update = crate::proto::config::ConfigUpdate { - key: setting_key_clone.clone(), - value: self.edit_state.edit_buffer.clone(), - category: None, // Optional field - }; - - let request = ConfigUpdateRequest { - updates: vec![config_update], - changed_by: std::env::var("USER") - .or_else(|_| std::env::var("USERNAME")) - .unwrap_or_else(|_| "tli_user".to_owned()), - reason: "Manual edit via TLI Configuration Dashboard".to_owned(), - validate_before_update: true, - }; - - match client.update_configuration(request).await { - Ok(response) => { - let update_response = response.into_inner(); - if update_response.success { - // Update was successful - self.edit_state.is_editing = false; - self.edit_state.has_changes = false; - self.ui_state.focused_panel = Panel::SettingsList; - - // Refresh the configuration data - self.refresh_data().await?; - - // Load history for this setting - self.load_setting_history(&setting_key_clone).await?; - - self.needs_redraw = true; - Ok(()) - } else { - // Validation failed - store result for display - let validation_result = ValidationResult { - is_valid: false, - valid: false, - errors: update_response - .validation_errors - .into_iter() - .map(|e| e.message) - .collect(), - warnings: vec![], - }; - self.validation_cache - .insert(setting_key_clone.clone(), validation_result); - self.ui_state.focused_panel = Panel::Validation; - self.needs_redraw = true; - Ok(()) - } - }, - Err(e) => { - // Create error validation result - let error_result = ValidationResult { - is_valid: false, - valid: false, - errors: vec![format!("Save failed: {}", e)], - warnings: Vec::new(), - }; - self.validation_cache - .insert(setting_key_clone, error_result); - self.ui_state.focused_panel = Panel::Validation; - self.needs_redraw = true; - Err(e.into()) - }, - } - } else { - Ok(()) - } - } - - /// Validate current edit without saving - #[allow(dead_code)] - async fn validate_current_edit(&mut self) -> Result<()> { - if let (Some(setting_key), Some(mut client)) = - (&self.selection.setting_key, self.config_client.clone()) - { - let validate_request = crate::proto::config::ValidateRequest { - validations: vec![crate::proto::config::ConfigValidation { - key: setting_key.to_string(), - value: self.edit_state.edit_buffer.clone(), - category: None, - }], - check_dependencies: false, - }; - match client.validate_configuration(validate_request).await { - Ok(response) => { - let validation_response = response.into_inner(); - let validation_result = ValidationResult { - is_valid: validation_response.valid, - valid: validation_response.valid, - errors: validation_response - .errors - .into_iter() - .map(|e| e.message) - .collect(), - warnings: validation_response - .warnings - .into_iter() - .map(|w| w.message) - .collect(), - }; - self.validation_cache - .insert(setting_key.clone(), validation_result); - self.ui_state.focused_panel = Panel::Validation; - self.needs_redraw = true; - Ok(()) - }, - Err(e) => { - let error_result = ValidationResult { - is_valid: false, - valid: false, - errors: vec![format!("Validation failed: {}", e)], - warnings: Vec::new(), - }; - self.validation_cache - .insert(setting_key.clone(), error_result); - self.ui_state.focused_panel = Panel::Validation; - self.needs_redraw = true; - Err(anyhow::Error::from(e)) - }, - } - } else { - Ok(()) - } - } - - /// Load history for the current setting - #[allow(dead_code)] - async fn load_setting_history(&mut self, setting_key: &str) -> Result<()> { - if let Some(client) = &mut self.config_client { - let history_request = crate::proto::config::HistoryRequest { - key: Some(setting_key.to_owned()), - start_time_unix_nanos: None, - end_time_unix_nanos: None, - changed_by: None, - limit: 20_u32, - offset: 0_u32, - }; - match client.get_configuration_history(history_request).await { - Ok(response) => { - let history_response = response.into_inner(); - self.current_history = history_response.entries; - self.needs_redraw = true; - Ok(()) - }, - Err(e) => { - self.current_history.clear(); - Err(anyhow::anyhow!("Failed to load setting history: {}", e)) - }, - } - } else { - Ok(()) - } - } - - /// Refresh configuration data from database - #[allow(dead_code)] - async fn refresh_data(&mut self) -> Result<()> { - if let Some(client) = &mut self.config_client.clone() { - let config_request = crate::proto::config::ConfigRequest { - keys: vec![], - category: None, - environment: None, - include_sensitive: false, - }; - match client.get_configuration(config_request).await { - Ok(response) => { - let config_response = response.into_inner(); - self.selection.flat_settings = config_response.settings; - self.flatten_categories(); - - // Reload current category settings if selected - if let Some(category_id) = self.selection.category_id { - self.load_category_settings(category_id); - } - - self.needs_redraw = true; - Ok(()) - }, - Err(e) => { - self.needs_redraw = true; - Err(anyhow::anyhow!( - "Failed to refresh configuration data: {}", - e - )) - }, - } - } else { - Ok(()) - } - } - - /// Toggle category expansion - fn toggle_category_expansion(&mut self, category_id: i32) { - if self.ui_state.expanded_categories.contains(&category_id) { - self.ui_state.expanded_categories.remove(&category_id); - } else { - self.ui_state.expanded_categories.insert(category_id); - } - self.flatten_categories(); - self.needs_redraw = true; - } - - /// Start search mode - fn start_search(&mut self) { - self.search_state.is_searching = true; - self.search_state.query.clear(); - self.search_state.results.clear(); - self.search_state.selected_result = 0; - self.needs_redraw = true; - } - - /// Perform search - #[allow(dead_code)] - async fn perform_search(&mut self) -> Result<()> { - if let Some(client) = &mut self.config_client { - if !self.search_state.query.trim().is_empty() { - let search_request = crate::proto::config::ConfigRequest { - keys: vec![self.search_state.query.clone()], - category: None, - environment: None, - include_sensitive: false, - }; - match client.get_configuration(search_request).await { - Ok(response) => { - let search_response = response.into_inner(); - self.search_state.results = search_response.settings; - self.search_state.selected_result = 0; - self.needs_redraw = true; - Ok(()) - }, - Err(e) => { - self.search_state.results.clear(); - Err(anyhow::anyhow!("Failed to perform search: {}", e)) - }, - } - } else { - self.search_state.results.clear(); - self.needs_redraw = true; - Ok(()) - } - } else { - Ok(()) - } - } - - /// Reset setting to default value - #[allow(dead_code)] - async fn reset_to_default(&mut self) -> Result<()> { - if let (Some(setting_key), Some(mut client)) = - (&self.selection.setting_key, self.config_client.clone()) - { - let setting_key_clone = setting_key.clone(); - let update_request = crate::proto::config::UpdateConfigRequest { - updates: vec![crate::proto::config::ConfigUpdate { - key: setting_key_clone.clone(), - value: "".to_owned(), // Reset to default - category: None, - }], - changed_by: "tli_user".to_owned(), - reason: "Reset to default".to_owned(), - validate_before_update: true, - }; - match client.update_configuration(update_request).await { - Ok(response) => { - let update_response = response.into_inner(); - if update_response.success { - // Reset successful - self.refresh_data().await?; - self.load_setting_history(&setting_key_clone).await?; - self.needs_redraw = true; - Ok(()) - } else { - // Store validation result for display - let validation_result = ValidationResult { - is_valid: false, - valid: false, - errors: update_response - .validation_errors - .into_iter() - .map(|e| e.message) - .collect(), - warnings: vec![], - }; - self.validation_cache - .insert(setting_key_clone.clone(), validation_result); - self.ui_state.focused_panel = Panel::Validation; - self.needs_redraw = true; - Ok(()) - } - }, - Err(e) => { - let error_result = ValidationResult { - is_valid: false, - valid: false, - errors: vec![format!("Reset failed: {}", e)], - warnings: Vec::new(), - }; - self.validation_cache - .insert(setting_key_clone, error_result); - self.ui_state.focused_panel = Panel::Validation; - self.needs_redraw = true; - Err(anyhow::Error::from(e)) - }, - } - } else { - Ok(()) - } - } -} - -impl Dashboard for ConfigurationDashboard { - fn render(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - // Main layout: [Header][Content][Footer] - let main_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), // Header - Constraint::Min(10), // Content - Constraint::Length(3), // Footer - ]) - .split(area); - - // Render header - self.render_header(frame, main_chunks[0_usize])?; - - // Content layout based on connection status - match &self.connection_status { - ConnectionStatus::Connected => { - self.render_connected_content(frame, main_chunks[1_usize])?; - }, - ConnectionStatus::Connecting => { - self.render_connecting_screen(frame, main_chunks[1_usize])?; - }, - ConnectionStatus::Disconnected => { - self.render_disconnected_screen(frame, main_chunks[1_usize])?; - }, - ConnectionStatus::Error(err) => { - self.render_error_screen(frame, main_chunks[1_usize], err)?; - }, - } - - // Render footer - self.render_footer(frame, main_chunks[2_usize])?; - - self.needs_redraw = false; - Ok(()) - } - - fn handle_input(&mut self, key: KeyEvent) -> Result> { - // Handle search mode - if self.search_state.is_searching { - return self.handle_search_input(key); - } - - // Handle edit mode - if self.edit_state.is_editing { - return self.handle_edit_input(key); - } - - // Normal navigation mode - match key.code { - KeyCode::Tab => { - // Cycle through panels - self.ui_state.focused_panel = match self.ui_state.focused_panel { - Panel::CategoryTree => Panel::SettingsList, - Panel::SettingsList => Panel::History, - Panel::History => Panel::Validation, - Panel::Validation => Panel::CategoryTree, - Panel::Editor => Panel::CategoryTree, - }; - self.needs_redraw = true; - Ok(None) - }, - KeyCode::Up => { - self.handle_up_navigation(); - Ok(None) - }, - KeyCode::Down => { - self.handle_down_navigation(); - Ok(None) - }, - KeyCode::Enter => { - self.handle_enter_key(); - Ok(None) - }, - KeyCode::Char(' ') => { - // Toggle category expansion or start editing - self.handle_space_key(); - Ok(None) - }, - KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::NONE) => { - // Start editing current setting - self.start_editing(); - Ok(None) - }, - KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::NONE) => { - // Reset to default - if let (Some(setting_key), Some(client)) = ( - self.selection.setting_key.clone(), - self.config_client.clone(), - ) { - let event_sender = self._event_sender.clone(); - tokio::spawn(async move { - let mut client = client; - let setting_key_clone = setting_key.clone(); - let username = std::env::var("USER") - .or_else(|_| std::env::var("USERNAME")) - .unwrap_or_else(|_| "tli_user".to_owned()); - - let update_request = crate::proto::config::UpdateConfigRequest { - updates: vec![crate::proto::config::ConfigUpdate { - key: setting_key_clone.clone(), - value: "".to_owned(), // Reset to default - category: None, - }], - changed_by: username, - reason: "Reset to default".to_owned(), - validate_before_update: true, - }; - - match client.update_configuration(update_request).await { - Ok(response) => { - if response.into_inner().success { - let _ = event_sender.send(DashboardEvent::RefreshConfig).await; - } - }, - Err(e) => { - tracing::error!("Failed to reset to default: {}", e); - }, - } - }); - } - Ok(None) - }, - KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::NONE) => { - // Start search - self.start_search(); - Ok(None) - }, - KeyCode::F(5) => { - // Refresh data - if let Some(client) = self.config_client.clone() { - let category_id = self.selection.category_id; - let event_sender = self._event_sender.clone(); - - tokio::spawn(async move { - let mut client = client; - let config_request = crate::proto::config::ConfigRequest { - keys: vec![], - category: None, - environment: None, - include_sensitive: false, - }; - - match client.get_configuration(config_request).await { - Ok(response) => { - let settings = response.into_inner().settings; - if let Some(cat_id) = category_id { - let _ = event_sender - .send(DashboardEvent::ConfigUpdate { - category_id: cat_id, - settings, - }) - .await; - } - }, - Err(e) => { - tracing::error!("Failed to refresh configuration: {}", e); - }, - } - }); - } - Ok(None) - }, - KeyCode::Char('q') | KeyCode::Esc => Ok(Some(DashboardEvent::Exit)), - _ => Ok(None), - } - } - - fn update(&mut self, event: DashboardEvent) -> Result<()> { - match event { - DashboardEvent::ConfigUpdate { - category_id, - settings, - } => { - if self.selection.category_id == Some(category_id) { - self.selection.flat_settings = settings; - self.needs_redraw = true; - } - }, - DashboardEvent::ConfigSearchResults { results } => { - self.search_state.results = results; - self.search_state.selected_result = 0; - self.needs_redraw = true; - }, - _ => {}, - } - Ok(()) - } - - fn title(&self) -> &str { - "Configuration" - } - - fn shortcut_key(&self) -> char { - 'c' - } - - fn needs_redraw(&self) -> bool { - self.needs_redraw - } - - fn mark_drawn(&mut self) { - self.needs_redraw = false; - } -} - -// Implementation of individual render methods and input handlers will continue... -// This is a comprehensive foundation for the Configuration Dashboard - -impl ConfigurationDashboard { - fn render_header(&self, frame: &mut Frame, area: Rect) -> Result<()> { - let status_text = match &self.connection_status { - ConnectionStatus::Connected => "\u{1f7e2} Connected", - ConnectionStatus::Connecting => "\u{1f7e1} Connecting...", - ConnectionStatus::Disconnected => "\u{1f534} Disconnected", - ConnectionStatus::Error(_) => "\u{1f534} Error", - }; - - let header_text = format!( - "Configuration Dashboard - {} | Environment: {} | Panel: {:?}", - status_text, self.ui_state.environment, self.ui_state.focused_panel - ); - - let header = Paragraph::new(header_text) - .block( - Block::default() - .borders(Borders::ALL) - .title("Configuration Management"), - ) - .style(Style::default().fg(Color::White)) - .wrap(Wrap { trim: true }); - - frame.render_widget(header, area); - Ok(()) - } - - fn render_connected_content(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - // Three-column layout: [Categories][Settings+Editor][History+Validation] - let content_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage(25), // Category tree - Constraint::Percentage(50), // Settings and editor - Constraint::Percentage(25), // History and validation - ]) - .split(area); - - // Render category tree - self.render_category_tree(frame, content_chunks[0_usize])?; - - // Split middle section for settings and editor - let middle_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage(60), // Settings list - Constraint::Percentage(40), // Editor - ]) - .split(content_chunks[1_usize]); - - self.render_settings_list(frame, middle_chunks[0_usize])?; - self.render_editor(frame, middle_chunks[1_usize])?; - - // Split right section for history and validation - let right_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage(60), // History - Constraint::Percentage(40), // Validation - ]) - .split(content_chunks[2_usize]); - - self.render_history(frame, right_chunks[0_usize])?; - self.render_validation(frame, right_chunks[1_usize])?; - - Ok(()) - } - - fn render_category_tree(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let items: Vec = self - .selection - .flat_categories - .iter() - .map(|cat| { - let indent = " ".repeat(cat.level); - let icon = if cat.setting_count > 0 { - if cat.is_expanded { - "\u{25bc}" - } else { - "\u{25b6}" - } - } else { - " " - }; - let text = format!("{}{} {} ({})", indent, icon, cat.name, cat.setting_count); - ListItem::new(text) - }) - .collect(); - - let list = List::new(items) - .block( - Block::default() - .borders(Borders::ALL) - .title("Categories") - .border_style(if self.ui_state.focused_panel == Panel::CategoryTree { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }), - ) - .highlight_style(Style::default().bg(Color::DarkGray)) - .highlight_symbol(">> "); - - frame.render_stateful_widget(list, area, &mut self.ui_state.category_list_state); - Ok(()) - } - - fn render_settings_list(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let items: Vec = self - .selection - .flat_settings - .iter() - .map(|setting| { - let value_display = if setting.sensitive && !self.ui_state.show_sensitive { - "\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}".to_owned() - } else { - setting.value.clone() - }; - - let hot_reload_indicator = if setting.hot_reload { " \u{1f525}" } else { "" }; - let required_indicator = if setting.required { " *" } else { "" }; - - let text = format!( - "{}{}{}: {}", - setting.key, required_indicator, hot_reload_indicator, value_display - ); - ListItem::new(text) - }) - .collect(); - - let list = List::new(items) - .block( - Block::default() - .borders(Borders::ALL) - .title("Settings") - .border_style(if self.ui_state.focused_panel == Panel::SettingsList { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }), - ) - .highlight_style(Style::default().bg(Color::DarkGray)) - .highlight_symbol(">> "); - - frame.render_stateful_widget(list, area, &mut self.ui_state.settings_list_state); - Ok(()) - } - - fn render_editor(&self, frame: &mut Frame, area: Rect) -> Result<()> { - let title = if self.edit_state.is_editing { - if self.edit_state.has_changes { - "Editor (Modified)" - } else { - "Editor" - } - } else { - "Editor (Read-only)" - }; - - let content = if self.edit_state.is_editing { - &self.edit_state.edit_buffer - } else if let Some(setting_index) = self.ui_state.settings_list_state.selected() { - if let Some(setting) = self.selection.flat_settings.get(setting_index) { - &setting.value - } else { - "No setting selected" - } - } else { - "No setting selected" - }; - - let editor = Paragraph::new(content) - .block( - Block::default() - .borders(Borders::ALL) - .title(title) - .border_style(if self.ui_state.focused_panel == Panel::Editor { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }), - ) - .wrap(Wrap { trim: true }) - .style(if self.edit_state.is_editing { - Style::default().fg(Color::Green) - } else { - Style::default() - }); - - frame.render_widget(editor, area); - Ok(()) - } - - fn render_history(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let items: Vec = self - .current_history - .iter() - .map(|entry| { - let timestamp = if entry.changed_at_unix_nanos > 0 { - let seconds = entry.changed_at_unix_nanos / 1_000_000_000; - let nanos = (entry.changed_at_unix_nanos % 1_000_000_000) as u32; - chrono::DateTime::from_timestamp(seconds, nanos) - .unwrap_or_else(chrono::Utc::now) - .format("%H:%M:%S") - .to_string() - } else { - chrono::Utc::now().format("%H:%M:%S").to_string() - }; - let text = format!( - "{} - {} -> {}", - timestamp, - entry.old_value.chars().take(20).collect::(), - entry.new_value.chars().take(20).collect::() - ); - ListItem::new(text) - }) - .collect(); - - let list = List::new(items) - .block( - Block::default() - .borders(Borders::ALL) - .title("Recent Changes") - .border_style(if self.ui_state.focused_panel == Panel::History { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }), - ) - .highlight_style(Style::default().bg(Color::DarkGray)) - .highlight_symbol(">> "); - - frame.render_stateful_widget(list, area, &mut self.ui_state.history_list_state); - Ok(()) - } - - fn render_validation(&self, frame: &mut Frame, area: Rect) -> Result<()> { - let content = if let Some(setting_key) = &self.selection.setting_key { - if let Some(validation) = self.validation_cache.get(setting_key) { - let mut lines = Vec::new(); - - if validation.valid { - lines.push("\u{2713} VALID".to_owned()); - } else { - lines.push("\u{2717} INVALID".to_owned()); - } - - if !validation.errors.is_empty() { - lines.push("".to_owned()); - lines.push("Errors:".to_owned()); - for error in &validation.errors { - lines.push(format!(" \u{2022} {}", error)); - } - } - - if !validation.warnings.is_empty() { - lines.push("".to_owned()); - lines.push("Warnings:".to_owned()); - for warning in &validation.warnings { - lines.push(format!(" \u{2022} {}", warning)); - } - } - - lines.join("\n") - } else { - "Press 'V' to validate current value".to_owned() - } - } else { - "No setting selected".to_owned() - }; - - let validation = Paragraph::new(content) - .block( - Block::default() - .borders(Borders::ALL) - .title("Validation Status") - .border_style(if self.ui_state.focused_panel == Panel::Validation { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }), - ) - .wrap(Wrap { trim: true }); - - frame.render_widget(validation, area); - Ok(()) - } - - fn render_footer(&self, frame: &mut Frame, area: Rect) -> Result<()> { - let help_text = if self.edit_state.is_editing { - "[Ctrl+S] Save | [Esc] Cancel | [Ctrl+V] Validate" - } else if self.search_state.is_searching { - "[Enter] Search | [Esc] Cancel" - } else { - "[E] Edit | [S] Search | [R] Reset | [Space] Toggle | [F5] Refresh | [Tab] Switch Panel | [Q] Quit" - }; - - let footer = Paragraph::new(help_text) - .block(Block::default().borders(Borders::ALL)) - .style(Style::default().fg(Color::Gray)) - .wrap(Wrap { trim: true }); - - frame.render_widget(footer, area); - Ok(()) - } - - fn render_connecting_screen(&self, frame: &mut Frame, area: Rect) -> Result<()> { - let connecting = Paragraph::new("Connecting to PostgreSQL...\n\nPlease wait while we establish connection to the configuration database.") - .block(Block::default().borders(Borders::ALL).title("Connecting")) - .style(Style::default().fg(Color::Yellow)) - .wrap(Wrap { trim: true }); - - frame.render_widget(connecting, area); - Ok(()) - } - - fn render_disconnected_screen(&self, frame: &mut Frame, area: Rect) -> Result<()> { - let disconnected = Paragraph::new("Not connected to configuration database.\n\nPress 'C' to connect or check your database configuration.") - .block(Block::default().borders(Borders::ALL).title("Disconnected")) - .style(Style::default().fg(Color::Red)) - .wrap(Wrap { trim: true }); - - frame.render_widget(disconnected, area); - Ok(()) - } - - fn render_error_screen(&self, frame: &mut Frame, area: Rect, error: &str) -> Result<()> { - let error_text = format!( - "Connection Error:\n\n{}\n\nPress 'R' to retry connection or 'Q' to quit.", - error - ); - - let error_display = Paragraph::new(error_text) - .block(Block::default().borders(Borders::ALL).title("Error")) - .style(Style::default().fg(Color::Red)) - .wrap(Wrap { trim: true }); - - frame.render_widget(error_display, area); - Ok(()) - } - - // Input handling methods - fn handle_search_input(&mut self, key: KeyEvent) -> Result> { - match key.code { - KeyCode::Char(c) => { - self.search_state.query.push(c); - self.trigger_search(); - Ok(None) - }, - KeyCode::Backspace => { - self.search_state.query.pop(); - self.trigger_search(); - Ok(None) - }, - KeyCode::Enter => { - // Select search result and navigate to it - if !self.search_state.results.is_empty() - && self.search_state.selected_result < self.search_state.results.len() - { - let selected_setting = - self.search_state.results[self.search_state.selected_result].clone(); - - // Find the category containing this setting and navigate to it - for category in &self.selection.flat_categories { - self.selection.category_id = Some(category.id); - self.load_category_settings(category.id); - - // Set the selection to the found setting - self.selection.setting_key = Some(selected_setting.key); - break; - } - } - self.search_state.is_searching = false; - self.needs_redraw = true; - Ok(None) - }, - KeyCode::Esc => { - self.search_state.is_searching = false; - self.needs_redraw = true; - Ok(None) - }, - _ => Ok(None), - } - } - - fn trigger_search(&mut self) { - if let Some(client) = self.config_client.clone() { - let query = self.search_state.query.clone(); - let event_sender = self._event_sender.clone(); - - tokio::spawn(async move { - if query.trim().is_empty() { - return; - } - - let mut client = client; - let search_request = crate::proto::config::ConfigRequest { - keys: vec![query.clone()], - category: None, - environment: None, - include_sensitive: false, - }; - - match client.get_configuration(search_request).await { - Ok(response) => { - let results = response.into_inner().settings; - let _ = event_sender - .send(DashboardEvent::ConfigSearchResults { results }) - .await; - }, - Err(e) => { - tracing::error!("Search failed: {}", e); - }, - } - }); - } - } - - fn handle_edit_input(&mut self, key: KeyEvent) -> Result> { - match key.code { - KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => { - self.edit_state - .edit_buffer - .insert(self.edit_state.cursor_position, c); - self.edit_state.cursor_position += 1; - self.edit_state.has_changes = - self.edit_state.edit_buffer != self.edit_state.original_value; - self.needs_redraw = true; - Ok(None) - }, - KeyCode::Backspace => { - if self.edit_state.cursor_position > 0 { - self.edit_state.cursor_position -= 1; - self.edit_state - .edit_buffer - .remove(self.edit_state.cursor_position); - self.edit_state.has_changes = - self.edit_state.edit_buffer != self.edit_state.original_value; - self.needs_redraw = true; - } - Ok(None) - }, - KeyCode::Left => { - if self.edit_state.cursor_position > 0 { - self.edit_state.cursor_position -= 1; - self.needs_redraw = true; - } - Ok(None) - }, - KeyCode::Right => { - if self.edit_state.cursor_position < self.edit_state.edit_buffer.len() { - self.edit_state.cursor_position += 1; - self.needs_redraw = true; - } - Ok(None) - }, - KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Save changes - call the existing async save_edit method - if let (Some(setting_key), Some(mut client)) = ( - self.selection.setting_key.clone(), - self.config_client.clone(), - ) { - let edit_buffer = self.edit_state.edit_buffer.clone(); - let event_sender = self._event_sender.clone(); - - tokio::spawn(async move { - let username = std::env::var("USER") - .or_else(|_| std::env::var("USERNAME")) - .unwrap_or_else(|_| "tli_user".to_owned()); - - let config_update = crate::proto::config::ConfigUpdate { - key: setting_key.clone(), - value: edit_buffer, - category: None, - }; - - let request = crate::proto::config::UpdateConfigRequest { - updates: vec![config_update], - changed_by: username, - reason: "Manual edit via TLI Configuration Dashboard".to_owned(), - validate_before_update: true, - }; - - match client.update_configuration(request).await { - Ok(response) => { - if response.into_inner().success { - let _ = event_sender.send(DashboardEvent::RefreshConfig).await; - } - }, - Err(e) => { - tracing::error!("Failed to save configuration: {}", e); - }, - } - }); - } - Ok(None) - }, - KeyCode::Char('v') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Validate - call the existing async validate_current_edit method - if let (Some(setting_key), Some(mut client)) = ( - self.selection.setting_key.clone(), - self.config_client.clone(), - ) { - let edit_buffer = self.edit_state.edit_buffer.clone(); - - tokio::spawn(async move { - let validate_request = crate::proto::config::ValidateRequest { - validations: vec![crate::proto::config::ConfigValidation { - key: setting_key.to_string(), - value: edit_buffer, - category: None, - }], - check_dependencies: false, - }; - - match client.validate_configuration(validate_request).await { - Ok(response) => { - let validation_response = response.into_inner(); - tracing::info!( - "Validation result: valid={}, errors={}, warnings={}", - validation_response.valid, - validation_response.errors.len(), - validation_response.warnings.len() - ); - }, - Err(e) => { - tracing::error!("Validation failed: {}", e); - }, - } - }); - } - Ok(None) - }, - KeyCode::Esc => { - self.cancel_editing(); - Ok(None) - }, - _ => Ok(None), - } - } - - fn handle_up_navigation(&mut self) { - match self.ui_state.focused_panel { - Panel::CategoryTree => { - let current = self.ui_state.category_list_state.selected().unwrap_or(0); - if current > 0 { - self.ui_state.category_list_state.select(Some(current - 1)); - self.needs_redraw = true; - } - }, - Panel::SettingsList => { - let current = self.ui_state.settings_list_state.selected().unwrap_or(0); - if current > 0 { - self.ui_state.settings_list_state.select(Some(current - 1)); - self.needs_redraw = true; - } - }, - Panel::History => { - let current = self.ui_state.history_list_state.selected().unwrap_or(0); - if current > 0 { - self.ui_state.history_list_state.select(Some(current - 1)); - self.needs_redraw = true; - } - }, - _ => {}, - } - } - - fn handle_down_navigation(&mut self) { - match self.ui_state.focused_panel { - Panel::CategoryTree => { - let current = self.ui_state.category_list_state.selected().unwrap_or(0); - if current < self.selection.flat_categories.len().saturating_sub(1) { - self.ui_state.category_list_state.select(Some(current + 1)); - self.needs_redraw = true; - } - }, - Panel::SettingsList => { - let current = self.ui_state.settings_list_state.selected().unwrap_or(0); - if current < self.selection.flat_settings.len().saturating_sub(1) { - self.ui_state.settings_list_state.select(Some(current + 1)); - self.needs_redraw = true; - } - }, - Panel::History => { - let current = self.ui_state.history_list_state.selected().unwrap_or(0); - if current < self.current_history.len().saturating_sub(1) { - self.ui_state.history_list_state.select(Some(current + 1)); - self.needs_redraw = true; - } - }, - _ => {}, - } - } - - fn handle_enter_key(&mut self) { - match self.ui_state.focused_panel { - Panel::CategoryTree => { - if let Some(index) = self.ui_state.category_list_state.selected() { - if let Some(category) = self.selection.flat_categories.get(index) { - self.load_category_settings(category.id); - self.ui_state.focused_panel = Panel::SettingsList; - self.needs_redraw = true; - } - } - }, - Panel::SettingsList => { - self.start_editing(); - }, - _ => {}, - } - } - - fn handle_space_key(&mut self) { - match self.ui_state.focused_panel { - Panel::CategoryTree => { - if let Some(index) = self.ui_state.category_list_state.selected() { - if let Some(category) = self.selection.flat_categories.get(index) { - self.toggle_category_expansion(category.id); - } - } - }, - Panel::SettingsList => { - self.start_editing(); - }, - _ => {}, - } - } -} diff --git a/tli/src/dashboards/mod.rs b/tli/src/dashboards/mod.rs deleted file mode 100644 index 9a519f897..000000000 --- a/tli/src/dashboards/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Dashboard implementations for TLI -//! -//! This module contains the actual dashboard implementations that are used -//! by the dashboard framework. - -pub mod config_manager; -pub mod configuration; - -// Re-export commonly used dashboard types -pub use configuration::ConfigurationDashboard; diff --git a/tli/src/error_consolidated.rs b/tli/src/error_consolidated.rs deleted file mode 100644 index 73eadcaf8..000000000 --- a/tli/src/error_consolidated.rs +++ /dev/null @@ -1,522 +0,0 @@ -//! Consolidated error handling for the TLI module using CommonError -//! -//! This module demonstrates the consolidated error handling pattern -//! using the common error system across all Foxhunt TLI services. - -// REMOVED: All pub use statements eliminated per cleanup requirements -// Use direct imports: common::error::{CommonError, CommonResult, ErrorCategory, RetryStrategy, ErrorSeverity} -use tonic::{Code, Status}; - -/// Result type for TLI operations using CommonError -pub type TliResult = common::error::CommonResult; - -/// TLI module specific error extensions -/// -/// For cases where we need domain-specific error information beyond CommonError -#[derive(Debug, thiserror::Error)] -pub enum TliServiceError { - /// Common error with context - #[error("TLI service error: {0}")] - Common(#[from] common::error::CommonError), - - /// gRPC connection specific error with service context - #[error("gRPC connection error: {service} at {endpoint} - {message}")] - GrpcConnection { - service: String, - endpoint: String, - message: String, - }, - - /// Order validation error with order context - #[error("Order validation error: {order_id} - {field}: {message}")] - OrderValidation { - order_id: String, - field: String, - message: String, - }, - - /// Dashboard rendering error - #[error("Dashboard rendering error: {widget} - {message}")] - DashboardRendering { - widget: String, - message: String, - }, - - /// Event buffer overflow - #[error("Event buffer overflow: {buffer_name} capacity {capacity} exceeded")] - EventBufferOverflow { - buffer_name: String, - capacity: usize, - }, - - /// Configuration hot-reload error - #[error("Configuration hot-reload error: {config_key} - {message}")] - ConfigHotReload { - config_key: String, - message: String, - }, - - /// Certificate validation error - #[error("Certificate validation error: {cert_type} - {message}")] - CertificateValidation { - cert_type: String, - message: String, - }, - - /// Trading service communication error - #[error("Trading service error: {operation} - {message}")] - TradingService { - operation: String, - message: String, - }, - - /// ML service communication error - #[error("ML service error: {operation} - {message}")] - MLService { - operation: String, - message: String, - }, - - /// Backtesting service communication error - #[error("Backtesting service error: {operation} - {message}")] - BacktestingService { - operation: String, - message: String, - }, -} - -impl TliServiceError { - /// Convert to CommonError for metrics and monitoring - pub fn to_common_error(self) -> common::error::CommonError { - match self { - TliServiceError::Common(err) => err, - TliServiceError::GrpcConnection { service, endpoint, message } => { - common::error::CommonError::connection( - format!("grpc://{}:{}", service, endpoint), - message - ) - } - TliServiceError::OrderValidation { order_id, field, message } => { - common::error::CommonError::validation( - format!("order[{}].{}", order_id, field), - message - ) - } - TliServiceError::DashboardRendering { widget, message } => { - common::error::CommonError::internal(format!("Dashboard widget {}: {}", widget, message)) - } - TliServiceError::EventBufferOverflow { buffer_name, capacity } => { - common::error::CommonError::resource_exhausted( - format!("Event buffer {} (capacity: {})", buffer_name, capacity) - ) - } - TliServiceError::ConfigHotReload { config_key, message } => { - common::error::CommonError::config(format!("Hot-reload {} failed: {}", config_key, message)) - } - TliServiceError::CertificateValidation { cert_type, message } => { - common::error::CommonError::authentication(format!("Certificate {}: {}", cert_type, message)) - } - TliServiceError::TradingService { operation, message } => { - common::error::CommonError::service( - common::error::ErrorCategory::Trading, - format!("Trading service {}: {}", operation, message) - ) - } - TliServiceError::MLService { operation, message } => { - common::error::CommonError::service( - common::error::ErrorCategory::ML, - format!("ML service {}: {}", operation, message) - ) - } - TliServiceError::BacktestingService { operation, message } => { - common::error::CommonError::service( - common::error::ErrorCategory::System, - format!("Backtesting service {}: {}", operation, message) - ) - } - } - } - - /// Get error category for metrics - pub fn category(&self) -> common::error::ErrorCategory { - match self { - TliServiceError::Common(_) => self.to_common_error().category(), - TliServiceError::TradingService { .. } => common::error::ErrorCategory::Trading, - TliServiceError::MLService { .. } => common::error::ErrorCategory::ML, - TliServiceError::BacktestingService { .. } => common::error::ErrorCategory::System, - TliServiceError::GrpcConnection { .. } => common::error::ErrorCategory::Network, - TliServiceError::OrderValidation { .. } => common::error::ErrorCategory::Validation, - TliServiceError::CertificateValidation { .. } => common::error::ErrorCategory::Security, - _ => common::error::ErrorCategory::System, - } - } - - /// Get error severity - pub fn severity(&self) -> common::error::ErrorSeverity { - match self { - TliServiceError::CertificateValidation { .. } => common::error::ErrorSeverity::Critical, - TliServiceError::ConfigHotReload { .. } => common::error::ErrorSeverity::Error, - TliServiceError::TradingService { .. } => common::error::ErrorSeverity::Error, - TliServiceError::MLService { .. } => common::error::ErrorSeverity::Error, - TliServiceError::BacktestingService { .. } => common::error::ErrorSeverity::Error, - TliServiceError::GrpcConnection { .. } => common::error::ErrorSeverity::Warn, - TliServiceError::EventBufferOverflow { .. } => common::error::ErrorSeverity::Warn, - TliServiceError::OrderValidation { .. } => common::error::ErrorSeverity::Info, - TliServiceError::DashboardRendering { .. } => common::error::ErrorSeverity::Info, - TliServiceError::Common(_) => self.to_common_error().severity(), - } - } - - /// Get retry strategy - pub fn retry_strategy(&self) -> common::error::RetryStrategy { - match self { - // Authentication/security errors should not be retried - TliServiceError::CertificateValidation { .. } => common::error::RetryStrategy::NoRetry, - TliServiceError::OrderValidation { .. } => common::error::RetryStrategy::NoRetry, - - // Network/connection errors can be retried with backoff - TliServiceError::GrpcConnection { .. } => common::error::RetryStrategy::Exponential { - base_delay_ms: 500, - max_delay_ms: 5000, - }, - TliServiceError::TradingService { .. } => common::error::RetryStrategy::Exponential { - base_delay_ms: 100, - max_delay_ms: 2000, - }, - TliServiceError::MLService { .. } => common::error::RetryStrategy::Linear { - base_delay_ms: 1000, - }, - TliServiceError::BacktestingService { .. } => common::error::RetryStrategy::Linear { - base_delay_ms: 2000, - }, - - // System errors can retry with delay - TliServiceError::ConfigHotReload { .. } => common::error::RetryStrategy::Linear { - base_delay_ms: 5000, - }, - TliServiceError::EventBufferOverflow { .. } => common::error::RetryStrategy::Linear { - base_delay_ms: 1000, - }, - TliServiceError::DashboardRendering { .. } => common::error::RetryStrategy::Immediate, - - TliServiceError::Common(_) => self.to_common_error().retry_strategy(), - } - } - - /// Check if error is retryable - pub fn is_retryable(&self) -> bool { - !matches!(self.retry_strategy(), common::error::RetryStrategy::NoRetry) - } - - /// Get error code for monitoring - pub fn error_code(&self) -> &'static str { - match self { - TliServiceError::Common(_) => "TLI_COMMON_ERROR", - TliServiceError::GrpcConnection { .. } => "TLI_GRPC_CONNECTION_ERROR", - TliServiceError::OrderValidation { .. } => "TLI_ORDER_VALIDATION_ERROR", - TliServiceError::DashboardRendering { .. } => "TLI_DASHBOARD_RENDERING_ERROR", - TliServiceError::EventBufferOverflow { .. } => "TLI_EVENT_BUFFER_OVERFLOW", - TliServiceError::ConfigHotReload { .. } => "TLI_CONFIG_HOT_RELOAD_ERROR", - TliServiceError::CertificateValidation { .. } => "TLI_CERTIFICATE_VALIDATION_ERROR", - TliServiceError::TradingService { .. } => "TLI_TRADING_SERVICE_ERROR", - TliServiceError::MLService { .. } => "TLI_ML_SERVICE_ERROR", - TliServiceError::BacktestingService { .. } => "TLI_BACKTESTING_SERVICE_ERROR", - } - } -} - -/// Convert standard errors to CommonError for consistent handling -impl From for TliServiceError { - fn from(err: std::io::Error) -> Self { - TliServiceError::Common(common::error::CommonError::network(format!("IO error: {}", err))) - } -} - -impl From for TliServiceError { - fn from(err: serde_json::Error) -> Self { - TliServiceError::Common(common::error::CommonError::serialization(format!("JSON error: {}", err))) - } -} - -impl From for TliServiceError { - fn from(err: anyhow::Error) -> Self { - TliServiceError::Common(common::error::CommonError::internal(format!("Anyhow error: {}", err))) - } -} - -impl From for TliServiceError { - fn from(status: tonic::Status) -> Self { - let message = status.message().to_string(); - match status.code() { - Code::InvalidArgument => TliServiceError::Common(common::error::CommonError::validation("request", message)), - Code::NotFound => TliServiceError::Common(common::error::CommonError::not_found("resource", message)), - Code::PermissionDenied => TliServiceError::Common(common::error::CommonError::authorization(message)), - Code::Unauthenticated => TliServiceError::Common(common::error::CommonError::authentication(message)), - Code::ResourceExhausted => TliServiceError::Common(common::error::CommonError::rate_limited(message)), - Code::FailedPrecondition => TliServiceError::Common(common::error::CommonError::validation("precondition", message)), - Code::Unavailable => TliServiceError::Common(common::error::CommonError::service_unavailable("grpc_service", message)), - Code::DeadlineExceeded => TliServiceError::Common(common::error::CommonError::timeout(5000, 2000)), - Code::Internal => TliServiceError::Common(common::error::CommonError::internal(message)), - _ => TliServiceError::Common(common::error::CommonError::internal(format!("gRPC error: {}", message))), - } - } -} - -/// Enhanced gRPC Status conversion with proper error mapping -impl From for tonic::Status { - fn from(err: TliServiceError) -> Self { - match err { - TliServiceError::OrderValidation { order_id, field, message } => { - tonic::Status::invalid_argument(format!("Order {} field {}: {}", order_id, field, message)) - } - TliServiceError::CertificateValidation { cert_type, message } => { - tonic::Status::unauthenticated(format!("Certificate {}: {}", cert_type, message)) - } - TliServiceError::GrpcConnection { service, endpoint, message } => { - tonic::Status::unavailable(format!("Service {} at {}: {}", service, endpoint, message)) - } - TliServiceError::EventBufferOverflow { buffer_name, capacity } => { - tonic::Status::resource_exhausted(format!("Buffer {} capacity {} exceeded", buffer_name, capacity)) - } - TliServiceError::ConfigHotReload { config_key, message } => { - tonic::Status::internal(format!("Config {} hot-reload failed: {}", config_key, message)) - } - TliServiceError::DashboardRendering { widget, message } => { - tonic::Status::internal(format!("Dashboard widget {}: {}", widget, message)) - } - TliServiceError::TradingService { operation, message } => { - tonic::Status::unavailable(format!("Trading service {}: {}", operation, message)) - } - TliServiceError::MLService { operation, message } => { - tonic::Status::unavailable(format!("ML service {}: {}", operation, message)) - } - TliServiceError::BacktestingService { operation, message } => { - tonic::Status::unavailable(format!("Backtesting service {}: {}", operation, message)) - } - TliServiceError::Common(common_err) => common_err.into(), - } - } -} - -/// Convenience functions for creating TLI service errors -impl TliServiceError { - /// Create gRPC connection error - pub fn grpc_connection, E: Into, M: Into>( - service: S, - endpoint: E, - message: M, - ) -> Self { - Self::GrpcConnection { - service: service.into(), - endpoint: endpoint.into(), - message: message.into(), - } - } - - /// Create order validation error - pub fn order_validation, F: Into, M: Into>( - order_id: O, - field: F, - message: M, - ) -> Self { - Self::OrderValidation { - order_id: order_id.into(), - field: field.into(), - message: message.into(), - } - } - - /// Create dashboard rendering error - pub fn dashboard_rendering, M: Into>(widget: W, message: M) -> Self { - Self::DashboardRendering { - widget: widget.into(), - message: message.into(), - } - } - - /// Create event buffer overflow error - pub fn event_buffer_overflow>(buffer_name: B, capacity: usize) -> Self { - Self::EventBufferOverflow { - buffer_name: buffer_name.into(), - capacity, - } - } - - /// Create config hot-reload error - pub fn config_hot_reload, M: Into>(config_key: K, message: M) -> Self { - Self::ConfigHotReload { - config_key: config_key.into(), - message: message.into(), - } - } - - /// Create certificate validation error - pub fn certificate_validation, M: Into>(cert_type: C, message: M) -> Self { - Self::CertificateValidation { - cert_type: cert_type.into(), - message: message.into(), - } - } - - /// Create trading service error - pub fn trading_service, M: Into>(operation: O, message: M) -> Self { - Self::TradingService { - operation: operation.into(), - message: message.into(), - } - } - - /// Create ML service error - pub fn ml_service, M: Into>(operation: O, message: M) -> Self { - Self::MLService { - operation: operation.into(), - message: message.into(), - } - } - - /// Create backtesting service error - pub fn backtesting_service, M: Into>(operation: O, message: M) -> Self { - Self::BacktestingService { - operation: operation.into(), - message: message.into(), - } - } - - /// Create network error using CommonError - pub fn network>(message: M) -> Self { - Self::Common(common::error::CommonError::network(message)) - } - - /// Create authentication error using CommonError - pub fn authentication>(message: M) -> Self { - Self::Common(common::error::CommonError::authentication(message)) - } - - /// Create configuration error using CommonError - pub fn configuration>(message: M) -> Self { - Self::Common(common::error::CommonError::config(message)) - } - - /// Create validation error using CommonError - pub fn validation, M: Into>(field: F, message: M) -> Self { - Self::Common(common::error::CommonError::validation(field, message)) - } - - /// Create timeout error using CommonError - pub fn timeout(actual_ms: u64, max_ms: u64) -> Self { - Self::Common(common::error::CommonError::timeout(actual_ms, max_ms)) - } - - /// Create internal error using CommonError - pub fn internal>(message: M) -> Self { - Self::Common(common::error::CommonError::internal(message)) - } - - /// Create not found error using CommonError - pub fn not_found, I: Into>(resource: R, identifier: I) -> Self { - Self::Common(common::error::CommonError::not_found(resource, identifier)) - } -} - -/// Convert to CommonError automatically for interop -impl From for common::error::CommonError { - fn from(err: TliServiceError) -> Self { - err.to_common_error() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_tli_service_error_categorization() { - let grpc_error = TliServiceError::grpc_connection("trading", "localhost:50051", "Connection refused"); - assert_eq!(grpc_error.category(), common::error::ErrorCategory::Network); - assert_eq!(grpc_error.error_code(), "TLI_GRPC_CONNECTION_ERROR"); - assert!(grpc_error.is_retryable()); - - let order_error = TliServiceError::order_validation("ORD123", "quantity", "Must be positive"); - assert_eq!(order_error.category(), common::error::ErrorCategory::Validation); - assert!(!order_error.is_retryable()); - } - - #[test] - fn test_service_specific_errors() { - let trading_error = TliServiceError::trading_service("submit_order", "Service unavailable"); - assert_eq!(trading_error.category(), common::error::ErrorCategory::Trading); - assert_eq!(trading_error.severity(), common::error::ErrorSeverity::Error); - assert!(trading_error.is_retryable()); - - let ml_error = TliServiceError::ml_service("train_model", "GPU memory exhausted"); - assert_eq!(ml_error.category(), common::error::ErrorCategory::ML); - assert!(ml_error.is_retryable()); - } - - #[test] - fn test_retry_strategies() { - let cert_error = TliServiceError::certificate_validation("TLS", "Certificate expired"); - assert!(!cert_error.is_retryable()); - assert_eq!(cert_error.retry_strategy(), common::error::RetryStrategy::NoRetry); - - let grpc_error = TliServiceError::grpc_connection("ml", "localhost:50052", "Connection timeout"); - assert!(grpc_error.is_retryable()); - match grpc_error.retry_strategy() { - common::error::RetryStrategy::Exponential { base_delay_ms, max_delay_ms } => { - assert_eq!(base_delay_ms, 500); - assert_eq!(max_delay_ms, 5000); - } - _ => panic!("Expected exponential backoff for gRPC connection errors"), - } - } - - #[test] - fn test_grpc_status_conversion() { - let order_error = TliServiceError::order_validation("ORD456", "price", "Must be greater than zero"); - let status: tonic::Status = order_error.into(); - - assert_eq!(status.code(), Code::InvalidArgument); - assert!(status.message().contains("ORD456")); - assert!(status.message().contains("price")); - - let cert_error = TliServiceError::certificate_validation("client", "Invalid signature"); - let cert_status: tonic::Status = cert_error.into(); - assert_eq!(cert_status.code(), Code::Unauthenticated); - } - - #[test] - fn test_buffer_overflow_error() { - let buffer_error = TliServiceError::event_buffer_overflow("order_events", 10000); - assert_eq!(buffer_error.category(), common::error::ErrorCategory::System); - assert_eq!(buffer_error.severity(), common::error::ErrorSeverity::Warn); - assert!(buffer_error.is_retryable()); - - let status: tonic::Status = buffer_error.into(); - assert_eq!(status.code(), Code::ResourceExhausted); - assert!(status.message().contains("10000")); - } - - #[test] - fn test_common_error_integration() { - let config_error = TliServiceError::configuration("Missing gRPC endpoint"); - let common_error: common::error::CommonError = config_error.into(); - - assert_eq!(common_error.category(), common::error::ErrorCategory::Configuration); - assert_eq!(common_error.severity(), common::error::ErrorSeverity::Critical); - assert!(!common_error.is_retryable()); - } - - #[test] - fn test_error_conversion_chain() { - let status = tonic::Status::deadline_exceeded("Request timeout"); - let tli_error: TliServiceError = status.into(); - let common_error: common::error::CommonError = tli_error.into(); - - assert_eq!(common_error.category(), common::error::ErrorCategory::System); - assert!(common_error.is_retryable()); - match common_error.retry_strategy() { - common::error::RetryStrategy::Linear { .. } => (), - _ => panic!("Expected linear backoff for timeout errors"), - } - } -} \ No newline at end of file diff --git a/tli/src/events/aggregator.rs b/tli/src/events/aggregator.rs deleted file mode 100644 index 45f83014c..000000000 --- a/tli/src/events/aggregator.rs +++ /dev/null @@ -1,919 +0,0 @@ -//! Event processing and deduplication with aggregation rules -//! -//! This module provides intelligent event processing with: -//! - Event deduplication based on configurable keys -//! - Aggregation rules for time-based windowing -//! - Event enrichment and transformation -//! - Pattern matching and correlation -//! - Statistical aggregation (count, sum, avg, min, max) -//! - Real-time event stream processing - -use crate::error::{TliError, TliResult}; -use crate::events::{Event, EventFilter, EventSeverity, EventType}; -use chrono::{DateTime, Duration as ChronoDuration, Timelike, Utc}; -use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, VecDeque}; -use std::hash::Hash; -use std::sync::Arc; -use tokio::sync::{mpsc, watch, RwLock}; -use tokio::time::{interval, Duration}; -use tracing::{debug, error, info, instrument, warn}; - -/// Configuration for event aggregation -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AggregationConfig { - /// Enable event deduplication - pub enable_deduplication: bool, - /// Deduplication window in seconds - pub dedup_window_seconds: u64, - /// Maximum number of duplicate events to track - pub max_dedup_entries: usize, - /// Enable time-based aggregation - pub enable_time_aggregation: bool, - /// Aggregation window size in seconds - pub aggregation_window_seconds: u64, - /// Enable statistical aggregation - pub enable_statistics: bool, - /// Enable event enrichment - pub enable_enrichment: bool, - /// Enable pattern matching - pub enable_pattern_matching: bool, - /// Maximum aggregation rules - pub max_aggregation_rules: usize, - /// Processing batch size - pub processing_batch_size: usize, - /// Processing interval in milliseconds - pub processing_interval_ms: u64, -} - -impl Default for AggregationConfig { - fn default() -> Self { - Self { - enable_deduplication: true, - dedup_window_seconds: 60_u64, - max_dedup_entries: 10000_usize, - enable_time_aggregation: true, - aggregation_window_seconds: 300_u64, // 5 minutes - enable_statistics: true, - enable_enrichment: true, - enable_pattern_matching: true, - max_aggregation_rules: 100_usize, - processing_batch_size: 50_usize, - processing_interval_ms: 100_u64, - } - } -} - -/// Aggregation rule definition -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AggregationRule { - /// Unique rule ID - pub id: String, - /// Rule name - pub name: String, - /// Event filter for matching events - pub filter: EventFilter, - /// Aggregation type - pub aggregation_type: AggregationType, - /// Time window for aggregation - pub window_seconds: u64, - /// Fields to aggregate - pub fields: Vec, - /// Grouping keys - pub group_by: Vec, - /// Minimum events required for aggregation - pub min_events: usize, - /// Maximum events in aggregation - pub max_events: usize, - /// Output event type for aggregated events - pub output_event_type: EventType, - /// Enable rule - pub enabled: bool, -} - -/// Types of aggregation operations -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum AggregationType { - /// Count events - Count, - /// Sum numeric values - Sum, - /// Calculate average - Average, - /// Find minimum value - Min, - /// Find maximum value - Max, - /// Collect unique values - Unique, - /// First event in window - First, - /// Last event in window - Last, - /// Merge event payloads - Merge, -} - -/// Deduplication key for identifying duplicate events -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct DeduplicationKey { - /// Event type - pub event_type: String, - /// Source service - pub source: String, - /// Key fields from payload - pub key_fields: Vec<(String, String)>, -} - -impl DeduplicationKey { - /// Create deduplication key from event - pub fn from_event(event: &Event, key_fields: &[String]) -> Self { - let mut fields = Vec::new(); - - for field in key_fields { - if let Some(value) = event.payload.get(field) { - fields.push((field.clone(), value.to_string())); - } - } - - Self { - event_type: event.event_type.as_str().to_owned(), - source: event.source.clone(), - key_fields: fields, - } - } -} - -/// Aggregation window for time-based processing -#[derive(Debug, Clone)] -struct AggregationWindow { - /// Window start time - start_time: DateTime, - /// Window end time - end_time: DateTime, - /// Events in this window - events: Vec, - /// Aggregation result - result: Option, - /// Processing status - processed: bool, -} - -impl AggregationWindow { - fn new(start_time: DateTime, window_seconds: u64) -> Self { - let end_time = start_time + ChronoDuration::seconds(window_seconds as i64); - - Self { - start_time, - end_time, - events: Vec::new(), - result: None, - processed: false, - } - } - - fn add_event(&mut self, event: Event) -> bool { - let event_time = event.timestamp_utc(); - - if event_time >= self.start_time && event_time < self.end_time { - self.events.push(event); - true - } else { - false - } - } - - fn is_complete(&self, current_time: DateTime) -> bool { - current_time >= self.end_time - } -} - -/// Event pattern for correlation -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EventPattern { - /// Pattern ID - pub id: String, - /// Pattern name - pub name: String, - /// Sequence of event filters - pub sequence: Vec, - /// Maximum time between events in seconds - pub max_time_between_seconds: u64, - /// Action to take when pattern matches - pub action: PatternAction, -} - -/// Action to take when pattern matches -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum PatternAction { - /// Generate a new event - GenerateEvent { - event_type: EventType, - severity: EventSeverity, - payload: serde_json::Value, - }, - /// Send alert - SendAlert { - message: String, - severity: EventSeverity, - }, - /// Log message - Log { level: String, message: String }, -} - -/// Main event aggregator -pub struct EventAggregator { - /// Configuration - config: AggregationConfig, - /// Aggregation rules - rules: Arc>>, - /// Deduplication cache - dedup_cache: Arc>>>, - /// Active aggregation windows - aggregation_windows: Arc>>>, - /// Event patterns - patterns: Arc>>, - /// Pattern state tracking - pattern_state: Arc>>>, - /// Processing queue - processing_queue: Arc>>, - /// Output channel for aggregated events - output_sender: mpsc::UnboundedSender, - output_receiver: Arc>>>, - /// Shutdown signal - shutdown_sender: watch::Sender, - shutdown_receiver: watch::Receiver, -} - -impl EventAggregator { - /// Create a new event aggregator - pub fn new(config: AggregationConfig) -> Self { - let (output_sender, output_receiver) = mpsc::unbounded_channel(); - let (shutdown_sender, shutdown_receiver) = watch::channel(false); - - let aggregator = Self { - config, - rules: Arc::new(RwLock::new(HashMap::new())), - dedup_cache: Arc::new(RwLock::new(HashMap::new())), - aggregation_windows: Arc::new(RwLock::new(HashMap::new())), - patterns: Arc::new(RwLock::new(HashMap::new())), - pattern_state: Arc::new(RwLock::new(HashMap::new())), - processing_queue: Arc::new(RwLock::new(VecDeque::new())), - output_sender, - output_receiver: Arc::new(RwLock::new(Some(output_receiver))), - shutdown_sender, - shutdown_receiver, - }; - - // Start processing tasks - aggregator.start_processing_tasks(); - - aggregator - } - - /// Process an event through the aggregation pipeline - #[instrument(skip(self, event))] - pub async fn process_event(&self, event: Event) -> TliResult<()> { - // Add to processing queue - { - let mut queue = self.processing_queue.write().await; - queue.push_back(event); - } - - Ok(()) - } - - /// Add aggregation rule - pub async fn add_rule(&self, rule: AggregationRule) -> TliResult<()> { - if !rule.enabled { - return Ok(()); - } - - let mut rules = self.rules.write().await; - - if rules.len() >= self.config.max_aggregation_rules { - return Err(TliError::InvalidRequest( - "Maximum number of aggregation rules reached".to_owned(), - )); - } - - let rule_id = rule.id.clone(); - rules.insert(rule_id.clone(), rule); - info!("Added aggregation rule: {}", rule_id); - - Ok(()) - } - - /// Remove aggregation rule - pub async fn remove_rule(&self, rule_id: &str) -> TliResult<()> { - let mut rules = self.rules.write().await; - - if rules.remove(rule_id).is_some() { - info!("Removed aggregation rule: {}", rule_id); - Ok(()) - } else { - Err(TliError::NotFound(format!("Rule not found: {}", rule_id))) - } - } - - /// Add event pattern - pub async fn add_pattern(&self, pattern: EventPattern) -> TliResult<()> { - let mut patterns = self.patterns.write().await; - let pattern_id = pattern.id.clone(); - patterns.insert(pattern_id.clone(), pattern); - info!("Added event pattern: {}", pattern_id); - Ok(()) - } - - /// Get aggregation output receiver - pub async fn get_output_receiver(&self) -> Option> { - self.output_receiver.write().await.take() - } - - /// Start background processing tasks - fn start_processing_tasks(&self) { - // Start event processing task - let processor = self.clone(); - tokio::spawn(async move { - processor.event_processing_loop().await; - }); - - // Start cleanup task - let cleaner = self.clone(); - tokio::spawn(async move { - cleaner.cleanup_loop().await; - }); - - // Start aggregation window processing - let window_processor = self.clone(); - tokio::spawn(async move { - window_processor.window_processing_loop().await; - }); - } - - /// Main event processing loop - async fn event_processing_loop(&self) { - let mut interval = interval(Duration::from_millis(self.config.processing_interval_ms)); - let mut shutdown = self.shutdown_receiver.clone(); - - loop { - tokio::select! { - _ = interval.tick() => { - if let Err(e) = self.process_queued_events().await { - error!("Event processing error: {}", e); - } - } - _ = shutdown.changed() => { - if *shutdown.borrow() { - debug!("Event processing loop shutting down"); - break; - } - } - } - } - } - - /// Process events from the queue - async fn process_queued_events(&self) -> TliResult<()> { - let mut events_to_process = Vec::new(); - - // Extract batch of events - { - let mut queue = self.processing_queue.write().await; - let batch_size = self.config.processing_batch_size.min(queue.len()); - - for _ in 0..batch_size { - if let Some(event) = queue.pop_front() { - events_to_process.push(event); - } - } - } - - // Process each event - for event in events_to_process { - // Check for duplicates - if self.config.enable_deduplication { - if self.is_duplicate(&event).await { - continue; - } - self.update_dedup_cache(&event).await; - } - - // Process through aggregation rules - if self.config.enable_time_aggregation { - self.process_aggregation_rules(&event).await?; - } - - // Check event patterns - if self.config.enable_pattern_matching { - self.check_event_patterns(&event).await?; - } - - // Enrich event - if self.config.enable_enrichment { - let enriched_event = self.enrich_event(event).await; - if let Err(e) = self.output_sender.send(enriched_event) { - warn!("Failed to send enriched event: {}", e); - } - } else if let Err(e) = self.output_sender.send(event) { - warn!("Failed to send event: {}", e); - } - } - - Ok(()) - } - - /// Check if event is a duplicate - async fn is_duplicate(&self, event: &Event) -> bool { - let dedup_key = DeduplicationKey::from_event(event, &["id".to_owned()]); - let cache = self.dedup_cache.read().await; - - if let Some(last_seen) = cache.get(&dedup_key) { - let window = ChronoDuration::seconds(self.config.dedup_window_seconds as i64); - let current_time = Utc::now(); - - current_time.signed_duration_since(*last_seen) < window - } else { - false - } - } - - /// Update deduplication cache - async fn update_dedup_cache(&self, event: &Event) { - let dedup_key = DeduplicationKey::from_event(event, &["id".to_owned()]); - let mut cache = self.dedup_cache.write().await; - - cache.insert(dedup_key, Utc::now()); - - // Cleanup old entries - if cache.len() > self.config.max_dedup_entries { - let cutoff = - Utc::now() - ChronoDuration::seconds(self.config.dedup_window_seconds as i64); - cache.retain(|_, &mut timestamp| timestamp > cutoff); - } - } - - /// Process event through aggregation rules - async fn process_aggregation_rules(&self, event: &Event) -> TliResult<()> { - let rules = self.rules.read().await; - - for rule in rules.values() { - if !rule.enabled || !rule.filter.matches(event) { - continue; - } - - self.add_event_to_window(rule, event.clone()).await?; - } - - Ok(()) - } - - /// Add event to aggregation window - async fn add_event_to_window(&self, rule: &AggregationRule, event: Event) -> TliResult<()> { - let mut windows = self.aggregation_windows.write().await; - let rule_windows = windows.entry(rule.id.clone()).or_insert_with(Vec::new); - - let event_time = event.timestamp_utc(); - let window_start = event_time - .with_second(0) - .unwrap_or(event_time) - .with_nanosecond(0) - .unwrap_or(event_time); - - // Find or create appropriate window - let mut found_window = false; - for window in rule_windows.iter_mut() { - if window.add_event(event.clone()) { - found_window = true; - break; - } - } - - // Create new window if needed - if !found_window { - let mut new_window = AggregationWindow::new(window_start, rule.window_seconds); - new_window.add_event(event); - rule_windows.push(new_window); - } - - Ok(()) - } - - /// Check event patterns for correlation - async fn check_event_patterns(&self, event: &Event) -> TliResult<()> { - let patterns = self.patterns.read().await; - let mut pattern_state = self.pattern_state.write().await; - - for pattern in patterns.values() { - // Check if event matches first step in pattern - if let Some(first_filter) = pattern.sequence.first() { - if first_filter.matches(event) { - // Start new pattern sequence - let state_key = format!("{}_{}", pattern.id, event.id); - let mut sequence = VecDeque::new(); - sequence.push_back(event.clone()); - pattern_state.insert(state_key, sequence); - continue; - } - } - - // Check existing pattern sequences - let mut completed_patterns = Vec::new(); - - for (state_key, sequence) in pattern_state.iter_mut() { - if !state_key.starts_with(&pattern.id) { - continue; - } - - let step_index = sequence.len(); - if step_index < pattern.sequence.len() { - if let Some(filter) = pattern.sequence.get(step_index) { - if filter.matches(event) { - sequence.push_back(event.clone()); - - // Check if pattern is complete - if sequence.len() == pattern.sequence.len() { - completed_patterns.push((state_key.clone(), sequence.clone())); - } - } - } - } - } - - // Execute actions for completed patterns - for (state_key, sequence) in completed_patterns { - self.execute_pattern_action(pattern, &sequence).await?; - pattern_state.remove(&state_key); - } - } - - Ok(()) - } - - /// Execute pattern action - async fn execute_pattern_action( - &self, - pattern: &EventPattern, - sequence: &VecDeque, - ) -> TliResult<()> { - match &pattern.action { - PatternAction::GenerateEvent { - event_type, - severity, - payload, - } => { - let mut correlation_event = Event::new( - event_type.clone(), - severity.clone(), - "aggregator".to_owned(), - payload.clone(), - ); - - // Add correlation metadata - correlation_event.add_metadata("pattern_id".to_owned(), pattern.id.clone()); - correlation_event.add_metadata("pattern_name".to_owned(), pattern.name.clone()); - correlation_event - .add_metadata("sequence_length".to_owned(), sequence.len().to_string()); - - if let Err(e) = self.output_sender.send(correlation_event) { - warn!("Failed to send pattern event: {}", e); - } - }, - PatternAction::SendAlert { message, severity } => { - let alert_event = Event::new( - EventType::System, - severity.clone(), - "aggregator".to_owned(), - serde_json::json!({ - "alert": true, - "message": message, - "pattern": pattern.name - }), - ); - - if let Err(e) = self.output_sender.send(alert_event) { - warn!("Failed to send alert event: {}", e); - } - }, - PatternAction::Log { level, message } => match level.as_str() { - "debug" => debug!("Pattern {}: {}", pattern.name, message), - "info" => info!("Pattern {}: {}", pattern.name, message), - "warn" => warn!("Pattern {}: {}", pattern.name, message), - "error" => error!("Pattern {}: {}", pattern.name, message), - _ => info!("Pattern {}: {}", pattern.name, message), - }, - } - - Ok(()) - } - - /// Enrich event with additional metadata - async fn enrich_event(&self, mut event: Event) -> Event { - // Add processing timestamp - event.add_metadata("processed_at".to_owned(), Utc::now().to_rfc3339()); - - // Add aggregator metadata - event.add_metadata("processed_by".to_owned(), "aggregator".to_owned()); - - event - } - - /// Window processing loop - async fn window_processing_loop(&self) { - let mut interval = interval(Duration::from_secs(10)); // Check every 10 seconds - let mut shutdown = self.shutdown_receiver.clone(); - - loop { - tokio::select! { - _ = interval.tick() => { - if let Err(e) = self.process_completed_windows().await { - error!("Window processing error: {}", e); - } - } - _ = shutdown.changed() => { - if *shutdown.borrow() { - debug!("Window processing loop shutting down"); - break; - } - } - } - } - } - - /// Process completed aggregation windows - async fn process_completed_windows(&self) -> TliResult<()> { - let current_time = Utc::now(); - let rules = self.rules.read().await; - let mut windows = self.aggregation_windows.write().await; - - for (rule_id, rule_windows) in windows.iter_mut() { - let rule = match rules.get(rule_id) { - Some(rule) => rule, - None => continue, - }; - - let mut completed_indices = Vec::new(); - - for (index, window) in rule_windows.iter_mut().enumerate() { - if window.is_complete(current_time) && !window.processed { - if window.events.len() >= rule.min_events { - if let Ok(aggregated_event) = self.aggregate_window(rule, window).await { - window.result = Some(aggregated_event.clone()); - - if let Err(e) = self.output_sender.send(aggregated_event) { - warn!("Failed to send aggregated event: {}", e); - } - } - } - - window.processed = true; - completed_indices.push(index); - } - } - - // Remove old completed windows - for &index in completed_indices.iter().rev() { - rule_windows.remove(index); - } - } - - Ok(()) - } - - /// Aggregate events in a window - async fn aggregate_window( - &self, - rule: &AggregationRule, - window: &AggregationWindow, - ) -> TliResult { - if window.events.is_empty() { - return Err(TliError::InvalidRequest("Empty window".to_owned())); - } - - let mut payload = serde_json::json!({ - "aggregation_type": format!("{:?}", rule.aggregation_type), - "window_start": window.start_time.to_rfc3339(), - "window_end": window.end_time.to_rfc3339(), - "event_count": window.events.len(), - "rule_id": rule.id, - "rule_name": rule.name - }); - - match rule.aggregation_type { - AggregationType::Count => { - payload["count"] = serde_json::json!(window.events.len()); - }, - AggregationType::Sum => { - let mut sum = 0.0; - for event in &window.events { - for field in &rule.fields { - if let Some(value) = event.payload.get(field) { - if let Some(num) = value.as_f64() { - sum += num; - } - } - } - } - payload["sum"] = serde_json::json!(sum); - }, - AggregationType::Average => { - let mut sum = 0.0; - let mut count = 0; - for event in &window.events { - for field in &rule.fields { - if let Some(value) = event.payload.get(field) { - if let Some(num) = value.as_f64() { - sum += num; - count += 1; - } - } - } - } - payload["average"] = if count > 0 { - serde_json::json!(sum / count as f64) - } else { - serde_json::json!(0.0) - }; - }, - AggregationType::First => { - if let Some(first_event) = window.events.first() { - payload["first_event"] = first_event.payload.clone(); - } - }, - AggregationType::Last => { - if let Some(last_event) = window.events.last() { - payload["last_event"] = last_event.payload.clone(); - } - }, - AggregationType::Merge => { - let mut merged = serde_json::json!({}); - for event in &window.events { - if let serde_json::Value::Object(obj) = &event.payload { - for (key, value) in obj { - merged[key] = value.clone(); - } - } - } - payload["merged"] = merged; - }, - _ => { - // Default aggregation - payload["events"] = serde_json::json!(window.events.len()); - }, - } - - let mut aggregated_event = Event::new( - rule.output_event_type.clone(), - EventSeverity::Info, - "aggregator".to_owned(), - payload, - ); - - // Add rule metadata - aggregated_event.add_metadata("aggregation_rule".to_owned(), rule.id.clone()); - - Ok(aggregated_event) - } - - /// Cleanup loop for old data - async fn cleanup_loop(&self) { - let mut interval = interval(Duration::from_secs(300)); // 5 minutes - let mut shutdown = self.shutdown_receiver.clone(); - - loop { - tokio::select! { - _ = interval.tick() => { - self.cleanup_old_data().await; - } - _ = shutdown.changed() => { - if *shutdown.borrow() { - debug!("Cleanup loop shutting down"); - break; - } - } - } - } - } - - /// Cleanup old data - async fn cleanup_old_data(&self) { - let cutoff = Utc::now() - ChronoDuration::hours(1); - - // Cleanup deduplication cache - { - let mut cache = self.dedup_cache.write().await; - cache.retain(|_, &mut timestamp| timestamp > cutoff); - } - - // Cleanup pattern state - { - let mut state = self.pattern_state.write().await; - state.retain(|_, sequence| { - if let Some(first_event) = sequence.front() { - first_event.timestamp_utc() > cutoff - } else { - false - } - }); - } - - debug!("Completed aggregator cleanup"); - } - - /// Shutdown the aggregator - pub async fn shutdown(&self) -> TliResult<()> { - info!("Shutting down event aggregator"); - - if let Err(e) = self.shutdown_sender.send(true) { - warn!("Failed to send shutdown signal: {}", e); - } - - // Process remaining events - self.process_queued_events().await?; - self.process_completed_windows().await?; - - info!("Event aggregator shutdown complete"); - Ok(()) - } -} - -impl Clone for EventAggregator { - fn clone(&self) -> Self { - Self { - config: self.config.clone(), - rules: self.rules.clone(), - dedup_cache: self.dedup_cache.clone(), - aggregation_windows: self.aggregation_windows.clone(), - patterns: self.patterns.clone(), - pattern_state: self.pattern_state.clone(), - processing_queue: self.processing_queue.clone(), - output_sender: self.output_sender.clone(), - output_receiver: self.output_receiver.clone(), - shutdown_sender: self.shutdown_sender.clone(), - shutdown_receiver: self.shutdown_receiver.clone(), - } - } -} - -#[cfg(test)] -#[allow(clippy::unwrap_used, clippy::expect_used)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_deduplication() { - let config = AggregationConfig::default(); - let aggregator = EventAggregator::new(config); - - let event1 = Event::new( - EventType::Trading, - EventSeverity::Info, - "test".to_owned(), - serde_json::json!({"id": "123"}), - ); - - let event2 = event1.clone(); - - // First event should not be duplicate - assert!(!aggregator.is_duplicate(&event1).await); - aggregator.update_dedup_cache(&event1).await; - - // Second identical event should be duplicate - assert!(aggregator.is_duplicate(&event2).await); - } - - #[test] - fn test_aggregation_window() { - let start_time = Utc::now(); - let mut window = AggregationWindow::new(start_time, 60); - - let event = Event::new( - EventType::Trading, - EventSeverity::Info, - "test".to_owned(), - serde_json::json!({}), - ); - - assert!(window.add_event(event)); - assert_eq!(window.events.len(), 1); - assert!(!window.is_complete(start_time + ChronoDuration::seconds(30))); - assert!(window.is_complete(start_time + ChronoDuration::seconds(70))); - } - - #[test] - fn test_deduplication_key() { - let event = Event::new( - EventType::Trading, - EventSeverity::Info, - "test".to_owned(), - serde_json::json!({"order_id": "123", "symbol": "AAPL"}), - ); - - let key = - DeduplicationKey::from_event(&event, &["order_id".to_owned(), "symbol".to_owned()]); - - assert_eq!(key.event_type, "trading"); - assert_eq!(key.source, "test"); - assert_eq!(key.key_fields.len(), 2); - } -} diff --git a/tli/src/events/event_buffer.rs b/tli/src/events/event_buffer.rs deleted file mode 100644 index e6ff6881c..000000000 --- a/tli/src/events/event_buffer.rs +++ /dev/null @@ -1,895 +0,0 @@ -//! Event aggregation and buffering with back-pressure handling -//! -//! This module provides memory-efficient event storage with: -//! - Circular buffer with configurable size limits -//! - Back-pressure handling and flow control -//! - Event TTL and automatic cleanup -//! - Memory usage monitoring and alerts -//! - Batch processing and compression -//! - Priority-based event handling - -use crate::error::{TliError, TliResult}; -use crate::events::{Event, EventFilter, EventSeverity}; -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, VecDeque}; -use std::sync::Arc; -use tokio::sync::{watch, RwLock, Semaphore}; -use tokio::time::{interval, Duration, Instant}; -use tracing::{debug, error, info, instrument, warn}; -use uuid::Uuid; - -/// Configuration for event buffer -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EventBufferConfig { - /// Maximum number of events to store - pub max_events: usize, - /// Maximum memory usage in bytes - pub max_memory_bytes: usize, - /// Event TTL in seconds (0 = no expiry) - pub default_ttl_seconds: u64, - /// Cleanup interval in seconds - pub cleanup_interval_seconds: u64, - /// Enable compression for stored events - pub enable_compression: bool, - /// Compression threshold in bytes - pub compression_threshold_bytes: usize, - /// Enable back-pressure when buffer is full - pub enable_backpressure: bool, - /// Back-pressure threshold (percentage of `max_events`) - pub backpressure_threshold_percent: f32, - /// Batch size for processing events - pub batch_size: usize, - /// Enable priority queue for critical events - pub enable_priority_queue: bool, - /// Memory warning threshold (percentage of `max_memory_bytes`) - pub memory_warning_threshold_percent: f32, -} - -impl Default for EventBufferConfig { - fn default() -> Self { - Self { - max_events: 100_000, - max_memory_bytes: 100 * 1024 * 1024, // 100MB - default_ttl_seconds: 3600, // 1 hour - cleanup_interval_seconds: 60, // 1 minute - enable_compression: true, - compression_threshold_bytes: 1024, // 1KB - enable_backpressure: true, - backpressure_threshold_percent: 0.8, // 80% - batch_size: 100, - enable_priority_queue: true, - memory_warning_threshold_percent: 0.9, // 90% - } - } -} - -/// Event buffer metrics for monitoring -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EventBufferMetrics { - /// Total events currently stored - pub events_stored: usize, - /// Memory usage in bytes - pub memory_usage_bytes: usize, - /// Events added since start - pub events_added: u64, - /// Events removed since start - pub events_removed: u64, - /// Events expired since start - pub events_expired: u64, - /// Events compressed since start - pub events_compressed: u64, - /// Current back-pressure status - pub backpressure_active: bool, - /// Number of times back-pressure was triggered - pub backpressure_count: u64, - /// Average event size in bytes - pub average_event_size_bytes: f64, - /// Events by type - pub events_by_type: HashMap, - /// Events by severity - pub events_by_severity: HashMap, - /// Last cleanup time - pub last_cleanup_at: Option>, - /// Buffer utilization percentage - pub utilization_percent: f32, -} - -impl Default for EventBufferMetrics { - fn default() -> Self { - Self { - events_stored: 0, - memory_usage_bytes: 0, - events_added: 0, - events_removed: 0, - events_expired: 0, - events_compressed: 0, - backpressure_active: false, - backpressure_count: 0, - average_event_size_bytes: 0.0, - events_by_type: HashMap::new(), - events_by_severity: HashMap::new(), - last_cleanup_at: None, - utilization_percent: 0.0, - } - } -} - -/// Stored event with metadata -#[derive(Debug, Clone)] -struct StoredEvent { - /// The event data - event: Event, - /// Size in bytes - size_bytes: usize, - /// Compressed payload (if compression enabled) - #[allow(dead_code)] - compressed_payload: Option>, - /// Insert timestamp - #[allow(dead_code)] - inserted_at: Instant, -} - -impl StoredEvent { - fn new(event: Event) -> Self { - let size_bytes = Self::calculate_size(&event); - Self { - event, - size_bytes, - compressed_payload: None, - inserted_at: Instant::now(), - } - } - - fn calculate_size(event: &Event) -> usize { - // Rough estimation of event size in memory - size_of::() - + event.source.len() - + event.payload.to_string().len() - + event - .metadata - .iter() - .map(|(k, v)| k.len() + v.len()) - .sum::() - } - - #[allow(dead_code)] - fn compress(&mut self) -> TliResult<()> { - if self.compressed_payload.is_some() { - return Ok(()); // Already compressed - } - - let payload_str = self.event.payload.to_string(); - if payload_str.len() < 1024 { - return Ok(()); // Too small to compress - } - - // Simple compression using flate2 (would need to add dependency) - // For now, just store as-is - self.compressed_payload = Some(payload_str.into_bytes()); - Ok(()) - } -} - -/// Priority level for events -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -enum EventPriority { - Low = 0_isize, - Normal = 1_isize, - High = 2_isize, - Critical = 3_isize, -} - -impl From for EventPriority { - fn from(severity: EventSeverity) -> Self { - match severity { - EventSeverity::Info => EventPriority::Low, - EventSeverity::Warning => EventPriority::Normal, - EventSeverity::Error => EventPriority::High, - EventSeverity::Critical => EventPriority::Critical, - } - } -} - -/// Event buffer that manages memory-efficient event storage -pub struct EventBuffer { - /// Configuration - config: EventBufferConfig, - /// Main event storage (circular buffer) - events: Arc>>, - /// Priority queue for critical events - priority_events: Arc>>, - /// Event index for fast lookups - event_index: Arc>>, - /// Buffer metrics - metrics: Arc>, - /// Back-pressure semaphore - backpressure_semaphore: Arc, - /// Shutdown signal - shutdown_sender: watch::Sender, - shutdown_receiver: watch::Receiver, -} - -impl EventBuffer { - /// Create a new event buffer - pub fn new(config: EventBufferConfig) -> Self { - let backpressure_permits = - (config.max_events as f32 * config.backpressure_threshold_percent) as usize; - let backpressure_semaphore = Arc::new(Semaphore::new(backpressure_permits)); - - let (shutdown_sender, shutdown_receiver) = watch::channel(false); - - let buffer = Self { - config, - events: Arc::new(RwLock::new(VecDeque::new())), - priority_events: Arc::new(RwLock::new(VecDeque::new())), - event_index: Arc::new(RwLock::new(HashMap::new())), - metrics: Arc::new(RwLock::new(EventBufferMetrics::default())), - backpressure_semaphore, - shutdown_sender, - shutdown_receiver, - }; - - // Start cleanup task - buffer.start_cleanup_task(); - - buffer - } - - /// Add an event to the buffer - #[instrument(skip(self, event))] - pub async fn add_event(&self, event: Event) -> TliResult<()> { - // Check back-pressure - if self.config.enable_backpressure { - let permit = self.backpressure_semaphore.try_acquire().map_err(|_| { - // Update back-pressure metrics - tokio::spawn({ - let metrics = self.metrics.clone(); - async move { - let mut m = metrics.write().await; - m.backpressure_active = true; - m.backpressure_count += 1; - } - }); - TliError::BufferFull("Event buffer back-pressure active".to_owned()) - })?; - - // Release permit after processing - std::mem::forget(permit); - } - - let stored_event = StoredEvent::new(event.clone()); - let event_id = event.id; - let priority = EventPriority::from(event.severity.clone()); - - // Determine which queue to use - let use_priority_queue = self.config.enable_priority_queue - && (priority == EventPriority::Critical || priority == EventPriority::High); - - if use_priority_queue { - // Add to priority queue - let mut priority_events = self.priority_events.write().await; - priority_events.push_back(stored_event); - - // Ensure priority queue doesn't grow too large - let max_priority_events = self.config.max_events / 10; // 10% of total - while priority_events.len() > max_priority_events { - if let Some(removed) = priority_events.pop_front() { - self.update_metrics_on_removal(&removed.event).await; - } - } - } else { - // Add to main buffer - let mut events = self.events.write().await; - let mut index = self.event_index.write().await; - - // Check if buffer is full - if events.len() >= self.config.max_events { - // Remove oldest event - if let Some(removed) = events.pop_front() { - index.remove(&removed.event.id); - self.update_metrics_on_removal(&removed.event).await; - } - } - - // Add new event - let position = events.len(); - events.push_back(stored_event); - index.insert(event_id, position); - } - - // Update metrics - self.update_metrics_on_addition(&event).await; - - // Check memory usage - self.check_memory_usage().await; - - Ok(()) - } - - /// Get events matching a filter - pub async fn get_events(&self, filter: &EventFilter, limit: Option) -> Vec { - let mut result = Vec::new(); - let max_results = limit.unwrap_or(1000); - - // Check priority events first - if self.config.enable_priority_queue { - let priority_events = self.priority_events.read().await; - for stored_event in priority_events.iter().rev() { - // Most recent first - if result.len() >= max_results { - break; - } - - if !stored_event.event.is_expired() && filter.matches(&stored_event.event) { - result.push(stored_event.event.clone()); - } - } - } - - // Check main buffer - if result.len() < max_results { - let events = self.events.read().await; - for stored_event in events.iter().rev() { - // Most recent first - if result.len() >= max_results { - break; - } - - if !stored_event.event.is_expired() && filter.matches(&stored_event.event) { - result.push(stored_event.event.clone()); - } - } - } - - result - } - - /// Get event by ID - pub async fn get_event_by_id(&self, id: &Uuid) -> Option { - // Check priority events first - if self.config.enable_priority_queue { - let priority_events = self.priority_events.read().await; - for stored_event in priority_events.iter() { - if stored_event.event.id == *id && !stored_event.event.is_expired() { - return Some(stored_event.event.clone()); - } - } - } - - // Check main buffer - let index = self.event_index.read().await; - if let Some(&position) = index.get(id) { - let events = self.events.read().await; - if let Some(stored_event) = events.get(position) { - if !stored_event.event.is_expired() { - return Some(stored_event.event.clone()); - } - } - } - - None - } - - /// Get events in a time range - pub async fn get_events_in_range( - &self, - start_time_nanos: i64, - end_time_nanos: i64, - limit: Option, - ) -> Vec { - let filter = EventFilter { - start_time_nanos: Some(start_time_nanos), - end_time_nanos: Some(end_time_nanos), - ..EventFilter::all() - }; - - self.get_events(&filter, limit).await - } - - /// Get buffer metrics - pub async fn get_metrics(&self) -> EventBufferMetrics { - self.metrics.read().await.clone() - } - - /// Manually trigger cleanup - pub async fn cleanup(&self) -> TliResult<()> { - let mut expired_count = 0; - let mut memory_freed = 0; - - // Clean priority events - if self.config.enable_priority_queue { - let mut priority_events = self.priority_events.write().await; - let original_len = priority_events.len(); - - priority_events.retain(|stored_event| { - let expired = stored_event.event.is_expired(); - if expired { - memory_freed += stored_event.size_bytes; - } - !expired - }); - - expired_count += original_len - priority_events.len(); - } - - // Clean main buffer - { - let mut events = self.events.write().await; - let mut index = self.event_index.write().await; - let original_len = events.len(); - - let mut retained_events = VecDeque::new(); - let mut new_index = HashMap::new(); - - for stored_event in events.drain(..) { - if !stored_event.event.is_expired() { - let new_pos = retained_events.len(); - let event_id = stored_event.event.id; - retained_events.push_back(stored_event); - new_index.insert(event_id, new_pos); - } else { - memory_freed += stored_event.size_bytes; - } - } - - *events = retained_events; - *index = new_index; - - expired_count += original_len - events.len(); - } - - // Update metrics - { - let mut metrics = self.metrics.write().await; - metrics.events_expired += expired_count as u64; - metrics.memory_usage_bytes = metrics.memory_usage_bytes.saturating_sub(memory_freed); - metrics.last_cleanup_at = Some(Utc::now()); - metrics.events_stored = metrics.events_stored.saturating_sub(expired_count); - - // Update utilization - metrics.utilization_percent = - (metrics.events_stored as f32 / self.config.max_events as f32) * 100.0; - } - - if expired_count > 0 { - debug!( - "Cleaned up {} expired events, freed {} bytes", - expired_count, memory_freed - ); - } - - Ok(()) - } - - /// Clear all events from buffer - pub async fn clear(&self) -> TliResult<()> { - { - let mut events = self.events.write().await; - let mut priority_events = self.priority_events.write().await; - let mut index = self.event_index.write().await; - - events.clear(); - priority_events.clear(); - index.clear(); - } - - // Reset metrics - { - let mut metrics = self.metrics.write().await; - *metrics = EventBufferMetrics::default(); - } - - info!("Event buffer cleared"); - Ok(()) - } - - /// Start the cleanup task - fn start_cleanup_task(&self) { - let buffer = self.clone(); - tokio::spawn(async move { - let mut interval = - interval(Duration::from_secs(buffer.config.cleanup_interval_seconds)); - let mut shutdown = buffer.shutdown_receiver.clone(); - - loop { - tokio::select! { - _ = interval.tick() => { - if let Err(e) = buffer.cleanup().await { - error!("Cleanup task error: {}", e); - } - } - _ = shutdown.changed() => { - if *shutdown.borrow() { - debug!("Cleanup task shutting down"); - break; - } - } - } - } - }); - } - - /// Update metrics when adding an event - async fn update_metrics_on_addition(&self, event: &Event) { - let mut metrics = self.metrics.write().await; - metrics.events_added += 1; - metrics.events_stored += 1; - - let event_size = StoredEvent::calculate_size(event); - metrics.memory_usage_bytes += event_size; - - // Update average size - metrics.average_event_size_bytes = (metrics.average_event_size_bytes - * (metrics.events_added - 1) as f64 - + event_size as f64) - / metrics.events_added as f64; - - // Update type counts - let type_key = event.event_type.as_str().to_owned(); - *metrics.events_by_type.entry(type_key).or_insert(0) += 1; - - // Update severity counts - let severity_key = match event.severity { - EventSeverity::Info => "info", - EventSeverity::Warning => "warning", - EventSeverity::Error => "error", - EventSeverity::Critical => "critical", - } - .to_owned(); - *metrics.events_by_severity.entry(severity_key).or_insert(0) += 1; - - // Update utilization - metrics.utilization_percent = - (metrics.events_stored as f32 / self.config.max_events as f32) * 100.0; - - // Reset back-pressure if no longer needed - if metrics.backpressure_active && metrics.utilization_percent < 70.0 { - metrics.backpressure_active = false; - } - } - - /// Update metrics when removing an event - async fn update_metrics_on_removal(&self, event: &Event) { - let mut metrics = self.metrics.write().await; - metrics.events_removed += 1; - metrics.events_stored = metrics.events_stored.saturating_sub(1); - - let event_size = StoredEvent::calculate_size(event); - metrics.memory_usage_bytes = metrics.memory_usage_bytes.saturating_sub(event_size); - - // Update type counts - let type_key = event.event_type.as_str().to_owned(); - if let Some(count) = metrics.events_by_type.get_mut(&type_key) { - *count = count.saturating_sub(1); - } - - // Update severity counts - let severity_key = match event.severity { - EventSeverity::Info => "info", - EventSeverity::Warning => "warning", - EventSeverity::Error => "error", - EventSeverity::Critical => "critical", - } - .to_owned(); - if let Some(count) = metrics.events_by_severity.get_mut(&severity_key) { - *count = count.saturating_sub(1); - } - - // Update utilization - metrics.utilization_percent = - (metrics.events_stored as f32 / self.config.max_events as f32) * 100.0; - } - - /// Check memory usage and trigger warnings - async fn check_memory_usage(&self) { - let metrics = self.metrics.read().await; - let usage_percent = - (metrics.memory_usage_bytes as f32 / self.config.max_memory_bytes as f32) * 100.0; - - if usage_percent > self.config.memory_warning_threshold_percent * 100.0 { - warn!( - "Event buffer memory usage high: {:.1}% ({} bytes)", - usage_percent, metrics.memory_usage_bytes - ); - } - } - - /// Shutdown the buffer - pub async fn shutdown(&self) -> TliResult<()> { - info!("Shutting down event buffer"); - - if let Err(e) = self.shutdown_sender.send(true) { - warn!("Failed to send shutdown signal: {}", e); - } - - // Final cleanup - self.cleanup().await?; - - info!("Event buffer shutdown complete"); - Ok(()) - } -} - -impl Clone for EventBuffer { - fn clone(&self) -> Self { - Self { - config: self.config.clone(), - events: self.events.clone(), - priority_events: self.priority_events.clone(), - event_index: self.event_index.clone(), - metrics: self.metrics.clone(), - backpressure_semaphore: self.backpressure_semaphore.clone(), - shutdown_sender: self.shutdown_sender.clone(), - shutdown_receiver: self.shutdown_receiver.clone(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::events::{Event, EventFilter, EventSeverity, EventType}; - - #[tokio::test] - async fn test_event_buffer_basic_operations() { - let config = EventBufferConfig { - max_events: 10, - ..EventBufferConfig::default() - }; - let buffer = EventBuffer::new(config); - - // Add some events - for i in 0..5 { - let event = Event::new( - EventType::Trading, - EventSeverity::Info, - "test".to_owned(), - serde_json::json!({"index": i}), - ); - buffer.add_event(event).await.unwrap(); - } - - // Check metrics - let metrics = buffer.get_metrics().await; - assert_eq!(metrics.events_stored, 5); - assert_eq!(metrics.events_added, 5); - - // Get all events - let events = buffer.get_events(&EventFilter::all(), None).await; - assert_eq!(events.len(), 5); - } - - #[tokio::test] - async fn test_event_buffer_overflow() { - let config = EventBufferConfig { - max_events: 3, - enable_backpressure: false, // Disable backpressure for overflow test - ..EventBufferConfig::default() - }; - let buffer = EventBuffer::new(config); - - // Add more events than the limit - for i in 0..5 { - let event = Event::new( - EventType::Trading, - EventSeverity::Info, - "test".to_owned(), - serde_json::json!({"index": i}), - ); - buffer.add_event(event).await.unwrap(); - } - - // Should only have max_events - let metrics = buffer.get_metrics().await; - assert_eq!(metrics.events_stored, 3); - assert_eq!(metrics.events_added, 5); - assert_eq!(metrics.events_removed, 2); - } - - #[tokio::test] - async fn test_event_filter() { - let buffer = EventBuffer::new(EventBufferConfig::default()); - - // Add events of different types - let trading_event = Event::new( - EventType::Trading, - EventSeverity::Info, - "test".to_owned(), - serde_json::json!({}), - ); - let market_event = Event::new( - EventType::MarketData, - EventSeverity::Warning, - "test".to_owned(), - serde_json::json!({}), - ); - - buffer.add_event(trading_event).await.unwrap(); - buffer.add_event(market_event).await.unwrap(); - - // Filter by type - let trading_filter = EventFilter::for_types(vec![EventType::Trading]); - let trading_events = buffer.get_events(&trading_filter, None).await; - assert_eq!(trading_events.len(), 1); - - // Filter by severity - let warning_filter = EventFilter::with_min_severity(EventSeverity::Warning); - let warning_events = buffer.get_events(&warning_filter, None).await; - assert_eq!(warning_events.len(), 1); - } - - #[tokio::test] - async fn test_event_buffer_priority_queue() { - let config = EventBufferConfig { - max_events: 10, - enable_priority_queue: true, - ..EventBufferConfig::default() - }; - let buffer = EventBuffer::new(config); - - // Add critical event - let critical_event = Event::new( - EventType::System, - EventSeverity::Critical, - "test".to_owned(), - serde_json::json!({"message": "critical"}), - ); - - // Add normal events - let normal_event = Event::new( - EventType::Trading, - EventSeverity::Info, - "test".to_owned(), - serde_json::json!({"message": "normal"}), - ); - - buffer.add_event(normal_event).await.unwrap(); - buffer.add_event(critical_event.clone()).await.unwrap(); - - // Priority events should be retrievable - let all_events = buffer.get_events(&EventFilter::all(), None).await; - assert_eq!(all_events.len(), 2); - } - - #[tokio::test] - async fn test_event_buffer_cleanup() { - let config = EventBufferConfig { - max_events: 10, - cleanup_interval_seconds: 1, - ..EventBufferConfig::default() - }; - let buffer = EventBuffer::new(config); - - // Add events - for i in 0..3 { - let mut event = Event::new( - EventType::Trading, - EventSeverity::Info, - "test".to_owned(), - serde_json::json!({"index": i}), - ); - // Set very short TTL for testing - event.set_ttl(1); // 1 second TTL - buffer.add_event(event).await.unwrap(); - } - - let metrics_before = buffer.get_metrics().await; - assert_eq!(metrics_before.events_stored, 3); - - // Wait for events to expire - tokio::time::sleep(Duration::from_secs(2)).await; - - // Manually trigger cleanup - buffer.cleanup().await.unwrap(); - - let metrics_after = buffer.get_metrics().await; - assert_eq!(metrics_after.events_stored, 0); - assert_eq!(metrics_after.events_expired, 3); - } - - #[tokio::test] - async fn test_event_buffer_by_id() { - let buffer = EventBuffer::new(EventBufferConfig::default()); - - let event = Event::new( - EventType::Trading, - EventSeverity::Info, - "test".to_owned(), - serde_json::json!({"test": "data"}), - ); - let event_id = event.id; - - buffer.add_event(event).await.unwrap(); - - // Get by ID - let retrieved = buffer.get_event_by_id(&event_id).await; - assert!(retrieved.is_some()); - assert_eq!(retrieved.unwrap().id, event_id); - - // Non-existent ID - let non_existent = buffer.get_event_by_id(&Uuid::new_v4()).await; - assert!(non_existent.is_none()); - } - - #[tokio::test] - async fn test_event_buffer_time_range() { - let buffer = EventBuffer::new(EventBufferConfig::default()); - - let start_time = crate::types::current_unix_nanos(); - - // Add events with small delay - for i in 0..3 { - let event = Event::new( - EventType::Trading, - EventSeverity::Info, - "test".to_owned(), - serde_json::json!({"index": i}), - ); - buffer.add_event(event).await.unwrap(); - tokio::time::sleep(Duration::from_millis(10)).await; - } - - let end_time = crate::types::current_unix_nanos(); - - // Get events in range - let events = buffer.get_events_in_range(start_time, end_time, None).await; - assert_eq!(events.len(), 3); - } - - #[tokio::test] - async fn test_event_buffer_clear() { - let buffer = EventBuffer::new(EventBufferConfig::default()); - - // Add events - for i in 0..5 { - let event = Event::new( - EventType::Trading, - EventSeverity::Info, - "test".to_owned(), - serde_json::json!({"index": i}), - ); - buffer.add_event(event).await.unwrap(); - } - - let metrics_before = buffer.get_metrics().await; - assert_eq!(metrics_before.events_stored, 5); - - // Clear buffer - buffer.clear().await.unwrap(); - - let metrics_after = buffer.get_metrics().await; - assert_eq!(metrics_after.events_stored, 0); - assert_eq!(metrics_after.events_added, 0); - } - - #[tokio::test] - async fn test_event_buffer_backpressure() { - let config = EventBufferConfig { - max_events: 10, - enable_backpressure: true, - backpressure_threshold_percent: 0.5, // 50% - ..EventBufferConfig::default() - }; - let buffer = EventBuffer::new(config); - - // Fill buffer to trigger backpressure - for i in 0..6 { - // More than 50% of max - let event = Event::new( - EventType::Trading, - EventSeverity::Info, - "test".to_owned(), - serde_json::json!({"index": i}), - ); - let result = buffer.add_event(event).await; - // First 5 should succeed, 6th might fail due to backpressure - if i < 5 { - result.unwrap(); - } - } - - let metrics = buffer.get_metrics().await; - assert!(metrics.events_stored >= 5); - } -} diff --git a/tli/src/events/mod.rs b/tli/src/events/mod.rs deleted file mode 100644 index 2d6841a4c..000000000 --- a/tli/src/events/mod.rs +++ /dev/null @@ -1,603 +0,0 @@ -//! Real-time event streaming system for TLI -//! -//! This module provides comprehensive event handling for live data including: -//! - gRPC streaming client management with automatic reconnection -//! - Event aggregation and buffering with back-pressure handling -//! - Event replay capabilities for historical analysis -//! - WebSocket support for browser clients -//! - Memory-efficient event storage and deduplication -//! - Performance metrics and monitoring -//! -//! Architecture: -//! ```text -//! gRPC Services → StreamManager → EventBuffer → Aggregator → [WebSocket|Replay] -//! ↓ ↓ ↓ -//! Reconnection Back-pressure Deduplication -//! Exponential Memory Mgmt Ordering -//! Backoff Flow Control Metrics -//! ``` - -pub mod aggregator; -pub mod event_buffer; -pub mod stream_manager; -// pub mod replay_system; // Disabled - client should not have database dependencies -// websocket_server module removed - TLI is pure client - -// NO RE-EXPORTS: Import directly from submodules -// Use tli::events::aggregator::{EventAggregator, AggregationConfig} instead -// Use tli::events::event_buffer::{EventBuffer, EventBufferConfig} instead -// Use tli::events::stream_manager::{StreamManager, StreamConfig} instead - -use crate::error::TliResult; -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::{broadcast, mpsc, RwLock}; -// BroadcastStream is now available with tokio-stream sync feature enabled -use tracing::{debug, error, info, warn}; -use uuid::Uuid; - -// Re-export main components for convenience -// These are commonly needed types that examples and client code use frequently -pub use aggregator::{ - AggregationConfig, AggregationRule, AggregationType, EventAggregator, EventPattern, - PatternAction, -}; -pub use event_buffer::{EventBuffer, EventBufferConfig, EventBufferMetrics}; -pub use stream_manager::{StreamConfig, StreamConnection, StreamHealth, StreamManager}; -// pub use replay_system::{ReplaySystem, ReplayConfig, ReplayFilter}; // Disabled -// WebSocketServer removed - TLI is pure client, no server components - -/// Event types supported by the streaming system -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum EventType { - /// Market data events (quotes, trades, order book) - MarketData, - /// Trading events (orders, executions, positions) - Trading, - /// Risk management events (limits, breaches, alerts) - Risk, - /// ML signals and predictions - MlSignal, - /// System health and monitoring - System, - /// Configuration changes - Config, - /// Custom user-defined events - Custom(String), -} - -impl EventType { - /// Convert to string for serialization - pub fn as_str(&self) -> &str { - match self { - EventType::MarketData => "market_data", - EventType::Trading => "trading", - EventType::Risk => "risk", - EventType::MlSignal => "ml_signal", - EventType::System => "system", - EventType::Config => "config", - EventType::Custom(name) => name, - } - } - - /// Parse from string - pub fn from_str(s: &str) -> Self { - match s { - "market_data" => EventType::MarketData, - "trading" => EventType::Trading, - "risk" => EventType::Risk, - "ml_signal" => EventType::MlSignal, - "system" => EventType::System, - "config" => EventType::Config, - name => EventType::Custom(name.to_owned()), - } - } -} - -/// Event severity levels for filtering and prioritization -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -pub enum EventSeverity { - /// Low priority informational events - Info, - /// Warning events that may require attention - Warning, - /// Error events that require immediate attention - Error, - /// Critical events that require urgent action - Critical, -} - -/// Core event structure for all streaming data -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Event { - /// Unique event identifier - pub id: Uuid, - /// Event type classification - pub event_type: EventType, - /// Event severity level - pub severity: EventSeverity, - /// Source service that generated the event - pub source: String, - /// Event timestamp (nanoseconds since Unix epoch) - pub timestamp_nanos: i64, - /// Sequence number for ordering within source - pub sequence: u64, - /// Event payload as JSON value - pub payload: serde_json::Value, - /// Optional correlation ID for related events - pub correlation_id: Option, - /// Event metadata and labels - pub metadata: HashMap, - /// TTL in seconds (0 = no expiry) - pub ttl_seconds: u64, -} - -impl Event { - /// Create a new event with required fields - pub fn new( - event_type: EventType, - severity: EventSeverity, - source: String, - payload: serde_json::Value, - ) -> Self { - Self { - id: Uuid::new_v4(), - event_type, - severity, - source, - timestamp_nanos: crate::types::current_unix_nanos(), - sequence: 0_u64, // Set by stream manager - payload, - correlation_id: None, - metadata: HashMap::new(), - ttl_seconds: 3600_u64, // 1 hour default TTL - } - } - - /// Create a new event with correlation ID - pub fn with_correlation( - event_type: EventType, - severity: EventSeverity, - source: String, - payload: serde_json::Value, - correlation_id: Uuid, - ) -> Self { - let mut event = Self::new(event_type, severity, source, payload); - event.correlation_id = Some(correlation_id); - event - } - - /// Set sequence number (called by stream manager) - pub fn set_sequence(&mut self, sequence: u64) { - self.sequence = sequence; - } - - /// Add metadata label - pub fn add_metadata(&mut self, key: String, value: String) { - self.metadata.insert(key, value); - } - - /// Set TTL in seconds - pub fn set_ttl(&mut self, ttl_seconds: u64) { - self.ttl_seconds = ttl_seconds; - } - - /// Check if event has expired - pub fn is_expired(&self) -> bool { - if self.ttl_seconds == 0 { - return false; // No expiry - } - - let current_nanos = crate::types::current_unix_nanos(); - let expiry_nanos = self.timestamp_nanos + (self.ttl_seconds as i64 * 1_000_000_000); - current_nanos > expiry_nanos - } - - /// Get event age in milliseconds - pub fn age_millis(&self) -> i64 { - let current_nanos = crate::types::current_unix_nanos(); - (current_nanos - self.timestamp_nanos) / 1_000_000 - } - - /// Convert to `DateTime` for display - pub fn timestamp_utc(&self) -> DateTime { - let secs = self.timestamp_nanos / 1_000_000_000; - let nanos = (self.timestamp_nanos % 1_000_000_000) as u32; - DateTime::from_timestamp(secs, nanos).unwrap_or_else(Utc::now) - } -} - -/// Event stream subscription filter -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EventFilter { - /// Event types to include (empty = all types) - pub event_types: Vec, - /// Minimum severity level - pub min_severity: EventSeverity, - /// Source services to include (empty = all sources) - pub sources: Vec, - /// Metadata filters (key-value pairs that must match) - pub metadata_filters: HashMap, - /// Correlation ID filter - pub correlation_id: Option, - /// Time range filter (start timestamp in nanos) - pub start_time_nanos: Option, - /// Time range filter (end timestamp in nanos) - pub end_time_nanos: Option, -} - -impl EventFilter { - /// Create a filter for all events - pub fn all() -> Self { - Self { - event_types: Vec::new(), - min_severity: EventSeverity::Info, - sources: Vec::new(), - metadata_filters: HashMap::new(), - correlation_id: None, - start_time_nanos: None, - end_time_nanos: None, - } - } - - /// Create a filter for specific event types - pub fn for_types(event_types: Vec) -> Self { - Self { - event_types, - ..Self::all() - } - } - - /// Create a filter for specific sources - pub fn for_sources(sources: Vec) -> Self { - Self { - sources, - ..Self::all() - } - } - - /// Create a filter for minimum severity - pub fn with_min_severity(min_severity: EventSeverity) -> Self { - Self { - min_severity, - ..Self::all() - } - } - - /// Check if event matches this filter - pub fn matches(&self, event: &Event) -> bool { - // Check event types - if !self.event_types.is_empty() && !self.event_types.contains(&event.event_type) { - return false; - } - - // Check severity - if event.severity < self.min_severity { - return false; - } - - // Check sources - if !self.sources.is_empty() && !self.sources.contains(&event.source) { - return false; - } - - // Check correlation ID - if let Some(filter_correlation_id) = &self.correlation_id { - if event.correlation_id.as_ref() != Some(filter_correlation_id) { - return false; - } - } - - // Check metadata filters - for (key, value) in &self.metadata_filters { - if event.metadata.get(key) != Some(value) { - return false; - } - } - - // Check time range - if let Some(start_time) = self.start_time_nanos { - if event.timestamp_nanos < start_time { - return false; - } - } - - if let Some(end_time) = self.end_time_nanos { - if event.timestamp_nanos > end_time { - return false; - } - } - - true - } -} - -/// Event subscription handle for managing live event streams -pub struct EventSubscription { - /// Subscription ID - pub id: Uuid, - /// Event filter - pub filter: EventFilter, - /// Event receiver - pub receiver: mpsc::UnboundedReceiver, - /// Subscription metadata - pub metadata: HashMap, -} - -impl EventSubscription { - /// Create a new subscription - pub fn new(filter: EventFilter, receiver: mpsc::UnboundedReceiver) -> Self { - Self { - id: Uuid::new_v4(), - filter, - receiver, - metadata: HashMap::new(), - } - } - - /// Add subscription metadata - pub fn add_metadata(&mut self, key: String, value: String) { - self.metadata.insert(key, value); - } -} - -/// Core event streaming system that coordinates all components -pub struct EventStreamingSystem { - /// Stream manager for gRPC connections - stream_manager: Arc, - /// Event buffer for aggregation and storage - event_buffer: Arc, - /// Event aggregator for processing - aggregator: Arc, - // replay_system: Arc, // Disabled - client should not have database dependencies - /// WebSocket server removed - TLI is pure client, no server components - /// - /// Event broadcast channel for live subscriptions - _event_sender: broadcast::Sender, - /// System shutdown signal - shutdown_sender: tokio::sync::watch::Sender, - shutdown_receiver: tokio::sync::watch::Receiver, - /// System metrics - metrics: Arc>, -} - -/// System-wide event streaming metrics -#[derive(Debug, Default, Clone)] -pub struct EventSystemMetrics { - /// Total events processed - pub events_processed: u64, - /// Events processed per second - pub events_per_second: f64, - /// Total active subscriptions - pub active_subscriptions: u64, - /// Stream connection health - pub stream_health: HashMap, - /// Memory usage in bytes - pub memory_usage_bytes: u64, - /// Last update timestamp - pub last_updated: DateTime, -} - -impl EventStreamingSystem { - /// Create a new event streaming system - pub async fn new( - stream_config: StreamConfig, - buffer_config: EventBufferConfig, - aggregation_config: AggregationConfig, - // replay_config: ReplayConfig, // Disabled - // websocket_config removed - TLI is pure client - ) -> TliResult { - info!("Initializing event streaming system"); - - // Create broadcast channel for live events - let (_event_sender, _) = broadcast::channel(10000); - - // Create shutdown channel - let (shutdown_sender, shutdown_receiver) = tokio::sync::watch::channel(false); - - // Initialize components - let stream_manager = Arc::new(StreamManager::new(stream_config).await?); - let event_buffer = Arc::new(EventBuffer::new(buffer_config)); - let aggregator = Arc::new(EventAggregator::new(aggregation_config)); - // let replay_system = Arc::new(ReplaySystem::new(replay_config).await?); // Disabled - - // WebSocket server initialization removed - TLI is pure client - - let metrics = Arc::new(RwLock::new(EventSystemMetrics::default())); - - Ok(Self { - stream_manager, - event_buffer, - aggregator, - // replay_system, // Disabled - // websocket_server removed - TLI is pure client - _event_sender, - shutdown_sender, - shutdown_receiver, - metrics, - }) - } - - /// Start the event streaming system - pub async fn start(&self) -> TliResult<()> { - info!("Starting event streaming system"); - - // Start stream manager - let stream_manager = self.stream_manager.clone(); - let _event_sender = self._event_sender.clone(); - let shutdown_receiver = self.shutdown_receiver.clone(); - - tokio::spawn(async move { - if let Err(e) = stream_manager.start(_event_sender, shutdown_receiver).await { - error!("Stream manager error: {}", e); - } - }); - - // Start event buffer processing - let buffer = self.event_buffer.clone(); - let aggregator = self.aggregator.clone(); - let mut event_receiver = self._event_sender.subscribe(); - let shutdown_receiver = self.shutdown_receiver.clone(); - - tokio::spawn(async move { - let mut shutdown = shutdown_receiver.clone(); - loop { - tokio::select! { - event_result = event_receiver.recv() => { - match event_result { - Ok(event) => { - if let Err(e) = buffer.add_event(event.clone()).await { - error!("Failed to add event to buffer: {}", e); - continue; - } - - if let Err(e) = aggregator.process_event(event).await { - error!("Failed to process event in aggregator: {}", e); - } - } - Err(broadcast::error::RecvError::Lagged(skipped)) => { - warn!("Event receiver lagged, skipped {} events", skipped); - } - Err(broadcast::error::RecvError::Closed) => { - debug!("Event receiver closed"); - break; - } - } - } - _ = shutdown.changed() => { - if *shutdown.borrow() { - debug!("Event buffer processing shutdown"); - break; - } - } - } - } - }); - - // WebSocket server startup removed - TLI is pure client, no server components - - // Start metrics collection - let metrics = self.metrics.clone(); - let shutdown_receiver = self.shutdown_receiver.clone(); - - tokio::spawn(async move { - let mut interval = tokio::time::interval(std::time::Duration::from_secs(5)); - let mut shutdown = shutdown_receiver.clone(); - - loop { - tokio::select! { - _ = interval.tick() => { - let mut metrics_guard = metrics.write().await; - metrics_guard.last_updated = Utc::now(); - // Update other metrics here - } - _ = shutdown.changed() => { - if *shutdown.borrow() { - debug!("Metrics collection shutdown"); - break; - } - } - } - } - }); - - info!("Event streaming system started successfully"); - Ok(()) - } - - /// Subscribe to events with a filter - pub async fn subscribe(&self, filter: EventFilter) -> TliResult { - let (sender, receiver) = mpsc::unbounded_channel(); - let mut event_receiver = self._event_sender.subscribe(); - let filter_clone = filter.clone(); - - tokio::spawn(async move { - while let Ok(event) = event_receiver.recv().await { - if filter_clone.matches(&event) && sender.send(event).is_err() { - debug!("Event subscription receiver dropped"); - break; - } - } - }); - - // Update subscription count - { - let mut metrics = self.metrics.write().await; - metrics.active_subscriptions += 1; - } - - Ok(EventSubscription::new(filter, receiver)) - } - - /// Get system metrics - pub async fn get_metrics(&self) -> EventSystemMetrics { - (*self.metrics.read().await).clone() - } - - /// Shutdown the event streaming system - pub async fn shutdown(&self) -> TliResult<()> { - info!("Shutting down event streaming system"); - - if let Err(e) = self.shutdown_sender.send(true) { - warn!("Failed to send shutdown signal: {}", e); - } - - // Give components time to shutdown gracefully - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - - info!("Event streaming system shutdown complete"); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_event_creation() { - let payload = serde_json::json!({"test": "data"}); - let event = Event::new( - EventType::Trading, - EventSeverity::Info, - "test_service".to_owned(), - payload, - ); - - assert_eq!(event.event_type, EventType::Trading); - assert_eq!(event.severity, EventSeverity::Info); - assert_eq!(event.source, "test_service"); - assert!(!event.is_expired()); - } - - #[test] - fn test_event_filter() { - let filter = EventFilter::for_types(vec![EventType::Trading]); - - let trading_event = Event::new( - EventType::Trading, - EventSeverity::Info, - "service".to_owned(), - serde_json::json!({}), - ); - - let market_event = Event::new( - EventType::MarketData, - EventSeverity::Info, - "service".to_owned(), - serde_json::json!({}), - ); - - assert!(filter.matches(&trading_event)); - assert!(!filter.matches(&market_event)); - } - - #[test] - fn test_event_severity_ordering() { - assert!(EventSeverity::Critical > EventSeverity::Error); - assert!(EventSeverity::Error > EventSeverity::Warning); - assert!(EventSeverity::Warning > EventSeverity::Info); - } -} diff --git a/tli/src/events/stream_manager.rs b/tli/src/events/stream_manager.rs deleted file mode 100644 index 50566fc67..000000000 --- a/tli/src/events/stream_manager.rs +++ /dev/null @@ -1,867 +0,0 @@ -//! gRPC streaming client management with automatic reconnection -//! -//! This module handles multiple concurrent gRPC streams with: -//! - Automatic reconnection with exponential backoff -//! - Stream health monitoring and metrics -//! - Back-pressure handling and flow control -//! - Connection pooling and load balancing -//! - Circuit breaker pattern for failed connections - -use crate::client::ServiceEndpoints; -use crate::error::{TliError, TliResult}; -use crate::events::{Event, EventSeverity, EventType}; -use crate::proto::trading::{ - trading_service_client::TradingServiceClient, MetricsEvent, SubscribeMetricsRequest, - SubscribeSystemStatusRequest, SystemStatusEvent, -}; -use chrono::{DateTime, Utc}; -use futures_util::StreamExt; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::sync::{broadcast, RwLock, Semaphore}; -use tonic::transport::{Channel, Endpoint}; -use tonic::{Request, Streaming}; -use tracing::{error, info, instrument, warn}; -use uuid::Uuid; - -/// Configuration for stream manager -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct StreamConfig { - /// Service endpoints to connect to - pub endpoints: ServiceEndpoints, - /// Maximum concurrent streams per service - pub max_concurrent_streams: usize, - /// Initial reconnection delay in milliseconds - pub initial_reconnect_delay_ms: u64, - /// Maximum reconnection delay in milliseconds - pub max_reconnect_delay_ms: u64, - /// Exponential backoff multiplier - pub backoff_multiplier: f64, - /// Maximum number of reconnection attempts (0 = infinite) - pub max_reconnect_attempts: u32, - /// Stream keepalive interval in seconds - pub keepalive_interval_secs: u64, - /// Connection timeout in seconds - pub connection_timeout_secs: u64, - /// Stream request timeout in seconds - pub stream_timeout_secs: u64, - /// Enable circuit breaker pattern - pub enable_circuit_breaker: bool, - /// Circuit breaker failure threshold - pub circuit_breaker_threshold: u32, - /// Circuit breaker recovery timeout in seconds - pub circuit_breaker_recovery_secs: u64, -} - -impl Default for StreamConfig { - fn default() -> Self { - Self { - endpoints: ServiceEndpoints::default(), - max_concurrent_streams: 10_usize, - initial_reconnect_delay_ms: 1000_u64, - max_reconnect_delay_ms: 30000_u64, - backoff_multiplier: 2.0, - max_reconnect_attempts: 0_u32, // Infinite retries - keepalive_interval_secs: 30_u64, - connection_timeout_secs: 10_u64, - stream_timeout_secs: 60_u64, - enable_circuit_breaker: true, - circuit_breaker_threshold: 5_u32, - circuit_breaker_recovery_secs: 60_u64, - } - } -} - -/// Stream health status -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum StreamHealth { - /// Stream is healthy and connected - Healthy, - /// Stream is connecting - Connecting, - /// Stream is reconnecting after failure - Reconnecting, - /// Stream has failed and stopped reconnecting - Failed, - /// Stream is disabled by circuit breaker - CircuitBreakerOpen, -} - -/// Individual stream connection information -#[derive(Debug, Clone)] -pub struct StreamConnection { - /// Unique stream ID - pub id: Uuid, - /// Service name - pub service: String, - /// Service endpoint URL - pub endpoint: String, - /// Current health status - pub health: StreamHealth, - /// Connection start time - pub connected_at: Option>, - /// Last successful message time - pub last_message_at: Option>, - /// Number of reconnection attempts - pub reconnect_attempts: u32, - /// Next reconnection time - pub next_reconnect_at: Option>, - /// Total messages received - pub messages_received: u64, - /// Total bytes received - pub bytes_received: u64, - /// Last error message - pub last_error: Option, -} - -impl StreamConnection { - fn new(service: String, endpoint: String) -> Self { - Self { - id: Uuid::new_v4(), - service, - endpoint, - health: StreamHealth::Connecting, - connected_at: None, - last_message_at: None, - reconnect_attempts: 0_u32, - next_reconnect_at: None, - messages_received: 0_u64, - bytes_received: 0_u64, - last_error: None, - } - } -} - -/// Circuit breaker for managing failed connections -#[derive(Debug)] -struct CircuitBreaker { - /// Number of consecutive failures - failure_count: u32, - /// Failure threshold before opening circuit - threshold: u32, - /// Time when circuit was opened - opened_at: Option, - /// Recovery timeout duration - recovery_timeout: Duration, - /// Current circuit state - is_open: bool, -} - -impl CircuitBreaker { - const fn new(threshold: u32, recovery_timeout: Duration) -> Self { - Self { - failure_count: 0_u32, - threshold, - opened_at: None, - recovery_timeout, - is_open: false, - } - } - - fn record_success(&mut self) { - self.failure_count = 0; - self.is_open = false; - self.opened_at = None; - } - - fn record_failure(&mut self) { - self.failure_count += 1; - if self.failure_count >= self.threshold { - self.is_open = true; - self.opened_at = Some(Instant::now()); - } - } - - fn can_attempt(&self) -> bool { - if !self.is_open { - return true; - } - - if let Some(opened_at) = self.opened_at { - opened_at.elapsed() >= self.recovery_timeout - } else { - true - } - } - - #[allow(dead_code)] - const fn is_circuit_open(&self) -> bool { - self.is_open - } -} - -/// Main stream manager that handles all gRPC streaming connections -pub struct StreamManager { - /// Configuration - config: StreamConfig, - /// Active stream connections - connections: Arc>>, - /// Circuit breakers per service - circuit_breakers: Arc>>, - /// Concurrency limiter - concurrency_limiter: Arc, - /// Sequence counter for events - sequence_counter: Arc>, -} - -impl StreamManager { - /// Create a new stream manager - pub async fn new(config: StreamConfig) -> TliResult { - let concurrency_limiter = Arc::new(Semaphore::new(config.max_concurrent_streams)); - - Ok(Self { - config, - connections: Arc::new(RwLock::new(HashMap::new())), - circuit_breakers: Arc::new(RwLock::new(HashMap::new())), - concurrency_limiter, - sequence_counter: Arc::new(RwLock::new(0)), - }) - } - - /// Start streaming from all configured services - #[instrument(skip(self, _event_sender, shutdown_receiver))] - pub async fn start( - &self, - _event_sender: broadcast::Sender, - mut shutdown_receiver: tokio::sync::watch::Receiver, - ) -> TliResult<()> { - info!("Starting stream manager"); - - // Initialize circuit breakers - { - let mut breakers = self.circuit_breakers.write().await; - let recovery_timeout = Duration::from_secs(self.config.circuit_breaker_recovery_secs); - - breakers.insert( - "trading".to_owned(), - CircuitBreaker::new(self.config.circuit_breaker_threshold, recovery_timeout), - ); - breakers.insert( - "monitoring".to_owned(), - CircuitBreaker::new(self.config.circuit_breaker_threshold, recovery_timeout), - ); - } - - // Start trading service stream - let trading_manager = self.clone(); - let trading_sender = _event_sender.clone(); - let trading_shutdown = shutdown_receiver.clone(); - tokio::spawn(async move { - trading_manager - .manage_trading_stream(trading_sender, trading_shutdown) - .await; - }); - - // Start monitoring service stream - let monitoring_manager = self.clone(); - let monitoring_sender = _event_sender.clone(); - let monitoring_shutdown = shutdown_receiver.clone(); - tokio::spawn(async move { - monitoring_manager - .manage_monitoring_stream(monitoring_sender, monitoring_shutdown) - .await; - }); - - // Start health monitoring - let health_manager = self.clone(); - let health_shutdown = shutdown_receiver.clone(); - tokio::spawn(async move { - health_manager.monitor_stream_health(health_shutdown).await; - }); - - // Wait for shutdown signal - while !*shutdown_receiver.borrow() { - if shutdown_receiver.changed().await.is_err() { - break; - } - } - - info!("Stream manager shutting down"); - Ok(()) - } - - /// Manage trading service stream with reconnection - async fn manage_trading_stream( - &self, - _event_sender: broadcast::Sender, - mut shutdown_receiver: tokio::sync::watch::Receiver, - ) { - let service_name = "trading".to_owned(); - let endpoint = self.config.endpoints.trading_engine.clone(); - - loop { - if *shutdown_receiver.borrow() { - break; - } - - // Check circuit breaker - if !self.can_attempt_connection(&service_name).await { - tokio::time::sleep(Duration::from_secs(1)).await; - continue; - } - - // Acquire concurrency permit - let permit = if let Ok(permit) = self.concurrency_limiter.try_acquire() { - permit - } else { - warn!("Too many concurrent streams, waiting..."); - tokio::time::sleep(Duration::from_millis(100)).await; - continue; - }; - - match self.connect_trading_stream(&endpoint).await { - Ok(mut stream) => { - info!("Connected to trading service: {}", endpoint); - self.update_connection_health(&service_name, StreamHealth::Healthy, None) - .await; - self.record_circuit_breaker_success(&service_name).await; - - // Process stream messages - while let Some(result) = stream.next().await { - if *shutdown_receiver.borrow() { - break; - } - - match result { - Ok(response) => { - if let Err(e) = self - .process_trading_response( - &service_name, - response, - &_event_sender, - ) - .await - { - error!("Failed to process trading response: {}", e); - } - }, - Err(e) => { - error!("Trading stream error: {}", e); - self.update_connection_health( - &service_name, - StreamHealth::Failed, - Some(e.to_string()), - ) - .await; - break; - }, - } - } - }, - Err(e) => { - error!("Failed to connect to trading service: {}", e); - self.update_connection_health( - &service_name, - StreamHealth::Failed, - Some(e.to_string()), - ) - .await; - self.record_circuit_breaker_failure(&service_name).await; - }, - } - - drop(permit); - - // Wait before reconnecting - let delay = self.calculate_reconnect_delay(&service_name).await; - self.update_connection_health(&service_name, StreamHealth::Reconnecting, None) - .await; - - tokio::select! { - _ = tokio::time::sleep(delay) => {} - _ = shutdown_receiver.changed() => { - if *shutdown_receiver.borrow() { - break; - } - } - } - } - } - - /// Manage monitoring service stream with reconnection - async fn manage_monitoring_stream( - &self, - _event_sender: broadcast::Sender, - mut shutdown_receiver: tokio::sync::watch::Receiver, - ) { - let service_name = "monitoring".to_owned(); - let endpoint = self.config.endpoints.market_data.clone(); - - loop { - if *shutdown_receiver.borrow() { - break; - } - - // Check circuit breaker - if !self.can_attempt_connection(&service_name).await { - tokio::time::sleep(Duration::from_secs(1)).await; - continue; - } - - // Acquire concurrency permit - let permit = if let Ok(permit) = self.concurrency_limiter.try_acquire() { - permit - } else { - warn!("Too many concurrent streams, waiting..."); - tokio::time::sleep(Duration::from_millis(100)).await; - continue; - }; - - match self.connect_monitoring_stream(&endpoint).await { - Ok(mut stream) => { - info!("Connected to monitoring service: {}", endpoint); - self.update_connection_health(&service_name, StreamHealth::Healthy, None) - .await; - self.record_circuit_breaker_success(&service_name).await; - - // Process stream messages - while let Some(result) = stream.next().await { - if *shutdown_receiver.borrow() { - break; - } - - match result { - Ok(response) => { - if let Err(e) = self - .process_monitoring_response( - &service_name, - response, - &_event_sender, - ) - .await - { - error!("Failed to process monitoring response: {}", e); - } - }, - Err(e) => { - error!("Monitoring stream error: {}", e); - self.update_connection_health( - &service_name, - StreamHealth::Failed, - Some(e.to_string()), - ) - .await; - break; - }, - } - } - }, - Err(e) => { - error!("Failed to connect to monitoring service: {}", e); - self.update_connection_health( - &service_name, - StreamHealth::Failed, - Some(e.to_string()), - ) - .await; - self.record_circuit_breaker_failure(&service_name).await; - }, - } - - drop(permit); - - // Wait before reconnecting - let delay = self.calculate_reconnect_delay(&service_name).await; - self.update_connection_health(&service_name, StreamHealth::Reconnecting, None) - .await; - - tokio::select! { - _ = tokio::time::sleep(delay) => {} - _ = shutdown_receiver.changed() => { - if *shutdown_receiver.borrow() { - break; - } - } - } - } - } - - /// Connect to trading service metrics stream - async fn connect_trading_stream(&self, endpoint: &str) -> TliResult> { - let channel = self.create_channel(endpoint).await?; - let mut client = TradingServiceClient::new(channel); - - let request = Request::new(SubscribeMetricsRequest { - metric_names: vec![ - "order_latency".to_owned(), - "execution_rate".to_owned(), - "pnl".to_owned(), - ], - interval_seconds: 1, - }); - - let response = client.subscribe_metrics(request).await.map_err(|e| { - TliError::Connection(format!("Failed to start trading metrics stream: {}", e)) - })?; - - Ok(response.into_inner()) - } - - /// Connect to system status stream - async fn connect_monitoring_stream( - &self, - endpoint: &str, - ) -> TliResult> { - let channel = self.create_channel(endpoint).await?; - let mut client = TradingServiceClient::new(channel); - - let request = Request::new(SubscribeSystemStatusRequest { - service_names: vec!["trading".to_owned(), "risk".to_owned(), "ml".to_owned()], - }); - - let response = client.subscribe_system_status(request).await.map_err(|e| { - TliError::Connection(format!("Failed to start system status stream: {}", e)) - })?; - - Ok(response.into_inner()) - } - - /// Create gRPC channel with timeouts - async fn create_channel(&self, endpoint: &str) -> TliResult { - let channel = Endpoint::from_shared(endpoint.to_owned()) - .map_err(|e| TliError::Connection(format!("Invalid endpoint {}: {}", endpoint, e)))? - .timeout(Duration::from_secs(self.config.stream_timeout_secs)) - .connect_timeout(Duration::from_secs(self.config.connection_timeout_secs)) - .connect() - .await - .map_err(|e| { - TliError::Connection(format!("Failed to connect to {}: {}", endpoint, e)) - })?; - - Ok(channel) - } - - /// Process trading metrics response - async fn process_trading_response( - &self, - service_name: &str, - response: MetricsEvent, - _event_sender: &broadcast::Sender, - ) -> TliResult<()> { - let sequence = self.next_sequence().await; - - let event_type = EventType::System; - let severity = EventSeverity::Info; - - // Convert metrics to JSON payload - let payload = serde_json::json!({ - "timestamp": response.timestamp_unix_nanos, - "metrics": response.metrics.iter().map(|m| { - serde_json::json!({ - "name": m.name, - "value": m.value, - "unit": m.unit, - "labels": m.labels, - "timestamp": m.timestamp_unix_nanos - }) - }).collect::>() - }); - - let mut event = Event::new(event_type, severity, service_name.to_owned(), payload); - - event.set_sequence(sequence); - - // Add metadata - event.add_metadata( - "metric_count".to_owned(), - response.metrics.len().to_string(), - ); - - // Update connection stats (estimate payload size) - let payload_size = response.metrics.len() * 100; // Rough estimate - self.update_connection_stats(service_name, payload_size as u64) - .await; - - // Send event - if let Err(e) = _event_sender.send(event) { - warn!("Failed to send event: {}", e); - } - - Ok(()) - } - - /// Process system status response - async fn process_monitoring_response( - &self, - service_name: &str, - response: SystemStatusEvent, - _event_sender: &broadcast::Sender, - ) -> TliResult<()> { - let sequence = self.next_sequence().await; - - let event_type = EventType::System; - - // Map status to severity - let severity = match response.status { - 0 => EventSeverity::Warning, // Unknown - 1 => EventSeverity::Info, // Healthy - 2 => EventSeverity::Warning, // Degraded - 3 => EventSeverity::Error, // Unhealthy - 4 => EventSeverity::Critical, // Critical - _ => EventSeverity::Warning, - }; - - // Convert status event to JSON payload - let payload = serde_json::json!({ - "service_name": response.service_name, - "status": response.status, - "previous_status": response.previous_status, - "message": response.message, - "timestamp": response.timestamp_unix_nanos - }); - - let mut event = Event::new(event_type, severity, service_name.to_owned(), payload); - - event.set_sequence(sequence); - - // Add metadata - event.add_metadata("affected_service".to_owned(), response.service_name.clone()); - event.add_metadata("status_code".to_owned(), response.status.to_string()); - - // Update connection stats (estimate payload size) - let payload_size = response.message.len() + response.service_name.len() + 100; - self.update_connection_stats(service_name, payload_size as u64) - .await; - - // Send event - if let Err(e) = _event_sender.send(event) { - warn!("Failed to send event: {}", e); - } - - Ok(()) - } - - /// Monitor stream health and send health events - async fn monitor_stream_health( - &self, - mut shutdown_receiver: tokio::sync::watch::Receiver, - ) { - let mut interval = - tokio::time::interval(Duration::from_secs(self.config.keepalive_interval_secs)); - - while !*shutdown_receiver.borrow() { - tokio::select! { - _ = interval.tick() => { - self.check_connection_health().await; - } - _ = shutdown_receiver.changed() => { - if *shutdown_receiver.borrow() { - break; - } - } - } - } - } - - /// Check health of all connections - async fn check_connection_health(&self) { - let connections = self.connections.read().await; - let now = Utc::now(); - - for (service, connection) in connections.iter() { - if let Some(last_message) = connection.last_message_at { - let elapsed = now.signed_duration_since(last_message); - - if elapsed.num_seconds() > (self.config.keepalive_interval_secs * 2) as i64 { - warn!( - "Stream {} appears stale, last message {} seconds ago", - service, - elapsed.num_seconds() - ); - } - } - } - } - - /// Get next sequence number - async fn next_sequence(&self) -> u64 { - let mut counter = self.sequence_counter.write().await; - *counter += 1; - *counter - } - - /// Update connection health status - async fn update_connection_health( - &self, - service: &str, - health: StreamHealth, - error: Option, - ) { - let mut connections = self.connections.write().await; - let connection = connections - .entry(service.to_owned()) - .or_insert_with(|| StreamConnection::new(service.to_owned(), "".to_owned())); - - connection.health = health.clone(); - - match health { - StreamHealth::Healthy => { - connection.connected_at = Some(Utc::now()); - connection.reconnect_attempts = 0; - connection.last_error = None; - }, - StreamHealth::Failed => { - connection.connected_at = None; - connection.reconnect_attempts += 1; - connection.last_error = error; - }, - StreamHealth::Reconnecting => { - let delay_ms = self.calculate_reconnect_delay_ms(connection.reconnect_attempts); - connection.next_reconnect_at = - Some(Utc::now() + chrono::Duration::milliseconds(delay_ms as i64)); - }, - _ => {}, - } - } - - /// Update connection statistics - async fn update_connection_stats(&self, service: &str, bytes_received: u64) { - let mut connections = self.connections.write().await; - if let Some(connection) = connections.get_mut(service) { - connection.messages_received += 1; - connection.bytes_received += bytes_received; - connection.last_message_at = Some(Utc::now()); - } - } - - /// Check if connection attempt is allowed by circuit breaker - async fn can_attempt_connection(&self, service: &str) -> bool { - if !self.config.enable_circuit_breaker { - return true; - } - - let breakers = self.circuit_breakers.read().await; - if let Some(breaker) = breakers.get(service) { - breaker.can_attempt() - } else { - true - } - } - - /// Record successful connection for circuit breaker - async fn record_circuit_breaker_success(&self, service: &str) { - if !self.config.enable_circuit_breaker { - return; - } - - let mut breakers = self.circuit_breakers.write().await; - if let Some(breaker) = breakers.get_mut(service) { - breaker.record_success(); - } - } - - /// Record failed connection for circuit breaker - async fn record_circuit_breaker_failure(&self, service: &str) { - if !self.config.enable_circuit_breaker { - return; - } - - let mut breakers = self.circuit_breakers.write().await; - if let Some(breaker) = breakers.get_mut(service) { - breaker.record_failure(); - } - } - - /// Calculate reconnection delay - async fn calculate_reconnect_delay(&self, service: &str) -> Duration { - let connections = self.connections.read().await; - if let Some(connection) = connections.get(service) { - let delay_ms = self.calculate_reconnect_delay_ms(connection.reconnect_attempts); - Duration::from_millis(delay_ms) - } else { - Duration::from_millis(self.config.initial_reconnect_delay_ms) - } - } - - /// Calculate reconnection delay in milliseconds - fn calculate_reconnect_delay_ms(&self, attempts: u32) -> u64 { - let delay = self.config.initial_reconnect_delay_ms as f64 - * self.config.backoff_multiplier.powi(attempts as i32); - (delay as u64).min(self.config.max_reconnect_delay_ms) - } - - /// Get current stream health status - pub async fn get_stream_health(&self) -> HashMap { - let connections = self.connections.read().await; - connections - .iter() - .map(|(service, connection)| (service.clone(), connection.health.clone())) - .collect() - } - - /// Get detailed connection information - pub async fn get_connections(&self) -> Vec { - let connections = self.connections.read().await; - connections.values().cloned().collect() - } -} - -impl Clone for StreamManager { - fn clone(&self) -> Self { - Self { - config: self.config.clone(), - connections: self.connections.clone(), - circuit_breakers: self.circuit_breakers.clone(), - concurrency_limiter: self.concurrency_limiter.clone(), - sequence_counter: self.sequence_counter.clone(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_circuit_breaker() { - let mut breaker = CircuitBreaker::new(3_u32, Duration::from_secs(60)); - - // Initial state - assert!(breaker.can_attempt()); - assert!(!breaker.is_circuit_open()); - - // Record failures - breaker.record_failure(); - breaker.record_failure(); - assert!(breaker.can_attempt()); - - breaker.record_failure(); // Should open circuit - assert!(!breaker.can_attempt()); - assert!(breaker.is_circuit_open()); - - // Success should reset - breaker.record_success(); - assert!(breaker.can_attempt()); - assert!(!breaker.is_circuit_open()); - } - - #[test] - fn test_reconnect_delay_calculation() { - let config = StreamConfig::default(); - let manager = StreamManager { - config: config.clone(), - connections: Arc::new(RwLock::new(HashMap::new())), - circuit_breakers: Arc::new(RwLock::new(HashMap::new())), - concurrency_limiter: Arc::new(Semaphore::new(config.max_concurrent_streams)), - sequence_counter: Arc::new(RwLock::new(0)), - }; - - assert_eq!(manager.calculate_reconnect_delay_ms(0), 1000); - assert_eq!(manager.calculate_reconnect_delay_ms(1), 2000); - assert_eq!(manager.calculate_reconnect_delay_ms(2), 4000); - assert_eq!(manager.calculate_reconnect_delay_ms(10), 30000); // Capped at max - } - - #[test] - fn test_stream_connection_creation() { - let connection = StreamConnection::new("test".to_owned(), "http://test".to_owned()); - - assert_eq!(connection.service, "test"); - assert_eq!(connection.endpoint, "http://test"); - assert_eq!(connection.health, StreamHealth::Connecting); - assert_eq!(connection.reconnect_attempts, 0); - assert_eq!(connection.messages_received, 0); - } -} diff --git a/tli/src/lib.rs b/tli/src/lib.rs index 78180ea48..a00964812 100644 --- a/tli/src/lib.rs +++ b/tli/src/lib.rs @@ -66,16 +66,13 @@ //! ``` // Suppress false-positive unused extern crate warnings for dependencies used in modules -use adaptive_strategy as _; use chrono as _; use clap as _; use colored as _; use common as _; -use crossterm as _; use dirs as _; // Used in config module for home directory use futures_util as _; use prost as _; -use ratatui as _; use rust_decimal as _; use serde as _; use serde_json as _; @@ -91,16 +88,8 @@ pub mod auth; pub mod client; pub mod commands; pub mod config; // Configuration file support (~/.foxhunt/config.toml) - // pub mod config_client; // Config client removed - use gRPC ConfigurationService instead -pub mod dashboard; -pub mod dashboards; pub mod error; -// pub mod health; // Health server module removed - TLI is pure client pub mod types; -pub mod ui; - -// Event system for client-side event handling and streaming -pub mod events; // Prelude module for convenient imports pub mod prelude; @@ -140,7 +129,6 @@ pub const BUILD_INFO: BuildInfo = BuildInfo { features: &[ // TLI features are defined by dependencies, not cargo features "tonic-tls", - "ratatui-ui", "grpc-client", ], }; diff --git a/tli/src/main.rs b/tli/src/main.rs index d49120e49..3f86ed513 100644 --- a/tli/src/main.rs +++ b/tli/src/main.rs @@ -1,11 +1,9 @@ //! TLI (Terminal Line Interface) - Client Application for Foxhunt HFT Trading System //! -//! Pure client terminal application that connects to trading services: -//! - Real-time trading dashboard with 5 specialized views -//! - Interactive terminal UI using Ratatui +//! Pure client CLI application that connects to trading services: +//! - CLI commands for trading, backtesting, ML training, and tuning //! - gRPC client connections to Trading and Backtesting services -//! - Live data streaming and event handling -//! - Remote monitoring and control capabilities +//! - Authentication and secure token management use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; @@ -15,7 +13,6 @@ use serde::{Deserialize, Serialize}; use std::time::{SystemTime, UNIX_EPOCH}; use tli::auth::token_manager::FileTokenStorage; use tli::{ - client::TliClientBuilder, commands::{ agent::{execute_agent_command, AgentArgs}, auth::{execute_auth_command, AuthCommand}, @@ -25,13 +22,11 @@ use tli::{ tune::{execute_tune_command, TuneCommand}, }, config::TliConfig, - ui::TliTerminal, }; -use tracing::{error, info, Level}; +use tracing::Level; use tracing_subscriber::FmtSubscriber; // Suppress false-positive unused extern crate warnings for dependencies used in modules -use adaptive_strategy as _; use aes_gcm as _; use argon2 as _; use async_trait as _; @@ -42,7 +37,6 @@ use colored as _; use comfy_table as _; use common as _; use console as _; -use crossterm as _; use dirs as _; use futures_util as _; use getrandom as _; @@ -52,7 +46,6 @@ use keyring as _; use owo_colors as _; use prost as _; use rand as _; -use ratatui as _; use rpassword as _; use rust_decimal as _; use serde as _; @@ -199,17 +192,6 @@ enum Commands { trade_args: TradeArgs, }, - /// Launch interactive trading dashboard (TUI) - #[clap(long_about = "Real-time trading dashboard with:\n\ - - Live position monitoring\n\ - - P&L tracking\n\ - - Risk metrics (VaR, Greeks)\n\ - - Order flow visualization\n\n\ - Keyboard shortcuts:\n\ - q - Quit\n\ - r - Refresh\n\ - h - Help")] - Dashboard, } /// JWT token claims structure for validation @@ -404,101 +386,29 @@ async fn main() -> Result<()> { eprintln!("Warning: Failed to set global tracing subscriber: {}", e); } - // Route commands before launching dashboard + // Route to subcommands match cli.command { Commands::Tune { tune_cmd } => { - // Get JWT token from storage for tune commands let jwt_token = load_jwt_token(&cli.api_gateway_url).await?; - - // Execute tune command - return execute_tune_command(tune_cmd, &cli.api_gateway_url, &jwt_token).await; - }, + execute_tune_command(tune_cmd, &cli.api_gateway_url, &jwt_token).await + } Commands::Train { train_cmd } => { - // Get JWT token from storage for train commands let jwt_token = load_jwt_token(&cli.api_gateway_url).await?; - - // Execute train command - return execute_train_command(train_cmd, &cli.api_gateway_url, &jwt_token).await; - }, - Commands::Auth { auth_cmd } => { - // Execute auth command (auth commands don't need prior authentication) - return execute_auth_command(auth_cmd).await; - }, + execute_train_command(train_cmd, &cli.api_gateway_url, &jwt_token).await + } + Commands::Auth { auth_cmd } => execute_auth_command(auth_cmd).await, Commands::Agent { agent_args } => { - // Get JWT token from storage for agent commands let jwt_token = load_jwt_token(&cli.api_gateway_url).await?; - return execute_agent_command(agent_args, &cli.api_gateway_url, &jwt_token).await; - }, + execute_agent_command(agent_args, &cli.api_gateway_url, &jwt_token).await + } Commands::Backtest { backtest_args } => { - // Backtest commands don't require authentication for now - return execute_backtest_ml_command(backtest_args).await; - }, + execute_backtest_ml_command(backtest_args).await + } Commands::Trade { trade_args } => { - // Get JWT token from storage for trade commands let jwt_token = load_jwt_token(&cli.api_gateway_url).await?; - return execute_trade_command(trade_args, &cli.api_gateway_url, &jwt_token).await; - }, - Commands::Dashboard => { - // Continue to launch dashboard - }, + execute_trade_command(trade_args, &cli.api_gateway_url, &jwt_token).await + } } - - info!("Starting TLI Terminal Client..."); - - // Use CLI api_gateway_url (already merged with config) - let api_gateway_endpoint = cli.api_gateway_url.clone(); - - info!("TLI Client Configuration:"); - info!(" API Gateway: {}", api_gateway_endpoint); - info!(" Log Level: {}", cli.log_level); - info!(" Token Storage: {}", cli.token_storage); - info!(" Note: TLI connects ONLY to API Gateway (port 50051)"); - info!(" API Gateway routes to backend services (Trading, Backtesting, ML)"); - - // Create TLI terminal - let (mut terminal, _event_sender) = TliTerminal::new(); - - // Create gRPC client - connects ONLY to API Gateway (pure client architecture) - match TliClientBuilder::new() - .with_service_endpoint("api_gateway".to_owned(), api_gateway_endpoint) - .build() - .await - { - Ok(client_suite) => { - info!("Successfully connected to API Gateway at port 50051"); - terminal.set_client_suite(client_suite); - }, - Err(e) => { - error!("Failed to connect to API Gateway: {}", e); - info!("Running in offline mode - dashboard will show demo data"); - }, - } - - // Start real-time data streaming (works in both online and offline modes) - if let Err(e) = terminal.start_streaming().await { - error!("Failed to start data streams: {}", e); - info!("Dashboard will show static data only"); - } else { - info!("Real-time data streaming started"); - } - info!("Starting TLI Terminal Interface..."); - info!("6 Interactive Dashboards (per TLI_PLAN.md):"); - info!(" [T] Trading Dashboard - Live positions, orders, executions, market data"); - info!(" [R] Risk Dashboard - VaR, limits, drawdown, emergency controls"); - info!(" [M] ML Dashboard - Model predictions, signals, ensemble voting"); - info!(" [P] Performance Dashboard - Returns, Sharpe ratios, analytics"); - info!(" [B] Backtesting Dashboard - Strategy testing, historical analysis"); - info!(" [C] Configuration Dashboard - Settings management, hot-reload"); - info!(" [ESC/Q] Exit"); - - // Run the terminal application - if let Err(e) = terminal.run().await { - error!("Terminal application error: {}", e); - return Err(e); - } - - info!("TLI Terminal Client stopped gracefully"); - Ok(()) } #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] @@ -543,20 +453,10 @@ mod tests { } } - #[test] - fn test_cli_parsing_dashboard_command() { - let cli = Cli::parse_from(&["tli", "dashboard"]); - - match cli.command { - Commands::Dashboard => {}, - _ => panic!("Expected Dashboard command"), - } - } - #[test] fn test_cli_default_values() { // Test default values when no flags provided - let cli = Cli::parse_from(&["tli", "dashboard"]); + let cli = Cli::parse_from(&["tli", "auth", "status"]); assert_eq!(cli.api_gateway_url, "http://localhost:50051"); assert_eq!(cli.log_level, "info"); @@ -574,7 +474,8 @@ mod tests { "debug", "--token-storage", "file", - "dashboard", + "auth", + "status", ]); assert_eq!(cli.api_gateway_url, "http://custom.com:8080"); diff --git a/tli/src/prelude.rs b/tli/src/prelude.rs index dacde7b35..ec878b3e3 100644 --- a/tli/src/prelude.rs +++ b/tli/src/prelude.rs @@ -12,19 +12,11 @@ pub use crate::client::{ClientFactory, ServiceEndpoints, TliClientBuilder, TliCl // Client configurations and implementations pub use crate::client::backtesting_client::{BacktestingClient, BacktestingClientConfig}; pub use crate::client::connection_manager::{ConnectionConfig, ConnectionManager}; -pub use crate::client::data_stream::{DataStreamConfig, DataStreamManager}; -pub use crate::client::event_stream::{EventStreamConfig, EventStreamManager}; pub use crate::client::ml_training_client::{MLTrainingClient, MLTrainingClientConfig}; pub use crate::client::trading_client::{TradingClient, TradingClientConfig}; -// Event types -pub use crate::events::{Event, EventFilter, EventSeverity, EventType}; - // Proto types - common trading types pub use crate::proto::trading::{ CancelOrderRequest, GetPositionsRequest, OrderSide, OrderStatus, OrderType, Position, SubmitOrderRequest, Trade, }; - -// Dashboard types if needed -pub use crate::dashboard::Dashboard; diff --git a/tli/src/tests.rs b/tli/src/tests.rs index f9adb3d8c..40a263d9e 100644 --- a/tli/src/tests.rs +++ b/tli/src/tests.rs @@ -454,25 +454,6 @@ mod benchmark_helpers { } } -#[cfg(test)] -mod ui_state_tests { - use crate::ui::TliTerminal; - - #[test] - fn test_terminal_creation() { - let (_terminal, _event_sender) = TliTerminal::new(); - // Test that terminal can be created successfully - assert!(true); - } - - #[test] - fn test_terminal_default() { - let _terminal = TliTerminal::default(); - // Test that terminal can be created with default - assert!(true); - } -} - #[cfg(test)] mod command_handling_tests { use crate::types::*; diff --git a/tli/src/ui/mod.rs b/tli/src/ui/mod.rs deleted file mode 100644 index c9727bdbe..000000000 --- a/tli/src/ui/mod.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! Terminal UI Module for TLI Client -//! -//! Provides the main terminal user interface implementation using Ratatui -//! and integrates with the dashboard framework for a complete trading terminal. - -use crate::client::data_stream::DataStreamManager; -use crate::client::TliClientSuite; -use crate::dashboard::events::DashboardEvent; -use crate::dashboard::DashboardManager; -use anyhow::Result; -use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, -}; -use ratatui::{ - backend::{Backend, CrosstermBackend}, - Terminal, -}; -use std::io; -use tokio::sync::mpsc; - -pub struct TliTerminal { - dashboard_manager: DashboardManager, - client_suite: Option, - _event_sender: mpsc::Sender, - stream_manager: Option, -} - -impl TliTerminal { - pub fn new() -> (Self, mpsc::Sender) { - let (dashboard_manager, _event_sender) = DashboardManager::new(); - - let terminal = Self { - dashboard_manager, - client_suite: None, - _event_sender: _event_sender.clone(), - stream_manager: None, - }; - - (terminal, _event_sender) - } - - pub fn set_client_suite(&mut self, client_suite: TliClientSuite) { - self.client_suite = Some(client_suite); - } - - pub async fn start_streaming(&mut self) -> Result<()> { - // Initialize stream manager for real-time data - use crate::client::data_stream::DataStreamConfig; - let config = DataStreamConfig { - buffer_size: 1000_usize, - max_latency_ms: 100_u64, - }; - let mut stream_manager = DataStreamManager::new(config); - stream_manager - .start_streams() - .await - .map_err(|e| anyhow::anyhow!(e))?; - self.stream_manager = Some(stream_manager); - Ok(()) - } - - pub async fn run(&mut self) -> Result<()> { - // Setup terminal - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - let result = self.run_app(&mut terminal).await; - - // Restore terminal - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - - result - } - - async fn run_app(&mut self, terminal: &mut Terminal) -> Result<()> { - loop { - // Render the UI - terminal.draw(|f| { - if let Err(e) = self.dashboard_manager.render(f) { - eprintln!("Render error: {}", e); - } - })?; - - // Handle events - if event::poll(std::time::Duration::from_millis(100))? { - if let Event::Key(key) = event::read()? { - // Handle global shortcuts - if key.code == KeyCode::Char('q') || key.code == KeyCode::Esc { - break; - } - - // Pass to dashboard manager - if let Some(dashboard_event) = self.dashboard_manager.handle_input(key)? { - let should_exit = - self.dashboard_manager.handle_event(dashboard_event).await?; - if should_exit { - break; - } - } - } - } - - // Process any pending dashboard events - while let Ok(event) = self.dashboard_manager.event_receiver.try_recv() { - let should_exit = self.dashboard_manager.handle_event(event).await?; - if should_exit { - break; - } - } - } - - Ok(()) - } -} - -impl Default for TliTerminal { - fn default() -> Self { - Self::new().0 - } -} diff --git a/tli/src/ui/widgets/candlestick_chart.rs b/tli/src/ui/widgets/candlestick_chart.rs deleted file mode 100644 index 343722d0b..000000000 --- a/tli/src/ui/widgets/candlestick_chart.rs +++ /dev/null @@ -1,491 +0,0 @@ -//! Real-time candlestick chart widget for financial data visualization -//! -//! Displays OHLC (Open, High, Low, Close) price data as candlesticks with: -//! - Real-time updates with minimal flicker -//! - Auto-scaling based on visible data range -//! - Volume indicators at the bottom -//! - Price grid lines and labels -//! - Interactive zoom and pan (future enhancement) - -use ratatui::{ - prelude::*, - symbols::DOT, - widgets::{Block, Borders, Widget, canvas::{Canvas, Line, Points}}, -}; -use std::collections::VecDeque; -use chrono::{DateTime, Utc}; - -use super::{ - FinancialWidget, FinancialColors, Candle, CircularBuffer, - create_block, format_price, price_change_color -}; - -/// Real-time candlestick chart widget -#[derive(Debug)] -pub struct CandlestickChart { - /// Chart title - title: String, - /// Candlestick data buffer - candles: CircularBuffer, - /// Color scheme - colors: FinancialColors, - /// Chart dimensions - width: u16, - height: u16, - /// Price precision for display - price_precision: u32, - /// Show volume bars - show_volume: bool, - /// Auto-scale prices - auto_scale: bool, - /// Manual price range (if not auto-scaling) - price_range: Option<(Decimal, Decimal)>, - /// Current price range for display - current_range: (Decimal, Decimal), - /// Time range in seconds - time_range_seconds: i64, -} - -impl CandlestickChart { - /// Create a new candlestick chart - pub fn new(title: &str, max_candles: usize) -> Self { - Self { - title: title.to_string(), - candles: CircularBuffer::new(max_candles), - colors: FinancialColors::default(), - width: 80, - height: 30, - price_precision: 2, - show_volume: true, - auto_scale: true, - price_range: None, - current_range: (Decimal::ZERO, Decimal::ZERO), - time_range_seconds: 300, // 5 minutes default - } - } - - /// Set chart dimensions - pub fn with_dimensions(mut self, width: u16, height: u16) -> Self { - self.width = width; - self.height = height; - self - } - - /// Set price precision for display - pub fn with_precision(mut self, precision: u32) -> Self { - self.price_precision = precision; - self - } - - /// Toggle volume display - pub fn with_volume(mut self, show_volume: bool) -> Self { - self.show_volume = show_volume; - self - } - - /// Set manual price range - pub fn with_price_range(mut self, min: Decimal, max: Decimal) -> Self { - self.auto_scale = false; - self.price_range = Some((min, max)); - self.current_range = (min, max); - self - } - - /// Set time range in seconds - pub fn with_time_range(mut self, seconds: i64) -> Self { - self.time_range_seconds = seconds; - self - } - - /// Add a single candle - pub fn add_candle(&mut self, candle: Candle) { - self.candles.push(candle); - - if self.auto_scale { - self.update_price_range(); - } - } - - /// Update price range based on visible candles - fn update_price_range(&mut self) { - if self.candles.is_empty() { - return; - } - - let mut min_price = Decimal::MAX; - let mut max_price = Decimal::MIN; - - for candle in &self.candles { - min_price = min_price.min(candle.low); - max_price = max_price.max(candle.high); - } - - // Add 5% padding to the range - let padding = (max_price - min_price) * Decimal::new(5, 2); // 0.05 - self.current_range = (min_price - padding, max_price + padding); - } - - /// Get the latest candle - pub fn latest_candle(&self) -> Option<&Candle> { - self.candles.iter().last() - } - - /// Calculate price change from previous candle - pub fn price_change(&self) -> Option<(Decimal, Decimal)> { - let candles: Vec<&Candle> = self.candles.iter().collect(); - if candles.len() < 2 { - return None; - } - - let current = candles[candles.len() - 1]; - let previous = candles[candles.len() - 2]; - let change = current.close - previous.close; - let percentage = if previous.close != Decimal::ZERO { - (change / previous.close) * Decimal::new(100, 0) - } else { - Decimal::ZERO - }; - - Some((change, percentage)) - } - - /// Convert price to screen Y coordinate - fn price_to_y(&self, price: Decimal, chart_height: u16) -> f64 { - let (min_price, max_price) = self.current_range; - let price_range = max_price - min_price; - - if price_range == Decimal::ZERO { - return (chart_height / 2) as f64; - } - - let normalized = (price - min_price) / price_range; - let y = chart_height as f64 * (1.0 - normalized.to_f64()); - y.clamp(0.0, chart_height as f64) - } - - /// Convert candle index to screen X coordinate - fn index_to_x(&self, index: usize, chart_width: u16) -> f64 { - let candle_count = self.candles.len(); - if candle_count <= 1 { - return 0.0; - } - - let x = (index as f64 / (candle_count - 1) as f64) * chart_width as f64; - x.clamp(0.0, chart_width as f64) - } - - /// Draw price grid lines - fn draw_price_grid(&self) -> Vec { - let mut lines = Vec::new(); - let (min_price, max_price) = self.current_range; - let price_range = max_price - min_price; - - if price_range == Decimal::ZERO { - return lines; - } - - // Draw 5 horizontal grid lines - for i in 0..=4 { - let price = min_price + (price_range * Decimal::new(i, 0) / Decimal::new(4, 0)); - let y = self.price_to_y(price, self.height); - - lines.push(Line { - x1: 0.0, - y1: y, - x2: self.width as f64, - y2: y, - color: Color::DarkGray, - }); - } - - lines - } - - /// Create candlestick visual elements - fn create_candlesticks(&self) -> (Vec, Vec) { - let mut wicks = Vec::new(); - let mut bodies = Vec::new(); - - for (index, candle) in self.candles.into_iter().enumerate() { - let x = self.index_to_x(index, self.width); - let open_y = self.price_to_y(candle.open, self.height); - let high_y = self.price_to_y(candle.high, self.height); - let low_y = self.price_to_y(candle.low, self.height); - let close_y = self.price_to_y(candle.close, self.height); - - // Determine candle color - let color = if candle.close >= candle.open { - self.colors.profit - } else { - self.colors.loss - }; - - // Draw wick (high-low line) - wicks.push(Line { - x1: x, - y1: high_y, - x2: x, - y2: low_y, - color, - }); - - // Draw body (open-close line, thicker) - bodies.push(Line { - x1: x, - y1: open_y, - x2: x, - y2: close_y, - color, - }); - } - - (wicks, bodies) - } - - /// Create volume bars at the bottom - fn create_volume_bars(&self) -> Vec { - if !self.show_volume || self.candles.is_empty() { - return Vec::new(); - } - - let mut bars = Vec::new(); - let volume_height = self.height as f64 * 0.2; // 20% of chart height for volume - - // Find max volume for scaling - let max_volume = self.candles.iter() - .map(|c| c.volume) - .max() - .unwrap_or(Decimal::ZERO); - - if max_volume == Decimal::ZERO { - return bars; - } - - for (index, candle) in self.candles.into_iter().enumerate() { - let x = self.index_to_x(index, self.width); - let volume_ratio = candle.volume / max_volume; - let bar_height = volume_height * volume_ratio.to_f64(); - - bars.push(Line { - x1: x, - y1: self.height as f64, - x2: x, - y2: self.height as f64 - bar_height, - color: Color::Blue, - }); - } - - bars - } -} - -impl FinancialWidget for CandlestickChart { - type Data = Vec; - - fn update_data(&mut self, candles: Self::Data) { - self.candles.clear(); - for candle in candles { - self.candles.push(candle); - } - - if self.auto_scale { - self.update_price_range(); - } - } - - fn clear(&mut self) { - self.candles.clear(); - self.current_range = (Decimal::ZERO, Decimal::ZERO); - } - - fn title(&self) -> &str { - &self.title - } - - fn has_data(&self) -> bool { - !self.candles.is_empty() - } -} - -impl Widget for CandlestickChart { - fn render(self, area: Rect, buf: &mut Buffer) { - // Create the main block - let block = create_block(&self.title, &self.colors); - let inner = block.inner(area); - block.render(area, buf); - - if !self.has_data() { - // Show "No Data" message - let no_data = ratatui::widgets::Paragraph::new("No data available") - .style(Style::default().fg(self.colors.text)) - .alignment(Alignment::Center); - no_data.render(inner, buf); - return; - } - - // Create status line with current price and change - let status_area = Rect { - x: inner.x, - y: inner.y, - width: inner.width, - height: 1, - }; - - if let Some(latest) = self.latest_candle() { - let mut status_text = format!("CLOSE: {}", format_price(latest.close, self.price_precision)); - - if let Some((change, percentage)) = self.price_change() { - let change_color = price_change_color(change, &self.colors); - status_text.push_str(&format!(" ({} {}%)", - if change >= Decimal::ZERO { "+" } else { "" }, - format_price(percentage, 2) - )); - } - - let status = ratatui::widgets::Paragraph::new(status_text) - .style(Style::default().fg(self.colors.text)); - status.render(status_area, buf); - } - - // Chart area (below status line) - let chart_area = Rect { - x: inner.x, - y: inner.y + 1, - width: inner.width, - height: inner.height.saturating_sub(1), - }; - - // Create canvas for drawing - let canvas = Canvas::default() - .block(Block::default()) - .x_bounds([0.0, self.width as f64]) - .y_bounds([0.0, self.height as f64]) - .paint(|ctx| { - // Draw price grid - for line in self.draw_price_grid() { - ctx.draw(&line); - } - - // Draw volume bars (if enabled) - if self.show_volume { - for bar in self.create_volume_bars() { - ctx.draw(&bar); - } - } - - // Draw candlesticks - let (wicks, bodies) = self.create_candlesticks(); - - for wick in wicks { - ctx.draw(&wick); - } - - for body in bodies { - ctx.draw(&body); - } - }); - - canvas.render(chart_area, buf); - - // Price labels on the right side - let (min_price, max_price) = self.current_range; - let label_area = Rect { - x: inner.x + inner.width.saturating_sub(10), - y: inner.y + 1, - width: 10, - height: inner.height.saturating_sub(1), - }; - - let price_labels = vec![ - format_price(max_price, self.price_precision), - format_price((max_price + min_price) / Decimal::new(2, 0), self.price_precision), - format_price(min_price, self.price_precision), - ]; - - for (i, label) in price_labels.into_iter().enumerate() { - let y = label_area.y + (i as u16 * (label_area.height / 3)); - let label_widget = ratatui::widgets::Paragraph::new(label.as_str()) - .style(Style::default().fg(self.colors.text)); - - let label_rect = Rect { - x: label_area.x, - y, - width: label_area.width, - height: 1, - }; - - label_widget.render(label_rect, buf); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use chrono::Utc; - - fn create_test_candle(open: f64, high: f64, low: f64, close: f64) -> Candle { - Candle { - timestamp: Utc::now(), - open: Decimal::try_from(open).unwrap(), - high: Decimal::try_from(high).unwrap(), - low: Decimal::try_from(low).unwrap(), - close: Decimal::try_from(close).unwrap(), - volume: Decimal::from(1000), - } - } - - #[test] - fn test_candlestick_chart_creation() { - let chart = CandlestickChart::new("Test Chart", 100); - assert_eq!(chart.title(), "Test Chart"); - assert!(!chart.has_data()); - } - - #[test] - fn test_add_candle() { - let mut chart = CandlestickChart::new("Test", 10); - let candle = create_test_candle(100.0, 105.0, 95.0, 102.0); - - chart.add_candle(candle); - assert!(chart.has_data()); - assert!(chart.latest_candle().is_some()); - } - - #[test] - fn test_price_change_calculation() { - let mut chart = CandlestickChart::new("Test", 10); - - chart.add_candle(create_test_candle(100.0, 105.0, 95.0, 102.0)); - chart.add_candle(create_test_candle(102.0, 108.0, 98.0, 105.0)); - - let (change, percentage) = chart.price_change().unwrap(); - assert_eq!(change, Decimal::from(3)); // 105 - 102 - assert!(percentage > Decimal::ZERO); - } - - #[test] - fn test_coordinate_conversion() { - let mut chart = CandlestickChart::new("Test", 10); - chart.current_range = (Decimal::from(100), Decimal::from(200)); - - let y = chart.price_to_y(Decimal::from(150), 100); - assert_eq!(y, 50.0); // Middle of range should map to middle of height - - let x = chart.index_to_x(5, 100); - assert!(x >= 0.0 && x <= 100.0); - } - - #[test] - fn test_update_data() { - let mut chart = CandlestickChart::new("Test", 10); - let candles = vec![ - create_test_candle(100.0, 105.0, 95.0, 102.0), - create_test_candle(102.0, 108.0, 98.0, 105.0), - ]; - - chart.update_data(candles); - assert_eq!(chart.candles.len(), 2); - assert!(chart.has_data()); - } -} \ No newline at end of file diff --git a/tli/src/ui/widgets/config_form.rs b/tli/src/ui/widgets/config_form.rs deleted file mode 100644 index efde578f5..000000000 --- a/tli/src/ui/widgets/config_form.rs +++ /dev/null @@ -1,699 +0,0 @@ -//! Configuration form widget with input validation -//! -//! Provides interactive forms for: -//! - Trading strategy parameters -//! - Risk management settings -//! - Connection configurations -//! - System preferences -//! -//! Features include real-time validation, keyboard navigation, -//! and different input field types (text, numeric, boolean, select). - -use ratatui::{ - prelude::*, - widgets::{Block, Borders, Widget, Paragraph, List, ListItem, ListState, Clear}, -}; -use std::collections::HashMap; - -use super::{ - FinancialWidget, FinancialColors, ConfigField, FormField, - create_block -}; - -/// Form input mode -#[derive(Debug, Clone, PartialEq)] -pub enum InputMode { - Normal, // Navigation mode - Editing, // Text input mode -} - -/// Form validation result -#[derive(Debug, Clone)] -pub struct ValidationResult { - pub is_valid: bool, - pub errors: HashMap, -} - -/// Configuration form widget -#[derive(Debug)] -pub struct ConfigForm { - /// Widget title - title: String, - /// Form fields - fields: Vec, - /// Color scheme - colors: FinancialColors, - /// Current input mode - input_mode: InputMode, - /// Currently selected field index - selected_field: usize, - /// Current input buffer (for text editing) - input_buffer: String, - /// Form validation result - validation: ValidationResult, - /// Show validation errors inline - show_inline_errors: bool, - /// Form is submittable - can_submit: bool, - /// Custom validators - validators: HashMap Result<(), String>>>, -} - -impl ConfigForm { - /// Create a new configuration form - pub fn new(title: &str) -> Self { - Self { - title: title.to_string(), - fields: Vec::new(), - colors: FinancialColors::default(), - input_mode: InputMode::Normal, - selected_field: 0, - input_buffer: String::new(), - validation: ValidationResult { - is_valid: true, - errors: HashMap::new(), - }, - show_inline_errors: true, - can_submit: false, - validators: HashMap::new(), - } - } - - /// Add a text field to the form - pub fn add_text_field( - mut self, - name: &str, - label: &str, - placeholder: &str, - required: bool, - ) -> Self { - self.fields.push(FormField { - name: name.to_string(), - label: label.to_string(), - field_type: ConfigField::Text { - value: String::new(), - placeholder: placeholder.to_string(), - }, - required, - validation_error: None, - }); - self - } - - /// Add a numeric field to the form - pub fn add_number_field( - mut self, - name: &str, - label: &str, - default_value: f64, - min: f64, - max: f64, - required: bool, - ) -> Self { - self.fields.push(FormField { - name: name.to_string(), - label: label.to_string(), - field_type: ConfigField::Number { - value: default_value, - min, - max, - }, - required, - validation_error: None, - }); - self - } - - /// Add a boolean field to the form - pub fn add_boolean_field( - mut self, - name: &str, - label: &str, - default_value: bool, - ) -> Self { - self.fields.push(FormField { - name: name.to_string(), - label: label.to_string(), - field_type: ConfigField::Boolean { value: default_value }, - required: false, - validation_error: None, - }); - self - } - - /// Add a select field to the form - pub fn add_select_field( - mut self, - name: &str, - label: &str, - options: Vec, - default_value: Option, - required: bool, - ) -> Self { - let value = default_value.unwrap_or_else(|| { - options.first().cloned().unwrap_or_default() - }); - - self.fields.push(FormField { - name: name.to_string(), - label: label.to_string(), - field_type: ConfigField::Select { value, options }, - required, - validation_error: None, - }); - self - } - - /// Add custom validator for a field - pub fn add_validator(mut self, field_name: &str, validator: F) -> Self - where - F: Fn(&str) -> Result<(), String> + 'static, - { - self.validators.insert(field_name.to_string(), Box::new(validator)); - self - } - - /// Toggle inline error display - pub fn with_inline_errors(mut self, show: bool) -> Self { - self.show_inline_errors = show; - self - } - - /// Handle keyboard input - pub fn handle_input(&mut self, key: crossterm::event::KeyCode) -> bool { - match self.input_mode { - InputMode::Normal => self.handle_navigation(key), - InputMode::Editing => self.handle_text_input(key), - } - } - - /// Handle navigation keys in normal mode - fn handle_navigation(&mut self, key: crossterm::event::KeyCode) -> bool { - use crossterm::event::KeyCode; - - match key { - KeyCode::Up => { - if self.selected_field > 0 { - self.selected_field -= 1; - } - false - }, - KeyCode::Down => { - if self.selected_field < self.fields.len().saturating_sub(1) { - self.selected_field += 1; - } - false - }, - KeyCode::Enter => { - self.start_editing(); - false - }, - KeyCode::Tab => { - self.next_field(); - false - }, - KeyCode::Char(' ') => { - self.toggle_boolean_field(); - false - }, - KeyCode::Char('s') | KeyCode::F(10) => { - self.submit_form() - }, - _ => false, - } - } - - /// Handle text input in editing mode - fn handle_text_input(&mut self, key: crossterm::event::KeyCode) -> bool { - use crossterm::event::KeyCode; - - match key { - KeyCode::Enter => { - self.finish_editing(); - false - }, - KeyCode::Esc => { - self.cancel_editing(); - false - }, - KeyCode::Char(c) => { - self.input_buffer.push(c); - false - }, - KeyCode::Backspace => { - self.input_buffer.pop(); - false - }, - KeyCode::Tab => { - self.finish_editing(); - self.next_field(); - false - }, - _ => false, - } - } - - /// Start editing the current field - fn start_editing(&mut self) { - if let Some(field) = self.fields.get(self.selected_field) { - match &field.field_type { - ConfigField::Text { value, .. } => { - self.input_buffer = value.clone(); - self.input_mode = InputMode::Editing; - }, - ConfigField::Number { value, .. } => { - self.input_buffer = value.to_string(); - self.input_mode = InputMode::Editing; - }, - ConfigField::Select { value, options } => { - // Cycle through options for select fields - if let Some(current_idx) = options.iter().position(|o| o == value) { - let next_idx = (current_idx + 1) % options.len(); - self.set_field_value(&field.name, &options[next_idx]); - } - }, - _ => {}, // Boolean fields don't need editing mode - } - } - } - - /// Finish editing and update the field - fn finish_editing(&mut self) { - if self.input_mode == InputMode::Editing { - if let Some(field) = self.fields.get(self.selected_field) { - let field_name = field.name.clone(); - self.set_field_value(&field_name, &self.input_buffer); - } - } - self.input_mode = InputMode::Normal; - self.input_buffer.clear(); - } - - /// Cancel editing and revert to original value - fn cancel_editing(&mut self) { - self.input_mode = InputMode::Normal; - self.input_buffer.clear(); - } - - /// Move to next field - fn next_field(&mut self) { - self.selected_field = (self.selected_field + 1) % self.fields.len(); - } - - /// Toggle boolean field value - fn toggle_boolean_field(&mut self) { - if let Some(field) = self.fields.get(self.selected_field) { - if let ConfigField::Boolean { value } = &field.field_type { - let field_name = field.name.clone(); - self.set_field_value(&field_name, &(!value).to_string()); - } - } - } - - /// Set field value and validate - fn set_field_value(&mut self, field_name: &str, value: &str) { - if let Some(field) = self.fields.iter_mut().find(|f| f.name == field_name) { - match &mut field.field_type { - ConfigField::Text { value: field_value, .. } => { - *field_value = value.to_string(); - }, - ConfigField::Number { value: field_value, min, max } => { - if let Ok(num) = value.parse::() { - *field_value = num.clamp(*min, *max); - } - }, - ConfigField::Boolean { value: field_value } => { - if let Ok(bool_val) = value.parse::() { - *field_value = bool_val; - } - }, - ConfigField::Select { value: field_value, options } => { - if options.contains(&value.to_string()) { - *field_value = value.to_string(); - } - }, - } - - // Validate the field - self.validate_field(field); - } - - self.validate_form(); - } - - /// Validate a single field - fn validate_field(&mut self, field: &mut FormField) { - field.validation_error = None; - - // Check required fields - if field.required { - let is_empty = match &field.field_type { - ConfigField::Text { value, .. } => value.is_empty(), - ConfigField::Select { value, .. } => value.is_empty(), - _ => false, - }; - - if is_empty { - field.validation_error = Some("This field is required".to_string()); - return; - } - } - - // Check numeric ranges - if let ConfigField::Number { value, min, max } = &field.field_type { - if *value < *min || *value > *max { - field.validation_error = Some(format!("Value must be between {} and {}", min, max)); - return; - } - } - - // Run custom validators - if let Some(validator) = self.validators.get(&field.name) { - let field_value = match &field.field_type { - ConfigField::Text { value, .. } => value.clone(), - ConfigField::Number { value, .. } => value.to_string(), - ConfigField::Boolean { value } => value.to_string(), - ConfigField::Select { value, .. } => value.clone(), - }; - - if let Err(error) = validator(&field_value) { - field.validation_error = Some(error); - } - } - } - - /// Validate the entire form - fn validate_form(&mut self) { - let mut errors = HashMap::new(); - let mut is_valid = true; - - for field in &self.fields { - if let Some(ref error) = field.validation_error { - errors.insert(field.name.clone(), error.clone()); - is_valid = false; - } - } - - self.validation = ValidationResult { is_valid, errors }; - self.can_submit = is_valid; - } - - /// Submit the form - fn submit_form(&mut self) -> bool { - self.validate_form(); - self.can_submit && self.validation.is_valid - } - - /// Get field value as string - pub fn get_field_value(&self, field_name: &str) -> Option { - self.fields.iter() - .find(|f| f.name == field_name) - .map(|field| match &field.field_type { - ConfigField::Text { value, .. } => value.clone(), - ConfigField::Number { value, .. } => value.to_string(), - ConfigField::Boolean { value } => value.to_string(), - ConfigField::Select { value, .. } => value.clone(), - }) - } - - /// Get all form values - pub fn get_all_values(&self) -> HashMap { - self.fields.iter() - .map(|field| { - let value = match &field.field_type { - ConfigField::Text { value, .. } => value.clone(), - ConfigField::Number { value, .. } => value.to_string(), - ConfigField::Boolean { value } => value.to_string(), - ConfigField::Select { value, .. } => value.clone(), - }; - (field.name.clone(), value) - }) - .collect() - } - - /// Create field display text - fn format_field_display(&self, field: &FormField, is_selected: bool) -> String { - let field_display = match &field.field_type { - ConfigField::Text { value, placeholder } => { - if value.is_empty() { - format!("{}: [{}]", field.label, placeholder) - } else { - format!("{}: {}", field.label, value) - } - }, - ConfigField::Number { value, min, max } => { - format!("{}: {} (range: {}-{})", field.label, value, min, max) - }, - ConfigField::Boolean { value } => { - let checkbox = if *value { "☑" } else { "☐" }; - format!("{} {}", checkbox, field.label) - }, - ConfigField::Select { value, options } => { - format!("{}: {} ▼ ({})", field.label, value, options.len()) - }, - }; - - if is_selected && self.input_mode == InputMode::Editing { - format!("► {} ◄", field_display) - } else if is_selected { - format!("> {}", field_display) - } else { - format!(" {}", field_display) - } - } -} - -impl FinancialWidget for ConfigForm { - type Data = Vec; - - fn update_data(&mut self, data: Self::Data) { - self.fields = data; - self.validate_form(); - } - - fn clear(&mut self) { - self.fields.clear(); - self.input_mode = InputMode::Normal; - self.selected_field = 0; - self.input_buffer.clear(); - self.validation = ValidationResult { - is_valid: true, - errors: HashMap::new(), - }; - } - - fn title(&self) -> &str { - &self.title - } - - fn has_data(&self) -> bool { - !self.fields.is_empty() - } -} - -impl Widget for ConfigForm { - fn render(self, area: Rect, buf: &mut Buffer) { - let block = create_block(&self.title, &self.colors); - let inner = block.inner(area); - block.render(area, buf); - - if !self.has_data() { - let no_data = ratatui::widgets::Paragraph::new("No form fields") - .style(Style::default().fg(self.colors.text)) - .alignment(Alignment::Center); - no_data.render(inner, buf); - return; - } - - // Form status area - let status_area = Rect { - x: inner.x, - y: inner.y, - width: inner.width, - height: 1, - }; - - let mode_text = match self.input_mode { - InputMode::Normal => "Navigate: ↑↓ Select: Enter Space: Toggle Submit: S", - InputMode::Editing => "Editing... Enter: Confirm Esc: Cancel", - }; - - let status_color = if self.can_submit { - self.colors.profit - } else { - self.colors.warning - }; - - let status = Paragraph::new(mode_text) - .style(Style::default().fg(status_color)); - status.render(status_area, buf); - - // Fields area - let fields_area = Rect { - x: inner.x, - y: inner.y + 1, - width: inner.width, - height: inner.height.saturating_sub(1), - }; - - // Create field list items - let mut items = Vec::new(); - for (index, field) in self.fields.into_iter().enumerate() { - let is_selected = index == self.selected_field; - let field_text = self.format_field_display(field, is_selected); - - let style = if is_selected { - Style::default().fg(self.colors.text).bg(Color::DarkGray) - } else { - Style::default().fg(self.colors.text) - }; - - let mut item = ListItem::new(field_text).style(style); - - // Add validation error if present and inline errors are enabled - if self.show_inline_errors { - if let Some(ref error) = field.validation_error { - let error_text = format!(" ⚠ {}", error); - item = ListItem::new(vec![ - Line::from(field_text).style(style), - Line::from(error_text).style(Style::default().fg(self.colors.critical)), - ]); - } - } - - items.push(item); - } - - let list = List::new(items) - .block(Block::default()) - .style(Style::default().fg(self.colors.text)); - - list.render(fields_area, buf); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crossterm::event::KeyCode; - - #[test] - fn test_config_form_creation() { - let form = ConfigForm::new("Test Form"); - assert_eq!(form.title(), "Test Form"); - assert!(!form.has_data()); - assert_eq!(form.input_mode, InputMode::Normal); - } - - #[test] - fn test_add_fields() { - let form = ConfigForm::new("Test") - .add_text_field("name", "Name", "Enter name", true) - .add_number_field("age", "Age", 25.0, 0.0, 100.0, true) - .add_boolean_field("enabled", "Enabled", false) - .add_select_field("type", "Type", vec!["A".to_string(), "B".to_string()], None, true); - - assert_eq!(form.fields.len(), 4); - assert!(form.has_data()); - } - - #[test] - fn test_field_validation() { - let mut form = ConfigForm::new("Test") - .add_text_field("required_field", "Required", "Enter value", true) - .add_number_field("number", "Number", 50.0, 0.0, 100.0, false); - - // Test required field validation - form.set_field_value("required_field", ""); - assert!(!form.validation.is_valid); - - form.set_field_value("required_field", "test"); - assert!(form.validation.is_valid); - - // Test number range validation - form.set_field_value("number", "150.0"); - if let Some(field) = form.fields.iter().find(|f| f.name == "number") { - if let ConfigField::Number { value, .. } = &field.field_type { - assert_eq!(*value, 100.0); // Should be clamped to max - } - } - } - - #[test] - fn test_navigation() { - let mut form = ConfigForm::new("Test") - .add_text_field("field1", "Field 1", "", false) - .add_text_field("field2", "Field 2", "", false); - - assert_eq!(form.selected_field, 0); - - form.handle_input(KeyCode::Down); - assert_eq!(form.selected_field, 1); - - form.handle_input(KeyCode::Up); - assert_eq!(form.selected_field, 0); - } - - #[test] - fn test_editing_mode() { - let mut form = ConfigForm::new("Test") - .add_text_field("test", "Test Field", "placeholder", false); - - assert_eq!(form.input_mode, InputMode::Normal); - - form.handle_input(KeyCode::Enter); - assert_eq!(form.input_mode, InputMode::Editing); - - form.handle_input(KeyCode::Char('h')); - form.handle_input(KeyCode::Char('i')); - assert_eq!(form.input_buffer, "hi"); - - form.handle_input(KeyCode::Enter); - assert_eq!(form.input_mode, InputMode::Normal); - assert_eq!(form.get_field_value("test"), Some("hi".to_string())); - } - - #[test] - fn test_boolean_toggle() { - let mut form = ConfigForm::new("Test") - .add_boolean_field("toggle", "Toggle", false); - - assert_eq!(form.get_field_value("toggle"), Some("false".to_string())); - - form.handle_input(KeyCode::Char(' ')); - assert_eq!(form.get_field_value("toggle"), Some("true".to_string())); - } - - #[test] - fn test_get_all_values() { - let mut form = ConfigForm::new("Test") - .add_text_field("name", "Name", "", false) - .add_boolean_field("enabled", "Enabled", true); - - form.set_field_value("name", "test_value"); - - let values = form.get_all_values(); - assert_eq!(values.get("name"), Some(&"test_value".to_string())); - assert_eq!(values.get("enabled"), Some(&"true".to_string())); - } - - #[test] - fn test_custom_validator() { - let form = ConfigForm::new("Test") - .add_text_field("email", "Email", "Enter email", true) - .add_validator("email", |value| { - if value.contains('@') { - Ok(()) - } else { - Err("Invalid email format".to_string()) - } - }); - - let mut form = form; - form.set_field_value("email", "invalid"); - assert!(!form.validation.is_valid); - - form.set_field_value("email", "test@example.com"); - assert!(form.validation.is_valid); - } -} \ No newline at end of file diff --git a/tli/src/ui/widgets/mod.rs b/tli/src/ui/widgets/mod.rs deleted file mode 100644 index 7cd242cb3..000000000 --- a/tli/src/ui/widgets/mod.rs +++ /dev/null @@ -1,278 +0,0 @@ -//! Custom Ratatui widgets for financial data visualization -//! -//! This module provides specialized widgets for the Foxhunt HFT trading terminal: -//! - Real-time candlestick charts with OHLC data -//! - Order book visualization with bid/ask spreads -//! - P&L heatmaps and sparklines for performance tracking -//! - Risk gauge widgets with color-coded status indicators -//! - Configuration forms with input validation -//! -//! All widgets are optimized for high-frequency updates and minimal screen flicker, -//! supporting mouse and keyboard interactions where appropriate. - -use ratatui::{ - prelude::*, - widgets::{Block, Borders, Widget}, - symbols::DOT, -}; -use std::collections::VecDeque; -use chrono::{DateTime, Utc}; -use rust_decimal::Decimal; -use adaptive_strategy::microstructure::OrderLevel; - -pub mod candlestick_chart; -pub mod order_book; -pub mod pnl_heatmap; -pub mod risk_gauge; -pub mod config_form; -pub mod sparkline; - - -/// Common color scheme for financial widgets -#[derive(Debug, Clone)] -pub struct FinancialColors { - pub profit: Color, - pub loss: Color, - pub neutral: Color, - pub bid: Color, - pub ask: Color, - pub warning: Color, - pub critical: Color, - pub background: Color, - pub text: Color, - pub border: Color, -} - -impl Default for FinancialColors { - fn default() -> Self { - Self { - profit: Color::Green, - loss: Color::Red, - neutral: Color::Yellow, - bid: Color::Cyan, - ask: Color::Magenta, - warning: Color::Yellow, - critical: Color::Red, - background: Color::Black, - text: Color::White, - border: Color::Gray, - } - } -} - -/// Base trait for all financial widgets with real-time data updates -pub trait FinancialWidget { - type Data; - - /// Update widget with new data - fn update_data(&mut self, data: Self::Data); - - /// Clear all data from the widget - fn clear(&mut self); - - /// Get the widget's title - fn title(&self) -> &str; - - /// Check if widget has data to display - fn has_data(&self) -> bool; -} - -/// Common data structures for financial widgets - -/// OHLC (Open, High, Low, Close) candle data -#[derive(Debug, Clone)] -pub struct Candle { - pub timestamp: DateTime, - pub open: Decimal, - pub high: Decimal, - pub low: Decimal, - pub close: Decimal, - pub volume: Decimal, -} - -/// Order book snapshot with bids and asks -#[derive(Debug, Clone)] -pub struct OrderBookSnapshot { - pub timestamp: DateTime, - pub bids: Vec, - pub asks: Vec, - pub spread: Decimal, -} - -/// P&L data point for performance tracking -#[derive(Debug, Clone)] -pub struct PnlData { - pub timestamp: DateTime, - pub realized_pnl: Decimal, - pub unrealized_pnl: Decimal, - pub total_pnl: Decimal, - pub strategy: String, -} - -/// Risk metrics for gauge display -#[derive(Debug, Clone)] -pub struct RiskMetrics { - pub var_utilization: f64, // 0.0 to 1.0 - pub position_utilization: f64, // 0.0 to 1.0 - pub drawdown: Decimal, - pub sharpe_ratio: f64, - pub risk_level: RiskLevel, -} - -/// Risk level classification -#[derive(Debug, Clone, PartialEq)] -pub enum RiskLevel { - Low, - Medium, - High, - Critical, -} - -impl RiskLevel { - pub fn color(&self, colors: &FinancialColors) -> Color { - match self { - RiskLevel::Low => colors.profit, - RiskLevel::Medium => colors.neutral, - RiskLevel::High => colors.warning, - RiskLevel::Critical => colors.critical, - } - } -} - -/// Configuration field types for forms -#[derive(Debug, Clone)] -pub enum ConfigField { - Text { value: String, placeholder: String }, - Number { value: f64, min: f64, max: f64 }, - Boolean { value: bool }, - Select { value: String, options: Vec }, -} - -/// Configuration form field definition -#[derive(Debug, Clone)] -pub struct FormField { - pub name: String, - pub label: String, - pub field_type: ConfigField, - pub required: bool, - pub validation_error: Option, -} - -/// Helper functions for common widget operations - -/// Format decimal for display with appropriate precision -pub fn format_price(price: Decimal, precision: u32) -> String { - format!("{:.precision$}", price, precision = precision as usize) -} - -/// Format percentage with sign -pub fn format_percentage(value: f64) -> String { - if value >= 0.0 { - format!("+{:.2}%", value * 100.0) - } else { - format!("{:.2}%", value * 100.0) - } -} - -/// Get color for price change -pub fn price_change_color(change: Decimal, colors: &FinancialColors) -> Color { - if change > Decimal::ZERO { - colors.profit - } else if change < Decimal::ZERO { - colors.loss - } else { - colors.neutral - } -} - -/// Create bordered block for widgets -pub fn create_block(title: &str, colors: &FinancialColors) -> Block { - Block::default() - .title(title) - .borders(Borders::ALL) - .border_style(Style::default().fg(colors.border)) - .title_style(Style::default().fg(colors.text).add_modifier(Modifier::BOLD)) -} - -/// Utility for maintaining fixed-size data buffers -pub struct CircularBuffer { - data: VecDeque, - capacity: usize, -} - -impl CircularBuffer { - pub fn new(capacity: usize) -> Self { - Self { - data: VecDeque::with_capacity(capacity), - capacity, - } - } - - pub fn push(&mut self, item: T) { - if self.data.len() >= self.capacity { - self.data.pop_front(); - } - self.data.push_back(item); - } - - pub fn iter(&self) -> impl Iterator { - self.data.iter() - } - - pub fn len(&self) -> usize { - self.data.len() - } - - pub fn is_empty(&self) -> bool { - self.data.is_empty() - } - - pub fn clear(&mut self) { - self.data.clear(); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_circular_buffer() { - let mut buffer: CircularBuffer = CircularBuffer::new(3); - - buffer.push(1); - buffer.push(2); - buffer.push(3); - assert_eq!(buffer.len(), 3); - - buffer.push(4); - assert_eq!(buffer.len(), 3); - - let values: Vec<&i32> = buffer.iter().collect(); - assert_eq!(values, vec![&2, &3, &4]); - } - - #[test] - fn test_format_price() { - let price = Decimal::new(12345, 2); // 123.45 - assert_eq!(format_price(price, 2), "123.45"); - assert_eq!(format_price(price, 4), "123.4500"); - } - - #[test] - fn test_format_percentage() { - assert_eq!(format_percentage(0.1234), "+12.34%"); - assert_eq!(format_percentage(-0.0567), "-5.67%"); - assert_eq!(format_percentage(0.0), "+0.00%"); - } - - #[test] - fn test_risk_level_color() { - let colors = FinancialColors::default(); - - assert_eq!(RiskLevel::Low.color(&colors), Color::Green); - assert_eq!(RiskLevel::Medium.color(&colors), Color::Yellow); - assert_eq!(RiskLevel::High.color(&colors), Color::Yellow); - assert_eq!(RiskLevel::Critical.color(&colors), Color::Red); - } -} \ No newline at end of file diff --git a/tli/src/ui/widgets/order_book.rs b/tli/src/ui/widgets/order_book.rs deleted file mode 100644 index 0c54ad096..000000000 --- a/tli/src/ui/widgets/order_book.rs +++ /dev/null @@ -1,527 +0,0 @@ -//! Order book visualization widget for market depth display -//! -//! Displays real-time order book data with: -//! - Bid/ask levels with price, size, and count -//! - Visual depth representation using bars -//! - Spread highlighting and calculation -//! - Size aggregation and formatting -//! - Real-time updates with minimal flicker - -use ratatui::{ - prelude::*, - widgets::{Block, Borders, Widget, Row, Table, Cell}, -}; -use std::cmp::Ordering; -use chrono::{DateTime, Utc}; - -use super::{ - FinancialWidget, FinancialColors, OrderLevel, OrderBookSnapshot, - create_block, format_price -}; - -/// Order book visualization widget -#[derive(Debug)] -pub struct OrderBookWidget { - /// Widget title - title: String, - /// Current order book snapshot - order_book: Option, - /// Color scheme - colors: FinancialColors, - /// Number of levels to display (per side) - depth_levels: usize, - /// Price precision for display - price_precision: u32, - /// Size precision for display - size_precision: u32, - /// Show order count column - show_count: bool, - /// Show visual depth bars - show_depth_bars: bool, - /// Aggregate sizes by price level - aggregate_sizes: bool, -} - -impl OrderBookWidget { - /// Create a new order book widget - pub fn new(title: &str) -> Self { - Self { - title: title.to_string(), - order_book: None, - colors: FinancialColors::default(), - depth_levels: 10, - price_precision: 2, - size_precision: 0, - show_count: true, - show_depth_bars: true, - aggregate_sizes: true, - } - } - - /// Set number of depth levels to display - pub fn with_depth_levels(mut self, levels: usize) -> Self { - self.depth_levels = levels; - self - } - - /// Set price precision - pub fn with_price_precision(mut self, precision: u32) -> Self { - self.price_precision = precision; - self - } - - /// Set size precision - pub fn with_size_precision(mut self, precision: u32) -> Self { - self.size_precision = precision; - self - } - - /// Toggle order count display - pub fn with_count_display(mut self, show_count: bool) -> Self { - self.show_count = show_count; - self - } - - /// Toggle depth bars display - pub fn with_depth_bars(mut self, show_bars: bool) -> Self { - self.show_depth_bars = show_bars; - self - } - - /// Toggle size aggregation - pub fn with_aggregation(mut self, aggregate: bool) -> Self { - self.aggregate_sizes = aggregate; - self - } - - /// Update order book data - pub fn update_order_book(&mut self, order_book: OrderBookSnapshot) { - self.order_book = Some(order_book); - } - - /// Get current spread - pub fn spread(&self) -> Option { - self.order_book.as_ref().map(|ob| ob.spread) - } - - /// Get best bid price - pub fn best_bid(&self) -> Option { - self.order_book.as_ref() - .and_then(|ob| ob.bids.first().map(|level| level.price)) - } - - /// Get best ask price - pub fn best_ask(&self) -> Option { - self.order_book.as_ref() - .and_then(|ob| ob.asks.first().map(|level| level.price)) - } - - /// Format size for display - fn format_size(&self, size: Decimal) -> String { - if size >= Decimal::new(1_000_000, 0) { - format!("{:.1}M", size / Decimal::new(1_000_000, 0)) - } else if size >= Decimal::new(1_000, 0) { - format!("{:.1}K", size / Decimal::new(1_000, 0)) - } else { - format!("{:.precision$}", size, precision = self.size_precision as usize) - } - } - - /// Create depth bar representation - fn create_depth_bar(&self, size: Decimal, max_size: Decimal, width: usize) -> String { - if max_size == Decimal::ZERO { - return " ".repeat(width); - } - - let ratio = (size / max_size).to_f64(); - let bar_length = (ratio * width as f64) as usize; - let bar = "█".repeat(bar_length); - let padding = " ".repeat(width.saturating_sub(bar_length)); - format!("{}{}", bar, padding) - } - - /// Aggregate order levels by price if enabled - fn aggregate_levels(&self, levels: &[OrderLevel]) -> Vec { - if !self.aggregate_sizes { - return levels.to_vec(); - } - - let mut aggregated = std::collections::HashMap::new(); - - for level in levels { - let entry = aggregated.entry(level.price).or_insert(OrderLevel { - price: level.price, - size: Decimal::ZERO, - count: 0, - }); - - entry.size += level.size; - entry.count += level.count; - } - - let mut result: Vec = aggregated.into_values().collect(); - result.sort_by(|a, b| b.price.cmp(&a.price)); // Sort descending by price - result - } - - /// Create table rows for order book display - fn create_order_book_rows(&self) -> Vec { - let mut rows = Vec::new(); - - if let Some(ref book) = self.order_book { - // Process asks (ascending price order for display) - let mut asks = self.aggregate_levels(&book.asks); - asks.sort_by(|a, b| a.price.cmp(&b.price)); - let asks_display: Vec<&OrderLevel> = asks.iter() - .take(self.depth_levels) - .collect(); - - // Process bids (descending price order) - let mut bids = self.aggregate_levels(&book.bids); - bids.sort_by(|a, b| b.price.cmp(&a.price)); - let bids_display: Vec<&OrderLevel> = bids.iter() - .take(self.depth_levels) - .collect(); - - // Find max size for depth bar scaling - let max_size = asks_display.iter() - .chain(bids_display.iter()) - .map(|level| level.size) - .max() - .unwrap_or(Decimal::ZERO); - - // Display asks (top to bottom, lowest to highest price) - for level in asks_display.into_iter().rev() { - let mut cells = vec![ - Cell::from("").style(Style::default()), // Empty bid side - Cell::from("").style(Style::default()), // Empty bid size - ]; - - if self.show_count { - cells.push(Cell::from("").style(Style::default())); // Empty bid count - } - - // Price column (centered) - cells.push( - Cell::from(format_price(level.price, self.price_precision)) - .style(Style::default().fg(self.colors.ask)) - ); - - // Ask size - cells.push( - Cell::from(self.format_size(level.size)) - .style(Style::default().fg(self.colors.ask)) - ); - - if self.show_count { - cells.push( - Cell::from(level.count.to_string()) - .style(Style::default().fg(self.colors.ask)) - ); - } - - if self.show_depth_bars { - cells.push( - Cell::from(self.create_depth_bar(level.size, max_size, 10)) - .style(Style::default().fg(self.colors.ask)) - ); - } - - rows.push(Row::new(cells)); - } - - // Add spread row - if let (Some(best_bid), Some(best_ask)) = (self.best_bid(), self.best_ask()) { - let spread = best_ask - best_bid; - let spread_bps = if best_bid != Decimal::ZERO { - ((spread / best_bid) * Decimal::new(10000, 0)).round() - } else { - Decimal::ZERO - }; - - let spread_text = format!("Spread: {} ({} bps)", - format_price(spread, self.price_precision), - spread_bps - ); - - let cell_count = if self.show_count { 7 } else { 5 }; - let cell_count = if self.show_depth_bars { cell_count + 1 } else { cell_count }; - - let spread_row = Row::new(vec![ - Cell::from(spread_text) - .style(Style::default().fg(self.colors.neutral).add_modifier(Modifier::BOLD)); - cell_count - ]); - - rows.push(spread_row); - } - - // Display bids (highest to lowest price) - for level in &bids_display { - let mut cells = vec![ - Cell::from(format_price(level.price, self.price_precision)) - .style(Style::default().fg(self.colors.bid)), - Cell::from(self.format_size(level.size)) - .style(Style::default().fg(self.colors.bid)), - ]; - - if self.show_count { - cells.push( - Cell::from(level.count.to_string()) - .style(Style::default().fg(self.colors.bid)) - ); - } - - // Empty ask columns - cells.push(Cell::from("").style(Style::default())); // Price (already filled by bid) - cells.push(Cell::from("").style(Style::default())); // Ask size - - if self.show_count { - cells.push(Cell::from("").style(Style::default())); // Ask count - } - - if self.show_depth_bars { - cells.insert( - if self.show_count { 3 } else { 2 }, - Cell::from(self.create_depth_bar(level.size, max_size, 10)) - .style(Style::default().fg(self.colors.bid)) - ); - - cells.push(Cell::from("").style(Style::default())); // Empty ask depth bar - } - - rows.push(Row::new(cells)); - } - } - - rows - } - - /// Create table headers - fn create_headers(&self) -> Row { - let mut headers = vec!["Bid Price", "Bid Size"]; - - if self.show_count { - headers.push("Bid Count"); - } - - if self.show_depth_bars { - headers.push("Bid Depth"); - } - - headers.push("Price"); - headers.push("Ask Size"); - - if self.show_count { - headers.push("Ask Count"); - } - - if self.show_depth_bars { - headers.push("Ask Depth"); - } - - Row::new(headers.into_iter().map(|h| { - Cell::from(h).style(Style::default().fg(self.colors.text).add_modifier(Modifier::BOLD)) - })) - } - - /// Calculate column widths - fn column_widths(&self) -> Vec { - let mut widths = vec![ - Constraint::Length(12), // Bid Price - Constraint::Length(10), // Bid Size - ]; - - if self.show_count { - widths.push(Constraint::Length(8)); // Bid Count - } - - if self.show_depth_bars { - widths.push(Constraint::Length(12)); // Bid Depth - } - - widths.push(Constraint::Length(12)); // Price (center) - widths.push(Constraint::Length(10)); // Ask Size - - if self.show_count { - widths.push(Constraint::Length(8)); // Ask Count - } - - if self.show_depth_bars { - widths.push(Constraint::Length(12)); // Ask Depth - } - - widths - } -} - -impl FinancialWidget for OrderBookWidget { - type Data = OrderBookSnapshot; - - fn update_data(&mut self, data: Self::Data) { - self.order_book = Some(data); - } - - fn clear(&mut self) { - self.order_book = None; - } - - fn title(&self) -> &str { - &self.title - } - - fn has_data(&self) -> bool { - self.order_book.is_some() - } -} - -impl Widget for OrderBookWidget { - fn render(self, area: Rect, buf: &mut Buffer) { - let block = create_block(&self.title, &self.colors); - let inner = block.inner(area); - block.render(area, buf); - - if !self.has_data() { - let no_data = ratatui::widgets::Paragraph::new("No order book data") - .style(Style::default().fg(self.colors.text)) - .alignment(Alignment::Center); - no_data.render(inner, buf); - return; - } - - // Create status line - let status_area = Rect { - x: inner.x, - y: inner.y, - width: inner.width, - height: 1, - }; - - let mut status_text = String::new(); - if let (Some(bid), Some(ask)) = (self.best_bid(), self.best_ask()) { - status_text = format!("Best: {} / {}", - format_price(bid, self.price_precision), - format_price(ask, self.price_precision) - ); - - if let Some(spread) = self.spread() { - status_text.push_str(&format!(" | Spread: {}", - format_price(spread, self.price_precision) - )); - } - } - - let status = ratatui::widgets::Paragraph::new(status_text) - .style(Style::default().fg(self.colors.text)); - status.render(status_area, buf); - - // Table area - let table_area = Rect { - x: inner.x, - y: inner.y + 1, - width: inner.width, - height: inner.height.saturating_sub(1), - }; - - // Create and render table - let rows = self.create_order_book_rows(); - let header = self.create_headers(); - let widths = self.column_widths(); - - let table = Table::new(rows, widths) - .header(header) - .block(Block::default()) - .style(Style::default().fg(self.colors.text)) - .highlight_style(Style::default().bg(Color::DarkGray)) - .column_spacing(1); - - table.render(table_area, buf); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use chrono::Utc; - - fn create_test_order_book() -> OrderBookSnapshot { - OrderBookSnapshot { - timestamp: Utc::now(), - bids: vec![ - OrderLevel { price: Decimal::new(10000, 2), size: Decimal::new(100, 0), count: 5 }, - OrderLevel { price: Decimal::new(9999, 2), size: Decimal::new(200, 0), count: 3 }, - OrderLevel { price: Decimal::new(9998, 2), size: Decimal::new(150, 0), count: 2 }, - ], - asks: vec![ - OrderLevel { price: Decimal::new(10001, 2), size: Decimal::new(120, 0), count: 4 }, - OrderLevel { price: Decimal::new(10002, 2), size: Decimal::new(180, 0), count: 6 }, - OrderLevel { price: Decimal::new(10003, 2), size: Decimal::new(90, 0), count: 1 }, - ], - spread: Decimal::new(1, 2), // 0.01 - } - } - - #[test] - fn test_order_book_widget_creation() { - let widget = OrderBookWidget::new("Test Order Book"); - assert_eq!(widget.title(), "Test Order Book"); - assert!(!widget.has_data()); - } - - #[test] - fn test_update_order_book() { - let mut widget = OrderBookWidget::new("Test"); - let book = create_test_order_book(); - - widget.update_order_book(book); - assert!(widget.has_data()); - } - - #[test] - fn test_best_bid_ask() { - let mut widget = OrderBookWidget::new("Test"); - let book = create_test_order_book(); - - widget.update_order_book(book); - - assert_eq!(widget.best_bid(), Some(Decimal::new(10000, 2))); - assert_eq!(widget.best_ask(), Some(Decimal::new(10001, 2))); - assert_eq!(widget.spread(), Some(Decimal::new(1, 2))); - } - - #[test] - fn test_format_size() { - let widget = OrderBookWidget::new("Test"); - - assert_eq!(widget.format_size(Decimal::new(500, 0)), "500"); - assert_eq!(widget.format_size(Decimal::new(1500, 0)), "1.5K"); - assert_eq!(widget.format_size(Decimal::new(2500000, 0)), "2.5M"); - } - - #[test] - fn test_depth_bar() { - let widget = OrderBookWidget::new("Test"); - let max_size = Decimal::new(1000, 0); - - let bar = widget.create_depth_bar(Decimal::new(500, 0), max_size, 10); - assert_eq!(bar.len(), 10); - assert!(bar.contains("█")); - } - - #[test] - fn test_aggregate_levels() { - let widget = OrderBookWidget::new("Test").with_aggregation(true); - let levels = vec![ - OrderLevel { price: Decimal::new(100, 0), size: Decimal::new(50, 0), count: 1 }, - OrderLevel { price: Decimal::new(100, 0), size: Decimal::new(30, 0), count: 2 }, - OrderLevel { price: Decimal::new(101, 0), size: Decimal::new(25, 0), count: 1 }, - ]; - - let aggregated = widget.aggregate_levels(&levels); - assert_eq!(aggregated.len(), 2); - - let level_100 = aggregated.iter().find(|l| l.price == Decimal::new(100, 0)).unwrap(); - assert_eq!(level_100.size, Decimal::new(80, 0)); - assert_eq!(level_100.count, 3); - } -} \ No newline at end of file diff --git a/tli/src/ui/widgets/pnl_heatmap.rs b/tli/src/ui/widgets/pnl_heatmap.rs deleted file mode 100644 index 2e4bf916e..000000000 --- a/tli/src/ui/widgets/pnl_heatmap.rs +++ /dev/null @@ -1,530 +0,0 @@ -//! P&L heatmap widget for portfolio performance visualization -//! -//! Displays profit and loss data as a color-coded heatmap with: -//! - Strategy-based or time-based grouping -//! - Color intensity based on P&L magnitude -//! - Interactive selection and details -//! - Real-time updates with performance metrics -//! - Configurable color schemes and thresholds - -use ratatui::{ - prelude::*, - widgets::{Block, Borders, Widget, Cell, Row, Table}, -}; -use std::collections::HashMap; -use chrono::{DateTime, Utc, Duration, Timelike}; - -use super::{ - FinancialWidget, FinancialColors, PnlData, - create_block, format_price, format_percentage -}; - -/// P&L heatmap grouping method -#[derive(Debug, Clone, PartialEq)] -pub enum HeatmapGrouping { - Strategy, // Group by trading strategy - TimeHourly, // Group by hour - TimeDaily, // Group by day - TimeWeekly, // Group by week - Instrument, // Group by trading instrument -} - -/// Heatmap cell data -#[derive(Debug, Clone)] -pub struct HeatmapCell { - pub label: String, - pub value: Decimal, - pub count: u32, - pub percentage: f64, - pub last_update: DateTime, -} - -/// P&L heatmap widget -#[derive(Debug)] -pub struct PnlHeatmap { - /// Widget title - title: String, - /// P&L data points - data: Vec, - /// Color scheme - colors: FinancialColors, - /// Grouping method - grouping: HeatmapGrouping, - /// Show percentage values - show_percentage: bool, - /// Show trade counts - show_counts: bool, - /// Value precision - precision: u32, - /// Color intensity levels - intensity_levels: Vec, - /// Selected cell (for interaction) - selected_cell: Option, -} - -impl PnlHeatmap { - /// Create a new P&L heatmap widget - pub fn new(title: &str) -> Self { - Self { - title: title.to_string(), - data: Vec::new(), - colors: FinancialColors::default(), - grouping: HeatmapGrouping::Strategy, - show_percentage: true, - show_counts: false, - precision: 2, - intensity_levels: vec![0.1, 0.25, 0.5, 0.75, 1.0], - selected_cell: None, - } - } - - /// Set grouping method - pub fn with_grouping(mut self, grouping: HeatmapGrouping) -> Self { - self.grouping = grouping; - self - } - - /// Toggle percentage display - pub fn with_percentage(mut self, show: bool) -> Self { - self.show_percentage = show; - self - } - - /// Toggle count display - pub fn with_counts(mut self, show: bool) -> Self { - self.show_counts = show; - self - } - - /// Set value precision - pub fn with_precision(mut self, precision: u32) -> Self { - self.precision = precision; - self - } - - /// Set color intensity levels - pub fn with_intensity_levels(mut self, levels: Vec) -> Self { - self.intensity_levels = levels; - self - } - - /// Add P&L data points - pub fn add_data(&mut self, pnl_data: Vec) { - self.data.extend(pnl_data); - } - - /// Set selected cell for highlighting - pub fn select_cell(&mut self, label: Option) { - self.selected_cell = label; - } - - /// Group P&L data according to current grouping method - fn group_data(&self) -> HashMap> { - let mut groups = HashMap::new(); - - for data in &self.data { - let key = match self.grouping { - HeatmapGrouping::Strategy => data.strategy.clone(), - HeatmapGrouping::TimeHourly => { - format!("{:02}:00", data.timestamp.hour()) - }, - HeatmapGrouping::TimeDaily => { - data.timestamp.format("%Y-%m-%d").to_string() - }, - HeatmapGrouping::TimeWeekly => { - let week_start = data.timestamp.date_naive() - - Duration::days(data.timestamp.weekday().num_days_from_monday() as i64); - format!("Week {}", week_start.format("%Y-%m-%d")) - }, - HeatmapGrouping::Instrument => { - // Extract instrument from strategy name if available - data.strategy.split('_').next().unwrap_or(&data.strategy).to_string() - }, - }; - - groups.entry(key).or_insert_with(Vec::new).push(data); - } - - groups - } - - /// Calculate aggregated metrics for a group - fn calculate_group_metrics(&self, group_data: &[&PnlData]) -> HeatmapCell { - if group_data.is_empty() { - return HeatmapCell { - label: "Empty".to_string(), - value: Decimal::ZERO, - count: 0, - percentage: 0.0, - last_update: Utc::now(), - }; - } - - let total_pnl: Decimal = group_data.iter() - .map(|d| d.total_pnl) - .sum(); - - let total_capital: Decimal = group_data.iter() - .map(|d| d.realized_pnl.abs() + d.unrealized_pnl.abs()) - .sum(); - - let percentage = if total_capital != Decimal::ZERO { - (total_pnl / total_capital).to_f64() * 100.0 - } else { - 0.0 - }; - - let last_update = group_data.iter() - .map(|d| d.timestamp) - .max() - .unwrap_or_else(Utc::now); - - HeatmapCell { - label: format!("Group ({})", group_data.len()), - value: total_pnl, - count: group_data.len() as u32, - percentage, - last_update, - } - } - - /// Get color for P&L value based on intensity - fn get_pnl_color(&self, value: Decimal, max_abs_value: Decimal) -> Color { - if value == Decimal::ZERO { - return Color::Gray; - } - - let intensity = if max_abs_value != Decimal::ZERO { - (value.abs() / max_abs_value).to_f64() - } else { - 0.0 - }; - - let base_color = if value > Decimal::ZERO { - self.colors.profit - } else { - self.colors.loss - }; - - // Adjust color intensity based on magnitude - match intensity { - i if i <= 0.2 => Color::DarkGray, - i if i <= 0.4 => self.dim_color(base_color, 0.6), - i if i <= 0.6 => self.dim_color(base_color, 0.8), - i if i <= 0.8 => base_color, - _ => self.brighten_color(base_color), - } - } - - /// Dim a color for lower intensity - fn dim_color(&self, color: Color, factor: f32) -> Color { - match color { - Color::Red => Color::Rgb( - (255.0 * factor) as u8, - 0, - 0 - ), - Color::Green => Color::Rgb( - 0, - (255.0 * factor) as u8, - 0 - ), - _ => color, - } - } - - /// Brighten a color for higher intensity - fn brighten_color(&self, color: Color) -> Color { - match color { - Color::Red => Color::LightRed, - Color::Green => Color::LightGreen, - Color::Yellow => Color::LightYellow, - _ => color, - } - } - - /// Create table rows for heatmap display - fn create_heatmap_rows(&self) -> (Vec, Vec) { - let groups = self.group_data(); - if groups.is_empty() { - return (Vec::new(), Vec::new()); - } - - let mut cells_data: Vec<(String, HeatmapCell)> = groups.into_iter() - .map(|(key, group)| (key.clone(), self.calculate_group_metrics(&group))) - .collect(); - - // Sort by P&L value (descending) - cells_data.sort_by(|a, b| b.1.value.cmp(&a.1.value)); - - // Find max absolute value for color scaling - let max_abs_value = cells_data.iter() - .map(|(_, cell)| cell.value.abs()) - .max() - .unwrap_or(Decimal::ZERO); - - let mut rows = Vec::new(); - - // Group into rows (e.g., 4 columns per row) - let cols_per_row = 4; - for chunk in cells_data.chunks(cols_per_row) { - let mut row_cells = Vec::new(); - - for (key, cell) in chunk { - let bg_color = self.get_pnl_color(cell.value, max_abs_value); - let text_color = if matches!(bg_color, Color::DarkGray | Color::Gray) { - Color::White - } else { - Color::Black - }; - - let is_selected = self.selected_cell.as_ref() - .map(|selected| selected == key) - .unwrap_or(false); - - let mut cell_text = format!("{}\n{}", - key, - format_price(cell.value, self.precision) - ); - - if self.show_percentage { - cell_text.push_str(&format!("\n{}", format_percentage(cell.percentage / 100.0))); - } - - if self.show_counts { - cell_text.push_str(&format!("\n({} trades)", cell.count)); - } - - let style = Style::default() - .bg(bg_color) - .fg(text_color); - - let style = if is_selected { - style.add_modifier(Modifier::BOLD | Modifier::UNDERLINED) - } else { - style - }; - - row_cells.push(Cell::from(cell_text).style(style)); - } - - // Fill remaining columns if needed - while row_cells.len() < cols_per_row { - row_cells.push(Cell::from("").style(Style::default())); - } - - rows.push(Row::new(row_cells).height(if self.show_counts { 4 } else { 3 })); - } - - let constraints = vec![Constraint::Percentage(25); cols_per_row]; - (rows, constraints) - } - - /// Create summary statistics - fn create_summary(&self) -> String { - if self.data.is_empty() { - return "No data available".to_string(); - } - - let total_pnl: Decimal = self.data.iter().map(|d| d.total_pnl).sum(); - let realized_pnl: Decimal = self.data.iter().map(|d| d.realized_pnl).sum(); - let unrealized_pnl: Decimal = self.data.iter().map(|d| d.unrealized_pnl).sum(); - - let profitable_count = self.data.iter() - .filter(|d| d.total_pnl > Decimal::ZERO) - .count(); - - let win_rate = if !self.data.is_empty() { - (profitable_count as f64 / self.data.len() as f64) * 100.0 - } else { - 0.0 - }; - - format!( - "Total: {} | Realized: {} | Unrealized: {} | Win Rate: {:.1}% ({}/{})", - format_price(total_pnl, self.precision), - format_price(realized_pnl, self.precision), - format_price(unrealized_pnl, self.precision), - win_rate, - profitable_count, - self.data.len() - ) - } -} - -impl FinancialWidget for PnlHeatmap { - type Data = Vec; - - fn update_data(&mut self, data: Self::Data) { - self.data = data; - } - - fn clear(&mut self) { - self.data.clear(); - self.selected_cell = None; - } - - fn title(&self) -> &str { - &self.title - } - - fn has_data(&self) -> bool { - !self.data.is_empty() - } -} - -impl Widget for PnlHeatmap { - fn render(self, area: Rect, buf: &mut Buffer) { - let block = create_block(&self.title, &self.colors); - let inner = block.inner(area); - block.render(area, buf); - - if !self.has_data() { - let no_data = ratatui::widgets::Paragraph::new("No P&L data") - .style(Style::default().fg(self.colors.text)) - .alignment(Alignment::Center); - no_data.render(inner, buf); - return; - } - - // Summary area - let summary_area = Rect { - x: inner.x, - y: inner.y, - width: inner.width, - height: 1, - }; - - let summary_text = self.create_summary(); - let summary = ratatui::widgets::Paragraph::new(summary_text) - .style(Style::default().fg(self.colors.text)) - .wrap(ratatui::widgets::Wrap { trim: true }); - summary.render(summary_area, buf); - - // Heatmap area - let heatmap_area = Rect { - x: inner.x, - y: inner.y + 1, - width: inner.width, - height: inner.height.saturating_sub(1), - }; - - let (rows, constraints) = self.create_heatmap_rows(); - - if !rows.is_empty() { - let table = Table::new(rows, constraints) - .block(Block::default()) - .style(Style::default()) - .column_spacing(1); - - table.render(heatmap_area, buf); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use chrono::Utc; - - fn create_test_pnl_data() -> Vec { - vec![ - PnlData { - timestamp: Utc::now(), - realized_pnl: Decimal::new(100, 0), - unrealized_pnl: Decimal::new(50, 0), - total_pnl: Decimal::new(150, 0), - strategy: "Strategy A".to_string(), - }, - PnlData { - timestamp: Utc::now(), - realized_pnl: Decimal::new(-80, 0), - unrealized_pnl: Decimal::new(20, 0), - total_pnl: Decimal::new(-60, 0), - strategy: "Strategy B".to_string(), - }, - PnlData { - timestamp: Utc::now(), - realized_pnl: Decimal::new(200, 0), - unrealized_pnl: Decimal::new(-30, 0), - total_pnl: Decimal::new(170, 0), - strategy: "Strategy A".to_string(), - }, - ] - } - - #[test] - fn test_pnl_heatmap_creation() { - let heatmap = PnlHeatmap::new("Test Heatmap"); - assert_eq!(heatmap.title(), "Test Heatmap"); - assert!(!heatmap.has_data()); - } - - #[test] - fn test_add_data() { - let mut heatmap = PnlHeatmap::new("Test"); - let data = create_test_pnl_data(); - - heatmap.add_data(data); - assert!(heatmap.has_data()); - assert_eq!(heatmap.data.len(), 3); - } - - #[test] - fn test_group_data_by_strategy() { - let mut heatmap = PnlHeatmap::new("Test").with_grouping(HeatmapGrouping::Strategy); - let data = create_test_pnl_data(); - heatmap.add_data(data); - - let groups = heatmap.group_data(); - assert_eq!(groups.len(), 2); // Strategy A and Strategy B - assert_eq!(groups["Strategy A"].len(), 2); - assert_eq!(groups["Strategy B"].len(), 1); - } - - #[test] - fn test_calculate_group_metrics() { - let heatmap = PnlHeatmap::new("Test"); - let data = create_test_pnl_data(); - let strategy_a_data: Vec<&PnlData> = data.iter() - .filter(|d| d.strategy == "Strategy A") - .collect(); - - let metrics = heatmap.calculate_group_metrics(&strategy_a_data); - assert_eq!(metrics.value, Decimal::new(320, 0)); // 150 + 170 - assert_eq!(metrics.count, 2); - } - - #[test] - fn test_color_calculation() { - let heatmap = PnlHeatmap::new("Test"); - let max_value = Decimal::new(1000, 0); - - let positive_color = heatmap.get_pnl_color(Decimal::new(500, 0), max_value); - let negative_color = heatmap.get_pnl_color(Decimal::new(-500, 0), max_value); - let zero_color = heatmap.get_pnl_color(Decimal::ZERO, max_value); - - assert_ne!(positive_color, negative_color); - assert_eq!(zero_color, Color::Gray); - } - - #[test] - fn test_update_data() { - let mut heatmap = PnlHeatmap::new("Test"); - let data = create_test_pnl_data(); - - heatmap.update_data(data); - assert_eq!(heatmap.data.len(), 3); - } - - #[test] - fn test_selected_cell() { - let mut heatmap = PnlHeatmap::new("Test"); - - heatmap.select_cell(Some("Strategy A".to_string())); - assert_eq!(heatmap.selected_cell, Some("Strategy A".to_string())); - - heatmap.select_cell(None); - assert_eq!(heatmap.selected_cell, None); - } -} \ No newline at end of file diff --git a/tli/src/ui/widgets/risk_gauge.rs b/tli/src/ui/widgets/risk_gauge.rs deleted file mode 100644 index 012a9f74f..000000000 --- a/tli/src/ui/widgets/risk_gauge.rs +++ /dev/null @@ -1,502 +0,0 @@ -//! Risk gauge widget for real-time risk monitoring -//! -//! Displays risk metrics as circular gauges with: -//! - VaR utilization percentage -//! - Position size utilization -//! - Drawdown indicators -//! - Color-coded risk levels -//! - Threshold warnings and alerts -//! - Historical risk trends - -use ratatui::{ - prelude::*, - widgets::{Block, Borders, Widget, Gauge, LineGauge}, - symbols::DOT, -}; -use std::collections::VecDeque; -use chrono::{DateTime, Utc}; -use num_traits::ToPrimitive; - -use super::{ - FinancialWidget, FinancialColors, RiskMetrics, RiskLevel, - create_block, format_price, format_percentage -}; - -/// Risk gauge display style -#[derive(Debug, Clone, PartialEq)] -pub enum GaugeStyle { - Circular, // Circular gauge (full circle) - Semicircular, // Half-circle gauge - Linear, // Linear progress bar - Compact, // Minimal linear display -} - -/// Individual risk metric gauge -#[derive(Debug, Clone)] -pub struct RiskGauge { - /// Widget title - title: String, - /// Current risk metrics - metrics: Option, - /// Color scheme - colors: FinancialColors, - /// Gauge display style - style: GaugeStyle, - /// Risk thresholds for color coding - thresholds: RiskThresholds, - /// Show percentage labels - show_labels: bool, - /// Show trend indicators - show_trends: bool, - /// Historical data for trend analysis - history: VecDeque, - /// Maximum history length - max_history: usize, -} - -/// Risk level thresholds -#[derive(Debug, Clone)] -pub struct RiskThresholds { - /// Low risk threshold (green) - pub low: f64, - /// Medium risk threshold (yellow) - pub medium: f64, - /// High risk threshold (orange) - pub high: f64, - /// Critical risk threshold (red) - pub critical: f64, -} - -impl Default for RiskThresholds { - fn default() -> Self { - Self { - low: 0.25, // 25% - medium: 0.50, // 50% - high: 0.75, // 75% - critical: 0.90, // 90% - } - } -} - -impl RiskGauge { - /// Create a new risk gauge widget - pub fn new(title: &str) -> Self { - Self { - title: title.to_string(), - metrics: None, - colors: FinancialColors::default(), - style: GaugeStyle::Semicircular, - thresholds: RiskThresholds::default(), - show_labels: true, - show_trends: false, - history: VecDeque::new(), - max_history: 100, - } - } - - /// Set gauge display style - pub fn with_style(mut self, style: GaugeStyle) -> Self { - self.style = style; - self - } - - /// Set risk thresholds - pub fn with_thresholds(mut self, thresholds: RiskThresholds) -> Self { - self.thresholds = thresholds; - self - } - - /// Toggle percentage labels - pub fn with_labels(mut self, show: bool) -> Self { - self.show_labels = show; - self - } - - /// Toggle trend indicators - pub fn with_trends(mut self, show: bool) -> Self { - self.show_trends = show; - self - } - - /// Set maximum history length - pub fn with_history_length(mut self, length: usize) -> Self { - self.max_history = length; - self - } - - /// Update risk metrics - pub fn update_metrics(&mut self, metrics: RiskMetrics) { - // Add to history - if self.history.len() >= self.max_history { - self.history.pop_front(); - } - self.history.push_back(metrics.clone()); - - self.metrics = Some(metrics); - } - - /// Get current risk level - pub fn current_risk_level(&self) -> RiskLevel { - self.metrics.as_ref() - .map(|m| m.risk_level.clone()) - .unwrap_or(RiskLevel::Low) - } - - /// Get risk level based on utilization - fn risk_level_from_utilization(&self, utilization: f64) -> RiskLevel { - if utilization >= self.thresholds.critical { - RiskLevel::Critical - } else if utilization >= self.thresholds.high { - RiskLevel::High - } else if utilization >= self.thresholds.medium { - RiskLevel::Medium - } else { - RiskLevel::Low - } - } - - /// Get color for utilization level - fn color_for_utilization(&self, utilization: f64) -> Color { - let level = self.risk_level_from_utilization(utilization); - level.color(&self.colors) - } - - /// Get trend indicator for a metric - fn get_trend(&self, current: f64, metric_extractor: fn(&RiskMetrics) -> f64) -> Option<&'static str> { - if !self.show_trends || self.history.len() < 2 { - return None; - } - - let previous = self.history.get(self.history.len() - 2) - .map(metric_extractor) - .unwrap_or(current); - - if current > previous * 1.05 { - Some("↗") - } else if current < previous * 0.95 { - Some("↘") - } else { - Some("→") - } - } - - /// Create gauge widget for a specific metric - fn create_gauge_widget(&self, title: &str, value: f64, area: Rect) -> impl Widget { - let percentage = (value * 100.0).min(100.0).max(0.0) as u16; - let color = self.color_for_utilization(value); - - let label = if self.show_labels { - if let Some(trend) = self.get_trend(value, |_| value) { - format!("{} {:.1}% {}", title, value * 100.0, trend) - } else { - format!("{} {:.1}%", title, value * 100.0) - } - } else { - title.to_string() - }; - - match self.style { - GaugeStyle::Circular | GaugeStyle::Semicircular => { - Gauge::default() - .block(Block::default().title(label).borders(Borders::ALL)) - .gauge_style(Style::default().fg(color)) - .percent(percentage) - .use_unicode(true) - }, - GaugeStyle::Linear => { - LineGauge::default() - .block(Block::default().title(label).borders(Borders::ALL)) - .gauge_style(Style::default().fg(color)) - .line_set(symbols::line::THICK) - .ratio(value) - }, - GaugeStyle::Compact => { - LineGauge::default() - .block(Block::default().title(label)) - .gauge_style(Style::default().fg(color)) - .line_set(symbols::line::NORMAL) - .ratio(value) - }, - } - } - - /// Create status summary - fn create_status_summary(&self) -> String { - if let Some(ref metrics) = self.metrics { - let var_pct = metrics.var_utilization * 100.0; - let pos_pct = metrics.position_utilization * 100.0; - let risk_text = match metrics.risk_level { - RiskLevel::Low => "LOW", - RiskLevel::Medium => "MEDIUM", - RiskLevel::High => "HIGH", - RiskLevel::Critical => "CRITICAL", - }; - - format!( - "VaR: {:.1}% | Pos: {:.1}% | DD: {} | Sharpe: {:.2} | Risk: {}", - var_pct, - pos_pct, - format_price(metrics.drawdown, 2), - metrics.sharpe_ratio, - risk_text - ) - } else { - "No risk data available".to_string() - } - } - - /// Calculate layout for multiple gauges - fn calculate_gauge_layout(&self, area: Rect) -> (Rect, Rect, Rect, Rect) { - match self.style { - GaugeStyle::Compact => { - // Stack gauges vertically for compact display - let height_per_gauge = area.height / 4; - ( - Rect { x: area.x, y: area.y, width: area.width, height: height_per_gauge }, - Rect { x: area.x, y: area.y + height_per_gauge, width: area.width, height: height_per_gauge }, - Rect { x: area.x, y: area.y + 2 * height_per_gauge, width: area.width, height: height_per_gauge }, - Rect { x: area.x, y: area.y + 3 * height_per_gauge, width: area.width, height: height_per_gauge }, - ) - }, - _ => { - // 2x2 grid for other styles - let width_half = area.width / 2; - let height_half = area.height / 2; - ( - Rect { x: area.x, y: area.y, width: width_half, height: height_half }, - Rect { x: area.x + width_half, y: area.y, width: width_half, height: height_half }, - Rect { x: area.x, y: area.y + height_half, width: width_half, height: height_half }, - Rect { x: area.x + width_half, y: area.y + height_half, width: width_half, height: height_half }, - ) - } - } - } -} - -impl FinancialWidget for RiskGauge { - type Data = RiskMetrics; - - fn update_data(&mut self, data: Self::Data) { - self.update_metrics(data); - } - - fn clear(&mut self) { - self.metrics = None; - self.history.clear(); - } - - fn title(&self) -> &str { - &self.title - } - - fn has_data(&self) -> bool { - self.metrics.is_some() - } -} - -impl Widget for RiskGauge { - fn render(self, area: Rect, buf: &mut Buffer) { - let block = create_block(&self.title, &self.colors); - let inner = block.inner(area); - block.render(area, buf); - - let metrics = match self.metrics.as_ref() { - Some(m) => m, - None => { - let no_data = ratatui::widgets::Paragraph::new("No risk data") - .style(Style::default().fg(self.colors.text)) - .alignment(Alignment::Center); - no_data.render(inner, buf); - return; - }, - }; - - // Status summary area - let status_area = Rect { - x: inner.x, - y: inner.y, - width: inner.width, - height: 1, - }; - - let status_text = self.create_status_summary(); - let status = ratatui::widgets::Paragraph::new(status_text) - .style(Style::default().fg(self.colors.text)) - .wrap(ratatui::widgets::Wrap { trim: true }); - status.render(status_area, buf); - - // Gauges area - let gauges_area = Rect { - x: inner.x, - y: inner.y + 1, - width: inner.width, - height: inner.height.saturating_sub(1), - }; - - if gauges_area.height == 0 { - return; - } - - let (var_area, pos_area, dd_area, sharpe_area) = self.calculate_gauge_layout(gauges_area); - - // VaR Utilization Gauge - if var_area.width > 0 && var_area.height > 0 { - let var_gauge = self.create_gauge_widget("VaR", metrics.var_utilization, var_area); - var_gauge.render(var_area, buf); - } - - // Position Utilization Gauge - if pos_area.width > 0 && pos_area.height > 0 { - let pos_gauge = self.create_gauge_widget("Position", metrics.position_utilization, pos_area); - pos_gauge.render(pos_area, buf); - } - - // Drawdown Gauge (as percentage of max acceptable) - if dd_area.width > 0 && dd_area.height > 0 { - let dd_ratio = if metrics.drawdown.abs() <= Decimal::new(1000, 0) { - ToPrimitive::to_f64(&(metrics.drawdown.abs() / Decimal::new(1000, 0))).unwrap_or(0.0) - } else { - 1.0 - }; - let dd_gauge = self.create_gauge_widget("Drawdown", dd_ratio, dd_area); - dd_gauge.render(dd_area, buf); - } - - // Sharpe Ratio Gauge (normalized to 0-1, where 1.0 Sharpe = 50% of gauge) - if sharpe_area.width > 0 && sharpe_area.height > 0 { - let sharpe_ratio = (metrics.sharpe_ratio / 2.0).min(1.0).max(0.0); - let sharpe_gauge = self.create_gauge_widget("Sharpe", sharpe_ratio, sharpe_area); - sharpe_gauge.render(sharpe_area, buf); - } - } -} - -#[cfg(test)] -#[allow(clippy::unwrap_used, clippy::expect_used)] -mod tests { - use super::*; - - fn create_test_risk_metrics() -> RiskMetrics { - RiskMetrics { - var_utilization: 0.65, - position_utilization: 0.45, - drawdown: Decimal::new(-250, 0), - sharpe_ratio: 1.25, - risk_level: RiskLevel::Medium, - } - } - - #[test] - fn test_risk_gauge_creation() { - let gauge = RiskGauge::new("Test Risk Gauge"); - assert_eq!(gauge.title(), "Test Risk Gauge"); - assert!(!gauge.has_data()); - assert_eq!(gauge.current_risk_level(), RiskLevel::Low); - } - - #[test] - fn test_update_metrics() { - let mut gauge = RiskGauge::new("Test"); - let metrics = create_test_risk_metrics(); - - gauge.update_metrics(metrics.clone()); - assert!(gauge.has_data()); - assert_eq!(gauge.current_risk_level(), RiskLevel::Medium); - assert_eq!(gauge.history.len(), 1); - } - - #[test] - fn test_risk_level_from_utilization() { - let gauge = RiskGauge::new("Test"); - - assert_eq!(gauge.risk_level_from_utilization(0.1), RiskLevel::Low); - assert_eq!(gauge.risk_level_from_utilization(0.4), RiskLevel::Medium); - assert_eq!(gauge.risk_level_from_utilization(0.8), RiskLevel::High); - assert_eq!(gauge.risk_level_from_utilization(0.95), RiskLevel::Critical); - } - - #[test] - fn test_color_for_utilization() { - let gauge = RiskGauge::new("Test"); - let colors = FinancialColors::default(); - - assert_eq!(gauge.color_for_utilization(0.1), colors.profit); - assert_eq!(gauge.color_for_utilization(0.4), colors.neutral); - assert_eq!(gauge.color_for_utilization(0.8), colors.warning); - assert_eq!(gauge.color_for_utilization(0.95), colors.critical); - } - - #[test] - fn test_trend_calculation() { - let mut gauge = RiskGauge::new("Test").with_trends(true); - - // Add first metric - let metrics1 = RiskMetrics { - var_utilization: 0.5, - position_utilization: 0.3, - drawdown: Decimal::new(-100, 0), - sharpe_ratio: 1.0, - risk_level: RiskLevel::Medium, - }; - gauge.update_metrics(metrics1); - - // Add second metric (higher utilization) - let metrics2 = RiskMetrics { - var_utilization: 0.6, - position_utilization: 0.35, - drawdown: Decimal::new(-120, 0), - sharpe_ratio: 1.1, - risk_level: RiskLevel::Medium, - }; - gauge.update_metrics(metrics2); - - let trend = gauge.get_trend(0.6, |m| m.var_utilization); - assert_eq!(trend, Some("↗")); - } - - #[test] - fn test_custom_thresholds() { - let thresholds = RiskThresholds { - low: 0.2, - medium: 0.4, - high: 0.6, - critical: 0.8, - }; - - let gauge = RiskGauge::new("Test").with_thresholds(thresholds); - - assert_eq!(gauge.risk_level_from_utilization(0.3), RiskLevel::Medium); - assert_eq!(gauge.risk_level_from_utilization(0.7), RiskLevel::High); - } - - #[test] - fn test_history_management() { - let mut gauge = RiskGauge::new("Test").with_history_length(3); - let metrics = create_test_risk_metrics(); - - // Add more metrics than max history - for i in 0..5 { - let mut m = metrics.clone(); - m.var_utilization = 0.1 * i as f64; - gauge.update_metrics(m); - } - - assert_eq!(gauge.history.len(), 3); - assert_eq!(gauge.history[0].var_utilization, 0.2); - assert_eq!(gauge.history[2].var_utilization, 0.4); - } - - #[test] - fn test_status_summary() { - let mut gauge = RiskGauge::new("Test"); - let metrics = create_test_risk_metrics(); - - gauge.update_metrics(metrics); - let summary = gauge.create_status_summary(); - - assert!(summary.contains("VaR: 65.0%")); - assert!(summary.contains("Pos: 45.0%")); - assert!(summary.contains("Sharpe: 1.25")); - assert!(summary.contains("Risk: MEDIUM")); - } -} \ No newline at end of file diff --git a/tli/src/ui/widgets/sparkline.rs b/tli/src/ui/widgets/sparkline.rs deleted file mode 100644 index c9e42ba8b..000000000 --- a/tli/src/ui/widgets/sparkline.rs +++ /dev/null @@ -1,433 +0,0 @@ -//! Sparkline widget for compact time series visualization -//! -//! Provides minimal chart display for: -//! - P&L trends over time -//! - Price movements -//! - Performance metrics -//! - Volume patterns -//! -//! Optimized for real-time updates in small display areas. - -use ratatui::{ - prelude::*, - widgets::{Block, Borders, Widget, Sparkline as RatatuiSparkline}, -}; -use std::collections::VecDeque; -use chrono::{DateTime, Utc}; - -use super::{ - FinancialWidget, FinancialColors, CircularBuffer, - create_block, format_price, price_change_color -}; - -/// Data point for sparkline display -#[derive(Debug, Clone)] -pub struct SparklineData { - pub timestamp: DateTime, - pub value: Decimal, - pub label: Option, -} - -/// Compact sparkline widget for time series data -#[derive(Debug)] -pub struct Sparkline { - /// Widget title - title: String, - /// Data points buffer - data: CircularBuffer, - /// Color scheme - colors: FinancialColors, - /// Data range for normalization - range: Option<(Decimal, Decimal)>, - /// Auto-scale data range - auto_scale: bool, - /// Show current value - show_current_value: bool, - /// Show min/max values - show_range: bool, - /// Value precision for display - precision: u32, - /// Sparkline style - style: SparklineStyle, -} - -/// Sparkline visual style -#[derive(Debug, Clone, PartialEq)] -pub enum SparklineStyle { - Line, - Bar, - Filled, -} - -impl Sparkline { - /// Create a new sparkline widget - pub fn new(title: &str, max_points: usize) -> Self { - Self { - title: title.to_string(), - data: CircularBuffer::new(max_points), - colors: FinancialColors::default(), - range: None, - auto_scale: true, - show_current_value: true, - show_range: false, - precision: 2, - style: SparklineStyle::Line, - } - } - - /// Set data range for normalization - pub fn with_range(mut self, min: Decimal, max: Decimal) -> Self { - self.auto_scale = false; - self.range = Some((min, max)); - self - } - - /// Enable auto-scaling - pub fn with_auto_scale(mut self, auto_scale: bool) -> Self { - self.auto_scale = auto_scale; - if auto_scale { - self.range = None; - } - self - } - - /// Show current value - pub fn with_current_value(mut self, show: bool) -> Self { - self.show_current_value = show; - self - } - - /// Show min/max range - pub fn with_range_display(mut self, show: bool) -> Self { - self.show_range = show; - self - } - - /// Set value precision - pub fn with_precision(mut self, precision: u32) -> Self { - self.precision = precision; - self - } - - /// Set sparkline style - pub fn with_style(mut self, style: SparklineStyle) -> Self { - self.style = style; - self - } - - /// Add a single data point - pub fn add_point(&mut self, data: SparklineData) { - self.data.push(data); - - if self.auto_scale { - self.update_range(); - } - } - - /// Add multiple data points - pub fn add_points(&mut self, points: Vec) { - for point in points { - self.data.push(point); - } - - if self.auto_scale { - self.update_range(); - } - } - - /// Update data range based on current points - fn update_range(&mut self) { - if self.data.is_empty() { - self.range = None; - return; - } - - let mut min_val = Decimal::MAX; - let mut max_val = Decimal::MIN; - - for point in &self.data { - min_val = min_val.min(point.value); - max_val = max_val.max(point.value); - } - - // Add 5% padding if min != max - if min_val != max_val { - let padding = (max_val - min_val) * Decimal::new(5, 2); // 0.05 - self.range = Some((min_val - padding, max_val + padding)); - } else { - // If all values are the same, create a small range around the value - let padding = if min_val == Decimal::ZERO { - Decimal::new(1, 0) - } else { - min_val.abs() * Decimal::new(1, 2) // 0.01 - }; - self.range = Some((min_val - padding, min_val + padding)); - } - } - - /// Get current (latest) value - pub fn current_value(&self) -> Option { - self.data.iter().last().map(|point| point.value) - } - - /// Get value change from first to last point - pub fn value_change(&self) -> Option<(Decimal, Decimal)> { - let points: Vec<&SparklineData> = self.data.iter().collect(); - if points.len() < 2 { - return None; - } - - let first = &points[0]; - let last = &points[points.len() - 1]; - let change = last.value - first.value; - let percentage = if first.value != Decimal::ZERO { - (change / first.value) * Decimal::new(100, 0) - } else { - Decimal::ZERO - }; - - Some((change, percentage)) - } - - /// Convert data to u64 values for ratatui sparkline - fn normalize_data(&self) -> Vec { - if let Some((min_val, max_val)) = self.range { - let range = max_val - min_val; - if range == Decimal::ZERO { - return vec![50; self.data.len()]; // Middle value if no range - } - - self.data.iter().map(|point| { - let normalized = (point.value - min_val) / range; - let scaled = normalized * Decimal::new(100, 0); // Scale to 0-100 - scaled.to_u64().unwrap_or(0).min(100) - }).collect() - } else { - vec![0; self.data.len()] - } - } - - /// Get display color based on trend - fn trend_color(&self) -> Color { - if let Some((change, _)) = self.value_change() { - price_change_color(change, &self.colors) - } else { - self.colors.neutral - } - } - - /// Create status text with current value and trend - fn create_status_text(&self) -> String { - let mut status = String::new(); - - if let Some(current) = self.current_value() { - status.push_str(&format!("Current: {}", format_price(current, self.precision))); - - if let Some((change, percentage)) = self.value_change() { - let sign = if change >= Decimal::ZERO { "+" } else { "" }; - status.push_str(&format!(" ({}{}%)", - sign, - format_price(percentage, 2) - )); - } - } - - if self.show_range { - if let Some((min_val, max_val)) = self.range { - status.push_str(&format!(" Range: {} - {}", - format_price(min_val, self.precision), - format_price(max_val, self.precision) - )); - } - } - - status - } -} - -impl FinancialWidget for Sparkline { - type Data = Vec; - - fn update_data(&mut self, data: Self::Data) { - self.data.clear(); - for point in data { - self.data.push(point); - } - - if self.auto_scale { - self.update_range(); - } - } - - fn clear(&mut self) { - self.data.clear(); - if self.auto_scale { - self.range = None; - } - } - - fn title(&self) -> &str { - &self.title - } - - fn has_data(&self) -> bool { - !self.data.is_empty() - } -} - -impl Widget for Sparkline { - fn render(self, area: Rect, buf: &mut Buffer) { - let block = create_block(&self.title, &self.colors); - let inner = block.inner(area); - block.render(area, buf); - - if !self.has_data() { - let no_data = ratatui::widgets::Paragraph::new("No data") - .style(Style::default().fg(self.colors.text)) - .alignment(Alignment::Center); - no_data.render(inner, buf); - return; - } - - // Status area (if showing current value) - let (sparkline_area, status_area) = if self.show_current_value { - let status_height = 1; - ( - Rect { - x: inner.x, - y: inner.y, - width: inner.width, - height: inner.height.saturating_sub(status_height), - }, - Rect { - x: inner.x, - y: inner.y + inner.height.saturating_sub(status_height), - width: inner.width, - height: status_height, - } - ) - } else { - (inner, Rect::default()) - }; - - // Render sparkline - let data = self.normalize_data(); - let trend_color = self.trend_color(); - - let sparkline = RatatuiSparkline::default() - .block(Block::default()) - .data(&data) - .style(Style::default().fg(trend_color)); - - sparkline.render(sparkline_area, buf); - - // Render status text - if self.show_current_value && status_area.height > 0 { - let status_text = self.create_status_text(); - let status = ratatui::widgets::Paragraph::new(status_text) - .style(Style::default().fg(self.colors.text)) - .alignment(Alignment::Center); - status.render(status_area, buf); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use chrono::Utc; - - fn create_test_data(values: Vec) -> Vec { - values.into_iter().enumerate().map(|(i, val)| { - SparklineData { - timestamp: Utc::now(), - value: Decimal::try_from(val).unwrap(), - label: Some(format!("Point {}", i)), - } - }).collect() - } - - #[test] - fn test_sparkline_creation() { - let sparkline = Sparkline::new("Test Sparkline", 50); - assert_eq!(sparkline.title(), "Test Sparkline"); - assert!(!sparkline.has_data()); - } - - #[test] - fn test_add_point() { - let mut sparkline = Sparkline::new("Test", 10); - let data = SparklineData { - timestamp: Utc::now(), - value: Decimal::from(100), - label: None, - }; - - sparkline.add_point(data); - assert!(sparkline.has_data()); - assert_eq!(sparkline.current_value(), Some(Decimal::from(100))); - } - - #[test] - fn test_value_change() { - let mut sparkline = Sparkline::new("Test", 10); - let points = create_test_data(vec![100.0, 110.0, 105.0]); - - sparkline.add_points(points); - - let (change, percentage) = sparkline.value_change().unwrap(); - assert_eq!(change, Decimal::from(5)); // 105 - 100 - assert_eq!(percentage, Decimal::from(5)); // 5% - } - - #[test] - fn test_auto_scaling() { - let mut sparkline = Sparkline::new("Test", 10).with_auto_scale(true); - let points = create_test_data(vec![10.0, 20.0, 30.0, 15.0]); - - sparkline.add_points(points); - - let (min_val, max_val) = sparkline.range.unwrap(); - assert!(min_val < Decimal::from(10)); - assert!(max_val > Decimal::from(30)); - } - - #[test] - fn test_normalize_data() { - let mut sparkline = Sparkline::new("Test", 10); - sparkline.range = Some((Decimal::from(0), Decimal::from(100))); - - let points = create_test_data(vec![0.0, 50.0, 100.0]); - sparkline.add_points(points); - - let normalized = sparkline.normalize_data(); - assert_eq!(normalized, vec![0, 50, 100]); - } - - #[test] - fn test_trend_color() { - let mut sparkline = Sparkline::new("Test", 10); - let colors = FinancialColors::default(); - - // Positive trend - let points = create_test_data(vec![100.0, 110.0]); - sparkline.add_points(points); - assert_eq!(sparkline.trend_color(), colors.profit); - - // Negative trend - sparkline.clear(); - let points = create_test_data(vec![100.0, 90.0]); - sparkline.add_points(points); - assert_eq!(sparkline.trend_color(), colors.loss); - } - - #[test] - fn test_update_data() { - let mut sparkline = Sparkline::new("Test", 10); - let points = create_test_data(vec![1.0, 2.0, 3.0]); - - sparkline.update_data(points); - assert_eq!(sparkline.data.len(), 3); - assert_eq!(sparkline.current_value(), Some(Decimal::from(3))); - } -} \ No newline at end of file diff --git a/tli/tests/market_data_edge_cases.rs b/tli/tests/market_data_edge_cases.rs deleted file mode 100644 index 50e790ee8..000000000 --- a/tli/tests/market_data_edge_cases.rs +++ /dev/null @@ -1,1042 +0,0 @@ -//! Market Data Edge Case Tests -//! -//! Comprehensive edge case coverage for market data handling, parsing, validation, -//! and real-time updates in the TLI. Tests cover: -//! - Price parsing (scientific notation, extremes, NaN/Inf) -//! - Symbol validation (invalid chars, empty, Unicode) -//! - Timestamp handling (future times, epoch boundaries) -//! - Update rate limiting and high-frequency scenarios -//! - Data staleness detection -//! - Order book edge cases (empty, single level, 100+ levels) -//! - Trade history extremes (zero trades, 10K+ trades) -//! - Connection interruptions and reconnection - -// Suppress false-positive unused_crate_dependencies warnings -// dev-dependencies are shared across ALL test targets in the crate -// This test may not use all deps, but they are required by other integration tests -#![allow(unused_crate_dependencies)] - -use chrono::{DateTime, Utc}; -use std::collections::VecDeque; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use tokio::sync::mpsc; -use tokio::time::{sleep, timeout}; - -use adaptive_strategy::microstructure::OrderLevel; -use tli::dashboard::events::MarketDataDisplayEvent; -use tli::types::{current_unix_nanos, unix_nanos_to_system_time, validate_price, validate_symbol}; - -// ============================================================================ -// HELPER FUNCTIONS AND TYPES -// ============================================================================ - -/// Parse price string that may contain scientific notation or extreme values -fn parse_price_string(price_str: &str) -> Result { - price_str - .parse::() - .map_err(|e| format!("Failed to parse price: {}", e)) -} - -/// Create test market data event -fn create_market_data(symbol: &str, price: f64, timestamp: i64) -> MarketDataDisplayEvent { - MarketDataDisplayEvent { - symbol: symbol.to_string(), - price, - volume: 1000, - timestamp, - bid: Some(price - 0.01), - ask: Some(price + 0.01), - change: Some(0.0), - change_percent: Some(0.0), - } -} - -/// Create order book snapshot -fn create_order_book_snapshot( - bids: Vec<(f64, f64, u32)>, - asks: Vec<(f64, f64, u32)>, -) -> OrderBookSnapshot { - OrderBookSnapshot { - timestamp: Utc::now(), - bids: bids - .into_iter() - .map(|(price, size, count)| OrderLevel { - price, - quantity: size, - order_count: count, - timestamp: Utc::now(), - }) - .collect(), - asks: asks - .into_iter() - .map(|(price, size, count)| OrderLevel { - price, - quantity: size, - order_count: count, - timestamp: Utc::now(), - }) - .collect(), - spread: 0.0, - } -} - -/// Order book snapshot structure for tests -#[derive(Debug, Clone)] -struct OrderBookSnapshot { - timestamp: DateTime, - bids: Vec, - asks: Vec, - spread: f64, -} - -impl OrderBookSnapshot { - fn calculate_spread(&mut self) { - if let (Some(best_bid), Some(best_ask)) = (self.bids.first(), self.asks.first()) { - self.spread = best_ask.price - best_bid.price; - } - } - - fn mid_price(&self) -> Option { - if let (Some(best_bid), Some(best_ask)) = (self.bids.first(), self.asks.first()) { - Some((best_bid.price + best_ask.price) / 2.0) - } else { - None - } - } -} - -// ============================================================================ -// PRICE PARSING TESTS (15 tests) -// ============================================================================ - -#[tokio::test] -async fn test_price_parsing_scientific_notation_positive() { - let price_str = "1.23e5"; - let parsed = parse_price_string(price_str).unwrap(); - assert_eq!(parsed, 123000.0); -} - -#[tokio::test] -async fn test_price_parsing_scientific_notation_negative_exponent() { - let price_str = "1.5e-3"; - let parsed = parse_price_string(price_str).unwrap(); - assert!((parsed - 0.0015).abs() < 1e-10); -} - -#[tokio::test] -async fn test_price_parsing_very_small_value() { - let price_str = "0.00000001"; - let parsed = parse_price_string(price_str).unwrap(); - assert!((parsed - 1e-8).abs() < 1e-15); -} - -#[tokio::test] -async fn test_price_parsing_very_large_value() { - let price_str = "999999999.99"; - let parsed = parse_price_string(price_str).unwrap(); - assert!((parsed - 999999999.99).abs() < 1e-2); -} - -#[tokio::test] -async fn test_price_parsing_zero_value() { - let price_str = "0.0"; - let parsed = parse_price_string(price_str).unwrap(); - assert_eq!(parsed, 0.0); -} - -#[tokio::test] -async fn test_price_parsing_nan_string() { - let price_str = "NaN"; - let parsed = parse_price_string(price_str).unwrap(); - assert!(parsed.is_nan()); -} - -#[tokio::test] -async fn test_price_parsing_infinity_positive() { - let price_str = "inf"; - let parsed = parse_price_string(price_str).unwrap(); - assert!(parsed.is_infinite() && parsed.is_sign_positive()); -} - -#[tokio::test] -async fn test_price_parsing_infinity_negative() { - let price_str = "-inf"; - let parsed = parse_price_string(price_str).unwrap(); - assert!(parsed.is_infinite() && parsed.is_sign_negative()); -} - -#[tokio::test] -async fn test_price_validation_rejects_nan() { - let result = validate_price(f64::NAN); - assert!(result.is_err()); -} - -#[tokio::test] -async fn test_price_validation_rejects_infinity() { - let result = validate_price(f64::INFINITY); - assert!(result.is_err()); -} - -#[tokio::test] -async fn test_price_validation_rejects_negative_infinity() { - let result = validate_price(f64::NEG_INFINITY); - assert!(result.is_err()); -} - -#[tokio::test] -async fn test_price_validation_rejects_zero() { - let result = validate_price(0.0); - assert!(result.is_err()); -} - -#[tokio::test] -async fn test_price_validation_rejects_negative() { - let result = validate_price(-100.0); - assert!(result.is_err()); -} - -#[tokio::test] -async fn test_price_validation_accepts_max_f64() { - let result = validate_price(f64::MAX); - assert!(result.is_ok()); -} - -#[tokio::test] -async fn test_price_validation_accepts_min_positive() { - let result = validate_price(f64::MIN_POSITIVE); - assert!(result.is_ok()); -} - -// ============================================================================ -// SYMBOL VALIDATION TESTS (15 tests) -// ============================================================================ - -#[tokio::test] -async fn test_symbol_validation_empty_string() { - let result = validate_symbol(""); - assert!(result.is_err()); -} - -#[tokio::test] -async fn test_symbol_validation_too_long() { - let symbol = "A".repeat(21); - let result = validate_symbol(&symbol); - assert!(result.is_err()); -} - -#[tokio::test] -async fn test_symbol_validation_max_length() { - let symbol = "A".repeat(20); - let result = validate_symbol(&symbol); - assert!(result.is_ok()); -} - -#[tokio::test] -async fn test_symbol_validation_special_characters_slash() { - let result = validate_symbol("BTC/USD"); - assert!(result.is_err()); -} - -#[tokio::test] -async fn test_symbol_validation_special_characters_space() { - let result = validate_symbol("BTC USD"); - assert!(result.is_err()); -} - -#[tokio::test] -async fn test_symbol_validation_unicode_emoji() { - let result = validate_symbol("BTC📈USD"); - assert!(result.is_err()); -} - -#[tokio::test] -async fn test_symbol_validation_unicode_chinese() { - let result = validate_symbol("比特币"); - assert!(result.is_err()); -} - -#[tokio::test] -async fn test_symbol_validation_allowed_dot() { - let result = validate_symbol("BTC.USD"); - assert!(result.is_ok()); -} - -#[tokio::test] -async fn test_symbol_validation_allowed_dash() { - let result = validate_symbol("BTC-USD"); - assert!(result.is_ok()); -} - -#[tokio::test] -async fn test_symbol_validation_allowed_underscore() { - let result = validate_symbol("BTC_USD"); - assert!(result.is_ok()); -} - -#[tokio::test] -async fn test_symbol_validation_numeric_only() { - let result = validate_symbol("12345"); - assert!(result.is_ok()); -} - -#[tokio::test] -async fn test_symbol_validation_alphanumeric() { - let result = validate_symbol("ES50"); - assert!(result.is_ok()); -} - -#[tokio::test] -async fn test_symbol_validation_whitespace_only() { - let result = validate_symbol(" "); - assert!(result.is_err()); -} - -#[tokio::test] -async fn test_symbol_validation_leading_trailing_spaces() { - let result = validate_symbol(" BTC "); - assert!(result.is_err()); -} - -#[tokio::test] -async fn test_symbol_validation_null_byte() { - let result = validate_symbol("BTC\0USD"); - assert!(result.is_err()); -} - -// ============================================================================ -// TIMESTAMP HANDLING TESTS (10 tests) -// ============================================================================ - -#[tokio::test] -async fn test_timestamp_epoch_zero() { - let system_time = unix_nanos_to_system_time(0); - assert_eq!(system_time, UNIX_EPOCH); -} - -#[tokio::test] -async fn test_timestamp_negative_value() { - let system_time = unix_nanos_to_system_time(-1000); - assert_eq!(system_time, UNIX_EPOCH); -} - -#[tokio::test] -async fn test_timestamp_future_far() { - // Year 2100 (approximated) - let future_nanos = 4_102_444_800_000_000_000i64; - let system_time = unix_nanos_to_system_time(future_nanos); - assert!(system_time > SystemTime::now()); -} - -#[tokio::test] -async fn test_timestamp_y2k38_boundary() { - // 2038-01-19 03:14:07 UTC (32-bit signed int overflow) - let y2k38_nanos = 2_147_483_647i64 * 1_000_000_000; - let system_time = unix_nanos_to_system_time(y2k38_nanos); - assert!(system_time > UNIX_EPOCH); -} - -#[tokio::test] -async fn test_timestamp_max_i64() { - let system_time = unix_nanos_to_system_time(i64::MAX); - assert!(system_time > UNIX_EPOCH); -} - -#[tokio::test] -async fn test_timestamp_current_time_roundtrip() { - let now_nanos = current_unix_nanos(); - let system_time = unix_nanos_to_system_time(now_nanos); - let diff = SystemTime::now() - .duration_since(system_time) - .unwrap_or_default(); - // Allow up to 10ms difference for test execution time - assert!(diff < Duration::from_millis(10)); -} - -#[tokio::test] -async fn test_timestamp_millisecond_precision() { - let base_nanos = 1_000_000_000i64; // 1 second - let millis_nanos = base_nanos + 500_000_000; // +500ms - let system_time = unix_nanos_to_system_time(millis_nanos); - let duration = system_time.duration_since(UNIX_EPOCH).unwrap(); - assert_eq!(duration.as_millis(), 1500); -} - -#[tokio::test] -async fn test_timestamp_microsecond_precision() { - let base_nanos = 1_000_000_000i64; // 1 second - let micros_nanos = base_nanos + 123_000_000; // +123ms = 123000μs - let system_time = unix_nanos_to_system_time(micros_nanos); - let duration = system_time.duration_since(UNIX_EPOCH).unwrap(); - assert_eq!(duration.as_micros(), 1_123_000); -} - -#[tokio::test] -async fn test_timestamp_ordering() { - let ts1 = current_unix_nanos(); - sleep(Duration::from_millis(1)).await; - let ts2 = current_unix_nanos(); - assert!(ts2 > ts1); -} - -#[tokio::test] -async fn test_timestamp_market_data_stale_detection() { - let old_timestamp = current_unix_nanos() - 5_000_000_000; // 5 seconds ago - let current = current_unix_nanos(); - let staleness_threshold = 1_000_000_000; // 1 second - let is_stale = (current - old_timestamp) > staleness_threshold; - assert!(is_stale); -} - -// ============================================================================ -// UPDATE RATE LIMITING TESTS (15 tests) -// ============================================================================ - -#[tokio::test] -async fn test_high_frequency_updates_1000_per_second() { - let (tx, mut rx) = mpsc::channel(1000); - - // Send 1000 updates rapidly - tokio::spawn(async move { - for i in 0..1000 { - let data = create_market_data("BTC/USD", 50000.0 + i as f64, current_unix_nanos()); - let _ = tx.send(data).await; - } - }); - - // Count received updates - let mut count = 0; - while let Ok(result) = timeout(Duration::from_millis(100), rx.recv()).await { - if result.is_some() { - count += 1; - } else { - break; - } - } - - // Should receive at least 990 (allow 1% loss) - assert!(count >= 990, "Only received {} out of 1000 updates", count); -} - -#[tokio::test] -async fn test_burst_updates_handling() { - let (tx, mut rx) = mpsc::channel(100); - - // Send burst of 100 updates - for i in 0..100 { - let data = create_market_data("ETH/USD", 3000.0 + i as f64, current_unix_nanos()); - tx.send(data).await.unwrap(); - } - - // Verify all received - let mut count = 0; - while rx.try_recv().is_ok() { - count += 1; - } - assert_eq!(count, 100); -} - -#[tokio::test] -async fn test_channel_saturation_backpressure() { - let (tx, mut rx) = mpsc::channel(10); // Small buffer - - // Send more than buffer size without draining - let mut send_count = 0; - for i in 0..20 { - let data = create_market_data("AAPL", 150.0 + i as f64, current_unix_nanos()); - if tx.try_send(data).is_ok() { - send_count += 1; - } - } - - // Should saturate at buffer size - assert!(send_count <= 10); - - // Drain and verify - let mut recv_count = 0; - while rx.try_recv().is_ok() { - recv_count += 1; - } - assert_eq!(recv_count, send_count); -} - -#[tokio::test] -async fn test_delayed_updates_ordering() { - let (tx, mut rx) = mpsc::channel(100); - - // Send updates with delays - tokio::spawn(async move { - for i in 0..10 { - let data = create_market_data("TSLA", 800.0 + i as f64, current_unix_nanos()); - tx.send(data).await.unwrap(); - sleep(Duration::from_millis(5)).await; - } - }); - - // Verify ordering - let mut prev_price = 0.0; - let mut ordered = true; - while let Ok(Some(data)) = timeout(Duration::from_millis(100), rx.recv()).await { - if data.price < prev_price { - ordered = false; - break; - } - prev_price = data.price; - } - assert!(ordered); -} - -#[tokio::test] -async fn test_update_rate_calculation() { - let (tx, mut rx) = mpsc::channel(100); - let start = SystemTime::now(); - - // Send 50 updates over 100ms - tokio::spawn(async move { - for i in 0..50 { - let data = create_market_data("SPY", 420.0 + i as f64, current_unix_nanos()); - let _ = tx.send(data).await; - sleep(Duration::from_micros(2000)).await; - } - }); - - let mut count = 0; - while let Ok(result) = timeout(Duration::from_millis(200), rx.recv()).await { - if result.is_some() { - count += 1; - } else { - break; - } - } - - let elapsed = start.elapsed().unwrap(); - let rate = (count as f64 / elapsed.as_secs_f64()) as u32; - // Should be roughly 500 updates/sec - assert!(rate >= 400 && rate <= 600); -} - -#[tokio::test] -async fn test_update_deduplication() { - let mut last_price = 0.0; - let mut last_timestamp = 0i64; - - for i in 0..10 { - let data = create_market_data("BTC/USD", 50000.0, current_unix_nanos()); - - // Only process if different from last - if data.price != last_price || data.timestamp != last_timestamp { - last_price = data.price; - last_timestamp = data.timestamp; - } - } - - // Should have deduplicated (only first update processed) - assert_eq!(last_price, 50000.0); -} - -#[tokio::test] -async fn test_update_buffer_overflow() { - let mut buffer: VecDeque = VecDeque::with_capacity(10); - - // Add more than capacity - for i in 0..20 { - let data = create_market_data("QQQ", 350.0 + i as f64, current_unix_nanos()); - if buffer.len() >= 10 { - buffer.pop_front(); - } - buffer.push_back(data); - } - - assert_eq!(buffer.len(), 10); - // Should have latest 10 updates (prices 360-369) - assert_eq!(buffer.back().unwrap().price, 369.0); -} - -#[tokio::test] -async fn test_concurrent_symbol_updates() { - let (tx, mut rx) = mpsc::channel(100); - let symbols = vec!["AAPL", "TSLA", "SPY", "QQQ", "NVDA"]; - - // Send updates for multiple symbols concurrently - for symbol in symbols.clone() { - let tx_clone = tx.clone(); - tokio::spawn(async move { - for i in 0..10 { - let data = create_market_data(symbol, 100.0 + i as f64, current_unix_nanos()); - let _ = tx_clone.send(data).await; - } - }); - } - drop(tx); - - // Collect all updates - let mut updates = Vec::new(); - while let Some(data) = rx.recv().await { - updates.push(data); - } - - // Should receive all 50 updates - assert_eq!(updates.len(), 50); - - // Verify all symbols present - for symbol in symbols { - let count = updates.iter().filter(|u| u.symbol == symbol).count(); - assert_eq!(count, 10); - } -} - -#[tokio::test] -async fn test_update_latency_tracking() { - let mut latencies = Vec::new(); - - for _ in 0..10 { - let sent_time = current_unix_nanos(); - sleep(Duration::from_micros(100)).await; - let recv_time = current_unix_nanos(); - latencies.push(recv_time - sent_time); - } - - let avg_latency = latencies.iter().sum::() / latencies.len() as i64; - // Should be around 100μs (100,000 nanos) - assert!(avg_latency > 50_000 && avg_latency < 200_000); -} - -#[tokio::test] -async fn test_missed_update_detection() { - let mut sequence_numbers = Vec::new(); - - // Simulate missing sequence number - for i in 0..10 { - if i != 5 { - // Skip 5 - sequence_numbers.push(i); - } - } - - // Check for gaps - let mut has_gap = false; - for window in sequence_numbers.windows(2) { - if window[1] - window[0] > 1 { - has_gap = true; - break; - } - } - - assert!(has_gap); -} - -#[tokio::test] -async fn test_update_compression() { - // Test that identical updates can be compressed - let updates = vec![ - create_market_data("BTC/USD", 50000.0, current_unix_nanos()), - create_market_data("BTC/USD", 50000.0, current_unix_nanos()), - create_market_data("BTC/USD", 50001.0, current_unix_nanos()), - create_market_data("BTC/USD", 50001.0, current_unix_nanos()), - ]; - - // Compress consecutive duplicates - let mut compressed = Vec::new(); - let mut last_price = -1.0; - for update in updates { - if update.price != last_price { - compressed.push(update.clone()); - last_price = update.price; - } - } - - assert_eq!(compressed.len(), 2); // Only 2 unique prices -} - -#[tokio::test] -async fn test_rate_limiter_token_bucket() { - // Simple token bucket rate limiter simulation - let mut tokens = 100.0; - let max_tokens = 100.0; - let refill_rate = 10.0; // tokens per second - - let mut accepted = 0; - let mut rejected = 0; - - for _ in 0..150 { - if tokens >= 1.0 { - tokens -= 1.0; - accepted += 1; - } else { - rejected += 1; - } - // Simulate refill - tokens = f64::min(tokens + refill_rate / 150.0, max_tokens as f64); - } - - // Should accept roughly 100-110 requests - assert!(accepted >= 100 && accepted <= 120); - assert!(rejected >= 30 && rejected <= 50); -} - -#[tokio::test] -async fn test_sliding_window_rate_limiter() { - let window_size = Duration::from_millis(100); - let max_requests = 10; - let mut timestamps = Vec::new(); - - let start = SystemTime::now(); - for i in 0..20 { - let now = SystemTime::now(); - // Remove timestamps outside window - timestamps.retain(|&ts: &SystemTime| now.duration_since(ts).unwrap() < window_size); - - if timestamps.len() < max_requests { - timestamps.push(now); - // Request accepted - } else { - // Request rejected - } - - if i < 10 { - sleep(Duration::from_millis(5)).await; - } - } - - // First 10 should be accepted, rest rejected - assert!(timestamps.len() <= max_requests); -} - -#[tokio::test] -async fn test_adaptive_rate_limiting() { - let mut rate_limit = 100; // Initial limit - let mut errors = 0; - - for i in 0..200 { - if i % rate_limit == 0 { - // Simulate checking if we hit rate limit - if errors > 5 { - // Reduce rate if too many errors - rate_limit = (rate_limit as f64 * 0.8) as usize; - errors = 0; - } - } - } - - // Rate limit should adapt - assert!(rate_limit < 100); -} - -// ============================================================================ -// ORDER BOOK EDGE CASES (15 tests) -// ============================================================================ - -#[tokio::test] -async fn test_order_book_empty_levels() { - let mut book = create_order_book_snapshot(vec![], vec![]); - book.calculate_spread(); - - assert_eq!(book.bids.len(), 0); - assert_eq!(book.asks.len(), 0); - assert_eq!(book.mid_price(), None); -} - -#[tokio::test] -async fn test_order_book_only_bids() { - let mut book = create_order_book_snapshot(vec![(100.0, 50.0, 1)], vec![]); - book.calculate_spread(); - - assert_eq!(book.bids.len(), 1); - assert_eq!(book.asks.len(), 0); - assert_eq!(book.mid_price(), None); -} - -#[tokio::test] -async fn test_order_book_only_asks() { - let mut book = create_order_book_snapshot(vec![], vec![(100.0, 50.0, 1)]); - book.calculate_spread(); - - assert_eq!(book.bids.len(), 0); - assert_eq!(book.asks.len(), 1); - assert_eq!(book.mid_price(), None); -} - -#[tokio::test] -async fn test_order_book_single_level_each_side() { - let mut book = create_order_book_snapshot(vec![(99.0, 100.0, 1)], vec![(100.0, 100.0, 1)]); - book.calculate_spread(); - - assert_eq!(book.spread, 1.0); - assert!(book.mid_price().is_some()); -} - -#[tokio::test] -async fn test_order_book_100_levels() { - let bids: Vec<_> = (0..100) - .map(|i| (100.0 - i as f64 * 0.01, 100.0, 1)) - .collect(); - let asks: Vec<_> = (0..100) - .map(|i| (100.01 + i as f64 * 0.01, 100.0, 1)) - .collect(); - - let book = create_order_book_snapshot(bids, asks); - - assert_eq!(book.bids.len(), 100); - assert_eq!(book.asks.len(), 100); -} - -#[tokio::test] -async fn test_order_book_zero_size_levels() { - let mut book = create_order_book_snapshot(vec![(100.0, 0.0, 1)], vec![(101.0, 0.0, 1)]); - book.calculate_spread(); - - assert_eq!(book.bids[0].quantity, 0.0); - assert_eq!(book.asks[0].quantity, 0.0); -} - -#[tokio::test] -async fn test_order_book_very_large_size() { - let large_size = 1_000_000_000.0; - let book = - create_order_book_snapshot(vec![(100.0, large_size, 1)], vec![(101.0, large_size, 1)]); - - assert!(book.bids[0].quantity > 0.0); -} - -#[tokio::test] -async fn test_order_book_very_tight_spread() { - let mut book = create_order_book_snapshot(vec![(100.000, 100.0, 1)], vec![(100.001, 100.0, 1)]); - book.calculate_spread(); - - assert!(book.spread < 1.0); - assert!(book.spread > 0.0); -} - -#[tokio::test] -async fn test_order_book_very_wide_spread() { - let mut book = create_order_book_snapshot(vec![(50.0, 100.0, 1)], vec![(100.0, 100.0, 1)]); - book.calculate_spread(); - - assert_eq!(book.spread, 50.0); -} - -#[tokio::test] -async fn test_order_book_crossed_book() { - // Invalid state: best bid > best ask - let book = create_order_book_snapshot(vec![(101.0, 100.0, 1)], vec![(100.0, 100.0, 1)]); - - // Should still calculate (negative spread indicates crossed book) - let bid_price = book.bids[0].price; - let ask_price = book.asks[0].price; - assert!(bid_price > ask_price); -} - -#[tokio::test] -async fn test_order_book_duplicate_price_levels() { - let bids = vec![(100.0, 50.0, 1), (100.0, 30.0, 2), (99.0, 100.0, 1)]; - let book = create_order_book_snapshot(bids, vec![]); - - // Should handle duplicates (may aggregate or keep separate) - assert!(book.bids.len() >= 2); -} - -#[tokio::test] -async fn test_order_book_out_of_order_levels() { - let unsorted_bids = vec![(99.0, 100.0, 1), (100.0, 100.0, 1), (98.0, 100.0, 1)]; - let book = create_order_book_snapshot(unsorted_bids, vec![]); - - // Should work regardless of input order - assert_eq!(book.bids.len(), 3); -} - -#[tokio::test] -async fn test_order_book_zero_count() { - let book = create_order_book_snapshot(vec![(100.0, 100.0, 0)], vec![]); - - assert_eq!(book.bids[0].order_count, 0); -} - -#[tokio::test] -async fn test_order_book_large_count() { - let book = create_order_book_snapshot(vec![(100.0, 100.0, 10000)], vec![]); - - assert_eq!(book.bids[0].order_count, 10000); -} - -#[tokio::test] -async fn test_order_book_snapshot_update() { - let mut book = create_order_book_snapshot(vec![(100.0, 100.0, 1)], vec![(101.0, 100.0, 1)]); - let old_timestamp = book.timestamp; - - sleep(Duration::from_millis(1)).await; - - // Update snapshot - book.timestamp = Utc::now(); - assert!(book.timestamp > old_timestamp); -} - -// ============================================================================ -// TRADE HISTORY TESTS (5 tests) -// ============================================================================ - -#[tokio::test] -async fn test_trade_history_zero_trades() { - let trades: Vec = vec![]; - assert_eq!(trades.len(), 0); -} - -#[tokio::test] -async fn test_trade_history_zero_volume() { - let trade = MarketDataDisplayEvent { - symbol: "BTC/USD".to_string(), - price: 50000.0, - volume: 0, - timestamp: current_unix_nanos(), - bid: Some(49999.0), - ask: Some(50001.0), - change: Some(0.0), - change_percent: Some(0.0), - }; - - assert_eq!(trade.volume, 0); -} - -#[tokio::test] -async fn test_trade_history_10k_trades() { - let mut trades = Vec::with_capacity(10000); - - for i in 0..10000 { - let trade = create_market_data("ETH/USD", 3000.0 + i as f64 * 0.01, current_unix_nanos()); - trades.push(trade); - } - - assert_eq!(trades.len(), 10000); -} - -#[tokio::test] -async fn test_trade_history_memory_efficiency() { - // Test circular buffer for trade history - let capacity = 1000; - let mut trades: VecDeque = VecDeque::with_capacity(capacity); - - for i in 0..5000 { - if trades.len() >= capacity { - trades.pop_front(); - } - let trade = create_market_data("SPY", 420.0 + i as f64 * 0.01, current_unix_nanos()); - trades.push_back(trade); - } - - assert_eq!(trades.len(), capacity); -} - -#[tokio::test] -async fn test_trade_history_time_range_filter() { - let now = current_unix_nanos(); - let hour_ago = now - 3_600_000_000_000; // 1 hour in nanos - - let mut trades = vec![ - create_market_data("BTC/USD", 50000.0, hour_ago), - create_market_data("BTC/USD", 50100.0, hour_ago + 1_800_000_000_000), // 30 min ago - create_market_data("BTC/USD", 50200.0, now), - ]; - - // Filter trades from last 45 minutes - let threshold = now - 2_700_000_000_000; // 45 min - trades.retain(|t| t.timestamp >= threshold); - - assert_eq!(trades.len(), 2); -} - -// ============================================================================ -// CONNECTION INTERRUPTION TESTS (5 tests) -// ============================================================================ - -#[tokio::test] -async fn test_connection_loss_recovery() { - let (tx, mut rx) = mpsc::channel(10); - - // Send some data - for i in 0..5 { - tx.send(create_market_data( - "BTC/USD", - 50000.0 + i as f64, - current_unix_nanos(), - )) - .await - .unwrap(); - } - - // Simulate disconnection (drop sender) - drop(tx); - - // Verify channel closed - assert!(rx.recv().await.is_some()); // Drain existing - while rx.try_recv().is_ok() {} // Drain all - assert!(rx.recv().await.is_none()); // Channel closed -} - -#[tokio::test] -async fn test_reconnection_backoff() { - let retry_delays = vec![100, 200, 400, 800, 1600]; // Exponential backoff in ms - let mut total_delay = 0; - - for delay in retry_delays { - total_delay += delay; - // Simulate retry delay - sleep(Duration::from_millis(delay as u64)).await; - } - - // Total delay should be sum of all retries - assert_eq!(total_delay, 3100); -} - -#[tokio::test] -async fn test_connection_heartbeat_timeout() { - let timeout_duration = Duration::from_millis(50); - let (tx, mut rx) = mpsc::channel(10); - - // Send heartbeat - tx.send(create_market_data("HEARTBEAT", 0.0, current_unix_nanos())) - .await - .unwrap(); - - // Wait for timeout - let result = timeout(timeout_duration, rx.recv()).await; - assert!(result.is_ok()); // Should receive heartbeat - - // Wait again without sending - let result = timeout(timeout_duration, rx.recv()).await; - assert!(result.is_err()); // Should timeout -} - -#[tokio::test] -async fn test_missed_updates_during_disconnect() { - let mut sequence = 0u64; - let mut expected = 0u64; - let mut missed = 0u64; - - // Simulate sequence with gap - let sequences = vec![0, 1, 2, 3, 7, 8, 9]; // Missing 4, 5, 6 - - for seq in sequences { - sequence = seq; - if sequence != expected { - missed += sequence - expected; - } - expected = sequence + 1; - } - - assert_eq!(missed, 3); -} - -#[tokio::test] -async fn test_connection_backfill_request() { - // Simulate requesting backfill for missed data - let last_received_sequence = 100u64; - let current_sequence = 150u64; - - let backfill_needed = current_sequence > last_received_sequence + 1; - let backfill_count = if backfill_needed { - current_sequence - last_received_sequence - 1 - } else { - 0 - }; - - assert!(backfill_needed); - assert_eq!(backfill_count, 49); -}