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:
17
src/agent.rs
17
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
43
src/swym.rs
43
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::<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()
|
||||
|
||||
Reference in New Issue
Block a user