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.
uv sync --all-extras # library + CLI + dev tools + Streamlit + APIOr install a minimal set:
uv sync --extra dev # library + CLI + dev tools onlyuv run streamlit run streamlit_app.pyOpens 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.
The Dockerfile has two runtime targets. The dashboard is the default:
docker build -t keep-rollin .
docker run --rm -p 8501:8501 keep-rollinThen 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:apiThen 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 NVDABoth 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 dockerrollin MSFT NVDAEvery 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 21Because 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-31Over 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.
uv sync --extra api
uv run uvicorn keep_rollin.api:app --reloadInteractive 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.
The bundled snapshot backs the dashboard when Yahoo Finance is unavailable. Regenerate it with:
uv run python scripts/refresh_fallback.pyIt 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.
uv run pytest
# with coverage
uv run pytest --cov=keep_rollinCI 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.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
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.
