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:
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user