feat: model-family-aware token budgets and prompt style

Add ModelFamily enum (config.rs) detected from the model name:
- DeepSeekR1: matched on "deepseek-r1", "r1-distill" — R1 thinking blocks
  consume thousands of output tokens before the JSON; max_output_tokens
  raised to 32768 and HTTP timeout to 300s; prompt tells the model its
  <think> output is stripped and only the bare JSON is used
- Generic: previous behaviour (8192 tokens, 120s timeout)

ClaudeClient stores the detected family and uses it for max_tokens and
the request timeout. family() accessor lets the caller (agent.rs) pass
it into system_prompt().

prompts::system_prompt() now accepts &ModelFamily and injects a
family-specific "output format" section in place of the hardcoded
"How to respond" block. New families can be added by extending the
enum and the match arms without touching prompt logic elsewhere.

Also: log full anyhow cause chain (:#) on JSON extraction failure and
show response length alongside the truncated preview, to make future
diagnosis easier.

Root cause of the 2026-03-09T18:29:22 run failure: R1's thinking tokens
counted against max_tokens:8192, leaving only ~500 chars for the actual
JSON, which was always truncated mid-object.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 18:39:51 +02:00
parent 6f4f864d28
commit 89f7ba66e0
4 changed files with 88 additions and 19 deletions

View File

@@ -1,9 +1,28 @@
/// System prompt for the strategy-generation Claude instance.
use crate::config::ModelFamily;
/// System prompt for the strategy-generation model.
///
/// This is the most important part of the agent — it defines how Claude
/// thinks about strategy design, what it knows about the DSL, and how
/// it should interpret backtest results.
pub fn system_prompt(dsl_schema: &str) -> String {
/// Accepts a `ModelFamily` so each family can receive tailored guidance
/// while sharing the common DSL schema and strategy evaluation rules.
pub fn system_prompt(dsl_schema: &str, family: &ModelFamily) -> String {
let output_instructions = match family {
ModelFamily::DeepSeekR1 => {
"## Output format\n\n\
Think through your strategy design carefully before committing to it. \
After your thinking, output ONLY a bare JSON object — no markdown fences, \
no commentary, no explanation. Start with `{` and end with `}`. \
Your thinking will be stripped automatically; only the JSON is used."
}
ModelFamily::Generic => {
"## How to respond\n\n\
You must respond with ONLY a valid JSON object — the strategy config.\n\
No prose, no markdown explanation, no commentary.\n\
Just the raw JSON starting with { and ending with }.\n\n\
The JSON must be a valid strategy with \"type\": \"rule_based\".\n\
Use \"usdc\" (not \"usdt\") as the quote asset for balance expressions."
}
};
format!(
r##"You are a quantitative trading strategy researcher. Your task is to design,
evaluate, and iteratively refine trading strategies expressed in the swym JSON DSL.
@@ -88,14 +107,7 @@ Every strategy MUST have:
- A time-based exit: use bars_since_entry to avoid holding losers indefinitely
- Reasonable position sizing: prefer ATR-based or percent-of-balance over fixed quantity
## How to respond
You must respond with ONLY a valid JSON object — the strategy config.
No prose, no markdown explanation, no commentary.
Just the raw JSON starting with {{ and ending with }}.
The JSON must be a valid strategy with "type": "rule_based".
Use "usdc" (not "usdt") as the quote asset for balance expressions.
{output_instructions}
## Interpreting backtest results