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
|
// Load DSL schema for the system prompt
|
||||||
let schema = include_str!("dsl-schema.json");
|
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());
|
info!("model family: {}", claude.family().name());
|
||||||
|
|
||||||
// Resolve ledger path: explicit --ledger-file takes precedence, else <output_dir>/run_ledger.jsonl
|
// 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
|
// Build the user prompt
|
||||||
let user_msg = if iteration == 1 {
|
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 {
|
} else {
|
||||||
let results_text = history
|
let results_text = history
|
||||||
.iter()
|
.iter()
|
||||||
@@ -579,6 +580,7 @@ async fn run_single_backtest(
|
|||||||
&inst.symbol,
|
&inst.symbol,
|
||||||
&inst.base(),
|
&inst.base(),
|
||||||
&inst.quote(),
|
&inst.quote(),
|
||||||
|
inst.market_kind(),
|
||||||
strategy,
|
strategy,
|
||||||
starts_at,
|
starts_at,
|
||||||
finishes_at,
|
finishes_at,
|
||||||
|
|||||||
@@ -174,4 +174,22 @@ impl Instrument {
|
|||||||
}
|
}
|
||||||
"usdc".to_string()
|
"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
|
/// Accepts a `ModelFamily` so each family can receive tailored guidance
|
||||||
/// while sharing the common DSL schema and strategy evaluation rules.
|
/// 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 {
|
let output_instructions = match family {
|
||||||
ModelFamily::DeepSeekR1 => {
|
ModelFamily::DeepSeekR1 => {
|
||||||
"## Output format\n\n\
|
"## 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.
|
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 create strategies with more than 5-6 conditions — overfitting risk
|
||||||
- Don't ignore fees — a strategy needs to overcome 0.1% per round trip
|
- 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"
|
- 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.
|
||||||
- Never add a short-entry (sell when flat) rule — spot markets are long-only
|
- 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.
|
- 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).
|
- `{{"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).
|
/// Build the user message for the first iteration (no prior results).
|
||||||
/// `prior_summary` contains a formatted summary of results from previous runs, if any.
|
/// `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 {
|
let prior_section = match prior_summary {
|
||||||
Some(s) => format!("{s}\n\n"),
|
Some(s) => format!("{s}\n\n"),
|
||||||
None => String::new(),
|
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 \
|
"Start with a multi-timeframe trend-following approach with proper risk management \
|
||||||
(stop-loss, time exit, and ATR-based position sizing)."
|
(stop-loss, time exit, and ATR-based position sizing)."
|
||||||
};
|
};
|
||||||
|
let market_type = if has_futures { "futures" } else { "spot" };
|
||||||
format!(
|
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 instruments: {}
|
||||||
Available candle intervals: {}
|
Available candle intervals: {}
|
||||||
|
|||||||
@@ -368,6 +368,7 @@ impl SwymClient {
|
|||||||
instrument_symbol: &str,
|
instrument_symbol: &str,
|
||||||
base_asset: &str,
|
base_asset: &str,
|
||||||
quote_asset: &str,
|
quote_asset: &str,
|
||||||
|
market_kind: &str,
|
||||||
strategy: &Value,
|
strategy: &Value,
|
||||||
starts_at: &str,
|
starts_at: &str,
|
||||||
finishes_at: &str,
|
finishes_at: &str,
|
||||||
@@ -385,7 +386,7 @@ impl SwymClient {
|
|||||||
"name_exchange": instrument_symbol,
|
"name_exchange": instrument_symbol,
|
||||||
"underlying": { "base": base_asset, "quote": quote_asset },
|
"underlying": { "base": base_asset, "quote": quote_asset },
|
||||||
"quote": "underlying_quote",
|
"quote": "underlying_quote",
|
||||||
"kind": "spot"
|
"kind": market_kind
|
||||||
},
|
},
|
||||||
"execution": {
|
"execution": {
|
||||||
"mocked_exchange": instrument_exchange,
|
"mocked_exchange": instrument_exchange,
|
||||||
|
|||||||
Reference in New Issue
Block a user