From 8de3ae5fe118ba6dde36e218fb24f5de9c5201a1 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Tue, 10 Mar 2026 18:13:06 +0200 Subject: [PATCH] Add Binance Futures support (long and short) - config.rs: add Instrument::market_kind() mapping exchange name to "spot"/"futures_um"/"futures_cm", and is_futures() helper - swym.rs: submit_backtest() accepts market_kind param; passes it as instrument.kind in the RunConfig instead of hardcoding "spot" - agent.rs: derive has_futures from instruments; pass to both system_prompt() and initial_prompt() - prompts.rs: - system_prompt() accepts has_futures; injects FUTURES_SHORT_EXAMPLES (Example 5: EMA trend-following short with ATR stop) when true - Rewrite position-state anti-patterns to cover both spot (long-only) and futures (long + short) semantics - initial_prompt() accepts has_futures; labels market as "spot" or "futures" and passes flag through to starting instruction context Co-Authored-By: Claude Sonnet 4.6 --- src/agent.rs | 6 ++-- src/config.rs | 18 ++++++++++++ src/prompts.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++---- src/swym.rs | 3 +- 4 files changed, 98 insertions(+), 9 deletions(-) diff --git a/src/agent.rs b/src/agent.rs index 4a62587..3360268 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -202,7 +202,8 @@ pub async fn run(cli: &Cli) -> Result<()> { // Load DSL schema for the system prompt let schema = include_str!("dsl-schema.json"); - let system = prompts::system_prompt(schema, claude.family()); + let has_futures = instruments.iter().any(|i| i.is_futures()); + let system = prompts::system_prompt(schema, claude.family(), has_futures); info!("model family: {}", claude.family().name()); // Resolve ledger path: explicit --ledger-file takes precedence, else /run_ledger.jsonl @@ -225,7 +226,7 @@ pub async fn run(cli: &Cli) -> Result<()> { // Build the user prompt let user_msg = if iteration == 1 { - prompts::initial_prompt(&instrument_names, &available_intervals, prior_summary.as_deref()) + prompts::initial_prompt(&instrument_names, &available_intervals, prior_summary.as_deref(), has_futures) } else { let results_text = history .iter() @@ -579,6 +580,7 @@ async fn run_single_backtest( &inst.symbol, &inst.base(), &inst.quote(), + inst.market_kind(), strategy, starts_at, finishes_at, diff --git a/src/config.rs b/src/config.rs index 5e4308c..ef09b48 100644 --- a/src/config.rs +++ b/src/config.rs @@ -174,4 +174,22 @@ impl Instrument { } "usdc".to_string() } + + /// Instrument kind for the paper-run config `instrument.kind` field. + /// Derived from the exchange identifier (case-insensitive). + pub fn market_kind(&self) -> &'static str { + let e = self.exchange.to_ascii_lowercase(); + if e.contains("futures_usd") || e.contains("futures_um") { + "futures_um" + } else if e.contains("futures_coin") || e.contains("futures_cm") { + "futures_cm" + } else { + "spot" + } + } + + /// True when this instrument is traded on a futures market. + pub fn is_futures(&self) -> bool { + self.market_kind() != "spot" + } } diff --git a/src/prompts.rs b/src/prompts.rs index 6625a5b..fad84d2 100644 --- a/src/prompts.rs +++ b/src/prompts.rs @@ -4,7 +4,7 @@ use crate::config::ModelFamily; /// /// 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 { +pub fn system_prompt(dsl_schema: &str, family: &ModelFamily, has_futures: bool) -> String { let output_instructions = match family { ModelFamily::DeepSeekR1 => { "## Output format\n\n\ @@ -484,17 +484,84 @@ CRITICAL: `apply_func` uses `"input"`, not `"expr"`. Writing `"expr":` will be r intervals, the issue is signal logic, not timeframe — fix the signal before changing interval. - Don't create strategies with more than 5-6 conditions — overfitting risk - Don't ignore fees — a strategy needs to overcome 0.1% per round trip -- Always gate buy rules with position state "flat" and sell rules with "long" -- Never add a short-entry (sell when flat) rule — spot markets are long-only +- 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. - `{{"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 { "" }, ) } +/// Short-entry and short-exit strategy examples, injected into the system prompt when +/// futures instruments are present. +const FUTURES_SHORT_EXAMPLES: &str = r##" + +### Example 5 — Futures short: EMA trend-following short with ATR stop + +On futures you can also short. Short entry = `"side": "sell"` when `"state": "flat"`; +short exit (cover) = `"side": "buy"` when `"state": "short"`. Stop-loss for a short +is price rising above entry, e.g. entry_price * 1.02. You may run long and short legs +in the same strategy (4 rules total), or a short-only strategy (2 rules). + +```json +{ + "type": "rule_based", + "candle_interval": "4h", + "rules": [ + { + "comment": "Short entry: EMA9 crosses below EMA21 while price is below EMA50 (downtrend)", + "when": { + "kind": "all_of", + "conditions": [ + {"kind": "position", "state": "flat"}, + {"kind": "ema_crossover", "fast_period": 9, "slow_period": 21, "direction": "below"}, + {"kind": "ema_trend", "period": 50, "direction": "below"} + ] + }, + "then": {"side": "sell", "quantity": {"method": "percent_of_balance", "percent": "10", "asset": "usdc"}} + }, + { + "comment": "Short exit: EMA9 crosses back above EMA21, OR 2% stop-loss, OR 48-bar time exit", + "when": { + "kind": "all_of", + "conditions": [ + {"kind": "position", "state": "short"}, + { + "kind": "any_of", + "conditions": [ + {"kind": "ema_crossover", "fast_period": 9, "slow_period": 21, "direction": "above"}, + { + "kind": "compare", + "left": {"kind": "field", "field": "close"}, + "op": ">", + "right": {"kind": "bin_op", "op": "mul", "left": {"kind": "entry_price"}, "right": {"kind": "literal", "value": "1.02"}} + }, + { + "kind": "compare", + "left": {"kind": "bars_since_entry"}, + "op": ">=", + "right": {"kind": "literal", "value": "48"} + } + ] + } + ] + }, + "then": {"side": "buy", "quantity": {"kind": "position_quantity"}} + } + ] +} +``` + +Key short-specific notes: +- Stop-loss for short = close > entry_price * (1 + stop_pct), e.g. `* 1.02` for 2% stop +- Take-profit for short = close < entry_price * (1 - target_pct), e.g. `* 0.97` for 3% target +- Short exit uses `"side": "buy"` with `{"kind": "position_quantity"}` (same as long exit uses sell) +- `percent_of_balance` for short entry uses `"usdc"` as the asset (the collateral currency)"##; + /// Build the user message for the first iteration (no prior results). /// `prior_summary` contains a formatted summary of results from previous runs, if any. -pub fn initial_prompt(instruments: &[String], candle_intervals: &[String], prior_summary: Option<&str>) -> String { +pub fn initial_prompt(instruments: &[String], candle_intervals: &[String], prior_summary: Option<&str>, has_futures: bool) -> String { let prior_section = match prior_summary { Some(s) => format!("{s}\n\n"), None => String::new(), @@ -515,8 +582,9 @@ that novelty attempt may you refine prior work.\n\ "Start with a multi-timeframe trend-following approach with proper risk management \ (stop-loss, time exit, and ATR-based position sizing)." }; + let market_type = if has_futures { "futures" } else { "spot" }; format!( - r#"{prior_section}Design a trading strategy for crypto spot markets. + r#"{prior_section}Design a trading strategy for crypto {market_type} markets. Available instruments: {} Available candle intervals: {} diff --git a/src/swym.rs b/src/swym.rs index 2df99cf..c88a3a3 100644 --- a/src/swym.rs +++ b/src/swym.rs @@ -368,6 +368,7 @@ impl SwymClient { instrument_symbol: &str, base_asset: &str, quote_asset: &str, + market_kind: &str, strategy: &Value, starts_at: &str, finishes_at: &str, @@ -385,7 +386,7 @@ impl SwymClient { "name_exchange": instrument_symbol, "underlying": { "base": base_asset, "quote": quote_asset }, "quote": "underlying_quote", - "kind": "spot" + "kind": market_kind }, "execution": { "mocked_exchange": instrument_exchange,