Skip to content

Strategy reuse and backtests

Research cells remain ordinary editable Rust. These helpers make existing strategy code and recorded results easier to reuse; they do not replace custom analysis or introduce a separate trading engine.

Under Strategy code & backtests:

  • Load research tools loads your strategy’s public library/components as project::, its normal dependencies, and the backtest/research helpers together. It does not connect a data feed or run a strategy.
  • Add discovery cell lists completed project backtests and graph parameter IDs.
  • Choose a baseline and candidate, then Add comparison cell.
  • Refresh saved runs updates the selectors after an experiment without resetting variables.
  • Add signal chart & heatmap creates an editable example for the selected run.
  • Add isolated backtest cell uses the historical snapshots already loaded in the notebook.

The Add… cell buttons insert code without executing it. Load research tools compiles and initializes the runtime, but never runs your saved notebook cells. If the same project revision is already imported into a running session, it reuses that session without recompiling, resetting variables or marking outputs stale. New or changed imports reset session variables and mark prior outputs stale.

Already-loaded bars and their original date/time configuration are restored when tools are loaded. Editing the session’s date inputs does not relabel an existing snapshot; load data again to use a different window.

The first import resolves the project’s dependencies, compiles AQE, Polars and the exported project modules, then links the notebook helpers and restores loaded snapshots. The session panel shows the preparation/compiler stage and elapsed time; Stop/reset cancels the operation. Compiled dependencies are cached per notebook across resets and app restarts. A notebook cold build is separate from the desktop or strategy executable build and can still take several minutes.

Notebook cells and imported dependencies use a fast-build development profile: optimization level 0, 256 code-generation units, incremental compilation, no debug symbols and no link-time optimization. Debug assertions, overflow checks and panic unwinding remain enabled. This prioritizes compile latency over execution speed; notebook timings are not release-mode strategy benchmarks. The overrides apply only to notebook compilation, not to live strategy builds, normal backtests or isolated experiment subprocesses.

After updating AQS, restart the notebook session to pick up compiler-setting changes. Cargo may rebuild dependencies once when the profile changes, then reuse that cache on subsequent loads. There is no need to delete the compilation cache.

Keep the same notebook and its compilation cache for repeated work. Use Refresh saved runs to update backtest selectors without compilation. After editing this project’s sources or manifest, click Load research tools again. After editing AQE or another external path dependency, use Stop/reset followed by Load research tools to force a fresh import check. Resetting clears variables, not the dependency cache; rerun custom setup cells afterward.

For example, a Three Amigo project exporting an openingrange module can use its actual alpha implementation:

use project::openingrange::Openingrange;
let alpha: Openingrange = Openingrange::new(
45, "07:00".to_string(), 14, 2.0, 1.5, false,
);
let study: AqsBacktest = aqs_run_alpha(alpha, &research_snapshot).await?;
let btc = study.bars("BTCUSD", "5m")?;
let signals = study.signals("BTCUSD")?;
aqs_chart(&btc).candles().signals(&signals)?.show()?;

Use your component’s actual module name and constructor. aqs_run_alpha performs an AQE historical replay with the alpha lifecycle and time-filtered history. It is a signal-only study, without an entry-submission pipe; it is not a strategy-return backtest. Alphas requiring custom lifecycle setup, additional timeframes, shared variables or execution state should be tested in a complete strategy instead.

Use explicit variable types at imported-code boundaries, as above. The Rust notebook’s incremental type analysis can require an annotation even when an ordinary Cargo build would infer the type. If a cell reports that a variable’s type cannot be determined, annotate it (for example let btc: DataFrame = ...;), then reload the session/setup if the diagnostic requests a reset.

An imported indicator can operate directly on data.bars(symbol)?. Imported insight pipes require a strategy context and the appropriate insight states. Register them in your custom AQE Strategy, then run that strategy:

// my_strategy implements AQE's existing Strategy interface.
let run = aqs_run_strategy(
my_strategy, &research_snapshot, 100_000.0, 10,
).await?;
aqs_table(&run.trades()?, 20)?;

This custom replay uses PaperBroker defaults for fees and the explicit cash/leverage arguments. It does not inherit graph lifecycle registrations or the graph’s fee settings. Use a saved-graph experiment below when those should be retained. You can also define functions, indicators, models and strategies directly in Rust cells.

aqs_table(&backtests.list()?, 100)?;
let run = backtests.load("backtests/your-run-id")?;
run.metrics()

The list includes completed runs in the linked project’s backtests/ directory and completed research/experiments/ runs. Use the returned ID; raw absolute paths and parent-directory traversal are rejected. Metrics and backtest.db must both exist.

let equity = run.equity()?;
let trades = run.trades()?; // Closed round trips
let insights = run.insights()?; // Full saved insight records
let btc = run.bars("BTCUSD", "5m")?;
let fills = run.table("trade_log")?;
let monthly = run.table("monthly_returns")?;
aqs_table(&insights, 20)?;
aqs_plot(&equity, "equity")?;

bars selects the exact saved symbol/timeframe and non-feature stream. It adds an Int64 UTC-millisecond timestamp column. equity adds that timestamp plus return_pct (equity change from the first observed equity) and drawdown_pct (relative to the running observed peak). These are not cash-flow-adjusted returns.

table(name) preserves stored column names and data. Available tables include bars, account history, insights, trade log, round trips, monthly returns, time performance, drawdown series, strategy/regime performance, factor exposure, correlations, position concentration, slippage, trade MAE, setup performance, rolling Sharpe, parameter sweeps and walk-forward results. Older runs may lack some tables and report an error rather than inventing data.

Full reads are bounded to 250,000 rows and 64 MiB per request. Exceeding the bound produces an error, not a silently truncated result. Paginate explicitly for larger runs:

let (page, has_more) = run.table_page("insights", 10_000, 0)?;
// Next page: run.table_page("insights", 10_000, 10_000)?
let baseline = backtests.load("backtests/baseline-id")?;
let candidate = backtests.load("backtests/candidate-id")?;
aqs_compare_backtests(&[&baseline, &candidate])?;
aqs_returns_heatmap(&[&baseline, &candidate])?;
let differences = aqs_compare_insights(&baseline, &candidate)?;
aqs_table(&differences, 30)?;
AQS Research notebook showing Rust code for backtest and insight comparisons, an account summary, and separate equity-change and drawdown charts for two saved runs.AQS Research notebook showing Rust code for backtest and insight comparisons, an account summary, and separate equity-change and drawdown charts for two saved runs.

Compare saved runs with an account summary, equity-change curves and drawdown curves. View the full-size comparison screenshotView the full-size comparison screenshot.

aqs_compare_backtests accepts 2–6 runs and emits a summary plus equity-change and drawdown overlays. Statistics use every loaded account observation; curves are display sampled. Each run keeps its observed UTC times. There is no forward filling or rebasing to an invented common observation.

Check matching test windows, initial capital, cash flows, source revision, dataset, warm-up, asset specifications and fees before attributing differences to a model change. The helper displays different periods but does not claim they are controlled experiments. Recorded metrics remain available through metrics().

aqs_compare_insights matches generated signals by symbol, exact creation time, alpha, side, timeframe and parent/child status—not random insight UUIDs. Duplicate signals are counted. It returns baseline/candidate counts, matched counts and signals unique to each run. It does not assert identical fills or P&L; inspect the full insight and trade tables for those.

The older aqs_compare(&data, symbols, "close") compares market prices, not backtest performance.

Mark generated signals or executed entries

Section titled “Mark generated signals or executed entries”
let candles = baseline.bars("BTCUSD", "5m")?;
let generated = baseline.signals("BTCUSD")?;
aqs_chart(&candles).candles().signals(&generated)?.show()?;
let executed = baseline.fills("BTCUSD")?;
aqs_chart(&candles).candles().signals(&executed)?.show()?;

Generated markers use created_at; executed-entry markers use filled_at. A generated signal is not proof of an order fill. Custom marker frames need timestamp (UTC milliseconds), side (Buy/Sell, case insensitive) and optionally label. Filter to one chart symbol yourself and at most 500 markers.

The interactive chart places a marker on the preceding displayed bar and keeps the exact event time in its text. Out-of-range markers are not displayed. Downsampling can change visual placement; it never changes the stored timestamp. AQI paper snapshots use the same interactive renderer for saved time-series charts and retain a static SVG fallback with event-time captions.

Save the graph and load historical data, then inspect actual node IDs/input names:

project_parameters
let baseline = aqs_backtest()
.name("Baseline")
.snapshot(&research_snapshot)
.run()?;
let candidate = aqs_backtest()
.name("Candidate")
.snapshot(&research_snapshot)
.parameter("actual-node-id", "actual-input-name", 45)
.run()?;
aqs_compare_backtests(&[&baseline, &candidate])?;

The runner snapshots project sources and the saved graph, verifies the historical Parquet fingerprints, creates CSV input, and uses normal AQE code generation with PaperBroker. It retains graph sizing, risk, pipe ordering, lifecycle and fee configuration. Overrides apply only to this experiment; the original .aqmeta, source and manifest are not rewritten. Unsaved editor graph changes are not included. Resolve hyperparameter defaults first; implicit combinatorial sweeps are not launched.

Each experiment lives in research/experiments/<id>/, with its source/config snapshot, provenance.json, execution.log and ordinary AQE backtest artifacts. Builds reuse research/experiment-target/. Stop/reset terminates the notebook process tree, including an active experiment. Failed runs keep diagnostics and are not listed as completed.

External path dependencies still refer to their existing source directories; they are not copied into the experiment. Keep those dependencies stable while comparing runs. The project lockfile is used as a starting point, but Cargo may resolve changes required by the research build; inspect the experiment’s final Cargo.lock when reproducing results.

Data-model limits: CSV replay uses synthetic assets, not MT5 contract specifications. The loaded timeframe must exactly match the graph execution timeframe; no missing stream is substituted or forward-filled. A single loaded timeframe does not provide extra feature timeframes or pre-window warm-up history. Inspect coverage and logs for models needing those streams. These results are not automatically equivalent to broker-feed backtests or live execution. Projects with custom build scripts are not supported by the isolated graph runner yet.

Selected code, comparison plots, marker charts, heatmaps and table previews can be included in an AQI notebook block. Readers do not execute anything. Sharing does not automatically upload project source trees, raw databases, datasets or credentials. Review any code, printed paths and data previews before saving or publishing a paper.