- Replace hardcoded i32 enum matches with ProtoEnum::try_from() in 8 command files (cluster, config, data, model, risk, service, train, tune) - Replace hardcoded i32 literals with enum variants (EmergencyStopType, ExportFormat, TrainingMode) - Add JSON-RPC 2.0 PARSE_ERROR (-32700) for malformed JSON input - Validate jsonrpc:"2.0" version on incoming MCP requests - Change unreachable execute_tool catch-all from Ok to Err - Fix TUI teardown to not mask original event_loop error - Rename config_cmd module to config (commands::config) - 77 tests pass, 0 clippy warnings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
259 lines
8.1 KiB
Rust
259 lines
8.1 KiB
Rust
//! `fxt data` -- data pipeline management.
|
|
|
|
use anyhow::Result;
|
|
use clap::{Parser, Subcommand};
|
|
use colored::Colorize;
|
|
use serde::Serialize;
|
|
|
|
use crate::grpc::FoxhuntClient;
|
|
use crate::output::{self, HumanReadable, OutputFormat};
|
|
|
|
#[derive(Parser, Debug)]
|
|
pub struct DataCommand {
|
|
#[command(subcommand)]
|
|
action: DataAction,
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
enum DataAction {
|
|
/// Download market data
|
|
Download {
|
|
/// Symbol to download
|
|
#[arg(long)]
|
|
symbol: String,
|
|
/// Start date (YYYY-MM-DD)
|
|
#[arg(long)]
|
|
from: String,
|
|
/// End date (YYYY-MM-DD)
|
|
#[arg(long)]
|
|
to: String,
|
|
/// Schema (ohlcv-1m, ohlcv-1s, trades, mbp-1)
|
|
#[arg(long, default_value = "ohlcv-1m")]
|
|
schema: String,
|
|
},
|
|
/// Show data pipeline status
|
|
Status,
|
|
/// Show data cache inventory
|
|
Cache,
|
|
/// Show live data feed status
|
|
Feeds,
|
|
}
|
|
|
|
// ── Result types ──────────────────────────────────────────────────────
|
|
|
|
#[derive(Serialize)]
|
|
struct DownloadResult {
|
|
job_id: String,
|
|
status: String,
|
|
estimated_cost_usd: f64,
|
|
message: String,
|
|
}
|
|
|
|
impl HumanReadable for DownloadResult {
|
|
fn print_human(&self) {
|
|
println!("{}", "Download Scheduled".green().bold());
|
|
println!(" Job ID: {}", self.job_id);
|
|
println!(" Status: {}", self.status);
|
|
println!(
|
|
" Est. Cost: ${:.2}",
|
|
self.estimated_cost_usd
|
|
);
|
|
println!(" Message: {}", self.message);
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct DownloadJobInfo {
|
|
job_id: String,
|
|
status: String,
|
|
dataset: String,
|
|
symbols: Vec<String>,
|
|
start_date: String,
|
|
end_date: String,
|
|
progress_pct: f32,
|
|
bytes_downloaded: u64,
|
|
total_bytes: u64,
|
|
records_count: u64,
|
|
data_quality_score: f64,
|
|
error_message: String,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct StatusResult {
|
|
jobs: Vec<DownloadJobInfo>,
|
|
total_count: u32,
|
|
}
|
|
|
|
impl HumanReadable for StatusResult {
|
|
fn print_human(&self) {
|
|
println!(
|
|
"{} ({} total)",
|
|
"Download Jobs".cyan().bold(),
|
|
self.total_count,
|
|
);
|
|
if self.jobs.is_empty() {
|
|
println!(" No download jobs found.");
|
|
return;
|
|
}
|
|
for job in &self.jobs {
|
|
let status_colored = match job.status.as_str() {
|
|
"COMPLETED" => job.status.green().to_string(),
|
|
"DOWNLOADING" | "VALIDATING" | "UPLOADING" => job.status.yellow().to_string(),
|
|
"FAILED" => job.status.red().to_string(),
|
|
"CANCELLED" => job.status.dimmed().to_string(),
|
|
_ => job.status.clone(),
|
|
};
|
|
println!(
|
|
" {} [{}] {:.0}% {} {} -> {}",
|
|
job.job_id.dimmed(),
|
|
status_colored,
|
|
job.progress_pct,
|
|
job.symbols.join(","),
|
|
job.start_date,
|
|
job.end_date,
|
|
);
|
|
if !job.error_message.is_empty() {
|
|
println!(" Error: {}", job.error_message.red());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct CacheResult {
|
|
message: String,
|
|
}
|
|
|
|
impl HumanReadable for CacheResult {
|
|
fn print_human(&self) {
|
|
println!("{}", "Data Cache".cyan().bold());
|
|
println!(" {}", self.message);
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct FeedsResult {
|
|
message: String,
|
|
}
|
|
|
|
impl HumanReadable for FeedsResult {
|
|
fn print_human(&self) {
|
|
println!("{}", "Live Data Feeds".cyan().bold());
|
|
println!(" {}", self.message);
|
|
}
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────
|
|
|
|
fn download_status_name(value: i32) -> &'static str {
|
|
use crate::proto::data_acquisition::DownloadStatus;
|
|
match DownloadStatus::try_from(value) {
|
|
Ok(DownloadStatus::Pending) => "PENDING",
|
|
Ok(DownloadStatus::Downloading) => "DOWNLOADING",
|
|
Ok(DownloadStatus::Validating) => "VALIDATING",
|
|
Ok(DownloadStatus::Uploading) => "UPLOADING",
|
|
Ok(DownloadStatus::Completed) => "COMPLETED",
|
|
Ok(DownloadStatus::Failed) => "FAILED",
|
|
Ok(DownloadStatus::Cancelled) => "CANCELLED",
|
|
_ => "UNKNOWN",
|
|
}
|
|
}
|
|
|
|
// ── Execute ───────────────────────────────────────────────────────────
|
|
|
|
impl DataCommand {
|
|
pub async fn execute(&self, client: &FoxhuntClient, format: OutputFormat) -> Result<()> {
|
|
match &self.action {
|
|
DataAction::Download {
|
|
symbol,
|
|
from,
|
|
to,
|
|
schema,
|
|
} => {
|
|
let response = client
|
|
.data_acquisition()
|
|
.schedule_download(crate::proto::data_acquisition::ScheduleDownloadRequest {
|
|
dataset: "GLBX.MDP3".to_owned(),
|
|
symbols: vec![symbol.clone()],
|
|
start_date: from.clone(),
|
|
end_date: to.clone(),
|
|
schema: schema.clone(),
|
|
description: String::new(),
|
|
tags: Default::default(),
|
|
priority: 3,
|
|
})
|
|
.await?
|
|
.into_inner();
|
|
|
|
let result = DownloadResult {
|
|
job_id: response.job_id,
|
|
status: download_status_name(response.status).to_owned(),
|
|
estimated_cost_usd: response.estimated_cost_usd,
|
|
message: response.message,
|
|
};
|
|
output::render(&result, format)?;
|
|
}
|
|
|
|
DataAction::Status => {
|
|
let response = client
|
|
.data_acquisition()
|
|
.list_download_jobs(
|
|
crate::proto::data_acquisition::ListDownloadJobsRequest {
|
|
page: 0,
|
|
page_size: 50,
|
|
status_filter: 0,
|
|
start_time: 0,
|
|
end_time: 0,
|
|
},
|
|
)
|
|
.await?
|
|
.into_inner();
|
|
|
|
let jobs: Vec<DownloadJobInfo> = response
|
|
.jobs
|
|
.into_iter()
|
|
.map(|j| DownloadJobInfo {
|
|
job_id: j.job_id,
|
|
status: download_status_name(j.status).to_owned(),
|
|
dataset: j.dataset,
|
|
symbols: j.symbols,
|
|
start_date: j.start_date,
|
|
end_date: j.end_date,
|
|
progress_pct: j.progress_percentage,
|
|
bytes_downloaded: 0,
|
|
total_bytes: 0,
|
|
records_count: 0,
|
|
data_quality_score: 0.0,
|
|
error_message: String::new(),
|
|
})
|
|
.collect();
|
|
|
|
let result = StatusResult {
|
|
total_count: response.total_count,
|
|
jobs,
|
|
};
|
|
output::render(&result, format)?;
|
|
}
|
|
|
|
DataAction::Cache => {
|
|
let result = CacheResult {
|
|
message:
|
|
"Data cache managed by MinIO. Use `fxt data download` to fetch new data."
|
|
.to_owned(),
|
|
};
|
|
output::render(&result, format)?;
|
|
}
|
|
|
|
DataAction::Feeds => {
|
|
let result = FeedsResult {
|
|
message:
|
|
"Live feed status requires a running data acquisition service. Use `fxt service status data-acquisition` for details."
|
|
.to_owned(),
|
|
};
|
|
output::render(&result, format)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|