Files
foxhunt/tli/src/main.rs
jgrusewski e364d447f5 refactor(tli): remove Ratatui dashboards, widgets, streaming stubs (14,235 lines)
Delete TLI terminal UI code replaced by web-dashboard architecture:
- dashboard/ (11 files): trading, risk, ML, performance, backtesting, config, events, vault
- dashboards/ (3 files): config manager, configuration
- ui/ (8 files): widgets (candlestick, order book, risk gauge, sparkline, PnL heatmap, config form)
- events/ (4 files): aggregator, event buffer, stream manager
- client stubs: data_stream, event_stream, stream_manager
- error_consolidated.rs, 4 examples, market_data_edge_cases test

Update lib.rs, prelude.rs, main.rs, client/mod.rs, tests.rs to remove references.
Remove ratatui, crossterm, adaptive-strategy dependencies from Cargo.toml.
Clean up test fixtures (TestEventPublisher removed).

TLI retains all CLI commands (tune, train, auth, agent, backtest, trade).
134 tests passing, 0 warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 00:18:43 +01:00

517 lines
17 KiB
Rust

//! TLI (Terminal Line Interface) - Client Application for Foxhunt HFT Trading System
//!
//! 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
//! - Authentication and secure token management
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use colored::Colorize;
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
use tli::auth::token_manager::FileTokenStorage;
use tli::{
commands::{
agent::{execute_agent_command, AgentArgs},
auth::{execute_auth_command, AuthCommand},
backtest_ml::{execute_backtest_ml_command, BacktestMlArgs},
trade::{execute_trade_command, TradeArgs},
train::{execute_train_command, TrainCommand},
tune::{execute_tune_command, TuneCommand},
},
config::TliConfig,
};
use tracing::Level;
use tracing_subscriber::FmtSubscriber;
// Suppress false-positive unused extern crate warnings for dependencies used in modules
use aes_gcm as _;
use argon2 as _;
use async_trait as _;
use base64 as _;
use chrono as _;
use clap as _;
use colored as _;
use comfy_table as _;
use common as _;
use console as _;
use dirs as _;
use futures_util as _;
use getrandom as _;
use hex as _;
use indicatif as _;
use keyring as _;
use owo_colors as _;
use prost as _;
use rand as _;
use rpassword as _;
use rust_decimal as _;
use serde as _;
use serde_json as _;
use sha2 as _;
use tabled as _;
use thiserror as _;
use toml as _;
use tonic as _;
use tonic_prost as _;
use uuid as _;
use zeroize as _;
// Configuration precedence (highest to lowest):
// 1. CLI arguments (--api-gateway-url)
// 2. Environment variables (API_GATEWAY_URL)
// 3. Config file (~/.foxhunt/config.toml)
// 4. Hardcoded defaults
/// TLI Command Line Interface
#[derive(Parser)]
#[clap(
name = "tli",
version,
about = "Foxhunt Trading System Terminal Interface",
long_about = "Foxhunt Trading System Terminal Interface\n\n\
Environment Variables:\n\
API_GATEWAY_URL API Gateway URL (default: http://localhost:50051)\n\
TLI_LOG_LEVEL Log level (default: info)\n\
TLI_TOKEN_STORAGE Token storage backend (default: keyring)\n\n\
Precedence: CLI args > Environment variables > Config file > Defaults"
)]
struct Cli {
/// Subcommand to execute
#[clap(subcommand)]
command: Commands,
/// API Gateway URL
#[clap(
long,
env = "API_GATEWAY_URL",
default_value = "http://localhost:50051",
help = "API Gateway URL (env: API_GATEWAY_URL)"
)]
api_gateway_url: String,
/// Log level (trace, debug, info, warn, error)
#[clap(
long,
env = "TLI_LOG_LEVEL",
default_value = "info",
help = "Log level (env: TLI_LOG_LEVEL)"
)]
log_level: String,
/// Token storage backend (keyring, file)
#[clap(
long,
env = "TLI_TOKEN_STORAGE",
default_value = "keyring",
help = "Token storage backend (env: TLI_TOKEN_STORAGE)"
)]
token_storage: String,
}
/// TLI subcommands
#[derive(Subcommand)]
enum Commands {
/// Hyperparameter tuning for ML models (DQN, PPO, MAMBA-2, TFT)
#[clap(
long_about = "Start, monitor, and manage hyperparameter tuning jobs.\n\n\
Supported models: DQN, PPO, MAMBA_2, TFT, TLOB, LIQUID\n\
Uses Optuna for Bayesian optimization.\n\n\
Examples:\n\
tli tune start --model DQN --trials 100\n\
tli tune status --job-id <uuid>\n\
tli tune best --job-id <uuid>\n\
tli tune stop --job-id <uuid>"
)]
Tune {
#[clap(subcommand)]
tune_cmd: TuneCommand,
},
/// Training job management (list, status, stop)
#[clap(
long_about = "Manage ML model training jobs.\n\n\
Supported operations:\n\
- List training jobs with filtering\n\
- Get detailed job status\n\
- Watch real-time training progress\n\
- Stop jobs gracefully or forcefully\n\n\
Examples:\n\
tli train list\n\
tli train list --status RUNNING --model TFT\n\
tli train status train_dqn_es_20251022_1430\n\
tli train watch train_tft_nq_20251022_1500\n\
tli train stop train_ppo_es_20251022_1600"
)]
Train {
#[clap(subcommand)]
train_cmd: TrainCommand,
},
/// Authentication and session management
#[clap(long_about = "Login, logout, and manage authentication tokens.\n\n\
JWT tokens are stored securely in OS keyring.\n\
Tokens auto-refresh when expiring (within 60 seconds).\n\n\
Examples:\n\
tli auth login --username trader1\n\
tli auth status\n\
tli auth refresh\n\
tli auth logout")]
Auth {
#[clap(subcommand)]
auth_cmd: AuthCommand,
},
/// Trading agent operations (universe selection, asset selection, portfolio allocation)
#[clap(
long_about = "Trading agent operations for automated portfolio management.\n\n\
Subcommands:\n\
allocate-portfolio - Allocate capital across selected assets\n\n\
Examples:\n\
tli agent allocate-portfolio --selection-id abc-123 --total-capital 100000\n\
tli agent allocate-portfolio --selection-id abc-123 --total-capital 100000 --strategy risk-parity"
)]
Agent {
#[command(flatten)]
agent_args: AgentArgs,
},
/// ML trading operations
#[clap(name = "backtest")]
Backtest {
#[command(flatten)]
backtest_args: BacktestMlArgs,
},
/// ML trading operations (legacy, use backtest ml instead)
#[clap(name = "trade")]
Trade {
#[command(flatten)]
trade_args: TradeArgs,
},
}
/// JWT token claims structure for validation
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
/// Subject (`user_id`)
sub: String,
/// Expiration time (Unix timestamp in seconds)
exp: u64,
/// Issued at (Unix timestamp in seconds)
iat: u64,
/// JWT ID
jti: String,
/// User roles
roles: Vec<String>,
/// User permissions
permissions: Vec<String>,
}
/// Load and validate JWT token from OS keyring with automatic refresh
///
/// This function retrieves the access token from secure storage, validates expiration,
/// and automatically attempts refresh if the token is expired or expiring within 60 seconds.
///
/// # Arguments
/// * `api_gateway_url` - API Gateway URL for token refresh requests
///
/// # Returns
/// - `Ok(String)` - Valid access token (possibly refreshed)
/// - `Err(anyhow::Error)` - Token not found, refresh failed, or invalid format
async fn load_jwt_token(api_gateway_url: &str) -> Result<String> {
use tli::auth::login::LoginClient;
use tli::auth::token_manager::{AuthTokenManager, TokenStorage};
use tonic::transport::Channel;
let storage = FileTokenStorage::new().context("Failed to initialize file token storage")?;
// Read access token directly from keyring
match storage.get_access_token().await? {
Some(token) => {
// Validate token expiry
if let Err(_) = validate_token_expiry(&token).await {
// Token expired - attempt refresh
tracing::info!("Token expired, attempting auto-refresh");
println!("{}", "Token expiring, refreshing...".yellow());
// Check if we have a refresh token
if storage.get_refresh_token().await?.is_none() {
anyhow::bail!(
"No refresh token available. Please login: {}",
"tli auth login".bright_cyan()
);
}
// Store old token for comparison
let old_token = token.clone();
// Refresh tokens
let auth_manager = AuthTokenManager::new(storage.clone());
let channel = Channel::from_shared(api_gateway_url.to_owned())
.context("Invalid API Gateway URL")?
.connect_lazy();
let login_client = LoginClient::new(channel);
login_client
.refresh_tokens(&auth_manager)
.await
.context("Failed to refresh tokens")?;
// Verify new token was stored in keyring
match storage.get_access_token().await? {
Some(new_token) => {
// Verify token was actually updated
if new_token != old_token {
tracing::info!("\u{2713} New access token confirmed in keyring");
} else {
tracing::error!(
"\u{26a0} Token refresh did not update access token in keyring"
);
}
// Verify refresh token is still in keyring
match storage.get_refresh_token().await? {
Some(stored_refresh) => {
tracing::info!("\u{2713} Refresh token confirmed in keyring");
// Verify refresh token wasn't accidentally cleared
if stored_refresh.is_empty() {
anyhow::bail!(
"Token refresh succeeded but refresh token is empty in keyring. Please login again: {}",
"tli auth login".bright_cyan()
)
}
},
None => {
anyhow::bail!(
"Token refresh succeeded but refresh token not found in keyring. Please login again: {}",
"tli auth login".bright_cyan()
)
},
}
println!("{}", "\u{2713} Token refreshed successfully".green());
Ok(new_token)
},
None => {
anyhow::bail!(
"Token refresh succeeded but new token not found in keyring. Please login again: {}",
"tli auth login".bright_cyan()
)
},
}
} else {
// Token is still valid
Ok(token)
}
},
None => {
anyhow::bail!(
"Not authenticated. Please run: {} first",
"tli auth login".bright_cyan()
)
},
}
}
/// Validate JWT token expiration only (for refresh decision)
///
/// Checks if the token expires within 60 seconds without full validation.
///
/// # Arguments
/// * `token` - JWT token string to check
///
/// # Returns
/// - `Ok(())` - Token not expired and has >60 seconds remaining
/// - `Err(anyhow::Error)` - Token expired or expiring soon
async fn validate_token_expiry(token: &str) -> Result<()> {
// Parse token WITHOUT verification (only check expiry)
let mut validation = Validation::new(Algorithm::HS256);
validation.insecure_disable_signature_validation();
validation.validate_exp = false;
let token_data = decode::<Claims>(token, &DecodingKey::from_secret(b"dummy"), &validation)
.context("Invalid token format")?;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
if token_data.claims.exp <= now + 60 {
anyhow::bail!("Token expired or expiring soon")
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
// Load config file (~/.foxhunt/config.toml)
let config = TliConfig::load().unwrap_or_default();
// Parse CLI args
let mut cli = Cli::parse();
// Merge: CLI args override config file (precedence: CLI > Config > Default)
if cli.api_gateway_url == "http://localhost:50051" {
// Using default, check if config has override
cli.api_gateway_url = config.api_gateway_url;
}
if cli.log_level == "info" {
// Using default, check if config has override
cli.log_level = config.log_level.clone();
}
// Parse log level
let log_level = match cli.log_level.to_lowercase().as_str() {
"trace" => Level::TRACE,
"debug" => Level::DEBUG,
"info" => Level::INFO,
"warn" => Level::WARN,
"error" => Level::ERROR,
_ => Level::INFO,
};
// Initialize tracing with configured log level
let subscriber = FmtSubscriber::builder().with_max_level(log_level).finish();
if let Err(e) = tracing::subscriber::set_global_default(subscriber) {
eprintln!("Warning: Failed to set global tracing subscriber: {}", e);
}
// Route to subcommands
match cli.command {
Commands::Tune { tune_cmd } => {
let jwt_token = load_jwt_token(&cli.api_gateway_url).await?;
execute_tune_command(tune_cmd, &cli.api_gateway_url, &jwt_token).await
}
Commands::Train { train_cmd } => {
let jwt_token = load_jwt_token(&cli.api_gateway_url).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 } => {
let jwt_token = load_jwt_token(&cli.api_gateway_url).await?;
execute_agent_command(agent_args, &cli.api_gateway_url, &jwt_token).await
}
Commands::Backtest { backtest_args } => {
execute_backtest_ml_command(backtest_args).await
}
Commands::Trade { trade_args } => {
let jwt_token = load_jwt_token(&cli.api_gateway_url).await?;
execute_trade_command(trade_args, &cli.api_gateway_url, &jwt_token).await
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_main_function_exists() {
// This test ensures the main function compiles
// Terminal application testing would require mock terminal backend
assert!(true);
}
#[test]
fn test_cli_parsing_tune_command() {
// Test that Cli struct parses tune command correctly
let cli = Cli::parse_from(&[
"tli",
"--api-gateway-url",
"http://test.com",
"tune",
"status",
"--job-id",
"550e8400-e29b-41d4-a716-446655440000",
]);
assert_eq!(cli.api_gateway_url, "http://test.com");
match cli.command {
Commands::Tune { .. } => {},
_ => panic!("Expected Tune command"),
}
}
#[test]
fn test_cli_parsing_auth_command() {
let cli = Cli::parse_from(&["tli", "auth", "status"]);
match cli.command {
Commands::Auth { .. } => {},
_ => panic!("Expected Auth command"),
}
}
#[test]
fn test_cli_default_values() {
// Test default values when no flags provided
let cli = Cli::parse_from(&["tli", "auth", "status"]);
assert_eq!(cli.api_gateway_url, "http://localhost:50051");
assert_eq!(cli.log_level, "info");
assert_eq!(cli.token_storage, "keyring");
}
#[test]
fn test_cli_custom_values() {
// Test that custom values override defaults
let cli = Cli::parse_from(&[
"tli",
"--api-gateway-url",
"http://custom.com:8080",
"--log-level",
"debug",
"--token-storage",
"file",
"auth",
"status",
]);
assert_eq!(cli.api_gateway_url, "http://custom.com:8080");
assert_eq!(cli.log_level, "debug");
assert_eq!(cli.token_storage, "file");
}
#[test]
fn test_tune_command_with_all_args() {
let cli = Cli::parse_from(&[
"tli",
"tune",
"start",
"--model",
"DQN",
"--trials",
"100",
"--config",
"test_config.yaml",
"--gpu",
]);
match cli.command {
Commands::Tune { .. } => {},
_ => panic!("Expected Tune command"),
}
}
#[test]
fn test_auth_login_command_parsing() {
let cli = Cli::parse_from(&["tli", "auth", "login", "--username", "testuser"]);
match cli.command {
Commands::Auth { .. } => {},
_ => panic!("Expected Auth command"),
}
}
}