SeekAlgo/Docs/Multi-Symbol Backtesting
Docs/Multi-Symbol Backtesting

Multi-Symbol Backtesting

Technical execution notes for the basket backtest engine and portfolio-level multi-symbol behavior.

Multi-Symbol Backtesting

This page is the technical companion to Multi-Symbol Strategies.

Read that guide first if your goal is to write or configure a basket strategy. Read this page when you want the execution model behind it.

Multi-symbol backtesting is portfolio-first.

  • one backtest shares one cash pool across the whole basket
  • each leg is identified by symbol and trade id
  • the basket universe is explicit and fixed in the script
  • the modern syntax uses symbols([...]) and symbol.*

Canonical Script Shape

script({
  type: 'strategy',
  priceStudy: true,
  initialCapital: 100000,
  orderSize: 50,
  orderType: 'poe',
  commission: 0.05,
  commissionType: 'per',
  slippage: 0,
  marketType: 'crypto',
  resolution: '15',
  tradeBelowCash: false,
});

symbols([
  "BTCUSDT:BINANCE:FUTURE:DEFAULT",
  "ETHUSDT:BINANCE:FUTURE:DEFAULT",
  "SOLUSDT:BINANCE:FUTURE:DEFAULT",
  "BNBUSDT:BINANCE:FUTURE:DEFAULT"
]);

const fastLen = input.integer("Fast EMA", 8);
const slowLen = input.integer("Slow EMA", 21);
const qty = input.integer("Qty", 1);

If a basket entry omits qty, the engine applies orderSize per leg using orderType. If a basket entry provides qty, that explicit value overrides the header default for that leg.

How Basket Data Is Read

The symbol namespace is the basket data surface.

const btcClose = symbol.close[0].get(0);
const btcPrev = symbol.close[0].get(1);
const ethClose = symbol.close[1].get(0);

The index order matches the order in symbols([...]).

Trading Pattern

The current basket samples use a fixed symbol list plus stable trade ids.

const SPECS = [
  "BTCUSDT:BINANCE:FUTURE:DEFAULT",
  "ETHUSDT:BINANCE:FUTURE:DEFAULT",
  "SOLUSDT:BINANCE:FUTURE:DEFAULT",
  "BNBUSDT:BINANCE:FUTURE:DEFAULT"
];

const IDS = ["btc_ema", "eth_ema", "sol_ema", "bnb_ema"];

Each leg can use the same rule set:

  • read the symbol series at symbol.close[index]
  • detect the signal
  • check strategy.param_by_id(id, "position")
  • send strategy.entry({ id, type, symbol }) for the default size
  • send strategy.entry({ id, type, qty, symbol }) for an explicit override
  • send strategy.exit({ id, symbol })

You can also read strategy.default_qty_value and strategy.default_qty_type inside basket scripts when you want the rule to depend on the configured defaults.

Single Position Across The Basket

The current first-signal sample keeps only one open trade at a time.

  • if a leg is already open, the script looks for its exit condition
  • if no leg is open, the first qualifying signal gets the entry
  • this concentrates capital on the first valid setup

What To Avoid

  • do not mix the old $script, $symbols, $input, and $symbol forms into new scripts
  • do not use an implicit basket universe
  • do not change the symbol order after you start mapping trade ids to indexes

Current Compatibility Note

Older basket scripts may still run through compatibility layers.

New scripts should use the modern syntax only:

  • script(...)
  • symbols([...])
  • input.*(...)
  • symbol.*[index]

What The Multi-Symbol UI Shows

  • a portfolio timeline
  • symbol-level trade drilldown
  • aggregate performance
  • symbol attribution and exposure

Good Basket Strategy Candidates

  • crossovers across a fixed universe
  • trend strength ranking
  • breakout selection
  • first-signal wins logic
  • one-position portfolio rotation