Use flat result_summary fields from swym patch 8fb410311

BacktestResult::from_response now reads total_positions, winning_positions,
losing_positions, win_rate, profit_factor, net_pnl, total_pnl, sharpe_ratio,
and total_fees directly from the top-level result_summary object instead of
deriving them from backtest_metadata + balance delta.

Removes the quote/initial_balance parameters that were only needed for the
workaround. Restores the full summary_line format with all metrics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 11:41:53 +02:00
parent 3892ab37c1
commit 87d31f8d7e
2 changed files with 23 additions and 41 deletions

View File

@@ -570,8 +570,7 @@ async fn run_single_backtest(
.await
.context("poll")?;
let initial_bal: f64 = initial_balance.parse().unwrap_or(10000.0);
Ok(BacktestResult::from_response(&final_resp, &inst.symbol, &inst.quote(), initial_bal))
Ok(BacktestResult::from_response(&final_resp, &inst.symbol))
}
fn save_validated_strategy(

View File

@@ -69,16 +69,10 @@ pub struct BacktestResult {
}
impl BacktestResult {
/// Parse a backtest response.
///
/// `quote` is the lowercase quote asset name (e.g. "usdc") used to locate
/// the ending balance in `result_summary.assets[].tear_sheet.balance_end`.
/// `initial_balance` is the starting quote balance used to compute net PnL.
/// Parse a backtest response using the flat summary fields added in swym patch 8fb410311.
pub fn from_response(
resp: &PaperRunResponse,
instrument: &str,
quote: &str,
initial_balance: f64,
) -> Self {
let summary = resp.result_summary.as_ref();
if let Some(s) = summary {
@@ -87,34 +81,29 @@ impl BacktestResult {
tracing::debug!("[{instrument}] result_summary: null");
}
// Actual structure: { backtest_metadata: { position_count }, assets: [...], condition_audit_summary }
let total_positions = summary.and_then(|s| {
s["backtest_metadata"]["position_count"].as_u64().map(|v| v as u32)
});
// net_pnl = ending quote balance starting quote balance
let net_pnl = summary.and_then(|s| {
s["assets"].as_array()?.iter()
.find(|a| a["asset"].as_str().unwrap_or("") == quote)?
["tear_sheet"]["balance_end"]["total"]
.as_str()
.and_then(|t| t.parse::<f64>().ok())
.map(|end| end - initial_balance)
});
let total_positions = summary.and_then(|s| s["total_positions"].as_u64().map(|v| v as u32));
let winning_positions = summary.and_then(|s| s["winning_positions"].as_u64().map(|v| v as u32));
let losing_positions = summary.and_then(|s| s["losing_positions"].as_u64().map(|v| v as u32));
let win_rate = summary.and_then(|s| parse_number(&s["win_rate"]));
let profit_factor = summary.and_then(|s| parse_number(&s["profit_factor"]));
let net_pnl = summary.and_then(|s| parse_number(&s["net_pnl"]));
let total_pnl = summary.and_then(|s| parse_number(&s["total_pnl"]));
let sharpe_ratio = summary.and_then(|s| parse_number(&s["sharpe_ratio"]));
let total_fees = summary.and_then(|s| parse_number(&s["total_fees"]));
Self {
run_id: resp.id,
instrument: instrument.to_string(),
status: resp.status.clone(),
total_positions,
winning_positions: None,
losing_positions: None,
win_rate: None,
profit_factor: None,
total_pnl: net_pnl,
winning_positions,
losing_positions,
win_rate,
profit_factor,
total_pnl,
net_pnl,
sharpe_ratio: None,
total_fees: None,
sharpe_ratio,
total_fees,
avg_bars_in_trade: None,
error_message: resp.error_message.clone(),
condition_audit_summary: summary.and_then(|s| s.get("condition_audit_summary").cloned()),
@@ -131,20 +120,14 @@ impl BacktestResult {
);
}
let mut s = format!(
"[{}] trades={} net_pnl={:.2}",
"[{}] trades={} win_rate={:.1}% pf={:.2} net_pnl={:.2} sharpe={:.2}",
self.instrument,
self.total_positions.unwrap_or(0),
self.win_rate.unwrap_or(0.0) * 100.0,
self.profit_factor.unwrap_or(0.0),
self.net_pnl.unwrap_or(0.0),
self.sharpe_ratio.unwrap_or(0.0),
);
if let Some(wr) = self.win_rate {
s.push_str(&format!(" win_rate={:.1}%", wr * 100.0));
}
if let Some(pf) = self.profit_factor {
s.push_str(&format!(" pf={:.2}", pf));
}
if let Some(sr) = self.sharpe_ratio {
s.push_str(&format!(" sharpe={:.2}", sr));
}
if self.total_positions.unwrap_or(0) == 0 {
if let Some(audit) = &self.condition_audit_summary {
let audit_str = format_audit_summary(audit);
@@ -165,7 +148,7 @@ impl BacktestResult {
if self.net_pnl.unwrap_or(0.0) <= 0.0 { return false; }
match self.sharpe_ratio {
Some(sr) => sr > min_sharpe,
None => true, // sharpe not yet in API response; net_pnl + trades is sufficient signal
None => true, // sharpe absent (e.g. 0 trades); net_pnl + trades is sufficient signal
}
}
}