Add get_latest_report tool with equity chart support
**New Tool: get_latest_report** - Returns the most recent backtest report from the database - Includes full report details: metrics, paths, tags, notes, verdict - Optional include_chart parameter (default: true) - Returns equity chart as base64 PNG when available **Implementation:** - ReportDb.get_latest(): Query for most recent report by created_at DESC - handle_get_latest_report(): Fetches report and reads equity.png from charts_dir - Base64 encoding using base64 crate v0.22 - Graceful handling when chart not found (equity_chart_error field) **Files Changed:** - storage/database.rs: Added get_latest() method - tools/definitions.rs: Added tool_get_latest_report() definition - tools/handlers/mod.rs: Added dispatch for get_latest_report - tools/handlers/reports.rs: Implemented handler with base64 chart encoding - Cargo.toml: Added base64 = 0.22 dependency
This commit is contained in:
@@ -62,6 +62,7 @@ impl ToolHandler {
|
||||
"list_set_files" => setfiles::handle_list_set_files(&self.config).await,
|
||||
|
||||
// Report handlers
|
||||
"get_latest_report" => reports::handle_get_latest_report(&self.config, args).await,
|
||||
"list_reports" => reports::handle_list_reports(args).await,
|
||||
"search_reports" => reports::handle_search_reports(args).await,
|
||||
"prune_reports" => reports::handle_prune_reports(&self.config, args).await,
|
||||
|
||||
@@ -1,10 +1,95 @@
|
||||
use anyhow::Result;
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use crate::models::Config;
|
||||
use crate::storage::{ReportDb, ReportFilters};
|
||||
|
||||
pub async fn handle_get_latest_report(_config: &Config, args: &Value) -> Result<Value> {
|
||||
let include_chart = args.get("include_chart").and_then(|v| v.as_bool()).unwrap_or(true);
|
||||
|
||||
let db = ReportDb::new(&Config::db_path());
|
||||
if let Err(e) = db.init() {
|
||||
return Ok(json!({
|
||||
"content": [{ "type": "text", "text": format!("DB error: {}", e) }],
|
||||
"isError": true
|
||||
}));
|
||||
}
|
||||
|
||||
match db.get_latest()? {
|
||||
Some(entry) => {
|
||||
let mut response = json!({
|
||||
"success": true,
|
||||
"report": {
|
||||
"id": entry.id,
|
||||
"expert": entry.expert,
|
||||
"symbol": entry.symbol,
|
||||
"timeframe": entry.timeframe,
|
||||
"from_date": entry.from_date,
|
||||
"to_date": entry.to_date,
|
||||
"created_at": entry.created_at,
|
||||
"net_profit": entry.net_profit,
|
||||
"profit_factor": entry.profit_factor,
|
||||
"max_dd_pct": entry.max_dd_pct,
|
||||
"sharpe_ratio": entry.sharpe_ratio,
|
||||
"total_trades": entry.total_trades,
|
||||
"win_rate_pct": entry.win_rate_pct,
|
||||
"recovery_factor": entry.recovery_factor,
|
||||
"deposit": entry.deposit,
|
||||
"currency": entry.currency,
|
||||
"leverage": entry.leverage,
|
||||
"duration_seconds": entry.duration_seconds,
|
||||
"set_file_original": entry.set_file_original,
|
||||
"set_snapshot_path": entry.set_snapshot_path,
|
||||
"report_dir": entry.report_dir,
|
||||
"charts_dir": entry.charts_dir,
|
||||
"tags": entry.tags,
|
||||
"notes": entry.notes,
|
||||
"verdict": entry.verdict,
|
||||
}
|
||||
});
|
||||
|
||||
// Include equity chart as base64 if requested and available
|
||||
if include_chart {
|
||||
if let Some(charts_dir) = &entry.charts_dir {
|
||||
let chart_path = Path::new(charts_dir).join("equity.png");
|
||||
if chart_path.exists() {
|
||||
match fs::read(&chart_path) {
|
||||
Ok(bytes) => {
|
||||
let base64 = BASE64.encode(&bytes);
|
||||
response["report"]["equity_chart_base64"] = json!(base64);
|
||||
response["report"]["equity_chart_format"] = json!("png");
|
||||
}
|
||||
Err(e) => {
|
||||
response["report"]["equity_chart_error"] = json!(format!("Failed to read chart: {}", e));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response["report"]["equity_chart_error"] = json!("equity.png not found in charts_dir");
|
||||
}
|
||||
} else {
|
||||
response["report"]["equity_chart_error"] = json!("No charts_dir available for this report");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(json!({
|
||||
"content": [{ "type": "text", "text": response.to_string() }],
|
||||
"isError": false
|
||||
}))
|
||||
}
|
||||
None => {
|
||||
Ok(json!({
|
||||
"content": [{ "type": "text", "text": json!({
|
||||
"success": false,
|
||||
"error": "No reports found in database"
|
||||
}).to_string() }],
|
||||
"isError": true
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_list_reports(args: &Value) -> Result<Value> {
|
||||
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(30) as usize;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user