From fcb9a2f553b2f2d19012df5da500398a13f43e22 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Wed, 11 Mar 2026 18:15:24 +0200 Subject: [PATCH] chore: attempt dedupe guidance in prompt --- src/agent.rs | 23 +++++++++++++++++++++++ src/prompts.rs | 18 +++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/agent.rs b/src/agent.rs index 3360268..4338ee0 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -218,6 +218,8 @@ pub async fn run(cli: &Cli) -> Result<()> { let mut conversation: Vec = Vec::new(); let mut best_strategy: Option<(f64, Value)> = None; // (avg_sharpe, strategy) let mut consecutive_failures = 0u32; + // Deduplication: track canonical strategy JSON → first iteration it was tested. + let mut tested_strategies: std::collections::HashMap = std::collections::HashMap::new(); let instrument_names: Vec = instruments.iter().map(|i| i.symbol.clone()).collect(); @@ -392,6 +394,27 @@ pub async fn run(cli: &Cli) -> Result<()> { } } + // Deduplication check: skip strategies identical to one already tested this run. + let strategy_key = serde_json::to_string(&strategy).unwrap_or_default(); + if let Some(&first_iter) = tested_strategies.get(&strategy_key) { + warn!("duplicate strategy (identical to iteration {first_iter}), skipping backtest"); + let record = IterationRecord { + iteration, + strategy: strategy.clone(), + results: vec![], + validation_notes: vec![format!( + "DUPLICATE: this exact strategy was already tested in iteration {first_iter}. \ + You submitted identical JSON. You MUST design a completely different strategy — \ + different indicator family, different entry conditions, or different timeframe. \ + Do NOT submit the same JSON again." + )], + }; + info!("{}", record.summary()); + history.push(record); + continue; + } + tested_strategies.insert(strategy_key, iteration); + // Run backtests against all instruments (in-sample) let mut results: Vec = Vec::new(); diff --git a/src/prompts.rs b/src/prompts.rs index 64f7a27..7eb670a 100644 --- a/src/prompts.rs +++ b/src/prompts.rs @@ -103,6 +103,14 @@ Buy a fixed number of base units (semantic alias for a decimal string): "right":{{"kind":"func","name":"atr","period":14}}}} ``` +CRITICAL — ATR sizing and balance limits: `N/atr(14)` expresses quantity in BASE asset units. +For BTC, 4h ATR ≈ $1500–3000. So `1000/atr(14)` ≈ 0.4–0.7 BTC ≈ $32k–56k notional — +silently rejected on a $10k account (fill returns None, 0 positions open, no error shown). +The numerator N represents your intended dollar risk per trade. For a $10k account keep N ≤ 200. +`200/atr(14)` ≈ 0.07–0.13 BTC ≈ $5.6k–10k notional — fits within a $10k account. +Prefer `percent_of_balance` for most sizing. Only reach for ATR-based Expr sizing when you need +volatility-scaled position risk, and keep the numerator proportional to your risk tolerance. + **4. Exit rules** — use `position_quantity` to close the exact open size: ```json {{"kind":"position_quantity"}} @@ -189,7 +197,11 @@ When I share results from previous iterations, use them to guide your next strat - **Zero trades**: The entry conditions are too restrictive or never co-occur. Relax thresholds, simplify conditions, or check if the indicator periods make - sense for the candle interval. + sense for the candle interval. Also check your position sizing — if using an + ATR-based Expr quantity (`N/atr(14)`), a large N can produce a notional value + exceeding your account balance (e.g. `1000/atr(14)` on BTC ≈ 0.4 BTC ≈ $32k), + which is silently rejected by the fill engine. Switch to `percent_of_balance` + or reduce N to ≤ 200 for a $10k account. - **Many trades but negative PnL**: The entry signal has no edge, or the exit logic is poor. Try different indicator combinations, add trend filters, or @@ -516,6 +528,10 @@ CRITICAL: `apply_func` uses `"input"`, not `"expr"`. Writing `"expr":` will be r - Spot markets are long-only: gate buy (entry) rules with state "flat" and sell (exit) rules with state "long". Never add a short-entry (sell when flat) rule on spot. - Futures markets support both directions: long entry = buy when flat; long exit = sell when long; short entry = sell when flat; short exit (cover) = buy when short. Always include a stop-loss and time exit for both long and short legs. - Never use a placeholder string for `quantity` — `"ATR_SIZED"`, `"FULL_BALANCE"`, `"dynamic"`, etc. are all invalid and will be rejected. +- Don't use large ATR-based sizing numerators. `N/atr(14)` gives BASE units; for BTC (ATR ≈ $2000 + on 4h), `1000/atr(14)` ≈ 0.5 BTC ≈ $40k — silently rejected on a $10k account. Keep N ≤ 200 + or use `percent_of_balance`. The condition audit may show entry conditions firing while 0 positions + open — this is the typical symptom of an oversized ATR quantity. - `{{"method":"position_quantity"}}` is WRONG for exit rules — use `{{"kind":"position_quantity"}}` (see Quantity section above). {futures_examples}"##, futures_examples = if has_futures { FUTURES_SHORT_EXAMPLES } else { "" },