fix: parse actual result_summary structure (backtest_metadata + assets)
The API doc described a flat result_summary that doesn't exist yet in the
deployed backend. The actual shape is:
{ backtest_metadata: { position_count }, assets: [...], condition_audit_summary }
- total_positions from backtest_metadata.position_count
- net_pnl from assets[quote].tear_sheet.balance_end.total - initial_balance
- win_rate, profit_factor, sharpe_ratio, total_fees, avg_bars_in_trade
remain None until the API adds them
from_response() takes quote and initial_balance again to locate the
right asset and compute PnL. summary_line() only prints metrics that
are actually present. is_promising() falls back to net_pnl>0 + trades
when sharpe is unavailable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -570,7 +570,8 @@ async fn run_single_backtest(
|
|||||||
.await
|
.await
|
||||||
.context("poll")?;
|
.context("poll")?;
|
||||||
|
|
||||||
Ok(BacktestResult::from_response(&final_resp, &inst.symbol))
|
let initial_bal: f64 = initial_balance.parse().unwrap_or(10000.0);
|
||||||
|
Ok(BacktestResult::from_response(&final_resp, &inst.symbol, &inst.quote(), initial_bal))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_validated_strategy(
|
fn save_validated_strategy(
|
||||||
|
|||||||
74
src/swym.rs
74
src/swym.rs
@@ -69,8 +69,17 @@ pub struct BacktestResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BacktestResult {
|
impl BacktestResult {
|
||||||
/// Parse a backtest response from the flat `result_summary` structure.
|
/// Parse a backtest response.
|
||||||
pub fn from_response(resp: &PaperRunResponse, instrument: &str) -> Self {
|
///
|
||||||
|
/// `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.
|
||||||
|
pub fn from_response(
|
||||||
|
resp: &PaperRunResponse,
|
||||||
|
instrument: &str,
|
||||||
|
quote: &str,
|
||||||
|
initial_balance: f64,
|
||||||
|
) -> Self {
|
||||||
let summary = resp.result_summary.as_ref();
|
let summary = resp.result_summary.as_ref();
|
||||||
if let Some(s) = summary {
|
if let Some(s) = summary {
|
||||||
tracing::debug!("[{instrument}] result_summary: {}", serde_json::to_string(s).unwrap_or_default());
|
tracing::debug!("[{instrument}] result_summary: {}", serde_json::to_string(s).unwrap_or_default());
|
||||||
@@ -78,20 +87,35 @@ impl BacktestResult {
|
|||||||
tracing::debug!("[{instrument}] result_summary: null");
|
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)
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
run_id: resp.id,
|
run_id: resp.id,
|
||||||
instrument: instrument.to_string(),
|
instrument: instrument.to_string(),
|
||||||
status: resp.status.clone(),
|
status: resp.status.clone(),
|
||||||
total_positions: summary.and_then(|s| s["total_positions"].as_u64().map(|v| v as u32)),
|
total_positions,
|
||||||
winning_positions: summary.and_then(|s| s["winning_positions"].as_u64().map(|v| v as u32)),
|
winning_positions: None,
|
||||||
losing_positions: summary.and_then(|s| s["losing_positions"].as_u64().map(|v| v as u32)),
|
losing_positions: None,
|
||||||
win_rate: summary.and_then(|s| parse_number(&s["win_rate"])),
|
win_rate: None,
|
||||||
profit_factor: summary.and_then(|s| parse_number(&s["profit_factor"])),
|
profit_factor: None,
|
||||||
total_pnl: summary.and_then(|s| parse_number(&s["total_pnl"])),
|
total_pnl: net_pnl,
|
||||||
net_pnl: summary.and_then(|s| parse_number(&s["net_pnl"])),
|
net_pnl,
|
||||||
sharpe_ratio: summary.and_then(|s| parse_number(&s["sharpe_ratio"])),
|
sharpe_ratio: None,
|
||||||
total_fees: summary.and_then(|s| parse_number(&s["total_fees"])),
|
total_fees: None,
|
||||||
avg_bars_in_trade: summary.and_then(|s| parse_number(&s["avg_bars_in_trade"])),
|
avg_bars_in_trade: None,
|
||||||
error_message: resp.error_message.clone(),
|
error_message: resp.error_message.clone(),
|
||||||
condition_audit_summary: summary.and_then(|s| s.get("condition_audit_summary").cloned()),
|
condition_audit_summary: summary.and_then(|s| s.get("condition_audit_summary").cloned()),
|
||||||
}
|
}
|
||||||
@@ -107,14 +131,20 @@ impl BacktestResult {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let mut s = format!(
|
let mut s = format!(
|
||||||
"[{}] trades={} win_rate={:.1}% pf={:.2} net_pnl={:.2} sharpe={:.2}",
|
"[{}] trades={} net_pnl={:.2}",
|
||||||
self.instrument,
|
self.instrument,
|
||||||
self.total_positions.unwrap_or(0),
|
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.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 self.total_positions.unwrap_or(0) == 0 {
|
||||||
if let Some(audit) = &self.condition_audit_summary {
|
if let Some(audit) = &self.condition_audit_summary {
|
||||||
let audit_str = format_audit_summary(audit);
|
let audit_str = format_audit_summary(audit);
|
||||||
@@ -128,11 +158,15 @@ impl BacktestResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Is this result promising enough to warrant out-of-sample validation?
|
/// Is this result promising enough to warrant out-of-sample validation?
|
||||||
|
/// Uses sharpe if available, otherwise falls back to net_pnl > 0.
|
||||||
pub fn is_promising(&self, min_sharpe: f64, min_trades: u32) -> bool {
|
pub fn is_promising(&self, min_sharpe: f64, min_trades: u32) -> bool {
|
||||||
self.status == "complete"
|
if self.status != "complete" { return false; }
|
||||||
&& self.sharpe_ratio.unwrap_or(0.0) > min_sharpe
|
if self.total_positions.unwrap_or(0) < min_trades { return false; }
|
||||||
&& self.total_positions.unwrap_or(0) >= min_trades
|
if self.net_pnl.unwrap_or(0.0) <= 0.0 { return false; }
|
||||||
&& self.net_pnl.unwrap_or(0.0) > 0.0
|
match self.sharpe_ratio {
|
||||||
|
Some(sr) => sr > min_sharpe,
|
||||||
|
None => true, // sharpe not yet in API response; net_pnl + trades is sufficient signal
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user