Skip to content

Commit c3be793

Browse files
committed
0.9.0: support for options backtesting
1 parent 0b02792 commit c3be793

24 files changed

Lines changed: 13164 additions & 98 deletions

CHANGELOG.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@
22

33
All notable changes to this project will be documented in this file.
44

5-
## [0.8.0] - 2026-05-03
5+
## [0.9.0] - 2026-05-04
66

7-
### Breaking changes ⚠️
7+
### Added
88

9-
- `PerformanceSummary` now includes additional equity-curve diagnostics, changing positional construction and the exact `performance_summary_table` schema.
9+
- Basic listed option support via `ContractKind.Option`, `OptionRight`, `OptionExerciseStyle`, and `option_instrument`.
10+
- Quote-driven option premium accounting, underlying mark updates through `OptionUnderlyingUpdate`, and cash-settled option expiry via `settle_option_expiry!`.
11+
- Conservative short-option margin with instrument-level `option_short_margin_rate` and `option_short_margin_min_rate` parameters, plus bounded multi-leg option margin relief for spreads, butterflies, and condors.
12+
- `fill_option_strategy!` for atomic multi-leg option fills checked against final package buying power.
13+
- IBKR Pro Fixed option commissions with premium tiers and per-order minimums.
14+
15+
## [0.8.0] - 2026-05-03
1016

1117
### Added
1218

19+
- `PerformanceSummary` now includes additional equity-curve diagnostics, changing positional construction and the exact `performance_summary_table` schema.
1320
- `performance_summary` now reports `n_periods`, `best_ret`, `worst_ret`, `positive_period_rate`, `expected_shortfall_95`, `skewness`, `kurtosis`, `downside_vol`, `max_dd_duration`, `pct_time_in_drawdown`, and `omega`.
1421
- `performance_summary_table` exposes the new `PerformanceSummary` fields as Tables.jl columns.
1522

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name = "Fastback"
22
uuid = "2b92286b-cbc8-46f1-be48-a629b4baefca"
33
authors = ["Rino Beeli <40661605+rbeeli@users.noreply.github.com>"]
44
repo = "https://github.com/rbeeli/fastback.jl.git"
5-
version = "0.8.0"
5+
version = "0.9.0"
66

77
[deps]
88
Crayons = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f"

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@
66

77
Fastback provides a lightweight, flexible and highly efficient event-based backtesting library for quantitative trading strategies.
88

9-
Fastback focuses on deterministic accounting: it tracks open positions, balances, equity, margin, and cashflows across multiple currencies.
9+
Fastback focuses on deterministic accounting: it tracks open positions, balances, equity, margin, option premium flows, and cashflows across multiple currencies.
1010
The execution pipeline supports broker-driven commissions/financing and partial fills; slippage and delays are modeled by the timestamps and fill prices you pass in.
11+
Listed options are supported as quote-driven, cash-settled contracts, including underlying mark updates, expiry settlement at intrinsic value, short-option margin, and atomic multi-leg fills for classic strategies such as vertical spreads, butterflies, and condors.
1112

1213
Fastback does not try to model every aspect of a trading system, e.g. data ingestion, strategy logic, OMS/execution gateways, or logging.
1314
Instead, it provides basic building blocks for creating a custom backtesting environment that is easy to understand and extend.
1415
Broker behavior is intentionally lightweight and pluggable via broker profiles (for commissions and financing schedules).
1516
For example, Fastback has no notion of "strategy" or "indicator"; such constructs are highly strategy specific, and therefore up to the user to define.
1617

1718
The event-based architecture aims to mimic how real-world trading systems ingest streaming data.
18-
You drive the engine with explicit mark, FX, and funding updates, plus optional expiry and liquidation steps, which reduces the implementation gap to live execution compared to vectorized backtesting frameworks.
19+
You drive the engine with explicit mark, option-underlying, FX, and funding updates, plus optional expiry and liquidation steps, which reduces the implementation gap to live execution compared to vectorized backtesting frameworks.
1920

2021
## Hello world backtest
2122

@@ -62,14 +63,15 @@ Fastback.plot_equity(equity_data)
6263

6364
## Features
6465

65-
- Event-driven accounting engine with explicit event processing (`process_step!`) for marks, FX, funding, expiries, and optional liquidation
66-
- Instruments: spot (including spot-on-margin), perpetuals, and futures with lifecycle guards (start/expiry), optional contract multipliers, and settlement styles (`PrincipalExchange`/`VariationMargin`)
66+
- Event-driven accounting engine with explicit event processing (`process_step!`) for marks, option-underlying marks, FX, funding, expiries, and optional liquidation
67+
- Instruments: spot (including spot-on-margin), perpetuals, futures, and listed options with lifecycle guards (start/expiry), optional contract multipliers, and settlement styles (`PrincipalExchange`/`VariationMargin`)
68+
- Options backtesting: premium cash accounting, cash-settled expiry, conservative naked-short margin, bounded multi-leg margin relief, and atomic package fills via `fill_option_strategy!`
6769
- Funding policies: fully funded or margined; per-currency or base-currency margin aggregation; percent-notional or fixed-per-contract margin requirements
6870
- Broker profiles for commissions/financing (e.g. flat-fee, IBKR-style, Binance-style)
6971
- Multi-currency cash book with FX conversion helpers and base-currency metrics
7072
- Execution & risk: broker-driven commissions, partial fills, liquidation-aware marking (bid/ask/last), and initial/maintenance margin checks
7173
- Netted positions with weighted-average cost, realized/unrealized P&L, and a cashflow ledger + accrual helpers (lend/borrow interest, broker-defined short-proceeds treatment, borrow fees on principal-exchange spot shorts, funding, variation margin)
72-
- Expiry handling for futures (auto-close via synthetic close) plus deterministic liquidation helpers
74+
- Expiry handling for futures and cash-settled options plus deterministic liquidation helpers
7375
- Collectors (periodic, predicate, drawdown, min/max) and Tables.jl views for balances, equity, positions, trades, cashflows; pretty-print helpers
7476
- Batch backtesting and parameter sweeps with threaded runner and ETA logging
7577
- Integrations

docs/makedocs.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ gen_markdown("2_portfolio_trading.jl");
101101
gen_markdown("3_multi_currency.jl");
102102
gen_markdown("4_USDm_perp_trading.jl");
103103
gen_markdown("5_VOO_vs_MES_comparison/main.jl"; name="5_VOO_vs_MES_comparison");
104+
gen_markdown("6_options_short_put_spread.jl");
105+
gen_markdown("7_options_strategy_fill.jl");
104106
gen_markdown(
105107
"1_Tables_integration.jl";
106108
source_root=INTEGRATIONS_ROOT,
@@ -144,6 +146,8 @@ Documenter.makedocs(
144146
"Multi-Currency trading" => "examples/gen/3_multi_currency.md",
145147
"USD-M perpetual trading" => "examples/gen/4_USDm_perp_trading.md",
146148
"VOO vs MES cost comparison" => "examples/gen/5_VOO_vs_MES_comparison.md",
149+
"Options short put spread" => "examples/gen/6_options_short_put_spread.md",
150+
"Atomic option strategy fills" => "examples/gen/7_options_strategy_fill.md",
147151
],
148152
"Plotting" => [
149153
"Plots extensions" => "plotting/gen/1_plots_extension.md",

docs/src/api_index.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ For details, open the REPL and type `?symbol` to view docstrings.
77
## Core types and enums
88

99
- `Price`, `Quantity`
10-
- `TradeDir`, `SettlementStyle`, `MarginRequirement`, `MarginAggregation`, `ContractKind`, `AccountFunding`, `CashflowKind`, `OrderRejectReason`, `OrderRejectError`, `TradeReason`
10+
- `TradeDir`, `SettlementStyle`, `MarginRequirement`, `MarginAggregation`, `ContractKind`, `OptionRight`, `OptionExerciseStyle`, `AccountFunding`, `CashflowKind`, `OrderRejectReason`, `OrderRejectError`, `TradeReason`
1111
- `Cash`, `CashSpec`, `InstrumentSpec`, `Instrument`, `Order`, `Trade`, `Cashflow`, `Position`, `Account`
1212
- `ExchangeRates`
1313

@@ -21,7 +21,7 @@ For details, open the REPL and type `?symbol` to view docstrings.
2121

2222
## Order and trade utilities
2323

24-
- `symbol`, `notional_value`, `fill_order!`
24+
- `symbol`, `notional_value`, `fill_order!`, `fill_option_strategy!`
2525
- `realized_notional_quote`, `is_realizing`, `realized_return_gross`, `realized_return_net`
2626

2727
## Cash ledger operations
@@ -47,6 +47,7 @@ For details, open the REPL and type `?symbol` to view docstrings.
4747
## Contract math
4848

4949
- `calc_value_quote`, `calc_pnl_quote`
50+
- `option_intrinsic_value`, `option_underlying_price`, `update_option_underlying_price!`
5051
- `margin_init_margin_ccy`, `margin_maint_margin_ccy`
5152

5253
## Exchange rate utilities
@@ -55,7 +56,7 @@ For details, open the REPL and type `?symbol` to view docstrings.
5556

5657
## Portfolio logic
5758

58-
- `update_marks!`, `settle_expiry!`
59+
- `update_marks!`, `settle_expiry!`, `settle_option_expiry!`
5960

6061
## Collectors
6162

@@ -65,7 +66,7 @@ For details, open the REPL and type `?symbol` to view docstrings.
6566

6667
## Event driver
6768

68-
- `MarkUpdate`, `FundingUpdate`, `FXUpdate`
69+
- `MarkUpdate`, `OptionUnderlyingUpdate`, `FundingUpdate`, `FXUpdate`
6970
- `advance_time!`, `process_step!`, `process_expiries!`
7071

7172
## Backtesting
@@ -88,7 +89,7 @@ For details, open the REPL and type `?symbol` to view docstrings.
8889
- `format_cash`, `format_base`, `format_quote`
8990
- `calc_base_qty_for_notional`
9091
- `has_expiry`, `is_expired`, `is_active`, `ensure_active`
91-
- `spot_instrument`, `perpetual_instrument`, `future_instrument`
92+
- `spot_instrument`, `perpetual_instrument`, `future_instrument`, `option_instrument`
9293

9394
## Printing helpers
9495

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
# # Systematic short put spread example
2+
#
3+
# This example shows a simple options strategy on synthetic SPY-like data.
4+
# The strategy sells one 30-delta put and buys a lower-strike put as protection,
5+
# holds the vertical spread to cash-settled expiry, and then repeats.
6+
#
7+
# The data is a static CSV pair: daily SPY-like underlying marks and a
8+
# ThetaData-style put option quote table with bid/ask/last/delta rows. The
9+
# important Fastback pieces are:
10+
#
11+
# - listed option instruments with strikes, expiries, rights, and multipliers
12+
# - `OptionUnderlyingUpdate` for one underlying/quote chain mark
13+
# - `MarkUpdate` for bid/ask/last option marks
14+
# - multi-leg fills with option premium cash settlement
15+
# - vertical spread margin relief and cash-settled expiry processing
16+
17+
using Fastback
18+
using Dates
19+
using CSV
20+
using DataFrames
21+
using Statistics
22+
23+
const START_CAPITAL = 100_000.0
24+
const CONTRACTS = 3.0
25+
const TARGET_DELTA = 0.30
26+
const SPREAD_WIDTH = 10.0
27+
const ENTRY_DTE = 30
28+
const ENTRY_GAP = 21
29+
30+
data_dir = joinpath(@__DIR__, "data");
31+
market = DataFrame(CSV.File(joinpath(data_dir, "options_spy_1d.csv"); dateformat="yyyy-mm-dd"));
32+
sort!(market, :dt);
33+
option_quotes = DataFrame(CSV.File(joinpath(data_dir, "options_spy_put_quotes_1d.csv"); dateformat="yyyy-mm-dd"));
34+
sort!(option_quotes, [:dt, :expiry, :strike]);
35+
36+
quote_by_key = Dict(
37+
(row.dt, row.expiry, row.strike) => (bid=row.bid, ask=row.ask, last=row.last)
38+
for row in eachrow(option_quotes)
39+
);
40+
41+
function option_symbol(expiry::Date, right::Char, strike::Real)
42+
Symbol("SPY_$(Dates.format(expiry, "yyyymmdd"))_$(right)$(Int(round(strike)))")
43+
end
44+
45+
function spy_put(expiry::Date, strike::Real)
46+
option_instrument(
47+
option_symbol(expiry, 'P', strike),
48+
:SPY,
49+
:USD;
50+
strike=Float64(strike),
51+
expiry=expiry,
52+
right=OptionRight.Put,
53+
multiplier=100.0,
54+
time_type=Date,
55+
)
56+
end
57+
58+
## account: margin-enabled, with IBKR Pro Fixed option commissions
59+
acc = Account(;
60+
time_type=Date,
61+
funding=AccountFunding.Margined,
62+
base_currency=CashSpec(:USD),
63+
broker=IBKRProFixedBroker(; time_type=Date),
64+
)
65+
usd = cash_asset(acc, :USD)
66+
deposit!(acc, :USD, START_CAPITAL)
67+
68+
## data collectors
69+
collect_equity, equity_data = periodic_collector(Float64, Day(1); time_type=Date)
70+
collect_drawdown, drawdown_data = drawdown_collector(DrawdownMode.Percentage, Day(1); time_type=Date)
71+
72+
registered_options = Instrument{Date}[]
73+
open_legs = Instrument{Date}[]
74+
entries = DataFrame(
75+
entry_dt=Date[],
76+
expiry=Date[],
77+
short_strike=Float64[],
78+
long_strike=Float64[],
79+
credit=Float64[],
80+
margin_used=Float64[],
81+
)
82+
83+
next_entry_idx = Ref(1)
84+
85+
for i in eachindex(market.dt)
86+
dt = market.dt[i]
87+
spot = market.spot[i]
88+
89+
## Mark all live options and update the option chain's underlying price.
90+
marks = MarkUpdate[]
91+
for inst in registered_options
92+
is_expired(inst, dt) && continue
93+
q = quote_by_key[(dt, inst.spec.expiry, inst.spec.strike)]
94+
push!(marks, MarkUpdate(inst.index, q.bid, q.ask, q.last))
95+
end
96+
97+
process_step!(
98+
acc,
99+
dt;
100+
option_underlyings=[OptionUnderlyingUpdate(:SPY, :USD, spot)],
101+
marks=marks,
102+
expiries=true,
103+
)
104+
105+
filter!(inst -> get_position(acc, inst).quantity != 0.0, open_legs)
106+
107+
## Enter one 30 DTE put credit spread at a time.
108+
if isempty(open_legs) && i >= next_entry_idx[] && i + ENTRY_DTE <= nrow(market)
109+
expiry = market.dt[i + ENTRY_DTE]
110+
chain = option_quotes[(option_quotes.dt .== dt) .& (option_quotes.expiry .== expiry) .& (option_quotes.right .== "P"), :]
111+
short_idx = argmin(abs.(abs.(chain.delta) .- TARGET_DELTA))
112+
short_quote = chain[short_idx, :]
113+
long_idx = argmin(abs.(chain.strike .- (short_quote.strike - SPREAD_WIDTH)))
114+
long_quote = chain[long_idx, :]
115+
short_strike = Float64(short_quote.strike)
116+
long_strike = Float64(long_quote.strike)
117+
118+
long_put = register_instrument!(acc, spy_put(expiry, long_strike))
119+
short_put = register_instrument!(acc, spy_put(expiry, short_strike))
120+
push!(registered_options, long_put)
121+
push!(registered_options, short_put)
122+
123+
## Buy the protective leg first, then sell the higher-strike put.
124+
fill_order!(
125+
acc,
126+
Order(oid!(acc), long_put, dt, long_quote.ask, CONTRACTS);
127+
dt=dt,
128+
fill_price=long_quote.ask,
129+
bid=long_quote.bid,
130+
ask=long_quote.ask,
131+
last=long_quote.last,
132+
underlying_price=spot,
133+
)
134+
fill_order!(
135+
acc,
136+
Order(oid!(acc), short_put, dt, short_quote.bid, -CONTRACTS);
137+
dt=dt,
138+
fill_price=short_quote.bid,
139+
bid=short_quote.bid,
140+
ask=short_quote.ask,
141+
last=short_quote.last,
142+
underlying_price=spot,
143+
)
144+
145+
credit = (short_quote.bid - long_quote.ask) * CONTRACTS * short_put.spec.multiplier
146+
push!(entries, (
147+
entry_dt=dt,
148+
expiry=expiry,
149+
short_strike=short_strike,
150+
long_strike=long_strike,
151+
credit=credit,
152+
margin_used=init_margin_used(acc, usd),
153+
))
154+
push!(open_legs, long_put)
155+
push!(open_legs, short_put)
156+
next_entry_idx[] = i + ENTRY_GAP
157+
end
158+
159+
if should_collect(equity_data, dt)
160+
equity_value = equity(acc, usd)
161+
collect_equity(dt, equity_value)
162+
collect_drawdown(dt, equity_value)
163+
end
164+
end
165+
166+
## account and strategy summary
167+
show(acc)
168+
169+
println("Spreads opened: ", nrow(entries))
170+
println("Final equity: \$", round(equity(acc, usd); digits=2))
171+
println("Maximum spread margin used: \$", round(maximum(entries.margin_used); digits=2))
172+
println("Average entry credit: \$", round(mean(entries.credit); digits=2))
173+
174+
#---------------------------------------------------------
175+
176+
# ### Trade log sample
177+
178+
trades = DataFrame(trades_table(acc));
179+
trades[1:min(10, nrow(trades)), [:trade_date, :symbol, :side, :fill_price, :fill_qty, :commission_settle, :cash_delta_settle, :reason]]
180+
181+
#---------------------------------------------------------
182+
183+
# ### Spread entries
184+
185+
entries
186+
187+
#---------------------------------------------------------
188+
189+
# ### Underlying and implied volatility
190+
191+
using Plots
192+
193+
theme(:juno);
194+
195+
plot(
196+
market.dt,
197+
market.spot;
198+
label="SPY synthetic",
199+
ylabel="underlying price",
200+
legend=:topleft,
201+
size=(800, 360),
202+
)
203+
plot!(
204+
twinx(),
205+
market.dt,
206+
market.iv;
207+
label="implied volatility proxy",
208+
color=:orange,
209+
ylabel="IV",
210+
legend=:topright,
211+
)
212+
213+
#---------------------------------------------------------
214+
215+
# ### Account equity curve
216+
217+
Fastback.plot_equity(equity_data; size=(800, 400))
218+
219+
#---------------------------------------------------------
220+
221+
# ### Account drawdown
222+
223+
Fastback.plot_drawdown(drawdown_data; size=(800, 220))
224+
225+
#---------------------------------------------------------
226+
227+
# ### Summary performance table
228+
229+
DataFrame(performance_summary_table(equity_data; periods_per_year=252))

0 commit comments

Comments
 (0)