diff --git a/src/agent.rs b/src/agent.rs index 71c69de..9d27b32 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -570,13 +570,7 @@ async fn run_single_backtest( .await .context("poll")?; - Ok(BacktestResult::from_response( - &final_resp, - &inst.symbol, - &inst.exchange, - &inst.base(), - &inst.quote(), - )) + Ok(BacktestResult::from_response(&final_resp, &inst.symbol)) } fn save_validated_strategy( diff --git a/src/prompts.rs b/src/prompts.rs index 667b059..a092c70 100644 --- a/src/prompts.rs +++ b/src/prompts.rs @@ -107,6 +107,8 @@ Buy a fixed number of base units (semantic alias for a decimal string): ```json {{"kind":"position_quantity"}} ``` +Alternatively, `"9999"` works for exits: sell quantities are automatically capped to the open +position size, so a large fixed number is equivalent to `position_quantity`. NEVER use placeholder strings like `"ATR_SIZED"`, `"FULL_BALANCE"`, `"all"`, `"dynamic"` — these are rejected immediately. @@ -251,7 +253,7 @@ Common mistakes to NEVER make: }} ] }}, - "then": {{"side": "sell", "quantity": "0.01"}} + "then": {{"side": "sell", "quantity": {{"kind": "position_quantity"}}}} }} ] }} @@ -302,7 +304,7 @@ Common mistakes to NEVER make: }} ] }}, - "then": {{"side": "sell", "quantity": "0.01"}} + "then": {{"side": "sell", "quantity": {{"kind": "position_quantity"}}}} }} ] }} @@ -364,7 +366,7 @@ Common mistakes to NEVER make: }} ] }}, - "then": {{"side": "sell", "quantity": "0.01"}} + "then": {{"side": "sell", "quantity": {{"kind": "position_quantity"}}}} }} ] }} @@ -449,7 +451,7 @@ The MACD line is `EMA(12) - EMA(26)`; the signal line is `EMA(9)` of the MACD li }} ] }}, - "then": {{"side": "sell", "quantity": "0.01"}} + "then": {{"side": "sell", "quantity": {{"kind": "position_quantity"}}}} }} ] }} diff --git a/src/swym.rs b/src/swym.rs index 6b0cdc6..480cf0c 100644 --- a/src/swym.rs +++ b/src/swym.rs @@ -44,6 +44,8 @@ pub struct CandleCoverage { pub first_open: String, pub last_close: String, pub count: u64, + pub expected_count: Option, + pub coverage_pct: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -66,17 +68,8 @@ pub struct BacktestResult { } impl BacktestResult { - /// Parse a backtest response. - /// - /// `exchange`, `base`, `quote` are needed to derive the instrument key used - /// in the `result_summary.instruments` map (e.g. `binancespot-eth_usdc`). - pub fn from_response( - resp: &PaperRunResponse, - instrument: &str, - exchange: &str, - base: &str, - quote: &str, - ) -> Self { + /// Parse a backtest response from the flat `result_summary` structure. + pub fn from_response(resp: &PaperRunResponse, instrument: &str) -> Self { let summary = resp.result_summary.as_ref(); if let Some(s) = summary { tracing::debug!("[{instrument}] result_summary: {}", serde_json::to_string(s).unwrap_or_default()); @@ -84,29 +77,20 @@ impl BacktestResult { tracing::debug!("[{instrument}] result_summary: null"); } - // The API key for per-instrument stats: "binance_spot" + "eth" + "usdc" → "binancespot-eth_usdc" - let inst_key = format!("{}-{}_{}", exchange.replace('_', ""), base, quote); - - let total_positions = summary.and_then(|s| { - s["backtest_metadata"]["position_count"].as_u64().map(|v| v as u32) - }); - - let inst_stats = summary.and_then(|s| s["instruments"].get(&inst_key)); - Self { run_id: resp.id, instrument: instrument.to_string(), status: resp.status.clone(), - total_positions, - winning_positions: None, - losing_positions: None, - win_rate: inst_stats.and_then(|s| parse_ratio_value(&s["win_rate"])), - profit_factor: inst_stats.and_then(|s| parse_ratio_value(&s["profit_factor"])), - total_pnl: inst_stats.and_then(|s| parse_decimal_str(&s["pnl"])), - net_pnl: inst_stats.and_then(|s| parse_decimal_str(&s["pnl"])), - sharpe_ratio: inst_stats.and_then(|s| parse_ratio_value(&s["sharpe_ratio"])), - total_fees: None, - avg_bars_in_trade: None, + total_positions: summary.and_then(|s| s["total_positions"].as_u64().map(|v| v as u32)), + winning_positions: summary.and_then(|s| s["winning_positions"].as_u64().map(|v| v as u32)), + losing_positions: summary.and_then(|s| s["losing_positions"].as_u64().map(|v| v as u32)), + win_rate: summary.and_then(|s| parse_number(&s["win_rate"])), + profit_factor: summary.and_then(|s| parse_number(&s["profit_factor"])), + total_pnl: summary.and_then(|s| parse_number(&s["total_pnl"])), + net_pnl: summary.and_then(|s| parse_number(&s["net_pnl"])), + sharpe_ratio: summary.and_then(|s| parse_number(&s["sharpe_ratio"])), + total_fees: summary.and_then(|s| parse_number(&s["total_fees"])), + avg_bars_in_trade: summary.and_then(|s| parse_number(&s["avg_bars_in_trade"])), error_message: resp.error_message.clone(), condition_audit_summary: summary.and_then(|s| s.get("condition_audit_summary").cloned()), } @@ -151,18 +135,10 @@ impl BacktestResult { } } -/// Parse a `{"interval": null, "value": "123.45"}` ratio wrapper. -/// Returns `None` for null, missing, or sentinel values (Decimal::MAX ≈ 7.9e28). -fn parse_ratio_value(v: &Value) -> Option { - let s = v.get("value")?.as_str()?; - let f: f64 = s.parse().ok()?; - if f.abs() > 1e20 { None } else { Some(f) } -} - -/// Parse a plain decimal string JSON value. -/// Returns `None` for null, missing, or sentinel values. -fn parse_decimal_str(v: &Value) -> Option { - let f: f64 = v.as_str()?.parse().ok()?; +/// Parse a numeric JSON value — accepts either a plain JSON number or a decimal string. +/// Returns `None` for null, missing, or sentinel values (>1e20 in magnitude). +fn parse_number(v: &Value) -> Option { + let f = v.as_f64().or_else(|| v.as_str()?.parse().ok())?; if f.abs() > 1e20 { None } else { Some(f) } }