Make analytics granular - individual tools + selective analyze_report

**Granular Analytics Tools (8 new tools):**
- analyze_monthly_pnl - Monthly profit/loss breakdown
- analyze_drawdown_events - Drawdown events from balance curve
- analyze_top_losses - Top N worst losses with grid depth
- analyze_loss_sequences - Consecutive loss streaks analysis
- analyze_position_pairs - Entry/exit position pairs
- analyze_direction_bias - Long vs Short performance stats
- analyze_streaks - Win/loss streaks with dates
- analyze_concurrent_peak - Peak concurrent open positions

**Updated analyze_report:**
- Now supports selective analytics via 'analytics' array parameter
- Runs all analytics by default (backward compatible)
- Optional top_losses_limit parameter
- Returns list of analytics that were run

**Changes:**
- analytics/analyze.rs: Made all analysis methods public
- tools/definitions.rs: Added 8 new tool definitions + updated analyze_report schema
- tools/handlers/mod.rs: Added dispatch for new tools
- tools/handlers/analysis.rs: Added helper load_report_data() + 8 granular handlers + selective analyze_report logic
This commit is contained in:
Devid HW
2026-04-19 02:13:33 +07:00
parent e94ec8153d
commit 1f63505cb9
4 changed files with 339 additions and 17 deletions
+200 -8
View File
@@ -1,16 +1,14 @@
use anyhow::Result;
use serde_json::{json, Value};
use std::collections::HashSet;
use std::fs;
use std::path::Path;
use crate::analytics::DealAnalyzer;
use crate::models::deals::Deal;
use crate::models::metrics::Metrics;
pub async fn handle_analyze_report(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
/// Helper to load deals and metrics from report directory
fn load_report_data(report_dir: &str) -> Result<(Vec<Deal>, Metrics)> {
let deals_csv = Path::new(report_dir).join("deals.csv");
let metrics_json = Path::new(report_dir).join("metrics.json");
@@ -27,11 +25,56 @@ pub async fn handle_analyze_report(_config: &Config, args: &Value) -> Result<Val
Metrics::default()
};
let _strategy = args.get("strategy").and_then(|v| v.as_str()).unwrap_or("grid");
let _deep = args.get("deep").and_then(|v| v.as_bool()).unwrap_or(false);
Ok((deals, metrics))
}
pub async fn handle_analyze_report(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, metrics) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.analyze(&deals, &metrics);
// Check if specific analytics requested
let requested: Option<HashSet<String>> = args.get("analytics")
.and_then(|v| v.as_array())
.map(|arr| arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect());
let top_losses_limit = args.get("top_losses_limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
let all = requested.is_none();
let req = |name: &str| all || requested.as_ref().map(|s| s.contains(name)).unwrap_or(false);
// Build selective result
let mut result = json!({});
if req("monthly_pnl") || all {
result["monthly"] = json!(analyzer.monthly_pnl(&deals));
}
if req("drawdown_events") || all {
result["dd_events"] = json!(analyzer.reconstruct_dd_events(&deals, &metrics));
}
if req("top_losses") || all {
result["top_losses"] = json!(analyzer.top_losses(&deals, top_losses_limit));
}
if req("loss_sequences") || all {
result["loss_sequences"] = json!(analyzer.loss_sequences(&deals));
}
if req("position_pairs") || all {
result["position_pairs"] = json!(analyzer.position_pairs(&deals));
}
if req("direction_bias") || all {
result["direction_bias"] = json!(analyzer.direction_bias(&deals));
}
if req("streak_analysis") || all {
result["streak_analysis"] = json!(analyzer.streak_analysis(&deals));
}
if req("concurrent_peak") || all {
result["concurrent_peak"] = json!(analyzer.concurrent_peak(&deals));
}
let analysis_path = Path::new(report_dir).join("analysis.json");
fs::write(&analysis_path, serde_json::to_string_pretty(&result)?)?;
@@ -40,6 +83,7 @@ pub async fn handle_analyze_report(_config: &Config, args: &Value) -> Result<Val
"content": [{ "type": "text", "text": json!({
"success": true,
"analysis_file": analysis_path.to_string_lossy(),
"analytics_run": requested.map(|s| s.iter().cloned().collect::<Vec<_>>()).unwrap_or_else(|| vec!["all".to_string()]),
"summary": result,
}).to_string() }],
"isError": false
@@ -113,5 +157,153 @@ pub async fn handle_compare_baseline(_config: &Config, args: &Value) -> Result<V
}))
}
// === Granular Analytics Handlers ===
pub async fn handle_analyze_monthly_pnl(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.monthly_pnl(&deals);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"monthly_pnl": result,
}).to_string() }],
"isError": false
}))
}
pub async fn handle_analyze_drawdown_events(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, metrics) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.reconstruct_dd_events(&deals, &metrics);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"drawdown_events": result,
}).to_string() }],
"isError": false
}))
}
pub async fn handle_analyze_top_losses(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.top_losses(&deals, limit);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"limit": limit,
"top_losses": result,
}).to_string() }],
"isError": false
}))
}
pub async fn handle_analyze_loss_sequences(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.loss_sequences(&deals);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"loss_sequences": result,
}).to_string() }],
"isError": false
}))
}
pub async fn handle_analyze_position_pairs(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.position_pairs(&deals);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"position_pairs": result,
}).to_string() }],
"isError": false
}))
}
pub async fn handle_analyze_direction_bias(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.direction_bias(&deals);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"direction_bias": result,
}).to_string() }],
"isError": false
}))
}
pub async fn handle_analyze_streaks(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.streak_analysis(&deals);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"streak_analysis": result,
}).to_string() }],
"isError": false
}))
}
pub async fn handle_analyze_concurrent_peak(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.concurrent_peak(&deals);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"concurrent_peak": result,
}).to_string() }],
"isError": false
}))
}
// Import Config for analysis module
use crate::models::Config;
+8
View File
@@ -49,6 +49,14 @@ impl ToolHandler {
// Analysis handlers
"analyze_report" => analysis::handle_analyze_report(&self.config, args).await,
"analyze_monthly_pnl" => analysis::handle_analyze_monthly_pnl(&self.config, args).await,
"analyze_drawdown_events" => analysis::handle_analyze_drawdown_events(&self.config, args).await,
"analyze_top_losses" => analysis::handle_analyze_top_losses(&self.config, args).await,
"analyze_loss_sequences" => analysis::handle_analyze_loss_sequences(&self.config, args).await,
"analyze_position_pairs" => analysis::handle_analyze_position_pairs(&self.config, args).await,
"analyze_direction_bias" => analysis::handle_analyze_direction_bias(&self.config, args).await,
"analyze_streaks" => analysis::handle_analyze_streaks(&self.config, args).await,
"analyze_concurrent_peak" => analysis::handle_analyze_concurrent_peak(&self.config, args).await,
"compare_baseline" => analysis::handle_compare_baseline(&self.config, args).await,
// Set file handlers