From fc9b7e094a307510fd180c8298e26a099955b0b4 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Mon, 9 Mar 2026 12:58:49 +0200 Subject: [PATCH] feat(agent): add strategy quality introspection Log full strategy JSON at debug level, show full anyhow cause chain on submit failures, surface condition_audit_summary for 0-trade results in both logs and the summary fed back to the AI each iteration. Co-Authored-By: Claude Sonnet 4.6 --- src/agent.rs | 17 ++++++++++++++--- src/swym.rs | 43 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/agent.rs b/src/agent.rs index 955a005..6fbc3f6 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -3,7 +3,7 @@ use std::time::Duration; use anyhow::{Context, Result}; use serde_json::Value; -use tracing::{error, info, warn}; +use tracing::{debug, error, info, warn}; use crate::claude::{self, ClaudeClient, Message}; use crate::config::{Cli, Instrument}; @@ -190,6 +190,7 @@ pub async fn run(cli: &Cli) -> Result<()> { strategy["candle_interval"].as_str().unwrap_or("?"), strategy["rules"].as_array().map(|r| r.len()).unwrap_or(0) ); + debug!("strategy JSON:\n{}", serde_json::to_string_pretty(&strategy).unwrap_or_default()); // Save the strategy JSON let strat_path = cli.output_dir.join(format!("strategy_{iteration:03}.json")); @@ -215,10 +216,15 @@ pub async fn run(cli: &Cli) -> Result<()> { { Ok(result) => { info!(" {}", result.summary_line()); + if result.total_positions.unwrap_or(0) == 0 { + if let Some(audit) = &result.condition_audit_summary { + info!(" condition audit: {}", serde_json::to_string_pretty(audit).unwrap_or_default()); + } + } results.push(result); } Err(e) => { - warn!(" backtest failed for {}: {e}", inst.symbol); + warn!(" backtest failed for {}: {e:#}", inst.symbol); results.push(BacktestResult { run_id: uuid::Uuid::nil(), instrument: inst.symbol.clone(), @@ -278,10 +284,15 @@ pub async fn run(cli: &Cli) -> Result<()> { { Ok(result) => { info!(" OOS {}", result.summary_line()); + if result.total_positions.unwrap_or(0) == 0 { + if let Some(audit) = &result.condition_audit_summary { + info!(" OOS condition audit: {}", serde_json::to_string_pretty(audit).unwrap_or_default()); + } + } oos_results.push(result); } Err(e) => { - warn!(" OOS backtest failed for {}: {e}", inst.symbol); + warn!(" OOS backtest failed for {}: {e:#}", inst.symbol); } } } diff --git a/src/swym.rs b/src/swym.rs index a639c8a..6715bea 100644 --- a/src/swym.rs +++ b/src/swym.rs @@ -82,7 +82,7 @@ impl BacktestResult { self.error_message.as_deref().unwrap_or("unknown error") ); } - format!( + let mut s = format!( "[{}] trades={} win_rate={:.1}% pf={:.2} net_pnl={:.2} sharpe={:.2} avg_bars={:.1}", self.instrument, self.total_positions.unwrap_or(0), @@ -91,7 +91,17 @@ impl BacktestResult { self.net_pnl.unwrap_or(0.0), self.sharpe_ratio.unwrap_or(0.0), self.avg_bars_in_trade.unwrap_or(0.0), - ) + ); + if self.total_positions.unwrap_or(0) == 0 { + if let Some(audit) = &self.condition_audit_summary { + let audit_str = format_audit_summary(audit); + if !audit_str.is_empty() { + s.push_str(" | audit: "); + s.push_str(&audit_str); + } + } + } + s } /// Is this result promising enough to warrant out-of-sample validation? @@ -103,6 +113,35 @@ impl BacktestResult { } } +/// Render a condition_audit_summary Value into a compact one-line string. +/// Handles both object and array shapes we might receive from the API. +fn format_audit_summary(audit: &Value) -> String { + match audit { + Value::Object(map) => map + .iter() + .map(|(k, v)| format!("{k}={v}")) + .collect::>() + .join(", "), + Value::Array(arr) => arr + .iter() + .filter_map(|item| { + let name = item.get("name").or_else(|| item.get("condition"))?.as_str()?; + // Try common field names for hit counts + if let (Some(true_count), Some(total)) = ( + item.get("true_count").or_else(|| item.get("hit_count")).or_else(|| item.get("true_bars")).and_then(|v| v.as_u64()), + item.get("total").or_else(|| item.get("total_bars")).or_else(|| item.get("evaluated")).and_then(|v| v.as_u64()), + ) { + Some(format!("{name}: {true_count}/{total}")) + } else { + Some(format!("{name}: {item}")) + } + }) + .collect::>() + .join(", "), + other => other.to_string(), + } +} + impl SwymClient { pub fn new(base_url: &str) -> Result { let client = Client::builder()