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:
Generated
+7
@@ -94,6 +94,12 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.11.1"
|
version = "2.11.1"
|
||||||
@@ -478,6 +484,7 @@ name = "mt5-quant"
|
|||||||
version = "1.27.0"
|
version = "1.27.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
|||||||
@@ -27,3 +27,4 @@ encoding_rs = "0.8"
|
|||||||
tempfile = "3.0"
|
tempfile = "3.0"
|
||||||
roxmltree = "0.21.1"
|
roxmltree = "0.21.1"
|
||||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||||
|
base64 = "0.22"
|
||||||
|
|||||||
@@ -323,4 +323,55 @@ impl ReportDb {
|
|||||||
let n: usize = conn.query_row("SELECT COUNT(*) FROM reports", [], |r| r.get(0))?;
|
let n: usize = conn.query_row("SELECT COUNT(*) FROM reports", [], |r| r.get(0))?;
|
||||||
Ok(n)
|
Ok(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the latest report by created_at (most recent first)
|
||||||
|
pub fn get_latest(&self) -> Result<Option<ReportEntry>> {
|
||||||
|
let conn = self.connect()?;
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT id, expert, symbol, timeframe, model, from_date, to_date, \
|
||||||
|
created_at, set_file_original, set_snapshot_path, report_dir, charts_dir, \
|
||||||
|
net_profit, profit_factor, max_dd_pct, sharpe_ratio, total_trades, \
|
||||||
|
win_rate_pct, recovery_factor, deposit, currency, leverage, \
|
||||||
|
duration_seconds, tags, notes, verdict \
|
||||||
|
FROM reports ORDER BY created_at DESC LIMIT 1"
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let entry = stmt
|
||||||
|
.query_map([], |row| {
|
||||||
|
let tags_json: String = row.get(23)?;
|
||||||
|
let tags: Vec<String> = serde_json::from_str(&tags_json).unwrap_or_default();
|
||||||
|
Ok(ReportEntry {
|
||||||
|
id: row.get(0)?,
|
||||||
|
expert: row.get(1)?,
|
||||||
|
symbol: row.get(2)?,
|
||||||
|
timeframe: row.get(3)?,
|
||||||
|
model: row.get(4)?,
|
||||||
|
from_date: row.get(5)?,
|
||||||
|
to_date: row.get(6)?,
|
||||||
|
created_at: row.get(7)?,
|
||||||
|
set_file_original: row.get(8)?,
|
||||||
|
set_snapshot_path: row.get(9)?,
|
||||||
|
report_dir: row.get(10)?,
|
||||||
|
charts_dir: row.get(11)?,
|
||||||
|
net_profit: row.get(12)?,
|
||||||
|
profit_factor: row.get(13)?,
|
||||||
|
max_dd_pct: row.get(14)?,
|
||||||
|
sharpe_ratio: row.get(15)?,
|
||||||
|
total_trades: row.get(16)?,
|
||||||
|
win_rate_pct: row.get(17)?,
|
||||||
|
recovery_factor: row.get(18)?,
|
||||||
|
deposit: row.get(19)?,
|
||||||
|
currency: row.get(20)?,
|
||||||
|
leverage: row.get(21)?,
|
||||||
|
duration_seconds: row.get(22)?,
|
||||||
|
tags,
|
||||||
|
notes: row.get(24)?,
|
||||||
|
verdict: row.get(25)?,
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.filter_map(|r| r.ok())
|
||||||
|
.next();
|
||||||
|
|
||||||
|
Ok(entry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ pub fn get_tools_list() -> Value {
|
|||||||
tool_get_backtest_status(),
|
tool_get_backtest_status(),
|
||||||
tool_get_optimization_status(),
|
tool_get_optimization_status(),
|
||||||
tool_prune_reports(),
|
tool_prune_reports(),
|
||||||
|
tool_get_latest_report(),
|
||||||
tool_list_reports(),
|
tool_list_reports(),
|
||||||
tool_search_reports(),
|
tool_search_reports(),
|
||||||
tool_tail_log(),
|
tool_tail_log(),
|
||||||
@@ -256,6 +257,19 @@ fn tool_prune_reports() -> Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tool_get_latest_report() -> Value {
|
||||||
|
json!({
|
||||||
|
"name": "get_latest_report",
|
||||||
|
"description": "Get the latest backtest report with full details and equity chart image",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"include_chart": { "type": "boolean", "description": "Include equity chart as base64 PNG (default: true)", "default": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn tool_list_reports() -> Value {
|
fn tool_list_reports() -> Value {
|
||||||
json!({
|
json!({
|
||||||
"name": "list_reports",
|
"name": "list_reports",
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ impl ToolHandler {
|
|||||||
"list_set_files" => setfiles::handle_list_set_files(&self.config).await,
|
"list_set_files" => setfiles::handle_list_set_files(&self.config).await,
|
||||||
|
|
||||||
// Report handlers
|
// Report handlers
|
||||||
|
"get_latest_report" => reports::handle_get_latest_report(&self.config, args).await,
|
||||||
"list_reports" => reports::handle_list_reports(args).await,
|
"list_reports" => reports::handle_list_reports(args).await,
|
||||||
"search_reports" => reports::handle_search_reports(args).await,
|
"search_reports" => reports::handle_search_reports(args).await,
|
||||||
"prune_reports" => reports::handle_prune_reports(&self.config, args).await,
|
"prune_reports" => reports::handle_prune_reports(&self.config, args).await,
|
||||||
|
|||||||
@@ -1,10 +1,95 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use crate::models::Config;
|
use crate::models::Config;
|
||||||
use crate::storage::{ReportDb, ReportFilters};
|
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> {
|
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;
|
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(30) as usize;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user