A day-stepping backtesting framework for quantitative trading strategies, designed for CRSP-style daily security data.
moving-avg and moving-sd for building
SMA crossovers or Bollinger BandsAs a library, from Clojars:
;; project.clj
[com.github.clojure-finance/clojure-backtesting "1.1.2"]
;; deps.edn
com.github.clojure-finance/clojure-backtesting {:mvn/version "1.1.2"}
To run the examples and the bundled sample data, clone the repository.
Clone the repository and start a REPL:
lein repl
or a Gorilla REPL, which opens the example worksheets as notebooks:
make start
The bundled sample dataset lets you run every API without external data. It holds three securities over January to March 1990 — 10001 pays a dividend on 1990-02-15, 10002 splits 2-for-1 on 1990-03-01, and 10003 has no prices from 1990-02-05 to 1990-02-09 — plus a matching Compustat-style dataset:
(load-dataset "resources/sample-data/main" "main" add-aprc)
(load-dataset "resources/sample-data/compustat" "compustat") ; optional
(init-portfolio "1990-01-02" 10000)
;; Buy 50 shares — fills at the next close
(order "10001" 50)
;; Step forward 5 trading days
(dotimes [_ 5]
(next-date)
(update-eval-report))
;; Sell 20 shares
(order "10001" -20)
;; Continue, then close out all positions
(dotimes [_ 3] (next-date) (update-eval-report))
(end-order)
;; Review results
(print-order-record)
(print-portfolio)
(print-portfolio-record -1)
(print-eval-report)
Run lein run for a quick strategy demo. Regenerate the sample dataset with
lein run -m clojure-backtesting.sample-data.
ORDER-EXPIRATION
trading days (three by default); pass :expiration n to order to change
it for a single order.init-portfolio again restarts the backtest and drops any orders
still pending, so they cannot fill in the new run.MISSING-DAYS-LIMIT trading
days in a row (ten by default) is treated as delisted and booked as cash at
its last value.INITIAL-MARGIN; if the margin falls below MAINTENANCE-MARGIN,
every position is closed.Preprocess your data with the included clojask-script tool,
which converts CRSP and Compustat-style CSVs into the partitioned layout the
backtester expects. See its README for column
mappings and parameters.
(load-dataset "/path/to/preprocessed/crsp" "main" add-aprc)
(load-dataset "/path/to/preprocessed/compustat" "compustat")
A Compustat-style dataset is joined onto each day's rows using, per security,
the latest filing that was public on that day: its period end must be on or
before the day, and so must its report date when the data has an rdq
column.
The clojure-backtesting.yahoo namespace downloads data from Yahoo Finance
and turns it into a dataset the backtester loads:
(require '[clojure-backtesting.yahoo :refer :all])
(download-yahoo! "data/tech" ["AAPL" "MSFT" "NVDA" "GOOGL" "SPY"] :start "2015-01-01")
(build-yahoo-dataset! "data/tech")
(load-yahoo-dataset "data/tech")
(init-portfolio "2023-01-03" 100000)
download-yahoo! saves Yahoo's answers unchanged under data/tech/raw, one
snapshot per security and download. It skips securities already downloaded
today, so an interrupted download can be run again, and it also fetches the
exchange rates needed when a company reports in another currency than it
trades in. Prices always run up to today (end them earlier with the build's
:end). Options: :start, :fundamentals? (true), :fields
(Yahoo statement fields, default-fields or :all), :concurrency (2),
:pause-ms (300) and :force?.
build-yahoo-dataset! works offline, so the dataset can be rebuilt with
other options without downloading again. It writes:
main/: one row per security and trading day in the CRSP layout:
PRC, OPENPRC, BIDLO, ASKHI, VOL and DIVAMT as traded on the
day, CFACPR/CFACSHR (the later splits), RET (total return) and
RETX (without dividends), CURRENCY, and the adjusted price columns
add-aprc would add. Yahoo divides old prices by every later split; the
dataset restores the traded prices, so share counts, splits and dividends
work as with CRSP data. The security id (PERMNO) is the ticker.fundamentals.edn: the point-in-time fundamentals.dataset.edn: coverage per security and the warnings, also printed:
tickers Yahoo did not find, missing exchange rates, and report dates
that had to be estimated.Options: :out-dir, :start, :end, :report-lag-days,
:after-close-hour, :vintage and :allow-mixed-currencies?; see
clojure-backtesting.yahoo.build/build-dataset!.
clojure-backtesting.fundamentals answers what was known on the backtest
date:
(fundamental "AAPL" :net-income) ; twelve months
(fundamental "AAPL" :net-income :quarterly) ; latest quarter
(fundamental "AAPL" :stockholders-equity) ; latest balance sheet
(fundamental-info "AAPL" :diluted-eps) ; the value and where it comes from
(fundamental-history "AAPL" :total-revenue :annual 4)
Fields are Yahoo's statement fields in kebab case (:total-revenue,
:operating-income, :diluted-eps, :total-assets,
:ordinary-shares-number, :free-cash-flow, …; available-fields lists
them). Frequencies are :quarterly, :annual, :trailing (Yahoo's
twelve-month figure), :ttm (Yahoo's trailing figure, the last four
quarters or the last fiscal year, whichever ends latest) and :latest.
Income-statement and cash-flow fields default to :ttm, balance-sheet
fields to :latest.
fundamental-info says so
(:report-date-source :lag). For US large caps the assumption is later
than the real report; markets with slower reporting may need a longer
:report-lag-days.fundamental-info keeps the reported value and currency.FUNDAMENTALS-MAX-AGE-MONTHS (18)
months before the day is nil.Ratios on the day's price: market-cap, pe-ratio, earnings-yield,
book-to-market, price-to-book, sales-to-price,
free-cash-flow-yield, enterprise-value, ev-to-ebitda,
dividend-yield, plus return-on-equity, return-on-assets,
gross-margin, operating-margin, net-margin, debt-to-equity,
current-ratio, growth, piotroski-signals and piotroski-f-score.
They return nil rather than a misleading number, for example a P/E on
losses. latest-earnings and days-since-earnings give the latest
earnings announcement with its EPS surprise; use :surprise-pct, since
Yahoo's EPS there is the adjusted figure, in an inconsistent share basis.
clojure-backtesting.strategy has new-period? (first trading day of a
:week, :month, :quarter or :year, and the first day of the run),
rank-by, top-n,
equal-weights, order-target-percent and rebalance. A monthly value
strategy:
(while (neg? (compare (get-date) "2024-12-31"))
(when (new-period? :month)
(rebalance (equal-weights (top-n earnings-yield 5))))
(next-date)
(update-eval-report))
(end-order)
(benchmark-stats "SPY")
benchmark-stats (in clojure-backtesting.evaluate) compares the
portfolio with any security in the dataset: returns, beta, alpha,
correlation, tracking error and information ratio.
:vintage :latest to change that).:allow-mixed-currencies? is set.The examples/ folder contains Gorilla REPL worksheets that run
against the sample datasets (the Yahoo ones against synthetic snapshots in
resources/sample-yahoo, regenerated by
lein run -m clojure-backtesting.sample-yahoo). Open them with make start, or read them as
plain Clojure files:
| Example | Description |
|---|---|
simple_trading_strategy | Basic buy/sell loop with order and portfolio output |
golden_cross | 15/30-day moving average crossover |
bollinger_bands | Mean reversion using Bollinger Bands |
relative_strength_index | RSI-based overbought/oversold signals |
rate_of_change | Momentum strategy using ROC |
buying_on_margin | Leveraged positions and margin mechanics |
bad_strategy | A losing short position through a dividend |
automation | Condition/action rules dispatched each trading day |
Plotting_frame | Line charts of security prices and returns |
Fundamental analysis | Filing visibility by report date and a return-on-equity screen |
yahoo_finance_data | Building a Yahoo dataset: traded prices, point-in-time fundamentals, ratios |
value_strategy | Monthly earnings-yield strategy against an index |
quality_and_earnings | Piotroski F-score and trading on earnings announcements |
Regenerate their outputs with:
lein run -m clojure-backtesting.worksheet examples/*.clj
Call these before init-portfolio:
| Function | Effect |
|---|---|
(update-reinvest-dividends false) | Pay dividends to cash instead of reinvesting |
(update-output-dir "out") | Write orders, values, and reports as CSV files |
(update-initial-margin 0.5) | Equity required for new borrowing; nil for none |
(update-maintenance-margin 0.25) | Margin floor below which all positions are closed |
(update-transaction-cost 0.001) | Commission as a fraction of trade value (default 0) |
(update-interest-rate 0.05) | Annual interest rate charged on loans (default 0) |
(update-fundamentals-max-age 12) | Months after its period end a fundamental stays usable (default 18) |
With dividends paid to cash, splits are recognised only if the main dataset
has the CRSP CFACPR column; otherwise a split is paid out as cash too.
ORDER-EXPIRATION and MISSING-DAYS-LIMIT in clojure-backtesting.parameters
have no setter; override them for a session with, for example,
(alter-var-root #'MISSING-DAYS-LIMIT (constantly 20)).
Full API documentation: clojure-finance.github.io/clojure-backtesting-website
Run the unit and end-to-end tests (no network):
lein test
and the tests that download from Yahoo Finance:
lein test :live
MIT
Can you improve this documentation? These fine people already did:
Matt Buehlmaier, awoo424, Yuchen Liu, Pilot Tam, Angel Woo, Joey Pathak-Dubey & clojure-financeEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |