Backtesting

Backtest/run_test drive the bar-by-bar loop for a single strategy; batch_backtest runs a threaded parameter sweep over per-job-copied markets; and the Tables.jl collectors (trades_table, weights_table, turnover_table) derive row tables from a completed run on demand, with no per-bar cost.

Running a backtest

Orcus.BacktestType
Backtest(market::Market, strategy::Strategy, cash::Real=1000)

Backtest a strategy on a market.

Random.seed!(1234);
x=asset();
y=asset();
M=market([x,y]);
T = Backtest(M,CrossOverStrategy,1000)
run_test(T)

# output

Backtest of CrossOverStrategy with -18.28 funds on market comprised of 2 assets.
source
Orcus.run_testFunction
run_test(BT::Backtest)

Run the backtest to completion, one bar at a time, then mark it completed.

Random.seed!(1234);
x=asset();
y=asset();
M=market([x,y]);
T=Backtest(M,CrossOverStrategy,1000);
run_test(T);
T.completed
# output

true
source

Batch sweeps

Orcus.batch_backtestFunction
batch_backtest(market::Market, strategy::Type, cash::Real, grid::AbstractVector; threaded::Bool=true, cost_model::CostModel=NoCost(), progress::Bool=false)

Run a parameter sweep of strategy over market, one backtest per entry of grid, returning a Vector{Backtest} in input order. grid is a vector of NamedTuples forwarded as keyword arguments to the strategy constructor — declare the swept parameters as typed fields with @generate_strategy (or a custom @strategy_methods struct):

Random.seed!(1);
@generate_strategy SMAcross crossover_next crossover_init fast::Int=10 slow::Int=20;
res = batch_backtest(market([copy(AAPL)]), SMAcross, 10_000, [(fast=5, slow=20), (fast=10, slow=30)]);
length(res)
# output

2

Keyword arguments:

  • threaded – run jobs across threads (default true; falls back to sequential when only one thread is available). Start Julia with -t auto/-t N to use more than one thread.
  • cost_modelCostModel applied to every job's broker (default NoCost()).
  • progress – when true, log elapsed time and ETA as jobs complete (default false, quiet).
source
batch_backtest(builder, n::Integer; threaded=true, progress=false)

Lower-level form: run n jobs where builder(i) returns an already-built Backtest, returning a Vector{Backtest} in input order. Use this for sweeps that vary the universe itself rather than just strategy parameters (the caller's builder is responsible for market isolation):

Random.seed!(1);
res = batch_backtest(2) do i
    run_test(Backtest(market([copy(AAPL)]), CrossOverStrategy, 10_000))
end;
length(res)
# output

2
source

Collectors

Orcus.cashflows_tableFunction
cashflows_table(B::Broker)

The cash balance over time (cash_history) as a Tables.jl row table.

Random.seed!(1);
B=broker(3,1000);
advance_to!(B.market, 1);
A=B.market.assets[2];
place_order!(B, Order(Buy(A,10)));
process_all!(B);
length(cashflows_table(B))
# output

3
source
Orcus.equity_tableFunction
equity_table(B::Broker)

The equity curve (B.equity_history) as a Tables.jl row table.

Random.seed!(1);
B=broker(3,1000);
advance_to!(B.market, 1);
process_all!(B);
length(equity_table(B))
# output

1
source
Orcus.positions_tableFunction
positions_table(B::Broker)

Every open position in B.portfolio as a Tables.jl row table.

Random.seed!(1);
B=broker(3,1000);
advance_to!(B.market, 1);
A=B.market.assets[2];
place_order!(B, Order(Buy(A,10)));
process_all!(B);
length(positions_table(B))
# output

1
source
Orcus.trades_tableFunction
trades_table(B::Broker)

Every fill in B.history as a Tables.jl row table.

Random.seed!(1);
B=broker(3,1000);
advance_to!(B.market, 1);
A=B.market.assets[2];
place_order!(B, Order(Buy(A,10)));
process_all!(B);
length(trades_table(B))
# output

1
source
Orcus.turnover_tableFunction
turnover_table(B::Broker)

Per-bar traded notional and turnover (traded_notional / equity) as a Tables.jl row table. Uses delta_cash rather than volume/price(T): close-out trades from resolve_portfolio! are recorded with volume == 0.0 (the cash impact is carried by delta_cash instead), so volume-based notional would silently drop every forced close.

Random.seed!(1);
B=broker(3,1000);
advance_to!(B.market, 1);
A=B.market.assets[2];
place_order!(B, Order(Buy(A,10)));
process_all!(B);
length(turnover_table(B))
# output

1
source
Orcus.weights_tableFunction
weights_table(B::Broker)

Per-bar, per-instrument notional value and weight-of-equity as a Tables.jl row table.

Random.seed!(1);
B=broker(3,1000);
advance_to!(B.market, 1);
A=B.market.assets[2];
place_order!(B, Order(Buy(A,10)));
process_all!(B);
length(weights_table(B))
# output

1
source