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 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 12:58:49 +02:00
parent deb28f6714
commit fc9b7e094a
2 changed files with 55 additions and 5 deletions

View File

@@ -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);
}
}
}

View File

@@ -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::<Vec<_>>()
.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::<Vec<_>>()
.join(", "),
other => other.to_string(),
}
}
impl SwymClient {
pub fn new(base_url: &str) -> Result<Self> {
let client = Client::builder()