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.Backtest — Type
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.
Orcus.run_test — Function
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
trueBatch sweeps
Orcus.batch_backtest — Function
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
2Keyword arguments:
threaded– run jobs across threads (defaulttrue; falls back to sequential when only one thread is available). Start Julia with-t auto/-t Nto use more than one thread.cost_model–CostModelapplied to every job's broker (defaultNoCost()).progress– whentrue, log elapsed time and ETA as jobs complete (defaultfalse, quiet).
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
2Collectors
Orcus.cashflows_table — Function
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
3Orcus.equity_table — Function
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
1Orcus.positions_table — Function
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
1Orcus.trades_table — Function
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
1Orcus.turnover_table — Function
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
1Orcus.weights_table — Function
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