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 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 18:13:06 +02:00
parent a435d3a99d
commit 8de3ae5fe1
4 changed files with 98 additions and 9 deletions

View File

@@ -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 <output_dir>/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,

View File

@@ -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"
}
}

View File

@@ -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: {}

View File

@@ -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,