Skip to content

Repository files navigation

Keep Rollin'

CI codecov Python 3.10+ License: MIT Open in Streamlit

Keep Rollin' dashboard

Description

Computes annualised risk/return metrics for multi-asset portfolios, including rolling windows:

  • Sharpe ratio — excess return per unit of total volatility
  • Sortino ratio — excess return per unit of downside volatility (doesn't penalise upside)
  • Max drawdown — largest peak-to-trough decline over the period
  • Rolling Sharpe ratio — Sharpe ratio computed over a sliding window (default: 63 trading days ≈ 1 quarter)
  • Rolling Sortino ratio — same, but penalising only downside volatility within each window

Two conventions apply everywhere — to the CLI, the dashboard and the API alike:

  • Excess return is measured against the benchmark, not a risk-free rate. Classical Sharpe uses the risk-free rate; measuring against a configurable benchmark makes this closer to an information ratio. That is a deliberate choice: the question here is "did this asset beat the benchmark, per unit of risk taken?" The benchmark is any Yahoo Finance symbol — an index, a sector ETF, or a single stock — so the same tooling answers both "did it beat the market?" and "did it beat that competitor?"
  • Date ranges are inclusive at both ends, and returns are annualised with a 252-trading-day year.

Data is fetched live from Yahoo Finance. If Yahoo Finance is unavailable, the dashboard falls back to a small price snapshot shipped with the package so it still renders — clearly flagged as offline data.

Installation

uv sync --all-extras          # library + CLI + dev tools + Streamlit + API

Or install a minimal set:

uv sync --extra dev           # library + CLI + dev tools only

Usage

Streamlit app

uv run streamlit run streamlit_app.py

Opens an interactive dashboard in your browser: pick tickers, benchmark, date range, and rolling window from the sidebar and click Analyse.

It opens on MSFT and NVDA against the S&P 500, over the five years ending on the previous trading day. Trading days are approximated as weekdays, so the default end date does not skip exchange holidays.

Docker

The Dockerfile has two runtime targets. The dashboard is the default:

docker build -t keep-rollin .
docker run --rm -p 8501:8501 keep-rollin

Then visit http://localhost:8501.

The API is a separate target:

docker build --target api -t keep-rollin:api .
docker run --rm -p 8000:8000 keep-rollin:api

Then visit http://localhost:8000/docs.

Target Serves Port Healthcheck
dashboard (default) Streamlit dashboard 8501 /_stcore/health
api FastAPI JSON API 8000 /health

The rollin CLI ships in both images, since it installs with the package. Override the command to use it without starting a server:

docker run --rm keep-rollin rollin MSFT NVDA

Both images run as a non-root user and include the bundled offline price snapshot, so the fallback works in the container too.

Installing Docker. On Debian/Ubuntu (including WSL2), the distro packages are enough:

sudo apt install docker.io docker-buildx   # buildx is required because the Dockerfile uses BuildKit cache mounts
sudo usermod -aG docker $USER   # then log out and back in, or run: newgrp docker

CLI

rollin MSFT NVDA

Every argument is optional and defaults to the same values as the dashboard and the API, so a bare rollin analyses the default tickers over the default window.

Argument Default Description
tickers MSFT, NVDA Yahoo Finance symbols, space-separated
--benchmark ^GSPC Benchmark symbol: any index, ETF or individual stock
--start 5 years before --end Start date YYYY-MM-DD
--end previous trading day End date YYYY-MM-DD, inclusive
--rolling-window 63 Rolling window in trading days, 2–252 (for both Sharpe and Sortino)
--plot off Display rolling Sharpe and Sortino ratio charts

Override any of them:

rollin AAPL --benchmark ^GSPC --start 2023-01-01 --end 2023-12-31 --rolling-window 21

Because the benchmark is just another symbol, pointing it at a competitor turns the same command into relative-value analysis:

rollin MSFT NVDA --benchmark AAPL --start 2023-01-01 --end 2023-12-31

Over 2023 that drops MSFT's Sharpe from 1.33 against the S&P 500 to 0.16 against AAPL: it beat the index comfortably and its competitor barely. Max drawdown is computed on prices rather than excess returns, so that column stays absolute and does not move with the benchmark.

HTTP API

uv sync --extra api
uv run uvicorn keep_rollin.api:app --reload

Interactive docs at http://localhost:8000/docs.

curl "http://localhost:8000/metrics?tickers=MSFT&tickers=NVDA&start=2023-01-01&end=2023-12-31"

Both bounds are inclusive, so that range covers the whole 2023 calendar year: the first and last bars are 2023-01-03 and 2023-12-29.

Endpoint Description
GET /metrics Same metrics as the CLI, as JSON — one object per asset
GET /health Liveness probe; also reports whether the offline snapshot is present

Query parameters for /metrics:

Parameter Default Description
tickers MSFT, NVDA Yahoo Finance symbol; repeat the parameter for several
start 5 years before end Start date YYYY-MM-DD
end previous trading day End date YYYY-MM-DD, inclusive
benchmark ^GSPC Benchmark symbol: any index, ETF or individual stock
rolling_window 63 Rolling window in trading days (2–252)

Every parameter is optional and each defaults independently, so curl http://localhost:8000/metrics is a valid request that returns the same defaults the dashboard opens on.

The response includes used_fallback, which is true when live data was unavailable and the offline snapshot was served instead. Metrics that are mathematically undefined for the data (an infinite Sortino ratio, for instance) are returned as null.

Refreshing the offline snapshot

The bundled snapshot backs the dashboard when Yahoo Finance is unavailable. Regenerate it with:

uv run python scripts/refresh_fallback.py

It refuses to overwrite the existing snapshot if the fetch fails or returns too little data, so a bad run cannot destroy the safety net. Pass tickers and --benchmark / --start / --end to change what it covers.

Running tests

uv run pytest
# with coverage
uv run pytest --cov=keep_rollin

CI also lints and type-checks. To reproduce it locally, run what the workflow runs:

uv run ruff check src tests scripts streamlit_app.py
uv run ruff format --check src tests scripts streamlit_app.py
uv run mypy src

Project structure

.github/workflows/ci.yml        — ruff, mypy and pytest on push/PR (Python 3.10 and 3.13)
Dockerfile                      — multi-stage build; dashboard and api targets
streamlit_app.py                — Streamlit dashboard
docs/img/
    dashboard.png               — screenshot used in this README
scripts/
    refresh_fallback.py         — regenerate the offline price snapshot
src/keep_rollin/
    data.py                     — fetch adjusted close prices, shared defaults, offline fallback
    metrics.py                  — Sharpe, Sortino, max drawdown, rolling variants, shared summary
    cli.py                      — command-line entry point
    api.py                      — FastAPI layer exposing the same metrics over HTTP
    resources/
        fallback_prices.parquet — offline snapshot used when Yahoo Finance is down
tests/
    conftest.py                 — pins matplotlib's headless backend for the suite
    test_api.py
    test_cli.py
    test_data.py
    test_metrics.py
    test_streamlit_app.py
pyproject.toml                  — project metadata, dependencies and optional extras
uv.lock                         — pinned dependency versions
requirements.txt                — pip entry point for Streamlit Community Cloud
codecov.yml                     — coverage thresholds and PR comment behaviour

Credits

The project was inspired by a DataCamp exercise on the Sharpe Ratio (original tasks by Stefan Jansen, completed in January 2022). However, everything in this repository was written from scratch.

About

Annualised risk/return metrics for multi-asset portfolios: Sharpe, Sortino, max drawdown and rolling variants, measured against any benchmark. Live Yahoo Finance data with an offline parquet fallback, exposed three ways: a CLI, a Streamlit dashboard and a FastAPI JSON API. Dockerised, typed and tested.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages