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:
Devid HW
2026-04-19 01:56:41 +07:00
parent 13e821bc5d
commit e94ec8153d
6 changed files with 159 additions and 0 deletions
+1
View File
@@ -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,
+85
View File
@@ -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;