From b5da424dfeeb08e94bc3881f4eab3c8034ce5bf7 Mon Sep 17 00:00:00 2001 From: herzogflorian Date: Wed, 7 Oct 2026 09:55:28 +0200 Subject: [PATCH] quantcore: standalone Apptainer setup for students' DGX Sparks The portfolio data, risk and mean-CVaR optimisation service, prepared for students who run it on their own DGX Spark without root, Docker or Tailscale. - deploy/apptainer/qc.sh: install, start/stop/status/logs, cli/api/python, data import/export and Yahoo setup/update. cuOpt and quantcore run as two Apptainer instances of the user and talk over 127.0.0.1; the API and the test bench listen on 127.0.0.1 only. Builds quantcore.sif without root, or falls back to a plain Python image plus a virtualenv when the Spark allows no unprivileged builds. Detects taken ports and suggests free ones. - deploy/apptainer/quantcore.def + constraints.txt: the image, with the library versions of the verified production container pinned. - cuOpt under Apptainer: NVIDIA's image sets HOME=/opt/cuopt, read-only in a SIF, so the solver died on its first job; qc.sh gives it a writable home. - solvers: a cuOpt that disappears during a solve now falls back to HiGHS instead of answering 500 (requests' ConnectionError escaped the fallback). - client: idempotent GETs retry a dropped keep-alive connection, which made `quantcore-api update --wait` fail when a poll met uvicorn's 5 s timeout. - config: cuOpt defaults to http://127.0.0.1:5000 and `quantcore serve` to 127.0.0.1; the Docker images keep passing their own values. - docs/SPARK_APPTAINER.md: the students' guide; README, USAGE, ARCHITECTURE and SPARK_DEPLOYMENT point to it. Tested on a DGX Spark (GB10, Apptainer 1.5.4, unprivileged user) in both modes: selftest on every endpoint, optimisation on cuOpt, Yahoo update, port conflicts; 139 tests pass in the container against the live cuOpt. Co-Authored-By: Claude Opus 5.5 --- .env.example | 12 + .gitignore | 22 + README.md | 251 ++++- deploy/Dockerfile | 24 + deploy/apptainer/constraints.txt | 48 + deploy/apptainer/qc.sh | 576 +++++++++++ deploy/apptainer/quantcore.def | 75 ++ deploy/apptainer/quantcore.env.example | 40 + deploy/deploy_to_spark.sh | 57 ++ deploy/docker-compose.quantcore.yml | 26 + deploy/docker-compose.students.yml | 30 + docs/ARCHITECTURE.md | 125 +++ docs/METHODOLOGY.md | 437 ++++++++ docs/SPARK_APPTAINER.md | 254 +++++ docs/SPARK_DEPLOYMENT.md | 110 ++ docs/USAGE.md | 342 +++++++ docs/measurement_and_models.pdf | Bin 0 -> 502567 bytes docs/measurement_and_models.tex | 807 +++++++++++++++ docs/optimization_model.pdf | Bin 0 -> 573970 bytes docs/optimization_model.tex | 1197 ++++++++++++++++++++++ pyproject.toml | 49 + quantcore/__init__.py | 24 + quantcore/api/__init__.py | 1 + quantcore/api/app.py | 556 ++++++++++ quantcore/api/schemas.py | 233 +++++ quantcore/api/ui/index.html | 27 + quantcore/api/ui/js/charts.js | 508 +++++++++ quantcore/api/ui/js/core.js | 393 +++++++ quantcore/api/ui/js/main.js | 85 ++ quantcore/api/ui/js/runs.js | 47 + quantcore/api/ui/js/sidebar.js | 179 ++++ quantcore/api/ui/js/tabs/constraints.js | 173 ++++ quantcore/api/ui/js/tabs/optimize.js | 511 +++++++++ quantcore/api/ui/js/tabs/risk.js | 299 ++++++ quantcore/api/ui/js/tabs/stocks.js | 217 ++++ quantcore/api/ui/js/tabs/views.js | 172 ++++ quantcore/api/ui/js/universe.js | 39 + quantcore/api/ui/styles.css | 254 +++++ quantcore/apicli.py | 672 ++++++++++++ quantcore/cli.py | 253 +++++ quantcore/client.py | 314 ++++++ quantcore/config.py | 125 +++ quantcore/measurement/__init__.py | 18 + quantcore/measurement/estimators.py | 228 +++++ quantcore/measurement/performance.py | 145 +++ quantcore/measurement/returns.py | 116 +++ quantcore/measurement/risk.py | 401 ++++++++ quantcore/models/__init__.py | 10 + quantcore/models/black_litterman.py | 231 +++++ quantcore/models/covariance.py | 145 +++ quantcore/models/sampling.py | 255 +++++ quantcore/models/simulation.py | 403 ++++++++ quantcore/optimization/__init__.py | 17 + quantcore/optimization/gaussian_exact.py | 95 ++ quantcore/optimization/problem.py | 544 ++++++++++ quantcore/optimization/rowgen.py | 151 +++ quantcore/optimization/service.py | 763 ++++++++++++++ quantcore/optimization/solvers.py | 317 ++++++ quantcore/universe/__init__.py | 18 + quantcore/universe/setup_service.py | 209 ++++ quantcore/universe/sources.py | 302 ++++++ quantcore/universe/symbols.py | 129 +++ scripts/experiments/README.md | 16 + scripts/experiments/empirical_noise.py | 136 +++ scripts/experiments/gurobi_ref.py | 46 + scripts/experiments/is_noise.py | 173 ++++ scripts/experiments/rank_and_size.py | 125 +++ scripts/experiments/rowgen.py | 115 +++ scripts/experiments/scaling.py | 54 + scripts/experiments/scenario_noise.py | 177 ++++ scripts/experiments/tilt_correlation.py | 155 +++ tests/conftest.py | 62 ++ tests/test_api.py | 209 ++++ tests/test_black_litterman.py | 91 ++ tests/test_cleaning.py | 132 +++ tests/test_client.py | 222 ++++ tests/test_data.py | 81 ++ tests/test_estimators.py | 54 + tests/test_problem.py | 166 +++ tests/test_returns.py | 67 ++ tests/test_risk.py | 208 ++++ tests/test_rowgen_gaussian.py | 275 +++++ tests/test_sampling.py | 122 +++ tests/test_service_equivalence.py | 281 +++++ tests/test_simulation.py | 197 ++++ tests/test_solvers.py | 165 +++ 86 files changed, 17388 insertions(+), 2 deletions(-) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 deploy/Dockerfile create mode 100644 deploy/apptainer/constraints.txt create mode 100755 deploy/apptainer/qc.sh create mode 100644 deploy/apptainer/quantcore.def create mode 100644 deploy/apptainer/quantcore.env.example create mode 100755 deploy/deploy_to_spark.sh create mode 100644 deploy/docker-compose.quantcore.yml create mode 100644 deploy/docker-compose.students.yml create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/METHODOLOGY.md create mode 100644 docs/SPARK_APPTAINER.md create mode 100644 docs/SPARK_DEPLOYMENT.md create mode 100644 docs/USAGE.md create mode 100644 docs/measurement_and_models.pdf create mode 100644 docs/measurement_and_models.tex create mode 100644 docs/optimization_model.pdf create mode 100644 docs/optimization_model.tex create mode 100644 pyproject.toml create mode 100644 quantcore/__init__.py create mode 100644 quantcore/api/__init__.py create mode 100644 quantcore/api/app.py create mode 100644 quantcore/api/schemas.py create mode 100644 quantcore/api/ui/index.html create mode 100644 quantcore/api/ui/js/charts.js create mode 100644 quantcore/api/ui/js/core.js create mode 100644 quantcore/api/ui/js/main.js create mode 100644 quantcore/api/ui/js/runs.js create mode 100644 quantcore/api/ui/js/sidebar.js create mode 100644 quantcore/api/ui/js/tabs/constraints.js create mode 100644 quantcore/api/ui/js/tabs/optimize.js create mode 100644 quantcore/api/ui/js/tabs/risk.js create mode 100644 quantcore/api/ui/js/tabs/stocks.js create mode 100644 quantcore/api/ui/js/tabs/views.js create mode 100644 quantcore/api/ui/js/universe.js create mode 100644 quantcore/api/ui/styles.css create mode 100644 quantcore/apicli.py create mode 100644 quantcore/cli.py create mode 100644 quantcore/client.py create mode 100644 quantcore/config.py create mode 100644 quantcore/measurement/__init__.py create mode 100644 quantcore/measurement/estimators.py create mode 100644 quantcore/measurement/performance.py create mode 100644 quantcore/measurement/returns.py create mode 100644 quantcore/measurement/risk.py create mode 100644 quantcore/models/__init__.py create mode 100644 quantcore/models/black_litterman.py create mode 100644 quantcore/models/covariance.py create mode 100644 quantcore/models/sampling.py create mode 100644 quantcore/models/simulation.py create mode 100644 quantcore/optimization/__init__.py create mode 100644 quantcore/optimization/gaussian_exact.py create mode 100644 quantcore/optimization/problem.py create mode 100644 quantcore/optimization/rowgen.py create mode 100644 quantcore/optimization/service.py create mode 100644 quantcore/optimization/solvers.py create mode 100644 quantcore/universe/__init__.py create mode 100644 quantcore/universe/setup_service.py create mode 100644 quantcore/universe/sources.py create mode 100644 quantcore/universe/symbols.py create mode 100644 scripts/experiments/README.md create mode 100644 scripts/experiments/empirical_noise.py create mode 100644 scripts/experiments/gurobi_ref.py create mode 100644 scripts/experiments/is_noise.py create mode 100644 scripts/experiments/rank_and_size.py create mode 100644 scripts/experiments/rowgen.py create mode 100644 scripts/experiments/scaling.py create mode 100644 scripts/experiments/scenario_noise.py create mode 100644 scripts/experiments/tilt_correlation.py create mode 100644 tests/conftest.py create mode 100644 tests/test_api.py create mode 100644 tests/test_black_litterman.py create mode 100644 tests/test_cleaning.py create mode 100644 tests/test_client.py create mode 100644 tests/test_data.py create mode 100644 tests/test_estimators.py create mode 100644 tests/test_problem.py create mode 100644 tests/test_returns.py create mode 100644 tests/test_risk.py create mode 100644 tests/test_rowgen_gaussian.py create mode 100644 tests/test_sampling.py create mode 100644 tests/test_service_equivalence.py create mode 100644 tests/test_simulation.py create mode 100644 tests/test_solvers.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..21df0b0 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# Copy to .env and adjust. All variables are optional; defaults are in quantcore/config.py. +QUANTCORE_DATA_DIR=./data # where the setup/download services persist everything +QUANTCORE_CUOPT_URL=http://127.0.0.1:5000 # cuOpt on this machine (Apptainer setup); a remote one: http://:5000 +QUANTCORE_CUOPT_TIME_LIMIT=120 # seconds, MILP time limit +QUANTCORE_BASE_CURRENCY=CHF +QUANTCORE_RISK_FREE_RATE=0.0 # annual, CHF +QUANTCORE_HISTORY_START=2004-12-01 # first date requested from Yahoo +QUANTCORE_REQUIRED_HISTORY_FROM=2005-01-31 # a stock must have prices on or before this date +QUANTCORE_N_US=500 +QUANTCORE_N_EUROPE=500 +QUANTCORE_LOG_LEVEL=INFO +QUANTCORE_ENABLE_UI=true # false: JSON API and /docs only, no test bench at /ui/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..214165a --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# data produced by the setup / download services lives outside version control +data/ +*.parquet +!quantcore/data/static/* +.env +__pycache__/ +*.pyc +.pytest_cache/ +.ruff_cache/ +*.egg-info/ +build/ +dist/ +.DS_Store +scratch/ +# LaTeX build files +*.aux +*.fdb_latexmk +*.fls +*.log +*.out +*.toc +*.synctex.gz diff --git a/README.md b/README.md index 5adb9d8..8c1fd49 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,250 @@ -# Portfolio_Risk_Management +# quantcore — Portfolio Risk Measurement & Optimisation -The API for risk management, data downloads and portfolio optimization for AISE502 \ No newline at end of file +The **deterministic quant core** of the AI-Augmented Portfolio Intelligence Platform (AISE502): +data, risk and performance measurement, stock-price models, Black–Litterman expected returns and +mean–CVaR portfolio optimisation in whole shares, solved as a MILP on **NVIDIA cuOpt** on the DGX Spark. + +Everything here is *deterministic* — same input, same output — and contains **no LLM call**. The AI +layer of the platform (news → insights → views) consumes these services through their contracts and +never computes a number itself. + +| Service | Module | What it does | +|---|---|---| +| **SetupService** | `quantcore.universe` | Builds the investment universe **once**: largest 500 US (S&P 500) and 500 European (STOXX 600; EU, CH, NO, UK) stocks, Yahoo metadata (name, country, listing currency, exchange, GICS sector/industry, market cap, shares outstanding), full price history since 2004, FX rates; keeps only stocks with prices since 2005. | +| **DataDownloadService** | `quantcore.data` | Fetches the newest Yahoo prices and FX rates (incremental with dividend-adjustment splicing, or full), snapshots for reproducible demos. | +| **MarketDataService** | `quantcore.data.market_data` | Aligned adjusted closes and log-returns, daily/weekly/monthly, in CHF or local currency; market caps, cap-weighted broad market index. | +| **RiskService** | `quantcore.measurement.risk` | Volatility, correlation, covariance, VaR and CVaR (95 %, 99 %) per asset and per share-based portfolio — moving windows and exponentially weighted (EWM), Gaussian and historical, for a day / week / month, as absolute CHF amounts. | +| **PerformanceService** | `quantcore.measurement.performance` | Annualised return, volatility, Sharpe, Sortino, drawdown, Calmar, skew/kurtosis, hit ratio; rolling and EWM series; portfolios in shares. | +| **Stock-price models** | `quantcore.models.simulation` | Geometric Brownian motion (multivariate normal) and the empirical-innovation model `eps_t = Sigma_{t-1}^{-1/2} r_t`, `r(j|T) = Sigma_T^{1/2} eps_j` with the latest rolling or EWM covariance. | +| **Expected returns** | `quantcore.models.black_litterman` | Market-cap equilibrium returns (broad-market average, market weights), Black–Litterman on a 10-year weekly covariance with **Idzorek confidence** views (diagonal Ω), sentiment → view helper. | +| **OptimizationService** | `quantcore.optimization` | Mean–CVaR MILP in integer share numbers: **new portfolio** (from cash) or **rebalancing** (existing holdings, transaction costs); constraints: max 10 % per stock, max per sector, no shorts, cost/turnover limits, optional cardinality; scenario sets with variance reduction (Sobol', moment matching, market-direction importance sampling) that stay identical when risk limits, constraints, costs or views change, tail-row generation for large sets, and an exact Gaussian (mean–variance-equivalent) mode; solved on cuOpt (GPU, over HTTP) with a HiGHS fallback (CPU, compiled into SciPy — runs in-process, nothing to deploy). | +| **HTTP API** | `quantcore.api` | FastAPI app (`/docs`) exposing all of the above; setup/update as background jobs. Runs as a container on the DGX Spark next to cuOpt. | + +Documentation: **[docs/USAGE.md](docs/USAGE.md) (start here if you are building on this package: the API, the +Python client, the `quantcore-api` command line, the conventions and the traps)**, +[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) (services, contracts, data flow, resilience), +[docs/METHODOLOGY.md](docs/METHODOLOGY.md) (every formula and convention), +[docs/optimization_model.pdf](docs/optimization_model.pdf) (derivation of the MILP from the Rockafellar–Uryasev +model in weights to whole shares with rebalancing and costs; sampling error of scenario sets, quasi-Monte Carlo, +moment matching, importance sampling, row generation, the exact Gaussian/mean–variance case and reproducibility; +source `docs/optimization_model.tex`), +[docs/measurement_and_models.pdf](docs/measurement_and_models.pdf) (risk and performance measures, rolling/EWM +estimators and shrinkage, GBM and empirical-innovation scenarios, Black–Litterman with Idzorek confidence; +source `docs/measurement_and_models.tex`), +[docs/SPARK_APPTAINER.md](docs/SPARK_APPTAINER.md) (**students: running it on your own DGX Spark** with +Apptainer, no root), [docs/SPARK_DEPLOYMENT.md](docs/SPARK_DEPLOYMENT.md) (the Docker setup on a Spark with root). + +--- + +## Quick start on your DGX Spark (students) + +The service and cuOpt run as two Apptainer containers on your Spark and talk over `127.0.0.1`. +No root rights, no Docker, nothing exposed to the network: + +```bash +git clone https://gitea.fhgr.ch/herzogfloria/Portfolio_Risk_Management.git +cd Portfolio_Risk_Management +deploy/apptainer/qc.sh install # once: cuOpt image, quantcore image, self-test +deploy/apptainer/qc.sh data-import quantcore_data.tar.gz # prepared data set (or: qc.sh setup, ~15 min) +deploy/apptainer/qc.sh start # -> http://127.0.0.1:5001/ui/ and /docs +deploy/apptainer/qc.sh api selftest # every endpoint, pass/fail +``` + +Your platform then calls the API at `http://127.0.0.1:5001`. The full guide, with settings, +several users on one Spark and troubleshooting, is [docs/SPARK_APPTAINER.md](docs/SPARK_APPTAINER.md). + +## Quick start (local) + +```bash +pip install -e ".[dev]" # Python 3.11+ +cp .env.example .env # optional: data dir, cuOpt URL, ... + +quantcore setup # once: universe + full history (~15 min, ~1100 Yahoo tickers) +quantcore status # what is stored, cuOpt health +quantcore update # later: newest prices / FX (incremental) +quantcore snapshot --name demo # freeze the data for a reproducible demo + +quantcore risk NESN.SW AAPL ASML.AS --horizon week --window 200 --lam 0.94 +quantcore performance NESN.SW AAPL --freq weekly +quantcore optimize --cash 1000000 --cvar-limit 40000 --confidence 0.95 --horizon week --max-assets 100 +quantcore serve # HTTP API on :5001 -> http://localhost:5001/docs + # and the test bench -> http://localhost:5001/ui/ +``` + +Against a *running* service -- from any machine, without the data directory -- use the API client +instead (`quantcore-api`, or `quantcore.client.QuantcoreClient` from Python): + +```bash +export QUANTCORE_API_URL=http://127.0.0.1:5001 # or the address of the machine that runs it +quantcore-api health +quantcore-api selftest # call every endpoint, pass/fail table, exit code +quantcore-api endpoints # the API surface, from its own OpenAPI schema +quantcore-api risk NESN.SW AAPL --horizon week --drift zero --space value +quantcore-api optimize --cash 1000000 --cvar-limit 40000 --horizon week --risk-drift zero +``` + +`quantcore setup` fetches the candidate lists from the web (Wikipedia S&P 500, iShares STOXX 600 +holdings); `--offline` uses the snapshots shipped in `quantcore/data/static/`. All data lands in +`QUANTCORE_DATA_DIR` (default `./data`): `universe/universe.csv`, `prices/adj_close.parquet`, +`fx/fx_chf.parquet`, `manifest.json`, `snapshots/`. + +## Using the services from Python + +```python +from quantcore import get_settings +from quantcore.data.market_data import MarketDataService +from quantcore.measurement.risk import RiskService +from quantcore.measurement.performance import PerformanceService +from quantcore.optimization.service import OptimizationService, OptimizationRequest + +settings = get_settings() +md = MarketDataService(settings) # or MarketDataService(settings, snapshot="demo") + +risk = RiskService(md) +risk.asset_risk(["NESN.SW", "AAPL"], horizon="week", window=200, lam=0.94) # per-asset table +risk.correlation(["NESN.SW", "AAPL", "ASML.AS"], method="ewm", lam=0.99) +risk.portfolio_risk({"NESN.SW": 500, "AAPL": 100}, cash=20_000, horizon="month", method="historical") + +PerformanceService(md).asset_performance(["NESN.SW", "AAPL"], freq="weekly") + +opt = OptimizationService(settings, md) +res = opt.optimize(OptimizationRequest( + mode="new", cash=1_000_000, max_assets=100, horizon="week", confidence=0.95, cvar_limit=40_000, + scenario_model="empirical", covariance={"method": "ewm", "lam": 0.99}, + views=[{"symbol": "NESN.SW", "sentiment": 0.7, "confidence": 0.6, "source": "insight-1234"}], +)) +res.positions_frame() # shares, value, weight, trade per stock +res.cvar, res.expected_pnl # CHF over the horizon +res.scenario_hash, res.reproducibility # same inputs -> same scenarios; 'exact' (HiGHS) | 'within_gap' (cuOpt) +res.scenario_key["id"] # varying the risk limit, constraints, costs or views keeps this id: one + # scenario set for a whole family of runs (and it is cached in the service) + +# scenario options: 4096 moment-matched Sobol' scenarios by default; importance sampling for the tail +# switches on automatically at beta = 0.99; large sets are solved by tail-row generation +res99 = opt.optimize(OptimizationRequest(cash=1_000_000, confidence=0.99, cvar_limit=60_000, n_scenarios=16_384)) +# the exact Gaussian (mean-variance-equivalent) solution: no scenarios, no sampling error +res_g = opt.optimize(OptimizationRequest(cash=1_000_000, cvar_limit=40_000, scenario_model="gaussian_exact")) +# n_scenarios defaults to 16 per candidate (4096-16384); the result warns if that is low for the +# candidate count or if the covariance estimate is ill-conditioned for it + +# rebalancing: existing holdings, transaction costs, turnover limit +res2 = opt.optimize(OptimizationRequest( + mode="rebalance", cash=res.cash_end, holdings={p["symbol"]: p["shares"] for p in res.positions}, + cvar_limit=30_000, costs={"proportional_buy": 0.001, "proportional_sell": 0.001, "fixed_per_trade": 10}, + constraints={"max_weight": 0.10, "max_sector_weight": 0.25, "max_turnover": 0.3}, +)) +``` + +## HTTP API (the platform's contract) + +| Method | Path | Purpose | +|---|---|---| +| GET | `/health` | store status, cuOpt health | +| GET | `/universe?region=§or=&country=` · `/universe/{symbol}` | universe with metadata | +| GET | `/prices?symbols=A,B&freq=weekly¤cy=base` · `/returns?...` | prices / log-returns | +| GET | `/market/index?freq=weekly` · `/market/weights` | cap-weighted market index, weights | +| POST | `/risk/assets` (`space: log|value`, `drift: estimated|zero`) · `/risk/series` · `/risk/covariance` · `/risk/correlation` · `/risk/portfolio` | risk measurement | +| POST | `/performance/assets` · `/performance/rolling` · `/performance/portfolio` | performance measurement | +| GET | `/data/symbols?region=§or=&country=&search=` | one table of the universe (metadata, market cap, price) plus the filter facets | +| POST | `/model/expected-returns` · `/model/covariance` · `/model/scenarios` | Black–Litterman posterior, the estimator the models use, scenario set (+ the P&L distribution of a portfolio) | +| POST | `/optimize` | mean–CVaR MILP (new / rebalance; a rebalancing also reports the starting portfolio under `before`) | +| POST | `/jobs/setup` · `/jobs/update?full=` · GET `/jobs/{id}` | background setup / data update | +| POST/GET | `/snapshots` | freeze / list data snapshots | + +Example: + +```bash +curl -s http://127.0.0.1:5001/optimize -H 'Content-Type: application/json' -d '{ + "mode": "new", "cash": 1000000, "max_assets": 100, "horizon": "week", + "confidence": 0.95, "cvar_limit": 40000, "scenario_model": "empirical", + "views": [{"symbol": "NESN.SW", "sentiment": 0.7, "confidence": 0.6}] +}' | python -m json.tool +``` + +## Test bench (browser UI) + +`http://127.0.0.1:5001/ui/` serves a single-page test bench — the +one place to drive every service by hand and judge what it produces. It is a pure client of the HTTP +API above: no numbers are computed in the browser. + +Because it is a client and not a part of the API, it can be left out of a deployment: +`QUANTCORE_ENABLE_UI=false` (read once at startup) serves the JSON API and `/docs` only, `/ui/` +answers 404 and `/` no longer redirects. The same image therefore also suits an installation whose +users build their own front end against these endpoints. + +The left panel holds the state every tab shares — the candidate stocks, the portfolio (new or +rebalancing, cash, holdings) and the models (frequency, horizon, stock-price model, scenario count, +seed, sampling, tail tilt, covariance estimator, expected-return model). Five tabs use it: + +| Tab | What it answers | +|---|---| +| **Stocks & data** | Which stocks exist (filter by region, sector, country, name) and what their history looks like: indexed prices, drawdowns, risk/return, market caps, and a per-stock table of volatility, VaR/CVaR (normal and historical), Sharpe, drawdown, skew and kurtosis. | +| **Risk & scenarios** | Per-stock scenario moments and quantiles in **price returns** (bounded below by −100 %) or, with one click, in the log-returns the model works in — a quarterly log-return of −2.0 is a price return of −86 %, and mixing the two is the easiest way to misread a simulation. The covariance the optimiser actually sees: correlation heat map, volatilities, eigenvalue spectrum, shrinkage and conditioning warnings. Then the scenario set: the horizon P&L distribution of a chosen portfolio (cap-weighted, equally weighted, your holdings or the last proposal) with VaR/CVaR markers, per-stock moments and quantiles, a single-stock distribution, the scenario hash, and a sampling-noise check across three seeds. | +| **Expected returns** | The market-implied (equilibrium) returns from reverse optimisation, the views you add (sentiment or explicit, with Idzorek confidence), and the posterior — side by side, with the change per stock and how each view was translated into `q` and `ω`. | +| **Constraints** | Weight, sector, cardinality, turnover, cost and investment-band limits, transaction costs, plus a feasibility check against the candidate set (are there enough names, do the sector caps leave room, is any stock unaffordable at one share?) and the sector composition against the caps. | +| **Optimisation** | Objective, confidence, CVaR limit, the risk convention (`risk_drift`), solver and time limit; the proposal in integer shares with trades and costs; for a rebalancing the **before and after** of the same portfolio on the same scenarios; the weights and sector exposure against the limits; the provenance (scenario key and hash, solver, MIP gap, row-generation passes, reproducibility); a run history and a risk-limit sweep that traces the risk/return trade-off. | + +A third: **the CVaR limit can stop constraining anything over a long horizon**. CVaR₉₉ ≈ −μₕ + 2.67 σₕ, +so with a one-year horizon and an expected return of 40 % against a portfolio volatility of ~13 %, the whole +1 % tail is a gain, the limit is slack and mean-CVaR degenerates into "buy the highest forecast returns up to +the weight caps". The result then warns, and `risk_drift="zero"` measures the CVaR on the centred scenarios +(the risk figure is then independent of the return forecast) while the objective keeps it. The `historical` +expected-return model makes this extreme: on this data it gives up to 821 % p.a. to a single stock. + +Two things to know while using it. The **time limit is the budget of the whole run**, not of each +solver call: row generation and the Gaussian outer approximation solve a sequence of MILPs, and each +one gets what is left of the budget (the default is 300 s; an empty field means no limit, and HiGHS +will then run until it can prove optimality -- hours on a large candidate set). And the service +answers **one request at a time** while a solve runs: the MILP holds the interpreter, so the other +tabs wait until it finishes. The progress panel says both, and a run can be cancelled. + +Typical checks it is built for: put 1 m or 50 m CHF into a new portfolio and see how the composition +changes with wealth (integer shares start to matter); restrict the universe to European stocks and +compare; add a view on one stock and watch the posterior and the proposal move; switch the +stock-price model from GBM to empirical innovations (or to the exact Gaussian mode, which has no +sampling error at all) and see the risk figures change; run the same request twice and confirm that +the scenario hash and the shares are identical. + +## On the DGX Spark + +There are two setups. Both run the same code and give the same results. + +* **Apptainer, no root** (the students' Sparks): `deploy/apptainer/qc.sh` runs cuOpt and the API as + two Apptainer instances of the user, bound to `127.0.0.1`. See + [docs/SPARK_APPTAINER.md](docs/SPARK_APPTAINER.md). +* **Docker Compose, with root** (the lecturer's Spark): `deploy/deploy_to_spark.sh --with-data` + adds a `quantcore` service next to the `cuopt` service of an existing compose file and reaches + cuOpt at `http://cuopt:5000`. See [docs/SPARK_DEPLOYMENT.md](docs/SPARK_DEPLOYMENT.md). + +## Tests + +```bash +pytest # 127 tests: reference values for VaR/CVaR, Idzorek closed form vs. numeric, brute-force MILP optimality, + # exact Gaussian mode vs. brute force, row generation vs. full MILP, sampling/moment matching/tilt, + # weighted quantiles and scenario distributions, API (incl. the before/after of a rebalancing), + # the HTTP client and every `quantcore-api` command against the app in-process +``` + +The cuOpt round-trip test runs only when the server is reachable. + +## Layout + +``` +quantcore/ + config.py settings (env QUANTCORE_*, .env) + universe/ candidate sources, Yahoo symbol mapping, SetupService + data/ YahooClient (timeouts/retry/breaker), FX, DataStore, DataDownloadService, MarketDataService, static/ + measurement/ returns, rolling & EWM estimators, risk (VaR/CVaR), performance + models/ CovarianceModel + matrix roots, GBM & empirical-innovation simulators, Black-Litterman/Idzorek + optimization/ MILP builder (problem.py), cuOpt/HiGHS solvers, OptimizationService + api/ FastAPI app + pydantic schemas + api/ui/ the browser test bench (static HTML/CSS/ES modules, no build step, no CDN); + QUANTCORE_ENABLE_UI=false serves the JSON API and /docs only + client.py QuantcoreClient -- the API as a Python object (needs only a URL) + cli.py `quantcore` command (in-process services, needs the data directory) + apicli.py `quantcore-api` command (HTTP client of a running service, incl. `selftest`) +deploy/apptainer/ qc.sh (install/start/stop/cli on a Spark without root), quantcore.def, pinned constraints.txt +deploy/ Dockerfile, compose service blocks (Spark + a bench-free one), deploy_to_spark.sh +docs/ USAGE.md, ARCHITECTURE.md, METHODOLOGY.md, SPARK_APPTAINER.md, SPARK_DEPLOYMENT.md +tests/ pytest suite (synthetic data store fixture) +``` diff --git a/deploy/Dockerfile b/deploy/Dockerfile new file mode 100644 index 0000000..a0bc145 --- /dev/null +++ b/deploy/Dockerfile @@ -0,0 +1,24 @@ +# quantcore -- portfolio quant service for the DGX Spark (aarch64) and any x86-64 host. +# Build context is the repository root: docker build -f deploy/Dockerfile -t local/quantcore:0.1 . +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1 \ + QUANTCORE_DATA_DIR=/data QUANTCORE_CUOPT_URL=http://cuopt:5000 \ + OPENBLAS_NUM_THREADS=4 OMP_NUM_THREADS=4 MKL_NUM_THREADS=4 +# BLAS threading: the innovation filter runs thousands of small (N x N) eigendecompositions; +# OpenBLAS with all 20 cores of the Spark is ~50x slower on those than with 1-4 threads. + +RUN apt-get update && apt-get install -y --no-install-recommends curl tini \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY pyproject.toml README.md ./ +COPY quantcore ./quantcore +RUN pip install . + +VOLUME ["/data"] +EXPOSE 5001 +HEALTHCHECK --interval=60s --timeout=10s --retries=2 CMD curl -sf http://localhost:5001/health >/dev/null || exit 1 + +ENTRYPOINT ["/usr/bin/tini", "--"] +CMD ["uvicorn", "quantcore.api.app:app", "--host", "0.0.0.0", "--port", "5001", "--workers", "1"] diff --git a/deploy/apptainer/constraints.txt b/deploy/apptainer/constraints.txt new file mode 100644 index 0000000..3e5a72a --- /dev/null +++ b/deploy/apptainer/constraints.txt @@ -0,0 +1,48 @@ +# Library versions of the quantcore image, taken from the container that runs on the lecturer's Spark +# (Python 3.12.14, aarch64, 2026-10-07). deploy/apptainer/quantcore.def installs with `pip install -c`, +# so a student's build in December gets the same SciPy/HiGHS, pandas and numpy as the verified one. +# To move to newer libraries: delete the pins, build, run the tests, and freeze again: +# apptainer exec quantcore.sif pip freeze --exclude quantcore +annotated-doc==0.0.5 +annotated-types==0.8.0 +anyio==4.15.1 +beautifulsoup4==4.15.0 +certifi==2026.7.22 +cffi==2.1.1 +charset-normalizer==3.5.1 +click==8.5.0 +curl_cffi==0.16.3 +fastapi==0.141.1 +h11==0.16.0 +httptools==0.8.0 +idna==3.20 +lxml==6.1.3 +msgpack==1.2.2 +multitasking==0.0.13 +numpy==2.5.3 +pandas==3.0.6 +peewee==4.5.1 +platformdirs==4.11.12 +protobuf==7.36.2 +pyarrow==25.0.1 +pycparser==3.0 +pydantic==2.13.5 +pydantic-settings==2.15.0 +pydantic_core==2.46.5 +python-dateutil==2.9.0.post0 +python-dotenv==1.2.3 +pytz==2026.3.post1 +PyYAML==6.0.3 +requests==2.34.2 +scipy==1.18.1 +six==1.17.0 +soupsieve==2.9.2 +starlette==1.7.0 +typing-inspection==0.4.4 +typing_extensions==4.16.0 +urllib3==2.8.0 +uvicorn==0.53.0 +uvloop==0.22.1 +watchfiles==1.3.0 +websockets==17.1 +yfinance==1.7.0 diff --git a/deploy/apptainer/qc.sh b/deploy/apptainer/qc.sh new file mode 100755 index 0000000..3641892 --- /dev/null +++ b/deploy/apptainer/qc.sh @@ -0,0 +1,576 @@ +#!/usr/bin/env bash +# qc.sh -- quantcore standalone on a DGX Spark with Apptainer: no root, no Docker, no Tailscale. +# +# cuOpt and quantcore each run in their own Apptainer container on this machine and talk over +# 127.0.0.1. The API and the browser test bench listen on 127.0.0.1 too, so they are used from this +# Spark (its desktop browser, a terminal, a notebook) and are not reachable from the network. +# +# qc.sh install once: config, cuOpt image, quantcore image, data directory +# qc.sh data-import FILE copy a prepared data set (.tar.gz or directory) ... +# qc.sh setup ... or build it from Yahoo Finance (~15 min) +# qc.sh start cuOpt + API -> http://127.0.0.1:5001/ui/ and /docs +# qc.sh status | logs [api|cuopt] [-f] | stop | restart +# qc.sh cli ... the quantcore CLI in the container, no API needed +# (e.g. qc.sh cli optimize --cash 1e6 --cvar-limit 40000) +# qc.sh api ... quantcore-api against the local API (e.g. qc.sh api selftest) +# qc.sh help all commands +# +# Settings: $QC_HOME/quantcore.env (QC_HOME defaults to ~/quantcore); see quantcore.env.example. +# Guide: docs/SPARK_APPTAINER.md. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +QC_HOME="${QC_HOME:-$HOME/quantcore}" +CONFIG="$QC_HOME/quantcore.env" +APPTAINER="${APPTAINER:-apptainer}" +API_INSTANCE="quantcore-api" +CUOPT_INSTANCE="quantcore-cuopt" +ME="$(id -un)" + +# --- settings: defaults < config file < environment ------------------------------------------ +_env_overrides="$(declare -p $(compgen -v | grep -E '^(QC_|QUANTCORE_)' || true) 2>/dev/null || true)" +QC_DATA="$QC_HOME/data" +QC_IMAGES="$QC_HOME/images" +QC_API_PORT=5001 +QC_CUOPT_PORT=5000 +QC_CUOPT_START=auto +QC_CUOPT_IMAGE="docker://nvidia/cuopt:26.8.0-cuda13.3-py3.14" +QC_IMAGE=build +QC_PYTHON_IMAGE="docker://python:3.12-slim" # base of the no-build fallback (same as quantcore.def) +if [[ -f "$CONFIG" ]]; then + set -a + # shellcheck disable=SC1090 + source "$CONFIG" + set +a +fi +[[ -n "$_env_overrides" ]] && eval "$_env_overrides" +unset _env_overrides +QC_API_URL="http://127.0.0.1:$QC_API_PORT" +QC_VENV="$QC_HOME/venv" +PY_SIF="$QC_IMAGES/python.sif" +CUOPT_URL="${QUANTCORE_CUOPT_URL:-http://127.0.0.1:$QC_CUOPT_PORT}" +CUOPT_EXTERNAL=0 # 1: QUANTCORE_CUOPT_URL names a cuOpt somewhere else -- never start our own +[[ -n "${QUANTCORE_CUOPT_URL:-}" ]] && CUOPT_EXTERNAL=1 + +# --- helpers ---------------------------------------------------------------------------------- +say() { printf '%s\n' "$*"; } +step() { printf '\n>> %s\n' "$*"; } +warn() { printf 'warning: %s\n' "$*" >&2; } +die() { printf 'error: %s\n' "$*" >&2; exit 1; } + +quantcore_sif() { if [[ "$QC_IMAGE" == *.sif && -f "$QC_IMAGE" ]]; then say "$QC_IMAGE"; else say "$QC_IMAGES/quantcore.sif"; fi; } + +# How quantcore runs: "sif" = the quantcore image (built or prebuilt); "venv" = the no-build fallback, +# a plain Python image plus a virtualenv in $QC_HOME/venv (for a Spark that allows no image builds). +qc_mode() { + if [[ "$QC_IMAGE" == "venv" ]]; then say venv; return; fi + if [[ -f "$(quantcore_sif)" ]]; then say sif; return; fi + if [[ -x "$QC_VENV/bin/quantcore" && -f "$PY_SIF" ]]; then say venv; return; fi + say none +} +qc_image() { if [[ "$(qc_mode)" == venv ]]; then say "$PY_SIF"; else quantcore_sif; fi; } +qc_bin() { if [[ "$(qc_mode)" == venv ]]; then say "$QC_VENV/bin/$1"; else say "$1"; fi; } +cuopt_sif() { if [[ -f "$QC_CUOPT_IMAGE" ]]; then say "$QC_CUOPT_IMAGE"; else say "$QC_IMAGES/cuopt.sif"; fi; } + +need_apptainer() { + command -v "$APPTAINER" >/dev/null 2>&1 \ + || die "'$APPTAINER' not found. This Spark needs Apptainer (ask the administrator), or set APPTAINER=singularity." +} + +need_image() { # path what + [[ -f "$1" ]] || die "$2 image missing ($1) -- run: $0 install" +} + +# HTTP GET to stdout; exit status 0 only for a 2xx answer. curl, else python3 (both on DGX OS). +http_get() { + local url="$1" timeout="${2:-3}" + if command -v curl >/dev/null 2>&1; then + curl -sf --max-time "$timeout" "$url" + elif command -v python3 >/dev/null 2>&1; then + python3 -c 'import sys, urllib.request as u; sys.stdout.write(u.urlopen(sys.argv[1], timeout=float(sys.argv[2])).read().decode())' \ + "$url" "$timeout" 2>/dev/null + else + return 2 + fi +} + +port_busy() { (exec 3<>"/dev/tcp/127.0.0.1/$1") 2>/dev/null; } + +free_port() { # first free port from $1 upwards + local p="$1" + while port_busy "$p"; do p=$((p + 1)); done + say "$p" +} + +instance_running() { + "$APPTAINER" instance list 2>/dev/null | awk 'NR > 1 {print $1}' | grep -qx "$1" +} + +instance_log() { # name out|err -> path of the instance log (Apptainer keeps them per host and user) + local f + for f in "$HOME/.apptainer/instances/logs/"*"/$ME/$1.$2"; do + [[ -f "$f" ]] && { say "$f"; return 0; } + done + return 1 +} + +show_log_tail() { # name + local f + for kind in err out; do + if f="$(instance_log "$1" "$kind")"; then + say "--- last lines of $f" + tail -n 25 "$f" >&2 || true + fi + done +} + +wait_for() { # url seconds label + local url="$1" limit="$2" label="$3" waited=0 + printf ' waiting for %s ' "$label" + until http_get "$url" 2 >/dev/null; do + if (( waited >= limit )); then printf ' timed out after %ss\n' "$limit"; return 1; fi + printf '.'; sleep 2; waited=$((waited + 2)) + done + printf ' up (%ss)\n' "$waited" +} + +# Hand the settings to a container through APPTAINERENV_* (unlike --env these survive commas, +# e.g. in no_proxy). The containers start with --cleanenv, so nothing else from the host leaks in: +# no PYTHONPATH, no activated virtualenv or conda, no LD_LIBRARY_PATH. +export_quantcore_env() { + local v + for v in $(compgen -v | grep '^QUANTCORE_' || true); do + export "APPTAINERENV_$v=${!v}" + done + export APPTAINERENV_QUANTCORE_DATA_DIR=/data + export APPTAINERENV_QUANTCORE_CUOPT_URL="$CUOPT_URL" + export APPTAINERENV_QUANTCORE_API_HOST=127.0.0.1 + export APPTAINERENV_QUANTCORE_API_PORT="$QC_API_PORT" + export APPTAINERENV_QUANTCORE_API_URL="$QC_API_URL" + # what quantcore.def's %environment sets, for the venv fallback (an explicit value wins) + export APPTAINERENV_PYTHONNOUSERSITE=1 APPTAINERENV_PYTHONUNBUFFERED=1 APPTAINERENV_PYTHONDONTWRITEBYTECODE=1 + export APPTAINERENV_OPENBLAS_NUM_THREADS="${OPENBLAS_NUM_THREADS:-4}" APPTAINERENV_OMP_NUM_THREADS="${OMP_NUM_THREADS:-4}" + export APPTAINERENV_MKL_NUM_THREADS="${MKL_NUM_THREADS:-4}" + for v in http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY TZ; do + [[ -n "${!v:-}" ]] && export "APPTAINERENV_$v=${!v}" + done + return 0 +} + +# Run a command in the quantcore container (the CLI needs no running API). +qc_binds() { # bind options for the quantcore container + local b=(--bind "$QC_DATA:/data") + [[ "$(qc_mode)" == venv ]] && b+=(--bind "$QC_VENV") + printf '%s\n' "${b[@]}" +} + +run_qc() { # command [args]: quantcore | quantcore-api | python ... + need_apptainer + [[ "$(qc_mode)" != none ]] || die "no quantcore image yet -- run: $0 install" + mkdir -p "$QC_DATA" + export_quantcore_env + local cmd="$1"; shift + local binds; mapfile -t binds < <(qc_binds) + exec "$APPTAINER" exec --cleanenv "${binds[@]}" "$(qc_image)" "$(qc_bin "$cmd")" "$@" +} + +data_is_empty() { [[ -z "$(ls -A "$QC_DATA" 2>/dev/null | grep -v -x -e jobs -e '.*\.log')" ]]; } + +# --- install ---------------------------------------------------------------------------------- +cmd_install() { + need_apptainer + local force=0; [[ "${1:-}" == "--force" ]] && force=1 + step "Apptainer" + say " $("$APPTAINER" --version)" + "$APPTAINER" instance run --help >/dev/null 2>&1 \ + || die "this Apptainer has no 'instance run' (needs Apptainer >= 1.1); ask the administrator to update it" + + step "Directories and settings ($QC_HOME)" + mkdir -p "$QC_HOME" "$QC_DATA" "$QC_IMAGES" + if [[ -f "$CONFIG" ]]; then + say " keeping $CONFIG" + else + cp "$SCRIPT_DIR/quantcore.env.example" "$CONFIG" + say " wrote $CONFIG" + fi + + step "GPU" + if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then + say " $(nvidia-smi -L | head -n 1)" + else + warn "no GPU visible (nvidia-smi). cuOpt will not start; optimisations will use HiGHS on the CPU." + fi + local free_gb; free_gb=$(( $(df -Pk "$QC_IMAGES" | awk 'NR == 2 {print $4}') / 1024 / 1024 )) + say " free disk under $QC_IMAGES: ${free_gb} GB (the cuOpt pull needs ~12 GB while it runs, 3.3 GB after)" + + if [[ "$QC_CUOPT_START" != "no" && "$CUOPT_EXTERNAL" == 0 ]]; then + cmd_pull_cuopt $([[ $force == 1 ]] && echo --force) + fi + step "quantcore image" + get_quantcore_image "$force" + step "Self-test of the quantcore container ($(qc_mode) mode)" + qc_selftest + + step "Data ($QC_DATA)" + if data_is_empty; then + say " empty. Either copy a prepared data set: $0 data-import quantcore_data.tar.gz" + say " or build it from Yahoo Finance (~15 min): $0 setup" + else + say " present ($(du -sh "$QC_DATA" | cut -f1))" + fi + step "Done. Next: $0 start" +} + +cmd_pull_cuopt() { + need_apptainer + local dst="$QC_IMAGES/cuopt.sif" + if [[ -f "$QC_CUOPT_IMAGE" ]]; then + step "cuOpt image: $QC_CUOPT_IMAGE (provided on this machine, not pulled)" + return 0 + fi + if [[ -f "$dst" && "${1:-}" != "--force" ]]; then + step "cuOpt image present: $dst" + return 0 + fi + step "Pulling cuOpt: $QC_CUOPT_IMAGE -> $dst (~3.3 GB, several minutes)" + mkdir -p "$QC_IMAGES" + rm -f "$dst.part" + "$APPTAINER" pull "$dst.part" "$QC_CUOPT_IMAGE" + mv -f "$dst.part" "$dst" +} + +qc_selftest() { + ( export_quantcore_env + local binds; mapfile -t binds < <(qc_binds) + "$APPTAINER" exec --cleanenv "${binds[@]}" "$(qc_image)" "$(qc_bin python)" -c ' +import importlib.resources as r, scipy, quantcore, quantcore.api.app as app +from scipy.optimize import milp +assert (r.files("quantcore.api") / "ui" / "index.html").is_file(), "test bench missing" +print(" quantcore", quantcore.__version__, "| scipy", scipy.__version__, "| UI mounted:", app.UI_ENABLED)' ) +} + +get_quantcore_image() { # force(0|1) + local dst="$QC_IMAGES/quantcore.sif" + case "$QC_IMAGE" in + build) + if [[ -f "$dst" && "$1" != 1 ]]; then say " present: $dst (rebuild: $0 build)"; return 0; fi + if [[ -x "$QC_VENV/bin/quantcore" && -f "$PY_SIF" && "$1" != 1 ]]; then + say " present: virtualenv $QC_VENV (no-build fallback; reinstall: $0 build)"; return 0 + fi + build_sif || { + say "" + say " The image build is not possible on this Spark without root. Falling back to the no-build" + say " setup: a plain Python image plus a virtualenv in $QC_VENV (same libraries, same results)." + build_venv + } ;; + venv) + if [[ -x "$QC_VENV/bin/quantcore" && -f "$PY_SIF" && "$1" != 1 ]]; then say " present: $QC_VENV"; else build_venv; fi ;; + docker://*|oras://*|library://*|http://*|https://*) + if [[ -f "$dst" && "$1" != 1 ]]; then say " present: $dst"; else + say " pulling $QC_IMAGE -> $dst" + rm -f "$dst.part"; "$APPTAINER" pull "$dst.part" "$QC_IMAGE"; mv -f "$dst.part" "$dst" + fi ;; + *) + [[ -f "$QC_IMAGE" ]] || die "QC_IMAGE=$QC_IMAGE is neither 'build', a URL, nor an existing .sif file" + say " using $QC_IMAGE" ;; + esac +} + +build_sif() { # build quantcore.sif from the definition; non-zero when this Apptainer cannot build + local dst="$QC_IMAGES/quantcore.sif" + mkdir -p "$QC_IMAGES" + say " building $dst from $REPO_DIR (a few minutes)" + rm -f "$dst.part" + # %files paths in the definition are relative to the repository root. Without root Apptainer + # builds through --fakeroot (user namespaces); a Spark may not allow that. + if ! (cd "$REPO_DIR" && "$APPTAINER" build "$dst.part" deploy/apptainer/quantcore.def); then + rm -f "$dst.part" + return 1 + fi + mv -f "$dst.part" "$dst" + say " built $dst ($(du -h "$dst" | cut -f1))" +} + +build_venv() { # the no-build fallback: pull a plain Python image, install quantcore into a virtualenv + mkdir -p "$QC_IMAGES" "$QC_HOME" + if [[ ! -f "$PY_SIF" ]]; then + say " pulling $QC_PYTHON_IMAGE -> $PY_SIF" + rm -f "$PY_SIF.part"; "$APPTAINER" pull "$PY_SIF.part" "$QC_PYTHON_IMAGE"; mv -f "$PY_SIF.part" "$PY_SIF" + fi + say " installing quantcore into $QC_VENV (pinned versions from deploy/apptainer/constraints.txt)" + local work; work="$(mktemp -d "${TMPDIR:-/tmp}/quantcore-src.XXXXXX")" + # install from a copy: pip writes build/ and *.egg-info next to the sources + cp -r "$REPO_DIR/pyproject.toml" "$REPO_DIR/README.md" "$REPO_DIR/quantcore" "$work/" + "$APPTAINER" exec --cleanenv --bind "$QC_HOME" --bind "$work" "$PY_SIF" python -m venv --clear "$QC_VENV" + "$APPTAINER" exec --cleanenv --env PIP_DISABLE_PIP_VERSION_CHECK=1 --bind "$QC_HOME" --bind "$work" \ + --bind "$REPO_DIR/deploy/apptainer/constraints.txt:/tmp/qc-constraints.txt" "$PY_SIF" \ + "$QC_VENV/bin/pip" install --quiet --no-cache-dir -c /tmp/qc-constraints.txt "$work" + rm -rf "$work" + say " installed: $("$APPTAINER" exec --cleanenv --bind "$QC_VENV" "$PY_SIF" "$QC_VENV/bin/quantcore" --help >/dev/null && echo ok)" +} + +cmd_build() { # rebuild after a code change, in the mode in use + need_apptainer + local mode; mode="$(qc_mode)" + step "Rebuilding quantcore ($([[ $mode == venv ]] && echo "virtualenv $QC_VENV" || echo "image $QC_IMAGES/quantcore.sif"))" + if [[ "$mode" == venv ]]; then + build_venv + elif [[ "$QC_IMAGE" == build ]]; then + build_sif || die "the image build failed (see above). Without root it needs Apptainer's --fakeroot. Use the + no-build fallback instead: set QC_IMAGE=venv in $CONFIG and run: $0 build" + else + die "QC_IMAGE=$QC_IMAGE is a prebuilt image; nothing to build" + fi + if instance_running "$API_INSTANCE"; then say " the running API still uses the old code: $0 restart"; fi +} + +# --- run -------------------------------------------------------------------------------------- +start_cuopt() { + if [[ "$CUOPT_EXTERNAL" == 1 ]]; then + if http_get "$CUOPT_URL/cuopt/health" >/dev/null; then say "cuOpt: using $CUOPT_URL (QUANTCORE_CUOPT_URL)" + else warn "cuOpt at $CUOPT_URL (QUANTCORE_CUOPT_URL) does not answer -- optimisations will use HiGHS"; fi + return 0 + fi + if [[ "$QC_CUOPT_START" == "no" ]]; then + say "cuOpt: not started (QC_CUOPT_START=no) -- optimisations use HiGHS on the CPU" + return 0 + fi + if http_get "$CUOPT_URL/cuopt/health" >/dev/null; then + if instance_running "$CUOPT_INSTANCE"; then + say "cuOpt: running ($CUOPT_INSTANCE, $CUOPT_URL)" + elif [[ "$QC_CUOPT_START" == "yes" ]]; then + die "QC_CUOPT_START=yes, but another cuOpt already serves port $QC_CUOPT_PORT. Use it (QC_CUOPT_START=auto) + or pick another port: QC_CUOPT_PORT=$(free_port $((QC_CUOPT_PORT + 100))) in $CONFIG" + else + say "cuOpt: using the server that already answers at $CUOPT_URL" + fi + return 0 + fi + if instance_running "$CUOPT_INSTANCE"; then # still starting, or hung + wait_for "$CUOPT_URL/cuopt/health" 180 "cuOpt ($CUOPT_INSTANCE)" && return 0 + show_log_tail "$CUOPT_INSTANCE" + die "cuOpt instance runs but does not answer; try: $0 restart" + fi + if port_busy "$QC_CUOPT_PORT"; then + die "port $QC_CUOPT_PORT is taken by something that is not cuOpt. + Set QC_CUOPT_PORT=$(free_port $((QC_CUOPT_PORT + 100))) in $CONFIG and run: $0 start" + fi + local sif; sif="$(cuopt_sif)" + need_image "$sif" cuOpt + say "cuOpt: starting instance $CUOPT_INSTANCE on 127.0.0.1:$QC_CUOPT_PORT (GPU)" + # NVIDIA's entrypoint sets HOME=/opt/cuopt. That is writable under Docker but read-only in a SIF, + # and the solver process dies on its first job when CuPy cannot write its kernel cache there. + # So the server runs with HOME on a writable directory of ours (CuPy, Numba and CUDA caches). + local cuopt_home="$QC_HOME/cuopt-home" + mkdir -p "$cuopt_home" + ( [[ -n "${CUDA_VISIBLE_DEVICES:-}" ]] && export APPTAINERENV_CUDA_VISIBLE_DEVICES="$CUDA_VISIBLE_DEVICES" + cd / + # the image's entrypoint runs its arguments: the cuOpt REST server, on localhost only + "$APPTAINER" instance run --nv --cleanenv --bind "$cuopt_home" "$sif" "$CUOPT_INSTANCE" \ + env HOME="$cuopt_home" python -m cuopt_server.cuopt_service \ + -i 127.0.0.1 -p "$QC_CUOPT_PORT" -l "${QC_CUOPT_LOG_LEVEL:-info}" >/dev/null ) + if ! wait_for "$CUOPT_URL/cuopt/health" 180 "cuOpt"; then + show_log_tail "$CUOPT_INSTANCE" + warn "cuOpt did not come up -- the API starts anyway and optimises with HiGHS" + fi +} + +start_api() { + if instance_running "$API_INSTANCE"; then + if http_get "$QC_API_URL/health" >/dev/null; then say "API: running ($API_INSTANCE, $QC_API_URL)"; return 0; fi + wait_for "$QC_API_URL/health" 60 "API ($API_INSTANCE)" && return 0 + show_log_tail "$API_INSTANCE" + die "the API instance runs but does not answer; try: $0 restart" + fi + if port_busy "$QC_API_PORT"; then + die "port $QC_API_PORT is taken (another user's quantcore?). + Set QC_API_PORT=$(free_port $((QC_API_PORT + 10))) in $CONFIG and run: $0 start" + fi + local mode; mode="$(qc_mode)" + [[ "$mode" != none ]] || die "no quantcore image yet -- run: $0 install" + mkdir -p "$QC_DATA" + say "API: starting instance $API_INSTANCE on $QC_API_URL ($mode mode)" + ( export_quantcore_env + local binds; mapfile -t binds < <(qc_binds) + # start from /: pydantic-settings reads a .env from the working directory, and the container's + # / has none (instances take the caller's directory; they have no --cwd option) + cd / + if [[ "$mode" == venv ]]; then # the Python image's runscript executes its arguments + "$APPTAINER" instance run --cleanenv "${binds[@]}" "$PY_SIF" "$API_INSTANCE" "$QC_VENV/bin/quantcore" serve >/dev/null + else # quantcore.def's startscript: quantcore serve + "$APPTAINER" instance start --cleanenv "${binds[@]}" "$(quantcore_sif)" "$API_INSTANCE" >/dev/null + fi ) + if ! wait_for "$QC_API_URL/health" 90 "API"; then + show_log_tail "$API_INSTANCE" + die "the API did not come up" + fi +} + +cmd_start() { + need_apptainer + step "Starting quantcore (user $ME, settings $([[ -f "$CONFIG" ]] && echo "$CONFIG" || echo "defaults"))" + start_cuopt + start_api + data_is_empty && warn "no market data yet: $0 data-import or $0 setup" + local ui=""; [[ "${QUANTCORE_ENABLE_UI:-true}" != "false" ]] && ui="$QC_API_URL/ui/ (test bench)" + step "Ready -- on this Spark:" + [[ -n "$ui" ]] && say " $ui" + say " $QC_API_URL/docs (API, Swagger)" + say " Python: QuantcoreClient(\"$QC_API_URL\") CLI: $0 api health" +} + +cmd_stop() { + need_apptainer + local name stopped=0 + for name in "$API_INSTANCE" "$CUOPT_INSTANCE"; do + if instance_running "$name"; then + "$APPTAINER" instance stop "$name" >/dev/null && say "stopped $name" && stopped=1 + fi + done + [[ $stopped == 1 ]] || say "nothing running" +} + +cmd_status() { + need_apptainer + step "Instances of $ME" + "$APPTAINER" instance list + step "cuOpt $CUOPT_URL" + local h + if h="$(http_get "$CUOPT_URL/cuopt/health")"; then say " up: $h"; else say " not answering"; fi + step "API $QC_API_URL" + if h="$(http_get "$QC_API_URL/health")"; then + if command -v python3 >/dev/null 2>&1; then + printf '%s' "$h" | python3 -c ' +import json, sys +h = json.load(sys.stdin); s = h.get("store", {}); c = h.get("cuopt", {}) +print(" up: version", h.get("version"), "| base currency", h.get("base_currency")) +print(" data:", {k: s[k] for k in list(s)[:6]}) +print(" cuOpt seen by the API:", "reachable" if c.get("reachable") else "NOT reachable -> HiGHS")' + else + say " up: $h" + fi + else + say " not answering" + fi + step "Files" + say " settings: $CONFIG $([[ -f "$CONFIG" ]] || echo '(missing: defaults in use)')" + say " data: $QC_DATA ($(du -sh "$QC_DATA" 2>/dev/null | cut -f1 || echo -))" + say " quantcore: $(qc_mode) mode, $(qc_image)$([[ $(qc_mode) == venv ]] && echo " + $QC_VENV")" + say " cuOpt: $(cuopt_sif)" +} + +cmd_logs() { + local which="${1:-api}" follow="" + [[ "${2:-}" == "-f" || "${1:-}" == "-f" ]] && follow=1 + [[ "$which" == "-f" ]] && which=api + local name="$API_INSTANCE"; [[ "$which" == "cuopt" ]] && name="$CUOPT_INSTANCE" + local out err + out="$(instance_log "$name" out || true)"; err="$(instance_log "$name" err || true)" + [[ -n "$out$err" ]] || die "no logs for $name yet" + if [[ -n "$follow" ]]; then exec tail -n 50 -F ${out:+"$out"} ${err:+"$err"}; fi + tail -n 50 ${out:+"$out"} ${err:+"$err"} +} + +# --- data ------------------------------------------------------------------------------------- +data_job() { # setup|update [args]: through the running API (which then reloads), else in-process + local job="$1"; shift + if http_get "$QC_API_URL/health" >/dev/null; then + say "the API is running: the $job job runs there, and the API reloads the data when it is done" + run_qc quantcore-api --url "$QC_API_URL" "$job" --wait "$@" + else + run_qc quantcore "$job" "$@" + fi +} + +cmd_data_import() { + local src="${1:-}" force="${2:-}" + [[ -n "$src" ]] || die "usage: $0 data-import [--force]" + [[ -e "$src" ]] || die "$src not found" + mkdir -p "$QC_DATA" + if ! data_is_empty && [[ "$force" != "--force" ]]; then + die "$QC_DATA already holds data; add --force to overwrite it" + fi + if [[ -d "$src" ]]; then + cp -a "$src/." "$QC_DATA/" + else + tar -xzf "$src" -C "$QC_DATA" + fi + say "imported into $QC_DATA ($(du -sh "$QC_DATA" | cut -f1))" + if instance_running "$API_INSTANCE"; then say "the running API still holds the old data: $0 restart"; fi +} + +cmd_data_export() { + local dst="${1:-quantcore_data.tar.gz}" + [[ "$dst" = /* ]] || dst="$PWD/$dst" + tar -czf "$dst" -C "$QC_DATA" --exclude './jobs' --exclude '*.log' . + say "wrote $dst ($(du -h "$dst" | cut -f1))" +} + +cmd_env() { + cat < [options] + The startscript serves the HTTP API: apptainer instance start quantcore.sif quantcore-api + -> http://127.0.0.1:5001/ui/ (test bench) and /docs (Swagger); bound to localhost only. + + Bind a writable data directory to /data (QUANTCORE_DATA_DIR). cuOpt is expected at + QUANTCORE_CUOPT_URL (default http://127.0.0.1:5000); without it every optimisation uses HiGHS. + All QUANTCORE_* variables of quantcore/config.py can be passed with --env. + +%files + pyproject.toml /opt/quantcore-src/pyproject.toml + README.md /opt/quantcore-src/README.md + quantcore /opt/quantcore-src/quantcore + deploy/apptainer/constraints.txt /opt/quantcore-src/constraints.txt + +%post + set -eu + export PIP_NO_CACHE_DIR=1 PIP_DISABLE_PIP_VERSION_CHECK=1 PIP_ROOT_USER_ACTION=ignore + # constraints.txt pins the library versions the image was verified with (SciPy decides the HiGHS + # version, and with it which of several equally good integer solutions HiGHS returns). + python -m pip install -c /opt/quantcore-src/constraints.txt /opt/quantcore-src + rm -rf /opt/quantcore-src + find /usr/local/lib -name '__pycache__' -type d -prune -exec rm -rf {} + 2>/dev/null || true + mkdir -p /data + python -c "import quantcore, scipy; print('quantcore', quantcore.__version__, '| scipy', scipy.__version__)" + +%environment + export PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 + # a student's own pip --user packages must not shadow the image's libraries + export PYTHONNOUSERSITE=1 + unset PYTHONPATH PYTHONHOME VIRTUAL_ENV + # small-matrix linear algebra (the innovation filter) is ~50x slower with all 20 cores + export OPENBLAS_NUM_THREADS="${OPENBLAS_NUM_THREADS:-4}" OMP_NUM_THREADS="${OMP_NUM_THREADS:-4}" + export MKL_NUM_THREADS="${MKL_NUM_THREADS:-4}" + export QUANTCORE_DATA_DIR="${QUANTCORE_DATA_DIR:-/data}" + export QUANTCORE_CUOPT_URL="${QUANTCORE_CUOPT_URL:-http://127.0.0.1:5000}" + export QUANTCORE_API_HOST="${QUANTCORE_API_HOST:-127.0.0.1}" + export QUANTCORE_API_PORT="${QUANTCORE_API_PORT:-5001}" + +%runscript + exec quantcore "$@" + +%startscript + exec quantcore serve + +%test + set -eu + python - <<'PY' +import importlib.resources as r +import quantcore, quantcore.api.app as app # imports FastAPI app, all services and the UI mount +from scipy.optimize import milp # HiGHS, the CPU fallback solver +assert (r.files("quantcore.api") / "ui" / "index.html").is_file(), "test bench missing from the wheel" +print("quantcore", quantcore.__version__, "ok; UI mounted:", app.UI_ENABLED) +PY diff --git a/deploy/apptainer/quantcore.env.example b/deploy/apptainer/quantcore.env.example new file mode 100644 index 0000000..d92303c --- /dev/null +++ b/deploy/apptainer/quantcore.env.example @@ -0,0 +1,40 @@ +# Settings of deploy/apptainer/qc.sh -- quantcore standalone on a DGX Spark with Apptainer. +# `qc.sh install` copies this file to $QC_HOME/quantcore.env (QC_HOME defaults to ~/quantcore). +# qc.sh reads it on every call; after a change run `qc.sh restart`. A variable that is also set in +# the shell environment wins over this file (e.g. `QC_API_PORT=5011 qc.sh start`). + +# --- where things live ------------------------------------------------------------------------ +#QC_DATA=$HOME/quantcore/data # market data: universe, prices, FX, snapshots (~40 MB) +#QC_IMAGES=$HOME/quantcore/images # quantcore.sif (~0.2 GB) and cuopt.sif (~3.3 GB) + +# --- network: nothing leaves this machine ----------------------------------------------------- +# Both servers listen on 127.0.0.1 only. Ports are shared by every user of this Spark: if +# `qc.sh start` reports a port as taken, pick another one here (it suggests a free one). +QC_API_PORT=5001 # quantcore API + test bench: http://127.0.0.1:5001/ui/ +QC_CUOPT_PORT=5000 # cuOpt server + +# --- cuOpt: the GPU MILP solver, in its own container ----------------------------------------- +# auto: use a cuOpt that already answers on QC_CUOPT_PORT (another user's or a shared one), +# otherwise start our own instance on the GPU +# yes: always start our own instance (fails if the port is taken) +# no: no cuOpt -- every optimisation runs on HiGHS (CPU, same model, same answer) +QC_CUOPT_START=auto +# Official NVIDIA image (arm64 + amd64), pulled once without root. If the Spark already provides a +# cuOpt image, put the path of its .sif file here instead. +QC_CUOPT_IMAGE=docker://nvidia/cuopt:26.8.0-cuda13.3-py3.14 + +# --- the quantcore image ---------------------------------------------------------------------- +# build: build it from this repository (Apptainer's unprivileged --fakeroot build); if this +# Spark does not allow builds, install falls back to 'venv' by itself +# venv: no build: a plain Python image + a virtualenv in $QC_HOME/venv (same libraries) +# /path/x.sif: a prebuilt image (e.g. handed out by the lecturer) -- used in place, not copied +# docker://... or oras://... to pull a published one +QC_IMAGE=build + +# --- passed unchanged into the quantcore container (see quantcore/config.py) ------------------- +QUANTCORE_CUOPT_TIME_LIMIT=300 # seconds per MILP +QUANTCORE_ENABLE_UI=true # false: JSON API and /docs only, no test bench at /ui/ +QUANTCORE_LOG_LEVEL=INFO +#QUANTCORE_BASE_CURRENCY=CHF +#QUANTCORE_CUOPT_URL=http://127.0.0.1:5000 # set only to use a cuOpt at another address; then + # qc.sh never starts its own diff --git a/deploy/deploy_to_spark.sh b/deploy/deploy_to_spark.sh new file mode 100755 index 0000000..343bcd0 --- /dev/null +++ b/deploy/deploy_to_spark.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Deploy the quant service to the DGX Spark: +# 1. rsync the repository to ~/services/work/Portfolio_Risk_Optimization +# 2. optionally rsync an already built data directory (saves the 15-minute setup on the Spark) +# 3. add the `quantcore` service block to ~/services/docker-compose.yml (idempotent) +# 4. build the image and start the container +# +# Usage: deploy/deploy_to_spark.sh [--with-data] [--host user_fh_fhgr@spark-6d4a] +set -euo pipefail + +HOST="user_fh_fhgr@spark-6d4a" +WITH_DATA=0 +for arg in "$@"; do + case "$arg" in + --with-data) WITH_DATA=1 ;; + --host=*) HOST="${arg#--host=}" ;; + --host) shift; HOST="$1" ;; + esac +done + +REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" +REMOTE_SERVICES="services" +REMOTE_REPO="$REMOTE_SERVICES/work/Portfolio_Risk_Optimization" +REMOTE_DATA="$REMOTE_SERVICES/work/quantcore_data" + +echo ">> rsync repository -> $HOST:$REMOTE_REPO" +ssh "$HOST" "mkdir -p $REMOTE_REPO $REMOTE_DATA" +rsync -az --delete \ + --exclude '.git' --exclude '/data/' --exclude '__pycache__' --exclude '*.pyc' --exclude '.pytest_cache' \ + --exclude '*.egg-info' --exclude '.ruff_cache' --exclude 'scratch/' \ + "$REPO_DIR/" "$HOST:$REMOTE_REPO/" + +if [[ "$WITH_DATA" == "1" && -d "$REPO_DIR/data" ]]; then + echo ">> rsync data directory -> $HOST:$REMOTE_DATA" + rsync -az --exclude 'jobs/' --exclude '*.log' "$REPO_DIR/data/" "$HOST:$REMOTE_DATA/" +fi + +echo ">> register the service in ~/services/docker-compose.yml (if missing)" +ssh "$HOST" bash -s <<'REMOTE' +set -euo pipefail +cd ~/services +if ! grep -q '^ quantcore:' docker-compose.yml; then + cp docker-compose.yml "docker-compose.yml.bak.$(date +%Y%m%d%H%M%S)" + # append the service block (everything below "services:" of the snippet) + awk 'f{print} /^services:/{f=1}' work/Portfolio_Risk_Optimization/deploy/docker-compose.quantcore.yml >> docker-compose.yml + echo " added quantcore service block" +else + echo " quantcore service already present" +fi +docker compose build quantcore +docker compose up -d quantcore +sleep 3 +docker compose ps quantcore +curl -s http://localhost:5001/health | head -c 600; echo +REMOTE + +echo ">> done. API: http://spark-6d4a:5001/docs" diff --git a/deploy/docker-compose.quantcore.yml b/deploy/docker-compose.quantcore.yml new file mode 100644 index 0000000..5a4ebc2 --- /dev/null +++ b/deploy/docker-compose.quantcore.yml @@ -0,0 +1,26 @@ +# Service block for the DGX Spark's ~/services/docker-compose.yml (deploy/deploy_to_spark.sh +# appends it). Standalone use: docker compose -f deploy/docker-compose.quantcore.yml up -d +# +# The service reaches cuOpt through the compose network (http://cuopt:5000) and keeps all its +# data under ~/services/work/quantcore_data on the host (survives rebuilds and restarts). +services: + quantcore: + build: + context: ./work/Portfolio_Risk_Optimization # the rsynced repository + dockerfile: deploy/Dockerfile + image: local/quantcore:0.1 + container_name: quantcore + restart: unless-stopped + ports: ["5001:5001"] + environment: + QUANTCORE_DATA_DIR: /data + QUANTCORE_CUOPT_URL: http://cuopt:5000 + QUANTCORE_CUOPT_TIME_LIMIT: "300" + QUANTCORE_LOG_LEVEL: INFO + QUANTCORE_ENABLE_UI: "true" # "false" for an installation whose users bring their own front end + OPENBLAS_NUM_THREADS: "4" # small-matrix linear algebra is much slower with 20 threads + OMP_NUM_THREADS: "4" + volumes: + - ./work/quantcore_data:/data + depends_on: + - cuopt diff --git a/deploy/docker-compose.students.yml b/deploy/docker-compose.students.yml new file mode 100644 index 0000000..2762f21 --- /dev/null +++ b/deploy/docker-compose.students.yml @@ -0,0 +1,30 @@ +# quantcore for a platform team: the JSON API and /docs, no test bench. +# +# docker compose -f deploy/docker-compose.students.yml up -d --build +# curl http://localhost:5001/health +# +# The MILP solver is optional. Point QUANTCORE_CUOPT_URL at a cuOpt server if you have one (on a DGX +# Spark: http://:5000); if it is unreachable, every optimisation falls back to HiGHS on the CPU +# -- slower on large problems, same model, same answer, and the result says which solver produced it. +# +# The data directory is mounted, not baked in: run the setup once (POST /jobs/setup, ~15 min) or copy +# a prepared ./data next to this file. +services: + quantcore: + build: + context: .. + dockerfile: deploy/Dockerfile + image: quantcore:0.1 + container_name: quantcore + restart: unless-stopped + ports: ["5001:5001"] + environment: + QUANTCORE_DATA_DIR: /data + QUANTCORE_CUOPT_URL: ${QUANTCORE_CUOPT_URL:-http://cuopt:5000} + QUANTCORE_CUOPT_TIME_LIMIT: "300" + QUANTCORE_ENABLE_UI: "false" # the test bench is not part of the deliverable + QUANTCORE_LOG_LEVEL: INFO + OPENBLAS_NUM_THREADS: "4" # small-matrix linear algebra is much slower with all cores + OMP_NUM_THREADS: "4" + volumes: + - ../data:/data diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..3033e02 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,125 @@ +# Architecture + +`quantcore` is the **deterministic core** of the platform sketched in the AISE502 project brief: +a set of services with plain contracts, no LLM inside, resilient against external data outages, +and reproducible through pinned data snapshots. The AI layer (news ingestion, `ResearchAgent`, +`AdvisorAgent`, …) sits *on top* and only reads these services through their contracts. + +``` + ┌──────────────────────────────────────────────────────────────────┐ + HTTP/JSON │ quantcore.api (FastAPI) – thin, no logic, background jobs │ + └───────┬───────────────┬───────────────┬──────────────┬───────────┘ + │ │ │ │ + ┌─────────────────▼──┐ ┌─────────▼─────────┐ ┌──▼──────────┐ ┌▼────────────────────┐ + │ SetupService │ │ RiskService │ │ Performance │ │ OptimizationService │ + │ DataDownloadService │ │ (measurement.risk) │ │ Service │ │ (optimization) │ + └─────────┬───────────┘ └─────────┬─────────┘ └──┬──────────┘ └┬───────────┬────────┘ + │ │ │ │ │ + ┌─────────▼───────────┐ ┌─────────▼───────────────▼──────────┐ │ ┌───────▼────────────┐ + │ YahooClient (ext.) │ │ MarketDataService │◄──┘ │ models: │ + │ timeout · retry · │ │ prices · FX → CHF · returns · │ │ CovarianceModel │ + │ circuit breaker │ │ market caps · cap-weighted index │ │ GBM / innovations │ + └─────────┬───────────┘ └─────────┬──────────────────────────┘ │ Black-Litterman │ + │ │ └───────┬────────────┘ + ┌─────────▼────────────────────────▼──────────┐ ┌────────────────▼────────────┐ + │ DataStore (parquet/csv under data_dir, │ │ MeanCVaRProblem → MILPModel │ + │ manifest, snapshots) │ │ CuOptSolver ⇄ HighsSolver │ + └─────────────────────────────────────────────┘ └──────────────┬──────────────┘ + │ REST + ┌─────────────▼─────────────┐ + │ cuOpt server (DGX Spark) │ + └───────────────────────────┘ +``` + +## Bounded contexts → modules + +| Context | Module | Contract (inputs → outputs) | +|---|---|---| +| Universe | `quantcore.universe` | web candidate lists → `universe.csv` (symbol, name, region, country, currency, exchange, GICS sector, Yahoo sector/industry, market cap local & CHF, shares outstanding, first price date, coverage) | +| Market data | `quantcore.data` | Yahoo → `adj_close.parquet` (date × symbol, listing currency), `fx_chf.parquet` (date × currency); `MarketDataService.prices/returns(symbols, start, end, currency, freq)` → aligned frames | +| Measurement | `quantcore.measurement` | returns frame → risk tables, series, matrices, `PortfolioRisk`; performance tables | +| Models | `quantcore.models` | returns frame → covariance, `ScenarioSet` (S × N horizon log-returns, optional probabilities, fingerprint; `sampling`: Sobol'/moment matching/importance sampling), `BlackLittermanResult` | +| Optimisation | `quantcore.optimization` | `OptimizationRequest` → `OptimizationResult` (shares, trades, cash, costs, CVaR, exposures, provenance of every model input, scenario hash, reproducibility level); `problem` (MILP, weighted scenarios or Gaussian cuts), `rowgen` (tail-row generation), `gaussian_exact` (outer approximation), `solvers` | +| Interface | `quantcore.api`, `quantcore.cli` | JSON/CLI surfaces of the above; no computation of their own | +| Test bench | `quantcore/api/ui` | a static single-page client of the JSON API (`/ui/`): stock data, covariance and scenarios, views, constraints, optimisation with before/after. Static HTML + ES modules, no build step and no CDN, so it also runs in the offline container. It formats and charts what the services return and computes nothing itself | + +Dependencies point downwards only: `api → optimization → models → measurement → data → config`. +`measurement` and `models` never touch storage or the network; they operate on DataFrames handed +to them, which is what makes them pure and testable with reference vectors. + +## Deterministic by construction + +* Every stochastic step takes an explicit `seed` (scenario generation: scrambled Sobol' points, + moment matched — the same request yields the same scenario matrix, fingerprinted as + `scenario_hash`; bit-identical within one environment, while across environments the *key* matches + and the digest need not (§9 of `docs/METHODOLOGY.md` measures why); the *scenario key* lists + exactly what it depends on — risk limits, constraints, + costs, objective, cash, holdings and views are not part of it, so a family of runs is compared on + one set, which the service also caches) and the same portfolio with HiGHS (`reproducibility="exact"`); on cuOpt the + figures agree within its MIP gap of 1e-4 (`"within_gap"`). `scenario_model="gaussian_exact"` needs + no scenarios at all. +* Every result carries the *provenance* of its inputs: covariance model, scenario model and seed, + expected-return model (incl. the views and their calibrated Ω), constraints, costs, solver, status, + gap, timings. An advisor can cite “CVaR 95 % = CHF 38 210 (OptimizationService, empirical model, + EWM λ=0.99, seed 42)”. +* Pinned snapshots (`DataStore.create_snapshot`, `MarketDataService(settings, snapshot=...)`) freeze + universe, prices and FX so that demos and grading are reproducible even if Yahoo misbehaves. + +## Where the solving happens + +The MILP is built in `quantcore.optimization.problem` and handed to one of two solvers, both fed the +same `MILPModel`: + +* **cuOpt** on the GPU, over HTTP to the cuOpt container on the same Spark (`QUANTCORE_CUOPT_URL`): + `http://127.0.0.1:5000` in the Apptainer setup, where both containers share the host's network, + and `http://cuopt:5000` inside the Docker compose network. The only GPU component; an external + dependency, hence the timeouts, retries and circuit breaker. +* **HiGHS** on the CPU, *in the same process as the service* — it is compiled into SciPy + (`scipy.optimize.milp`), so there is nothing to deploy or start, and no network hop. In production + that means inside the `quantcore` container (SciPy 1.18.1 → HiGHS 1.12.0, 4 threads). + +`solver="auto"` tries cuOpt and falls back to HiGHS; the result names the solver that produced it and +how reproducible it is. See `docs/METHODOLOGY.md` §9 for versions and the measured comparison. + +## Resilience on every external call + +| External dependency | Pattern | Fallback | +|---|---|---| +| Yahoo Finance (`YahooClient`) | per-request timeout, 4 retries with exponential back-off, circuit breaker (5 failures → 60 s open) | the stored data / a snapshot; the API keeps serving from the store | +| Candidate lists (Wikipedia, iShares) | timeout, one source after the other | static snapshots shipped in `quantcore/data/static/` | +| cuOpt (`CuOptSolver`) | submit with retry/back-off, polling with client-side deadline, circuit breaker, results deleted server-side | local HiGHS MILP (scipy) — the result says `solver: "highs"` and carries a warning | + +## Data model conventions + +* prices: adjusted close (splits & dividends), one column per Yahoo symbol, union calendar of all + exchanges, holidays forward-filled (≤ 5 days); pence-quoted London lines converted to GBP. +* FX: cross rates against the base currency, repaired for quoting-unit changes (`clean_fx`) — + a rate returned 100× off for a stretch of days would otherwise be read as a 100× move of + every stock in that currency (see `docs/METHODOLOGY.md` §1). +* returns: natural logarithms; CHF return = local return + FX return (additive). +* frequencies: daily, weekly (Friday close), monthly (month end); horizons: day = 1, week = 5, + month = 21 trading days (or 1 / 4 weeks at weekly frequency). +* money: everything in the base currency (CHF); positions are **integer share numbers**. + +## Where the AI layer plugs in + +* `ResearchAgent` insights (ticker, sentiment ∈ [−1, 1], degree of belief) become Black–Litterman + views through `view_from_sentiment` / the `views` field of `/optimize` — the numbers (view return, + Ω, posterior) are computed here, the agent only supplies the assessment and its provenance. +* `RiskAgent` / `OptimizationAgent` call `/risk/*` and `/optimize` and *explain* the returned figures. +* The ontology guard can validate tickers and sectors against `/universe`. + +## Judging the system by hand + +The test bench at `/ui/` exists so that every model choice can be exercised without writing code: +the same scenario set can be seen as a distribution, as per-stock moments and as the risk figures of +an optimised portfolio, and a run's provenance (scenario key and hash, solver, gap, reproducibility) +is shown next to its result. It is also the quickest way to see the invariants hold — two identical +requests share a scenario hash and produce identical shares, and a family of runs that varies only +the risk limit or the constraints reuses one scenario set. + +## Operations + +* `quantcore setup` (once), `quantcore update` (scheduled, e.g. nightly), `quantcore snapshot`. +* On the Spark the API runs as the `quantcore` container next to `cuopt`, `gurobi`, `jupyter` + (see `docs/SPARK_DEPLOYMENT.md`); data lives in `~/services/work/quantcore_data`. diff --git a/docs/METHODOLOGY.md b/docs/METHODOLOGY.md new file mode 100644 index 0000000..d323b44 --- /dev/null +++ b/docs/METHODOLOGY.md @@ -0,0 +1,437 @@ +# Methodology + +Every formula the services implement, with the conventions and default parameters. Section +numbers follow the project brief ("Portfolio Risk Measurement and Optimization"). + +## 1. Investment universe (`quantcore.universe`) + +* **Candidates.** US: the S&P 500 constituents (Wikipedia). Europe (EU, CH, NO, UK): the STOXX + Europe 600 constituents, read from the iShares STOXX Europe 600 UCITS ETF holdings file, which + gives issuer ticker, exchange, listing currency, country and GICS sector. Domiciles outside + EU/CH/NO/UK (e.g. Bermuda, Israel) are excluded. Both lists are snapshotted into + `quantcore/data/static/` for offline use. +* **Yahoo symbols.** `` per exchange (`.SW .L .DE .PA .AS .MI .MC .ST .CO .OL .HE + .BR .VI .WA .IR .LS .AT`), share classes with a dash (`NOVO-B.CO`, `BRK-B`), London codes lose + the trailing dot (`RR.` → `RR.L`). All 1 117 symbols were verified against Yahoo, and the + European prices were cross-checked against the ETF file (after FX) — zero mismatches. +* **Metadata** from `Ticker.info`: long name, domicile, listing currency (GBp → GBP with a price + scale of 100), exchange, Yahoo sector/industry, market cap, shares outstanding, first trade date. +* **Selection.** One listing per company (share classes collapsed on a normalised name, largest cap + kept) → rank by market cap in CHF within each region → keep the largest 500 US and 500 European + → drop stocks whose first price is after `required_history_from` (2005-01-31) or whose coverage + since then is below 90 % of union-calendar days. The result has fewer than 1 000 names. +* **Market capitalisation** (for index weights) at any date: shares outstanding (today's, held + constant) × adjusted close × FX to CHF. +* **Price cleaning** (`quantcore.data.cleaning`, applied by the setup and download services and + re-runnable with `quantcore clean`): non-positive adjusted closes (broken dividend/rights + adjustments, e.g. ABI.BR before 2008) → NaN; *spike blocks* — at most three days whose entry + move `|r| > 0.4` is reversed within the block (stale pre-split ticks on exchange holidays, e.g. + 2× ticks in NOVO-B.CO) → NaN. Genuine jumps are not reversed and therefore untouched. Stocks + whose cleaned series has a gap longer than `max_missing_run` (20 business days) after + 2005-01-31 are excluded from the universe. On the 2026-09-21 data: 1 347 non-positive values in + 2 symbols, 28 spike ticks in 8 symbols; final universe 745 stocks (382 US, 363 European). +* **FX quoting-unit repair** (`clean_fx`, same module and the same two entry points): Yahoo returns + a cross rate in the wrong unit for whole stretches of days. NOK/CHF came back **100× too small on + 96 observations** — 2016-08-15 to 2017-01-09 in three runs, plus 2022-11-04 — which put the + measured volatility of the *currency* at 337 % a year instead of 12 %, and with it every Norwegian + stock (a CHF return is the local return plus the FX return, so EQNR.OL measured 338 % against its + true 31 %, and its Black–Litterman implied return came out above 100 % a year). A day-on-day + log-change within 0.15 of `k·ln(10)` is treated as a unit change, accumulated, and the series is + put back on the scale that most of its observations share (ties go to the scale it ends on). + The tolerance is tight on purpose: a unit change is *exactly* a power of ten apart from the day's + own move, while a loose tolerance swallows real events — at 0.5 it flagged ABVX.PA's +510 % + trial-result day (ratio 6.1) as a decimal shift. A scan of all 1 117 price series with the tight + rule finds one further case, ROSE.L (11 observations quoted in pence rather than GBP), which is + not in the investable universe. +* **FX spike ticks** (same function, thresholds an order of magnitude tighter than for prices: + entry move `|r| > 0.05` reversed within 0.02 over at most three days). Five ticks in twenty-one + years: EUR, GBP and USD on 2009-02-06 (one bad CHF quote hits every cross that day), SEK on + 2010-09-13, NOK on 2022-10-10. Removing them takes the lag-1 autocorrelation of daily FX + log-returns to about zero — EUR −0.053 → −0.005, GBP −0.040 → −0.001, USD −0.034 → −0.002 — which + is what a clean daily close series looks like. The one violent day in the sample, 2015-01-16 + (the SNB abandons the euro floor, every CHF cross moves ~18 %), does not reverse and is kept. + +**Why the rates are taken directly against CHF and not crossed through USD.** USD pairs are the most +liquid instruments, so triangulating `CUR/CHF = (USD/CHF) / (USD/CUR)` looks attractive; measured on +this feed it is worse, and the package therefore uses the direct pair wherever it has history (the +USD cross remains the fallback, and is what PLN uses, since `PLNCHF=X` only starts in 2026): + +| | direct `CURCHF=X` | crossed through USD | +|---|---|---| +| EUR volatility p.a. / lag-1 AC | 7.49 % / −0.005 | 9.99 % / **−0.219** | +| NOK volatility p.a. / lag-1 AC | 11.45 % / −0.047 | 17.14 % / **−0.330** | +| GBP volatility p.a. | 10.10 % | 10.35 % | +| reversing bad ticks, 2005–2026 | 5 over all currencies | 8, five of them in `EUR=X` alone | + +The cross combines two series snapped at different times, and the non-synchronicity shows up as a +strongly negative lag-1 autocorrelation and inflated variance — a portfolio risk figure built on it +would be overstated. It also has its own bad ticks (`NOK=X` falls 33 % on 2020-03-20 and rises 42 % +back on 2020-03-23; `EUR=X` moves −16 % then +14 % on 2008-12-08/09). Triangulation buys only the +removal of stale quotes (SEK 3.9 % of days unchanged, DKK 1.6 %), which costs far less than it adds. + +## 2. Prices, FX and log-returns (`quantcore.data`, `quantcore.measurement.returns`) + +* Adjusted closes `P_i(t)` in the listing currency on the union calendar of all exchanges, holidays + forward-filled (≤ 5 days; leading NaNs untouched). +* FX frame `X_c(t)` = CHF per unit of currency `c`, from Yahoo `cCHF=X`, crossed through USD when + the direct pair lacks history (`c/CHF = (USD/CHF)/(USD/c)`, e.g. PLN). +* Log-returns `r_i(t) = ln P_i(t) − ln P_i(t−1)`; CHF returns add the FX log-return: + `r_i^CHF(t) = r_i(t) + ln X_{c(i)}(t) − ln X_{c(i)}(t−1)`. +* Weekly returns: Friday-to-Friday closes; monthly: month-end to month-end. +* Horizon `h` in periods: day = 1, week = 5, month = 21 (daily data); at weekly frequency week = 1, + month = 4. Returns are assumed i.i.d., so `h`-period returns are sums. +* Portfolio log-return with weights `w` fixed at the start of the period (exact aggregation): + `r_p = ln(1 + Σ_i w_i (e^{r_i} − 1))`. + +## 3. Estimators (`quantcore.measurement.estimators`) + +Two families, both centred (the corresponding mean is subtracted): + +* **Moving window** of `w` observations (default 200 days): sample mean, standard deviation + (ddof 1), covariance `np.cov`, correlation. +* **Exponentially weighted** with decay `λ` (defaults 0.94 for measurement, 0.99 for the slow + optimisation covariance): weight of the observation `k` periods back ∝ `λ^k`, normalised to one + (pandas `ewm(alpha=1−λ, adjust=True)`); covariance + `Σ = Σ_t ω_t (r_t − m)(r_t − m)'`, `m = Σ_t ω_t r_t`. Effective window ≈ `1/(1−λ)`. +* Streamed sequences `Σ_{t−1}` for every `t` (needed by the innovation filter): the rolling + estimator recomputes the window, the EWM estimator uses the exact centred recursion + `m_t = m_{t−1} + α d`, `Σ_t = λ Σ_{t−1} + α d (r_t − m_t)'`, `d = r_t − m_{t−1}`, `α = 1 − λ`. +* Linear shrinkage towards the diagonal `(1−s)Σ + s·diag(Σ)` with `s` either given or `"auto"`: + the Schäfer–Strimmer / Ledoit–Wolf intensity + `s* = Σ_{i≠j} Var̂(s_ij) / Σ_{i≠j} s_ij²` (clipped to [0, 1]) computed on the effective estimation + window (`w` observations, or `3/(1−λ)` for EWM). It is ≈ 0 for a long, strongly correlated + sample and → 1 when `N` approaches the number of observations — exactly when the sample + covariance becomes ill-conditioned. `"auto"` is the default of the optimisation covariance + (≈ 0.05 for 100 stocks / λ = 0.99, ≈ 0.11 for the full universe). +* Annualisation: means × periods per year (252 / 52 / 12), volatilities × √(periods per year). + +## 4. Risk measures (`quantcore.measurement.risk`) + +Losses are positive. For a log-return `r` over the horizon the loss is `−r` in return space and +`V₀(1 − e^{r})` in CHF (exact, monotone mapping — the CHF VaR/CVaR are obtained by applying it to +the return-space figures, or directly from money P&L for scenario sets). + +* **Gaussian (GBM)**: with per-period mean `μ` and volatility `σ` from either estimator, + `μ_h = hμ`, `σ_h = √h σ`, + `VaR_β = −μ_h + z_β σ_h`, `CVaR_β = −μ_h + σ_h φ(z_β)/(1−β)`. + Reference: `σ = 1 %`, `μ = 0`: VaR₉₅ = 1.645 %, CVaR₉₅ = 2.063 %, VaR₉₉ = 2.326 %, CVaR₉₉ = 2.665 %. +* **Historical**: `VaR_β = −q_{1−β}(r_h)`, `CVaR_β = −E[r_h | r_h ≤ q_{1−β}]` on the last + `max(5w, 750)` observations aggregated to overlapping `h`-period sums. +* **Time series** (`risk_series`): rolling / EWM volatilities and the Gaussian VaR/CVaR built from + them, plus rolling historical quantiles and tail means. +* **Portfolio** (`portfolio_risk`): holdings in shares (+ cash) → value weights `w` at today's CHF + prices; Gaussian with `σ_p² = w'Σw`, historical on the realised portfolio return, or on a scenario + set (`method="scenarios"`). +* **Scenario CVaR** (`scenario_cvar`): with `S` equiprobable losses sorted descending and + `k = (1−β)S`, `CVaR_β = (Σ_{s≤⌊k⌋} L_(s) + (k−⌊k⌋) L_(⌊k⌋+1)) / k` — exactly what the + Rockafellar–Uryasev LP reproduces; `VaR_β = L_(⌈k⌉)`. + +## 5. Performance (`quantcore.measurement.performance`) + +Cumulative return `e^{Σr} − 1`; annualised log-return `ppy · mean(r)` and its geometric version +`e^{ppy·mean} − 1`; annualised volatility; Sharpe `(ann. return − r_f)/ann. vol` with the CHF +risk-free rate (`QUANTCORE_RISK_FREE_RATE`, default 0); Sortino (downside deviation); drawdown of +the cumulative value path, max drawdown, Calmar; skewness, excess kurtosis, hit ratio, best/worst +period. Rolling (window) and EWM (λ) versions as time series. Portfolios in shares: constant-mix +(today's value weights) or buy-and-hold. + +## 6. Stock-price models (`quantcore.models.simulation`) + +Both assume i.i.d. one-period log-returns; `Σ_T` is the latest matrix of the chosen +`CovarianceModel` (e.g. 200-day equally weighted, or EWM λ = 0.99). + +* **Geometric Brownian motion**: `r_h ~ N(hm, hΣ_T)`, sampled with `A z`, `A A' = Σ_T` + (eigen-root with eigenvalues descending by default, so the first draw coordinate drives the market + factor; symmetric and Cholesky roots optional). +* **Empirical innovations** (filtered historical simulation): + `ε_t = Σ_{t−1}^{−1/2} r_t` with `Σ_{t−1}` estimated from data up to `t−1` (streamed sequence, + symmetric root `V Λ^{−1/2} V'` — Cholesky optional for large `N`); forward simulation + `r(j | T) = hm + Σ_T^{1/2} Σ_{k=1..h} ε_{j_k}` with the dates `j_k` drawn with replacement from + the history (scrambled Sobol' points on the market-factor ranks by default, `"mc"` uniform or + `"balanced"` optional). Innovations are centred so the scenario mean equals the supplied drift. + **Re-standardisation**: a lagged volatility filter leaves the innovations with variance above + one (the forecast is too low right after every shock — 1.1 for 10 stocks, 1.5 for 100 with + λ = 0.99), so the centred sample is rescaled before resampling: `standardize="diagonal"` + (default) divides each asset's innovations by their standard deviation, `"full"` whitens with + the innovations' sample covariance (needs `T ≥ 3N`). The scenario covariance then reproduces + `hΣ_T` (per-asset volatility within ±2 %, exactly with full whitening) while skewness, fat + tails and non-linear co-movements of the data are preserved. Innovations beyond ±8 standard + deviations are winsorised (a numerical guard affecting ≈ 0.2 % of the values); ill-conditioned + `Σ_{t−1}` for large `N` is handled by the automatic shrinkage of Section 3. +* **Sampling and moment matching** (`quantcore.models.sampling`): the optimiser is a sample-average + approximation and the sample mean of a plain Monte-Carlo set is off by `σ/√S` per asset — 3–13 % + p.a. for `S = 2000` — which the optimiser maximises (measured: two seeds give portfolios differing + by 100 % of wealth). Therefore: scrambled Sobol' points (`S` rounded up to a power of two), + **moment matching** `z̃ = C^{-1/2}(z − z̄)` so that the (probability-weighted) scenario mean and + covariance are *exactly* `hm` and `hΣ_T`, and the model expected returns in the optimiser's + objective. Only the shape of the tail remains sampled. +* **Drift convention**: the expected-return model delivers *arithmetic* expected returns `μ` + (per period, annual / 252). The log-drift is the Itô-corrected `m_i = ln(1 + hμ_i)/h − σ_i²/2`, + so that `E[e^{r_h} − 1] = hμ` exactly (`drift_convention="arithmetic"`); `"log"` uses `μ` as the + log-drift and adds `σ²/2` to the expected simple return (5 % p.a. for a typical stock, 34 % for + the most volatile one — an unintended tilt to volatile stocks). +* **Importance sampling** (`importance_sampling="auto"` = `"market"`): the loss direction `d` of + the *cap-weighted candidate portfolio*; two blocks of 25 % of the scenarios are shifted along it + by `z_0.95` and `z_0.99` (GBM: `z ↦ z + c_k d`; empirical: dates drawn with exponentially tilted + probabilities `q_t ∝ exp(ϑ_k d'ε_t)`), so one set serves both confidence levels. Every scenario + carries the self-normalised likelihood ratio of the defensive mixture (bounded by + `1/(1 − Σ mixture)`) as its probability `p_s`. Tail noise falls 2–4×; 2 048 tilted scenarios ≈ + 8 192 plain ones at β = 0.99. The shift is driven by the *covariance*: in return space it is + `−c_k Σv/√(v'Σv)`, i.e. each asset moves by its covariance with the reference portfolio (beta), + not by its own volatility — on the top 30 the correlation of the shift with beta is −1.00 versus + −0.93 with own volatility, and a negatively correlated stock is shifted *up*. The weighted + tilted set still reproduces `hΣ` and all correlations exactly (the unweighted one does not). The + gain a given portfolio gets scales with the correlation between its loss and the reference's + (`tilt_alignment` in the result: 5.3× at 1.00, 2.3× at 0.90 — the usual value for the optimiser's + solutions — and nothing below ~0.3, where a warning is raised). `"adaptive"` tilts towards a first-pass solution (sharper at 99 %, + but the set then depends on the risk parameters); `"off"` disables the tilt. +* **One scenario set per family of runs**: the scenario *key* (`OptimizationService.scenario_key`, + reported with every result) contains only model inputs — candidates, as-of date, frequency and + horizon, covariance model, scenario model, `n_scenarios`, sampling, moment matching, seed, tilt + mode, innovation settings. The CVaR limit, confidence, objective, constraints, costs, cash, + holdings and the expected-return model are *not* in it: varying them reuses the same scenarios + (cached in the service), so differences between runs are the effect of the parameter and not + sampling noise. The drift is applied after the random part (`ScenarioSet.with_drift`), so a new + view shifts the scenarios without touching the shocks (`shock_hash` is unchanged). +* `ScenarioSet` reports `expected_simple` (the expectation the optimiser uses), `probabilities`, + the effective sample size and a SHA-256 fingerprint `hash` of the matrix. + +## 7. Expected returns — Black–Litterman with Idzorek confidence (`quantcore.models.black_litterman`) + +Weekly CHF log-returns of the candidate stocks over the last 10 years (`bl_years`): + +* `Σ` = 10-year sample covariance of weekly returns; `w` = current market-cap weights of the + candidates; `μ_M` = average weekly log-return of the **cap-weighted broad market index** of the + whole universe (weights drift with prices between rebalancings, today's shares held constant); + `r_f` = CHF risk-free rate per week. +* Risk aversion `δ = (μ_M − r_f)/(w'Σw)`; equilibrium excess returns `π = δ Σ w` (so the market + portfolio earns exactly `μ_M − r_f`). +* Views `P μ = Q + ε`, `ε ~ N(0, Ω)`, Ω diagonal. A view is `{assets: {ticker: weight}, q, confidence}`; + `q` is the annual total return of the view portfolio (converted to weekly excess internally; + the risk-free part cancels for relative views). +* **Idzorek confidence** `C_k ∈ (0, 1]`: the view's uncertainty is the `ω_k` for which the + unconstrained posterior weights move the fraction `C_k` of the way from `w` to the weights implied + by the view at 100 % confidence. For a diagonal Ω that calibration has the closed form + `ω_k = (1 − C_k)/C_k · p_k (τΣ) p_k'`; the tilt-matching minimisation of the paper is implemented + as `idzorek_omega_numeric` and the tests show both agree to 1e-4 (Idzorek 2005; the closed form is + due to Walters 2014). +* Posterior `μ_BL = π + τΣP'(PτΣP' + Ω)^{-1}(Q − Pπ)`, + `Σ_BL = Σ + τΣ − τΣP'(PτΣP' + Ω)^{-1}PτΣ`; `τ` default 0.05. +* Sentiment → view (`view_from_sentiment`): an assessment `s ∈ [−1, 1]` with degree of belief `C` + on stock `i` becomes the absolute view `q = π_i^{ann} + r_f + s · strength · σ_i^{ann}`, + `strength` = 0.25 by default (a fully positive view = +¼ annual volatility above equilibrium). +* Output: annual and per-period posterior total returns (used as scenario drift), Ω, δ, `μ_M`. + Alternatives: `expected_returns="historical"` (10-year means) or `"zero"`. + +## 8. Mean–CVaR optimisation as a MILP (`quantcore.optimization.problem`) + +Scenarios `R_{s,i}` = simple horizon returns `e^{r_{s,i}} − 1` in CHF (Section 6, with the drift of +Section 7), prices `P_i`, wealth `W = cash₀ + Σ_i n⁰_i P_i`. + +Variables: shares `n_i ∈ ℤ₊` (no shorts); rebalancing adds buys `b_i`, sells `s_i ∈ ℤ₊` with +`n_i = n⁰_i + b_i − s_i`; `α`, `u_s ≥ 0` bounded by the worst gain/loss attainable within the share +caps (free variables make the CVaR objective numerically hard — cuOpt's concurrent method aborts on +such models; the client then retries once with its dual simplex); binaries `y_i` (cardinality / minimum position) and +`t_i` (fixed cost per trade) only when those options are used. + +Transaction costs `TC = Σ_i (c_b P_i b_i + c_s P_i s_i) + F Σ_i t_i` (defaults 10 bp, no fixed cost). + +Loss in scenario `s`: `L_s = TC − Σ_i P_i R_{s,i} n_i`. CVaR (Rockafellar–Uryasev): + +``` +u_s ≥ L_s − α (S rows) +CVaR_β = α + 1/(1−β) Σ_s p_s u_s (p_s = 1/S unless importance sampling is used) +``` + +Objectives: `max_return` — maximise `E[P&L] = Σ_i P_i μ_i n_i − TC` (model expected simple returns +`μ_i` over the horizon; the probability-weighted scenario mean if none are given) s.t. `CVaR_β ≤ L` +(the absolute CHF risk budget for the period, e.g. "CVaR₉₅ over a week ≤ 40 000"); `min_cvar` +s.t. `E[P&L] ≥ target`; `utility` — maximise `E[P&L] − λ·CVaR_β`. + +Constraints: budget `cash₀ + Σ P_i s_i − Σ P_i b_i − TC ≥ 0` (cash may stay uninvested); single-stock +`P_i n_i ≤ w_max W` (10 %, implemented as tight variable bounds `n_i ≤ ⌊w_max W / P_i⌋`); sector +`Σ_{i∈k} P_i n_i ≤ w_k W` (default 25 %, per-sector dict possible); `TC ≤ TC_max`; turnover +`Σ P_i (b_i + s_i) ≤ τ W`; min/max invested fraction; cardinality `Σ y_i ≤ K`, `n_i ≤ M y_i`; +minimum position `P_i n_i ≥ v_min y_i`; `b_i + s_i ≤ M t_i`. + +The reported expected P&L (from the model means), VaR and CVaR of the decoded integer portfolio are +recomputed from the weighted scenario set and agree with the solver's values (`tests/test_problem.py`, +including a brute-force optimality check). Note that `α` is only *a* minimiser of the RU function — +it equals the VaR only when the CVaR constraint is tight. + +Sizes: `S + N(+2N) + 1` variables, `S + N + K + O(1)` rows, `≈ S·(N+2)` non-zeros; with the default +`N = 100`, `S = 4096` about 420 k non-zeros, solved by cuOpt in ~10 s. Solve times grow faster than +linearly in `S` (HiGHS LP ≈ `S^1.9`, cuOpt ≈ `S^1.2–1.6`): 2 000 → 100 000 scenarios costs ≈ 45× on +cuOpt. + +**Tail-row generation** (`quantcore.optimization.rowgen`, `row_generation="auto"` from 8 192 +scenarios on): only tail scenarios have `u_s > 0` at the optimum, so the MILP is solved on a working +set (the `2(1−β)S` worst scenarios of a reference portfolio), the losses of all scenarios are +checked, violated ones (`L_s > α*`) are added, and the loop stops when none is violated — the +restricted problem is a relaxation, so the final solution is optimal for the full set (within the +MIP gap). Measured: working sets ≈ 11 % of `S`, 2–3 iterations, 6–7× faster at 32 k–100 k scenarios. + +**Exact Gaussian mode** (`scenario_model="gaussian_exact"`, `quantcore.optimization.gaussian_exact`): +with normal horizon returns `R ~ N(m, C)` the CVaR is `TC − m'v + k_β √(v'Cv)` (`v = P∘n`, +`k_β = φ(z_β)/(1−β)`); the mean–CVaR problem then coincides with the mean–variance problem (the +CVaR bound corresponds to an implicit variance bound `σ̄ = (Λ + m'v*)/k_β`). In integer shares this is +a convex MISOCP, solved by outer approximation: tangent cuts `t ≥ TC + (k_β C v_k/σ_k − m)'v` on the +same MILP with an epigraph variable `t` (`cvar`), a new cut at each solution until the true CVaR equals +`t` (20–70 cuts; identical to Gurobi's direct MISOCP solution to the cent). No scenarios, no sampling +error; the reference for the sampling tests. + +## 9. Solvers (`quantcore.optimization.solvers`) + +Both solvers receive the *same* `MILPModel` (objective, CSR matrix, row and variable bounds, +variable types); `solve_with_fallback` picks between them and the result always names the one that +produced it. + +**cuOpt** — NVIDIA's GPU optimisation library, used through its self-hosted REST server +(`nvidia/cuopt`, version 26.08 here) running as the `cuopt` container on the DGX Spark: +`POST /cuopt/request` with a msgpack body, then poll `GET /cuopt/solution/{id}`; `"inf"/"ninf"` for +infinite bounds, `variable_types`, a mandatory `time_limit`. Statuses `Optimal` / `FeasibleFound` +(time limit — check `mip_gap`) / `Infeasible`. Docs: . It is the +only part of the pipeline that uses the GPU. + +**HiGHS** — the open-source LP/MILP/QP solver of ERGO-Code, University of Edinburgh (MIT licence, +, source , reference: Huangfu & Hall, +*Parallelizing the dual revised simplex method*, Math. Prog. Comp. 10, 119–142, 2018). quantcore +neither installs nor starts it separately: **HiGHS ships compiled inside SciPy**, and +`scipy.optimize.milp()` is a thin wrapper around it. It therefore runs **in-process, on the CPU, in +whichever process calls quantcore** — no network, no daemon, no separate container — and its version +follows the installed SciPy: + +| where | Python | SciPy | HiGHS | notes | +|---|---|---|---|---| +| `quantcore` container on the Spark (production) | 3.12.14 | 1.18.1 | 1.12.0 (`4f96ee8`) | aarch64 Linux, 4 threads (`OMP/OPENBLAS/MKL_NUM_THREADS=4`); numpy 2.5.3, pandas 3.0.6 | +| developer machine used for this work | 3.11.11 | 1.13.1 | 1.2.0 | arm64 macOS; numpy 1.26.4, pandas 2.1.4 | + +The container tracks the newest releases (nothing is pinned beyond the lower bounds in +`pyproject.toml`), so a rebuild can move it a long way from the developer machine — the build of +2026-09-23 jumped it to pandas 3, where the month-end resampling alias is `'ME'` and the old `'M'` +is gone (`quantcore.measurement.returns.MONTH_END` resolves this at import time, and is the reason +monthly frequency works on both). + +Verify it in any environment with +`python -c "import scipy.optimize as so, numpy as np, scipy.sparse as sp; print(so.milp(np.array([-3.,-2.]), constraints=so.LinearConstraint(sp.csr_matrix([[2.,3.],[4.,1.]]), [-np.inf]*2, [12.,10.]), integrality=np.ones(2), bounds=so.Bounds([0,0],[np.inf]*2), options={'disp': True}))"`, +which prints the HiGHS banner with its version. + +HiGHS is used in four places: `solver="highs"`; as the fallback when cuOpt is unreachable or errors +(`solver="auto"`); for the cut MILPs of the exact Gaussian mode, which are small and numerous; and as +the oracle in the tests (brute-force optimality checks). + +**Scenario count and candidate count**: `n_scenarios` defaults to 16 per candidate within +[4 096, 16 384]. The reported figures (expected P&L, CVaR) are stable at any size from ~2 000 on +(sd < 0.1 %), but the *composition* moves between near-equivalent portfolios in proportion to N/S — +3.5 % of wealth between seeds at N = 30, S = 2 048 against 12.5 % at N = 100 — and ~64 per candidate +is needed to hold it near 5 %. The result warns below 8 per candidate. + +**Covariance rank**: an estimator with `T_eff` effective observations (rolling: the window; EWM: +`3/(1−λ)`) cannot support more than `T_eff` assets. A 200-day window with N = 300 gives 101 zero +eigenvalues and a portfolio with *negative* estimated variance; `shrinkage="auto"` (the default) +raises the intensity with N/T (0.033 at N = 30 → 0.128 at N = 745) and keeps the smallest eigenvalue +above 2 % of the average variance in every case. `CovarianceModel.conditioning()` reports the +figures and the optimisation warns when the estimate is singular or ill-conditioned for the size of +the candidate set. Long-only with a 10 % cap limits the damage in any case: the optimised portfolio's +out-of-sample/in-sample volatility ratio exceeds the cap-weighted portfolio's by only 0.89–1.18× +across N = 30…745, with or without shrinkage. + +**cuOpt transport**: the model travels as one msgpack body (9.9 M non-zeros = 108 MB at N = 300, +S = 32 768), accepted at ~10 MB/s, so transport is not the constraint. The client uses a payload-scaled +timeout and a *fresh connection per attempt* — retrying on a pooled connection appends a second body and +the server rejects it with "unpack received extra data" (422) — and `solver="auto"` falls back to HiGHS +on failure. cuOpt's concurrent method also aborts on some CVaR-objective models; the client retries once +with its dual simplex (`method=2`). With row generation only the working set is sent. + +**Which models go to cuOpt.** cuOpt 26.08 aborts on one specific combination: the CVaR in the +*objective* (`objective="min_cvar"`) **and** probability-weighted scenarios. The cause is the scaling +of the objective vector. In the min-CVaR formulation the coefficient on each excess `u_s` is +`p_s/(1−β)`, so the scenario probabilities *are* the objective coefficients; importance sampling +spreads them over about six orders of magnitude. Measured on a 4 137-variable / 4 106-row model +(40 candidates, 4 096 scenarios, β = 99 %): + +| scenarios | objective `|c|` range | ratio | cuOpt | +|---|---|---|---| +| importance-sampled | 1.1e-06 … 1.0 | 8.8e5 | aborts in ~5 s | +| uniform (`importance_sampling="off"`) | 2.4e-02 … 1.0 | 4.1e1 | **Optimal in 3.8 s**, 5 runs out of 5, matching HiGHS to 7e-5 | + +Relaxing the integrality does not help — the pure LP aborts too, so it is the LP and not the branch +and bound — and multiplying the objective by a constant does not either, since that leaves the ratio +unchanged. In the `max_return` formulation the same probabilities sit in a *constraint row* instead +(the objective is then just 40 expected returns, range 1e3), where cuOpt's row scaling copes, and it +solves at either weighting. Its methods are not interchangeable either: `method=1` (PDLP) returned the +weighted optimum once in 4.9 s and then aborted three times on the identical model, while `method=2` +and `method=3` always abort. + +So `solver="auto"` diverts exactly that combination to HiGHS (1.7 s for that model) and the result +says why — otherwise a green cuOpt health pill next to `solver: "highs"` looks like the GPU being +ignored. Everything else keeps the GPU path. An explicit `solver="cuopt"` is always honoured and +returns cuOpt's own answer, abort included; an abort under `"auto"` is retried down +`CUOPT_RETRY_METHODS = (1, 2)` before the fallback. + +**cuOpt vs HiGHS, both on the Spark** (end-to-end through the API: scenario generation, model build, +solve, decode; GBM model, weekly, β = 0.95, no tilt, 600 s solver time limit): + +| case | cuOpt (GPU) | HiGHS (CPU, 4 threads) | +|---|---|---| +| N = 30, S = 8 192 | 15.6 s | 14.5 s | +| N = 30, S = 32 768 | 171.4 s | 171.0 s | +| N = 100, S = 4 096 | 10.0 s | 8.9 s | +| N = 100, S = 32 768 | 606.9 s (time limit, `FeasibleFound`) | 350.7 s (`Optimal`) | +| N = 300, S = 8 192 (row generation) | 139 s | 64 s | + +HiGHS is at least as fast in every case measured and 2× faster on the two largest. That is not a +statement about GPUs in general: cuOpt's published speedups are for *LP* with its PDLP first-order +method, while [its own documentation](https://docs.nvidia.com/cuopt/user-guide/latest/milp-features.html) +states that for MILP the GPU runs primal heuristics and **branch and bound runs on the CPU**, that the +MIP solver is beta, and that "proving feasible solutions optimal remains under active development". +Our models have an easy LP relaxation and a small integrality gap, so the work sits almost entirely in +proving optimality — the part cuOpt does not accelerate — while HiGHS' dual simplex warm-starts across +branch-and-bound nodes. HiGHS is also a strong reference point: in the [Mittelmann MILP benchmark](https://plato.asu.edu/ftp/milp.html) +(7 July 2026) HiGHS 1.15.1 solves 179 of 240 instances at a scaled shifted geometric mean of 5.44, +against 1.72 for COPT 8.0.3 and 9.93 for SCIP. Caveats: cuOpt's `time_limit` covers only its internal +solve, it ignores the MIP-gap settings sent over REST (so it may prove optimality to a tighter +tolerance), and this is one problem family on one machine, not a benchmark. + +**Reproducibility**: the scenario set depends only on the seed (`scenario_hash` in every result), and +the hash is identical for two runs **in the same environment**. Across environments it is not, and the +reason is the fingerprint rather than the mathematics. Measured between the developer Mac (numpy 1.26.4, +pandas 2.1.4, scipy 1.13.1) and the Spark container (numpy 2.5.3, pandas 3.0.6, scipy 1.18.1) on the +same 100 stocks and the same stored prices: scrambled Sobol' points and the numpy RNG stream are +bit-identical, the estimated covariance agrees to `max |dC| = 1.7e-18` (entries ~5e-4), its +eigenvalues to 1.4e-17 and the columns of `pca_root` to 4e-13 relative — no sign flips, no rotation. +The scenario matrices therefore agree to 13–14 significant digits. But `scenario_hash` fingerprints +`np.round(scenarios, 12)`, and on a 4096 × 100 matrix of weekly log-returns a *relative* perturbation +of 1e-15 already pushes ~3 entries of 409 600 across a rounding boundary (1e-14 pushes ~47), which +changes the digest. The figures are unaffected: the same request optimised on both machines gave +E[P&L] 1 888.65 against 1 888.76 and CVaR 39 999.71 against 39 999.82 on a CHF 1 M portfolio. + +The portable identity is the **scenario key**, not the hash: it is built from the request and the +model description, so two environments that agree on the inputs produce the same `key` (verified: +`7162939ddf75ff74` on both machines for the run above) even where the digest of the realised matrix +differs. Read `scenario_hash` as "these two results used bit-identical scenarios" — a statement about +one machine — and `scenario_key` as "these two results were asked for the same scenarios". +HiGHS' branch-and-bound is deterministic → identical shares for the same inputs *and the same HiGHS +build* (`reproducibility="exact"`); across SciPy versions (1.2.0 on the developer machine against +1.12.0 in the container) the search can end on a different optimal vertex among ties, with the same +objective. cuOpt 26.08 +ignores the MIP-gap settings sent over REST and stops within its default relative gap of 1e-4: the +objective, expected P&L and CVaR of two runs agree to that gap (≈ 0.2 CHF/week on CHF 1 M) while +near-tied share counts can differ by a few shares (`reproducibility="within_gap"`). The Gaussian exact +mode runs on HiGHS by default. + +## 10. Default parameters + +| Parameter | Default | Where | +|---|---|---| +| history start / required first price | 2004-12-01 / ≤ 2005-01-31, coverage ≥ 90 % | `Settings` | +| base currency, risk-free | CHF, 0 % | `Settings` | +| measurement window / λ | 200 periods / 0.94 | `RiskService` | +| optimisation covariance | EWM λ = 0.99 (≈ 100-day effective window) | `OptimizationRequest.covariance` | +| scenarios | empirical innovations, 4 096 scrambled Sobol' draws on the market-factor ranks, moment matched, arithmetic drift, last 2 500 days, diagonal re-standardisation, seed 42 | `OptimizationRequest` | +| importance sampling / row generation | `auto` = market tilt (blocks at z₀.₉₅, z₀.₉₉, 25 % each); row generation from 8 192 scenarios | `OptimizationRequest` | +| MIP gap | 1e-4 relative (HiGHS; cuOpt uses its own default) | `Settings.mip_rel_gap` | +| Black–Litterman | 10 years weekly, τ = 0.05, sentiment strength 0.25 | `OptimizationRequest`, `view_from_sentiment` | +| constraints | 10 % per stock, 25 % per sector, no shorts, 10 bp costs | `OptimizationConstraints`, `TransactionCosts` | +| CVaR limit | explicit CHF amount or fraction (5 % of wealth if nothing is given) | `OptimizationRequest` | +| cuOpt time limit | 120 s (`QUANTCORE_CUOPT_TIME_LIMIT`) | `Settings` | diff --git a/docs/SPARK_APPTAINER.md b/docs/SPARK_APPTAINER.md new file mode 100644 index 0000000..a3910cf --- /dev/null +++ b/docs/SPARK_APPTAINER.md @@ -0,0 +1,254 @@ +# quantcore on your DGX Spark (Apptainer, no root) + +This guide sets up the quant service of the project on your own DGX Spark. The service covers +market data, risk measurement, Black–Litterman expected returns and the mean-CVaR optimiser. +Everything runs on the Spark itself: + +- **cuOpt**, NVIDIA's GPU solver, runs in its own Apptainer container. +- **quantcore**, this repository, runs in a second Apptainer container. It calls cuOpt at + `http://127.0.0.1:5000`. +- The API and the browser test bench listen on `http://127.0.0.1:5001`. You reach them from the + Spark: its desktop browser, a terminal, a notebook, or the platform you build. They are not + reachable from the network. + +You need no root rights, no Docker and no VPN. One script, `deploy/apptainer/qc.sh`, does +everything. + +## What you need + +| Requirement | Check | +|---|---| +| Apptainer 1.1 or newer | `apptainer --version` | +| The GPU | `nvidia-smi -L` lists the NVIDIA GB10. Without it, optimisation runs on HiGHS on the CPU. | +| About 5 GB in your home directory | The cuOpt image is 3.3 GB, quantcore 0.2 GB, the data 40 MB. Pulling cuOpt needs about 12 GB for a few minutes. | +| Internet access | Docker Hub for the images, PyPI for the Python libraries, Yahoo Finance for market data. | + +## Quick start + +```bash +git clone https://gitea.fhgr.ch/herzogfloria/Portfolio_Risk_Management.git +cd Portfolio_Risk_Management + +deploy/apptainer/qc.sh install # once, ~10 min: settings, cuOpt image, quantcore image, self-test +deploy/apptainer/qc.sh data-import /path/to/quantcore_data.tar.gz # the prepared data set +# or build the data set from Yahoo Finance yourself (~15 min): deploy/apptainer/qc.sh setup +deploy/apptainer/qc.sh start # starts cuOpt and the API +``` + +Then open **http://127.0.0.1:5001/ui/** in the Spark's browser. The interactive API +documentation is at **http://127.0.0.1:5001/docs**. + +Working on the Spark over SSH from your laptop? Forward the port, then open the same address in +the laptop's browser. Nothing else on the network can reach the service: + +```bash +ssh -L 5001:127.0.0.1:5001 @ +``` + +A shorter command saves typing. Put this line in your `~/.bashrc`: + +```bash +alias qc="$HOME/Portfolio_Risk_Management/deploy/apptainer/qc.sh" +``` + +The rest of this guide writes `qc` for the script. + +## Daily use + +```bash +qc start # cuOpt + API; after a reboot, run it again +qc status # what runs, whether cuOpt is reachable, how much data there is +qc logs # API log; `qc logs cuopt` for the solver, add -f to follow +qc update # the newest prices and FX rates from Yahoo Finance +qc stop # stop both containers +``` + +The containers keep running after you log out. They stop when the Spark reboots. + +## Using the service + +**In the browser.** The test bench at `http://127.0.0.1:5001/ui/` shows every service: the +stock universe, risk and scenarios, expected returns and views, constraints, and the +optimisation with a before/after comparison. + +**From your own code.** This is how the platform you build uses the service. The API speaks +JSON over HTTP, so any language works. Your code reaches it at `http://127.0.0.1:5001`. That +address also works inside your own Apptainer containers, because Apptainer containers share +the Spark's network. For Python there is a ready-made client: + +```python +from quantcore.client import QuantcoreClient + +qc = QuantcoreClient("http://127.0.0.1:5001") +print(qc.health()["cuopt"]["reachable"]) +risk = qc.asset_risk(["NESN.SW", "AAPL"], horizon="week") +``` + +Install the client into your own environment with +`pip install "git+https://gitea.fhgr.ch/herzogfloria/Portfolio_Risk_Management.git"`. +[USAGE.md](USAGE.md) describes every endpoint and the request fields. + +**From the command line, through the API.** `qc api` runs the API client against your +service: + +```bash +qc api health +qc api selftest # calls every endpoint, pass/fail table +qc api risk NESN.SW AAPL --horizon week +qc api optimize --cash 1000000 --cvar-limit 40000 --horizon week +``` + +**From the command line, without the API.** `qc cli` runs the services directly in the +container. The API does not need to be running: + +```bash +qc cli status +qc cli risk NESN.SW AAPL --horizon week +qc cli optimize --cash 1000000 --cvar-limit 40000 --horizon week --max-assets 50 +``` + +## Settings + +`qc install` writes the settings file `~/quantcore/quantcore.env`. Edit it, then run +`qc restart`. A value set in the shell wins over the file, for example +`QC_API_PORT=5011 qc start`. + +| Setting | Default | Meaning | +|---|---|---| +| `QC_API_PORT` | `5001` | Port of the API and the test bench. | +| `QC_CUOPT_PORT` | `5000` | Port of the cuOpt server. | +| `QC_CUOPT_START` | `auto` | `auto` uses a cuOpt that already answers on the port, or starts one. `yes` always starts its own. `no` never starts one, so HiGHS solves every model. | +| `QC_CUOPT_IMAGE` | `docker://nvidia/cuopt:26.8.0-cuda13.3-py3.14` | Where the cuOpt image comes from. Set the path of a `.sif` file if your Spark already provides one. | +| `QC_IMAGE` | `build` | How quantcore gets its container; see the next section. | +| `QUANTCORE_*` | | Passed into the container unchanged. Examples are `QUANTCORE_CUOPT_TIME_LIMIT` and `QUANTCORE_ENABLE_UI`. All of them are listed in `quantcore/config.py`. | + +**Several users on one Spark.** Ports belong to the whole machine. If someone else already uses +port 5001, `qc start` stops and suggests a free port to put into your settings file. A cuOpt +that another user already runs on port 5000 is simply used: cuOpt keeps no state between +requests. + +## How quantcore gets its container + +`QC_IMAGE=build` is the default. `qc install` then builds the image `quantcore.sif` from this +repository with Apptainer's unprivileged build. Some Spark installations do not allow +unprivileged builds. In that case `qc install` falls back by itself to a setup that needs no +build. It pulls a plain Python image and installs quantcore into a virtual environment in +`~/quantcore/venv`. The libraries and the results are the same, because both setups install the +pinned versions in `deploy/apptainer/constraints.txt`. You can choose the fallback directly +with `QC_IMAGE=venv`. + +`QC_IMAGE` also accepts the path of a prebuilt `quantcore.sif`, or a `docker://` or `oras://` +address to pull one from. + +**After `git pull`**, rebuild and restart: + +```bash +git pull && qc build && qc restart +``` + +## How it works + +``` + DGX Spark, your user account, no root + ┌────────────────────────────────────────────────────────────────────────────┐ + │ browser / your platform / qc api │ + │ │ HTTP │ + │ ▼ │ + │ 127.0.0.1:5001 instance "quantcore-api" quantcore.sif │ + │ FastAPI + test bench ~/quantcore/data -> /data │ + │ │ HTTP (msgpack) │ + │ ▼ │ + │ 127.0.0.1:5000 instance "quantcore-cuopt" cuopt.sif, started with --nv │ + │ cuOpt server ──► GB10 GPU │ + └────────────────────────────────────────────────────────────────────────────┘ +``` + +- An Apptainer **instance** is a container that runs in the background, like a service. + `apptainer instance list` shows yours. +- Apptainer containers use the Spark's network directly, with no port mapping. A server inside a + container that listens on `127.0.0.1` is therefore reachable from the Spark, and only from it. +- The images are read-only. Everything that changes lives in `~/quantcore/data`, which is + mounted into the container as `/data`. +- The containers start with `--cleanenv`. Variables from your shell, such as an activated + virtual environment or `PYTHONPATH`, cannot interfere. `qc.sh` passes only the settings above, + plus proxy variables if you have them. +- NVIDIA's cuOpt image sets its home directory to `/opt/cuopt`. That folder is writable under + Docker but read-only in an Apptainer image, and the solver would crash on its first job when + CuPy writes its kernel cache there. `qc.sh` therefore starts the cuOpt server with its home + directory in `~/quantcore/cuopt-home`. +- When cuOpt is not reachable, or stops during a solve, every optimisation falls back to HiGHS on + the CPU. The result says which solver produced it. + +| Path | Content | +|---|---| +| `~/quantcore/quantcore.env` | your settings | +| `~/quantcore/data/` | universe, prices, FX, snapshots | +| `~/quantcore/images/cuopt.sif` | the cuOpt image | +| `~/quantcore/images/quantcore.sif` | the quantcore image; in the fallback, `python.sif` plus `~/quantcore/venv/` | +| `~/quantcore/cuopt-home/` | cuOpt's caches (CuPy kernels, CUDA) | +| `~/.apptainer/instances/logs/` | instance logs, also shown by `qc logs` | +| `~/.apptainer/cache/` | downloaded image layers; `qc clean-cache` frees the space | + +## Troubleshooting + +| Symptom | Cause and fix | +|---|---| +| `port 5001 is taken` | Another user or program uses the port. Set the suggested `QC_API_PORT` in `~/quantcore/quantcore.env`, then run `qc start`. | +| `qc status` shows cuOpt as not answering | Run `qc logs cuopt`. Check that `nvidia-smi` works. Then `qc restart`. Optimisations keep working on HiGHS in the meantime. | +| The cuOpt log says `Read-only file system: '/opt/cuopt/.cupy'` | cuOpt was started without `qc.sh`. Start it with `qc start`, or give it a writable home directory as described in "Running cuOpt by hand". | +| `/optimize` answers 503, "no market data yet" | Run `qc data-import ` or `qc setup`. | +| Yahoo errors during `qc setup` or `qc update` | Yahoo limits how fast it answers. Run the command again later; stored data stays intact. | +| `qc install` stops with "no 'instance run'" | The Spark's Apptainer is older than 1.1. Ask the administrator to update it. | +| The cuOpt pull fails with "no space left on device" | Apptainer unpacks images in `/tmp`. Point it at a larger directory: `APPTAINER_TMPDIR=~/quantcore/tmp qc pull-cuopt`. | +| Model status `FeasibleFound` instead of `Optimal` | The time limit was reached. Raise `time_limit` in the request or `QUANTCORE_CUOPT_TIME_LIMIT`, or reduce `n_scenarios` or `max_assets`. | + +## Running cuOpt by hand + +`qc start` does this for you. If you want to run the cuOpt image yourself, for example for another +project, start it like this: + +```bash +apptainer pull cuopt.sif docker://nvidia/cuopt:26.8.0-cuda13.3-py3.14 +mkdir -p ~/cuopt-home +apptainer instance run --nv --cleanenv --bind ~/cuopt-home cuopt.sif my-cuopt \ + env HOME=$HOME/cuopt-home python -m cuopt_server.cuopt_service -i 127.0.0.1 -p 5000 +curl http://127.0.0.1:5000/cuopt/health +apptainer instance stop my-cuopt +``` + +## For the lecturer + +**Data set for the students.** Many students running `qc setup` at the same time from one +network can hit Yahoo's rate limits. A prepared data set avoids that, and every student then +works on the same data. Pack it on a machine that has the data: + +```bash +qc data-export quantcore_data.tar.gz # or, from any data directory: +tar -czf quantcore_data.tar.gz -C --exclude ./jobs --exclude '*.log' . +``` + +The archive is about 30 MB. It is not part of the public repository: Yahoo's terms do not allow +redistributing their data. + +**Prebuilt image.** Build `quantcore.sif` on any aarch64 Linux machine where you can build +images, then hand out the file: + +```bash +apptainer build quantcore.sif deploy/apptainer/quantcore.def # from the repository root +``` + +Students set `QC_IMAGE=/path/to/quantcore.sif` in their settings file. A copy built and tested on +the lecturer's Spark is described in the verification note below. + +**The lecturer's own Spark** runs the Docker setup in [SPARK_DEPLOYMENT.md](SPARK_DEPLOYMENT.md). +That setup needs root and Docker. Students do not need it. + +**Verification.** The setup was tested end to end on 7 October 2026 on a DGX Spark: NVIDIA GB10, +driver 580.159, Ubuntu 24.04, Apptainer 1.5.4, as a user without root. That Spark allowed no +unprivileged image builds, so `qc install` took the virtualenv fallback by itself. The image mode +was then tested with a prebuilt `quantcore.sif`. In both modes: + +- `qc api selftest --full` passed on every endpoint, and the optimisation ran on cuOpt on the GPU. +- `qc update` fetched the newest Yahoo prices through the running API. +- A taken API port stopped `qc start` with a suggested free port, and that port worked. +- The test suite passed inside the container, including the round trip to the live cuOpt server. diff --git a/docs/SPARK_DEPLOYMENT.md b/docs/SPARK_DEPLOYMENT.md new file mode 100644 index 0000000..ddc0987 --- /dev/null +++ b/docs/SPARK_DEPLOYMENT.md @@ -0,0 +1,110 @@ +# Running the quant service on the DGX Spark (Docker, with root) + +> This is the setup of the lecturer's Spark, which has root rights, Docker Compose and Tailscale. +> **Students** run the service on their own Spark with Apptainer and without root: +> see [SPARK_APPTAINER.md](SPARK_APPTAINER.md). + +The Spark (`spark-6d4a`, reachable over Tailscale) already runs cuOpt, Gurobi and JupyterLab as +Docker containers defined in `~/services/docker-compose.yml`. The quant service joins that +compose file as the container **`quantcore`** on port **5001**; it reaches cuOpt over the compose +network (`http://cuopt:5000`) and keeps its data on the host in `~/services/work/quantcore_data`. + +| Service | Address | What it is | +|---|---|---| +| **quantcore** | `http://spark-6d4a:5001` (`/docs` for Swagger, `/ui/` for the test bench) | portfolio data, risk/performance measurement, models, mean-CVaR MILP | +| cuOpt | `http://spark-6d4a:5000` | the GPU MILP solver the service uses | + +## Deploy (from the Mac) + +```bash +deploy/deploy_to_spark.sh --with-data # first time: also ships the locally built data set +deploy/deploy_to_spark.sh # later: code only +``` + +The script rsyncs the repository to `~/services/work/Portfolio_Risk_Optimization`, appends the +service block from `deploy/docker-compose.quantcore.yml` to the compose file (once; a backup of the +compose file is kept), builds the image (`python:3.12-slim`, aarch64) and starts the container. + +The block is appended **only if `quantcore:` is not already in `~/services/docker-compose.yml`**, so +later edits to `deploy/docker-compose.quantcore.yml` (a new environment variable, a changed port) do +not travel with a redeploy — apply them to the Spark's compose file by hand, or delete the block +there and re-run the script. Code and image changes need no such step. + +Without `--with-data` the container starts empty; run the setup once through the API: + +```bash +curl -X POST http://spark-6d4a:5001/jobs/setup # ~15 min: 1 117 tickers' metadata + history +curl http://spark-6d4a:5001/jobs/ # status: queued | running | done | failed +``` + +## Operate + +```bash +ssh user_fh_fhgr@spark-6d4a +cd ~/services +docker compose ps quantcore # status (healthcheck on /health) +docker compose logs -f quantcore # live logs +docker compose restart quantcore +docker compose up -d --build quantcore # rebuild after a code change (or re-run the deploy script) + +# inside the container: the CLI works against the same data volume +docker exec -it quantcore quantcore status +docker exec -it quantcore quantcore update # newest prices (incremental) +docker exec -it quantcore quantcore snapshot --name demo +``` + +Nightly data refresh (host cron on the Spark), e.g. `crontab -e`: + +``` +15 23 * * 1-5 curl -s -X POST http://localhost:5001/jobs/update >/dev/null +``` + +## Use from anywhere on the tailnet + +```bash +curl http://spark-6d4a:5001/health +curl "http://spark-6d4a:5001/universe?region=Europe§or=Health%20Care" +curl -s http://spark-6d4a:5001/risk/assets -H 'Content-Type: application/json' \ + -d '{"symbols": ["NESN.SW", "ROP.SW", "AAPL"], "horizon": "week"}' +curl -s http://spark-6d4a:5001/optimize -H 'Content-Type: application/json' \ + -d '{"cash": 1000000, "max_assets": 100, "cvar_limit": 40000, "horizon": "week"}' +``` + +The test bench is served from the same container (`QUANTCORE_ENABLE_UI`, default `true`, in the +compose block). Set it to `"false"` and rebuild for a deployment that exposes the JSON API and +`/docs` only — for users who build their own front end against these endpoints. +`deploy/docker-compose.students.yml` is that deployment, standalone and with the bench already +off; `docs/USAGE.md` is the guide its users need. + +Python clients simply point `QUANTCORE_CUOPT_URL` / the API base URL at the Spark; the package +itself also runs on a laptop (`quantcore serve`) and uses the Spark's cuOpt remotely +(`QUANTCORE_CUOPT_URL=http://spark-6d4a:5000`), falling back to HiGHS — which needs no service of its +own: it is compiled into SciPy inside the `quantcore` image (SciPy 1.18.1 → HiGHS 1.12.0) and runs on +the container's CPU — when the +Spark is unreachable. + +## Sizing + +| Candidates | Scenarios | MILP size (vars / rows / nnz) | cuOpt solve | +|---|---|---|---| +| 100 | 2 000 | 2 101 / 2 013 / 206 k | ~6–10 s | +| 300 | 2 000 | 2 301 / 2 013 / 607 k | ~12 s | +| 745 (all) | 1 000 | 1 746 / 1 013 / 749 k | ~17 s | +| 100, rebalancing with fixed costs | 2 000 | 2 401 / 2 215 / 808 k (400 integers) | ~33 s | + +The time limit (`QUANTCORE_CUOPT_TIME_LIMIT`, request field `time_limit`) bounds the solve; a +`FeasibleFound` status with a small `mip_gap` is a valid, slightly sub-optimal answer. + +BLAS threads are pinned to 4 (`OPENBLAS_NUM_THREADS`): the empirical-innovation filter performs +thousands of small eigendecompositions, which OpenBLAS runs ~50× slower with all 20 cores. + +## Troubleshooting + +| Symptom | Cause / fix | +|---|---| +| `/health` shows `cuopt.reachable: false` | cuOpt container down or unhealthy: `docker compose restart cuopt` (see the Spark README §5). Optimisations fall back to HiGHS and say so in `solver` / `warnings`. | +| `/optimize` answers 503 "no market data yet" | run `POST /jobs/setup` or deploy with `--with-data`. | +| Yahoo errors in the setup/update job | rate limiting; the client retries with back-off and opens a circuit after 5 failures. Re-run the job later; stored data stays intact. | +| `FeasibleFound` instead of `Optimal` | time limit hit; raise `time_limit` or reduce `n_scenarios` / `max_assets`. | +| `Infeasible` | the constraints contradict each other (e.g. `min_invested` with a tiny CVaR limit, or a rebalancing turnover cap that prevents reaching the risk budget). | +| Tailscale SSH asks for a browser check | `ssh user_fh_fhgr@spark-6d4a` prints a login URL; open it once, then re-run the deploy script. | diff --git a/docs/USAGE.md b/docs/USAGE.md new file mode 100644 index 0000000..350a711 --- /dev/null +++ b/docs/USAGE.md @@ -0,0 +1,342 @@ +# Using quantcore + +`quantcore` is the **deterministic quant core** your advisory platform builds on: market data, risk +and performance measurement, stock-price models, Black–Litterman expected returns and mean–CVaR +portfolio optimisation in whole shares. + +It contains **no LLM call and makes no decisions**. It computes numbers from data and from the inputs +you hand it, and every answer carries the provenance of those inputs. Your platform supplies the +judgement — which news matters, how strongly to believe it, what to tell the client — and calls in +here for every figure it quotes. + +You never have to modify this package. Treat it as a service with a contract. + +--- + +## 1. Two ways to use it + +| | **HTTP API** | **Python library** | +|---|---|---| +| you need | a URL | the package *and* the data directory on the same machine | +| import | nothing — `requests`, or `quantcore.client` | `from quantcore.measurement.risk import RiskService` | +| language | any | Python | +| good for | the normal case: your platform calls a running service | notebooks, experiments, running your own backtest loop | + +Start with the API. Everything in this guide is available both ways, and the library classes are +documented in `README.md` and in `docs/ARCHITECTURE.md` if you need them. + +## 2. Install + +```bash +git clone https://gitea.fhgr.ch/herzogfloria/Portfolio_Risk_Management.git +cd Portfolio_Risk_Management +pip install -e . # Python 3.11+ +``` + +On a DGX Spark you do not need this step to run the service: `deploy/apptainer/qc.sh` runs it in +containers (see §3 and [SPARK_APPTAINER.md](SPARK_APPTAINER.md)). Install the package only where your +own code imports it, for example for `quantcore.client`. + +This gives you two commands: + +| command | what it is | +|---|---| +| `quantcore` | runs the services **in this process**; needs the data directory locally | +| `quantcore-api` | talks to a **running** service over HTTP; needs only its URL | + +## 3. Point at a service, or run your own + +**On your DGX Spark** (the usual case): the service and cuOpt run as Apptainer containers of your +user, reachable at `127.0.0.1` on the Spark only. [SPARK_APPTAINER.md](SPARK_APPTAINER.md) has the details: + +```bash +deploy/apptainer/qc.sh install && deploy/apptainer/qc.sh data-import quantcore_data.tar.gz +deploy/apptainer/qc.sh start # API at http://127.0.0.1:5001 (the client's default URL) +deploy/apptainer/qc.sh api health +``` + +**Someone else runs it:** point the client at that machine. + +```bash +export QUANTCORE_API_URL=http://:5001 +quantcore-api health +``` + +**You run it yourself.** The data has to be built once, which downloads about 1 100 tickers from +Yahoo Finance and takes ~15 minutes: + +```bash +quantcore setup # once +quantcore serve # HTTP API on :5001, interactive docs at /docs +``` + +Or as a container, which is how it runs in production: + +```bash +docker build -f deploy/Dockerfile -t quantcore:0.1 . +docker run -p 5001:5001 -v $PWD/data:/data \ + -e QUANTCORE_CUOPT_URL=http://:5000 quantcore:0.1 +``` + +Configuration is environment variables with a `QUANTCORE_` prefix (see `.env.example`); the ones you +are likely to touch: + +| variable | default | meaning | +|---|---|---| +| `QUANTCORE_API_URL` | `http://127.0.0.1:5001` | which service `quantcore-api` and `QuantcoreClient` talk to | +| `QUANTCORE_DATA_DIR` | `./data` | where the prices and the universe live | +| `QUANTCORE_CUOPT_URL` | `http://127.0.0.1:5000` | the GPU MILP solver (a cuOpt container on the same machine); unreachable is fine, see §9 | +| `QUANTCORE_BASE_CURRENCY` | `CHF` | reporting currency of every figure | +| `QUANTCORE_ENABLE_UI` | `true` | `false` serves the JSON API and `/docs` only | + +## 4. Check that it works + +```bash +quantcore-api selftest +``` + +This calls **every endpoint** with a small request and prints what each one answered: + +``` + endpoint result ms evidence +0 GET /health ok 260 745 stocks, last 2026-09-21, cuOpt up +5 GET /prices ok 28 5667 dates x 3 symbols +9 POST /risk/assets ok 10 3 rows, cvar95_hist 0.1259 +21 POST /optimize (gaussian_exact) ok 63 Optimal on highs, 7 positions, CVaR 44,551 +... +24 ok, 0 failed, 3 skipped in 1.0 s against http://127.0.0.1:8765 +``` + +Exit code 0 means the deployment is sound. Run this first whenever something behaves oddly — it +separates "the service is broken" from "my request is wrong" in one second. The three skipped probes +write to the store or download data; `--write` includes them. `--full` optimises on a real scenario +set instead of the fast Gaussian mode. + +`quantcore-api endpoints` lists the API surface, and `/docs` is the interactive reference with +every field of every request. **That is the authoritative contract** — this guide is the tour. + +## 5. Conventions you have to know + +These are not negotiable per request; they are how the whole package thinks. + +* **Returns are natural logarithms.** `ln(P_t / P_{t-1})`. A CHF return of a foreign stock is the + local log-return plus the FX log-return — they add. +* **Money is CHF**, always, whatever the listing currency. +* **Positions are integer share numbers**, never percentages. That is the point of the MILP: you + cannot buy 0.3 of a share, and at CHF 1 m the difference matters. +* **A horizon is `"day"`, `"week"`, `"month"`, `"quarter"`, `"year"`, or an integer number of + periods** of the chosen frequency. `"week"` at daily frequency is 5 trading days. +* **Risk is CVaR** (expected shortfall) at 95 % and 99 %, expressed as an absolute amount over the + horizon. VaR comes along for reference. +* **Two knobs on every parametric risk figure**, and the defaults are not always what you want: + * `drift`: `"estimated"` subtracts the window's mean return; `"zero"` uses volatility only. Use + `"zero"` for anything longer than a week — over a quarter the estimated mean is the noisiest + input in the calculation and it can halve the reported risk. + * `space`: `"log"` reports the figure as a log-return; `"value"` as a fraction of the position's + value (`1 - e^{-x}`), which cannot exceed 100 %. Use `"value"` in anything a client reads. + +## 6. The endpoints + +| method | path | what it answers | +|---|---|---| +| GET | `/health` | is the service up, what data does it hold, is the GPU solver reachable | +| GET | `/universe`, `/universe/{symbol}` | the investable stocks and their metadata | +| GET | `/data/symbols` | the universe as one table plus filter facets — feeds a stock picker | +| GET | `/prices`, `/returns` | adjusted closes and log-returns, daily/weekly/monthly, CHF or local | +| GET | `/market/index`, `/market/weights` | the cap-weighted broad market and its weights | +| POST | `/risk/assets` | per-stock volatility, VaR, CVaR (normal and historical, rolling and EWM) | +| POST | `/risk/series` | those figures through time, for a chart | +| POST | `/risk/covariance`, `/risk/correlation` | the matrices as measured | +| POST | `/risk/portfolio` | VaR/CVaR of a portfolio held in shares | +| POST | `/performance/assets`, `/performance/rolling`, `/performance/portfolio` | return, Sharpe, Sortino, drawdown, Calmar, skew, kurtosis | +| POST | `/model/expected-returns` | Black–Litterman: equilibrium π, your views, the posterior | +| POST | `/model/covariance` | the covariance the models use, with its conditioning | +| POST | `/model/scenarios` | a scenario set, its moments, a portfolio's P&L distribution | +| POST | `/optimize` | the mean–CVaR MILP: a new portfolio or a rebalancing | +| POST | `/jobs/setup`, `/jobs/update`, GET `/jobs`, `/jobs/{id}` | background data work | +| GET/POST | `/snapshots` | freeze the data so a demo stays reproducible | + +## 7. From Python + +```python +from quantcore.client import QuantcoreClient + +qc = QuantcoreClient("http://127.0.0.1:5001") # or omit: $QUANTCORE_API_URL, else this default + +qc.health()["store"]["last_price_date"] # '2026-09-21' + +# what can I invest in? +swiss = qc.universe(country="Switzerland") +health = [s["symbol"] for s in qc.universe(region="Europe", sector="Health Care")] + +# what is one stock's risk over a week, as a client would read it? +row = qc.asset_risk(["NESN.SW"], horizon="week", drift="zero", space="value")[0] +row["cvar95_normal_ewm"] # 0.0665 -> 6.7 % of the position in the worst 5 % + +# what is my client's current portfolio risking? +qc.portfolio_risk({"NESN.SW": 500, "AAPL": 100}, cash=20_000, horizon="month") + +# the research agent found something positive on Apple, with middling confidence +views = [{"symbol": "AAPL", "sentiment": 0.6, "confidence": 0.4}] +post = qc.expected_returns(max_assets=100, views=views) + +# propose a portfolio for CHF 1 m that risks at most CHF 30 000 in the worst 5 % of weeks +res = qc.optimize(cash=1_000_000, horizon="week", confidence=0.95, cvar_limit=30_000, + views=views, risk_drift="zero", time_limit=300) + +res["status"] # 'Optimal' +res["cvar"] # 30000.00 -- the limit binds exactly +for p in res["positions"]: + print(p["symbol"], p["shares"], round(p["weight"], 4)) +``` + +Every method passes its keyword arguments through to the API unchanged, so the field names in +`/docs` are the field names here. A non-2xx answer raises `QuantcoreError` with the API's own +message; an unreachable server raises it too, with `unreachable = True`. + +## 8. From the command line + +The same surface, for looking at things and for scripting: + +```bash +quantcore-api universe --region Europe --sector "Health Care" +quantcore-api prices NESN.SW AAPL --freq weekly --tail 5 +quantcore-api risk NESN.SW AAPL --horizon quarter --drift zero --space value +quantcore-api correlation NESN.SW NOVN.SW ROG.SW +quantcore-api risk-portfolio --holdings '{"NESN.SW": 500, "AAPL": 100}' --cash 20000 +quantcore-api expected-returns --max-assets 50 --view AAPL:0.6:0.4 +quantcore-api scenarios NESN.SW AAPL --model gbm --n-scenarios 4096 +quantcore-api optimize --cash 1000000 --cvar-limit 30000 --horizon week --risk-drift zero +quantcore-api optimize --request my_request.json --json > result.json +quantcore-api update --wait # refresh prices and follow the job +``` + +`--view SYMBOL:SENTIMENT[:CONFIDENCE[:STRENGTH]]` is the shorthand for a sentiment view, repeatable. +`--json` prints the raw answer (pipe it into `jq`, or save it as a fixture); it works before or after +the subcommand. `--request FILE` takes a full JSON request, which is the right way to keep a +complicated setup reproducible — its fields win over the flags. + +## 9. The five things the project actually needs + +**Pick a universe.** `/data/symbols` gives you rows *and* the counts per region, sector and country +in one call, so a picker needs no second request. Pass the resulting symbols as `symbols` to +`/optimize` to restrict the candidates; leave it out and the largest `max_assets` by market cap are +used. + +**Measure what the client holds.** `/risk/portfolio` with their share numbers and cash. Use +`method="historical"` alongside the default `"normal"` and show both — where they disagree, the +distribution is not Gaussian, and that is worth saying out loud. + +**Turn an assessment into a view.** A view is a statement about an *annual expected return* with a +degree of belief. Either explicit (`{"assets": {"NESN.SW": 1.0}, "q": 0.09}`: "Nestlé will return +9 %") or from sentiment (`{"symbol": "NESN.SW", "sentiment": 0.6, "confidence": 0.4}`). Sentiment in +[−1, 1] scales `strength` (default 25 % p.a. at full sentiment); `confidence` in (0, 1] is the +Idzorek confidence that sets Ω. Call `/model/expected-returns` *before* optimising to see what your +view did to the posterior — that is the number you will have to defend. + +**Propose a new portfolio.** `/optimize` with `mode="new"` and `cash`. Set `cvar_limit` (absolute +CHF) or `cvar_limit_fraction` (of wealth). Constraints live in `constraints`, costs in `costs`: + +```json +{"mode": "new", "cash": 1000000, "horizon": "week", "confidence": 0.95, "cvar_limit": 30000, + "risk_drift": "zero", + "constraints": {"max_weight": 0.10, "max_sector_weight": 0.25, "max_positions": 25, + "min_position_value": 5000, "min_invested": 0.95}, + "costs": {"proportional_buy": 0.001, "proportional_sell": 0.001, "fixed_per_trade": 0}} +``` + +**Rebalance an existing one.** `mode="rebalance"` with `holdings` (shares) and the `cash` available. +Transaction costs are charged and the result carries a `before` block, so you can show the client +both states and what the trade bought them: + +```python +res = qc.optimize(mode="rebalance", holdings={"NESN.SW": 500, "AAPL": 100}, cash=50_000, + horizon="week", cvar_limit=30_000, risk_drift="zero", + constraints={"max_turnover": 200_000}) +res["before"]["cvar"], res["cvar"] # risk before and after +res["total_cost"], res["turnover"] # what the change costs +[p for p in res["positions"] if p["trade"] != 0] # the trades to place +``` + +## 10. Reading a result + +Every optimisation answers with its own provenance. The fields you will quote: + +| field | meaning | +|---|---| +| `status` | `Optimal` \| `FeasibleFound` (time limit — check the gap) \| `Infeasible` \| `Error` | +| `solver` | `cuopt` (GPU) or `highs` (CPU) — which one actually produced this | +| `reproducibility` | `exact` (HiGHS) \| `within_gap` (cuOpt, 1e-4) \| `none` | +| `expected_pnl`, `expected_return` | over the horizon, in CHF and as a fraction | +| `var`, `cvar`, `cvar_limit`, `confidence` | the risk figures and the limit they had to respect | +| `positions` | per stock: `shares`, `shares_before`, `trade`, `value`, `weight`, `cost`, `expected_return_ann` | +| `sector_exposure`, `invested`, `cash_end`, `total_cost`, `turnover` | the resulting portfolio | +| `model` | covariance estimator, scenario model and seed, expected-return model, the views and the Ω that was calibrated from them | +| `scenario_key`, `scenario_hash` | what the scenarios depend on, and a fingerprint of the draws | +| `warnings` | **read these** — see below | +| `timings` | seconds per stage | + +So a sentence in your advisory output can be backed end to end: *"CVaR 95 % = CHF 29 999 over one +week (mean–CVaR MILP, EWM λ = 0.99, GBM scenarios, seed 42, solved to optimality on HiGHS)."* + +**`warnings` is not decoration.** It is where the service tells you that the answer is technically +correct and economically doubtful: a risk limit that could not bind, a covariance estimate too +singular for the number of candidates, too few scenarios per candidate, an expected return above +60 % p.a. on a stock it decided to hold. Surface them or handle them; do not drop them. + +**Determinism.** The same request gives the same scenarios (`scenario_hash`) and the same portfolio, +on the same machine. Across machines — a different numpy or pandas — the `scenario_key` still +matches while the digest may not, because floating-point results agree to 13–14 digits rather than +bit for bit; the figures agree to well under a franc. Compare runs by `scenario_key`. For a demo +that must be identical next week, freeze the data with a snapshot (`POST /snapshots`). + +## 11. Things that will bite you + +* **The service answers one request at a time while a solve runs.** The MILP holds the interpreter. + Do not build a UI that fires an optimisation and then polls something else — it will look hung. +* **`time_limit` is the budget of the whole request**, not of each solver call (row generation and + the Gaussian mode solve a *sequence* of MILPs). Without it, a large candidate set on HiGHS can run + for hours. Set it. 300 s is a sane default. +* **The GPU solver is optional.** `solver="auto"` uses cuOpt if it answers and HiGHS otherwise; the + result says which, and adds a warning. A cuOpt outage degrades speed, never correctness. +* **`risk_drift="model"` (the default) measures the CVaR on scenarios that include the expected + return.** At long horizons the drift grows with *h* while the spread grows with *√h*, so a bullish + forecast can push the whole loss tail above zero and the risk limit stops binding — the optimiser + will then happily propose something absurd. Use `risk_drift="zero"` for anything beyond a few + weeks; the objective still maximises expected return, but the risk measurement no longer believes + the forecast. +* **Scenario count drives stability.** The default is 16 per candidate (within 4 096…16 384). The + reported figures are stable from ~2 000 on, but the *composition* moves between near-equivalent + portfolios; about 64 per candidate pins it. The result warns below 8. +* **`scenario_model="gaussian_exact"` is fast but bounds the CVaR from below.** It replaces the + scenarios with linear cuts, and until those converge the limit can be overshot by a hair (a franc + or two on a million, reported in `warnings`). Use it to explore; use `"gbm"` or `"empirical"` when + the limit has to hold exactly. +* **`Infeasible` means your constraints contradict each other**, most often `min_invested` against a + tight `cvar_limit`, or a `max_turnover` that cannot reach the risk budget. Relax one and re-run. + +## 12. Troubleshooting + +| symptom | what it means | +|---|---| +| `cannot reach the quant-core API at ...` | no service there. `quantcore serve`, or fix `--url` / `QUANTCORE_API_URL` | +| `503 no market data yet` | the store is empty: `POST /jobs/setup` (~15 min) or mount a prepared data directory | +| `422` with a field name | your request does not match the schema; the message names the field. `/docs` has the valid values | +| `/health` shows `cuopt.reachable: false` | the GPU solver is down; optimisations fall back to HiGHS and say so | +| `FeasibleFound` instead of `Optimal` | the time limit hit; raise `time_limit`, or lower `n_scenarios` / `max_assets` | +| an optimisation "seems stuck" | it is solving, and the service is single-threaded. `timings` afterwards shows where the time went | +| numbers differ slightly from a colleague's | different environment, same inputs: compare `scenario_key`, not `scenario_hash` (§10) | +| Yahoo errors during a data job | rate limiting; the client retries and opens a circuit. Re-run later, the stored data is intact | + +Everything failing at once? `quantcore-api selftest` tells you whether it is the service or you. + +## 13. Where the details are + +| document | what is in it | +|---|---| +| `/docs` | every endpoint and field, interactive — the contract | +| `README.md` | the services, the library API, quick start | +| `docs/ARCHITECTURE.md` | how the pieces fit, what depends on what, where the AI layer plugs in | +| `docs/METHODOLOGY.md` | every formula, estimator and convention, with the measured numbers | +| `docs/optimization_model.pdf` | the MILP derived from Rockafellar–Uryasev to whole shares, and the sampling theory | +| `docs/measurement_and_models.pdf` | risk and performance measures, GBM and empirical innovations, Black–Litterman with Idzorek confidence | diff --git a/docs/measurement_and_models.pdf b/docs/measurement_and_models.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5da216e535a56a8afb01a8c4e8711260892ec097 GIT binary patch literal 502567 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

oBY-vUKuEwYj#?uhIr~<`(Y|tFN!2W)hY8bE3lE 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

ERqN`J@n095(0z@_U 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

dalo*yzg>I07OnDZveF9+YA&BLA(iD~k*{O42%% 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 #zK>vjy_n;=` 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 fc*xlmH6w#^dj!M>X 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/measurement_and_models.tex b/docs/measurement_and_models.tex new file mode 100644 index 0000000..d1d9f45 --- /dev/null +++ b/docs/measurement_and_models.tex @@ -0,0 +1,807 @@ +% !TEX encoding = UTF-8 Unicode +% --------------------------------------------------------------------------------------------- +% quantcore -- documentation of the measurement layer, the scenario models and the +% expected-return model, as implemented in quantcore/measurement and quantcore/models. +% Build: latexmk -pdf docs/measurement_and_models.tex +% --------------------------------------------------------------------------------------------- +\documentclass[11pt,a4paper]{article} +\usepackage[utf8]{inputenc} +\usepackage[T1]{fontenc} +\usepackage[british]{babel} +\usepackage{lmodern} +\usepackage{microtype} +\usepackage[margin=2.4cm]{geometry} +\usepackage{amsmath,amssymb,amsthm,mathtools} +\usepackage{booktabs,array} +\usepackage{enumitem} +\usepackage{xcolor} +\usepackage[colorlinks=true,linkcolor=blue!50!black,citecolor=blue!50!black,urlcolor=blue!50!black]{hyperref} + +\newtheorem{theorem}{Theorem}[section] +\newtheorem{proposition}[theorem]{Proposition} +\theoremstyle{definition} +\newtheorem{definition}[theorem]{Definition} +\newtheorem{example}[theorem]{Example} +\theoremstyle{remark} +\newtheorem{remark}[theorem]{Remark} + +\DeclareMathOperator{\CVaR}{CVaR} +\DeclareMathOperator{\VaR}{VaR} +\DeclareMathOperator{\E}{\mathbb{E}} +\DeclareMathOperator{\Var}{Var} +\DeclareMathOperator{\Cov}{Cov} +\DeclareMathOperator{\diag}{diag} +\DeclareMathOperator{\tr}{tr} +\newcommand{\R}{\mathbb{R}} +\newcommand{\pos}[1]{\left[#1\right]^{+}} +\newcommand{\code}[1]{\texttt{\hyphenchar\font=-1 #1}} +\newcommand{\bs}{\boldsymbol} +\setlength{\parskip}{4pt} +\setlength{\parindent}{0pt} + +\title{Risk and Performance Measurement, Scenario Generation\\ and the Expected-Return Model\\[1ex] +\large From log-returns to VaR/CVaR, from the covariance estimators to simulated horizon +scenarios, and from market capitalisations to Black--Litterman expected returns with +Idzorek confidence views, as implemented in \code{quantcore}} +\author{AISE502 -- AI-Augmented Portfolio Intelligence Platform\\ \small Deterministic quant core, modules \code{quantcore.measurement} and \code{quantcore.models}} +\date{September 2026} + +\begin{document} +\maketitle + +\begin{abstract} +\noindent +This note is the companion of \emph{Mean--CVaR Portfolio Optimisation in Whole Shares} +(the file \code{optimization\_model.pdf} in \code{docs/}). It documents the three model layers that feed the +optimiser and the platform's risk and performance figures: (i) the \emph{measurement} layer --- +log-returns in CHF, moving-window and exponentially weighted estimators of mean, volatility and +covariance, Value-at-Risk and Conditional Value-at-Risk at 95\,\% and 99\,\% for a day, a week +or a month, and the performance statistics; (ii) the \emph{stock-price models} that turn the +latest covariance into a set of horizon scenarios --- geometric Brownian motion and the +empirical-innovation model $\varepsilon_t=\Sigma_{t-1}^{-1/2}r_t$, $r(j\,|\,T)=\Sigma_T^{1/2}\varepsilon_j$; +and (iii) the \emph{expected-return model} --- market-capitalisation weights, the broad-market +average, reverse optimisation, and the Black--Litterman posterior with views whose uncertainty is +calibrated from a degree of belief following Idzorek. Every formula is stated with its +conventions and defaults, the numerical safeguards (covariance shrinkage, innovation +re-standardisation) are motivated with measurements on the platform's own data set, and the last +section maps the mathematics to the functions, parameters and output fields of the code. +\end{abstract} + +\tableofcontents + +% ============================================================================================ +\section{Data, conventions and notation} +% ============================================================================================ + +The universe consists of $N$ stocks with adjusted closing prices $P_i(t)$ in their listing +currency $c(i)$, observed on the \emph{union calendar} of all exchanges (a day on which at least +one exchange in the universe trades). A holiday of one exchange is filled with that exchange's +previous close (at most five days), so every trading day of the union calendar has a price for +every stock. Prices that are cleaned away (broken adjustments, spike ticks; see +\code{docs/METHODOLOGY.md}, Section~1) are treated the same way. + +FX rates $X_c(t)$ are quoted as CHF per one unit of currency $c$ (so $X_{\mathrm{CHF}}\equiv1$), +and the base currency of everything below is CHF. + +Three frequencies are used: \emph{daily} (union calendar, 252 periods per year), \emph{weekly} +(Friday closes, 52) and \emph{monthly} (month-end closes, 12); $\pi$ denotes the number of +periods per year of the chosen frequency. A \emph{horizon} is expressed as a number $h$ of +periods: at daily frequency a day is $h=1$, a week $h=5$, a month $h=21$; at weekly frequency +a week is $h=1$ and a month $h=4$. + +Throughout, $r$ denotes a log-return, $R=e^{r}-1$ the corresponding simple return, and +\emph{losses are positive numbers}. Vectors are columns; $\mathbf 1$ is the vector of ones. + +% ============================================================================================ +\section{Returns} +% ============================================================================================ + +\subsection{Log-returns and the currency conversion} + +The one-period log-return of stock $i$ in its listing currency is +\begin{equation} + r_i(t) \;=\; \ln P_i(t) - \ln P_i(t-1). + \label{eq:logret} +\end{equation} +The price in CHF is $P_i^{\mathrm{CHF}}(t)=P_i(t)\,X_{c(i)}(t)$, hence its log-return is the +\emph{sum} of the local return and the FX return, +\begin{equation} + r_i^{\mathrm{CHF}}(t) \;=\; r_i(t) + \big(\ln X_{c(i)}(t) - \ln X_{c(i)}(t-1)\big), + \label{eq:chfret} +\end{equation} +exactly and without any cross term --- the reason log-returns are the working unit of the whole +platform. (With simple returns the conversion is $(1+R_i)(1+R_X)-1$, which is neither additive +nor symmetric.) The implementation converts prices with the FX frame and takes the log-return of +the product; \eqref{eq:chfret} is checked by a unit test. + +\subsection{Aggregation over time and over assets} + +Under the i.i.d.\ assumption of the price models (Section~\ref{sec:models}) log-returns add over +time: the $h$-period return is $r^{(h)}(t)=\sum_{k=0}^{h-1} r(t-k)$. The historical risk +measures use \emph{overlapping} $h$-period sums (more observations, mildly autocorrelated); +non-overlapping blocks are available as an option. + +Over assets, log-returns do not add. The exact log-return of a portfolio whose value weights +$w_i$ are fixed at the start of the period is +\begin{equation} + r_p \;=\; \ln\Big(1+\sum_i w_i\,(e^{r_i}-1)\Big), + \label{eq:portret} +\end{equation} +because simple returns aggregate linearly. A cash residual $1-\mathbf 1^\top w$ earns zero. This +formula (\code{portfolio\_log\_returns}) is used wherever a portfolio return series is needed +--- the historical portfolio VaR and the portfolio performance statistics. + +\subsection{Why log-returns are annualised additively} + +Means of log-returns scale with time: $\E[r^{(h)}]=h\mu$, and under independence +$\Var[r^{(h)}]=h\sigma^2$. An annualised log-return is therefore $\pi\mu$, an annualised +volatility $\sqrt{\pi}\,\sigma$; the geometric (compounded) annual return is $e^{\pi\mu}-1$. Both +are reported. + +% ============================================================================================ +\section{Moving-window and exponentially weighted estimators} +\label{sec:estimators} +% ============================================================================================ + +Every statistic exists in two flavours (\code{quantcore.measurement.estimators}). + +\subsection{Moving window} + +With window length $w$ (default 200 periods) the estimates at date $t$ use the observations +$t-w+1,\dots,t$ with equal weight $1/w$: the sample mean $m_t$, the sample standard deviation +(divisor $w-1$), the sample covariance +\begin{equation} + \Sigma_t^{\mathrm{roll}} \;=\; \frac{1}{w-1}\sum_{k=0}^{w-1}(r_{t-k}-m_t)(r_{t-k}-m_t)^\top . + \label{eq:rollcov} +\end{equation} +Rows with a missing value are dropped for matrices; a series needs at least $w/2$ observations +before a value is reported. + +\subsection{Exponential weighting} + +With decay $\lambda\in(0,1)$ the observation $k$ periods back receives the weight +$\lambda^k$ (RiskMetrics \cite{RiskMetrics1996}); the weights are normalised over the available +history, +\begin{equation} + \omega_k \;=\; \frac{\lambda^k}{\sum_{j=0}^{n-1}\lambda^j}, \qquad k=0,\dots,n-1, + \label{eq:ewmw} +\end{equation} +(pandas \code{ewm(alpha=1-$\lambda$, adjust=True)}), and the estimators are the weighted +moments +\begin{equation} + m_t^{\mathrm{ewm}} = \sum_k\omega_k\, r_{t-k}, \qquad + \Sigma_t^{\mathrm{ewm}} = \sum_k \omega_k\,(r_{t-k}-m_t^{\mathrm{ewm}})(r_{t-k}-m_t^{\mathrm{ewm}})^\top, + \label{eq:ewmcov} +\end{equation} +with the diagonal of \eqref{eq:ewmcov} as the EWM variance (\code{var(bias=True)}). Both +estimators are \emph{centred}: the corresponding mean is subtracted. The effective window is +$1/(1-\lambda)$ observations (half-life $\ln 0.5/\ln\lambda$): + +\begin{center} +\small +\begin{tabular}{@{}lcccc@{}} +\toprule +$\lambda$ & 0.94 & 0.97 & 0.99 & 0.997 \\ +\midrule +effective window $1/(1-\lambda)$ & 17 & 33 & 100 & 333 \\ +half-life (periods) & 11.2 & 22.8 & 69.0 & 230.7 \\ +weight of the observation 100 periods back & 0.002 & 0.048 & 0.366 & 0.741 \\ +\bottomrule +\end{tabular} +\end{center} + +$\lambda=0.94$ is the RiskMetrics value for daily data and the default of the measurement +services (fast adaptation, noisy); $\lambda=0.99$ is the default of the optimisation covariance +(a smoother matrix that still adapts within a quarter). For matrices the history is truncated +where the weights fall below $10^{-4}$ (917 observations for $\lambda=0.99$). + +\subsection{Sequences of matrices} + +The innovation filter of Section~\ref{sec:fhs} needs $\Sigma_{t-1}$ for \emph{every} date $t$, +i.e.\ the covariance estimated from the data up to and including $t-1$. Storing $T$ matrices of +size $N\times N$ is impossible for a thousand stocks, so \code{covariance\_sequence} streams +them: the rolling estimator recomputes \eqref{eq:rollcov} for each date ($O(wN^2)$ per step), +while the exponentially weighted estimator uses the exact recursion of the centred filter with +$\alpha=1-\lambda$, +\begin{equation} + d_t = r_t - m_{t-1}, \qquad m_t = m_{t-1} + \alpha\, d_t, \qquad + \Sigma_t = \lambda\,\Sigma_{t-1} + \alpha\, d_t\,(r_t-m_t)^\top , + \label{eq:ewmrec} +\end{equation} +which costs $O(N^2)$ per step and, after a warm-up of $\max(50, 2/(1-\lambda))$ observations +initialised with \eqref{eq:ewmcov}, coincides with the normalised-weight estimate up to the +factor $1/(1-\lambda^n)\to1$ (verified by a unit test at 5\,\% for $\lambda=0.97$). + +\subsection{Shrinkage towards the diagonal} +\label{sec:shrinkage} + +A covariance estimated from $n$ effective observations is ill-conditioned when $N$ is not small +against $n$ and singular for $N>n$: the smallest eigenvalues are underestimated, the largest +overestimated (the Marchenko--Pastur effect), and anything that inverts the matrix --- the +innovation filter, mean-variance weights --- amplifies the error. The classical remedy is +linear shrinkage \cite{LedoitWolf2004} towards a structured target; the platform shrinks towards +the diagonal (the target of Schäfer and Strimmer \cite{SchaeferStrimmer2005}), which keeps the +variances and pulls the correlations towards zero: +\begin{equation} + \Sigma^{(s)} \;=\; (1-s)\,\Sigma + s\,\diag(\Sigma), \qquad s\in[0,1]. + \label{eq:shrink} +\end{equation} +The intensity can be given, or estimated from the data (\code{shrinkage="auto"}): for centred +observations $x_k$ ($k=1,\dots,n$), products $w_{kij}=x_{ki}x_{kj}$ and sample covariances +$s_{ij}=\frac{n}{n-1}\bar w_{ij}$, the intensity that minimises the expected squared Frobenius +loss of \eqref{eq:shrink} is +\begin{equation} + s^\ast \;=\; \frac{\sum_{i\ne j}\widehat{\Var}(s_{ij})}{\sum_{i\ne j}s_{ij}^2}, + \qquad + \widehat{\Var}(s_{ij}) \;=\; \frac{n}{(n-1)^3}\sum_{k=1}^n \big(w_{kij}-\bar w_{ij}\big)^2 , + \label{eq:ss} +\end{equation} +clipped to $[0,1]$. It is small for a long, strongly correlated sample and approaches one when +$N$ approaches $n$ --- exactly when the sample covariance becomes unreliable. For the EWM +estimator the intensity is computed on the last $3/(1-\lambda)$ observations with equal weights. +On the platform's data set (daily CHF returns, $\lambda=0.99$) the automatic intensity is 0.047 +for the 100 largest stocks, 0.069 for 300 and 0.114 for the full universe of 745 names. The +sequence \eqref{eq:ewmrec} uses one intensity for all its matrices. + +% ============================================================================================ +\section{Risk measures} +\label{sec:risk} +% ============================================================================================ + +\subsection{Loss, VaR and CVaR} + +For a position of value $V_0$ and a horizon log-return $r$, the loss in return space is +$\ell=-r$ and in money it is $V_0(1-e^{r})=V_0(1-e^{-\ell})$. The Value-at-Risk at level +$\beta$ is the $\beta$-quantile of the loss distribution, the Conditional Value-at-Risk (expected +shortfall) the mean of the loss beyond it, +\begin{equation} + \VaR_\beta = q_\beta(\ell), \qquad + \CVaR_\beta = \frac{1}{1-\beta}\int_\beta^1 q_\gamma(\ell)\,d\gamma + \;\;\big(=\E[\ell\mid\ell\ge\VaR_\beta]\text{ for continuous }\ell\big), + \label{eq:varcvar} +\end{equation} +see \cite{AcerbiTasche2002,MFE2015}. The platform reports both at $\beta=0.95$ and $0.99$. +Because $\ell\mapsto V_0(1-e^{-\ell})$ is increasing, quantiles commute with it, and a +return-space VaR translates into money exactly as $V_0(1-e^{-\VaR_\beta})$; the CVaR in money +is computed the same way from the return-space CVaR (which is exact for VaR and a very good +approximation for CVaR: for a weekly CVaR of 4.6\,\% the difference between the exact tail mean +of $V_0(1-e^{-\ell})$ and $V_0(1-e^{-\CVaR})$ is below 0.1\,\% of $V_0$; the scenario-based +portfolio risk of Section~\ref{sec:portfoliorisk} works in money directly and is exact). + +\subsection{Gaussian (GBM) VaR and CVaR} + +If $r\sim N(\mu,\sigma^2)$ then $\ell=-r\sim N(-\mu,\sigma^2)$, its $\beta$-quantile is +$-\mu+z_\beta\sigma$ with $z_\beta=\Phi^{-1}(\beta)$, and the tail mean follows from +$\E[Z\mid Z\ge z_\beta]=\varphi(z_\beta)/(1-\beta)$ for a standard normal $Z$: +\begin{equation} + \VaR_\beta = -\mu + z_\beta\,\sigma, \qquad + \CVaR_\beta = -\mu + \sigma\,\frac{\varphi(z_\beta)}{1-\beta}. + \label{eq:normal} +\end{equation} +The multipliers are $z_{0.95}=1.645$, $\varphi(z_{0.95})/0.05=2.063$, $z_{0.99}=2.326$, +$\varphi(z_{0.99})/0.01=2.665$. Under the i.i.d.\ assumption the horizon parameters are +$\mu_h=h\mu$ and $\sigma_h=\sqrt h\,\sigma$; $\mu$ and $\sigma$ come from either estimator of +Section~\ref{sec:estimators}. The mean is kept (a positive drift lowers the loss quantile +one-for-one); for daily data it is negligible, for a month it is not. + +\begin{example}[Reference values] + A stock with daily volatility $\sigma=1\,\%$ and zero mean, one week ($h=5$, + $\sigma_h=2.236\,\%$): $\VaR_{0.95}=3.678\,\%$, $\CVaR_{0.95}=4.612\,\%$, $\VaR_{0.99}=5.202\,\%$, + $\CVaR_{0.99}=5.960\,\%$. For a position of CHF~100\,000 the exact money figures are + $\VaR_{0.95}=3\,611$, $\CVaR_{0.95}=4\,508$, $\VaR_{0.99}=5\,069$, $\CVaR_{0.99}=5\,786$ (the + linear approximation $V_0\cdot\CVaR$ would give 4\,612 and 5\,960). The daily figures + $\VaR_{0.95}=1.645\,\%$, $\CVaR_{0.95}=2.063\,\%$, $\VaR_{0.99}=2.326\,\%$, $\CVaR_{0.99}=2.665\,\%$ + and the weekly ones are pinned by \code{tests/test\_risk.py}. +\end{example} + +\subsection{Historical VaR and CVaR} + +The empirical counterparts replace the Gaussian by the empirical distribution of the realised +$h$-period returns $r^{(h)}$ over a window of $\max(5w,750)$ periods: +\begin{equation} + \VaR_\beta^{\mathrm{hist}} = -\hat q_{1-\beta}\big(r^{(h)}\big), \qquad + \CVaR_\beta^{\mathrm{hist}} = -\frac{1}{|\mathcal T|}\sum_{t\in\mathcal T} r^{(h)}_t, \quad + \mathcal T=\big\{t: r^{(h)}_t\le \hat q_{1-\beta}\big\}, + \label{eq:hist} +\end{equation} +with $\hat q$ the empirical quantile (linear interpolation). They capture fat tails and skewness +that the Gaussian misses, at the price of noise: at $\beta=0.99$ with 750 daily observations the +tail contains eight points. + +\subsection{Discrete scenario sets} + +For $S$ equiprobable losses (a simulated scenario set, Section~\ref{sec:models}) the definition +\eqref{eq:varcvar} evaluates exactly: with $k=(1-\beta)S$ and the losses sorted decreasingly +$\ell_{(1)}\ge\ell_{(2)}\ge\dots$, +\begin{equation} + \CVaR_\beta = \frac1k\Big(\sum_{s=1}^{\lfloor k\rfloor}\ell_{(s)} + (k-\lfloor k\rfloor)\,\ell_{(\lfloor k\rfloor+1)}\Big), + \qquad \VaR_\beta = \ell_{(\lceil k\rceil)} , + \label{eq:scen} +\end{equation} +which is the value the Rockafellar--Uryasev linear programme of the optimisation returns +(function \code{scenario\_cvar}; see Remark~2.3 of the companion note). For $S=2\,000$ and +$\beta=0.99$ the tail has $k=20$ scenarios. + +\subsection{Per-asset tables and time series} + +\code{RiskService.asset\_risk} returns, for a list of stocks, a frequency, a horizon, a window +$w$ and a decay $\lambda$, one row per stock with the annualised rolling and EWM volatilities and, +for each confidence level $\beta\in\{0.95,0.99\}$, the Gaussian VaR and CVaR from the rolling +estimator, the same from the EWM estimator, and the historical VaR and CVaR --- six figures per +level, all in log-return units over the horizon (Table~\ref{tab:impl}). \code{risk\_series} gives +the same quantities as time series: Gaussian figures from the rolling / EWM volatility and mean +series, historical ones from rolling quantiles and rolling tail means of the one-period returns, +scaled by $\sqrt h$ for longer horizons (an approximation used only in the series). + +\subsection{Portfolio risk in money} +\label{sec:portfoliorisk} + +A portfolio is given in \emph{numbers of shares} $n_i$ plus cash $C$. With today's CHF prices +$P_i$ the value is $V_0=C+\sum_iP_in_i$ and the value weights are $w_i=P_in_i/V_0$ (the cash +weight $C/V_0$ is riskless). Three methods are offered (\code{portfolio\_risk}): +\begin{itemize}[nosep] + \item \textbf{normal}: $\sigma_p^2=w^\top\Sigma w$ with the rolling or EWM covariance, + $\mu_p=w^\top m$ with the window mean, then \eqref{eq:normal} with $h\mu_p$ and + $\sqrt h\sigma_p$, converted to money; + \item \textbf{historical}: the realised portfolio return series \eqref{eq:portret} over the + last $\max(5w,750)$ periods, aggregated to $h$ periods, then \eqref{eq:hist}; + \item \textbf{scenarios}: money P\&L $\Pi_s=V_0\sum_iw_i(e^{r_{s,i}}-1)$ for each scenario of a + simulated set, then \eqref{eq:scen} on the losses $-\Pi_s$ --- exact in money, and + precisely the risk the optimiser constrains. +\end{itemize} +The result (\code{PortfolioRisk}) carries the value, the weights, the horizon, the method, the +annualised portfolio volatility, and per confidence level the VaR and CVaR in return space and in +money. A unit test checks that Gaussian scenarios with the same mean and covariance reproduce the +normal figures within sampling error. + +% ============================================================================================ +\section{Performance measures} +% ============================================================================================ + +All statistics (\code{performance\_table}) are computed from one-period log-returns $r_t$, +$t=1,\dots,T$, at the chosen frequency with $\pi$ periods per year and the annual CHF risk-free +rate $r_f$ (0 by default): +\begin{align} + \text{cumulative return} &= e^{\sum_t r_t}-1, \quad + \text{ann.\ log-return} = \pi\,\bar r, \quad + \text{ann.\ return} = e^{\pi\bar r}-1, \notag\\ + \text{ann.\ volatility} &= \sqrt{\pi}\,\hat\sigma, \quad + \text{Sharpe} = \frac{\pi\bar r - r_f}{\sqrt\pi\,\hat\sigma}, \quad + \text{Calmar} = \frac{e^{\pi\bar r}-1}{-\mathrm{MDD}}, \notag\\ + \text{Sortino} &= \frac{\pi\bar r - r_f}{\sqrt{\pi\cdot\frac1T\sum_t \min(r_t,0)^2}}, + \label{eq:perf} +\end{align} +where $\hat\sigma$ is the sample standard deviation (divisor $T-1$) and the maximum drawdown +$\mathrm{MDD}=\min_t\big(V_t/\max_{s\le t}V_s-1\big)$ is taken on the value path +$V_t=\exp(\sum_{s\le t}r_s)$. Skewness and excess kurtosis of $r_t$, the hit ratio (share of +positive periods), the best and the worst period, the number of observations and the start and +end dates complete the row. + +\emph{Rolling} versions use the window mean and standard deviation of +Section~\ref{sec:estimators} for the annualised return, volatility and Sharpe ratio, plus the +cumulative return over the window and the running drawdown; \emph{EWM} versions use the +exponentially weighted mean and standard deviation. + +For a portfolio in shares the return series is either the \emph{constant-mix} series +\eqref{eq:portret} with today's value weights (the natural benchmark of a rebalanced +allocation) or the \emph{buy-and-hold} series $\ln(V_t/V_{t-1})$ with $V_t=\sum_iP_i(t)n_i$ +(the share numbers held over the whole history). Both are available; the API's +\code{/performance/portfolio} defaults to constant mix. + +% ============================================================================================ +\section{Stock-price models and scenario generation} +\label{sec:models} +% ============================================================================================ + +The optimiser and the scenario-based portfolio risk need a joint distribution of the horizon +log-returns of the candidate stocks. Two models are implemented +(\code{quantcore.models.simulation}); both assume that one-period log-returns are i.i.d.\ (no +autocorrelation), so that an $h$-period return is the sum of $h$ independent one-period draws, +both use the \emph{latest} covariance $\Sigma_T$ of a \code{CovarianceModel} (rolling window or +EWM, with shrinkage), and both take the expected returns $\mu$ from the expected-return model of +Section~\ref{sec:er} (converted to the log-drift $m$ as described in Section~\ref{sec:sampling-mm}). +The output is an $S\times N$ matrix of horizon log-returns $r_{s,i}$, optionally with scenario +probabilities, together with its provenance (model, covariance model and the shrinkage actually +applied, sampling scheme, seed, horizon, date, a fingerprint of the matrix), the \code{ScenarioSet}. + +\subsection{Geometric Brownian motion} + +Under multivariate GBM the horizon log-return is multivariate normal, +\begin{equation} + r^{(h)} \;\sim\; N\big(h m,\; h\Sigma_T\big), + \label{eq:gbm} +\end{equation} +sampled as $r_s=hm+\sqrt h\,A z_s$ with $z_s\sim N(0,I)$ and $AA^\top=\Sigma_T$ (the +eigen-root $V\Lambda^{1/2}$ with eigenvalues descending by default, so that the first coordinate +of $z_s$ drives the market factor; the symmetric and Cholesky roots give the same distribution). +The draws $z_s$ are scrambled Sobol' points passed through $\Phi^{-1}$ and moment-matched +(Section~\ref{sec:sampling-mm}), so mean and covariance of the set are exact. GBM is the natural +partner of the Gaussian risk figures of Section~\ref{sec:risk} and of mean-variance thinking; it +has no fat tails, no skewness and no tail dependence. + +\subsection{Empirical innovations (filtered historical simulation)} +\label{sec:fhs} + +The second model keeps the shape of the observed return distribution and only rescales it to +today's covariance. It is the multivariate form of filtered historical simulation +\cite{BaroneAdesi1999}. + +\paragraph{Filtering.} For every date $t$ after the warm-up of the estimator, the return vector +is standardised with the covariance known at the end of the previous day, +\begin{equation} + \varepsilon_t \;=\; \Sigma_{t-1}^{-1/2}\, r_t , + \label{eq:innov} +\end{equation} +where $\Sigma_{t-1}$ comes from the streamed sequence \eqref{eq:ewmrec} (or the rolling +recomputation) and $\Sigma^{-1/2}$ is the \emph{symmetric} inverse root $V\Lambda^{-1/2}V^\top$ +from the eigendecomposition $\Sigma=V\Lambda V^\top$ (eigenvalues floored at $10^{-10}$ of the +largest; the Cholesky root is available and used automatically above 250 stocks, where the +$O(TN^3)$ eigendecompositions become slow). If the model were exactly right, the innovations would +be uncorrelated with unit variance --- and, unlike Gaussian noise, they carry the empirical fat +tails, skewness and non-linear co-movements of the data. + +\paragraph{Resampling.} With $A=\Sigma_T^{1/2}$ (symmetric root) and log-drift $m$, the $s$-th +horizon scenario draws $h$ dates $j_1,\dots,j_h$ with replacement from the innovation history +and sets +\begin{equation} + r_s \;=\; hm + A\sum_{k=1}^{h}\tilde\varepsilon_{j_k} , + \label{eq:fhs} +\end{equation} +where $\tilde\varepsilon$ are the centred and re-standardised innovations described next. Since +the $h$ draws are independent with identity covariance, $\Cov(r_s)=hA A^\top=h\Sigma_T$, +consistent with \eqref{eq:gbm}, while the marginal shape of every asset and the joint tail +behaviour come from the data. The dates are drawn by scrambled Sobol' points in $h$ dimensions +mapped to the ranks of the innovations sorted by their market-factor component (so that every +day of the horizon covers the loss distribution evenly), alternatively uniformly (\code{"mc"}) +or balanced (every date equally often); the resampled sums are moment-matched to mean zero and +covariance $hI$ (Section~\ref{sec:sampling-mm}). + +\paragraph{Centring and re-standardisation.} The innovations are centred so that the scenario +mean equals the supplied drift exactly (the drift is the expected-return model's job, not the +sample mean's). Their variance, however, is systematically \emph{above one}: any lagged +volatility filter is too low right after a shock, so the standardised returns of the following +days are too large --- the classical reason why filtered historical simulation rescales the +residuals before resampling \cite{BaroneAdesi1999}. Measured on the platform's data set (daily +CHF returns 2016--2026, EWM $\lambda=0.99$, automatic shrinkage, symmetric root): + +\begin{center} +\small +\begin{tabular}{@{}lccc@{}} +\toprule +number of stocks $N$ & 10 & 30 & 100 \\ +\midrule +median variance of the raw innovations & 1.11 & 1.25 & 1.61 \\ +scenario volatility / model volatility without re-standardisation & 1.06 & 1.13 & 1.28 \\ +\bottomrule +\end{tabular} +\end{center} + +The growth with $N$ is the conditioning effect of Section~\ref{sec:shrinkage}: with an +effective window of 100 days and 100 stocks, $\Sigma_{t-1}$ is nearly singular and its inverse +root inflates the directions it underestimates. The implementation therefore rescales the +centred innovation sample before resampling: \code{standardize="diagonal"} (default) divides +each asset's innovations by their sample standard deviation, \code{"full"} whitens them with +the inverse root of their sample covariance (only when the history has at least $3N$ dates). +After rescaling, the per-asset scenario volatility matches the model volatility within +$\pm2\,\%$ (diagonal) or $\pm0.5\,\%$ (full), and the scenario correlations match the model's +with a mean absolute error of 0.02 (diagonal) or 0.004 (full). The diagonal option leaves every +asset's marginal distribution intact and works for any $N$, which is why it is the default. +Finally, innovations beyond $\pm8$ are winsorised as a numerical guard against the same +conditioning effect (about 0.2\,\% of the values on the data set); this is a safeguard, not a +model choice, and it is reported. + +\subsection{Sampling, moment matching, the drift convention and importance sampling} +\label{sec:sampling-mm} + +\paragraph{Why not plain Monte Carlo.} The optimiser is a sample-average approximation: it sees +the model only through the scenario set, and the sample mean of a plain Monte-Carlo set of +$S$ scenarios is off by $\sigma_i/\sqrt S$ per asset --- for $S=2\,000$ and weekly volatilities +of 3--11\,\% that is 3--13\,\% per year, the size of the expected returns themselves. Measured +on the 30 largest stocks, two seeds then give portfolios that differ by 100\,\% of wealth and a +true expected profit 10\,\% below the optimum. The remedies (all in +\code{quantcore.models.sampling}, derived and measured in the optimisation note) are: +scrambled Sobol' points instead of pseudo-random draws ($S$ is rounded up to a power of two); +\emph{moment matching}, the affine transform $\tilde z_s=C^{-1/2}(z_s-\bar z)$ of the sample +that makes its (probability-weighted) mean and covariance exactly $0$ and $I$, so that every +scenario set has mean $hm$ and covariance $h\Sigma_T$ exactly and only the shape of the tail is +sampled; and the model expected returns in the optimiser's objective instead of the scenario +mean. With these, the turnover between two seeds falls to 4--6\,\%, the flatness of the optimum. + +\paragraph{Drift convention.} The expected-return model delivers \emph{arithmetic} expected +returns $\mu$ (Section~\ref{sec:er}): reverse optimisation and Black--Litterman are statements +about simple returns. The scenarios are generated in log-returns and turned into money with +$R=e^{r}-1$, whose expectation under normality is $e^{hm+h\sigma^2/2}-1$. The log-drift is +therefore the It\^o-corrected +\begin{equation} + m_i \;=\; \frac{\ln(1+h\mu_i)}{h}-\frac{\sigma_i^2}{2}, + \qquad\text{so that}\qquad \E\big[e^{r^{(h)}_i}-1\big]=h\mu_i + \label{eq:ito} +\end{equation} +exactly (\code{drift\_convention="arithmetic"}, the default). Using $\mu_i$ itself as the +log-drift (\code{"log"}) adds $\sigma_i^2/2$ to the expected simple return: on the data set the +median bonus over the 30 largest stocks is 5.4\,\% per year against a median $\mu$ of 8.9\,\%, +and for the most volatile names it is 20--34\,\% (Micron: $\mu=16.9\,\%$ would become +$65.6\,\%$) --- a strong and unintended tilt towards volatile stocks. The scenario set reports +the expected simple return per asset (\code{expected\_simple}) that the optimiser uses. + +\paragraph{Importance sampling.} Only the $(1-\beta)S$ scenarios in the tail matter for the +CVaR. By default (\code{importance\_sampling="auto"}) the sampling distribution is therefore +tilted towards the losses of the \emph{cap-weighted portfolio of the candidate stocks}: its loss +direction is $d=-A^\top v/\|A^\top v\|$, and two blocks of a quarter of the scenarios each are +shifted along $d$ by $z_{0.95}$ and $z_{0.99}$ (GBM: $z_s\mapsto z_s+c_kd$; empirical model: the +dates are drawn with exponentially tilted probabilities +$q^k_t\propto\exp(\vartheta_k d^\top\tilde\varepsilon_t)$), so that one set covers both usual +confidence levels. The shift is a covariance statement: in return space it is +$-c_k\Sigma v/\sqrt{v^\top\Sigma v}$, so each asset moves by its covariance with the reference +portfolio --- a high-beta stock is stressed hard, a negatively correlated one moves up --- and, +an exponential tilt of a Gaussian being a mean shift, the weighted set still has exactly the +model covariance and correlations. Every scenario carries the self-normalised likelihood ratio of the mixture +proposal as its probability $p_s$, bounded by $1/(1-\sum_k\varepsilon_k)$; the weighted set is +moment-matched with these probabilities, the optimiser uses $p_s$ in its CVaR rows and the risk +figures of Section~\ref{sec:risk} use the weighted tail. At equal $S$ the tail noise falls by a +factor 2--4; at $\beta=0.99$ 2\,048 tilted scenarios are as good as 8\,192 plain ones. Because +the direction comes from the market portfolio rather than from a solution of the optimisation, +the scenario set does not depend on the risk limit, the constraints or the confidence level --- +one set serves a whole family of runs (\code{"adaptive"} tilts towards a first-pass solution +instead, which is sharper at $\beta=0.99$ but gives up that property). + +\paragraph{Which model when.} GBM is the transparent baseline; the empirical model is the +default of the optimisation because CVaR is a tail measure and the empirical tails are heavier +than Gaussian ones (the innovation kurtosis on the data set is well above zero). Both models +produce identical second moments by construction, so differences in the optimised portfolios +are entirely due to the tail shape. + +\paragraph{Cost.} The filter needs one eigendecomposition per date: for 100 stocks and 2\,500 +dates about two seconds, for 745 stocks with the Cholesky root and 1\,500 dates about six +seconds (four BLAS threads). The innovations depend only on the data and the covariance model, +so they can be cached between optimisations with different views or constraints. + +% ============================================================================================ +\section{The expected-return model} +\label{sec:er} +% ============================================================================================ + +The scenario drift $\mu$ --- and therefore the mean side of the mean--CVaR trade-off --- comes +from a Black--Litterman model (\code{quantcore.models.black\_litterman}) on \emph{weekly} CHF +log-returns over the last ten years. Weekly data reduce the asynchronous-trading noise of the +daily union calendar, and ten years give $\approx520$ observations for the long-term covariance. +Everything in this section is per week; annual figures are $52\times$ the means and +$\sqrt{52}\times$ the volatilities. + +\subsection{Market capitalisation, weights and the broad-market average} + +The market capitalisation of stock $i$ in CHF is +$M_i(t)=\text{shares outstanding}_i\times P_i(t)\times X_{c(i)}(t)$ with today's shares +outstanding held constant back in time. The \emph{cap-weighted broad market index} of the whole +universe has the log-return +\begin{equation} + r_M(t) \;=\; \ln\Big(1+\sum_i \omega_i(t-1)\,\big(e^{r_i(t)}-1\big)\Big), \qquad + \omega_i(t-1)=\frac{M_i(t-1)}{\sum_jM_j(t-1)}, + \label{eq:index} +\end{equation} +i.e.\ \eqref{eq:portret} with weights that drift with prices between rebalancings, exactly like +an index. Its average over the ten-year window, converted to an arithmetic expectation, +$\mu_M=\overline{r_M}+\tfrac12\widehat{\Var}(r_M)$, is the platform's estimate of the +expected market return ("the average of a broad market weighted by current capitalisation"); +on the data set as of 2026-09-21 the mean log-return is 11.5\,\% per year with a volatility of +16.2\,\%, i.e.\ $\mu_M=12.9\,\%$. The current weights $w_i=M_i(T)/\sum_jM_j(T)$, renormalised over the candidate stocks +of an optimisation, are the \emph{market weights} of the model. + +\subsection{Reverse optimisation: the equilibrium returns} + +A mean-variance investor with risk aversion $\delta$ solves +$\max_w\; w^\top\mu-\frac{\delta}{2}w^\top\Sigma w$, whose first-order condition is +$\mu=\delta\Sigma w$. Black and Litterman \cite{BlackLitterman1992,HeLitterman1999} read this +backwards: if the market portfolio $w$ is the optimal portfolio of the representative investor, +the excess returns that make it optimal are the \emph{implied equilibrium returns} +\begin{equation} + \pi \;=\; \delta\,\Sigma\,w, \qquad + \delta \;=\; \frac{\mu_M - r_f}{w^\top\Sigma w}, + \label{eq:pi} +\end{equation} +where $\delta$ is chosen such that the market portfolio itself earns exactly the market excess +return: $w^\top\pi=\delta\,w^\top\Sigma w=\mu_M-r_f$. Here $\Sigma$ is the ten-year weekly +covariance of the candidate stocks and $r_f$ the weekly CHF risk-free rate (0 by default). The +equilibrium returns are proportional to each stock's covariance with the market portfolio --- +its beta --- and are the neutral starting point: an investor without views holds the market. +On the data set, with the 100 largest stocks as candidates (their market portfolio has a +volatility of 19.7\,\%), $\delta\approx3.3$; the equilibrium returns range from about 2.7\,\% +(Unilever, Nestl\'e --- low beta in CHF) to about 23\,\% (Nvidia) per year. + +\begin{remark} + $\mu_M$ is the average of the \emph{broad} index of all 745 stocks, whereas $w$ and $\Sigma$ + refer to the candidate subset of an optimisation. This follows the specification (one + market-wide average) and keeps $\delta$ stable across candidate sets; the alternative of + using the candidate portfolio's own average is a one-line change. +\end{remark} + +\subsection{The Black--Litterman posterior} + +The model treats the (unknown) vector of expected excess returns $\mu$ as a random variable with +the equilibrium as prior, $\mu\sim N(\pi,\tau\Sigma)$, where the scalar $\tau$ (0.05 by default) +expresses how uncertain the equilibrium is relative to the return covariance. $K$ views are +linear statements about $\mu$ with their own uncertainty, +\begin{equation} + P\mu \;=\; Q+\varepsilon, \qquad \varepsilon\sim N(0,\Omega), \quad \Omega=\diag(\omega_1,\dots,\omega_K), + \label{eq:views} +\end{equation} +where each row $p_k$ of $P$ ($K\times N$) is a portfolio: $p_k=e_i$ for an \emph{absolute} view +("stock $i$ will return $q_k$"), $p_k=e_i-e_j$ for a \emph{relative} view ("$i$ will outperform +$j$ by $q_k$"). Combining prior and views by Bayes' rule (equivalently, generalised least squares) +gives the posterior distribution of $\mu$ with mean and covariance +\begin{equation} + \mu_{\mathrm{BL}} = \big[(\tau\Sigma)^{-1}+P^\top\Omega^{-1}P\big]^{-1}\big[(\tau\Sigma)^{-1}\pi+P^\top\Omega^{-1}Q\big], \qquad + \Sigma_\mu = \big[(\tau\Sigma)^{-1}+P^\top\Omega^{-1}P\big]^{-1}. + \label{eq:bl1} +\end{equation} +The implementation uses the algebraically identical form that needs no inverse of $\tau\Sigma$ +(a $K\times K$ system only), which is numerically preferable and also valid when $\Sigma$ is +singular: +\begin{equation} + \mu_{\mathrm{BL}} = \pi + \tau\Sigma P^\top\big(P\tau\Sigma P^\top+\Omega\big)^{-1}(Q-P\pi), \qquad + \Sigma_{\mathrm{BL}} = \Sigma + \tau\Sigma - \tau\Sigma P^\top\big(P\tau\Sigma P^\top+\Omega\big)^{-1}P\tau\Sigma . + \label{eq:bl2} +\end{equation} +The posterior mean is the prior corrected in the direction of the views by the amount by which +they disagree with the prior ($Q-P\pi$), weighted by how confident they are relative to the +prior; $\Sigma_{\mathrm{BL}}=\Sigma+\Sigma_\mu$ is the covariance of the returns themselves +(return noise plus the remaining uncertainty about the mean) and is reported for completeness. +Without views, $\mu_{\mathrm{BL}}=\pi$. The posterior mean is the drift handed to the scenario +models: per day, $\mu=\mu_{\mathrm{BL}}^{\mathrm{ann}}/252$ (total return, i.e.\ excess plus +$r_f$). + +\subsection{Idzorek's confidence: from a degree of belief to $\Omega$} + +The weak point of the model is $\Omega$: nobody can state the variance of a view. Idzorek +\cite{Idzorek2005} replaces it by a \emph{confidence} $C_k\in(0,1]$ with a portfolio meaning. +Consider one view $k$ alone. At 100\,\% confidence ($\omega_k=0$) the posterior is +$\mu_{100}=\pi+\tau\Sigma p_k^\top(p_k\tau\Sigma p_k^\top)^{-1}(q_k-p_k\pi)$ and the +unconstrained mean-variance weights move from $w$ to $w_{100}=(\delta\Sigma)^{-1}\mu_{100}$. +Idzorek defines the confidence as the fraction of that move the investor wants to make: +the target weights are $w+C_k\,(w_{100}-w)$, and $\omega_k$ is the value for which the +posterior weights $w(\omega_k)=(\delta\Sigma)^{-1}\mu_{\mathrm{BL}}(\omega_k)$ hit the target +(in the paper by a one-dimensional least-squares search). For a single view this calibration +has a closed form. From \eqref{eq:bl2} with one view, +\begin{equation} + w(\omega_k)-w \;=\; (\delta\Sigma)^{-1}\tau\Sigma p_k^\top\,\frac{q_k-p_k\pi}{p_k\tau\Sigma p_k^\top+\omega_k} + \;=\; \frac{\tau}{\delta}\,p_k^\top\,\frac{q_k-p_k\pi}{p_k\tau\Sigma p_k^\top+\omega_k}, +\end{equation} +so the tilt is always in the direction $p_k^\top$ and its size relative to the full tilt +($\omega_k=0$) is $p_k\tau\Sigma p_k^\top/(p_k\tau\Sigma p_k^\top+\omega_k)$. Setting this equal to +$C_k$ gives +\begin{equation} + \boxed{\;\omega_k \;=\; \frac{1-C_k}{C_k}\;p_k\,\tau\Sigma\,p_k^\top\;} + \label{eq:idzorek} +\end{equation} +\cite{Walters2014}: the view's variance is the prior variance of the view portfolio scaled by +the odds against the view; $C_k=1$ gives $\omega_k=0$ (the view is imposed), $C_k=0.5$ gives +$\omega_k=p_k\tau\Sigma p_k^\top$ (the He--Litterman choice, available as +\code{omega\_method="he\_litterman"}), $C_k\to0$ removes the view. The implementation uses +\eqref{eq:idzorek} for every view and then computes the joint posterior \eqref{eq:bl2}; the +numerical search of the paper is kept as \code{idzorek\_omega\_numeric} and a unit test shows +that both agree to $10^{-4}$. With several views, each $\omega_k$ is calibrated for its view +alone, as in the paper. + +\begin{example}[Four assets] + \label{ex:bl} + Annual volatilities $(20,25,15,30)\,\%$, correlations + $\rho_{12}=0.5,\rho_{13}=0.3,\rho_{14}=0.2,\rho_{23}=0.4,\rho_{24}=0.1,\rho_{34}=0.3$, market + weights $(0.4,0.3,0.2,0.1)$, market return 7\,\%, $r_f=1\,\%$, $\tau=0.05$, weekly + covariance $\Sigma=\Sigma^{\mathrm{ann}}/52$. Then $w^\top\Sigma^{\mathrm{ann}}w=0.0250$ + (market volatility 15.8\,\%), $\delta=0.06/0.0250=2.40$, and the equilibrium total returns + are $\pi+r_f=(7.36,\ 8.80,\ 4.35,\ 5.50)\,\%$ --- the market portfolio earns + $0.4\cdot7.36+0.3\cdot8.80+0.2\cdot4.35+0.1\cdot5.50=7.00\,\%$. An absolute view "asset 2 + returns 15\,\%" with confidence $C=0.4$ gives $\omega=\frac{0.6}{0.4}\,p\tau\Sigma p^\top=1.5\times + 6.01\cdot10^{-5}=9.01\cdot10^{-5}$ (weekly units; the numerical search returns the same value) + and the posterior total returns $(8.35,\ 11.28,\ 4.94,\ 5.80)\,\%$: asset 2 moves 40\,\% of the + way from 8.80\,\% to the 15\,\% it would reach at full confidence, and the correlated assets + follow. The unconstrained weights move from $0.3$ to $0.4656$ for asset 2 --- exactly + $0.3+0.4\,(0.7139-0.3)$ --- and stay unchanged for the others, as an absolute view on one + asset only tilts that asset. The numbers are pinned by \code{tests/test\_black\_litterman.py}. +\end{example} + +\subsection{Views from the AI layer: sentiment with a degree of belief} + +The platform's news pipeline delivers, per stock, an assessment "positive / negative" with a +degree of belief rather than a return forecast. \code{view\_from\_sentiment} turns +$(\text{stock } i,\ \text{sentiment } s\in[-1,1],\ \text{confidence } C)$ into the absolute view +\begin{equation} + q \;=\; \pi_i^{\mathrm{ann}} + r_f^{\mathrm{ann}} + s\cdot\gamma\cdot\sigma_i^{\mathrm{ann}}, \qquad \gamma=0.25 \text{ by default}, + \label{eq:sentiment} +\end{equation} +i.e.\ a fully positive assessment claims one quarter of the stock's annual volatility above its +equilibrium return (about $+5\,\%$ for a 20\,\%-volatility stock), and the degree of belief +becomes the Idzorek confidence $C$. The strength $\gamma$ is a modelling parameter; the view +carries the provenance of the insight that produced it (\code{source}). On the data set, a +sentiment of $+0.8$ with confidence $0.6$ on Nestl\'e states $q=6.1\,\%$ against an equilibrium +of $2.7\,\%$, calibrates $\omega=1.8\cdot10^{-5}$ and lifts the posterior to $4.7\,\%$ per +year. Explicit views (\code{assets}, \code{q}, \code{confidence}), including relative ones, are +accepted alongside. + +\subsection{Units and alternatives} + +Views state \emph{annual total} returns; internally they are converted to weekly excess returns +($q/52$ minus the weekly $r_f$ times the sum of the view weights, which vanishes for relative +views). Results are reported per week and annualised; the optimiser receives the posterior +total return per day, $\mu_{\mathrm{BL}}^{\mathrm{ann}}/252$, as the \emph{arithmetic} +expected return, converts it to the It\^o-corrected log-drift \eqref{eq:ito} of \eqref{eq:gbm} +and \eqref{eq:fhs}, and uses the expected simple return over the horizon, +$h\mu_{\mathrm{BL}}^{\mathrm{ann}}/252$, in its objective. Inputs that are log-return means +(the index average, the historical means) are converted with $+\tfrac12\sigma^2$. Two simpler +models are available for comparison: \code{expected\_returns="historical"} (the ten-year weekly +means, converted) and \code{"zero"} (no drift, pure risk minimisation). + +% ============================================================================================ +\section{Implementation map} +% ============================================================================================ + +\begin{table}[h] +\centering +\footnotesize +\begin{tabular}{@{}p{3.2cm}p{5.6cm}p{6.4cm}@{}} +\toprule +concept & code & parameters (defaults) / output \\ +\midrule +log-returns, alignment, frequencies, horizons & \code{measurement.returns}: \code{log\_returns}, \code{align\_prices}, \code{resample\_prices}, \code{horizon\_periods}, \code{horizon\_returns}, \code{portfolio\_log\_returns} & \code{freq} daily $|$ weekly $|$ monthly; \code{horizon} day $|$ week $|$ month $|$ integer \\ +CHF conversion & \code{data.fx.FXConverter}; \newline \code{MarketDataService.prices}, \code{.returns} & \code{currency="base"} (CHF) $|$ \code{"local"} \\ +rolling / EWM series & \code{estimators.rolling\_mean}, \code{rolling\_vol}, \code{ewm\_mean}, \code{ewm\_vol} & \code{window=200}, \code{lam=0.94}; annualised with the periods per year \\ +covariance matrices & \code{estimators.rolling\_cov}, \code{ewm\_cov}, \code{covariance}, \code{cov\_to\_corr}, \code{covariance\_sequence}; \newline \code{models.covariance.CovarianceModel} & \code{method} rolling $|$ ewm, \code{window}, \code{lam}, \code{shrinkage} $\in[0,1]$ or \code{"auto"} \\ +VaR / CVaR & \code{risk.normal\_var}, \code{normal\_cvar}, \code{historical\_var}, \code{historical\_cvar}, \code{scenario\_cvar}, \code{loss\_to\_money} & $\beta\in\{0.95,0.99\}$; losses positive; money via $V_0(1-e^{-\ell})$ \\ +per-asset risk table & \code{RiskService.asset\_risk} $\to$ \code{asset\_risk\_table} & columns \code{mean\_rolling\_ann}, \code{vol\_rolling\_ann}, \code{vol\_ewm\_ann}, \code{var95\_normal\_rolling}, \code{cvar95\_normal\_rolling}, \code{var95\_normal\_ewm}, \code{cvar95\_normal\_ewm}, \code{var95\_hist}, \code{cvar95\_hist} (and the same for 99), \code{horizon\_periods}, \code{n\_obs}, \code{as\_of} \\ +risk time series & \code{RiskService.risk\_series} & same names, one row per date \\ +portfolio risk & \code{RiskService.portfolio\_risk} $\to$ \code{PortfolioRisk} & \code{holdings} (shares), \code{cash}, \code{method} normal $|$ historical $|$ scenarios, \code{cov\_method}; per level \code{var}, \code{cvar}, \code{var\_money}, \code{cvar\_money} \\ +performance & \code{performance\_table}, \code{rolling\_performance}, \code{ewm\_performance}, \code{PerformanceService} & \code{risk\_free} from the settings; \code{rebalanced=True} = constant mix \\ +scenario models & \code{models.simulation.GBMSimulator}, \newline \code{EmpiricalInnovationSimulator} $\to$ \code{ScenarioSet} & \code{n\_scenarios=4096}, \code{seed=42}, \code{sampling} sobol $|$ mc $|$ antithetic $|$ lhs $|$ balanced, \code{moment\_match=True}, \code{tilt} (\code{TiltSpec}: direction, shifts, mixtures), \code{drift\_convention} arithmetic $|$ log, \code{root}, \code{standardize} diagonal $|$ full $|$ None, \code{clip=8}, \code{max\_history=2500}; output \code{probabilities}, \code{expected\_simple}, \code{hash} \\ +sampling schemes & \code{models.sampling}: \code{standard\_normal\_sample}, \code{moment\_match}, \code{pca\_root}, \code{tilt\_normal\_sample}, \code{tilted\_resample}, \code{resample\_indices}, \code{scenario\_hash} & Sobol' sizes are powers of two; likelihood ratios bounded by $1/(1-\sum_k\code{mixture}_k)$ \\ +one set per family of runs & \code{OptimizationService.scenario\_key}, \code{.market\_exposure}, \code{.clear\_scenario\_cache}; \code{ScenarioSet.with\_drift}, \code{.shocks}, \code{.shock\_hash} & the key excludes risk limit, confidence, constraints, costs, objective, cash, holdings and the expected-return model \\ +market index and weights & \code{MarketDataService}: \newline \code{cap\_weighted\_index}, \code{market\_caps}, \code{market\_weights} & shares outstanding $\times$ price $\times$ FX \\ +Black--Litterman & \code{BlackLittermanModel}, \code{View}, \code{view\_from\_sentiment}, \code{idzorek\_omega} $\to$ \code{BlackLittermanResult} & \code{tau=0.05}, \code{periods\_per\_year=52}, \code{omega\_method} idzorek $|$ he\_litterman; \code{prior\_ann}, \code{posterior\_ann}, \code{omega}, \code{delta}, \code{market\_return\_ann} \\ +pipeline in the optimiser & \code{OptimizationService}: \newline \code{expected\_returns}, \code{scenarios} & \code{bl\_years=10}, \code{expected\_returns} black\_litterman $|$ historical $|$ zero, \code{views}, \code{covariance} = \{ewm, 0.99, auto\}, \code{scenario\_model} empirical $|$ gbm $|$ gaussian\_exact, \code{importance\_sampling} auto $|$ market $|$ adaptive $|$ off, \code{row\_generation} auto $|$ on $|$ off \\ +HTTP & \code{/risk/*}, \code{/performance/*}, \code{/model/expected-returns}, \code{/model/scenarios}, \code{/market/*} & see \code{/docs} \\ +\bottomrule +\end{tabular} +\caption{Where each concept lives in \code{quantcore}.} +\label{tab:impl} +\end{table} + +\paragraph{Determinism and provenance.} Every figure is a deterministic function of the stored +data and the stated parameters; simulations take an explicit seed. Every result carries the +parameters that produced it (window, $\lambda$, shrinkage applied, model, seed, horizon, date, +views with their calibrated $\omega$), so that the advisor layer can cite them and a reviewer +can recompute them. + +% ============================================================================================ +\begin{thebibliography}{99} +\bibitem{RiskMetrics1996} + J.P.~Morgan/Reuters, \emph{RiskMetrics --- Technical Document}, 4th edition, New York, 1996. +\bibitem{LedoitWolf2004} + O.~Ledoit and M.~Wolf, \emph{A well-conditioned estimator for large-dimensional covariance + matrices}, Journal of Multivariate Analysis 88(2), 365--411, 2004. +\bibitem{SchaeferStrimmer2005} + J.~Sch\"afer and K.~Strimmer, \emph{A shrinkage approach to large-scale covariance matrix + estimation and implications for functional genomics}, Statistical Applications in Genetics and + Molecular Biology 4(1), Article 32, 2005. +\bibitem{AcerbiTasche2002} + C.~Acerbi and D.~Tasche, \emph{On the coherence of expected shortfall}, + Journal of Banking \& Finance 26(7), 1487--1503, 2002. +\bibitem{MFE2015} + A.\,J.~McNeil, R.~Frey and P.~Embrechts, \emph{Quantitative Risk Management: Concepts, + Techniques and Tools}, revised edition, Princeton University Press, 2015. +\bibitem{BaroneAdesi1999} + G.~Barone-Adesi, K.~Giannopoulos and L.~Vosper, \emph{VaR without correlations for portfolios + of derivative securities}, Journal of Futures Markets 19(5), 583--602, 1999. +\bibitem{BlackLitterman1992} + F.~Black and R.~Litterman, \emph{Global portfolio optimization}, + Financial Analysts Journal 48(5), 28--43, 1992. +\bibitem{HeLitterman1999} + G.~He and R.~Litterman, \emph{The intuition behind Black--Litterman model portfolios}, + Goldman Sachs Investment Management Research, 1999. +\bibitem{Idzorek2005} + T.\,M.~Idzorek, \emph{A step-by-step guide to the Black--Litterman model: incorporating + user-specified confidence levels}, working paper, Ibbotson Associates, 2005 (also in + S.~Satchell (ed.), \emph{Forecasting Expected Returns in the Financial Markets}, 2007). +\bibitem{Walters2014} + J.~Walters, \emph{The Black--Litterman model in detail}, working paper (SSRN), 2014. +\end{thebibliography} + +\end{document} diff --git a/docs/optimization_model.pdf b/docs/optimization_model.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d553bb68f62932f15cf63b82709becf92296a023 GIT binary patch literal 573970 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%0DKV17RecAP%Q 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

+*)$os%($eBj=(-McTu*Kj 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�YBl`ID4O-*{G(;8i$IY0X4@}Y)0o23m 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 in the base currency +(CHF). The investor has cash $c^0\ge 0$ and, in the rebalancing case, existing holdings +$n^0_i\in\Z_{\ge 0}$ shares; the total wealth is +\begin{equation} + W \;=\; c^0 + \sum_{i=1}^N P_i\, n^0_i . + \label{eq:wealth} +\end{equation} +The uncertainty is described by $S$ equiprobable scenarios of simple returns over the horizon, +\begin{equation} + R_{s,i} \;=\; e^{\,r_{s,i}} - 1 , \qquad s = 1,\dots,S, + \label{eq:scenarios} +\end{equation} +where the log-returns $r_{s,i}$ (in CHF) are simulated by one of the stock-price models of the +platform (geometric Brownian motion or empirical innovations) with the expected returns of the +Black--Litterman model as drift; see \code{docs/METHODOLOGY.md}, Sections~6--7. Each scenario has +probability $1/S$. The confidence level of the risk measure is $\beta\in(0,1)$ (0.95 or 0.99). + +Throughout, \emph{losses are positive numbers}: a loss of $L$ means the portfolio is worth $L$ +less than at the start of the horizon. Vectors are columns, $\pos{z}=\max(z,0)$, and +$\mathbf{1}$ is the vector of ones. + +% ============================================================================================ +\section{Conditional Value-at-Risk and its linear representation} +% ============================================================================================ + +Let $L(x,Y)$ be the loss of a decision $x\in X\subseteq\R^N$ under the random outcome $Y$, with +distribution function $\Psi(x,\zeta)=\mathbb{P}\{L(x,Y)\le\zeta\}$. + +\begin{definition}[VaR and CVaR] + The \emph{Value-at-Risk} at level $\beta$ is the $\beta$-quantile of the loss, + $\VaR_\beta(x)=\min\{\zeta\in\R:\Psi(x,\zeta)\ge\beta\}$. The \emph{Conditional Value-at-Risk} + (expected shortfall) is the mean of the $\beta$-tail of the loss distribution, + \begin{equation} + \CVaR_\beta(x) \;=\; \frac{1}{1-\beta}\int_{\beta}^{1} \VaR_{\gamma}(x)\,d\gamma , + \label{eq:cvar-def} + \end{equation} + which for a continuous loss distribution equals $\E\big[L(x,Y)\,\big|\,L(x,Y)\ge\VaR_\beta(x)\big]$. + For distributions with atoms (in particular for a finite set of scenarios) the definition + \eqref{eq:cvar-def} splits the atom at the VaR appropriately \cite{RockafellarUryasev2002, + AcerbiTasche2002}. +\end{definition} + +CVaR is a coherent risk measure (monotone, sub-additive, positively homogeneous, translation +invariant), which VaR is not. Its value for optimisation purposes rests on the following +representation. + +\begin{theorem}[Rockafellar--Uryasev \cite{RockafellarUryasev2000,RockafellarUryasev2002}] + \label{thm:ru} + Define the auxiliary function + \begin{equation} + F_\beta(x,\zeta) \;=\; \zeta + \frac{1}{1-\beta}\,\E\Big[\pos{L(x,Y)-\zeta}\Big]. + \label{eq:F} + \end{equation} + Then $F_\beta(x,\cdot)$ is finite and convex, and + \begin{equation} + \CVaR_\beta(x) \;=\; \min_{\zeta\in\R} F_\beta(x,\zeta), + \qquad + \VaR_\beta(x) \;=\; \text{left endpoint of } \argmin_{\zeta} F_\beta(x,\zeta). + \label{eq:ru} + \end{equation} + If $L(x,Y)$ is convex in $x$, then $F_\beta$ is jointly convex in $(x,\zeta)$ and + \begin{equation} + \min_{x\in X}\CVaR_\beta(x) \;=\; \min_{(x,\zeta)\in X\times\R} F_\beta(x,\zeta), + \label{eq:ru-joint} + \end{equation} + with $(x^*,\zeta^*)$ minimising the right-hand side if and only if $x^*$ minimises the left-hand + side and $\zeta^*\in\argmin_\zeta F_\beta(x^*,\zeta)$. +\end{theorem} + +The theorem removes the need to know the VaR before computing the CVaR: both come out of one +minimisation over the scalar $\zeta$, and $\zeta$ can simply be added to the decision +variables. A second consequence is used for risk \emph{constraints} +\cite{KrokhmalPalmquistUryasev2002}: for any budget $\omega$, +\begin{equation} + \CVaR_\beta(x)\le\omega + \quad\Longleftrightarrow\quad + \exists\,\zeta:\; F_\beta(x,\zeta)\le\omega , + \label{eq:cvar-constraint} +\end{equation} +because the minimum over $\zeta$ is at most $\omega$ exactly when some $\zeta$ attains at most +$\omega$. + +\paragraph{Scenario approximation and linearisation.} +With $S$ equiprobable scenarios the expectation in \eqref{eq:F} becomes an average, +\begin{equation} + \tilde F_\beta(x,\zeta) \;=\; \zeta + \frac{1}{(1-\beta)S}\sum_{s=1}^{S} \pos{L_s(x)-\zeta}, + \qquad L_s(x)=L(x,Y_s). + \label{eq:F-scen} +\end{equation} +The positive parts are linearised with auxiliary variables $u_s\ge 0$: since $u_s\ge L_s(x)-\zeta$ +and $u_s\ge 0$ imply $u_s\ge\pos{L_s(x)-\zeta}$, and the objective/constraint pushes $u_s$ down, +\begin{equation} + \min_\zeta \tilde F_\beta(x,\zeta) + \;=\; + \min_{\zeta,\,u}\Big\{\zeta + \frac{1}{(1-\beta)S}\sum_{s} u_s \;:\; + u_s \ge L_s(x)-\zeta,\; u_s\ge 0,\; s=1,\dots,S\Big\}. + \label{eq:lin} +\end{equation} +Whenever $L_s(x)$ is linear in $x$, \eqref{eq:lin} is a linear programme in $(x,\zeta,u)$. + +\begin{remark}[The discrete tail and the meaning of $\zeta$] + \label{rem:discrete} + For $S$ scenarios and $k=(1-\beta)S$, the minimum of \eqref{eq:F-scen} equals the average of + the $k$ largest losses, where a fractional $k$ gives the $(\lfloor k\rfloor+1)$-th largest loss + the fractional weight $k-\lfloor k\rfloor$. This is exactly what + \code{quantcore.measurement.risk.scenario\_cvar} computes, and it is the value the LP returns. + When $k$ is an integer, $\tilde F_\beta(x,\cdot)$ is flat between the $(k{+}1)$-th and the $k$-th + largest loss, so the minimiser $\zeta$ is \emph{not unique}: any point of that interval is + optimal, and the VaR is its left endpoint. Consequently the $\zeta$ (called $\alpha$ in the + code) reported by a solver need not equal the VaR, in particular when the CVaR constraint is + not tight; the implementation therefore recomputes VaR and CVaR of the decoded portfolio from + the scenarios rather than reading them off the LP variables. +\end{remark} + +% ============================================================================================ +\section{The classical model in portfolio weights} +% ============================================================================================ + +The textbook formulation \cite{RockafellarUryasev2000,KrokhmalPalmquistUryasev2002} describes a +portfolio by weights $w\in\R^N$, the fractions of wealth invested in each stock. Its loss over +the horizon, relative to the initial wealth, is minus the portfolio return, +\begin{equation} + L_s(w) \;=\; -\sum_{i=1}^N R_{s,i}\,w_i \;=\; -R_s^{\top} w , + \label{eq:loss-w} +\end{equation} +which is linear in $w$, and the expected return is $\bar R^{\top}w$ with +$\bar R_i=\frac1S\sum_s R_{s,i}$. With a risk budget $\omega$ expressed as a fraction of wealth +(e.g.\ ``the 95\,\% CVaR over one week may not exceed 4\,\% of the portfolio value''), the +\emph{mean--CVaR linear programme} reads +\begin{subequations} +\label{eq:lp-w} +\begin{alignat}{3} + \max_{w,\zeta,u}\quad & \bar R^{\top} w \label{eq:lp-w-obj}\\ + \text{s.t.}\quad + & u_s \;\ge\; -R_s^{\top} w - \zeta, \quad u_s\ge 0, & \quad & s=1,\dots,S, \label{eq:lp-w-u}\\ + & \zeta + \frac{1}{(1-\beta)S}\sum_{s=1}^S u_s \;\le\; \omega, \label{eq:lp-w-cvar}\\ + & \mathbf{1}^{\top} w \;\le\; 1, \qquad w \;\ge\; 0, \label{eq:lp-w-budget}\\ + & w_i \;\le\; \bar w, & & i=1,\dots,N, \label{eq:lp-w-single}\\ + & \sum_{i\in G_k} w_i \;\le\; \bar w_k, & & k=1,\dots,K. \label{eq:lp-w-sector} +\end{alignat} +\end{subequations} +Here \eqref{eq:lp-w-u}--\eqref{eq:lp-w-cvar} are \eqref{eq:lin} and \eqref{eq:cvar-constraint} +combined, \eqref{eq:lp-w-budget} is the budget with a non-negative cash residual $1-\mathbf{1}^\top w$ +(earning nothing over the horizon) and the no-short-sale condition, \eqref{eq:lp-w-single} caps +each stock at the fraction $\bar w$ (10\,\% in the platform) and \eqref{eq:lp-w-sector} caps every +GICS sector $G_k$ at $\bar w_k$. + +\paragraph{Three equivalent objectives.} +Krokhmal, Palmquist and Uryasev \cite{KrokhmalPalmquistUryasev2002} show that the three +problems +\begin{equation} + \text{(a)}\;\max\,\bar R^{\top}w \text{ s.t. } \CVaR_\beta\le\omega, \quad + \text{(b)}\;\min\,\CVaR_\beta \text{ s.t. } \bar R^{\top}w\ge\rho, \quad + \text{(c)}\;\max\,\bar R^{\top}w-\lambda\CVaR_\beta + \label{eq:three} +\end{equation} +trace out the same efficient frontier as $\omega$, $\rho$ and $\lambda\ge 0$ vary. All three are +linear after the substitution \eqref{eq:lin}; the implementation offers them as the objectives +\code{max\_return}, \code{min\_cvar} and \code{utility}. The platform's primary mode is (a), +because the risk-management specification is an \emph{absolute} loss level for a period. + +\paragraph{Properties.} +Problem \eqref{eq:lp-w} has $N+1+S$ variables and $S+1+N+K+1$ constraints (the weight caps are +variable bounds). Its constraint matrix is dense in the $S$ scenario rows: $S(N+2)$ non-zeros. It +is a linear programme, so every local optimum is global, the solution is a vertex, and it can be +solved for thousands of scenarios and hundreds of assets in seconds. + +% ============================================================================================ +\section{From weights to numbers of shares} +\label{sec:shares} +% ============================================================================================ + +Weights are a convenient abstraction, but a real portfolio consists of whole shares: one cannot +buy 2.66 shares of a stock, and for a stock priced at CHF~2\,000 the difference between 2 and 3 +shares is 10\,\% of a CHF~10\,000 portfolio. The platform therefore optimises directly over the +integer numbers of shares. + +\subsection{Money instead of fractions} + +Let $n_i\in\Z_{\ge0}$ be the number of shares of stock $i$ held over the horizon. The value of +the position is $P_i n_i$, and the weight of the classical model is recovered as +\begin{equation} + w_i \;=\; \frac{P_i\,n_i}{W}. + \label{eq:w-n} +\end{equation} +Substituting \eqref{eq:w-n} into the relative loss \eqref{eq:loss-w} and multiplying by $W$ +turns it into a loss \emph{in money}: +\begin{equation} + W L_s(w) \;=\; -\sum_i R_{s,i}\,P_i\,n_i \;=\; -\sum_i g_{s,i}\,n_i, \qquad g_{s,i} := P_i R_{s,i}, + \label{eq:loss-n} +\end{equation} +where $g_{s,i}$ is the CHF gain of one share of stock $i$ in scenario $s$. Because CVaR is +positively homogeneous, $\CVaR_\beta(W L)=W\CVaR_\beta(L)$: the CVaR of the money loss is the +money amount the risk manager specifies (``the 95\,\% CVaR over a week may not exceed +CHF~40\,000''), and the relative budget $\omega$ of \eqref{eq:lp-w-cvar} becomes the absolute +budget $\Lambda=\omega W$. The expected return $\bar R^\top w$ becomes the expected profit +\begin{equation} + \E[\text{P\&L}] \;=\; \sum_i \bar g_i\,n_i, \qquad \bar g_i=\frac1S\sum_s g_{s,i}=P_i\bar R_i . + \label{eq:epnl} +\end{equation} +Both the objective and the risk constraint use the \emph{same} scenario set, so the trade-off +they describe is internally consistent (the drift of the scenarios is the expected-return model). + +\subsection{Budget, cash and the constraints} + +Multiplying each constraint of \eqref{eq:lp-w} by $W$ and substituting \eqref{eq:w-n} gives: +\begin{itemize}[nosep] + \item budget: $\sum_i P_i n_i \le W$ --- the purchases may not exceed the available cash, what + is not invested stays in cash; + \item single stock: $P_i n_i\le \bar w W$; sector: $\sum_{i\in G_k}P_i n_i\le \bar w_k W$; + \item no short sales: $n_i\ge 0$. +\end{itemize} +The single-stock cap deserves a closer look. As a linear constraint it is a row of the model, but +for an integer variable it is equivalent to the \emph{variable bound} +\begin{equation} + n_i \;\le\; \bar n_i := \Big\lfloor \frac{\bar w\,W}{P_i} \Big\rfloor , + \label{eq:nbar} +\end{equation} +which is strictly tighter than the linear row whenever $\bar w W/P_i$ is not an integer: the +rounding down is free information that a MILP solver exploits (tighter LP relaxation, smaller +branch-and-bound tree). The implementation uses \eqref{eq:nbar} with $\bar w$ replaced by +$\min(\bar w, f_{\max})$ if a maximum invested fraction $f_{\max}<1$ is given, and never leaves +the cap to a row. + +\subsection{The mean--CVaR MILP in shares} + +Putting the pieces together, the new-portfolio problem (all wealth in cash, $n^0=0$, $W=c^0$) +reads +\begin{subequations} +\label{eq:milp-n} +\begin{alignat}{3} + \max_{n,\zeta,u}\quad & \sum_{i} \bar g_i\, n_i \label{eq:milp-n-obj}\\ + \text{s.t.}\quad + & \sum_i g_{s,i}\, n_i + \zeta + u_s \;\ge\; 0, \quad u_s\ge 0, & \quad & s=1,\dots,S, \label{eq:milp-n-u}\\ + & \zeta + \frac{1}{(1-\beta)S}\sum_{s} u_s \;\le\; \Lambda, \label{eq:milp-n-cvar}\\ + & \sum_i P_i\, n_i \;\le\; c^0, \label{eq:milp-n-budget}\\ + & \sum_{i\in G_k} P_i\, n_i \;\le\; \bar w_k\, W, & & k=1,\dots,K, \label{eq:milp-n-sector}\\ + & n_i \in \{0,1,\dots,\bar n_i\}, & & i=1,\dots,N, \label{eq:milp-n-int}\\ + & \zeta\in\R . \nonumber +\end{alignat} +\end{subequations} +Row \eqref{eq:milp-n-u} is \eqref{eq:lp-w-u} written as ``$\ge 0$'' with the money loss +\eqref{eq:loss-n} inserted ($u_s\ge L_s-\zeta \Leftrightarrow \sum_i g_{s,i}n_i+\zeta+u_s\ge0$). +Apart from the integrality \eqref{eq:milp-n-int} the problem is the LP \eqref{eq:lp-w} scaled by +$W$; with the integrality it is a mixed-integer linear programme, and the LP obtained by dropping +\eqref{eq:milp-n-int} is its \emph{LP relaxation}. + +\subsection{Why the integrality cannot be handled by rounding} + +The optimum of the LP relaxation is a vertex, and its coordinates $n_i$ are generically +fractional. Rounding them is not a substitute for solving the MILP: +\begin{itemize}[nosep] + \item rounding \emph{up} (or to the nearest integer) can violate the budget, a cap, or the + CVaR constraint --- the risk budget is exceeded; + \item rounding \emph{down} is always feasible (all constraints are ``$\le$'' in $n$, and the + scenario rows are satisfied by re-optimising $\zeta,u$) but can be far from optimal, + because the ``lost'' fractions can often be re-invested in other, cheaper shares; + \item the true integer optimum can have a different \emph{structure} than the relaxed one, not + just rounded coordinates. +\end{itemize} +The gap is largest when $W/P_i$ is small, i.e.\ for expensive stocks or small portfolios; for a +CHF~1\,000\,000 portfolio and stocks below CHF~1\,000 it is negligible, but the 10\,\% cap keeps +each position to at most $\bar w W/P_i$ shares, and a few stocks in the universe trade above +CHF~10\,000 (Lindt \& Spr\"ungli partizipation certificates above CHF~100\,000). + +\begin{example}[Two stocks, eight scenarios] + \label{ex:rounding} + Prices $P=(572,\ 2208)$, cash $W=c^0=10\,000$, $\beta=0.75$ (so $k=(1-\beta)S=2$: the CVaR is + the mean of the two worst scenarios), risk budget $\Lambda=250$, caps $\bar w=0.7$, no costs. + The simple returns of the eight scenarios are + \[ + R=\begin{pmatrix} + 0.005&-0.082\\ 0.014&-0.052\\ -0.069&-0.020\\ -0.002&0.055\\ + -0.009&0.083\\ 0.048&0.064\\ 0.044&0.028\\ -0.011&0.042 + \end{pmatrix}, \qquad \bar R=(0.0025,\ 0.01475). + \] + Stock 2 has six times the expected return of stock 1 but a heavier left tail. + \begin{center} + \small + \begin{tabular}{@{}lcccccl@{}} + \toprule + portfolio & $n_1$ & $n_2$ & invested & $\E[\text{P\&L}]$ & $\CVaR_{0.75}$ & feasible \\ + \midrule + LP relaxation ($n\in\R^2_{\ge0}$) & 2.6605 & 1.7876 & 5\,468.90 & 62.02 & 250.00 & yes, CVaR binding \\ + nearest rounding & 3 & 2 & 6\,132 & 69.43 & 280.13 & \textbf{no}: $\CVaR>\Lambda$ \\ + rounding down & 2 & 1 & 3\,352 & 35.43 & 149.22 & yes, $-43\,\%$ vs.\ LP \\ + MILP optimum & 7 & 1 & 6\,212 & 42.58 & 240.74 & yes \\ + \bottomrule + \end{tabular} + \end{center} + Rounding to the nearest integers breaks the risk budget (the CVaR limit, not the cash: 6\,132 + of 10\,000 are invested). The relaxed solution buys 1.79 shares of the attractive stock~2; the + second whole share of it is too large a block of risk for the budget, so the integer optimum keeps one share of stock~2 + and fills the remaining risk budget with \emph{seven} shares of stock~1 --- a portfolio that + no rounding of $(2.66, 1.79)$ produces. The enumeration of all feasible integer portfolios + confirms $(7,1)$ as the optimum; the integrality gap $62.02-42.58$ is 31\,\% of the relaxed + value. (The numbers were computed with \code{MeanCVaRProblem} and HiGHS and are pinned by + \code{tests/test\_problem.py::test\_documentation\_example\_rounding\_fails}.) +\end{example} + +% ============================================================================================ +\section{Rebalancing an existing portfolio with transaction costs} +% ============================================================================================ + +When holdings $n^0\ne 0$ exist, the decision is no longer ``which shares to buy'' but ``which +shares to buy and which to sell'', and every trade costs money. Weights cannot express this +naturally (a weight change of $+2\,\%$ says nothing about the number of shares traded, and the +cost depends on the traded value), whereas in share numbers it is immediate. + +\subsection{Trades, linking and the cash balance} + +Introduce the integer trade variables $b_i\ge0$ (shares bought) and $s_i\ge0$ (shares sold), +linked to the final position by +\begin{equation} + n_i \;=\; n^0_i + b_i - s_i, \qquad s_i\le n^0_i \;(\text{implied by } n_i\ge0), + \label{eq:link} +\end{equation} +and the transaction cost with proportional rates $c^b, c^s$ (10 basis points by default) and an +optional fixed cost $F$ per traded stock, +\begin{equation} + \TC(b,s,t) \;=\; \sum_i \big(c^b P_i\, b_i + c^s P_i\, s_i\big) + F\sum_i t_i, + \qquad b_i + s_i \le M_i\, t_i,\; t_i\in\{0,1\}, + \label{eq:tc} +\end{equation} +where $t_i$ indicates whether stock $i$ is traded at all and $M_i=\max(\bar n_i, n^0_i)+1$ is +a valid big-M (no trade can exceed it). The fixed cost makes small trades unattractive, which +is what it should do; it is the only place where binary variables enter the cost model. + +The budget \eqref{eq:milp-n-budget} becomes a \emph{cash balance}: sales bring cash in, +purchases and costs take it out, and the cash at the end of the rebalancing may not be negative, +\begin{equation} + c^{\text{end}} \;=\; c^0 + \sum_i P_i s_i - \sum_i P_i b_i - \TC \;\ge\; 0 + \quad\Longleftrightarrow\quad + \sum_i P_i b_i - \sum_i P_i s_i + \TC \;\le\; c^0 . + \label{eq:cash} +\end{equation} + +\begin{remark}[Buying and selling the same stock] + With positive proportional costs an optimal solution never has $b_i>0$ and $s_i>0$ + simultaneously: reducing both by one share leaves $n_i$ unchanged and saves + $(c^b+c^s)P_i$ in cost and cash. With zero costs the decomposition of a net trade into $b_i$ + and $s_i$ is not unique but harmless. The decoded solution reports the net trade $b_i-s_i$. +\end{remark} + +\subsection{Transaction costs enter the loss} + +The costs are paid at the start of the horizon and are therefore a deterministic part of the +loss relative to the initial wealth $W$ of \eqref{eq:wealth}: +\begin{equation} + L_s(n,b,s,t) \;=\; \TC(b,s,t) - \sum_i g_{s,i}\, n_i . + \label{eq:loss-reb} +\end{equation} +By translation invariance, $\CVaR_\beta(\TC-G)=\TC+\CVaR_\beta(-G)$: the costs shift the +whole loss distribution and consume part of the risk budget, exactly as they reduce the expected +profit, +\begin{equation} + \E[\text{P\&L}] \;=\; \sum_i \bar g_i\, n_i - \TC(b,s,t). + \label{eq:epnl-reb} +\end{equation} +The scenario rows \eqref{eq:milp-n-u} therefore become +\begin{equation} + \sum_i g_{s,i}\, n_i - \TC(b,s,t) + \zeta + u_s \;\ge\; 0, \qquad s=1,\dots,S . + \label{eq:u-reb} +\end{equation} + +\subsection{Limits on the rebalancing itself} + +Two constraints of the specification concern the trading rather than the resulting portfolio: +a cap on the total transaction cost and a cap on the turnover (the traded value as a fraction of +wealth), +\begin{equation} + \TC(b,s,t)\;\le\;\TC_{\max}, \qquad \sum_i P_i\,(b_i+s_i)\;\le\;\tau\,W . + \label{eq:turnover} +\end{equation} +Both are optional. The single-stock and sector caps apply to the \emph{final} positions +$n=n^0+b-s$: a legacy position above the cap has to be sold down to it, and if a turnover cap +prevents that, the problem is (correctly) infeasible. + +\subsection{The new portfolio as a special case} + +With $n^0=0$ nothing can be sold, so $s=0$ and $b=n$; the linking rows \eqref{eq:link} become +trivial and are dropped, the costs become $\TC=c^b\sum_i P_i n_i+F\sum_i t_i$, and the cash +balance \eqref{eq:cash} reduces to $\sum_i P_i(1+c^b)\,n_i+F\sum_i t_i\le c^0$. This is +\eqref{eq:milp-n} with proportional costs, i.e.\ mode \code{new} of the implementation. + +\subsection{Optional cardinality and minimum position size} + +A portfolio of 30 stocks is easier to monitor than one of 120 with many tiny positions; the +integer model can express both a maximum number of positions and a minimum value per open +position with one binary $y_i$ per stock: +\begin{equation} + n_i \;\le\; \max(\bar n_i,1)\, y_i, \qquad P_i n_i \;\ge\; v_{\min}\, y_i, \qquad \sum_i y_i\le K_{\max}, + \qquad y_i\in\{0,1\}. + \label{eq:card} +\end{equation} +These constraints are optional (\code{max\_positions}, \code{min\_position\_value}); they make the +problem combinatorially harder and are usually not needed --- the CVaR constraint and the +integrality already produce concentrated portfolios. + +% ============================================================================================ +\section{Scenario sets: sampling error, variance reduction and large sets} +\label{sec:sampling} +% ============================================================================================ + +The MILP sees the return distribution only through the scenario matrix $g_{s,i}=P_iR_{s,i}$ +and the probabilities of its rows. It is a \emph{sample-average approximation} (SAA) of the +problem one would like to solve --- the one with the true distribution of the horizon returns +\cite{ShapiroDentchevaRuszczynski2009} --- and its solution inherits the sampling error of the +scenario set. This section quantifies that error on the real universe, derives the three +remedies implemented in \code{quantcore.models.sampling} and \code{quantcore.optimization}, +and closes with the Gaussian special case, in which the exact solution is available and the +mean--CVaR problem coincides with the mean--variance problem. + +\subsection{How noisy is a plain Monte-Carlo set?} +\label{sec:mc-noise} + +Let the scenarios be $S$ independent draws from the model with mean $\mu$ (per asset, over the +horizon) and covariance $\Sigma$. The sample mean of asset $i$ has standard error +$\sigma_i/\sqrt S$. With $S=2\,000$ and weekly volatilities of 3--11\,\% this is +0.07--0.25\,\% per week, i.e.\ 3--13\,\% per year --- the same order as the expected returns +themselves (5--12\,\% p.a.\ from the Black--Litterman model). The optimiser cannot tell signal +from noise: it overweights the assets whose sample mean happens to be high, the classical +``error maximisation'' of \cite{Michaud1989}. The tail is only the second problem: the CVaR at +95\,\% is estimated from the $(1-\beta)S=100$ worst scenarios, with a relative standard error of +about $0.37/\sqrt{(1-\beta)S}\approx 2\,\%$ for a Gaussian tail. + +Table~\ref{tab:noise} measures the consequences on the 30 largest stocks of the universe +(CHF 1 million, weekly horizon, $\CVaR_{0.95}\le 5\,\%$ of wealth, eight seeds per method). The +reference is the exact optimum of Section~\ref{sec:gaussian}, so ``true'' figures are exact. +The comparison is made in portfolio weights (the continuous relaxation) so that integrality +neither hides nor causes noise; the integer results are the same to within a percent. + +\begin{table}[ht] +\centering +\footnotesize +\setlength{\tabcolsep}{4pt} +\begin{tabular}{@{}lrrrrr@{}} +\toprule +$S=2\,048$, $\beta=0.95$ & plain MC & antithetic & MC + mm & Sobol' & Sobol' + mm \\ +\midrule +in-sample $\E[\text{P\&L}]$, mean $\pm$ sd & $2\,791\pm552$ & $2\,228\pm19$ & $2\,221\pm10$ & $2\,221\pm8$ & $2\,220\pm5$ \\ +true $\E[\text{P\&L}]$ of the chosen portfolio & $1\,991\pm119$ & $2\,228\pm19$ & $2\,221\pm10$ & $2\,220\pm5$ & $2\,220\pm5$ \\ +loss versus the optimum ($2\,219$) & $-228$ & $-9$ & $-2$ & $-0.6$ & $-0.4$ \\ +true CVaR over the limit, mean $\pm$ sd & $-0.5\pm5.6\,\%$ & $+1.8\pm2.8\,\%$ & $+0.6\pm1.7\,\%$ & $+0.2\pm0.7\,\%$ & $+0.15\pm0.7\,\%$ \\ +turnover between two seeds & $101\,\%$ & $12.6\,\%$ & $11.7\,\%$ & $7.2\,\%$ & $5.8\,\%$ \\ +turnover to the optimum & $75\,\%$ & $8.9\,\%$ & $8.3\,\%$ & $5.0\,\%$ & $4.1\,\%$ \\ +\bottomrule +\end{tabular} +\caption{Sampling noise of the mean--CVaR optimisation (30 stocks, eight seeds, CHF per week; +``turnover'' is $\sum_i|w_i-w_i'|$). ``mm'' = moment matching.} +\label{tab:noise} +\end{table} + +With plain Monte Carlo two seeds give essentially disjoint portfolios (turnover 101\,\%), the +in-sample expected profit is 26\,\% too high and the true expected profit 10\,\% below the +optimum. Fixing the first two moments of the set removes the illusion entirely; Sobol' points +halve the remaining tail noise. The residual 4--6\,\% turnover is the flatness of the optimum +(many portfolios within 0.02\,\% of the optimal objective), not sampling. Increasing $S$ does +not rescue plain Monte Carlo: at $S=8\,192$ its turnover between seeds is still 76\,\% +(the mean error decays like $1/\sqrt S$), while Sobol' + mm is at 4.0\,\%. + +\subsection{Quasi-Monte Carlo and moment matching} +\label{sec:qmc} + +\paragraph{Sobol' points.} Instead of pseudo-random $z_s\sim N(0,I)$ the implementation uses +scrambled Sobol' points \cite{Owen1998}: $S$ (rounded up to a power of two) points $u_s\in(0,1)^N$ +that fill the unit cube far more evenly than random points, mapped through the inverse normal +distribution function, $z_s=\Phi^{-1}(u_s)$. The scrambling is seeded, so the set is +reproducible. The first coordinates of a Sobol' sequence are the best distributed, so the draws +are coloured with the eigen-root $A=V\Lambda^{1/2}$ of $\Sigma$ with eigenvalues in descending +order: the first coordinate drives the market factor, which dominates every long-only +portfolio's loss \cite[ch.~5]{Glasserman2004}. + +\paragraph{Moment matching.} Whatever the base sample, its first two moments can be made +\emph{exact} \cite{HoylandWallace2001}. With probabilities $p_s$ (equal unless importance +sampling is used, Section~\ref{sec:is}), let $m=\sum_sp_sz_s$ and $C=\sum_sp_s(z_s-m)(z_s-m)^\top$; +then +\begin{equation} + \tilde z_s \;=\; C^{-1/2}(z_s-m), \qquad + \sum_sp_s\tilde z_s=0,\quad \sum_sp_s\tilde z_s\tilde z_s^\top=I, + \label{eq:mm} +\end{equation} +and the scenarios $r_s=h m_0+\sqrt h\,A\tilde z_s$ have mean $hm_0$ and covariance $h\Sigma$ +exactly, for any $S>N$. Only the \emph{shape} of the tail remains sampled. For the empirical +innovation model the same transform is applied to the resampled sums of innovations (target +covariance $hI$), so that both models reproduce the latest covariance exactly. + +\paragraph{The objective uses the model mean.} Once the mean is exact there is no reason to +estimate it from the scenarios at all. The implementation puts the model's expected simple +return $\mu_i$ (the Black--Litterman posterior, converted to the horizon) directly into the +objective, $\bar g_i=P_i\mu_i$, and uses the scenarios only in the CVaR rows. This also keeps +the objective exact under importance sampling, where a tail-focused sample estimates the mean +\emph{worse} than a plain one. The convention behind $\mu_i$ matters: Black--Litterman returns +are arithmetic expectations, so the log-drift of the scenarios is $\ln(1+h\mu_i)/h-\sigma_i^2/2$ +(It\^o correction) and $\E[e^{r_i}-1]=h\mu_i$; using $\mu_i$ as the log-drift would add +$\sigma_i^2/2$ to the expected simple return --- 5\,\% p.a.\ for a typical stock and 34\,\% for +the most volatile one in the universe --- and tilt the optimiser towards volatile names. + +\subsection{Importance sampling for the tail} +\label{sec:is} + +Only the scenarios in the tail of the loss matter for the CVaR: at $\beta=0.99$, 1\,\% of them. +Importance sampling draws more scenarios where they matter and corrects with likelihood ratios +\cite{GlassermanHeidelbergerShahabuddin2000}. Three things are specific to the optimisation +setting. + +\emph{The tail depends on the decision.} The loss of portfolio $v=P\circ n$ is +$L=-v^\top r=-v^\top hm_0-(\sqrt hA^\top v)^\top z$, so its tail is the half-space +$\{z:\,d^\top z\ge c\}$ with the unit direction $d=-A^\top v/\|A^\top v\|$. Because all +long-only, capped, sector-limited candidates load mainly on the market factor, one direction +serves all of them. The default takes $v$ from the \emph{cap-weighted portfolio of the candidate +stocks} (\code{importance\_sampling="market"}), which depends on the universe and the covariance +but on none of the parameters a user varies; the alternative \code{"adaptive"} takes it from a +first-pass solution of the problem itself, which is marginally sharper at $\beta=0.99$ but makes +the scenario set depend on the risk limit and the constraints (Section~\ref{sec:onesetup}). + +\emph{Bounded likelihood ratios.} A share $\varepsilon$ (default one half) of the Sobol' points +is shifted, $z_s\mapsto z_s+\theta$ with $\theta=z_\beta d$, which centres the loss of $v^{(1)}$ +at its VaR; the rest stay nominal. The proposal is the defensive mixture +$g=(1-\varepsilon)\varphi+\varepsilon\varphi(\cdot-\theta)$ \cite{Hesterberg1995}, whose +likelihood ratios +\begin{equation} + \ell_s=\frac{\varphi(z_s)}{g(z_s)}=\frac{1}{(1-\varepsilon)+\varepsilon\,e^{\theta^\top z_s-\|\theta\|^2/2}} + \;\le\;\frac{1}{1-\varepsilon} + \label{eq:lr} +\end{equation} +are bounded, so a candidate whose tail points elsewhere is never wildly mis-estimated (the +optimiser would exploit any such region). The scenario probabilities are the self-normalised +ratios $p_s=\ell_s/\sum_{s'}\ell_{s'}$, and the moment matching \eqref{eq:mm} is applied with +these weights. + +\emph{Weighted scenarios in the MILP.} Theorem~\ref{thm:ru} holds for any discrete +distribution: with probabilities $p_s$ the CVaR row \eqref{eq:milp-n-cvar} becomes +\begin{equation} + \zeta+\frac{1}{1-\beta}\sum_sp_su_s\;\le\;\Lambda, + \label{eq:cvar-weighted} +\end{equation} +and nothing else changes; the decoded VaR and CVaR are computed from the weighted sample in the +same way (the tail is the set of largest losses whose probability adds up to $1-\beta$, the last +one taken fractionally). + +For the empirical innovation model the tilt acts on the days: the resampling probabilities are +$q_t\propto\exp(\vartheta\,d^\top\varepsilon_t)$ with $\vartheta$ chosen so that the tilted mean +of $d^\top\varepsilon$ equals $z_\beta/\sqrt h$ per day (a one-dimensional root), the mixture +draws all $h$ days of a scenario either from $q$ or uniformly, and the likelihood ratio of a +scenario is $1/\big((1-\varepsilon)+\varepsilon\prod_kTq_{t_k}\big)$. + +\begin{table}[ht] +\centering +\footnotesize +\setlength{\tabcolsep}{3.2pt} +\begin{tabular}{@{}lrrrrrrrr@{}} +\toprule +& \multicolumn{2}{c}{$0.95$, $2\,048$} & \multicolumn{2}{c}{$0.95$, $8\,192$} & \multicolumn{2}{c}{$0.99$, $2\,048$} & \multicolumn{2}{c}{$0.99$, $8\,192$} \\ +\cmidrule(lr){2-3}\cmidrule(lr){4-5}\cmidrule(lr){6-7}\cmidrule(lr){8-9} +$\beta$, $S$ & S+mm & + IS & S+mm & + IS & S+mm & + IS & S+mm & + IS \\ +\midrule +scenarios beyond the VaR & 101 & 560 & 410 & 2\,249 & 19 & 512 & 80 & 2\,075 \\ +true CVaR over the limit & 0.15\,\% & 0.21\,\% & 0.08\,\% & 0.01\,\% & 2.45\,\% & 0.50\,\% & 0.75\,\% & 0.03\,\% \\ +sd of the true CVaR (\% of limit) & 0.74 & 0.30 & 0.36 & 0.15 & 1.67 & 0.40 & 0.79 & 0.20 \\ +loss versus the optimum (CHF) & 0.4 & 1.1 & 0.3 & 0.0 & 10.7 & 2.7 & 3.6 & 0.1 \\ +turnover between two seeds & 5.9\,\% & 3.6\,\% & 4.0\,\% & 1.6\,\% & 29\,\% & 11.5\,\% & 15.4\,\% & 5.7\,\% \\ +\bottomrule +\end{tabular} +\caption{Importance sampling (``+ IS'': mixture $\varepsilon=0.5$, two-stage tilt) versus +moment-matched Sobol' points (``S+mm'') at equal $S$; 30 stocks, eight seeds, exact Gaussian +reference.} +\label{tab:is} +\end{table} + +At equal $S$ the tail noise falls by a factor 2--2.5 at 95\,\% and 3--4 at 99\,\%; equivalently, +2\,048 tilted scenarios are as good as 8\,192 plain ones. The gain is largest exactly where it +is needed, at $\beta=0.99$, where a plain set of 2\,048 scenarios has 19 tail points. The +tilted LP is slower per scenario (a quarter of its rows are active instead of $1-\beta$), but the +market tilt is on by default (\code{importance\_sampling="auto"} $=$ \code{"market"}) because it +costs nothing in accuracy at 95\,\% and halves the noise at 99\,\%. Table~\ref{tab:is-market} +compares the two directions. Under the Gaussian model importance sampling is unnecessary --- the +exact solution of Section~\ref{sec:gaussian} needs no scenarios at all. + +\begin{table}[ht] +\centering +\footnotesize +\setlength{\tabcolsep}{4pt} +\begin{tabular}{@{}lrrrrrr@{}} +\toprule +& \multicolumn{3}{c}{$\beta=0.95$} & \multicolumn{3}{c}{$\beta=0.99$} \\ +\cmidrule(lr){2-4}\cmidrule(lr){5-7} +$S=2\,048$ & no tilt & market & adaptive & no tilt & market & adaptive \\ +\midrule +true CVaR over the limit & 0.14\,\% & 0.37\,\% & 0.20\,\% & 2.43\,\% & 1.37\,\% & 0.50\,\% \\ +sd of the true CVaR (\% of limit) & 0.74 & 0.72 & 0.30 & 1.69 & 0.61 & 0.42 \\ +turnover between two seeds & 5.7\,\% & 3.2\,\% & 3.6\,\% & 28.5\,\% & 17.4\,\% & 11.6\,\% \\ +turnover to the optimum & 4.1\,\% & 2.6\,\% & 2.4\,\% & 22.4\,\% & 13.0\,\% & 8.7\,\% \\ +depends on the risk parameters & no & no & \textbf{yes} & no & no & \textbf{yes} \\ +\bottomrule +\end{tabular} +\caption{Tilt direction: the cap-weighted candidate portfolio (``market'', two blocks at +$z_{0.95}$ and $z_{0.99}$) versus a first-pass solution (``adaptive'', one block at $z_\beta$); +30 stocks, eight seeds.} +\label{tab:is-market} +\end{table} + +\emph{How far does one direction carry?} The tilt enriches the tail of the \emph{reference} +portfolio; another portfolio benefits in proportion to how much its loss is correlated with the +reference's. Table~\ref{tab:alignment} measures this for portfolios from market-like to +deliberately orthogonal. Long-only portfolios under single-stock and sector caps are market-like +in practice --- the optimiser's solutions come out at an alignment of about $0.90$ --- and the +reference is the cap-weighted portfolio \emph{of the candidate stocks}, so a defensive universe +gets a defensive reference. Where the alignment is nevertheless low the tilt does not help and +costs a little precision (the nominal half of the mixture bounds the damage); the optimisation +result reports the realised alignment (\code{tilt\_alignment}) and warns below $0.3$, where +\code{importance\_sampling="adaptive"} or \code{"off"} is the better choice. + +\begin{table}[ht] +\centering +\footnotesize +\setlength{\tabcolsep}{4pt} +\begin{tabular}{@{}lrrrrrr@{}} +\toprule +& & \multicolumn{2}{c}{scenarios beyond the VaR} & \multicolumn{3}{c}{noise of the CVaR estimate} \\ +\cmidrule(lr){3-4}\cmidrule(lr){5-7} +portfolio & alignment & plain & tilted & plain & tilted & reduction \\ +\midrule +cap-weighted (the reference) & 1.00 & 41 & 789 & 0.99\,\% & 0.19\,\% & 5.3$\times$ \\ +equal weight & 0.89 & 41 & 644 & 0.79\,\% & 0.30\,\% & 2.6$\times$ \\ +mean--CVaR optimum & 0.90 & 41 & 630 & 1.39\,\% & 0.61\,\% & 2.3$\times$ \\ +five highest-beta stocks & 0.68 & 41 & 368 & 0.22\,\% & 0.34\,\% & 0.6$\times$ \\ +minimum variance & 0.69 & 41 & 350 & 1.34\,\% & 1.25\,\% & 1.1$\times$ \\ +five lowest-beta stocks & $-0.09$ & 41 & 34 & 1.66\,\% & 1.96\,\% & 0.8$\times$ \\ +three least market-like stocks & $-0.15$ & 41 & 31 & 1.93\,\% & 1.95\,\% & 1.0$\times$ \\ +\bottomrule +\end{tabular} +\caption{Reach of one tilt direction: ``alignment'' is the correlation between the portfolio's +loss and the reference's; $S=4\,096$, $\beta=0.99$, 12 seeds, 30 stocks. Noise is the standard +deviation of the CVaR estimate across seeds, relative to its mean.} +\label{tab:alignment} +\end{table} + +\subsection{One scenario set for a family of runs} +\label{sec:onesetup} + +A portfolio proposal is rarely produced once: a risk limit is varied, a constraint tightened, a +view added, two confidence levels compared. If each of those runs drew its own scenarios, the +differences between the results would mix the effect of the parameter with the sampling noise of +a new set --- precisely the noise Sections~\ref{sec:qmc} and \ref{sec:is} remove. The +implementation therefore makes the scenario set a function of the \emph{model} only. Its +\emph{scenario key} (reported with every result) contains +\begin{center} +\footnotesize +\begin{tabular}{@{}p{7.2cm}p{7.2cm}@{}} +\toprule +in the key (changing it draws new scenarios) & not in the key (the set is reused) \\ +\midrule +candidate stocks, data as-of date, frequency and horizon, covariance model, scenario model, +number of scenarios, sampling scheme, moment matching, seed, tilt mode and mixture, innovation +history and re-standardisation +& +CVaR limit (absolute or as a fraction), confidence level $\beta$, objective, risk aversion, +target return, all constraints, transaction costs, cash, existing holdings, expected-return +model and views, solver and time limit \\ +\bottomrule +\end{tabular} +\end{center} +Two properties make this possible. First, the tilt of Section~\ref{sec:is} uses the cap-weighted +candidate portfolio and blocks at both usual confidence levels, so it does not look at the +optimisation parameters. Second, the drift enters \emph{after} the random part: scenarios are +generated with zero expected return and the expected-return model is added as a per-asset +constant, $r_s\mapsto r_s+hm$, by \code{ScenarioSet.with\_drift}. Changing a view therefore +shifts every scenario by a constant and leaves the shocks --- and hence the whole risk picture --- +untouched; the result reports a fingerprint of the shocks (\code{shock\_hash}) next to the one of +the scenarios (\code{hash}). Sets are cached per key inside the service, so a family of runs also +pays the expensive innovation filter only once (measured: 14\,s for the first run, 0\,s +afterwards). + +Only \code{importance\_sampling="adaptive"} breaks the property, since its tilt comes from a +first-pass solve; the key then lists the parameters of that solve, so runs that differ in them +are not silently compared on different scenarios. + +\subsection{How many scenarios, and how many assets?} +\label{sec:howmany} + +\paragraph{The count has to grow with the candidate set.} The approximation searches an +$N$-dimensional space, so a fixed $S$ buys less precision as $N$ grows. Measured against the +exact optimum of Section~\ref{sec:gaussian} (GBM model, four seeds): + +\begin{center} +\footnotesize +\begin{tabular}{@{}rrrrrr@{}} +\toprule +$N$ & $S$ & $S/N$ & turnover between two seeds & turnover to the optimum & sd of $\E[\text{P\&L}]$ \\ +\midrule + 30 & 512 & 17 & 9.6\,\% & 7.2\,\% & 17.6 CHF \\ + 30 & 2\,048 & 68 & 3.5\,\% & 4.2\,\% & 2.8 \\ + 30 & 8\,192 & 273 & 2.6\,\% & 3.8\,\% & 1.1 \\ +100 & 512 & 5 & 26.5\,\% & 21.5\,\% & 15.5 \\ +100 & 2\,048 & 20 & 12.5\,\% & 10.0\,\% & 2.8 \\ +100 & 8\,192 & 82 & 6.2\,\% & 6.2\,\% & 2.0 \\ +300 & 512 & 1.7 & 22.7\,\% & 23.4\,\% & 18.5 \\ +300 & 2\,048 & 6.8 & 9.0\,\% & 6.8\,\% & 4.2 \\ +\bottomrule +\end{tabular} +\end{center} + +Two readings of the same table matter. The \emph{reported figures} are stable everywhere: from +2\,000 scenarios on, the expected profit of the proposal varies by less than $0.1\,\%$ between +seeds. The \emph{composition} is what moves, and at fixed $S$ it moves roughly in proportion to +$N$ (3.5\,\% at $N=30$ against 12.5\,\% at $N=100$, both at $S=2\,048$) --- with hundreds of +candidates and a 10\,\% cap there are many portfolios within a fraction of a percent of the +optimum, and the sample decides between them. About 64 scenarios per candidate keeps the +disagreement near 5\,\%; as a default that is too expensive ($N=300$ would mean 32\,768 +scenarios and minutes per solve), so the implementation draws 16 per candidate within +$[4\,096,\,16\,384]$ and says in the result when the ratio is low. Where the composition itself +has to be reproducible --- a backtest, a teaching example --- either raise $S$ or use +\code{scenario\_model="gaussian\_exact"}, which has no sampling error at all. + +\paragraph{Cost at the top end, and which solver.} A model with $N=300$ and $S=32\,768$ has 9.9\,M +non-zeros: building it takes 3.3\,s, encoding it as msgpack 0.6\,s, and the 108\,MB body transfers to +the cuOpt server at about 10\,MB/s. Transport is therefore not the constraint; proving optimality is. +Table~\ref{tab:solvers} compares the two back ends with both running on the Spark, end to end through +the service (scenario generation, build, solve, decode). + +\begin{table}[ht] +\centering +\footnotesize +\begin{tabular}{@{}lrr@{}} +\toprule +case & cuOpt (GPU) & HiGHS (CPU, 4 threads) \\ +\midrule +$N=30$, $S=8\,192$ & 15.6\,s & 14.5\,s \\ +$N=30$, $S=32\,768$ & 171.4\,s & 171.0\,s \\ +$N=100$, $S=4\,096$ & 10.0\,s & 8.9\,s \\ +$N=100$, $S=32\,768$ & 606.9\,s (time limit) & 350.7\,s (optimal) \\ +$N=300$, $S=8\,192$, row generation & 139\,s & 64\,s \\ +\bottomrule +\end{tabular} +\caption{The same models on the same machine, weekly horizon, $\beta=0.95$, 600\,s solver time limit.} +\label{tab:solvers} +\end{table} + +HiGHS is at least as fast in every case and twice as fast on the two largest. This is not a statement +about GPU optimisation in general. cuOpt's published speedups concern \emph{linear} programmes solved +with its PDLP first-order method; for mixed-integer problems its documentation states that the GPU runs +primal heuristics while \emph{branch and bound runs on the CPU}, that the MIP solver is in beta, and +that proving feasible solutions optimal ``remains under active development''. The models here have an +easy LP relaxation and a small integrality gap, so nearly all the work is the optimality proof --- the +part that is not accelerated --- whereas HiGHS warm-starts its dual simplex across branch-and-bound +nodes and, with row generation, solves a sequence of small problems in-process while cuOpt pays a fixed +cost per request. HiGHS is a demanding reference: in the Mittelmann benchmark of 7 July 2026 it solves +179 of 240 instances at a scaled shifted geometric mean of 5.44, against 1.72 for the commercial COPT +and 9.93 for SCIP. Three caveats: cuOpt's time limit covers only its internal solve, it ignores the +MIP-gap settings sent over REST and may therefore prove optimality to a tighter tolerance, and this is +one problem family on one machine rather than a benchmark. + +\subsection{Tail-row generation for large scenario sets} +\label{sec:rowgen} + +The MILP has one row and one variable $u_s$ per scenario; with $N+2$ non-zeros per row the +matrix grows linearly in $S$, but the simplex work does not: HiGHS' dual simplex on the LP +relaxation scales roughly like $S^{1.9}$ and the branch-and-bound like $S^{1.65}$, cuOpt like +$S^{1.2}$--$S^{1.6}$ (Table~\ref{tab:scaling}). Going from 2\,000 to 100\,000 scenarios costs +about 45$\times$ on cuOpt and more than 200$\times$ on HiGHS. + +\begin{table}[ht] +\centering +\small +\begin{tabular}{@{}rrrrrrr@{}} +\toprule +$S$ & non-zeros & HiGHS LP & HiGHS MILP & cuOpt MILP & payload & row generation (HiGHS) \\ +\midrule +\multicolumn{7}{@{}l}{$N=30$} \\ +2\,048 & 68\,k & 0.03\,s & 1.5\,s & 5.1\,s & 1.7\,MB & -- \\ +8\,192 & 270\,k & 0.4\,s & 14.5\,s & 10.8\,s & 6.6\,MB & 5.4\,s (2 iterations) \\ +32\,768 & 1.1\,M & 6.0\,s & 166\,s & 66.6\,s & 27\,MB & 25\,s (3 iterations) \\ +100\,000 & 3.3\,M & 56.5\,s & $>300$\,s & 220\,s & 82\,MB & 158\,s (2 iterations) \\ +\multicolumn{7}{@{}l}{$N=100$} \\ +2\,048 & 211\,k & 0.1\,s & 9.5\,s & 7.0\,s & 5\,MB & \\ +8\,192 & 844\,k & 1.2\,s & 50\,s & 11.5\,s & 21\,MB & \\ +32\,768 & 3.4\,M & 16.5\,s & 232\,s & 101\,s & 82\,MB & \\ +\bottomrule +\end{tabular} +\caption{Solve times versus the number of scenarios (MIP gap $10^{-4}$, $\beta=0.95$). The HiGHS +column was measured on a development laptop whose SciPy bundles HiGHS 1.2.0 and the cuOpt column on +the DGX Spark, so the two columns show the growth with $S$ for each solver but are not a like-for-like +comparison; Table~\ref{tab:solvers} compares them on the same machine.} +\label{tab:scaling} +\end{table} + +At the optimum only the scenarios in the tail have $u_s>0$; every other scenario row is slack. +\emph{Row generation} \cite{KunziBayMayer2006} exploits this: +\begin{enumerate}[nosep] +\item Start with a working set $\mathcal W$: the $2(1-\beta)S$ worst scenarios of a reference + portfolio (the current holdings, or an equal-weight allocation). +\item Solve the MILP with the scenario rows of $\mathcal W$ only, keeping the full-set + probabilities $p_s$ in \eqref{eq:cvar-weighted} (omitted scenarios contribute $u_s=0$) and the + full-set objective. +\item Compute the losses $L_s$ of all $S$ scenarios for the solution $(n^\ast,\zeta^\ast)$; add + every $s\notin\mathcal W$ with $L_s>\zeta^\ast$ to $\mathcal W$ and repeat; stop when there is + none. +\end{enumerate} +The restricted problem drops constraints, so it is a relaxation and its objective bounds the +full one from above; when no omitted scenario is violated the restricted solution is feasible +for the full problem with $u_s=0$ on the omitted rows, hence optimal (within the MIP gap). The +working set stays at about 11\,\% of $S$ at $\beta=0.95$ (2--3\,\% at $\beta=0.99$), two or three +iterations suffice, and the speed-up over the full MILP is 6--7$\times$ from 32\,768 scenarios +on; only the working-set rows are sent to the solver. The default +(\code{row\_generation="auto"}) uses it from 8\,192 scenarios on. + +\subsection{The Gaussian special case: mean--variance equivalence and an exact solution} +\label{sec:gaussian} + +\begin{proposition} +\label{prop:elliptic} +Let the horizon returns be elliptically distributed with mean $m$ and dispersion $C$, e.g.\ +$R\sim N(m,C)$. Then for every portfolio $\CVaR_\beta(L(w))=-m^\top w+k_\beta\,\sigma(w)$ with +$\sigma(w)=\sqrt{w^\top Cw}$ and a constant $k_\beta$ ($k_\beta=\varphi(z_\beta)/(1-\beta)$ in +the normal case, $2.063$ at 95\,\% and $2.665$ at 99\,\%). Consequently the solution of +``maximise $m^\top w$ subject to $\CVaR_\beta\le\Lambda$'' lies on the mean--variance efficient +frontier, and it is also the solution of ``maximise $m^\top w$ subject to +$\sigma(w)\le\bar\sigma$'' with the implicit bound $\bar\sigma=(\Lambda+m^\top w^\ast)/k_\beta$. +\end{proposition} + +\begin{proof} +For elliptical returns the loss of every portfolio is a location--scale transform of one and +the same symmetric distribution, so every positively homogeneous, translation-invariant risk +measure is an affine function of $\sigma(w)$ \cite{EmbrechtsMcNeilStraumann2002}. Let $w^\ast$ +be mean--CVaR optimal with mean $m^\ast$. Any feasible $w$ with $m^\top w\ge m^\ast$ and +$\sigma(w)\le\bar\sigma$ has $\CVaR_\beta\le-m^\ast+k_\beta\bar\sigma=\Lambda$, hence is +mean--CVaR feasible and cannot beat $m^\ast$: the two problems have the same optimal value, and +the same solutions up to ties. The same argument applies over the integer feasible set. +\end{proof} + +The variance bound is only known \emph{ex post} (it depends on $m^\ast$), so the mean--CVaR +problem with a Gaussian model is solved directly. In whole shares it is +\begin{equation} + \max_n\; m^\top v-\TC \quad\text{s.t.}\quad + g(n):=\TC-m^\top v+k_\beta\sqrt{v^\top Cv}\;\le\;\Lambda,\qquad v=P\circ n, + \label{eq:misocp} +\end{equation} +plus the linear constraints of Section~\ref{sec:shares}: a convex mixed-integer +second-order-cone programme (a mixed-integer QP in the utility form), which neither cuOpt nor +HiGHS solves directly. The function $g$ is convex, so it can be outer-approximated by tangent +cuts on the same MILP \cite{Kelley1960,DuranGrossmann1986}. At an exposure $v_k$ with +$\sigma_k=\sqrt{v_k^\top Cv_k}>0$ the gradient of $\sigma$ is $Cv_k/\sigma_k$, and because +$(Cv_k)^\top v_k/\sigma_k=\sigma_k$ the tangent simplifies to +\begin{equation} + g(n)\;\ge\;\TC+\Big(k_\beta\frac{Cv_k}{\sigma_k}-m\Big)^{\!\top}v . + \label{eq:cut} +\end{equation} +With an epigraph variable $t$ (\code{cvar} in the code) the MILP carries the rows +$\TC+(k_\beta Cv_k/\sigma_k-m)^\top(P\circ n)-t\le0$ for the cuts collected so far and +$t\le\Lambda$ (or $t$ in the objective for \code{min\_cvar} and \code{utility}). The loop +starts with a cut at a reference portfolio, solves the MILP, compares the true $g(n^\ast)$ with +$t^\ast$, and adds a cut at $v^\ast$ until they agree (relative tolerance $10^{-6}$). Each cut +removes the current infeasible point and the integer set is finite, so the loop terminates at +the exact optimum within the MIP gap. On the universe it takes 20--70 cuts (each a MILP with +$N$ integers and a few dozen rows, milliseconds on HiGHS) and reproduces Gurobi's direct +MISOCP solution to the cent. This is \code{scenario\_model="gaussian\_exact"}: no scenarios, +no sampling error, fully deterministic. + +\begin{remark} +The GBM scenario model is lognormal in simple returns ($R=e^r-1$ with $r$ normal), the exact +mode is normal in simple returns. At weekly volatilities of 4--5\,\% the lognormal left tail is +2--3\,\% thinner, so the scenario optimum takes a few percent more exposure; the sharp test of +the sampling machinery therefore uses Gaussian scenarios linearly, where moment-matched Sobol' +points reproduce the exact optimum within 1\,\% and plain Monte Carlo does not +(\code{tests/test\_rowgen\_gaussian.py}). For the empirical innovation model no closed form +exists --- that is the point of using it --- and the equivalence serves only as a calibration of +the sampling mechanism. +\end{remark} + +\subsection{Rank of the covariance estimate} +\label{sec:rank} + +An estimator with $T_{\mathrm{eff}}$ effective observations cannot support more than +$T_{\mathrm{eff}}$ assets: a 200-day window with $N=300$ gives a sample covariance of rank 200, +and a portfolio in its null space has \emph{zero} estimated variance --- an optimiser maximising +return under a risk cap will find it. The shrinkage of the measurement note repairs exactly this, +and its data-driven intensity rises with $N/T$ on its own: + +\begin{center} +\footnotesize +\begin{tabular}{@{}lrrrrr@{}} +\toprule +estimator, $N$ & $T_{\mathrm{eff}}$ & intensity & $\lambda_{\min}/\overline{\sigma^2}$ & condition & zero eigenvalues \\ +\midrule +rolling 200, $N=300$ & 200 & 0 & $-3.4\cdot10^{-15}$ & $4\cdot10^{298}$ & 101 \\ +rolling 200, $N=300$ & 200 & 0.080 & 0.023 & $3.0\cdot10^{3}$ & 0 \\ +rolling 200, $N=745$ & 200 & 0 & $-2.0\cdot10^{-14}$ & $6\cdot10^{298}$ & 546 \\ +rolling 200, $N=745$ & 200 & 0.128 & 0.025 & $4.3\cdot10^{3}$ & 0 \\ +EWM $\lambda=0.99$, $N=745$ & 300 & 0 & $2.2\cdot10^{-5}$ & $5.5\cdot10^{6}$ & 44 (numerically) \\ +EWM $\lambda=0.99$, $N=745$ & 300 & 0.114 & 0.023 & $4.8\cdot10^{3}$ & 0 \\ +\bottomrule +\end{tabular} +\end{center} + +With \code{shrinkage="auto"} --- the default everywhere --- the smallest eigenvalue never falls +below 2\,\% of the average variance, whatever the number of candidates. Without it the +degeneracy is real and silent: a null-space portfolio comes out at variance $-1.6\cdot10^{-20}$ +and the Sobol' draws span only 222 of 300 dimensions. \code{CovarianceModel.conditioning} +reports the figures and the optimisation warns when the estimate is singular or +ill-conditioned for the size of the candidate set. + +Two things soften the problem in this particular model, and neither should be relied on. An EWM +estimator over the full history is formally full rank (every observation keeps a positive +weight), so its degeneracy is numerical rather than exact; and no-short-selling with a 10\,\% +cap makes null directions hard to reach, since exploiting one generally requires a short leg. +Measured across $N=30\ldots745$, the optimised portfolio's out-of-sample-to-in-sample volatility +ratio exceeds the cap-weighted portfolio's by a factor of only 0.89--1.18, with or without +shrinkage --- the optimiser is not systematically mining the estimate. The exactly singular case +does bite elsewhere, though: with a 200-day window and $N\ge300$ the outer approximation of +Section~\ref{sec:gaussian} stops converging and returns a time-limited solution. + +\subsection{Reproducibility} +\label{sec:reproducibility} + +Two runs of the same request use the same scenario set, and so do two runs that differ only in +the parameters listed on the right of Section~\ref{sec:onesetup}: the base sample depends only on the +seed (scrambled Sobol' points and the PCG64 generator are deterministic across platforms), the +transformations are deterministic, and the result reports a fingerprint of the final matrix and +its probabilities (\code{scenario\_hash}). The fingerprint is exact on one platform; between +BLAS implementations (laptop versus the DGX Spark) the GBM sets agree to $10^{-11}$ and keep the +same hash, while the empirical model's innovation filter --- thousands of eigendecompositions --- +can differ at the $10^{-10}$ level, which changes the hash but none of the reported figures. +Whether the \emph{solution} is the same depends on the solver. HiGHS' branch-and-bound is deterministic: identical inputs give identical shares +(\code{reproducibility="exact"}) as long as the HiGHS build is the same --- across SciPy versions +the search may end on a different optimal vertex among ties, at the same objective. cuOpt combines GPU heuristics with a time-limited search and, +in version 26.08, ignores the MIP-gap settings sent over its REST interface; it proves +optimality within its default relative gap of $10^{-4}$, so the objective, expected profit and +CVaR of two runs agree to that gap (about 0.2 CHF per week on a CHF 1 million portfolio) while +near-tied share counts can differ by a few shares (\code{reproducibility="within\_gap"}). A +result that hit the time limit (\code{FeasibleFound}) is not reproducible at all. The +Gaussian exact mode runs on HiGHS by default and is exact in this sense. + +% ============================================================================================ +\section{The complete model as implemented} +% ============================================================================================ + +\code{MeanCVaRProblem.build()} assembles the following MILP from a \code{ProblemSpec}. The +variable vector is laid out as $x=(n,\;[b,\;s],\;\alpha,\;u,\;[y],\;[t])$; blocks in brackets +exist only in rebalancing mode ($b,s$), with cardinality/minimum-position rules ($y$) or with a +fixed cost per trade ($t$). The CVaR level $\zeta$ is called $\alpha$ in the code. + +\begin{center} +\small +\begin{tabular}{@{}llll@{}} +\toprule +block & name in the code & type, bounds & meaning \\ +\midrule +$n_i$ & \code{n[symbol]} & integer, $[0,\bar n_i]$ & final number of shares \\ +$b_i$ & \code{buy[symbol]} & integer, $[0,\bar n_i]$ & shares bought (rebalancing) \\ +$s_i$ & \code{sell[symbol]} & integer, $[0,n^0_i]$ & shares sold (rebalancing) \\ +$\alpha$ & \code{alpha} & continuous, free & the $\zeta$ of Theorem~\ref{thm:ru} \\ +$u_s$ & \code{u[s]} & continuous, $[0,\infty)$ & excess loss of scenario $s$ over $\alpha$ \\ +$t_{\mathrm{cvar}}$ & \code{cvar} & continuous, free & Gaussian CVaR epigraph (Section~\ref{sec:gaussian}) \\ +$y_i$ & \code{y[symbol]} & binary & position open (cardinality / minimum position) \\ +$t_i$ & \code{t[symbol]} & binary & stock traded (fixed cost) \\ +\bottomrule +\end{tabular} +\end{center} + +With $\bar n_i=\lfloor \min(\bar w,f_{\max})\,W/P_i\rfloor$, $g_{s,i}=P_iR_{s,i}$, +$\bar g_i=P_i\mu_i$ (the model's expected simple return over the horizon; the +probability-weighted scenario mean if no model mean is supplied), $\kappa_s=\frac{p_s}{1-\beta}$ +with $p_s=1/S$ unless the scenarios carry importance-sampling probabilities, and +$\TC=\sum_i(c^bP_ib_i+c^sP_is_i)+F\sum_it_i$ (in mode \code{new}: $c^b\sum_iP_in_i+F\sum_it_i$), +the rows are: + +\begin{center} +\footnotesize +\begin{tabular}{@{}lp{6.1cm}p{2.9cm}l@{}} +\toprule +row name & constraint & count & origin \\ +\midrule +\code{cvar\_scen\_s} & $\sum_i g_{s,i}n_i - \TC + \alpha + u_s \ge 0$ & $S$ & \eqref{eq:lin}, \eqref{eq:u-reb} \\ +\code{cvar\_limit} & $\alpha + \sum_s \kappa_s u_s \le \Lambda$ \newline (Gaussian: $t_{\mathrm{cvar}}\le\Lambda$) & 1, if a limit is given & \eqref{eq:cvar-constraint}, \eqref{eq:cvar-weighted} \\ +\code{cut\_j} & $\TC+(k_\beta Cv_j/\sigma_j-m)^{\top}(P\circ n)$ \newline $\quad-\,t_{\mathrm{cvar}}\le0$ & one per cut, \newline Gaussian mode & \eqref{eq:cut} \\ +\code{budget} & $\sum_iP_ib_i-\sum_iP_is_i+\TC\le c^0$ \newline (new: $\sum_iP_in_i+\TC\le c^0$) & 1 & \eqref{eq:cash} \\ +\code{link\_i} & $n_i-b_i+s_i=n^0_i$ & $N$, rebalancing & \eqref{eq:link} \\ +\code{invested} & $f_{\min}W\le\sum_iP_in_i\le f_{\max}W$ & $\le1$, if bounds given & invested fraction \\ +\code{sector\_k} & $\sum_{i\in G_k}P_in_i\le\bar w_kW$ & $K$, binding sectors & \eqref{eq:milp-n-sector} \\ +\code{max\_cost} & $\TC\le\TC_{\max}$ & $\le1$ & \eqref{eq:turnover} \\ +\code{turnover} & $\sum_iP_i(b_i+s_i)\le\tau W$ & $\le1$, rebalancing & \eqref{eq:turnover} \\ +\code{open\_i} & $n_i-\max(\bar n_i,1)\,y_i\le0$ & $N$, optional & \eqref{eq:card} \\ +\code{minpos\_i} & $P_in_i-v_{\min}y_i\ge0$ & $N$, optional & \eqref{eq:card} \\ +\code{cardinality} & $\sum_iy_i\le K_{\max}$ & $\le1$, optional & \eqref{eq:card} \\ +\code{trade\_i} & $b_i+s_i-M_it_i\le0$ \newline (new: $n_i-M_it_i\le0$) & $N$, fixed cost only & \eqref{eq:tc} \\ +\code{target\_pnl} & $\sum_i\bar g_in_i-\TC\ge\rho$ & 1, \code{min\_cvar} only & \eqref{eq:three}(b) \\ +\bottomrule +\end{tabular} +\end{center} + +The objective is one of +\begin{align} + \code{max\_return}:&\quad \max\; \sum_i\bar g_i n_i - \TC \quad\text{subject to \code{cvar\_limit}},\\ + \code{min\_cvar}:&\quad \min\; \alpha+\sum_s\kappa_s u_s \quad\text{subject to \code{target\_pnl}},\\ + \code{utility}:&\quad \max\; \sum_i\bar g_i n_i - \TC - \lambda\Big(\alpha+\sum_s\kappa_s u_s\Big), +\end{align} +the three forms of \eqref{eq:three} (in Gaussian mode $\alpha+\sum_s\kappa_su_s$ is replaced by +$t_{\mathrm{cvar}}$). All coefficients are in CHF; the scenario rows carry $N(+2N)+2$ non-zeros +each, so the matrix has about $S(N+2)$ non-zeros (420\,k for $N=100$, $S=4\,096$; 3\,M for the +full universe of 745 stocks with $S=4\,096$). Duplicate index pairs +that arise when a variable appears in several terms of a row (e.g.\ $n_i$ in the gain and in the +cost of mode \code{new}) are summed when the sparse matrix is assembled. + +\paragraph{The two solvers.} +The same \code{MILPModel} is accepted by both back ends. \emph{cuOpt} is NVIDIA's GPU +optimisation library, reached over HTTP as a self-hosted server (version 26.08 here) running as a +container on the DGX Spark; it is the only component that uses the GPU. \emph{HiGHS} is the +open-source LP/MILP solver of ERGO-Code at the University of Edinburgh (MIT licence, +\url{https://highs.dev}) \cite{HuangfuHall2018}; it is not installed or started separately but +ships compiled inside SciPy, so \code{scipy.optimize.milp} runs it \emph{in the calling process on +the CPU} --- in production inside the same container as the service (SciPy 1.18.1, HiGHS 1.12.0, +four threads). It serves as the fallback when cuOpt is unreachable, as the solver of the small cut +MILPs of the Gaussian mode, and as the oracle of the test suite. + +\paragraph{Solving and decoding.} +The model is sent to the cuOpt server as an LP/MILP request (CSR matrix, bounds, variable types, +a mandatory time limit; msgpack-encoded) and polled until a solution arrives; if the server is +unreachable the same \code{MILPModel} is handed to HiGHS. From 8\,192 scenarios on the +service solves by row generation (Section~\ref{sec:rowgen}), i.e.\ a sequence of such MILPs on +the working set; in Gaussian mode by the outer-approximation loop (Section~\ref{sec:gaussian}). +A status \code{Optimal} means proven optimality within the solver's relative gap; +\code{FeasibleFound} means the time limit stopped the search at an incumbent whose +\code{mip\_gap} is reported. The result states the scenario fingerprint and the level of +reproducibility (Section~\ref{sec:reproducibility}). From the solution vector, +\code{MeanCVaRProblem.decode()} rounds the integer blocks (they are integral up to solver +tolerance), rebuilds trades, costs, cash, position values and weights, and \emph{recomputes} the +expected profit (from the model means), VaR and CVaR of the decoded portfolio from the +weighted scenario set (Remark~\ref{rem:discrete}) or, in Gaussian mode, from the closed form; +these recomputed figures are what the service reports, together +with the provenance of every model input (covariance model, scenario model and seed, expected +returns and views, constraints, costs, solver statistics). + +\paragraph{What the model does not do.} +It assumes the scenario set represents the horizon distribution and that all trades execute at +today's prices; it holds the portfolio unchanged over the horizon (no intra-horizon rebalancing); +cash earns nothing over the horizon (the CHF risk-free rate is zero); it is single-period. These +are deliberate simplifications of a teaching platform, not limitations of the formulation --- +multi-period extensions replace the scenario rows by a scenario tree. + +% ============================================================================================ +\begin{thebibliography}{99} +\bibitem{RockafellarUryasev2000} + R.\,T.~Rockafellar and S.~Uryasev, + \emph{Optimization of conditional value-at-risk}, + Journal of Risk 2(3), 21--41, 2000. +\bibitem{RockafellarUryasev2002} + R.\,T.~Rockafellar and S.~Uryasev, + \emph{Conditional value-at-risk for general loss distributions}, + Journal of Banking \& Finance 26(7), 1443--1471, 2002. +\bibitem{KrokhmalPalmquistUryasev2002} + P.~Krokhmal, J.~Palmquist and S.~Uryasev, + \emph{Portfolio optimization with conditional value-at-risk objective and constraints}, + Journal of Risk 4(2), 43--68, 2002. +\bibitem{AcerbiTasche2002} + C.~Acerbi and D.~Tasche, + \emph{On the coherence of expected shortfall}, + Journal of Banking \& Finance 26(7), 1487--1503, 2002. +\bibitem{LoboFazelBoyd2007} + M.\,S.~Lobo, M.~Fazel and S.~Boyd, + \emph{Portfolio optimization with linear and fixed transaction costs}, + Annals of Operations Research 152, 341--365, 2007. +\bibitem{KellererMansiniSperanza2000} + H.~Kellerer, R.~Mansini and M.\,G.~Speranza, + \emph{Selecting portfolios with fixed costs and minimum transaction lots}, + Annals of Operations Research 99, 287--304, 2000. +\bibitem{HuangfuHall2018} + Q.~Huangfu and J.\,A.\,J.~Hall, + \emph{Parallelizing the dual revised simplex method}, + Mathematical Programming Computation 10, 119--142, 2018. +\bibitem{ShapiroDentchevaRuszczynski2009} + A.~Shapiro, D.~Dentcheva and A.~Ruszczy\'nski, + \emph{Lectures on Stochastic Programming: Modeling and Theory}, + SIAM, Philadelphia, 2009. +\bibitem{Michaud1989} + R.\,O.~Michaud, + \emph{The Markowitz optimization enigma: is `optimized' optimal?}, + Financial Analysts Journal 45(1), 31--42, 1989. +\bibitem{Owen1998} + A.\,B.~Owen, + \emph{Scrambling Sobol' and Niederreiter--Xing points}, + Journal of Complexity 14(4), 466--489, 1998. +\bibitem{Glasserman2004} + P.~Glasserman, + \emph{Monte Carlo Methods in Financial Engineering}, + Springer, New York, 2004. +\bibitem{HoylandWallace2001} + K.~H\o yland and S.\,W.~Wallace, + \emph{Generating scenario trees for multistage decision problems}, + Management Science 47(2), 295--307, 2001. +\bibitem{GlassermanHeidelbergerShahabuddin2000} + P.~Glasserman, P.~Heidelberger and P.~Shahabuddin, + \emph{Variance reduction techniques for estimating value-at-risk}, + Management Science 46(10), 1349--1364, 2000. +\bibitem{Hesterberg1995} + T.~Hesterberg, + \emph{Weighted average importance sampling and defensive mixture distributions}, + Technometrics 37(2), 185--194, 1995. +\bibitem{KunziBayMayer2006} + A.~K\"unzi-Bay and J.~Mayer, + \emph{Computational aspects of minimizing conditional value-at-risk}, + Computational Management Science 3(1), 3--27, 2006. +\bibitem{EmbrechtsMcNeilStraumann2002} + P.~Embrechts, A.\,J.~McNeil and D.~Straumann, + \emph{Correlation and dependence in risk management: properties and pitfalls}, + in: M.~Dempster (ed.), Risk Management: Value at Risk and Beyond, Cambridge University Press, + 176--223, 2002. +\bibitem{Kelley1960} + J.\,E.~Kelley, + \emph{The cutting-plane method for solving convex programs}, + Journal of the SIAM 8(4), 703--712, 1960. +\bibitem{DuranGrossmann1986} + M.\,A.~Duran and I.\,E.~Grossmann, + \emph{An outer-approximation algorithm for a class of mixed-integer nonlinear programs}, + Mathematical Programming 36, 307--339, 1986. +\bibitem{MansiniOgryczakSperanza2014} + R.~Mansini, W.~Ogryczak and M.\,G.~Speranza, + \emph{Twenty years of linear programming based portfolio optimization}, + European Journal of Operational Research 234(2), 518--535, 2014. +\end{thebibliography} + +\end{document} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..92d0fdc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,49 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "quantcore" +version = "0.1.0" +description = "Deterministic quant core of the AI-Augmented Portfolio Intelligence Platform: data, risk/performance measurement, stock-price models, Black-Litterman expected returns and mean-CVaR MILP optimisation on NVIDIA cuOpt." +readme = "README.md" +requires-python = ">=3.11" +license = { text = "MIT" } +authors = [{ name = "Florian Herzog" }] +dependencies = [ + "numpy>=1.26", + "pandas>=2.1", + "scipy>=1.11", + "pyarrow>=14", + "yfinance>=0.2.50", + "requests>=2.31", + "lxml>=4.9", + "pydantic>=2.5", + "pydantic-settings>=2.1", + "fastapi>=0.109", + "uvicorn[standard]>=0.27", + "msgpack>=1.0", +] + +[project.optional-dependencies] +dev = ["pytest>=7.4", "pytest-cov", "httpx>=0.25", "ruff"] + +[project.scripts] +quantcore = "quantcore.cli:main" +quantcore-api = "quantcore.apicli:main" + +[tool.setuptools.packages.find] +include = ["quantcore*"] + +[tool.setuptools.package-data] +"quantcore.data" = ["static/*.csv", "static/*.json"] +"quantcore.api" = ["ui/*.html", "ui/*.css", "ui/js/*.js", "ui/js/tabs/*.js"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-q" +filterwarnings = ["ignore::DeprecationWarning", "ignore::FutureWarning"] + +[tool.ruff] +line-length = 110 +target-version = "py311" diff --git a/quantcore/__init__.py b/quantcore/__init__.py new file mode 100644 index 0000000..317908c --- /dev/null +++ b/quantcore/__init__.py @@ -0,0 +1,24 @@ +"""quantcore -- the deterministic quant core of the AI-Augmented Portfolio Intelligence Platform. + +The package is organised as a set of *services* with plain-Python contracts: + +* :mod:`quantcore.universe` -- ``SetupService``: builds the investment universe once + (largest 500 US + 500 European stocks, metadata, first price date, market capitalisation). +* :mod:`quantcore.data` -- ``DataDownloadService`` and ``DataStore``: Yahoo Finance + prices and FX rates, incremental updates, snapshots, resilient HTTP access. +* :mod:`quantcore.measurement` -- ``RiskService`` / ``PerformanceService``: log-returns, + moving-window and exponentially weighted volatility, correlation, VaR and CVaR, performance. +* :mod:`quantcore.models` -- covariance estimators, market index, stock-price simulation + (GBM and empirical innovations), Black-Litterman with Idzorek confidence views. +* :mod:`quantcore.optimization` -- mean-CVaR MILP in integer share numbers solved on NVIDIA + cuOpt (with a local HiGHS fallback), in "new portfolio" and "rebalancing" mode. +* :mod:`quantcore.api` -- FastAPI application exposing the services over HTTP/JSON. + +Everything in here is deterministic: same input, same output. No LLM is called anywhere in +this package -- the AI layer of the platform consumes these services through their contracts. +""" + +from quantcore.config import Settings, get_settings + +__all__ = ["Settings", "get_settings", "__version__"] +__version__ = "0.1.0" diff --git a/quantcore/api/__init__.py b/quantcore/api/__init__.py new file mode 100644 index 0000000..eb58ebd --- /dev/null +++ b/quantcore/api/__init__.py @@ -0,0 +1 @@ +"""HTTP/JSON API (FastAPI) exposing the deterministic services.""" diff --git a/quantcore/api/app.py b/quantcore/api/app.py new file mode 100644 index 0000000..4cadca8 --- /dev/null +++ b/quantcore/api/app.py @@ -0,0 +1,556 @@ +"""FastAPI application -- the HTTP/JSON surface of the quant core. + +Run with ``quantcore serve`` (or ``uvicorn quantcore.api.app:app``). Interactive docs at +``/docs``. Long-running work (setup, data update) runs as background jobs; everything else +answers synchronously. All numbers come from the deterministic services -- the API adds no +logic of its own. +""" + +from __future__ import annotations + +import json +import logging +import threading +import uuid +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +import numpy as np +import pandas as pd +from fastapi import BackgroundTasks, FastAPI, HTTPException, Query +from fastapi.responses import RedirectResponse +from fastapi.staticfiles import StaticFiles + +from quantcore import __version__ +from quantcore.api import schemas as S +from quantcore.config import Settings, get_settings +from quantcore.data.download_service import DataDownloadService +from quantcore.data.market_data import MarketDataService +from quantcore.data.store import DataStore +from quantcore.measurement.performance import PerformanceService, ewm_performance, rolling_performance +from quantcore.measurement.returns import horizon_periods, periods_per_year +from quantcore.measurement.risk import RiskService +from quantcore.models.covariance import CovarianceModel +from quantcore.optimization.service import OptimizationRequest, OptimizationService +from quantcore.optimization.solvers import CuOptSolver +from quantcore.universe.setup_service import SetupService + +log = logging.getLogger(__name__) + +app = FastAPI( + title="quantcore -- Portfolio Risk Measurement & Optimisation", + version=__version__, + description=( + "Deterministic services of the AI-Augmented Portfolio Intelligence Platform: market data, " + "risk and performance measurement, stock-price models, Black-Litterman expected returns and " + "mean-CVaR MILP optimisation (NVIDIA cuOpt)." + ), +) + + +# ------------------------------------------------------------------------------------------------ +# state +# ------------------------------------------------------------------------------------------------ +class State: + def __init__(self, settings: Settings | None = None): + self.settings = settings or get_settings() + self.store = DataStore(self.settings) + self._md: MarketDataService | None = None + self.jobs: dict[str, dict] = {} + self.lock = threading.Lock() + + @property + def md(self) -> MarketDataService: + if self._md is None: + if not self.store.has_prices(): + raise HTTPException(503, "no market data yet - run the setup job (POST /jobs/setup)") + self._md = MarketDataService(self.settings, self.store) + return self._md + + def reload(self) -> None: + self._md = None + + +state = State() + + +def _records(df: pd.DataFrame, index_name: str = "symbol") -> list[dict]: + out = df.copy() + out.index.name = index_name + out = out.reset_index() + return json.loads(out.to_json(orient="records", date_format="iso", double_precision=10)) + + +def _frame(df: pd.DataFrame) -> dict: + """Wide frame -> ``{"index": [...], "columns": [...], "data": [[...], ...]}`` (NaN -> null).""" + return { + "index": [i.isoformat() if hasattr(i, "isoformat") else str(i) for i in df.index], + "columns": [str(c) for c in df.columns], + "data": json.loads(json.dumps(df.values.tolist(), default=float)) if df.size else [], + } + + +def _clean(obj: Any) -> Any: + """Recursively turn numpy / NaN / timestamps into JSON-safe values.""" + if isinstance(obj, dict): + return {str(k): _clean(v) for k, v in obj.items()} + if isinstance(obj, (list, tuple)): + return [_clean(v) for v in obj] + if isinstance(obj, (np.floating, float)): + return None if not np.isfinite(obj) else float(obj) + if isinstance(obj, (np.integer,)): + return int(obj) + if isinstance(obj, np.ndarray): + return _clean(obj.tolist()) + if isinstance(obj, (pd.Timestamp, datetime)): + return obj.isoformat() + if isinstance(obj, pd.Series): + return _clean(obj.to_dict()) + return obj + + +def _http(exc: Exception) -> HTTPException: + if isinstance(exc, (KeyError, ValueError)): + return HTTPException(422, str(exc)) + if isinstance(exc, FileNotFoundError): + return HTTPException(503, str(exc)) + log.exception("request failed") + return HTTPException(500, f"{type(exc).__name__}: {exc}") + + +# ------------------------------------------------------------------------------------------------ +# health, universe, data +# ------------------------------------------------------------------------------------------------ +@app.get("/health", tags=["system"]) +def health() -> dict: + return _clean({"status": "ok", "version": __version__, "store": state.store.status(), + "cuopt": CuOptSolver(state.settings).health(), "base_currency": state.settings.base_currency}) + + +@app.get("/universe", tags=["data"]) +def universe(region: str | None = None, sector: str | None = None, country: str | None = None) -> list[dict]: + try: + md = state.md + symbols = md.symbols(region, sector, country) + return _records(md.universe.loc[symbols]) + except Exception as exc: # noqa: BLE001 + raise _http(exc) from exc + + +@app.get("/universe/{symbol}", tags=["data"]) +def universe_symbol(symbol: str) -> dict: + md = state.md + if symbol not in md.universe.index: + raise HTTPException(404, f"{symbol} not in the universe") + return _clean({"symbol": symbol, **md.universe.loc[symbol].to_dict()}) + + +@app.get("/prices", tags=["data"]) +def prices(symbols: str = Query(..., description="comma-separated symbols"), start: str | None = None, + end: str | None = None, currency: str = "base", freq: str = "daily") -> dict: + try: + px = state.md.prices(symbols.split(","), start, end, currency, freq) + return {"currency": currency, "freq": freq, **_frame(px)} + except Exception as exc: # noqa: BLE001 + raise _http(exc) from exc + + +@app.get("/returns", tags=["data"]) +def returns(symbols: str = Query(...), start: str | None = None, end: str | None = None, currency: str = "base", + freq: str = "daily") -> dict: + try: + r = state.md.returns(symbols.split(","), start, end, currency, freq) + return {"currency": currency, "freq": freq, "kind": "log_returns", **_frame(r)} + except Exception as exc: # noqa: BLE001 + raise _http(exc) from exc + + +@app.get("/data/symbols", tags=["data"]) +def data_symbols(region: str | None = None, sector: str | None = None, country: str | None = None, + search: str | None = None, limit: int = Query(200, ge=1, le=2000), + sort: str = Query("market_cap", pattern="^(market_cap|symbol|name|sector)$")) -> dict: + """The universe as one table (metadata, current market cap and price) plus the filter facets. + + One call feeds a stock picker: the matching rows and the counts per region, sector and country. + """ + try: + md = state.md + u = md.universe + caps = md.market_caps().reindex(u.index) + px = md.latest_prices(u.index.tolist(), currency="base").reindex(u.index) + table = pd.DataFrame({ + "name": u["name"], "region": u["region"], "country": u["country"], "sector": u["gics_sector"], + "industry": u["yahoo_industry"], "currency": u["listing_currency"], "exchange": u["exchange"], + "market_cap": caps, "market_weight": caps / caps.sum(), "price": px, + "price_local": u["last_price"], "first_price_date": u["first_price_date"].astype(str), + "price_coverage": u["price_coverage"], "index_weight": u["index_weight"], + }) + facets = {"regions": table["region"].value_counts().to_dict(), + "sectors": table["sector"].fillna("Unknown").value_counts().to_dict(), + "countries": table["country"].value_counts().to_dict()} + m = table + if region: + m = m[m["region"].str.lower() == region.lower()] + if sector: + m = m[m["sector"].fillna("Unknown").str.lower() == sector.lower()] + if country: + m = m[m["country"].str.lower() == country.lower()] + if search: + q = search.lower() + m = m[[q in str(i).lower() or q in str(n).lower() for i, n in zip(m.index, m["name"])]] + m = m.sort_values("market_cap", ascending=False) if sort == "market_cap" else m.sort_values(sort) + return _clean({"as_of": md.last_date(), "base_currency": state.settings.base_currency, + "n_universe": int(len(table)), "n_matched": int(len(m)), "facets": facets, + "symbols": _records(m.head(limit))}) + except Exception as exc: # noqa: BLE001 + raise _http(exc) from exc + + +@app.get("/market/index", tags=["data"]) +def market_index(start: str | None = None, end: str | None = None, freq: str = "weekly") -> dict: + try: + r = state.md.cap_weighted_index(start=start, end=end, freq=freq) + ppy = periods_per_year(freq) + return _clean({"freq": freq, "mean_ann": float(r.mean() * ppy), "vol_ann": float(r.std() * np.sqrt(ppy)), + "n": int(len(r)), **_frame(r.to_frame())}) + except Exception as exc: # noqa: BLE001 + raise _http(exc) from exc + + +@app.get("/market/weights", tags=["data"]) +def market_weights(max_assets: int | None = None) -> dict: + try: + w = state.md.market_weights().sort_values(ascending=False) + if max_assets: + w = w.iloc[:max_assets] + return _clean({"as_of": state.md.last_date(), "weights": w.round(8).to_dict()}) + except Exception as exc: # noqa: BLE001 + raise _http(exc) from exc + + +# ------------------------------------------------------------------------------------------------ +# risk +# ------------------------------------------------------------------------------------------------ +@app.post("/risk/assets", tags=["risk"]) +def risk_assets(req: S.AssetRiskRequest) -> list[dict]: + try: + table = RiskService(state.md).asset_risk(req.symbols, req.freq, req.currency, req.window, req.lam, + tuple(req.confidence), req.horizon, req.start, req.end, + space=req.space, drift=req.drift) + return _records(table) + except Exception as exc: # noqa: BLE001 + raise _http(exc) from exc + + +@app.post("/risk/series", tags=["risk"]) +def risk_series(req: S.RiskSeriesRequest) -> dict: + try: + df = RiskService(state.md).risk_series(req.symbol, req.freq, req.currency, req.window, req.lam, + tuple(req.confidence), req.horizon, req.start, req.end) + return {"symbol": req.symbol, **_frame(df.dropna(how="all"))} + except Exception as exc: # noqa: BLE001 + raise _http(exc) from exc + + +@app.post("/risk/covariance", tags=["risk"]) +def risk_covariance(req: S.CovarianceRequest) -> dict: + try: + cov = RiskService(state.md).covariance(req.symbols, req.freq, req.currency, req.method, req.window, req.lam, + req.shrinkage, req.start, req.end) + return {"method": req.method, "freq": req.freq, **_frame(cov)} + except Exception as exc: # noqa: BLE001 + raise _http(exc) from exc + + +@app.post("/risk/correlation", tags=["risk"]) +def risk_correlation(req: S.CovarianceRequest) -> dict: + try: + corr = RiskService(state.md).correlation(req.symbols, req.freq, req.currency, req.method, req.window, req.lam, + req.start, req.end) + return {"method": req.method, "freq": req.freq, **_frame(corr)} + except Exception as exc: # noqa: BLE001 + raise _http(exc) from exc + + +@app.post("/risk/portfolio", tags=["risk"]) +def risk_portfolio(req: S.PortfolioRiskRequest) -> dict: + try: + pr = RiskService(state.md).portfolio_risk(req.holdings, req.cash, req.freq, req.currency, req.horizon, + tuple(req.confidence), req.method, req.cov_method, req.window, + req.lam, req.shrinkage, req.start, req.end) + return _clean(pr.as_dict()) + except Exception as exc: # noqa: BLE001 + raise _http(exc) from exc + + +# ------------------------------------------------------------------------------------------------ +# performance +# ------------------------------------------------------------------------------------------------ +def _perf() -> PerformanceService: + return PerformanceService(state.md, state.settings.risk_free_rate) + + +@app.post("/performance/assets", tags=["performance"]) +def performance_assets(req: S.PerformanceRequest) -> list[dict]: + try: + return _records(_perf().asset_performance(req.symbols, req.freq, req.currency, req.start, req.end)) + except Exception as exc: # noqa: BLE001 + raise _http(exc) from exc + + +@app.post("/performance/rolling", tags=["performance"]) +def performance_rolling(req: S.RollingPerformanceRequest) -> dict: + try: + r = state.md.returns([req.symbol], req.start, req.end, req.currency, req.freq)[req.symbol].dropna() + rf = state.settings.risk_free_rate + df = ewm_performance(r, req.lam, req.freq, rf) if req.lam else rolling_performance(r, req.window, req.freq, rf) + return {"symbol": req.symbol, **_frame(df.dropna(how="all"))} + except Exception as exc: # noqa: BLE001 + raise _http(exc) from exc + + +@app.post("/performance/portfolio", tags=["performance"]) +def performance_portfolio(req: S.PortfolioPerformanceRequest) -> dict: + try: + row = _perf().portfolio_performance(req.holdings, req.freq, req.currency, req.start, req.end, req.rebalanced) + return _clean(row.to_dict()) + except Exception as exc: # noqa: BLE001 + raise _http(exc) from exc + + +# ------------------------------------------------------------------------------------------------ +# models +# ------------------------------------------------------------------------------------------------ +@app.post("/model/expected-returns", tags=["models"]) +def expected_returns(req: S.ExpectedReturnsRequest) -> dict: + try: + md = state.md + svc = OptimizationService(state.settings, md) + oreq = OptimizationRequest(symbols=req.symbols, max_assets=req.max_assets, views=[v.as_dict() for v in req.views], + bl_tau=req.tau, bl_years=req.years, bl_omega_method=req.omega_method, cvar_limit=1.0) + symbols = svc.candidate_symbols(oreq) + # the very model the optimiser uses (arithmetic equilibrium return, same tau, same views) + mu_ann, desc, result = svc.expected_returns(oreq, symbols) + start = (md.last_date() - pd.DateOffset(years=req.years)).date().isoformat() + weekly = md.returns(symbols, start=start, currency="base", freq="weekly").dropna() + out = result.as_dict() + out["volatility_ann"] = (weekly.std() * np.sqrt(52)).round(6).to_dict() + out["expected_returns_ann"] = mu_ann.round(6).to_dict() + out["model"] = desc + out["omega_method"] = req.omega_method + return _clean(out) + except Exception as exc: # noqa: BLE001 + raise _http(exc) from exc + + +@app.post("/model/covariance", tags=["models"]) +def model_covariance(req: S.ModelCovarianceRequest) -> dict: + """Covariance and correlation of the estimator the scenario generator and the optimiser use. + + Unlike ``/risk/covariance`` (a measurement with explicit parameters) this takes the same + ``covariance`` model dict as ``/optimize``, scales it to the horizon and reports how well + conditioned the estimate is for the number of assets. + """ + try: + md = state.md + model = CovarianceModel.from_dict(req.covariance) + r = md.returns(req.symbols, start=req.start, end=req.end, currency="base", freq=req.freq).dropna() + cov = model.estimate(r) + h = horizon_periods(req.horizon, req.freq) + ppy = periods_per_year(req.freq) + sd = np.sqrt(np.diag(cov.values)) + corr = pd.DataFrame(cov.values / np.outer(sd, sd), index=cov.index, columns=cov.columns) + vals = np.linalg.eigvalsh(cov.values)[::-1] + off = corr.values[np.triu_indices(len(sd), 1)] + return _clean({ + "model": model.describe(), "shrinkage_applied": model.intensity(r), + "conditioning": model.conditioning(r), "observations": int(len(r)), + "freq": req.freq, "horizon_periods": h, "as_of": r.index[-1], + "volatility_ann": dict(zip(cov.index, sd * np.sqrt(ppy))), + "volatility_horizon": dict(zip(cov.index, sd * np.sqrt(h))), + "eigenvalues": (vals / vals.sum()).tolist(), + "correlation_summary": {"mean": float(off.mean()), "min": float(off.min()), "max": float(off.max())}, + "covariance": _frame(cov * h), "correlation": _frame(corr), + }) + except Exception as exc: # noqa: BLE001 + raise _http(exc) from exc + + +@app.post("/model/scenarios", tags=["models"]) +def scenarios(req: S.ScenarioRequest) -> dict: + try: + md = state.md + svc = OptimizationService(state.settings, md) + oreq = OptimizationRequest(symbols=req.symbols, expected_returns=req.expected_returns, cvar_limit=1.0, + horizon=req.horizon, freq=req.freq, covariance=req.covariance, seed=req.seed, + n_scenarios=req.n_scenarios, scenario_model=req.model, + innovation_history=req.innovation_history, sampling=req.sampling, + moment_match=req.moment_match, drift_convention=req.drift_convention, + importance_sampling=req.importance_sampling, bl_tau=req.bl_tau, + bl_years=req.bl_years, bl_omega_method=req.bl_omega_method, + views=[v.as_dict() for v in req.views]) + symbols = svc.candidate_symbols(oreq) + mu_ann, desc, _ = svc.expected_returns(oreq, symbols) + scen = svc.scenarios(oreq, symbols, mu_ann) + simple = scen.simple_returns() + out = {"description": scen.describe(), "expected_returns": desc, "scenario_key": scen.extra.get("key"), + "expected_returns_ann": _clean(mu_ann.reindex(symbols).to_dict()), + "summary": _records(scen.summary()), "horizon_periods": horizon_periods(req.horizon, req.freq)} + if req.focus: + out["asset_histograms"] = {} + for sym in req.focus: + if sym not in symbols: + continue + col = simple[:, symbols.index(sym)] + mass, edges = np.histogram(col, bins=req.bins, weights=scen.weights) + out["asset_histograms"][sym] = {"edges": edges.tolist(), "mass": mass.tolist()} + exposure, value = _exposure(req, symbols) + if exposure is not None: + out["portfolio"] = { + "value": value, "cash": req.cash, "invested": float(exposure.sum()), + "exposure": {s: float(v) for s, v in zip(symbols, exposure) if v != 0}, + **scen.distribution(exposure, bins=req.bins, confidence=tuple(req.confidence)), + } + return _clean(out) + except Exception as exc: # noqa: BLE001 + raise _http(exc) from exc + + +def _exposure(req: S.ScenarioRequest, symbols: list[str]) -> tuple[np.ndarray | None, float]: + """Money exposure per candidate from ``holdings`` (shares) or ``weights`` (of ``value``).""" + if req.holdings: + px = state.md.latest_prices(symbols, currency="base").reindex(symbols) + v = np.array([float(req.holdings.get(s, 0.0)) for s in symbols]) * px.values + return v, float(v.sum() + req.cash) + if req.weights: + v = np.array([float(req.weights.get(s, 0.0)) for s in symbols]) * req.value + return v, float(req.value) + return None, 0.0 + + +# ------------------------------------------------------------------------------------------------ +# optimisation +# ------------------------------------------------------------------------------------------------ +@app.post("/optimize", tags=["optimisation"]) +def optimize(req: S.OptimizeRequest) -> dict: + try: + payload = req.model_dump() + payload["views"] = [v.as_dict() for v in req.views] + oreq = OptimizationRequest(**payload) + result = OptimizationService(state.settings, state.md).optimize(oreq) + return _clean(result.as_dict()) + except Exception as exc: # noqa: BLE001 + raise _http(exc) from exc + + +# ------------------------------------------------------------------------------------------------ +# jobs: setup and data update (background), snapshots +# ------------------------------------------------------------------------------------------------ +def _run_job(job_id: str, fn) -> None: + job = state.jobs[job_id] + job["status"], job["started_at"] = "running", datetime.now(timezone.utc).isoformat(timespec="seconds") + try: + job["result"] = _clean(fn()) + job["status"] = "done" + state.reload() + except Exception as exc: # noqa: BLE001 + log.exception("job %s failed", job_id) + job["status"], job["error"] = "failed", f"{type(exc).__name__}: {exc}" + job["finished_at"] = datetime.now(timezone.utc).isoformat(timespec="seconds") + try: + state.settings.jobs_dir.mkdir(parents=True, exist_ok=True) + (state.settings.jobs_dir / f"{job_id}.json").write_text(json.dumps(job, indent=2, default=str)) + except OSError: + pass + + +def _submit(kind: str, fn, background: BackgroundTasks) -> dict: + with state.lock: + running = [j for j in state.jobs.values() if j["status"] in ("queued", "running") and j["kind"] in ("setup", "update")] + if running and kind in ("setup", "update"): + raise HTTPException(409, f"a {running[0]['kind']} job is already running ({running[0]['id']})") + job_id = uuid.uuid4().hex[:12] + state.jobs[job_id] = {"id": job_id, "kind": kind, "status": "queued", "started_at": None, "finished_at": None, + "result": None, "error": None} + background.add_task(_run_job, job_id, fn) + return state.jobs[job_id] + + +@app.post("/jobs/setup", tags=["jobs"], status_code=202) +def job_setup(background: BackgroundTasks, offline: bool = False, max_per_region: int | None = None) -> dict: + return _submit("setup", lambda: SetupService(state.settings, state.store).run(allow_web=not offline, + max_per_region=max_per_region).as_dict(), + background) + + +@app.post("/jobs/update", tags=["jobs"], status_code=202) +def job_update(background: BackgroundTasks, full: bool = False) -> dict: + return _submit("update", lambda: DataDownloadService(state.settings, state.store).update(full=full).as_dict(), background) + + +@app.get("/jobs", tags=["jobs"]) +def jobs() -> list[dict]: + return list(state.jobs.values()) + + +@app.get("/jobs/{job_id}", tags=["jobs"]) +def job(job_id: str) -> dict: + if job_id not in state.jobs: + raise HTTPException(404, "unknown job") + return state.jobs[job_id] + + +@app.post("/snapshots", tags=["jobs"], status_code=201) +def create_snapshot(req: S.SnapshotRequest) -> dict: + try: + return {"path": str(state.store.create_snapshot(req.name))} + except FileExistsError as exc: + raise HTTPException(409, f"snapshot exists: {exc}") from exc + + +@app.get("/snapshots", tags=["jobs"]) +def list_snapshots() -> list[str]: + return state.store.list_snapshots() + + +# ------------------------------------------------------------------------------------------------ +# the test bench (single-page UI served from this app) +# ------------------------------------------------------------------------------------------------ +UI_DIR = Path(__file__).with_name("ui") + + +class _NoCacheStatic(StaticFiles): + """Static files that must be revalidated on every request. + + The bench is edited while it is open, and without a ``Cache-Control`` header a browser applies + heuristic caching to the ES modules: it keeps serving yesterday's ``optimize.js`` and the reader + sees a fixed bug that is still there. ``no-cache`` does not mean "do not store" -- the ETag still + produces a 304 for an unchanged file, so revalidation costs a round trip and no body. + """ + + async def get_response(self, path: str, scope): # noqa: ANN001 (Starlette's own signature) + response = await super().get_response(path, scope) + response.headers["Cache-Control"] = "no-cache, must-revalidate" + return response + + +def mount_test_bench(application: FastAPI, settings: Settings) -> bool: + """Serve the static test bench at ``/ui/`` and redirect ``/`` to it; ``True`` when mounted. + + A deployment-time switch (``QUANTCORE_ENABLE_UI=false``, read once when this module is + imported): the bench is a client of this API, not part of it, so an installation whose users + build their own front end can ship the same image without it. ``/docs`` is unaffected. + """ + if not settings.enable_ui or not UI_DIR.is_dir(): + return False + application.mount("/ui", _NoCacheStatic(directory=UI_DIR, html=True), name="ui") + + @application.get("/", include_in_schema=False) + def _root() -> RedirectResponse: + return RedirectResponse("/ui/") + + return True + + +UI_ENABLED = mount_test_bench(app, state.settings) diff --git a/quantcore/api/schemas.py b/quantcore/api/schemas.py new file mode 100644 index 0000000..8dd3a05 --- /dev/null +++ b/quantcore/api/schemas.py @@ -0,0 +1,233 @@ +"""Pydantic request/response models of the HTTP API (the *contracts* of the services).""" + +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel, Field + + +class SymbolsRequest(BaseModel): + symbols: list[str] = Field(..., min_length=1, description="Yahoo symbols from the universe") + freq: str = Field("daily", pattern="^(daily|weekly|monthly)$") + currency: str = Field("base", pattern="^(base|local)$") + start: str | None = None + end: str | None = None + + +class AssetRiskRequest(SymbolsRequest): + window: int = Field(200, ge=10, description="moving-window length in periods") + lam: float = Field(0.94, gt=0, lt=1, description="EWM decay") + confidence: list[float] = Field(default=[0.95, 0.99]) + horizon: str | int = Field("day", description="'day' | 'week' | 'month' | integer periods") + drift: str = Field("estimated", pattern="^(estimated|zero)$", + description="'estimated': the window's mean enters the parametric VaR/CVaR; 'zero': " + "volatility only (the usual market-risk convention, and the only sane one " + "for long horizons, where the mean is the noisiest input)") + space: str = Field("log", pattern="^(log|value)$", + description="'log': VaR/CVaR as log-returns (the modelling space); 'value': as a " + "fraction of the position's value, 1 - exp(-x), which cannot reach 100 %") + + +class RiskSeriesRequest(BaseModel): + symbol: str + freq: str = Field("daily", pattern="^(daily|weekly|monthly)$") + currency: str = Field("base", pattern="^(base|local)$") + window: int = Field(200, ge=10) + lam: float = Field(0.94, gt=0, lt=1) + confidence: list[float] = Field(default=[0.95, 0.99]) + horizon: str | int = "day" + start: str | None = None + end: str | None = None + + +class CovarianceRequest(SymbolsRequest): + method: str = Field("rolling", pattern="^(rolling|ewm)$") + window: int = Field(200, ge=10) + lam: float = Field(0.94, gt=0, lt=1) + shrinkage: float = Field(0.0, ge=0, le=1) + + +class PortfolioRiskRequest(BaseModel): + holdings: dict[str, float] = Field(..., description="number of shares per symbol") + cash: float = 0.0 + freq: str = Field("daily", pattern="^(daily|weekly|monthly)$") + currency: str = Field("base", pattern="^(base|local)$") + horizon: str | int = "week" + confidence: list[float] = Field(default=[0.95, 0.99]) + method: str = Field("normal", pattern="^(normal|historical)$") + cov_method: str = Field("rolling", pattern="^(rolling|ewm)$") + window: int = Field(200, ge=10) + lam: float = Field(0.94, gt=0, lt=1) + shrinkage: float = Field(0.0, ge=0, le=1) + start: str | None = None + end: str | None = None + + +class PerformanceRequest(SymbolsRequest): + pass + + +class RollingPerformanceRequest(BaseModel): + symbol: str + window: int = Field(252, ge=10) + lam: float | None = Field(None, gt=0, lt=1, description="if given: exponentially weighted instead of rolling") + freq: str = Field("daily", pattern="^(daily|weekly|monthly)$") + currency: str = Field("base", pattern="^(base|local)$") + start: str | None = None + end: str | None = None + + +class PortfolioPerformanceRequest(BaseModel): + holdings: dict[str, float] + freq: str = Field("daily", pattern="^(daily|weekly|monthly)$") + currency: str = Field("base", pattern="^(base|local)$") + start: str | None = None + end: str | None = None + rebalanced: bool = True + + +class ViewSpec(BaseModel): + """Either an explicit view (``assets`` + ``q``) or a sentiment view (``symbol`` + ``sentiment``).""" + + assets: dict[str, float] | None = None + q: float | None = Field(None, description="annual expected return of the view portfolio") + symbol: str | None = None + sentiment: float | None = Field(None, ge=-1, le=1) + strength: float = Field(0.25, ge=0) + confidence: float = Field(0.5, gt=0, le=1) + label: str = "" + source: str = "" + + def as_dict(self) -> dict: + d = self.model_dump(exclude_none=True) + return d + + +class ExpectedReturnsRequest(BaseModel): + symbols: list[str] | None = None + max_assets: int = 100 + views: list[ViewSpec] = Field(default_factory=list) + tau: float = Field(0.05, gt=0) + years: int = Field(10, ge=1) + omega_method: str = Field("idzorek", pattern="^(idzorek|he_litterman)$") + + +class ModelCovarianceRequest(BaseModel): + """Covariance as the *models* see it (the estimator of the scenario generator / optimiser).""" + + symbols: list[str] = Field(..., min_length=1) + freq: str = Field("daily", pattern="^(daily|weekly)$") + covariance: dict = Field(default_factory=lambda: {"method": "ewm", "lam": 0.99, "shrinkage": "auto"}) + horizon: str | int = "week" + start: str | None = None + end: str | None = None + + +class ScenarioRequest(BaseModel): + symbols: list[str] = Field(..., min_length=1) + model: str = Field("empirical", pattern="^(gbm|empirical)$") + n_scenarios: int = Field(4096, ge=100, le=200_000, description="rounded up to a power of two for Sobol' sampling") + horizon: str | int = "week" + freq: str = Field("daily", pattern="^(daily|weekly)$") + covariance: dict = Field(default_factory=lambda: {"method": "ewm", "lam": 0.99, "shrinkage": "auto"}) + seed: int = 42 + sampling: str = Field("sobol", pattern="^(sobol|mc|antithetic|lhs|balanced)$") + moment_match: bool = Field(True, description="exact scenario mean and covariance") + drift_convention: str = Field("arithmetic", pattern="^(arithmetic|log)$") + expected_returns: str = Field("zero", pattern="^(zero|historical|black_litterman)$") + innovation_history: int | None = 2500 + importance_sampling: str = Field("auto", pattern="^(auto|market|adaptive|off)$") + views: list["ViewSpec"] = Field(default_factory=list) + bl_tau: float = Field(0.05, gt=0) + bl_years: int = Field(10, ge=1) + bl_omega_method: str = Field("idzorek", pattern="^(idzorek|he_litterman)$") + # the portfolio whose P&L distribution is reported (shares, or weights of `value`) + holdings: dict[str, float] = Field(default_factory=dict) + weights: dict[str, float] = Field(default_factory=dict) + value: float = Field(1_000_000.0, gt=0, description="portfolio value when `weights` are given") + cash: float = Field(0.0, ge=0) + confidence: list[float] = Field(default=[0.95, 0.99]) + bins: int = Field(60, ge=5, le=400) + focus: list[str] = Field(default_factory=list, description="assets to return a histogram for") + + +class ConstraintsSpec(BaseModel): + max_weight: float = Field(0.10, gt=0, le=1) + max_sector_weight: float | dict[str, float] = 0.25 + max_positions: int | None = Field(None, ge=1) + min_position_value: float = Field(0.0, ge=0) + max_turnover: float | None = Field(None, ge=0) + max_transaction_cost: float | None = Field(None, ge=0) + min_invested: float = Field(0.0, ge=0, le=1) + max_invested: float = Field(1.0, gt=0, le=1) + + +class CostsSpec(BaseModel): + proportional_buy: float = Field(0.001, ge=0) + proportional_sell: float = Field(0.001, ge=0) + fixed_per_trade: float = Field(0.0, ge=0) + + +class OptimizeRequest(BaseModel): + mode: str = Field("new", pattern="^(new|rebalance)$") + cash: float = Field(1_000_000.0, ge=0) + holdings: dict[str, int] = Field(default_factory=dict) + symbols: list[str] | None = None + max_assets: int = Field(100, ge=2) + horizon: str | int = "week" + freq: str = Field("daily", pattern="^(daily|weekly)$") + confidence: float = Field(0.95, gt=0.5, lt=1) + cvar_limit: float | None = Field(None, gt=0, description="max expected shortfall in CHF over the horizon") + cvar_limit_fraction: float | None = Field(None, gt=0, lt=1) + objective: str = Field("max_return", pattern="^(max_return|min_cvar|utility)$") + risk_aversion: float = Field(1.0, ge=0) + target_return: float | None = None + scenario_model: str = Field("empirical", pattern="^(gbm|empirical|gaussian_exact)$", + description="'gaussian_exact': no scenarios, outer approximation of the normal model") + n_scenarios: int | None = Field(None, ge=100, le=200_000, + description="default: 16 per candidate, within [4096, 16384]; rounded up to a " + "power of two for Sobol' sampling. About 64 per candidate is " + "needed to pin the composition; the result warns below 8") + seed: int = 42 + sampling: str = Field("sobol", pattern="^(sobol|mc|antithetic|lhs|balanced)$") + moment_match: bool = Field(True, description="exact scenario mean and covariance") + importance_sampling: str = Field("auto", pattern="^(auto|market|adaptive|off)$", + description="'market': tilt towards the tail of the cap-weighted candidate " + "portfolio (independent of the risk parameters, so a family of runs " + "shares one scenario set); 'adaptive': tilt towards a first-pass " + "solution; 'auto': 'market' when the tail would be thin") + is_mixture: float = Field(0.25, gt=0, lt=0.5, description="share of scenarios per tilted block") + drift_convention: str = Field("arithmetic", pattern="^(arithmetic|log)$") + risk_drift: str = Field("model", pattern="^(model|zero)$", + description="'model': the CVaR is measured on the scenarios including the expected " + "return; 'zero': on the centred scenarios, so a high return forecast " + "cannot relax the risk limit (the usual market-risk convention)") + row_generation: str = Field("auto", pattern="^(auto|on|off)$", + description="solve on the tail rows only (large scenario sets)") + covariance: dict = Field(default_factory=lambda: {"method": "ewm", "lam": 0.99, "shrinkage": "auto"}) + expected_returns: str = Field("black_litterman", pattern="^(black_litterman|historical|zero)$") + views: list[ViewSpec] = Field(default_factory=list) + bl_tau: float = Field(0.05, gt=0) + bl_years: int = Field(10, ge=1) + bl_omega_method: str = Field("idzorek", pattern="^(idzorek|he_litterman)$") + constraints: ConstraintsSpec = Field(default_factory=ConstraintsSpec) + costs: CostsSpec = Field(default_factory=CostsSpec) + solver: str = Field("auto", pattern="^(auto|cuopt|highs)$") + time_limit: float | None = Field(None, gt=0) + innovation_history: int | None = 2500 + innovation_standardize: str | None = Field("diagonal", pattern="^(diagonal|full)$") + + +class JobStatus(BaseModel): + id: str + kind: str + status: str # queued | running | done | failed + started_at: str | None = None + finished_at: str | None = None + result: Any | None = None + error: str | None = None + + +class SnapshotRequest(BaseModel): + name: str | None = None diff --git a/quantcore/api/ui/index.html b/quantcore/api/ui/index.html new file mode 100644 index 0000000..b0580b2 --- /dev/null +++ b/quantcore/api/ui/index.html @@ -0,0 +1,27 @@ + + + + + +quantcore test bench + + + + + +

+
+
quantcoretest bench
+
checking …
+ +
+ +
+ +
+
+
+
+ + + diff --git a/quantcore/api/ui/js/charts.js b/quantcore/api/ui/js/charts.js new file mode 100644 index 0000000..232fd9d --- /dev/null +++ b/quantcore/api/ui/js/charts.js @@ -0,0 +1,508 @@ +/* Small SVG chart library: line, bar, histogram, heatmap, scatter. + No dependencies (the container may have no internet). Colours come from the CSS custom + properties in styles.css, so the theme switch is a re-render, never a second palette. + Every chart keeps its config on the node and re-draws on resize and on a theme change. */ + +const NS = "http://www.w3.org/2000/svg"; +const PLOT = { left: 58, right: 16, top: 10, bottom: 26 }; + +export function svgEl(name, attrs = {}) { + const e = document.createElementNS(NS, name); + for (const [k, v] of Object.entries(attrs)) if (v !== null && v !== undefined) e.setAttribute(k, String(v)); + return e; +} + +export function cssVar(name) { + return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || "#888"; +} + +export function seriesColor(i) { return cssVar(`--series-${(i % 8) + 1}`); } + +/* ------------------------------------------------------------------ scales and ticks */ +const lin = (d0, d1, r0, r1) => (v) => (d1 === d0 ? (r0 + r1) / 2 : r0 + ((v - d0) / (d1 - d0)) * (r1 - r0)); + +export function niceTicks(lo, hi, count = 5) { + if (!isFinite(lo) || !isFinite(hi)) return [0]; + if (lo === hi) return [lo]; + const span = (hi - lo) / count; + const mag = Math.pow(10, Math.floor(Math.log10(Math.abs(span)))); + const step = [1, 2, 2.5, 5, 10].map((m) => m * mag).find((s) => s >= span) || 10 * mag; + const out = []; + for (let t = Math.ceil(lo / step) * step; t <= hi + step * 1e-6; t += step) out.push(Math.abs(t) < step * 1e-9 ? 0 : t); + return out; +} + +function padDomain([lo, hi], pad = 0.06, includeZero = false) { + if (includeZero) { lo = Math.min(lo, 0); hi = Math.max(hi, 0); } + if (lo === hi) { const d = Math.abs(lo) || 1; return [lo - d * 0.1, hi + d * 0.1]; } + const p = (hi - lo) * pad; + return [lo - p, hi + p]; +} + +const extent = (vals) => vals.reduce(([a, b], v) => [Math.min(a, v), Math.max(b, v)], [Infinity, -Infinity]); + +/* ------------------------------------------------------------------ tooltip (singleton) */ +function tipNode() { + let t = document.getElementById("tooltip"); + if (!t) { t = document.createElement("div"); t.id = "tooltip"; document.body.appendChild(t); } + return t; +} + +export function showTip(ev, title, rows) { + const t = tipNode(); + t.replaceChildren(); + if (title) { const h = document.createElement("div"); h.className = "tt-title"; h.textContent = title; t.appendChild(h); } + for (const r of rows) { + const row = document.createElement("div"); row.className = "tt-row"; + if (r.color) { const k = document.createElement("span"); k.className = "key"; k.style.background = r.color; row.appendChild(k); } + const nm = document.createElement("span"); nm.className = "nm"; nm.textContent = r.name ?? ""; row.appendChild(nm); + const v = document.createElement("span"); v.className = "v"; v.textContent = r.value ?? ""; row.appendChild(v); + t.appendChild(row); + } + t.style.opacity = "1"; + const pad = 14, w = t.offsetWidth, h = t.offsetHeight; + let x = ev.clientX + pad, y = ev.clientY + pad; + if (x + w > window.innerWidth - 8) x = ev.clientX - w - pad; + if (y + h > window.innerHeight - 8) y = ev.clientY - h - pad; + t.style.left = `${Math.max(4, x)}px`; t.style.top = `${Math.max(4, y)}px`; +} + +export function hideTip() { const t = document.getElementById("tooltip"); if (t) t.style.opacity = "0"; } + +/* ------------------------------------------------------------------ chart frame */ +function frame(node, cfg) { + const height = cfg.height || 220; + const margin = { ...PLOT, ...(cfg.margin || {}) }; + const width = Math.max(260, node.clientWidth || node.parentElement?.clientWidth || 640); + const w = width - margin.left - margin.right; + const h = height - margin.top - margin.bottom; + const svg = svgEl("svg", { viewBox: `0 0 ${width} ${height}`, width, height, role: "img" }); + const g = svgEl("g", { transform: `translate(${margin.left},${margin.top})` }); + svg.appendChild(g); + return { svg, g, w, h, width, height, margin, grid: cssVar("--border"), ink: cssVar("--text-secondary"), + muted: cssVar("--text-muted"), surface: cssVar("--surface-1") }; +} + +function yAxis(f, y, ticks, format) { + for (const t of ticks) { + const yy = y(t); + f.g.appendChild(svgEl("line", { x1: 0, x2: f.w, y1: yy, y2: yy, stroke: f.grid, "stroke-width": 1 })); + const lab = svgEl("text", { x: -8, y: yy + 3.5, "text-anchor": "end", fill: f.muted, "font-size": 10.5 }); + lab.textContent = format(t); + f.g.appendChild(lab); + } +} + +function xLabels(f, entries) { // entries: [{x, text}] + for (const e of entries) { + const t = svgEl("text", { x: e.x, y: f.h + 15, "text-anchor": "middle", fill: f.muted, "font-size": 10.5 }); + t.textContent = e.text; + f.g.appendChild(t); + } +} + +function legendFor(node, items) { + const box = document.createElement("div"); + box.className = "legend"; + for (const it of items) { + const s = document.createElement("span"); s.className = "item"; + const k = document.createElement("span"); k.className = `key${it.rect ? " rect" : ""}`; k.style.background = it.color; + const n = document.createElement("span"); n.textContent = it.name; + s.append(k, n); box.appendChild(s); + } + node.appendChild(box); +} + +/** Render `fn` into `node` now, and again on resize / theme change. */ +function mount(node, fn) { + node.__render = fn; + fn(); + if (!node.__observed) { + node.__observed = true; + const ro = new ResizeObserver(() => { if (node.isConnected) requestAnimationFrame(() => node.__render?.()); }); + ro.observe(node); + } +} + +export function redrawAll(root = document) { + root.querySelectorAll(".chart").forEach((n) => n.__render?.()); +} + +/* ------------------------------------------------------------------ line chart */ +/** series: [{name, points: [[x, y], ...], color?}] ; xType 'time' | 'linear' */ +export function lineChart(node, cfg) { + mount(node, () => { + node.replaceChildren(); + const series = (cfg.series || []).filter((s) => s.points?.length); + if (!series.length) { node.appendChild(emptyBox("no data")); return; } + const f = frame(node, cfg); + const xs = series.flatMap((s) => s.points.map((p) => p[0])); + const ys = series.flatMap((s) => s.points.map((p) => p[1])).filter(Number.isFinite); + const [x0, x1] = extent(xs); + const [y0, y1] = padDomain(extent(ys), 0.08, cfg.includeZero); + const x = lin(x0, x1, 0, f.w), y = lin(y0, y1, f.h, 0); + const yFmt = cfg.yFormat || ((v) => v.toFixed(2)); + yAxis(f, y, niceTicks(y0, y1, cfg.yTicks || 5), yFmt); + if (cfg.zeroLine && y0 < 0 && y1 > 0) { + f.g.appendChild(svgEl("line", { x1: 0, x2: f.w, y1: y(0), y2: y(0), stroke: cssVar("--border-strong"), "stroke-width": 1 })); + } + // x labels: ~6 evenly spaced actual data positions + const xFmt = cfg.xFormat || ((v) => String(Math.round(v))); + const uniq = [...new Set(xs)].sort((a, b) => a - b); + const n = Math.min(6, uniq.length); + const picks = Array.from({ length: n }, (_, i) => uniq[Math.round((i * (uniq.length - 1)) / Math.max(n - 1, 1))]); + xLabels(f, picks.map((v) => ({ x: x(v), text: xFmt(v) }))); + + series.forEach((s, i) => { + const color = s.color || seriesColor(i); + const d = s.points.filter((p) => Number.isFinite(p[1])) + .map((p, j) => `${j ? "L" : "M"}${x(p[0]).toFixed(2)},${y(p[1]).toFixed(2)}`).join(" "); + if (cfg.area) { + const pts = s.points.filter((p) => Number.isFinite(p[1])); + const base = y(Math.max(y0, Math.min(0, y1))); + const area = `${d} L${x(pts[pts.length - 1][0]).toFixed(2)},${base} L${x(pts[0][0]).toFixed(2)},${base} Z`; + f.g.appendChild(svgEl("path", { d: area, fill: color, "fill-opacity": 0.1, stroke: "none" })); + } + f.g.appendChild(svgEl("path", { d, fill: "none", stroke: color, "stroke-width": 2, + "stroke-linejoin": "round", "stroke-linecap": "round" })); + }); + + // crosshair + one tooltip listing every series + const hair = svgEl("line", { y1: 0, y2: f.h, stroke: cssVar("--border-strong"), "stroke-width": 1, opacity: 0 }); + f.g.appendChild(hair); + const dots = series.map((s, i) => { + const c = svgEl("circle", { r: 4, fill: s.color || seriesColor(i), stroke: f.surface, "stroke-width": 2, opacity: 0 }); + f.g.appendChild(c); return c; + }); + const overlay = svgEl("rect", { x: 0, y: 0, width: f.w, height: f.h, fill: "transparent" }); + f.g.appendChild(overlay); + const onMove = (ev) => { + const box = f.svg.getBoundingClientRect(); + const px = ((ev.clientX - box.left) / box.width) * f.width - f.margin.left; + const target = x0 + (px / f.w) * (x1 - x0); + let best = uniq[0]; + for (const v of uniq) if (Math.abs(v - target) < Math.abs(best - target)) best = v; + hair.setAttribute("x1", x(best)); hair.setAttribute("x2", x(best)); hair.setAttribute("opacity", 1); + const rows = []; + series.forEach((s, i) => { + const p = s.points.find((q) => q[0] === best); + dots[i].setAttribute("opacity", p && Number.isFinite(p[1]) ? 1 : 0); + if (p && Number.isFinite(p[1])) { + dots[i].setAttribute("cx", x(p[0])); dots[i].setAttribute("cy", y(p[1])); + rows.push({ color: s.color || seriesColor(i), name: s.name, value: yFmt(p[1]) }); + } + }); + showTip(ev, xFmt(best), rows); + }; + overlay.addEventListener("pointermove", onMove); + overlay.addEventListener("pointerleave", () => { + hair.setAttribute("opacity", 0); dots.forEach((d) => d.setAttribute("opacity", 0)); hideTip(); + }); + node.appendChild(f.svg); + if (series.length > 1) legendFor(node, series.map((s, i) => ({ name: s.name, color: s.color || seriesColor(i) }))); + }); +} + +/* ------------------------------------------------------------------ bar chart */ +/** items: [{label, values: [v, ...], meta?}] ; groups: [name, ...] (one entry = single series) */ +export function barChart(node, cfg) { + mount(node, () => { + node.replaceChildren(); + const items = cfg.items || []; + if (!items.length) { node.appendChild(emptyBox(cfg.emptyText || "nothing to show")); return; } + const groups = cfg.groups || [""]; + const horizontal = cfg.horizontal !== false; + const fmt = cfg.valueFormat || ((v) => v.toFixed(2)); + const all = items.flatMap((it) => it.values).filter(Number.isFinite); + const refs = (cfg.references || []).map((r) => r.value); + const dom = padDomain(extent([...all, ...refs]), 0.04, true); + const colorOf = (gi, v) => cfg.diverging ? (v < 0 ? cssVar("--div-neg") : cssVar("--div-pos")) + : (cfg.colors?.[gi] || seriesColor(gi)); + + if (horizontal) { + const rowH = Math.min(cfg.rowHeight || 26, 34); + const height = items.length * rowH + 34; + const f = frame(node, { ...cfg, height, margin: { ...PLOT, left: cfg.labelWidth || 92, bottom: 22, top: 6 } }); + const x = lin(dom[0], dom[1], 0, f.w); + const band = f.h / items.length; + const thick = Math.min(24, Math.max(4, (band - 8) / groups.length - (groups.length > 1 ? 2 : 0))); + for (const t of niceTicks(dom[0], dom[1], 5)) { + f.g.appendChild(svgEl("line", { x1: x(t), x2: x(t), y1: 0, y2: f.h, stroke: f.grid, "stroke-width": 1 })); + const lab = svgEl("text", { x: x(t), y: f.h + 14, "text-anchor": "middle", fill: f.muted, "font-size": 10.5 }); + lab.textContent = fmt(t); f.g.appendChild(lab); + } + items.forEach((it, i) => { + const y0 = i * band + (band - thick * groups.length - (groups.length - 1) * 2) / 2; + const lab = svgEl("text", { x: -8, y: i * band + band / 2 + 3.5, "text-anchor": "end", + fill: cssVar("--text-secondary"), "font-size": 11 }); + lab.textContent = it.label; f.g.appendChild(lab); + it.values.forEach((v, gi) => { + if (!Number.isFinite(v)) return; + const yy = y0 + gi * (thick + 2); + const zero = x(Math.max(dom[0], Math.min(0, dom[1]))); + const xv = x(v); + const r = svgEl("path", { d: roundedBar(Math.min(zero, xv), yy, Math.abs(xv - zero), thick, v >= 0 ? "right" : "left"), + fill: colorOf(gi, v) }); + r.style.cursor = "default"; + r.addEventListener("pointermove", (ev) => showTip(ev, it.label, + groups.map((g, k) => ({ color: colorOf(k, it.values[k]), name: g || (cfg.valueName || "value"), + value: Number.isFinite(it.values[k]) ? fmt(it.values[k]) : "-" })))); + r.addEventListener("pointerleave", hideTip); + f.g.appendChild(r); + if (cfg.valueLabels !== false && items.length <= 24 && groups.length === 1) { + const text = fmt(v); + const outside = v >= 0 ? xv + 5 : xv - 5; + const t = svgEl("text", { x: outside, y: yy + thick / 2 + 3.5, "text-anchor": v >= 0 ? "start" : "end", + fill: f.muted, "font-size": 10 }); + t.textContent = text; + if (outside + (v >= 0 ? text.length * 5.6 : -text.length * 5.6) <= f.w + 4 && + outside + (v >= 0 ? 0 : -text.length * 5.6) >= -4) f.g.appendChild(t); + } + }); + }); + for (const r of cfg.references || []) drawRef(f, x(r.value), r, true); + node.appendChild(f.svg); + } else { + const f = frame(node, { ...cfg, height: cfg.height || 220, margin: { ...PLOT, bottom: cfg.bottom || 46 } }); + const y = lin(dom[0], dom[1], f.h, 0); + const band = f.w / items.length; + const thick = Math.min(24, Math.max(3, (band - 6) / groups.length - (groups.length > 1 ? 2 : 0))); + yAxis(f, y, niceTicks(dom[0], dom[1], 5), fmt); + items.forEach((it, i) => { + const x0 = i * band + (band - thick * groups.length - (groups.length - 1) * 2) / 2; + it.values.forEach((v, gi) => { + if (!Number.isFinite(v)) return; + const zero = y(Math.max(dom[0], Math.min(0, dom[1]))); + const yv = y(v); + const r = svgEl("path", { d: roundedBar(x0 + gi * (thick + 2), Math.min(zero, yv), thick, Math.abs(yv - zero), + v >= 0 ? "up" : "down", true), fill: colorOf(gi, v) }); + r.addEventListener("pointermove", (ev) => showTip(ev, it.label, + groups.map((g, k) => ({ color: colorOf(k, it.values[k]), name: g || (cfg.valueName || "value"), + value: Number.isFinite(it.values[k]) ? fmt(it.values[k]) : "-" })))); + r.addEventListener("pointerleave", hideTip); + f.g.appendChild(r); + }); + if (items.length <= 30) { + const t = svgEl("text", { x: i * band + band / 2, y: f.h + 14, "text-anchor": "end", fill: f.muted, + "font-size": 10, transform: `rotate(-35 ${i * band + band / 2} ${f.h + 14})` }); + t.textContent = it.label; f.g.appendChild(t); + } + }); + node.appendChild(f.svg); + } + if (groups.length > 1) legendFor(node, groups.map((g, i) => ({ name: g, color: cfg.colors?.[i] || seriesColor(i), rect: true }))); + }); +} + +function roundedBar(x, y, w, h, dir, vertical = false) { + const r = Math.min(4, vertical ? w / 2 : h / 2, vertical ? h : w); + if (w <= 0.5 || h <= 0.5) return `M${x},${y}h${Math.max(w, 0.5)}v${Math.max(h, 0.5)}h${-Math.max(w, 0.5)}Z`; + if (!vertical) { + return dir === "right" + ? `M${x},${y} h${w - r} a${r},${r} 0 0 1 ${r},${r} v${h - 2 * r} a${r},${r} 0 0 1 ${-r},${r} h${-(w - r)} Z` + : `M${x + r},${y} h${w - r} v${h} h${-(w - r)} a${r},${r} 0 0 1 ${-r},${-r} v${-(h - 2 * r)} a${r},${r} 0 0 1 ${r},${-r} Z`; + } + return dir === "up" + ? `M${x},${y + h} v${-(h - r)} a${r},${r} 0 0 1 ${r},${-r} h${w - 2 * r} a${r},${r} 0 0 1 ${r},${r} v${h - r} Z` + : `M${x},${y} h${w} v${h - r} a${r},${r} 0 0 1 ${-r},${r} h${-(w - 2 * r)} a${r},${r} 0 0 1 ${-r},${-r} Z`; +} + +function drawRef(f, pos, ref, vertical) { + const line = vertical + ? svgEl("line", { x1: pos, x2: pos, y1: 0, y2: f.h, stroke: ref.color || cssVar("--text-muted"), "stroke-width": 1 }) + : svgEl("line", { x1: 0, x2: f.w, y1: pos, y2: pos, stroke: ref.color || cssVar("--text-muted"), "stroke-width": 1 }); + f.g.appendChild(line); + if (ref.label) { + const near = pos > f.w - Math.max(56, ref.label.length * 5.4); // keep the label inside the plot + const t = svgEl("text", { x: vertical ? (near ? pos - 4 : pos + 4) : f.w, y: vertical ? f.h - 4 : pos - 4, + "text-anchor": vertical ? (near ? "end" : "start") : "end", + fill: ref.color || cssVar("--text-muted"), "font-size": 10, + // a surface-coloured halo keeps it legible where it crosses a mark + stroke: f.surface, "stroke-width": 3, "paint-order": "stroke fill" }); + t.textContent = ref.label; f.g.appendChild(t); + } +} + +/* ------------------------------------------------------------------ histogram */ +/** cfg: {edges, mass, markers: [{value, label, color}], tailFrom, valueFormat} */ +export function histogram(node, cfg) { + mount(node, () => { + node.replaceChildren(); + const { edges, mass } = cfg; + if (!edges?.length || !mass?.length) { node.appendChild(emptyBox("no distribution")); return; } + const f = frame(node, { ...cfg, height: cfg.height || 240, margin: { ...PLOT, left: 52, bottom: 28 } }); + const x = lin(edges[0], edges[edges.length - 1], 0, f.w); + const ymax = Math.max(...mass); + const y = lin(0, ymax, f.h, 0); + const fmt = cfg.valueFormat || ((v) => v.toFixed(2)); + const pct = (v) => `${(v * 100).toFixed(1)} %`; + for (const t of niceTicks(0, ymax, 4)) { + f.g.appendChild(svgEl("line", { x1: 0, x2: f.w, y1: y(t), y2: y(t), stroke: f.grid, "stroke-width": 1 })); + const lab = svgEl("text", { x: -8, y: y(t) + 3.5, "text-anchor": "end", fill: f.muted, "font-size": 10.5 }); + lab.textContent = pct(t); f.g.appendChild(lab); + } + const main = cssVar("--series-1"), tail = cssVar("--series-2"); + const tailFrom = cfg.tailFrom; + mass.forEach((m, i) => { + const x0 = x(edges[i]), x1 = x(edges[i + 1]); + const gap = Math.min(2, (x1 - x0) * 0.3); + const w = Math.max(x1 - x0 - gap, 0.6); + const isTail = tailFrom !== undefined && edges[i + 1] <= tailFrom; + const h = f.h - y(m); + const bar = svgEl("path", { d: roundedBar(x0 + gap / 2, y(m), w, h, "up", true), fill: isTail ? tail : main }); + bar.addEventListener("pointermove", (ev) => showTip(ev, `${fmt(edges[i])} … ${fmt(edges[i + 1])}`, + [{ color: isTail ? tail : main, name: "probability", value: pct(m) }])); + bar.addEventListener("pointerleave", hideTip); + f.g.appendChild(bar); + }); + for (const t of niceTicks(edges[0], edges[edges.length - 1], 6)) { + const lab = svgEl("text", { x: x(t), y: f.h + 15, "text-anchor": "middle", fill: f.muted, "font-size": 10.5 }); + lab.textContent = fmt(t); f.g.appendChild(lab); + } + (cfg.markers || []).forEach((m, i) => { + const px = x(m.value); + f.g.appendChild(svgEl("line", { x1: px, x2: px, y1: 0, y2: f.h, stroke: m.color || cssVar("--text-primary"), + "stroke-width": 1 })); + const t = svgEl("text", { x: px + 3, y: 10 + i * 12, "text-anchor": px > f.w - 60 ? "end" : "start", + fill: m.color || cssVar("--text-secondary"), "font-size": 10 }); + t.setAttribute("x", px > f.w - 60 ? px - 3 : px + 3); + t.textContent = m.label; f.g.appendChild(t); + }); + node.appendChild(f.svg); + const items = [{ name: cfg.mainLabel || "outcomes", color: main, rect: true }]; + if (tailFrom !== undefined) items.push({ name: cfg.tailLabel || "beyond VaR", color: tail, rect: true }); + legendFor(node, items); + }); +} + +/* ------------------------------------------------------------------ heatmap */ +/** cfg: {labels, matrix, diverging (default true), domain, format} */ +export function heatmap(node, cfg) { + mount(node, () => { + node.replaceChildren(); + const labels = cfg.labels || [], m = cfg.matrix || []; + if (!labels.length) { node.appendChild(emptyBox("no matrix")); return; } + const n = labels.length; + const width = Math.max(260, node.clientWidth || 600); + const pad = Math.min(96, Math.max(44, width * 0.14)); + const cell = Math.max(6, Math.min(34, (width - pad - 66) / n)); + const size = cell * n; + const f = { svg: svgEl("svg", { viewBox: `0 0 ${width} ${size + pad + 30}`, width, height: size + pad + 30 }), + muted: cssVar("--text-muted") }; + const g = svgEl("g", { transform: `translate(${pad},${pad})` }); + f.svg.appendChild(g); + const dom = cfg.domain || (cfg.diverging === false ? [0, Math.max(...m.flat())] : [-1, 1]); + const fmt = cfg.format || ((v) => v.toFixed(2)); + const show = n <= 26; + for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + const v = m[i]?.[j]; + if (!Number.isFinite(v)) continue; + const c = colorFor(v, dom, cfg.diverging !== false); + const r = svgEl("rect", { x: j * cell, y: i * cell, width: Math.max(cell - 1, 1), height: Math.max(cell - 1, 1), + fill: c, rx: Math.min(2, cell / 6) }); + r.addEventListener("pointermove", (ev) => showTip(ev, `${labels[i]} · ${labels[j]}`, + [{ color: c, name: cfg.valueName || "correlation", value: fmt(v) }])); + r.addEventListener("pointerleave", hideTip); + g.appendChild(r); + } + if (show) { + const ty = svgEl("text", { x: -6, y: i * cell + cell / 2 + 3, "text-anchor": "end", fill: f.muted, "font-size": 9.5 }); + ty.textContent = labels[i]; g.appendChild(ty); + const tx = svgEl("text", { x: i * cell + cell / 2, y: -6, "text-anchor": "start", fill: f.muted, "font-size": 9.5, + transform: `rotate(-60 ${i * cell + cell / 2} -6)` }); + tx.textContent = labels[i]; g.appendChild(tx); + } + } + // scale legend + const lw = Math.min(180, size), lx = pad, ly = pad + size + 18; + const grad = svgEl("linearGradient", { id: `hm-${Math.random().toString(36).slice(2)}` }); + for (let k = 0; k <= 10; k++) { + grad.appendChild(svgEl("stop", { offset: `${k * 10}%`, + "stop-color": colorFor(dom[0] + (k / 10) * (dom[1] - dom[0]), dom, cfg.diverging !== false) })); + } + const defs = svgEl("defs"); defs.appendChild(grad); f.svg.appendChild(defs); + f.svg.appendChild(svgEl("rect", { x: lx, y: ly, width: lw, height: 8, rx: 2, fill: `url(#${grad.id})` })); + [dom[0], (dom[0] + dom[1]) / 2, dom[1]].forEach((v, k) => { + const t = svgEl("text", { x: lx + (k * lw) / 2, y: ly + 20, "text-anchor": k === 0 ? "start" : k === 2 ? "end" : "middle", + fill: f.muted, "font-size": 10 }); + t.textContent = fmt(v); f.svg.appendChild(t); + }); + node.appendChild(f.svg); + }); +} + +function hex2rgb(h) { + const s = h.replace("#", "").trim(); + const v = s.length === 3 ? s.split("").map((c) => c + c).join("") : s; + return [parseInt(v.slice(0, 2), 16), parseInt(v.slice(2, 4), 16), parseInt(v.slice(4, 6), 16)]; +} +const mix = (a, b, t) => `rgb(${a.map((v, i) => Math.round(v + (b[i] - v) * t)).join(",")})`; + +export function colorFor(v, [lo, hi], diverging = true) { + if (diverging) { + const neg = hex2rgb(cssVar("--div-neg")), mid = hex2rgb(cssVar("--div-mid")), pos = hex2rgb(cssVar("--div-pos")); + const t = Math.max(-1, Math.min(1, v / Math.max(Math.abs(lo), Math.abs(hi) || 1))); + return t < 0 ? mix(mid, neg, -t) : mix(mid, pos, t); + } + const t = Math.max(0, Math.min(1, (v - lo) / ((hi - lo) || 1))); + const stops = ["--seq-100", "--seq-250", "--seq-400", "--seq-550", "--seq-700"].map((s) => hex2rgb(cssVar(s))); + const k = Math.min(stops.length - 2, Math.floor(t * (stops.length - 1))); + return mix(stops[k], stops[k + 1], t * (stops.length - 1) - k); +} + +/* ------------------------------------------------------------------ scatter */ +/** cfg: {points: [{x, y, label, group}], groups, xFormat, yFormat, xLabel, yLabel} */ +export function scatterChart(node, cfg) { + mount(node, () => { + node.replaceChildren(); + const pts = (cfg.points || []).filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y)); + if (!pts.length) { node.appendChild(emptyBox("no points")); return; } + const f = frame(node, { ...cfg, height: cfg.height || 260, margin: { ...PLOT, bottom: 40, left: 62 } }); + const [x0, x1] = padDomain(extent(pts.map((p) => p.x)), 0.08); + const [y0, y1] = padDomain(extent(pts.map((p) => p.y)), 0.08); + const x = lin(x0, x1, 0, f.w), y = lin(y0, y1, f.h, 0); + const xFmt = cfg.xFormat || ((v) => v.toFixed(2)), yFmt = cfg.yFormat || ((v) => v.toFixed(2)); + yAxis(f, y, niceTicks(y0, y1, 5), yFmt); + for (const t of niceTicks(x0, x1, 5)) { + f.g.appendChild(svgEl("line", { x1: x(t), x2: x(t), y1: 0, y2: f.h, stroke: f.grid, "stroke-width": 1 })); + const lab = svgEl("text", { x: x(t), y: f.h + 15, "text-anchor": "middle", fill: f.muted, "font-size": 10.5 }); + lab.textContent = xFmt(t); f.g.appendChild(lab); + } + if (cfg.xLabel) { + const t = svgEl("text", { x: f.w / 2, y: f.h + 33, "text-anchor": "middle", fill: f.muted, "font-size": 10.5 }); + t.textContent = cfg.xLabel; f.g.appendChild(t); + } + if (cfg.yLabel) { + const t = svgEl("text", { x: -f.margin.left + 12, y: f.h / 2, "text-anchor": "middle", fill: f.muted, "font-size": 10.5, + transform: `rotate(-90 ${-f.margin.left + 12} ${f.h / 2})` }); + t.textContent = cfg.yLabel; f.g.appendChild(t); + } + const groups = cfg.groups || [...new Set(pts.map((p) => p.group ?? ""))]; + pts.forEach((p) => { + const gi = Math.max(0, groups.indexOf(p.group ?? "")); + const c = p.color || seriesColor(gi); + const dot = svgEl("circle", { cx: x(p.x), cy: y(p.y), r: 5, fill: c, stroke: f.surface, "stroke-width": 2 }); + const hit = svgEl("circle", { cx: x(p.x), cy: y(p.y), r: 12, fill: "transparent" }); + hit.addEventListener("pointermove", (ev) => showTip(ev, p.label, [ + { color: c, name: cfg.xLabel || "x", value: xFmt(p.x) }, { color: c, name: cfg.yLabel || "y", value: yFmt(p.y) }, + ])); + hit.addEventListener("pointerleave", hideTip); + f.g.append(dot, hit); + }); + if (cfg.labelPoints !== false && pts.length <= 14) { + pts.forEach((p) => { + const t = svgEl("text", { x: x(p.x) + 8, y: y(p.y) - 7, fill: f.muted, "font-size": 9.5 }); + t.textContent = p.label; f.g.appendChild(t); + }); + } + node.appendChild(f.svg); + if (groups.length > 1) legendFor(node, groups.map((g, i) => ({ name: g, color: seriesColor(i) }))); + }); +} + +function emptyBox(text) { + const d = document.createElement("div"); + d.className = "empty"; d.textContent = text; + return d; +} diff --git a/quantcore/api/ui/js/core.js b/quantcore/api/ui/js/core.js new file mode 100644 index 0000000..9414aec --- /dev/null +++ b/quantcore/api/ui/js/core.js @@ -0,0 +1,393 @@ +/* Shared state, API client, formatting and DOM helpers for the test bench. */ + +/* ------------------------------------------------------------------ API */ +const BASE = location.pathname.replace(/\/ui\/?.*$/, ""); + +async function request(path, { method = "GET", body, signal } = {}) { + const res = await fetch(BASE + path, { + method, + headers: body ? { "Content-Type": "application/json" } : undefined, + body: body ? JSON.stringify(body) : undefined, + signal, + }); + const text = await res.text(); + let data = null; + try { data = text ? JSON.parse(text) : null; } catch { data = text; } + if (!res.ok) { + const detail = data && typeof data === "object" ? data.detail ?? JSON.stringify(data) : String(data || res.statusText); + throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail)); + } + return data; +} + +export const api = { + get: (path, params) => request(path + (params ? "?" + new URLSearchParams( + Object.entries(params).filter(([, v]) => v !== null && v !== undefined && v !== "")).toString() : "")), + post: (path, body, opts = {}) => request(path, { method: "POST", body, ...opts }), +}; + +/* ------------------------------------------------------------------ state */ +const KEY = "quantcore-bench-v2"; + +export const DEFAULT_STATE = { + ui: { tab: "stocks", theme: "auto", migrations: [] }, + universe: { region: "", sector: "", country: "", search: "", top: 30 }, + selection: [], + portfolio: { mode: "new", cash: 1000000, holdings: {} }, + model: { + freq: "daily", horizon: "week", scenario_model: "gbm", n_scenarios: 4096, seed: 42, + sampling: "sobol", moment_match: true, importance_sampling: "auto", drift_convention: "arithmetic", + innovation_history: 2500, + covariance: { method: "ewm", lam: 0.99, window: 200, shrinkage: "auto" }, + expected_returns: "black_litterman", bl_tau: 0.05, bl_years: 10, bl_omega_method: "idzorek", + }, + views: [], + constraints: { + max_weight: 0.1, max_sector_weight: 0.25, sector_overrides: {}, max_positions: null, + min_position_value: 0, max_turnover: null, max_transaction_cost: null, min_invested: 0, max_invested: 1, + }, + costs: { proportional_buy: 0.001, proportional_sell: 0.001, fixed_per_trade: 0 }, + opt: { + objective: "max_return", confidence: 0.95, cvar_mode: "fraction", cvar_limit_fraction: 0.05, + cvar_limit: null, risk_aversion: 1, target_return: null, solver: "auto", time_limit: 300, + row_generation: "auto", auto_scenarios: true, risk_drift: "model", + }, + data: { start: "", end: "", freq: "daily" }, +}; + +function deepMerge(base, add) { + if (Array.isArray(base) || add === null || typeof add !== "object" || typeof base !== "object") return add ?? base; + const out = { ...base }; + for (const [k, v] of Object.entries(add)) out[k] = k in base ? deepMerge(base[k], v) : v; + return out; +} + +function load() { + try { + const raw = localStorage.getItem(KEY); + return raw ? migrate(deepMerge(structuredClone(DEFAULT_STATE), JSON.parse(raw))) : structuredClone(DEFAULT_STATE); + } catch { return structuredClone(DEFAULT_STATE); } +} + +/** Fixes for settings stored by an earlier version (each applied once). */ +function migrate(s) { + s.ui.migrations = s.ui.migrations || []; + if (!s.ui.migrations.includes("time-limit")) { + // a stored `null` meant "no limit", and without one HiGHS can run for hours on a large problem + if (s.opt.time_limit === null) s.opt.time_limit = DEFAULT_STATE.opt.time_limit; + s.ui.migrations.push("time-limit"); + } + return s; +} + +export const state = load(); + +const listeners = new Set(); +export function onChange(fn) { listeners.add(fn); return () => listeners.delete(fn); } + +/** Persist and notify. `topic` says what changed ('selection', 'model', ...); `source` names the + tab that changed it, so that tab can update itself without being re-rendered from outside. */ +export function changed(topic = "state", source = null) { + try { localStorage.setItem(KEY, JSON.stringify(state)); } catch { /* private mode: keep going */ } + for (const fn of listeners) fn(topic, source); +} + +export function resetState() { + try { localStorage.removeItem(KEY); } catch { /* ignore */ } + location.reload(); +} + +/* ------------------------------------------------------------------ requests built from the state */ +export function covarianceSpec() { + const c = state.model.covariance; + return { method: c.method, lam: Number(c.lam), window: Number(c.window), + shrinkage: c.shrinkage === "auto" ? "auto" : Number(c.shrinkage) }; +} + +export function viewPayload() { + return state.views.map((v) => (v.kind === "sentiment" + ? { symbol: v.symbol, sentiment: Number(v.sentiment), confidence: Number(v.confidence), + strength: Number(v.strength), label: v.label || "", source: v.source || "" } + : { assets: v.assets, q: Number(v.q), confidence: Number(v.confidence), label: v.label || "", source: v.source || "" })); +} + +export function constraintPayload() { + const c = state.constraints; + const overrides = Object.entries(c.sector_overrides || {}).filter(([, v]) => v !== "" && v !== null); + return { + max_weight: Number(c.max_weight), + max_sector_weight: overrides.length + ? { "*": Number(c.max_sector_weight), ...Object.fromEntries(overrides.map(([k, v]) => [k, Number(v)])) } + : Number(c.max_sector_weight), + max_positions: c.max_positions ? Number(c.max_positions) : null, + min_position_value: Number(c.min_position_value) || 0, + max_turnover: c.max_turnover === null || c.max_turnover === "" ? null : Number(c.max_turnover), + max_transaction_cost: c.max_transaction_cost === null || c.max_transaction_cost === "" ? null : Number(c.max_transaction_cost), + min_invested: Number(c.min_invested) || 0, + max_invested: Number(c.max_invested) || 1, + }; +} + +export function scenarioPayload(extra = {}) { + const m = state.model; + return { + symbols: state.selection, model: m.scenario_model === "gaussian_exact" ? "gbm" : m.scenario_model, + n_scenarios: Number(m.n_scenarios), horizon: m.horizon, freq: m.freq, covariance: covarianceSpec(), + seed: Number(m.seed), sampling: m.sampling, moment_match: !!m.moment_match, + drift_convention: m.drift_convention, expected_returns: m.expected_returns, + innovation_history: m.innovation_history ? Number(m.innovation_history) : null, + importance_sampling: m.importance_sampling, views: viewPayload(), + bl_tau: Number(m.bl_tau), bl_years: Number(m.bl_years), + bl_omega_method: m.bl_omega_method || "idzorek", ...extra, + }; +} + +export function optimizePayload(extra = {}) { + const m = state.model, o = state.opt, p = state.portfolio; + const body = { + mode: p.mode, cash: Number(p.cash), holdings: p.mode === "rebalance" ? intHoldings(p.holdings) : {}, + symbols: state.selection.length ? state.selection : null, max_assets: Math.max(state.selection.length || 30, 2), + horizon: m.horizon, freq: m.freq, confidence: Number(o.confidence), + objective: o.objective, risk_aversion: Number(o.risk_aversion), + scenario_model: m.scenario_model, seed: Number(m.seed), sampling: m.sampling, + moment_match: !!m.moment_match, importance_sampling: m.importance_sampling, + drift_convention: m.drift_convention, row_generation: o.row_generation, risk_drift: o.risk_drift, + covariance: covarianceSpec(), expected_returns: m.expected_returns, views: viewPayload(), + bl_tau: Number(m.bl_tau), bl_years: Number(m.bl_years), bl_omega_method: m.bl_omega_method || "idzorek", + constraints: constraintPayload(), costs: { + proportional_buy: Number(state.costs.proportional_buy), proportional_sell: Number(state.costs.proportional_sell), + fixed_per_trade: Number(state.costs.fixed_per_trade), + }, + solver: o.solver, time_limit: o.time_limit ? Number(o.time_limit) : null, + innovation_history: m.innovation_history ? Number(m.innovation_history) : null, + }; + if (!o.auto_scenarios && m.scenario_model !== "gaussian_exact") body.n_scenarios = Number(m.n_scenarios); + if (o.objective === "max_return") { + if (o.cvar_mode === "money" && o.cvar_limit) body.cvar_limit = Number(o.cvar_limit); + else body.cvar_limit_fraction = Number(o.cvar_limit_fraction); + } else if (o.objective === "min_cvar") { + body.target_return = o.target_return === null || o.target_return === "" ? null : Number(o.target_return); + } + return { ...body, ...extra }; // `extra` wins: a sweep overrides exactly the field it varies +} + +export function intHoldings(h) { + return Object.fromEntries(Object.entries(h || {}).map(([k, v]) => [k, Math.round(Number(v) || 0)]).filter(([, v]) => v > 0)); +} + +/* ------------------------------------------------------------------ formatting */ +const nf = (min, max) => new Intl.NumberFormat("de-CH", { minimumFractionDigits: min, maximumFractionDigits: max }); + +export const fmt = { + money: (v, d = 0) => (Number.isFinite(v) ? nf(d, d).format(v) : "–"), + money2: (v) => (Number.isFinite(v) ? nf(2, 2).format(v) : "–"), + signed: (v, d = 0) => (Number.isFinite(v) ? (v > 0 ? "+" : "") + nf(d, d).format(v) : "–"), + compact: (v) => { + if (!Number.isFinite(v)) return "–"; + const a = Math.abs(v); + if (a >= 1e12) return `${nf(2, 2).format(v / 1e12)} T`; + if (a >= 1e9) return `${nf(1, 1).format(v / 1e9)} bn`; + if (a >= 1e6) return `${nf(1, 1).format(v / 1e6)} m`; + if (a >= 1e3) return `${nf(1, 1).format(v / 1e3)} k`; + return nf(0, 1).format(v); + }, + pct: (v, d = 1) => (Number.isFinite(v) ? `${nf(d, d).format(v * 100)} %` : "–"), + pctSigned: (v, d = 1) => (Number.isFinite(v) ? `${v > 0 ? "+" : ""}${nf(d, d).format(v * 100)} %` : "–"), + bp: (v) => (Number.isFinite(v) ? `${nf(0, 1).format(v * 10000)} bp` : "–"), + num: (v, d = 2) => (Number.isFinite(v) ? nf(d, d).format(v) : "–"), + int: (v) => (Number.isFinite(v) ? nf(0, 0).format(Math.round(v)) : "–"), + date: (v) => (typeof v === "string" ? v.slice(0, 10) : new Date(v).toISOString().slice(0, 10)), + secs: (v) => (Number.isFinite(v) ? `${nf(1, 1).format(v)} s` : "–"), +}; + +export const parseDate = (s) => Date.parse(String(s).slice(0, 10) + "T00:00:00Z"); + +/* ------------------------------------------------------------------ DOM helpers */ +export function el(tag, props = {}, ...children) { + const e = document.createElement(tag); + for (const [k, v] of Object.entries(props || {})) { + if (v === null || v === undefined) continue; + if (k === "class") e.className = v; + else if (k === "text") e.textContent = v; + else if (k === "html") e.innerHTML = v; // only for constants in this file + else if (k === "style") Object.assign(e.style, v); + else if (k.startsWith("on") && typeof v === "function") e.addEventListener(k.slice(2), v); + else if (k === "value") e.value = v; + else if (typeof v === "boolean") { if (v) e.setAttribute(k, ""); } + else e.setAttribute(k, String(v)); + } + for (const c of children.flat()) { + if (c === null || c === undefined || c === false) continue; + e.append(c instanceof Node ? c : document.createTextNode(String(c))); + } + return e; +} + +/** `replaceChildren` that drops null/false entries (so `cond ? node : null` is safe). */ +export function setChildren(node, ...kids) { + node.replaceChildren(...kids.flat().filter((k) => k !== null && k !== undefined && k !== false)); +} + +export function card(title, sub, ...content) { + const head = el("header", {}, title ? el("h2", { text: title }) : null, sub ? el("span", { class: "sub", text: sub }) : null); + return el("section", { class: "card" }, title || sub ? head : null, ...content); +} + +export function field(label, control, hint) { + return el("div", { class: "field" }, el("label", { text: label }), control, + hint ? el("span", { class: "hint", text: hint }) : null); +} + +export function input(type, value, onInput, attrs = {}) { + const e = el("input", { type, value: value ?? "", ...attrs }); + e.addEventListener("change", () => onInput(type === "number" ? (e.value === "" ? null : Number(e.value)) : e.value)); + return e; +} + +export function select(options, value, onInput, attrs = {}) { + const e = el("select", attrs); + for (const o of options) { + const [v, label] = Array.isArray(o) ? o : [o, o]; + e.appendChild(el("option", { value: v, text: label, selected: String(v) === String(value) })); + } + e.value = value; + e.addEventListener("change", () => onInput(e.value)); + return e; +} + +export function checkbox(label, value, onInput) { + const box = el("input", { type: "checkbox" }); + box.checked = !!value; + box.addEventListener("change", () => onInput(box.checked)); + return el("label", { class: "check" }, box, el("span", { text: label })); +} + +export function segmented(options, value, onInput) { + const wrap = el("div", { class: "seg" }); + for (const o of options) { + const [v, label] = Array.isArray(o) ? o : [o, o]; + const b = el("button", { type: "button", text: label, "aria-pressed": String(v) === String(value) }); + b.addEventListener("click", () => onInput(v)); + wrap.appendChild(b); + } + return wrap; +} + +export function button(label, onClick, cls = "btn", title = "") { + return el("button", { class: cls, type: "button", text: label, onclick: onClick, title }); +} + +/** Sortable table. columns: [{key, label, num, fmt, cls, title}] */ +export function dataTable(columns, rows, opts = {}) { + const wrap = el("div", { class: "table-wrap" }); + const table = el("table", { class: "data" }); + const thead = el("thead"), tbody = el("tbody"); + let sortKey = opts.sortKey ?? null, sortDir = opts.sortDir ?? -1; + + const headRow = el("tr"); + for (const c of columns) { + const th = el("th", { class: `${c.num ? "num" : ""}${opts.sortable === false ? "" : " sortable"}`, title: c.title || "" }); + th.append(document.createTextNode(c.label)); + if (opts.sortable !== false) { + th.appendChild(el("span", { class: "arrow", text: sortKey === c.key ? (sortDir < 0 ? " ▼" : " ▲") : "" })); + th.addEventListener("click", () => { + if (sortKey === c.key) sortDir = -sortDir; else { sortKey = c.key; sortDir = c.num ? -1 : 1; } + render(); + }); + } + headRow.appendChild(th); + } + thead.appendChild(headRow); + + function render() { + const data = [...rows]; + if (sortKey) { + data.sort((a, b) => { + const x = a[sortKey], y = b[sortKey]; + if (x === y) return 0; + if (x === null || x === undefined) return 1; + if (y === null || y === undefined) return -1; + return (typeof x === "number" && typeof y === "number" ? x - y : String(x).localeCompare(String(y))) * sortDir; + }); + } + tbody.replaceChildren(); + for (const r of data.slice(0, opts.limit || 5000)) { + const tr = el("tr", opts.rowClass ? { class: opts.rowClass(r) || "" } : {}); + if (opts.onRowClick) { tr.style.cursor = "pointer"; tr.addEventListener("click", () => opts.onRowClick(r)); } + for (const c of columns) { + const td = el("td", { class: [c.num ? "num" : "", c.cls ? (typeof c.cls === "function" ? c.cls(r) : c.cls) : ""].join(" ").trim() }); + const v = c.render ? c.render(r) : (c.fmt ? c.fmt(r[c.key], r) : r[c.key]); + if (v instanceof Node) td.appendChild(v); + else td.textContent = v === null || v === undefined ? "–" : String(v); + tr.appendChild(td); + } + tbody.appendChild(tr); + } + headRow.querySelectorAll(".arrow").forEach((a, i) => { + a.textContent = sortKey === columns[i].key ? (sortDir < 0 ? " ▼" : " ▲") : ""; + }); + } + render(); + table.append(thead, tbody); + wrap.appendChild(table); + return wrap; +} + +export function statTiles(items) { + return el("div", { class: "stats" }, ...items.map((s) => el("div", { class: "stat" }, + el("div", { class: "label", text: s.label }), + el("div", { class: "value", text: s.value, title: s.title || "" }), + s.delta ? el("div", { class: `delta ${s.deltaClass || "muted"}`, text: s.delta }) : null))); +} + +export function kv(pairs) { + const dl = el("dl", { class: "kv" }); + for (const [k, v] of pairs) { + if (v === null || v === undefined) continue; + dl.append(el("dt", { text: k }), el("dd", { text: String(v) })); + } + return dl; +} + +/** The panel currently in the document for `id` -- a re-render elsewhere may have replaced the + node a pending request captured, and writing a result into a detached node loses it. */ +export function live(id, fallback) { + return document.getElementById(id) || fallback; +} + +export function chartBox(height) { + return el("div", { class: "chart", style: height ? { minHeight: `${height}px` } : {} }); +} + +export function warnBox(messages) { + if (!messages?.length) return null; + return el("div", {}, ...messages.map((m) => el("div", { class: "warn-box", text: m }))); +} + +export function errorBox(err) { + return el("div", { class: "error-box", text: String(err?.message || err) }); +} + +export function empty(text) { return el("div", { class: "empty", text }); } + +/** Run an async task, showing a spinner in `btn` and errors in `target`. */ +export async function withBusy(btn, target, fn) { + const label = btn?.textContent; + if (btn) { btn.disabled = true; btn.replaceChildren(el("span", { class: "spinner" }), document.createTextNode(" " + label)); } + target?.classList.add("busy"); + try { + return await fn(); + } catch (err) { + target?.replaceChildren(errorBox(err)); + throw err; + } finally { + if (btn) { btn.disabled = false; btn.textContent = label; } + target?.classList.remove("busy"); + } +} + +export const HORIZONS = [["day", "1 day"], ["week", "1 week"], ["month", "1 month"], ["quarter", "1 quarter"], ["year", "1 year"]]; +export const SECTOR_ORDER = [ + "Information Technology", "Financials", "Health Care", "Consumer Discretionary", "Industrials", + "Communication Services", "Consumer Staples", "Energy", "Materials", "Utilities", "Real Estate", "Unknown", +]; diff --git a/quantcore/api/ui/js/main.js b/quantcore/api/ui/js/main.js new file mode 100644 index 0000000..3bcbe45 --- /dev/null +++ b/quantcore/api/ui/js/main.js @@ -0,0 +1,85 @@ +/* Entry point: the tab frame, the theme and the wiring between the sidebar and the tabs. */ + +import { changed, el, onChange, state } from "./core.js"; +import { redrawAll } from "./charts.js"; +import { buildSidebar, refreshHealth } from "./sidebar.js"; +import { universe } from "./universe.js"; +import * as stocks from "./tabs/stocks.js"; +import * as risk from "./tabs/risk.js"; +import * as views from "./tabs/views.js"; +import * as constraints from "./tabs/constraints.js"; +import * as optimize from "./tabs/optimize.js"; + +const TABS = [ + { id: "stocks", label: "Stocks & data", mod: stocks, deps: ["selection", "universe", "data", "model"] }, + { id: "risk", label: "Risk & scenarios", mod: risk, deps: ["selection", "model", "portfolio", "views"] }, + { id: "views", label: "Expected returns", mod: views, deps: ["selection", "model", "views"] }, + { id: "constraints", label: "Constraints", mod: constraints, deps: ["selection", "constraints", "costs", "portfolio"] }, + { id: "optimize", label: "Optimisation", mod: optimize, deps: ["selection", "model", "portfolio", "opt", "constraints", "costs", "views"] }, +]; + +const tabBar = document.getElementById("tabs"); +const main = document.getElementById("main"); + +function activate(id, force = false) { + if (!force && state.ui.tab === id && main.dataset.tab === id) return; + state.ui.tab = id; + changed("ui"); + main.dataset.tab = id; + for (const b of tabBar.children) b.setAttribute("aria-selected", String(b.dataset.tab === id)); + const tab = TABS.find((t) => t.id === id) || TABS[0]; + main.replaceChildren(el("div", { class: "tabpanel" })); + const panel = main.firstChild; + Promise.resolve(tab.mod.render(panel)).catch((err) => { + panel.replaceChildren(el("div", { class: "error-box", text: String(err?.message || err) })); + }); +} + +for (const t of TABS) { + const b = el("button", { type: "button", role: "tab", text: t.label, "data-tab": t.id, "aria-selected": "false" }); + b.addEventListener("click", () => activate(t.id, true)); + tabBar.appendChild(b); +} + +// a tab can send the reader to another tab ("edit these constraints") +document.addEventListener("quantcore:tab", (ev) => { + if (TABS.some((t) => t.id === ev.detail)) activate(ev.detail, true); +}); + +/* ------------------------------------------------------------------ theme */ +const themeBtn = document.getElementById("theme"); +function applyTheme() { + const t = state.ui.theme; + if (t === "auto") document.documentElement.removeAttribute("data-theme"); + else document.documentElement.setAttribute("data-theme", t); + themeBtn.textContent = t === "auto" ? "Theme: system" : t === "dark" ? "Theme: dark" : "Theme: light"; + requestAnimationFrame(() => redrawAll()); +} +themeBtn.addEventListener("click", () => { + state.ui.theme = { auto: "light", light: "dark", dark: "auto" }[state.ui.theme] || "auto"; + changed("ui"); + applyTheme(); +}); +applyTheme(); +matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { if (state.ui.theme === "auto") applyTheme(); }); + +/* ------------------------------------------------------------------ start */ +buildSidebar(document.getElementById("sidebar")); +refreshHealth(document.getElementById("status")); +activate(state.ui.tab || "stocks", true); + +// Every tab reads names, sectors and prices synchronously through `info()`, but only the Stocks tab +// used to fetch the universe -- so a reload that restored any other tab (the tab is part of the +// stored state) drew blank names and a price of 0, and the holdings editor showed 0 CHF positions. +// Paint first, then redraw once the reference data is in. +universe().then(() => activate(state.ui.tab, true)).catch(() => { /* the tab shows its own error */ }); + +let pending = null; +onChange((topic, source) => { + if (topic === "ui") return; + if (source && source === state.ui.tab) return; // the tab already updated itself + const tab = TABS.find((t) => t.id === state.ui.tab); + if (!tab?.deps.includes(topic)) return; + clearTimeout(pending); + pending = setTimeout(() => activate(state.ui.tab, true), 60); +}); diff --git a/quantcore/api/ui/js/runs.js b/quantcore/api/ui/js/runs.js new file mode 100644 index 0000000..750e80a --- /dev/null +++ b/quantcore/api/ui/js/runs.js @@ -0,0 +1,47 @@ +/* Optimisation runs of this session, kept in memory so several tabs can refer to them. + * + * One of them is *selected*: the run the bench is showing and the one every "use this result" action + * works on. A new run selects itself; clicking a row in the history selects that one instead. Runs + * are addressed by `id`, never by their position -- `addRun` unshifts, so an index captured while + * rendering would point at a different run as soon as the next run arrives. + */ + +const runs = []; +let selectedId = null; +let nextId = 1; + +export function addRun(entry) { + const run = { ...entry, id: nextId++, at: new Date() }; + runs.unshift(run); + if (runs.length > 12) runs.pop(); + selectedId = run.id; + return run; +} + +export function allRuns() { return runs; } + +/* There is deliberately no `lastRun()`: "the newest run" and "the run the user is looking at" are + * different things once the history has more than one entry, and reaching for the newest was exactly + * the bug -- the holdings adopted were not the ones on screen. Use `selectedRun()`. */ + +/** The run on screen: what "this result" means in every tab. Falls back to the newest. */ +export function selectedRun() { + return runs.find((r) => r.id === selectedId) || runs[0] || null; +} + +export function selectRun(id) { + if (runs.some((r) => r.id === id)) selectedId = id; + return selectedRun(); +} + +export function isSelected(id) { + const sel = selectedRun(); + return !!sel && sel.id === id; +} + +export function clearRuns() { runs.length = 0; selectedId = null; } + +/** Shares of a result, as a {symbol: shares} map. */ +export function sharesOf(result) { + return Object.fromEntries((result?.positions || []).filter((p) => p.shares > 0).map((p) => [p.symbol, p.shares])); +} diff --git a/quantcore/api/ui/js/sidebar.js b/quantcore/api/ui/js/sidebar.js new file mode 100644 index 0000000..cca1d05 --- /dev/null +++ b/quantcore/api/ui/js/sidebar.js @@ -0,0 +1,179 @@ +/* The sidebar: the state every tab shares -- candidate stocks, the portfolio and the models. */ + +import { + api, button, changed, checkbox, el, field, fmt, input, intHoldings, onChange, resetState, + segmented, select, state, HORIZONS, +} from "./core.js"; +import { info, topSymbols, universe } from "./universe.js"; + +const SCENARIO_MODELS = [ + ["gbm", "GBM (normal)"], + ["empirical", "empirical innovations"], + ["gaussian_exact", "exact Gaussian (no scenarios)"], +]; + +const SHOWN = ["selection", "universe", "portfolio", "model", "state"]; // topics the sidebar displays +let root = null; + +export function buildSidebar(node) { + root = node; + draw(node); + // Only re-draw for changes made elsewhere: rebuilding the panel while someone is editing it + // would replace the control under the pointer (a button clicked right after typing is lost). + onChange((topic, source) => { + if (source !== "sidebar" && SHOWN.includes(topic)) draw(root); + }); +} + +/** Re-draw the sidebar itself (for edits that change which controls it shows). */ +function refresh() { if (root) draw(root); } + +const notify = (topic) => changed(topic, "sidebar"); + +function section(title, count, open, ...content) { + return el("details", { class: "section", open }, + el("summary", {}, document.createTextNode(title), el("span", { class: "count", text: count ?? "" })), + el("div", {}, ...content)); +} + +function draw(node) { + node.replaceChildren( + candidatesSection(), + portfolioSection(), + modelSection(), + el("div", { class: "btnbar", style: { marginTop: "12px" } }, + button("Reset all settings", () => { if (confirm("Reset every setting of the test bench?")) resetState(); }, "btn small")), + ); +} + +/* ------------------------------------------------------------------ candidates */ +function candidatesSection() { + const u = state.universe; + const body = el("div"); + const chips = el("div", { class: "chips", style: { marginTop: "8px" } }); + + const redraw = () => { + chips.replaceChildren(); + if (!state.selection.length) { + chips.appendChild(el("span", { class: "hint", text: "no stocks selected - pick them in the Stocks tab or fill from the market" })); + } + for (const s of state.selection.slice(0, 60)) { + chips.appendChild(el("span", { class: "chip" }, document.createTextNode(s), + button("x", () => { + state.selection = state.selection.filter((x) => x !== s); + notify("selection"); redraw(); + }, ""))); + } + if (state.selection.length > 60) chips.appendChild(el("span", { class: "hint", text: `+ ${state.selection.length - 60} more` })); + }; + redraw(); + + const fill = async () => { + const { rows } = await universe(); + state.selection = topSymbols(rows, { region: u.region, sector: u.sector }, Number(u.top)); + notify("selection"); + refresh(); + }; + + body.append( + el("div", { class: "row" }, + field("Region", select([["", "world"], ["US", "US"], ["Europe", "Europe"]], u.region, + (v) => { u.region = v; notify("universe"); })), + field("Largest", input("number", u.top, (v) => { u.top = v || 30; notify("universe"); }, { min: 2, max: 500, step: 1 }))), + el("div", { class: "btnbar" }, + button("Fill from market caps", fill, "btn small primary"), + button("Clear", () => { state.selection = []; notify("selection"); refresh(); }, "btn small")), + chips, + ); + return section("Candidate stocks", `${state.selection.length}`, true, body); +} + +/* ------------------------------------------------------------------ portfolio */ +function portfolioSection() { + const p = state.portfolio; + const holdings = intHoldings(p.holdings); + const nHold = Object.keys(holdings).length; + const value = Object.entries(holdings).reduce((a, [s, n]) => a + (info(s)?.price || 0) * n, 0); + const body = el("div", + {}, + field("Mode", segmented([["new", "New portfolio"], ["rebalance", "Rebalance"]], p.mode, (v) => { + p.mode = v; notify("portfolio"); refresh(); + })), + field(p.mode === "new" ? "Cash to invest (CHF)" : "Free cash (CHF)", + input("number", p.cash, (v) => { p.cash = v ?? 0; notify("portfolio"); }, { min: 0, step: 10000 })), + el("div", { class: "btnbar", style: { marginBottom: "6px" } }, + ...[["1 m", 1e6], ["10 m", 1e7], ["50 m", 5e7]].map(([label, amount]) => + button(label, () => { p.cash = amount; notify("portfolio"); refresh(); }, "btn small"))), + p.mode === "rebalance" + ? el("div", { class: "note" }, + `${nHold} holdings, ${fmt.compact(value)} CHF at the last close. Edit them in the Optimisation tab.`) + : el("div", { class: "note", text: "A new portfolio starts from cash; holdings are ignored." }), + ); + return section("Portfolio", p.mode === "rebalance" ? `${nHold} positions` : fmt.compact(p.cash), true, body); +} + +/* ------------------------------------------------------------------ models */ +function modelSection() { + const m = state.model; + const set = (k, v) => { m[k] = v; notify("model"); }; + const setShape = (k, v) => { m[k] = v; notify("model"); refresh(); }; + const cov = m.covariance; + const exact = m.scenario_model === "gaussian_exact"; + + const covControls = cov.method === "ewm" + ? field("EWM decay λ", input("number", cov.lam, (v) => { cov.lam = v; notify("model"); }, { min: 0.5, max: 0.9999, step: 0.005 }), + `≈ ${Math.round(3 / (1 - Number(cov.lam)))} effective observations`) + : field("Window (periods)", input("number", cov.window, (v) => { cov.window = v; notify("model"); }, { min: 10, step: 10 })); + + return section("Models", m.scenario_model, true, el("div", {}, + el("div", { class: "row" }, + field("Frequency", select([["daily", "daily"], ["weekly", "weekly"]], m.freq, (v) => set("freq", v))), + field("Horizon", select(HORIZONS, m.horizon, (v) => set("horizon", v)))), + field("Stock-price model", select(SCENARIO_MODELS, m.scenario_model, (v) => setShape("scenario_model", v))), + exact ? el("div", { class: "note", text: "Exact Gaussian: mean-CVaR solved as the equivalent mean-variance problem by outer approximation - no sampling, no scenario noise." }) : null, + !exact ? el("div", { class: "row" }, + field("Scenarios", input("number", m.n_scenarios, (v) => set("n_scenarios", v), { min: 256, step: 1024 }), + "rounded up to a power of two"), + field("Seed", input("number", m.seed, (v) => set("seed", v), { step: 1 }))) : null, + !exact ? el("div", { class: "row" }, + field("Sampling", select([["sobol", "Sobol'"], ["mc", "plain MC"], ["antithetic", "antithetic"], ["lhs", "Latin hypercube"], ["balanced", "balanced (empirical)"]], m.sampling, (v) => set("sampling", v))), + field("Tail tilt", select([["auto", "auto"], ["market", "market"], ["adaptive", "adaptive"], ["off", "off"]], m.importance_sampling, (v) => set("importance_sampling", v)))) : null, + !exact ? checkbox("Moment matching (exact mean and covariance)", m.moment_match, (v) => set("moment_match", v)) : null, + el("div", { class: "row", style: { marginTop: "8px" } }, + field("Covariance", select([["ewm", "EWM"], ["rolling", "rolling"]], cov.method, (v) => { cov.method = v; notify("model"); refresh(); })), + covControls), + field("Shrinkage", select([["auto", "auto (Schäfer-Strimmer)"], [0, "none"], [0.1, "0.10"], [0.25, "0.25"], [0.5, "0.50"]], + cov.shrinkage, (v) => { cov.shrinkage = v === "auto" ? "auto" : Number(v); notify("model"); })), + field("Expected returns", select([["black_litterman", "Black-Litterman"], ["historical", "historical mean"], ["zero", "zero drift"]], + m.expected_returns, (v) => setShape("expected_returns", v))), + m.expected_returns === "black_litterman" + ? el("div", { class: "row" }, + field("τ", input("number", m.bl_tau, (v) => set("bl_tau", v), { min: 0.001, step: 0.01 })), + field("History (years)", input("number", m.bl_years, (v) => set("bl_years", v), { min: 1, max: 20, step: 1 }))) + : null, + field("Drift convention", select([["arithmetic", "arithmetic (Itô-corrected)"], ["log", "log"]], m.drift_convention, (v) => set("drift_convention", v))), + )); +} + +/* ------------------------------------------------------------------ health pill */ +export async function refreshHealth(node) { + try { + const h = await api.get("/health"); + const store = h.store || {}; + const cuopt = h.cuopt || {}; + node.replaceChildren( + pill("data", `${store.n_universe ?? "?"} stocks · ${fmt.date(store.last_price_date || "")}`, "ok"), + pill("cuOpt", cuopt.reachable ? (cuopt.version || "up") : "unreachable", cuopt.reachable ? "ok" : "warn"), + pill("HiGHS", "in-process", "ok"), + pill("base", h.base_currency || "CHF", "ok"), + el("span", { class: "pill" }, document.createTextNode(`v${h.version}`)), + ); + } catch (err) { + node.replaceChildren(pill("api", String(err.message).slice(0, 60), "bad")); + } +} + +function pill(label, value, kind) { + return el("span", { class: "pill" }, el("span", { class: `dot ${kind}` }), + document.createTextNode(label + " "), el("b", { text: value })); +} diff --git a/quantcore/api/ui/js/tabs/constraints.js b/quantcore/api/ui/js/tabs/constraints.js new file mode 100644 index 0000000..258516c --- /dev/null +++ b/quantcore/api/ui/js/tabs/constraints.js @@ -0,0 +1,173 @@ +/* Tab 4 -- constraints and costs, with a feasibility check against the candidate set. */ + +import { + card, changed, chartBox, dataTable, el, empty, field, fmt, input, intHoldings, setChildren, state, + statTiles, +} from "../core.js"; +import { barChart } from "../charts.js"; +import { info } from "../universe.js"; + +export function render(node) { + node.replaceChildren(); + if (!state.selection.length) { node.appendChild(empty("select candidate stocks in the Stocks tab first")); return; } + const derived = el("div", { id: "constraint-derived" }); + const refresh = () => { + const live = document.getElementById("constraint-derived") || derived; + setChildren(live, feasibilityCard(), sectorCard()); + }; + refresh(); + node.append(limitsCard(refresh), costsCard(refresh), derived); +} + +function wealth() { + const h = intHoldings(state.portfolio.holdings); + const held = state.portfolio.mode === "rebalance" + ? Object.entries(h).reduce((a, [s, n]) => a + (info(s)?.price || 0) * n, 0) : 0; + return Number(state.portfolio.cash) + held; +} + +/* ------------------------------------------------------------------ limits */ +function limitsCard(refresh) { + const c = state.constraints; + const set = (k) => (v) => { c[k] = v; changed("constraints", "constraints"); refresh(); }; + const W = wealth(); + const sectors = [...new Set(state.selection.map((s) => info(s)?.sector || "Unknown"))].sort(); + + const overrides = el("div"); + const drawOverrides = () => { + overrides.replaceChildren(el("div", { class: "row" }, ...sectors.map((sec) => + field(sec, input("number", c.sector_overrides?.[sec] ?? "", (v) => { + c.sector_overrides = { ...(c.sector_overrides || {}) }; + if (v === null || v === "") delete c.sector_overrides[sec]; else c.sector_overrides[sec] = v; + changed("constraints", "constraints"); refresh(); + }, { min: 0, max: 1, step: 0.05, placeholder: String(c.max_sector_weight) }))))); + }; + drawOverrides(); + + return card("Portfolio constraints", `fractions of wealth (${fmt.compact(W)} CHF)`, el("div", {}, + el("div", { class: "row" }, + field("Max weight per stock", input("number", c.max_weight, set("max_weight"), { min: 0.001, max: 1, step: 0.01 }), + `fraction of ${fmt.compact(W)} CHF per name`), + field("Max weight per sector", input("number", c.max_sector_weight, set("max_sector_weight"), { min: 0.01, max: 1, step: 0.05 }), + "applies to every GICS sector unless overridden below"), + field("Max positions", input("number", c.max_positions ?? "", set("max_positions"), { min: 1, step: 1, placeholder: "no limit" }), + "cardinality limit (adds binaries)"), + field("Min position value", input("number", c.min_position_value, set("min_position_value"), { min: 0, step: 1000 }), + "CHF; a position is either 0 or at least this")), + el("div", { class: "row" }, + field("Min invested", input("number", c.min_invested, set("min_invested"), { min: 0, max: 1, step: 0.05 }), + "fraction of wealth that must be in stocks"), + field("Max invested", input("number", c.max_invested, set("max_invested"), { min: 0.01, max: 1, step: 0.05 }), + "the rest stays in cash"), + field("Max turnover", input("number", c.max_turnover ?? "", set("max_turnover"), { min: 0, step: 0.05, placeholder: "no limit" }), + "buys + sells as a fraction of wealth"), + field("Max transaction cost", input("number", c.max_transaction_cost ?? "", set("max_transaction_cost"), + { min: 0, step: 500, placeholder: "no limit" }), "CHF")), + el("h3", { text: "Sector overrides", style: { marginTop: "10px" } }), + el("div", { class: "hint", text: "empty = the general sector cap" }), + overrides)); +} + +/* ------------------------------------------------------------------ costs */ +function costsCard(refresh) { + const k = state.costs; + const set = (key) => (v) => { k[key] = v ?? 0; changed("costs", "constraints"); refresh(); }; + return card("Transaction costs", "charged on the traded value; they enter the objective and the cash balance", + el("div", { class: "row" }, + field("Buy", input("number", k.proportional_buy, set("proportional_buy"), { min: 0, step: 0.0005 }), fmt.bp(k.proportional_buy)), + field("Sell", input("number", k.proportional_sell, set("proportional_sell"), { min: 0, step: 0.0005 }), fmt.bp(k.proportional_sell)), + field("Fixed per traded stock", input("number", k.fixed_per_trade, set("fixed_per_trade"), { min: 0, step: 5 }), + k.fixed_per_trade > 0 ? "CHF - adds one binary per stock" : "CHF (0 = no binaries)"))); +} + +/* ------------------------------------------------------------------ feasibility */ +function feasibilityCard() { + const c = state.constraints, W = wealth(); + const sel = state.selection; + const bySector = {}; + for (const s of sel) (bySector[info(s)?.sector || "Unknown"] ||= []).push(s); + const cap = (sec) => Number(c.sector_overrides?.[sec] ?? c.max_sector_weight); + + const reachable = sel.filter((s) => (info(s)?.price || 0) <= c.max_weight * W); + const headroomBySector = Object.entries(bySector) + .map(([sec, list]) => Math.min(cap(sec), list.length * Number(c.max_weight))); + const headroom = headroomBySector.reduce((a, b) => a + b, 0); + const cardHeadroom = c.max_positions ? Number(c.max_positions) * Number(c.max_weight) : Infinity; + + const checks = [ + { ok: sel.length * c.max_weight >= c.min_invested, + label: "Enough names for the minimum investment", + detail: `${sel.length} candidates × ${fmt.pct(c.max_weight, 0)} = ${fmt.pct(sel.length * c.max_weight, 0)} of wealth available, ${fmt.pct(c.min_invested, 0)} required` }, + { ok: headroom >= c.min_invested, + label: "Sector caps leave room for the minimum investment", + detail: `the sector caps allow at most ${fmt.pct(headroom, 0)} of wealth in total` }, + { ok: cardHeadroom >= c.min_invested, + label: "Cardinality limit leaves room", + detail: c.max_positions ? `${c.max_positions} positions × ${fmt.pct(c.max_weight, 0)} = ${fmt.pct(cardHeadroom, 0)}` : "no cardinality limit" }, + { ok: !c.min_position_value || c.min_position_value <= c.max_weight * W, + label: "Minimum position fits under the weight cap", + detail: `minimum ${fmt.compact(c.min_position_value)} CHF vs cap ${fmt.compact(c.max_weight * W)} CHF` }, + { ok: c.min_invested <= c.max_invested, + label: "Investment band is consistent", + detail: `${fmt.pct(c.min_invested, 0)} … ${fmt.pct(c.max_invested, 0)}` }, + { ok: reachable.length === sel.length, + label: "Every candidate is affordable as one share", + detail: reachable.length === sel.length ? "one share of each candidate fits under the weight cap" + : `${sel.length - reachable.length} stocks cost more per share than ${fmt.compact(c.max_weight * W)} CHF and can never be held` }, + ]; + + const failed = checks.filter((c2) => !c2.ok); + return card("Feasibility", failed.length ? `${failed.length} issue${failed.length > 1 ? "s" : ""} - the solver would return infeasible or ignore these names` + : "the constraints and the candidate set are consistent", + el("div", {}, + statTiles([ + { label: "Candidates", value: fmt.int(sel.length), delta: `${Object.keys(bySector).length} sectors` }, + { label: "Wealth", value: `${fmt.compact(W)} CHF` }, + { label: "Cap per name", value: fmt.compact(c.max_weight * W), delta: fmt.pct(c.max_weight, 1) }, + { label: "Max reachable", value: fmt.pct(Math.min(headroom, cardHeadroom, sel.length * c.max_weight, c.max_invested), 0), + delta: "of wealth under all caps" }, + { label: "Min positions needed", value: fmt.int(Math.ceil(c.min_invested / c.max_weight)), + delta: `to reach ${fmt.pct(c.min_invested, 0)}` }, + ]), + el("div", { style: { marginTop: "10px" } }, dataTable([ + { key: "ok", label: "", render: (r) => el("span", { class: r.ok ? "pos" : "neg", text: r.ok ? "✓" : "✗" }) }, + { key: "label", label: "Check" }, + { key: "detail", label: "Detail", cls: "muted" }, + ], checks, { sortable: false })))); +} + +/* ------------------------------------------------------------------ sector composition */ +function sectorCard() { + const c = state.constraints; + const sel = state.selection; + const bySector = {}; + for (const s of sel) { + const sec = info(s)?.sector || "Unknown"; + (bySector[sec] ||= { n: 0, cap: 0 }); + bySector[sec].n += 1; + bySector[sec].cap += info(s)?.market_cap || 0; + } + const totalCap = Object.values(bySector).reduce((a, b) => a + b.cap, 0) || 1; + const items = Object.entries(bySector).sort((a, b) => b[1].cap - a[1].cap).map(([sec, v]) => ({ + label: sec.length > 22 ? sec.slice(0, 21) + "…" : sec, values: [v.cap / totalCap], + })); + const box = chartBox(); + const out = card("Sectors of the candidate set", "cap-weighted share of the selection against the cap that will be enforced", + el("div", {}, box, + dataTable([ + { key: "sector", label: "Sector" }, + { key: "n", label: "Stocks", num: true, fmt: (v) => fmt.int(v) }, + { key: "share", label: "Cap-weighted share", num: true, fmt: (v) => fmt.pct(v, 1) }, + { key: "limit", label: "Limit", num: true, fmt: (v) => fmt.pct(v, 0) }, + { key: "room", label: "Max under the caps", num: true, fmt: (v) => fmt.pct(v, 0), + title: "the smaller of the sector cap and (number of stocks × the per-stock cap)" }, + ], Object.entries(bySector).map(([sec, v]) => ({ + sector: sec, n: v.n, share: v.cap / totalCap, + limit: Number(c.sector_overrides?.[sec] ?? c.max_sector_weight), + room: Math.min(Number(c.sector_overrides?.[sec] ?? c.max_sector_weight), v.n * Number(c.max_weight)), + })), { sortKey: "share", sortDir: -1 }))); + barChart(box, { items, valueFormat: (v) => fmt.pct(v, 0), horizontal: true, rowHeight: 24, labelWidth: 150, + valueName: "cap-weighted share", + references: [{ value: Number(c.max_sector_weight), label: `sector cap ${fmt.pct(c.max_sector_weight, 0)}` }] }); + return out; +} diff --git a/quantcore/api/ui/js/tabs/optimize.js b/quantcore/api/ui/js/tabs/optimize.js new file mode 100644 index 0000000..c43971a --- /dev/null +++ b/quantcore/api/ui/js/tabs/optimize.js @@ -0,0 +1,511 @@ +/* Tab 5 -- the optimisation itself: inputs, the proposal, and before/after for a rebalancing. */ + +import { + api, button, card, changed, chartBox, constraintPayload, dataTable, el, empty, field, fmt, input, + intHoldings, kv, live, optimizePayload, segmented, select, setChildren, state, statTiles, warnBox, + withBusy, +} from "../core.js"; +import { barChart, scatterChart } from "../charts.js"; +import { info } from "../universe.js"; +import { addRun, allRuns, clearRuns, isSelected, selectRun, selectedRun, sharesOf } from "../runs.js"; + +/** The result on screen. It is the *selected* run, so every "use this result" action and the + * history table agree on which one that is -- see js/runs.js. */ +const shown = () => selectedRun()?.result || null; + +export function render(node) { + node.replaceChildren(); + if (!state.selection.length) { node.appendChild(empty("select candidate stocks in the Stocks tab first")); return; } + const rerender = () => render(node); + const out = el("div", { id: "opt-result" }); + node.append(inputsCard(out, rerender), out, historyCard(rerender)); + const current = shown(); + if (current) drawResult(out, current); + else out.appendChild(empty("run the optimisation to see the proposal")); +} + +/* ------------------------------------------------------------------ inputs */ +function inputsCard(out, rerender) { + const o = state.opt, p = state.portfolio; + // A plain value edit must not rebuild the card: the change event fires on blur, so rebuilding + // here would replace the button the user is clicking and swallow the click. + const set = (k) => (v) => { o[k] = v; changed("opt", "optimize"); }; + const setShape = (k) => (v) => { o[k] = v; changed("opt", "optimize"); rerender(); }; + let runBtn, sweepBtn; + + const run = async (extra = {}, label = null) => { + const panel = live("opt-result", out); + await withBusy(runBtn, panel, async () => { + const body = optimizePayload(extra); + const t0 = performance.now(); + const abort = new AbortController(); + const stop = showProgress(panel, body, "", () => abort.abort()); + let result; + try { + result = await api.post("/optimize", body, { signal: abort.signal }); + } catch (err) { + if (err.name === "AbortError") { cancelled(live("opt-result", panel)); return; } + throw err; + } finally { stop(); } + addRun({ label: label || describeRun(body), request: body, result, wall: (performance.now() - t0) / 1000 }); + drawResult(live("opt-result", panel), result); + rerender(); + }).catch(() => {}); + }; + + // The sweep traces the frontier along the parameter the *selected* objective is steered by. It + // used to force objective='max_return' and vary the risk limit whatever was selected, so a sweep + // started under "minimise CVaR" silently ran five constrained max-return problems and every one + // of them came back sitting on its CVaR limit. + const sweepPlan = () => { + if (o.objective === "min_cvar") { + const base = Number(o.target_return); + if (!Number.isFinite(base) || base <= 0) return null; + return { + what: "the target return", + steps: [0.25, 0.5, 1, 1.5, 2].map((k) => ({ + extra: { target_return: round4(base * k) }, + label: `E[r] ≥ ${fmt.pct(base * k, 3)}`, + })), + }; + } + if (o.objective === "utility") { + const base = Number(o.risk_aversion) || 1; + return { + what: "the risk aversion λ", + steps: [0.25, 0.5, 1, 2, 4].map((k) => ({ + extra: { risk_aversion: round4(base * k) }, + label: `λ = ${round4(base * k)}`, + })), + }; + } + return { + what: "the risk limit", + steps: [0.01, 0.02, 0.03, 0.05, 0.08].map((f) => ({ + extra: { cvar_limit_fraction: f, cvar_limit: null }, + label: `CVaR ≤ ${fmt.pct(f, 0)}`, + })), + }; + }; + + const sweep = async () => { + const plan = sweepPlan(); + if (!plan) { alert("set a target return first - the sweep varies it around the value you entered"); return; } + await withBusy(sweepBtn, live("opt-result", out), async () => { + for (const step of plan.steps) { + const body = optimizePayload(step.extra); + const t0 = performance.now(); + const abort = new AbortController(); + const stop = showProgress(live("opt-result", out), body, step.label, () => abort.abort()); + let result; + try { + result = await api.post("/optimize", body, { signal: abort.signal }); + } catch (err) { + if (err.name === "AbortError") { cancelled(live("opt-result", out)); break; } + throw err; + } finally { stop(); } + addRun({ label: step.label, request: body, result, wall: (performance.now() - t0) / 1000 }); + drawResult(live("opt-result", out), result); + } + rerender(); + }).catch(() => {}); + }; + + runBtn = button("Run optimisation", () => run(), "btn primary"); + const sweepWhat = o.objective === "min_cvar" ? "target return" + : o.objective === "utility" ? "\u03bb" : "risk limit"; + sweepBtn = button(`Sweep the ${sweepWhat} (5 runs)`, sweep, "btn", + `five runs varying ${sweepWhat} around the current value, keeping the objective`); + + // the hint under the limit is updated in place (see `set` above) + const limitHint = el("span", { class: "hint", text: limitHintText() }); + const limitField = (label, control) => el("div", { class: "field" }, el("label", { text: label }), control, limitHint); + const onLimit = (k) => (v) => { set(k)(v); limitHint.textContent = limitHintText(); }; + const limitControls = o.objective === "max_return" + ? el("div", { class: "row" }, + field("CVaR limit", segmented([["fraction", "% of wealth"], ["money", "CHF"]], o.cvar_mode, setShape("cvar_mode"))), + o.cvar_mode === "fraction" + // entered in percent, because the control above says "% of wealth" + ? limitField("Limit (% of wealth)", input("number", round4((o.cvar_limit_fraction || 0) * 100), + (v) => onLimit("cvar_limit_fraction")((v ?? 0) / 100), + { min: 0.1, max: 99, step: 0.5 })) + : limitField("Limit (CHF)", input("number", o.cvar_limit, onLimit("cvar_limit"), { min: 1, step: 1000 }))) + : o.objective === "min_cvar" + ? field("Target return over the horizon", input("number", o.target_return ?? "", set("target_return"), { step: 0.001, placeholder: "none" }), + "fraction of wealth, e.g. 0.002") + : field("Risk aversion λ", input("number", o.risk_aversion, set("risk_aversion"), { min: 0, step: 0.01 }), + "maximise E[P&L] − λ · CVaR; both are money, so λ is the francs of expected return you " + + "give up per franc of CVaR — over a week the interesting range is around 0.01–0.05, " + + "and a large λ keeps everything in cash"); + + return card("Optimisation", "mean-CVaR in integer shares; the model panel on the left decides how the scenarios are made", + el("div", {}, + el("div", { class: "row" }, + field("Mode", segmented([["new", "New portfolio"], ["rebalance", "Rebalance"]], p.mode, (v) => { + p.mode = v; changed("portfolio", "optimize"); rerender(); })), + field("Objective", select([["max_return", "maximise expected return"], ["min_cvar", "minimise CVaR"], + ["utility", "expected return − λ · CVaR"]], o.objective, setShape("objective"))), + field("Confidence β", select([[0.9, "90 %"], [0.95, "95 %"], [0.99, "99 %"]], o.confidence, (v) => set("confidence")(Number(v)))), + field("Solver", select([["auto", "auto (cuOpt, then HiGHS)"], ["cuopt", "cuOpt (GPU)"], ["highs", "HiGHS (CPU)"]], o.solver, set("solver"))), + field("Time limit (s)", input("number", o.time_limit ?? "", set("time_limit"), { min: 1, step: 10, placeholder: "no limit" }), + o.time_limit ? "budget for the whole run; the solver returns its best solution at that point" + : "empty: HiGHS runs until proven optimal - a large problem can take hours")), + limitControls, + el("div", { class: "row" }, + field("Risk measured on", select([["model", "scenarios incl. the return forecast"], + ["zero", "centred scenarios (no drift)"]], o.risk_drift, set("risk_drift")), + o.risk_drift === "model" + ? "over a long horizon a large forecast can push the whole tail into profit" + : "the risk figure does not depend on the return forecast"), + field("Tail rows", select([["auto", "auto"], ["on", "always"], ["off", "never"]], o.row_generation, set("row_generation")), + "solve on the worst scenarios only, then verify"), + el("div", { class: "field" }, + el("label", { text: "Scenario count" }), + segmented([[true, "automatic"], [false, `fixed (${fmt.int(state.model.n_scenarios)})`]], o.auto_scenarios, + (v) => setShape("auto_scenarios")(v === "true" || v === true))), + el("div", { class: "field auto" }, el("label", { text: " " }), el("div", { class: "btnbar" }, runBtn, sweepBtn))), + constraintStrip(), + p.mode === "rebalance" ? holdingsEditor(rerender) : null)); +} + +/** The constraints and costs this run will send -- edited in the Constraints tab, shown here so it + is never a guess which limits are in force. */ +function constraintStrip() { + const c = constraintPayload(), k = state.costs; + const sector = typeof c.max_sector_weight === "object" + ? `${fmt.pct(c.max_sector_weight["*"], 0)} per sector (${Object.keys(c.max_sector_weight).length - 1} overridden)` + : `${fmt.pct(c.max_sector_weight, 0)} per sector`; + const parts = [ + `${fmt.pct(c.max_weight, 0)} per stock`, sector, + c.max_positions ? `at most ${c.max_positions} positions` : "no cardinality limit", + c.min_position_value ? `min position ${fmt.compact(c.min_position_value)} CHF` : null, + `invested ${fmt.pct(c.min_invested, 0)} – ${fmt.pct(c.max_invested, 0)}`, + c.max_turnover !== null ? `turnover ≤ ${fmt.pct(c.max_turnover, 0)}` : null, + c.max_transaction_cost !== null ? `cost ≤ ${fmt.compact(c.max_transaction_cost)} CHF` : null, + `costs ${fmt.bp(k.proportional_buy)} buy / ${fmt.bp(k.proportional_sell)} sell`, + Number(k.fixed_per_trade) ? `+ ${fmt.money(k.fixed_per_trade)} CHF per traded stock` : null, + ].filter(Boolean); + return el("div", { class: "note", style: { marginTop: "10px" } }, + el("b", { text: "Constraints in force: " }), document.createTextNode(parts.join(" · ") + " "), + button("edit", () => document.dispatchEvent(new CustomEvent("quantcore:tab", { detail: "constraints" })), "btn small")); +} + +/** A live "solving …" panel with a cancel button; returns the function that stops the timer. */ +function showProgress(out, body, extra = "", onCancel = null) { + const started = performance.now(); + const secs = el("b", { text: "0 s" }); + const line = el("div", { class: "empty" }, el("span", { class: "spinner" }), + document.createTextNode(" solving … "), secs, + onCancel ? document.createTextNode(" ") : null, + onCancel ? button("Cancel", onCancel, "btn small") : null); + const limit = body.time_limit ? `, time limit ${fmt.int(body.time_limit)} s` : ", no time limit"; + const detail = el("div", { class: "hint", style: { marginTop: "6px" } }, + `${body.scenario_model} · ${body.n_scenarios ? fmt.int(body.n_scenarios) + " scenarios" : "automatic scenario count"} · ` + + `${body.symbols?.length ?? body.max_assets} candidates · solver ${body.solver}${limit}${extra ? " · " + extra : ""}`); + // the service solves one problem at a time (the MILP holds the interpreter), so say so + const queued = el("div", { class: "hint" }, + "The service answers one request at a time while a solve runs - the other tabs wait until this finishes."); + out.replaceChildren(line, detail, queued); + const t = setInterval(() => { secs.textContent = `${((performance.now() - started) / 1000).toFixed(0)} s`; }, 500); + return () => clearInterval(t); +} + +function cancelled(panel) { + setChildren(panel, el("div", { class: "warn-box", text: + "Run cancelled. The server finishes the solve it already started (it stops at the time limit); " + + "the next run is unaffected." })); +} + +const round4 = (v) => Math.round(v * 1e4) / 1e4; // 0.045 * 100 must read as 4.5, not 4.499999 + +function limitHintText() { + const o = state.opt; + return o.cvar_mode === "fraction" + ? `the expected shortfall may not exceed ${fmt.compact((o.cvar_limit_fraction || 0) * wealth())} CHF ` + + `over one ${state.model.horizon}` + : `${fmt.pct((o.cvar_limit || 0) / wealth(), 1)} of wealth over one ${state.model.horizon}`; +} + +function wealth() { + const h = intHoldings(state.portfolio.holdings); + const held = state.portfolio.mode === "rebalance" + ? Object.entries(h).reduce((a, [s, n]) => a + (info(s)?.price || 0) * n, 0) : 0; + return Number(state.portfolio.cash) + held; +} + +function describeRun(body) { + const risk = body.objective === "min_cvar" + ? `min CVaR, E[r] \u2265 ${fmt.pct(body.target_return ?? 0, 3)}` + : body.objective === "utility" ? `E[P&L] \u2212 ${round4(body.risk_aversion)} \u00b7 CVaR` + : body.cvar_limit_fraction ? `CVaR \u2264 ${fmt.pct(body.cvar_limit_fraction, 1)}` + : body.cvar_limit ? `CVaR \u2264 ${fmt.compact(body.cvar_limit)}` : body.objective; + return `${body.mode} · ${body.scenario_model} · ${risk} · β ${fmt.pct(body.confidence, 0)}`; +} + +/** Cash is money: two decimals, not the full float that a valuation produces. */ +function round2(x) { return Math.round(x * 100) / 100; } + +/* ------------------------------------------------------------------ holdings */ +function holdingsEditor(rerender) { + const p = state.portfolio; + const holdings = intHoldings(p.holdings); + const rows = Object.entries(holdings).map(([symbol, shares]) => ({ + symbol, name: info(symbol)?.name || symbol, shares, price: info(symbol)?.price, + value: (info(symbol)?.price || 0) * shares, + })); + const total = rows.reduce((a, r) => a + r.value, 0); + + const addSel = select(["", ...state.selection.filter((s) => !(s in holdings))].map((s) => [s, s || "add a stock …"]), "", + (v) => { if (!v) return; p.holdings = { ...holdings, [v]: 1 }; changed("portfolio", "optimize"); rerender(); }); + + return el("div", { style: { marginTop: "12px" } }, + el("h3", { text: "Current holdings" }), + el("div", { class: "hint", text: `${rows.length} positions, ${fmt.compact(total)} CHF at the last close, plus ${fmt.compact(p.cash)} CHF cash` }), + el("div", { class: "btnbar", style: { margin: "7px 0" } }, + button("Use the shown result", () => { + // the run on screen, not the newest one: with several runs in the history those differ, and + // the weights that get adopted have to be the ones the user is looking at + const r = selectedRun(); + if (!r) { alert("no optimisation has run yet"); return; } + p.holdings = sharesOf(r.result); + // the proposal spent the cash: carry over what it left, or the portfolio would be the new + // positions *plus* the money already invested in them and every weight would come out low + p.cash = round2(Math.max(0, Number(r.result.cash_end ?? p.cash))); + p.mode = "rebalance"; changed("portfolio", "optimize"); rerender(); + }, "btn small", selectedRun() ? `takes the ${selectedRun().result.n_positions} positions of "${selectedRun().label}"` : "run an optimisation first"), + selectedRun() ? el("span", { class: "hint", text: `shown: ${selectedRun().label}` }) : null, + button("Equal weight from cash", () => { + const cash = Number(p.cash); + const each = cash / Math.max(state.selection.length, 1); + p.holdings = Object.fromEntries(state.selection.map((s) => [s, Math.floor(each / (info(s)?.price || 1))]) + .filter(([, n]) => n > 0)); + // likewise: the cash has been turned into shares, only the rounding remainder is left + const spent = Object.entries(p.holdings).reduce((a, [s, n]) => a + (info(s)?.price || 0) * n, 0); + p.cash = round2(Math.max(0, cash - spent)); + changed("portfolio", "optimize"); rerender(); + }, "btn small", "buys equal amounts of the selected stocks and leaves the rounding remainder as cash"), + button("Clear", () => { p.holdings = {}; changed("portfolio", "optimize"); rerender(); }, "btn small"), + addSel), + rows.length ? dataTable([ + { key: "symbol", label: "Symbol", cls: "sym" }, + { key: "name", label: "Name", cls: "name" }, + { key: "price", label: "Price", num: true, fmt: (v) => fmt.money2(v) }, + { key: "shares", label: "Shares", num: true, render: (r) => { + const box = el("input", { type: "number", value: r.shares, min: 0, step: 1, style: { width: "92px", textAlign: "right" } }); + box.addEventListener("change", () => { + const next = { ...intHoldings(p.holdings) }; + const n = Math.max(0, Math.round(Number(box.value) || 0)); + if (n === 0) delete next[r.symbol]; else next[r.symbol] = n; + p.holdings = next; changed("portfolio", "optimize"); + }); + return box; + } }, + { key: "value", label: "Value (CHF)", num: true, fmt: (v) => fmt.money(v) }, + { key: "w", label: "Weight", num: true, render: (r) => document.createTextNode(fmt.pct(r.value / (total + Number(p.cash)), 1)) }, + { key: "del", label: "", render: (r) => button("remove", () => { + const next = { ...intHoldings(p.holdings) }; delete next[r.symbol]; + p.holdings = next; changed("portfolio", "optimize"); rerender(); + }, "btn small") }, + ], rows, { sortKey: "value", sortDir: -1 }) : el("div", { class: "note", text: "no holdings - add stocks or start from a previous result" })); +} + +/* ------------------------------------------------------------------ result */ +function drawResult(out, r) { + const before = r.before; + const ok = r.status === "Optimal" || r.status === "FeasibleFound"; + const tiles = [ + { label: "Status", value: r.status, delta: `${r.solver} · ${fmt.secs(r.timings?.solve_s)}`, + deltaClass: ok ? "muted" : "neg" }, + { label: "Expected P&L", value: fmt.money(r.expected_pnl), delta: `${fmt.pctSigned(r.expected_return, 2)} of wealth`, + deltaClass: r.expected_pnl >= 0 ? "pos" : "neg" }, + { label: `CVaR ${fmt.pct(r.confidence, 0)}`, value: fmt.money(r.cvar), + delta: r.cvar_limit ? `limit ${fmt.money(r.cvar_limit)}` : "no limit", + deltaClass: r.cvar_limit && r.cvar > r.cvar_limit * 1.001 ? "neg" : "muted" }, + { label: `VaR ${fmt.pct(r.confidence, 0)}`, value: fmt.money(r.var) }, + { label: "Invested", value: fmt.money(r.invested), delta: `${fmt.pct(r.invested / r.wealth, 1)} of ${fmt.compact(r.wealth)}` }, + { label: "Positions", value: fmt.int(r.n_positions), delta: `of ${fmt.int(r.n_candidates)} candidates` }, + { label: "Turnover", value: fmt.pct(r.turnover, 1), delta: `cost ${fmt.money(r.total_cost)}` }, + { label: "Reproducible", value: r.reproducibility, delta: r.scenario_hash ? `set ${r.scenario_hash.slice(0, 8)}` : "no scenarios", + deltaClass: r.reproducibility === "exact" ? "pos" : "muted" }, + ]; + + const weightChart = chartBox(), sectorChart = chartBox(); + const positions = r.positions || []; + const parts = [warnBox(r.warnings), statTiles(tiles)]; + + if (before) parts.push(beforeAfterCard(r, before)); + + parts.push(el("div", { class: "grid two", style: { marginTop: "12px" } }, + card("Portfolio weights", before ? "before and after the rebalancing" : "share of wealth per stock", weightChart), + card("Sector exposure", "against the limit that was enforced", sectorChart))); + + parts.push(card("Positions", before ? "shares before and after, with the trades" : "the proposed portfolio", + positionsTable(positions, !!before, r.wealth))); + + parts.push(provenanceCard(r)); + setChildren(out, ...parts); + + const held = positions.filter((p) => p.shares > 0 || p.shares_before > 0) + .sort((a, b) => b.value - a.value).slice(0, 25); + barChart(weightChart, { + items: held.map((p) => ({ label: p.symbol, values: before ? [p.shares_before * p.price / r.wealth, p.weight] : [p.weight] })), + groups: before ? ["before", "after"] : [""], + valueFormat: (v) => fmt.pct(v, 1), horizontal: true, rowHeight: before ? 24 : 20, labelWidth: 84, + references: [{ value: state.constraints.max_weight, label: "cap" }], valueName: "weight", + }); + const sectors = Object.entries(r.sector_exposure || {}).sort((a, b) => b[1] - a[1]); + const cap = Number(state.constraints.max_sector_weight); + barChart(sectorChart, { + items: sectors.map(([sec, v]) => ({ label: sec.length > 20 ? sec.slice(0, 19) + "…" : sec, + values: before ? [before.sector_exposure?.[sec] || 0, v] : [v] })), + groups: before ? ["before", "after"] : [""], valueName: "exposure", + valueFormat: (v) => fmt.pct(v, 0), horizontal: true, rowHeight: before ? 28 : 22, labelWidth: 140, + references: [{ value: cap, label: `sector cap ${fmt.pct(cap, 0)}` }], + }); +} + +function beforeAfterCard(r, before) { + const rows = [ + { k: "Invested (CHF)", a: before.invested, b: r.invested, f: fmt.money }, + { k: "Cash (CHF)", a: before.cash, b: r.cash_end, f: fmt.money }, + { k: "Positions", a: before.n_positions, b: r.n_positions, f: fmt.int }, + { k: "Expected P&L (CHF)", a: before.expected_pnl, b: r.expected_pnl, f: fmt.money }, + { k: "Expected return of wealth", a: before.expected_return, b: r.expected_return, f: (v) => fmt.pctSigned(v, 3) }, + { k: `VaR ${fmt.pct(r.confidence, 0)} (CHF)`, a: before.var, b: r.var, f: fmt.money }, + { k: `CVaR ${fmt.pct(r.confidence, 0)} (CHF)`, a: before.cvar, b: r.cvar, f: fmt.money }, + { k: "CVaR of wealth", a: before.cvar_fraction, b: r.cvar / r.wealth, f: (v) => fmt.pct(v, 2) }, + ].map((row) => ({ ...row, d: row.b - row.a })); + + return card("Before and after", "the starting portfolio measured on the same scenarios as the proposal", + dataTable([ + { key: "k", label: "" }, + { key: "a", label: "Before", num: true, fmt: (v, r2) => r2.f(v) }, + { key: "b", label: "After", num: true, fmt: (v, r2) => r2.f(v) }, + { key: "d", label: "Change", num: true, fmt: (v, r2) => (r2.f === fmt.int ? fmt.signed(v) : r2.f(v)), + cls: (r2) => (Math.abs(r2.d) < 1e-9 ? "muted" : r2.d > 0 ? "pos" : "neg") }, + ], rows, { sortable: false })); +} + +function positionsTable(positions, rebalancing, wealthValue) { + const cols = [ + { key: "symbol", label: "Symbol", cls: "sym" }, + { key: "name", label: "Name", cls: "name" }, + { key: "sector", label: "Sector" }, + { key: "price", label: "Price", num: true, fmt: (v) => fmt.money2(v) }, + ]; + if (rebalancing) cols.push( + { key: "shares_before", label: "Shares before", num: true, fmt: (v) => fmt.int(v) }, + { key: "shares", label: "Shares after", num: true, fmt: (v) => fmt.int(v) }, + { key: "trade", label: "Trade", num: true, fmt: (v) => fmt.signed(v), + cls: (r) => (r.trade > 0 ? "pos" : r.trade < 0 ? "neg" : "muted") }, + { key: "trade_value", label: "Traded (CHF)", num: true, fmt: (v) => fmt.signed(v) }, + ); + else cols.push({ key: "shares", label: "Shares", num: true, fmt: (v) => fmt.int(v) }); + cols.push( + { key: "value", label: "Value (CHF)", num: true, fmt: (v) => fmt.money(v) }, + { key: "weight", label: "Weight", num: true, fmt: (v) => fmt.pct(v, 2) }, + { key: "cost", label: "Cost (CHF)", num: true, fmt: (v) => fmt.money2(v) }, + { key: "expected_return_ann", label: "E[r] p.a.", num: true, fmt: (v) => fmt.pct(v, 1) }, + { key: "expected_return_horizon", label: "E[r] horizon", num: true, fmt: (v) => fmt.pct(v, 2) }, + ); + return dataTable(cols, positions, { sortKey: "value", sortDir: -1 }); +} + +function provenanceCard(r) { + const m = r.model || {}, s = r.solver_info || {}; + const scen = m.scenarios || {}; + const rg = s.row_generation; + return card("How this number was produced", "everything needed to reproduce the run", + el("div", { class: "grid three" }, + el("div", {}, el("h3", { text: "Scenarios" }), + kv([ + ["model", scen.model], ["scenarios", fmt.int(scen.n_scenarios)], ["sampling", m.sampling?.sampling], + ["moment matching", String(m.sampling?.moment_match)], ["tail tilt", m.sampling?.importance_sampling], + ["effective sample", scen.effective_sample_size ? fmt.int(scen.effective_sample_size) : null], + ["risk measured on", m.sampling?.risk_drift === "zero" ? "centred scenarios" : "scenarios incl. the forecast"], + ["tilt alignment", Number.isFinite(s.tilt_alignment) ? fmt.num(s.tilt_alignment, 2) : null], + ["scenario set", r.scenario_hash], + ["reused from cache", r.solver_info?.scenario_cache ? (r.solver_info.scenario_cache.reused ? "yes" : "no") : null], + ["as of", scen.as_of], ["seed", scen.seed], + ])), + el("div", {}, el("h3", { text: "Solver" }), + kv([ + ["solver", r.solver], ["status", r.status], ["objective", Number.isFinite(s.objective) ? fmt.money2(s.objective) : null], + ["MIP gap", Number.isFinite(s.mip_gap) ? s.mip_gap.toExponential(1) : s.mip_gap], + ["variables", fmt.int(m.milp?.n_vars)], + ["rows solved", rg ? `${fmt.int(rg.rows_final)} of ${fmt.int(scen.n_scenarios)}` : null], + ["row-generation passes", rg?.iterations], ["cuts (Gaussian)", s.outer_approximation?.iterations], + ["solve time", fmt.secs(s.solve_time_s)], ["total", fmt.secs(r.timings?.total_s)], + ["reproducibility", r.reproducibility], + ])), + el("div", {}, el("h3", { text: "Constraints used" }), + kv(constraintRows(m.constraints || {}, m.costs || {}))), + el("div", {}, el("h3", { text: "Expected returns" }), + kv([ + ["model", m.expected_returns?.type], ["market return p.a.", m.expected_returns?.market_return_ann ? fmt.pct(m.expected_returns.market_return_ann, 2) : null], + ["δ", m.expected_returns?.delta ? fmt.num(m.expected_returns.delta, 2) : null], + ["τ", m.expected_returns?.tau], ["views", m.expected_returns?.views?.length ?? 0], + ["drift convention", m.sampling?.drift_convention], + ["covariance", `${scen.covariance_model?.method ?? ""} ${scen.covariance_model?.lam ?? scen.covariance_model?.window ?? ""}`], + ["shrinkage", scen.shrinkage_applied ? fmt.pct(scen.shrinkage_applied, 1) : null], + ])))); +} + +function constraintRows(c, k) { + const sector = c.max_sector_weight && typeof c.max_sector_weight === "object" + ? Object.entries(c.max_sector_weight).map(([sec, v]) => `${sec === "*" ? "all" : sec} ${fmt.pct(v, 0)}`).join(", ") + : fmt.pct(c.max_sector_weight, 0); + return [ + ["max per stock", fmt.pct(c.max_weight, 1)], + ["max per sector", sector], + ["max positions", c.max_positions ?? "no limit"], + ["min position", c.min_position_value ? `${fmt.money(c.min_position_value)} CHF` : "none"], + ["invested", `${fmt.pct(c.min_invested, 0)} – ${fmt.pct(c.max_invested, 0)}`], + ["max turnover", c.max_turnover === null || c.max_turnover === undefined ? "no limit" : fmt.pct(c.max_turnover, 0)], + ["max cost", c.max_transaction_cost === null || c.max_transaction_cost === undefined + ? "no limit" : `${fmt.money(c.max_transaction_cost)} CHF`], + ["trading cost", `${fmt.bp(k.proportional_buy)} buy / ${fmt.bp(k.proportional_sell)} sell`], + ["fixed per trade", k.fixed_per_trade ? `${fmt.money(k.fixed_per_trade)} CHF` : "none"], + ]; +} + +/* ------------------------------------------------------------------ history */ +function historyCard(rerender) { + const runs = allRuns(); + if (!runs.length) return el("span"); + const rows = runs.map((run) => ({ + id: run.id, label: run.label, status: run.result.status, + pnl: run.result.expected_pnl, ret: run.result.expected_return, cvar: run.result.cvar, + cvar_w: run.result.cvar / run.result.wealth, n: run.result.n_positions, + turnover: run.result.turnover, solver: run.result.solver, secs: run.result.timings?.total_s, + hash: run.result.scenario_hash?.slice(0, 8) || "–", + })); + const frontier = chartBox(); + const box = card("Runs in this session", rows.length > 1 + ? "click a row to show that run and make it the one \u201cUse the shown result\u201d adopts; the chart is the risk/return trade-off of the runs" + : "click a row to show that run again", + el("div", {}, + el("div", { class: "btnbar", style: { marginBottom: "8px" } }, + button("Clear history", () => { clearRuns(); rerender(); }, "btn small")), + rows.length > 1 ? frontier : null, + dataTable([ + { key: "label", label: "Run" }, + { key: "status", label: "Status", cls: (r) => (r.status === "Optimal" ? "pos" : "muted") }, + { key: "pnl", label: "E[P&L]", num: true, fmt: (v) => fmt.money(v) }, + { key: "ret", label: "E[return]", num: true, fmt: (v) => fmt.pctSigned(v, 3) }, + { key: "cvar", label: "CVaR", num: true, fmt: (v) => fmt.money(v) }, + { key: "cvar_w", label: "CVaR of wealth", num: true, fmt: (v) => fmt.pct(v, 2) }, + { key: "n", label: "Positions", num: true, fmt: (v) => fmt.int(v) }, + { key: "turnover", label: "Turnover", num: true, fmt: (v) => fmt.pct(v, 1) }, + { key: "solver", label: "Solver" }, + { key: "secs", label: "Time", num: true, fmt: (v) => fmt.secs(v) }, + { key: "hash", label: "Scenario set", cls: "mono" }, + ], rows, { sortable: false, rowClass: (row) => (isSelected(row.id) ? "selected" : ""), + onRowClick: (row) => { selectRun(row.id); rerender(); } }))); + if (rows.length > 1) scatterChart(frontier, { + points: rows.filter((r) => r.status !== "Infeasible").map((r) => ({ x: r.cvar_w, y: r.ret, label: r.label })), + height: 240, xLabel: "CVaR of wealth", yLabel: "expected return over the horizon", + xFormat: (v) => fmt.pct(v, 1), yFormat: (v) => fmt.pct(v, 2), labelPoints: false, + }); + return box; +} diff --git a/quantcore/api/ui/js/tabs/risk.js b/quantcore/api/ui/js/tabs/risk.js new file mode 100644 index 0000000..1690739 --- /dev/null +++ b/quantcore/api/ui/js/tabs/risk.js @@ -0,0 +1,299 @@ +/* Tab 2 -- risk models: the covariance the optimiser sees and the scenarios it is given. */ + +import { + api, button, card, chartBox, dataTable, el, empty, field, fmt, kv, live, scenarioPayload, + segmented, select, setChildren, state, statTiles, warnBox, withBusy, +} from "../core.js"; +import { barChart, heatmap, histogram } from "../charts.js"; +import { info } from "../universe.js"; +import { selectedRun, sharesOf } from "../runs.js"; + +let covResult = null, scenResult = null, noise = null; +let basis = "cap"; // which portfolio the distribution is measured for +let focus = ""; // asset shown as a single-stock distribution +let space = "simple"; // 'simple' (price returns) | 'log' (the modelling space) + +export function render(node) { + node.replaceChildren(); + if (!state.selection.length) { node.appendChild(empty("select candidate stocks in the Stocks tab first")); return; } + node.append(covarianceCard(), scenarioCard()); +} + +/* ------------------------------------------------------------------ covariance / correlation */ +function covarianceCard() { + const out = el("div", { id: "risk-cov" }); + let btn; + const run = async () => { + await withBusy(btn, live("risk-cov", out), async () => { + covResult = await api.post("/model/covariance", { + symbols: state.selection, freq: state.model.freq, horizon: state.model.horizon, + covariance: scenarioPayload().covariance, + }); + drawCovariance(live("risk-cov", out), covResult); + }).catch(() => {}); + }; + btn = button("Estimate covariance", run, "btn primary"); + const body = el("div", {}, el("div", { class: "btnbar", style: { marginBottom: "10px" } }, btn, + el("span", { class: "muted", text: `${state.selection.length} stocks · ${state.model.covariance.method} · shrinkage ${state.model.covariance.shrinkage}` })), out); + if (covResult) drawCovariance(out, covResult); else out.appendChild(empty("estimate the covariance to see correlations, volatilities and conditioning")); + return card("Covariance model", "the estimator the scenario generator and the optimiser use", body); +} + +function drawCovariance(out, r) { + const c = r.conditioning; + const warnings = []; + if (c.rank_deficient) warnings.push(`The estimate is singular: ${c.effective_observations} effective observations for ${c.n_assets} assets. Some portfolios look riskless to the optimiser - use shrinkage 'auto' or a longer window.`); + else if (c.under_determined) warnings.push(`Fewer effective observations (${c.effective_observations}) than assets (${c.n_assets}); shrinkage of ${fmt.pct(c.shrinkage_applied, 0)} keeps the smallest eigenvalue at ${fmt.pct(c.min_eigenvalue_ratio, 1)} of the average variance.`); + + const labels = r.correlation.columns; + const vols = labels.map((s) => ({ label: s, values: [r.volatility_ann[s]] })) + .sort((a, b) => b.values[0] - a.values[0]).slice(0, 25); + const eig = r.eigenvalues.slice(0, Math.min(20, r.eigenvalues.length)) + .map((v, i) => ({ label: `${i + 1}`, values: [v] })); + + const hm = chartBox(), volBox = chartBox(), eigBox = chartBox(); + setChildren(out, + warnBox(warnings), + statTiles([ + { label: "Assets", value: fmt.int(c.n_assets) }, + { label: "Observations", value: fmt.int(r.observations) }, + { label: "Effective obs.", value: fmt.int(c.effective_observations), delta: `${state.model.covariance.method}` }, + { label: "Shrinkage", value: fmt.pct(r.shrinkage_applied, 1), delta: "towards the diagonal" }, + { label: "Mean correlation", value: fmt.num(r.correlation_summary.mean, 2), + delta: `${fmt.num(r.correlation_summary.min, 2)} … ${fmt.num(r.correlation_summary.max, 2)}` }, + { label: "Condition number", value: fmt.compact(c.condition_number) }, + { label: "Min eigenvalue", value: fmt.pct(c.min_eigenvalue_ratio, 2), delta: "of the average variance", + deltaClass: c.rank_deficient ? "neg" : "muted" }, + { label: "Horizon", value: `${r.horizon_periods} × ${r.freq.slice(0, 1)}` }, + ]), + el("div", { class: "grid two", style: { marginTop: "12px" } }, + card("Correlation", labels.length > 26 ? `${labels.length} stocks (labels omitted above 26)` : "of the model's returns", hm), + el("div", {}, + card("Volatility p.a.", "the 25 most volatile of the selection", volBox), + card("Eigenvalue spectrum", "share of total variance per principal component", eigBox))), + ); + heatmap(hm, { labels, matrix: r.correlation.data, domain: [-1, 1], valueName: "correlation" }); + barChart(volBox, { items: vols, valueFormat: (v) => fmt.pct(v, 0), horizontal: true, rowHeight: 18, + labelWidth: 84, valueName: "volatility p.a." }); + barChart(eigBox, { items: eig, valueFormat: (v) => fmt.pct(v, 0), horizontal: false, height: 190, + valueName: "share of variance", bottom: 26 }); +} + +/* ------------------------------------------------------------------ scenarios */ +function exposureFor(kind) { + const sel = state.selection; + const held = Object.entries(state.portfolio.holdings || {}) + .reduce((a, [sym, n]) => a + (info(sym)?.price || 0) * Math.round(Number(n) || 0), 0); + const wealth = (Number(state.portfolio.cash) || 0) + (state.portfolio.mode === "rebalance" ? held : 0) || 1e6; + if (kind === "holdings") return { holdings: state.portfolio.holdings, value: null }; + if (kind === "result") { + const r = selectedRun(); // the run shown in the Optimisation tab, not the newest + return r ? { holdings: sharesOf(r.result), value: null } : null; + } + if (kind === "equal") { + const w = Object.fromEntries(sel.map((s) => [s, 1 / sel.length])); + return { weights: w, value: wealth }; + } + const caps = sel.map((s) => info(s)?.market_cap || 0); + const total = caps.reduce((a, b) => a + b, 0) || 1; + return { weights: Object.fromEntries(sel.map((s, i) => [s, caps[i] / total])), value: wealth }; +} + +function scenarioCard() { + const out = el("div", { id: "risk-scen" }); + let btn, noiseBtn; + + const run = async () => { + await withBusy(btn, live("risk-scen", out), async () => { + const exp = exposureFor(basis); + if (!exp) throw new Error("no optimisation result yet - run one in the Optimisation tab"); + const body = scenarioPayload({ bins: 60, confidence: [0.95, 0.99], focus: focus ? [focus] : [] }); + if (exp.holdings) Object.assign(body, { holdings: exp.holdings, cash: 0 }); + else Object.assign(body, { weights: exp.weights, value: exp.value }); + scenResult = await api.post("/model/scenarios", body); + drawScenarios(live("risk-scen", out), scenResult); + }).catch(() => {}); + }; + + const noiseCheck = async () => { + await withBusy(noiseBtn, live("risk-scen", out), async () => { + const exp = exposureFor(basis); + const seeds = [0, 1, 2].map((d) => Number(state.model.seed) + d); + const results = []; + for (const seed of seeds) { + const body = scenarioPayload({ seed, bins: 20, confidence: [0.95, 0.99] }); + if (exp.holdings) Object.assign(body, { holdings: exp.holdings, cash: 0 }); + else Object.assign(body, { weights: exp.weights, value: exp.value }); + results.push(await api.post("/model/scenarios", body)); + } + noise = { seeds, results }; + drawScenarios(live("risk-scen", out), scenResult, noise); + }).catch(() => {}); + }; + + btn = button("Generate scenarios", run, "btn primary"); + noiseBtn = button("Sampling-noise check (3 seeds)", noiseCheck, "btn"); + + const body = el("div", {}, + el("div", { class: "row", style: { marginBottom: "10px" } }, + field("Measured portfolio", select([ + ["cap", "cap-weighted selection"], ["equal", "equally weighted selection"], + ["holdings", "current holdings"], ["result", "the optimisation result on screen"]], basis, (v) => { basis = v; run(); })), + field("Single-stock view", select([["", "none"], ...state.selection.map((s) => [s, s])], focus, (v) => { focus = v; run(); })), + el("div", { class: "field auto" }, el("label", { text: " " }), el("div", { class: "btnbar" }, btn, noiseBtn))), + out); + if (scenResult) drawScenarios(out, scenResult, noise); + else out.appendChild(empty("generate the scenario set to see the horizon distribution of the portfolio and of each stock")); + return card("Scenarios", `${state.model.scenario_model === "gaussian_exact" ? "GBM (the exact Gaussian mode draws no scenarios)" : state.model.scenario_model} · ${state.model.sampling} sampling · ${state.model.horizon} horizon`, body); +} + +function drawScenarios(out, r, noiseData) { + if (!r) return; + const d = r.description, p = r.portfolio; + const tiles = [ + { label: "Scenarios", value: fmt.int(d.n_scenarios), delta: d.weighted ? "probability weighted" : "equiprobable" }, + { label: "Effective sample", value: fmt.int(d.effective_sample_size), + delta: d.weighted ? `${fmt.pct(d.effective_sample_size / d.n_scenarios, 0)} of the draws` : "no tilt" }, + { label: "Scenario set", value: d.hash?.slice(0, 8) || "–", delta: "same inputs ⇒ same set", title: d.hash }, + { label: "Shocks", value: d.shock_hash?.slice(0, 8) || "–", delta: "drift removed" }, + ]; + if (p) { + tiles.push( + { label: "Expected P&L", value: fmt.money(p.expected_pnl), delta: fmt.pctSigned(p.expected_pnl / p.value, 2), + deltaClass: p.expected_pnl >= 0 ? "pos" : "neg" }, + { label: "P&L volatility", value: fmt.money(p.volatility) }, + { label: "VaR 95 %", value: fmt.money(p.risk["0.95"].var) }, + { label: "CVaR 95 %", value: fmt.money(p.risk["0.95"].cvar), delta: fmt.pct(p.risk["0.95"].cvar / p.value, 2) }, + { label: "CVaR 99 %", value: fmt.money(p.risk["0.99"].cvar), delta: fmt.pct(p.risk["0.99"].cvar / p.value, 2) }, + { label: "Skewness", value: fmt.num(p.skewness, 2), delta: `kurtosis ${fmt.num(p.kurtosis, 1)}` }); + } + + const hist = chartBox(280), assetBox = chartBox(240); + const parts = [statTiles(tiles)]; + + if (p) { + parts.push(el("div", { class: "grid two", style: { marginTop: "12px" } }, + card(`P&L over one ${state.model.horizon}`, `${basisLabel()} · ${fmt.compact(p.value)} CHF`, hist, + el("div", { class: "note", style: { marginTop: "6px" } }, + `Losses beyond the 95 % VaR are shaded; CVaR is their probability-weighted average.`)), + card("Quantiles of the P&L", "probability-weighted, in CHF", quantileTable(p)))); + } + if (r.asset_histograms && Object.keys(r.asset_histograms).length) { + const sym = Object.keys(r.asset_histograms)[0]; + parts.push(card(`${sym} — simple return over one ${state.model.horizon}`, info(sym)?.name || "", assetBox)); + } + if (noiseData) parts.push(noiseCard(noiseData)); + parts.push(summaryCard(r)); + parts.push(card("Provenance", "the inputs this scenario set depends on - any run with the same key reuses it", + kv(Object.entries(r.scenario_key || {}).map(([k, v]) => [ + k, Array.isArray(v) ? `${v.length}: ${v.slice(0, 8).join(", ")}${v.length > 8 ? " …" : ""}` + : typeof v === "object" && v !== null ? Object.entries(v).map(([a, b]) => `${a} ${b}`).join(", ") + : String(v)])))); + + setChildren(out, ...parts); + + if (p) { + histogram(hist, { + edges: p.histogram.edges, mass: p.histogram.mass, height: 280, + valueFormat: (v) => fmt.compact(v), tailFrom: -p.risk["0.95"].var, + mainLabel: "P&L outcomes", tailLabel: "worst 5 %", + markers: [ + { value: -p.risk["0.95"].var, label: `VaR 95 % ${fmt.compact(p.risk["0.95"].var)}` }, + { value: -p.risk["0.95"].cvar, label: `CVaR 95 % ${fmt.compact(p.risk["0.95"].cvar)}` }, + { value: p.expected_pnl, label: `mean ${fmt.compact(p.expected_pnl)}` }, + ], + }); + } + if (r.asset_histograms) { + const sym = Object.keys(r.asset_histograms)[0]; + if (sym) { + const h = r.asset_histograms[sym]; + histogram(assetBox, { edges: h.edges, mass: h.mass, height: 240, valueFormat: (v) => fmt.pct(v, 0), + mainLabel: `${sym} outcomes` }); + } + } +} + +function basisLabel() { + return { cap: "cap-weighted selection", equal: "equally weighted selection", holdings: "current holdings", + result: "the optimisation result on screen" }[basis]; +} + +function quantileTable(p) { + const rows = Object.entries(p.quantiles).map(([q, v]) => ({ q: `${(Number(q) * 100).toFixed(0)} %`, v, r: v / p.value })); + rows.push({ q: "mean", v: p.expected_pnl, r: p.expected_pnl / p.value }); + rows.push({ q: "worst", v: p.min, r: p.min / p.value }); + rows.push({ q: "best", v: p.max, r: p.max / p.value }); + return dataTable([ + { key: "q", label: "Quantile" }, + { key: "v", label: "P&L (CHF)", num: true, fmt: (v) => fmt.money(v), cls: (r) => (r.v >= 0 ? "pos" : "neg") }, + { key: "r", label: "of wealth", num: true, fmt: (v) => fmt.pctSigned(v, 2) }, + ], rows, { sortable: false }); +} + +/** Per-stock scenario table. Price returns by default: a log-return of -2.0 over a quarter is a + price return of -86 %, and only the latter is what a position can actually lose. */ +function summaryCard(r) { + const slot = el("div"); + const fill = () => setChildren(slot, summaryTable(r)); + fill(); + const head = el("div", { class: "row", style: { marginBottom: "8px" } }, + field("Shown as", segmented([["simple", "price returns"], ["log", "log-returns"]], space, + (v) => { space = v; fill(); }))); + return card("Scenario set per stock", + space === "simple" + ? `probability-weighted moments and quantiles of the horizon price return (e^x − 1, never below −100 %)` + : `probability-weighted moments and quantiles of the horizon log-return (the space the model works in)`, + head, slot); +} + +function summaryTable(r) { + const rows = r.summary.map((s) => ({ ...s, mu: r.expected_returns_ann?.[s.symbol] })); + const k = (name) => (space === "simple" ? `${name}_simple` : name); + const cols = [ + { key: "symbol", label: "Symbol", cls: "sym" }, + { key: "mu", label: "E[r] p.a.", num: true, fmt: (v) => fmt.pct(v, 1), title: "the expected-return model" }, + ]; + if (space === "simple") { + cols.push({ key: "expected_simple", label: "E[r] horizon", num: true, fmt: (v) => fmt.pct(v, 2), + title: "the model's expected price return over the horizon" }); + } + cols.push( + { key: k("mean"), label: space === "simple" ? "scenario mean" : "mean log", num: true, fmt: (v) => fmt.pct(v, 2) }, + { key: k("std"), label: "sd", num: true, fmt: (v) => fmt.pct(v, 2) }, + { key: k("q05"), label: "5 %", num: true, fmt: (v) => fmt.pct(v, 1) }, + { key: k("q50"), label: "median", num: true, fmt: (v) => fmt.pct(v, 2) }, + { key: k("q95"), label: "95 %", num: true, fmt: (v) => fmt.pct(v, 1) }, + { key: k("min"), label: "worst", num: true, fmt: (v) => fmt.pct(v, 1), cls: "neg" }, + { key: k("max"), label: "best", num: true, fmt: (v) => fmt.pct(v, 1), cls: "pos" }, + ); + return dataTable(cols, rows, { sortKey: k("std"), sortDir: -1 }); +} + +function noiseCard(nd) { + const rows = nd.results.map((r, i) => ({ + seed: nd.seeds[i], pnl: r.portfolio.expected_pnl, vol: r.portfolio.volatility, + cvar95: r.portfolio.risk["0.95"].cvar, cvar99: r.portfolio.risk["0.99"].cvar, hash: r.description.hash.slice(0, 8), + })); + const spread = (key) => { + const v = rows.map((r) => r[key]); + const mean = v.reduce((a, b) => a + b, 0) / v.length; + const sd = Math.sqrt(v.reduce((a, b) => a + (b - mean) ** 2, 0) / (v.length - 1)); + return { mean, sd, rel: sd / Math.abs(mean) }; + }; + const c95 = spread("cvar95"), c99 = spread("cvar99"); + return card("Sampling noise across seeds", "the same model and portfolio, three independent scenario sets", + el("div", {}, + statTiles([ + { label: "CVaR 95 % spread", value: fmt.pct(c95.rel, 2), delta: `sd ${fmt.money(c95.sd)} of ${fmt.money(c95.mean)}` }, + { label: "CVaR 99 % spread", value: fmt.pct(c99.rel, 2), delta: `sd ${fmt.money(c99.sd)} of ${fmt.money(c99.mean)}` }, + ]), + el("div", { style: { marginTop: "10px" } }, dataTable([ + { key: "seed", label: "Seed", num: true, fmt: (v) => fmt.int(v) }, + { key: "hash", label: "Scenario set", cls: "mono" }, + { key: "pnl", label: "E[P&L]", num: true, fmt: (v) => fmt.money(v) }, + { key: "vol", label: "sd of P&L", num: true, fmt: (v) => fmt.money(v) }, + { key: "cvar95", label: "CVaR 95 %", num: true, fmt: (v) => fmt.money(v) }, + { key: "cvar99", label: "CVaR 99 %", num: true, fmt: (v) => fmt.money(v) }, + ], rows, { sortable: false })))); +} diff --git a/quantcore/api/ui/js/tabs/stocks.js b/quantcore/api/ui/js/tabs/stocks.js new file mode 100644 index 0000000..54b9fc5 --- /dev/null +++ b/quantcore/api/ui/js/tabs/stocks.js @@ -0,0 +1,217 @@ +/* Tab 1 -- stock data: pick the candidate stocks and look at what the models are fed. */ + +import { + api, button, card, changed, chartBox, dataTable, el, empty, errorBox, field, fmt, input, + live, parseDate, segmented, select, setChildren, state, withBusy, +} from "../core.js"; +import { barChart, lineChart, scatterChart } from "../charts.js"; +import { filterRows, info, universe } from "../universe.js"; + +const RANGES = [["1y", 1], ["3y", 3], ["5y", 5], ["10y", 10], ["max", 0]]; +let range = "5y"; +let analysis = null; // {prices, risk, perf} of the last fetch + +export async function render(node) { + node.replaceChildren(el("div", { class: "empty" }, el("span", { class: "spinner" }), document.createTextNode(" loading the universe …"))); + let uni; + try { uni = await universe(); } catch (err) { node.replaceChildren(errorBox(err)); return; } + node.replaceChildren(); + node.append(pickerCard(uni), analysisCard()); +} + +/* ------------------------------------------------------------------ stock picker */ +function pickerCard(uni) { + const u = state.universe; + const body = el("div"); + const tableSlot = el("div"); + const summary = el("div", { class: "row", style: { marginBottom: "8px" } }); + + const draw = () => { + const rows = filterRows(uni.rows, { region: u.region, sector: u.sector, country: u.country, search: u.search }); + const selected = new Set(state.selection); + summary.replaceChildren( + el("span", { class: "muted", text: `${rows.length} of ${uni.rows.length} stocks match · ${selected.size} selected` }), + el("span", { class: "spacer", style: { marginLeft: "auto" } }), + el("div", { class: "btnbar" }, + button(`Select top ${u.top}`, () => { + state.selection = rows.slice(0, Number(u.top)).map((r) => r.symbol); + changed("selection", "stocks"); draw(); + }, "btn small primary"), + button("Add all matching", () => { + const add = rows.slice(0, 500).map((r) => r.symbol); + state.selection = [...new Set([...state.selection, ...add])]; + changed("selection", "stocks"); draw(); + }, "btn small"), + button("Clear", () => { state.selection = []; changed("selection", "stocks"); draw(); }, "btn small"))); + + tableSlot.replaceChildren(dataTable([ + { key: "selected", label: "", render: (r) => { + const box = el("input", { type: "checkbox" }); + box.checked = selected.has(r.symbol); + box.addEventListener("change", () => { + state.selection = box.checked + ? [...new Set([...state.selection, r.symbol])] + : state.selection.filter((s) => s !== r.symbol); + changed("selection", "stocks"); + draw(); + }); + return box; + } }, + { key: "symbol", label: "Symbol", cls: "sym" }, + { key: "name", label: "Name", cls: "name" }, + { key: "sector", label: "Sector" }, + { key: "country", label: "Country" }, + { key: "market_cap", label: "Market cap (CHF)", num: true, fmt: (v) => fmt.compact(v) }, + { key: "market_weight", label: "Weight", num: true, fmt: (v) => fmt.pct(v, 2) }, + { key: "price", label: "Price (CHF)", num: true, fmt: (v) => fmt.money2(v) }, + { key: "price_coverage", label: "Coverage", num: true, fmt: (v) => fmt.pct(v, 1) }, + ], rows, { sortKey: "market_cap", sortDir: -1, limit: 400 })); + }; + + const sectors = ["", ...Object.keys(uni.facets.sectors)]; + const countries = ["", ...Object.keys(uni.facets.countries).sort()]; + body.append( + el("div", { class: "row" }, + field("Region", select([["", "world"], ...Object.keys(uni.facets.regions).map((r) => [r, r])], u.region, + (v) => { u.region = v; changed("universe", "stocks"); draw(); })), + field("Sector", select(sectors.map((s) => [s, s || "all sectors"]), u.sector, + (v) => { u.sector = v; changed("universe", "stocks"); draw(); })), + field("Country", select(countries.map((s) => [s, s || "all countries"]), u.country || "", + (v) => { u.country = v; changed("universe", "stocks"); draw(); })), + field("Search", input("search", u.search, (v) => { u.search = v; changed("universe", "stocks"); draw(); }, + { placeholder: "symbol or name" }))), + summary, tableSlot); + draw(); + return card("Investment universe", `${uni.n_universe} stocks, market caps as of ${fmt.date(uni.as_of)}`, body); +} + +/* ------------------------------------------------------------------ time series and statistics */ +function analysisCard() { + const body = el("div"); + const out = el("div", { id: "stocks-analysis" }); + const controls = el("div", { class: "row", style: { marginBottom: "10px" } }); + let btn; + + const run = async () => { + const symbols = state.selection; + const panel = live("stocks-analysis", out); + if (!symbols.length) { panel.replaceChildren(empty("select stocks above to see prices, risk and performance")); return; } + await withBusy(btn, panel, async () => { + const years = RANGES.find(([k]) => k === range)?.[1] ?? 5; + const start = years ? new Date(Date.now() - years * 365.25 * 864e5).toISOString().slice(0, 10) : null; + const list = symbols.slice(0, 150).join(","); + const [prices, risk, perf] = await Promise.all([ + api.get("/prices", { symbols: list, start, freq: state.data.freq, currency: "base" }), + api.post("/risk/assets", { symbols: symbols.slice(0, 150), freq: state.data.freq, horizon: state.model.horizon, + start, currency: "base", space: "value", drift: "zero" }), + api.post("/performance/assets", { symbols: symbols.slice(0, 150), freq: state.data.freq, start, currency: "base" }), + ]); + analysis = { prices, risk, perf }; + drawAnalysis(live("stocks-analysis", panel), prices, risk, perf); + }).catch(() => {}); + }; + + btn = button("Load data", run, "btn primary"); + controls.append( + field("History", segmented(RANGES.map(([k]) => [k, k]), range, (v) => { range = v; run(); })), + field("Frequency", select([["daily", "daily"], ["weekly", "weekly"], ["monthly", "monthly"]], state.data.freq, + (v) => { state.data.freq = v; changed("data", "stocks"); run(); })), + el("div", { class: "field auto" }, el("label", { text: " " }), btn)); + body.append(controls, out); + out.appendChild(empty("select stocks above, then load the data")); + if (state.selection.length && analysis) drawAnalysis(out, analysis.prices, analysis.risk, analysis.perf); + return card("Time series, risk and performance", "prices in CHF; log-returns; the window is the range above", body); +} + +function drawAnalysis(out, prices, risk, perf) { + const { index, columns, data } = prices; + const x = index.map(parseDate); + const shown = [...columns].sort((a, b) => (info(b)?.market_cap ?? 0) - (info(a)?.market_cap ?? 0)).slice(0, 8); + + // indexed prices (a common base is the only honest way to put several stocks on one axis) + const series = shown.map((c) => { + const j = columns.indexOf(c); + const col = data.map((row) => row[j]); + const base = col.find((v) => Number.isFinite(v)) ?? 1; + return { name: c, points: col.map((v, i) => [x[i], Number.isFinite(v) ? (v / base) * 100 : NaN]) }; + }); + const dd = shown.map((c) => { + const j = columns.indexOf(c); + let peak = -Infinity; + const pts = []; + data.forEach((row, i) => { + const v = row[j]; + if (!Number.isFinite(v)) return; + peak = Math.max(peak, v); + pts.push([x[i], v / peak - 1]); + }); + return { name: c, points: pts }; + }); + + const perfBy = Object.fromEntries(perf.map((p) => [p.symbol, p])); + const riskBy = Object.fromEntries(risk.map((p) => [p.symbol, p])); + const caps = state.selection.map((s) => ({ label: s, values: [info(s)?.market_cap ?? NaN] })) + .filter((r) => Number.isFinite(r.values[0])).sort((a, b) => b.values[0] - a.values[0]).slice(0, 20); + + const priceChart = chartBox(260), ddChart = chartBox(220), capChart = chartBox(), scatter = chartBox(280); + setChildren(out, + el("div", { class: "grid two" }, + card("Price, indexed to 100", `${shown.length} of ${columns.length} stocks · ${index.length} ${state.data.freq} observations`, priceChart), + card("Drawdown from the running peak", "same stocks", ddChart)), + el("div", { class: "grid two" }, + card("Risk and return", `annualised, from ${fmt.date(index[0])}`, scatter), + card("Market capitalisation", "the 20 largest of the selection, CHF", capChart)), + card("Per-stock statistics", + `risk over a ${state.model.horizon} horizon, ${state.data.freq} data; VaR and CVaR as a share of the ` + + `position's value, volatility only (no drift extrapolated into the horizon)`, + statsTable(state.selection, riskBy, perfBy)), + ); + + lineChart(priceChart, { series, height: 260, xFormat: (v) => new Date(v).toISOString().slice(0, 7), + yFormat: (v) => fmt.num(v, 0) }); + lineChart(ddChart, { series: dd, height: 220, xFormat: (v) => new Date(v).toISOString().slice(0, 7), + yFormat: (v) => fmt.pct(v, 0), area: dd.length === 1 }); + barChart(capChart, { items: caps, valueFormat: (v) => fmt.compact(v), horizontal: true, rowHeight: 20, + labelWidth: 84, valueName: "market cap" }); + scatterChart(scatter, { + points: state.selection.filter((s) => perfBy[s]).map((s) => ({ + x: perfBy[s].annualised_volatility, y: perfBy[s].annualised_return, label: s })), + height: 280, xLabel: "volatility p.a.", yLabel: "return p.a.", + xFormat: (v) => fmt.pct(v, 0), yFormat: (v) => fmt.pct(v, 0), labelPoints: state.selection.length <= 14, + }); +} + +function statsTable(symbols, riskBy, perfBy) { + const rows = symbols.filter((s) => riskBy[s] || perfBy[s]).map((s) => ({ + symbol: s, + vol: perfBy[s]?.annualised_volatility, + vol_ewm: riskBy[s]?.vol_ewm_ann, + ret: perfBy[s]?.annualised_return, + sharpe: perfBy[s]?.sharpe, + mdd: perfBy[s]?.max_drawdown, + skew: perfBy[s]?.skewness, + kurt: perfBy[s]?.excess_kurtosis, + var95: riskBy[s]?.var95_normal_ewm, + cvar95: riskBy[s]?.cvar95_normal_ewm, + cvar95_hist: riskBy[s]?.cvar95_hist, + cvar99_hist: riskBy[s]?.cvar99_hist, + n: perfBy[s]?.n_obs, + })); + return dataTable([ + { key: "symbol", label: "Symbol", cls: "sym" }, + { key: "ret", label: "Return p.a.", num: true, fmt: (v) => fmt.pct(v, 1), cls: (r) => (r.ret >= 0 ? "pos" : "neg") }, + { key: "vol", label: "Vol p.a.", num: true, fmt: (v) => fmt.pct(v, 1) }, + { key: "vol_ewm", label: "Vol p.a. (EWM)", num: true, fmt: (v) => fmt.pct(v, 1), + title: "exponentially weighted: the volatility the models use today" }, + { key: "sharpe", label: "Sharpe", num: true, fmt: (v) => fmt.num(v, 2) }, + { key: "mdd", label: "Max drawdown", num: true, fmt: (v) => fmt.pct(v, 0), cls: "neg" }, + { key: "skew", label: "Skew", num: true, fmt: (v) => fmt.num(v, 2) }, + { key: "kurt", label: "Excess kurtosis", num: true, fmt: (v) => fmt.num(v, 1) }, + { key: "var95", label: "VaR 95 %", num: true, fmt: (v) => fmt.pct(v, 2), + title: "normal, EWM volatility, over the horizon; the loss as a share of the position's value" }, + { key: "cvar95", label: "CVaR 95 %", num: true, fmt: (v) => fmt.pct(v, 2) }, + { key: "cvar95_hist", label: "CVaR 95 % hist.", num: true, fmt: (v) => fmt.pct(v, 2), title: "empirical, same horizon" }, + { key: "cvar99_hist", label: "CVaR 99 % hist.", num: true, fmt: (v) => fmt.pct(v, 2) }, + { key: "n", label: "Obs.", num: true, fmt: (v) => fmt.int(v) }, + ], rows, { sortKey: "vol", sortDir: -1 }); +} diff --git a/quantcore/api/ui/js/tabs/views.js b/quantcore/api/ui/js/tabs/views.js new file mode 100644 index 0000000..154aa55 --- /dev/null +++ b/quantcore/api/ui/js/tabs/views.js @@ -0,0 +1,172 @@ +/* Tab 3 -- expected returns: the implied (equilibrium) returns, the views and the posterior. */ + +import { + api, button, card, changed, chartBox, dataTable, el, empty, field, fmt, input, live, select, setChildren, + state, statTiles, viewPayload, withBusy, +} from "../core.js"; +import { barChart } from "../charts.js"; +import { info } from "../universe.js"; + +let result = null; + +export function render(node) { + node.replaceChildren(); + if (!state.selection.length) { node.appendChild(empty("select candidate stocks in the Stocks tab first")); return; } + node.append(viewsCard(), resultCard()); +} + +/* ------------------------------------------------------------------ the views */ +function viewsCard() { + const body = el("div"); + const list = el("div"); + + const drawList = () => { + list.replaceChildren(); + if (!state.views.length) { list.appendChild(el("div", { class: "note", text: "No views: the posterior equals the market-implied (equilibrium) returns." })); return; } + list.appendChild(dataTable([ + { key: "label", label: "View", render: (v) => document.createTextNode(describe(v)) }, + { key: "confidence", label: "Confidence", num: true, fmt: (v) => fmt.pct(v, 0) }, + { key: "del", label: "", render: (v) => button("remove", () => { + state.views = state.views.filter((x) => x !== v); changed("views", "views"); drawList(); }, "btn small") }, + ], state.views, { sortable: false })); + }; + drawList(); + + const draft = { kind: "sentiment", symbol: state.selection[0] || "", sentiment: 0.5, confidence: 0.5, strength: 0.25, + assets: "", q: 0.1 }; + const form = el("div"); + const drawForm = () => { + form.replaceChildren( + el("div", { class: "row" }, + field("Kind", select([["sentiment", "sentiment on one stock"], ["explicit", "explicit return view"]], draft.kind, + (v) => { draft.kind = v; drawForm(); })), + draft.kind === "sentiment" + ? field("Stock", select(state.selection.map((s) => [s, `${s} — ${info(s)?.name || ""}`.slice(0, 44)]), draft.symbol, + (v) => { draft.symbol = v; })) + : field("Portfolio of the view", input("text", draft.assets, (v) => { draft.assets = v; }, + { placeholder: "AAPL:1, MSFT:-1" }), "weights of a long/short view portfolio"), + draft.kind === "sentiment" + ? field("Sentiment −1 … +1", input("number", draft.sentiment, (v) => { draft.sentiment = v; }, + { min: -1, max: 1, step: 0.1 })) + : field("Expected return p.a.", input("number", draft.q, (v) => { draft.q = v; }, { step: 0.01 }), + "as a fraction, e.g. 0.12"), + field("Confidence", input("number", draft.confidence, (v) => { draft.confidence = v; }, + { min: 0.01, max: 1, step: 0.05 }), "Idzorek: 1 = fully trust the view"), + draft.kind === "sentiment" + ? field("Strength", input("number", draft.strength, (v) => { draft.strength = v; }, { min: 0, step: 0.05 }), + "return added at sentiment +1") + : null, + el("div", { class: "field auto" }, el("label", { text: " " }), button("Add view", () => { + const v = draft.kind === "sentiment" + ? { kind: "sentiment", symbol: draft.symbol, sentiment: Number(draft.sentiment), + confidence: Number(draft.confidence), strength: Number(draft.strength) } + : { kind: "explicit", assets: parseAssets(draft.assets), q: Number(draft.q), confidence: Number(draft.confidence) }; + if (v.kind === "explicit" && !Object.keys(v.assets).length) { alert("give the view portfolio as SYMBOL:weight pairs"); return; } + state.views = [...state.views, v]; + changed("views", "views"); drawList(); + }, "btn primary")))); + }; + drawForm(); + + body.append(list, el("div", { style: { marginTop: "10px" } }, form)); + return card("Views", "Black-Litterman: a view moves the posterior away from the market-implied return, by as much as its confidence allows", body); +} + +function describe(v) { + if (v.kind === "sentiment") return `${v.symbol}: sentiment ${v.sentiment > 0 ? "+" : ""}${v.sentiment} (strength ${fmt.pct(v.strength, 0)})`; + const parts = Object.entries(v.assets).map(([s, w]) => `${w > 0 ? "+" : ""}${w} ${s}`).join(" "); + return `${parts} → ${fmt.pct(v.q, 1)} p.a.`; +} + +function parseAssets(text) { + const out = {}; + for (const part of String(text).split(/[,;]/)) { + const [s, w] = part.split(":").map((x) => x.trim()); + if (s && w !== undefined && !Number.isNaN(Number(w))) out[s.toUpperCase()] = Number(w); + } + return out; +} + +/* ------------------------------------------------------------------ the model output */ +function resultCard() { + const out = el("div", { id: "bl-result" }); + let btn; + const run = async () => { + await withBusy(btn, live("bl-result", out), async () => { + result = await api.post("/model/expected-returns", { + symbols: state.selection, max_assets: state.selection.length, views: viewPayload(), + tau: Number(state.model.bl_tau), years: Number(state.model.bl_years), + omega_method: state.model.bl_omega_method || "idzorek", + }); + draw(live("bl-result", out), result); + }).catch(() => {}); + }; + btn = button("Compute expected returns", run, "btn primary"); + const body = el("div", {}, + el("div", { class: "row", style: { marginBottom: "10px" } }, + field("Ω (view uncertainty)", select([["idzorek", "Idzorek (from the confidence)"], ["he_litterman", "He-Litterman"]], + state.model.bl_omega_method || "idzorek", (v) => { state.model.bl_omega_method = v; changed("model", "views"); run(); })), + field("τ", input("number", state.model.bl_tau, (v) => { state.model.bl_tau = v; changed("model", "views"); }, { step: 0.01, min: 0.001 })), + field("History (years)", input("number", state.model.bl_years, (v) => { state.model.bl_years = v; changed("model", "views"); }, { min: 1, max: 20 })), + el("div", { class: "field auto" }, el("label", { text: " " }), btn)), + out); + if (result) draw(out, result); else out.appendChild(empty("compute the model to see implied and posterior expected returns")); + return card("Implied and posterior expected returns", "reverse optimisation of the cap-weighted market, then the views", body); +} + +function draw(out, r) { + const symbols = Object.keys(r.prior_ann); + const rows = symbols.map((s) => ({ + symbol: s, name: info(s)?.name || s, weight: r.market_weights[s], vol: r.volatility_ann[s], + prior: r.prior_ann[s], post: r.posterior_ann[s], change: r.posterior_ann[s] - r.prior_ann[s], + sharpe: r.volatility_ann[s] ? (r.posterior_ann[s] - r.risk_free_ann) / r.volatility_ann[s] : null, + })); + const moved = [...rows].sort((a, b) => Math.abs(b.change) - Math.abs(a.change)); + const top = moved.slice(0, 20); + const chart = chartBox(), changeChart = chartBox(); + + setChildren(out, + statTiles([ + { label: "Market return p.a.", value: fmt.pct(r.market_return_ann, 2), delta: "arithmetic, cap-weighted index" }, + { label: "Market volatility", value: fmt.pct(r.market_volatility_ann, 1) }, + { label: "Implied risk aversion δ", value: fmt.num(r.delta, 2) }, + { label: "τ", value: fmt.num(r.tau, 3) }, + { label: "Risk-free", value: fmt.pct(r.risk_free_ann, 2) }, + { label: "Views", value: fmt.int(r.views.length), delta: r.omega_method }, + ]), + el("div", { class: "grid two", style: { marginTop: "12px" } }, + card("Implied vs posterior", `the ${top.length} stocks the views move most`, chart), + card("Change from the views", "posterior − implied, p.a.", changeChart)), + r.views.length + ? card("How the views were translated", "q is the view's expected return p.a.; ω its variance (smaller = more trusted)", + dataTable([ + { key: "label", label: "View" }, + { key: "q_ann", label: "q p.a.", num: true, fmt: (v) => fmt.pct(v, 2) }, + { key: "confidence", label: "Confidence", num: true, fmt: (v) => fmt.pct(v, 0) }, + { key: "omega", label: "ω", num: true, fmt: (v) => v?.toExponential(2) }, + ], r.views, { sortable: false })) + : null, + card("Per-stock expected returns", "implied = what the market's weights say; posterior = after the views", + dataTable([ + { key: "symbol", label: "Symbol", cls: "sym" }, + { key: "name", label: "Name", cls: "name" }, + { key: "weight", label: "Market weight", num: true, fmt: (v) => fmt.pct(v, 2) }, + { key: "vol", label: "Vol p.a.", num: true, fmt: (v) => fmt.pct(v, 1) }, + { key: "prior", label: "Implied p.a.", num: true, fmt: (v) => fmt.pct(v, 2) }, + { key: "post", label: "Posterior p.a.", num: true, fmt: (v) => fmt.pct(v, 2) }, + { key: "change", label: "Change", num: true, fmt: (v) => fmt.pctSigned(v, 2), + cls: (row) => (Math.abs(row.change) < 1e-9 ? "muted" : row.change > 0 ? "pos" : "neg") }, + { key: "sharpe", label: "Return / vol", num: true, fmt: (v) => fmt.num(v, 2) }, + ], rows, { sortKey: "weight", sortDir: -1 })), + ); + + barChart(chart, { + items: top.map((r2) => ({ label: r2.symbol, values: [r2.prior, r2.post] })), + groups: ["implied", "posterior"], valueFormat: (v) => fmt.pct(v, 0), horizontal: true, rowHeight: 24, labelWidth: 84, + }); + barChart(changeChart, { + items: top.map((r2) => ({ label: r2.symbol, values: [r2.change] })), + valueFormat: (v) => fmt.pctSigned(v, 1), horizontal: true, rowHeight: 24, labelWidth: 84, diverging: true, + valueName: "change", + }); +} diff --git a/quantcore/api/ui/js/universe.js b/quantcore/api/ui/js/universe.js new file mode 100644 index 0000000..a0807c4 --- /dev/null +++ b/quantcore/api/ui/js/universe.js @@ -0,0 +1,39 @@ +/* The investable universe, fetched once and filtered in the browser (745 rows). */ + +import { api } from "./core.js"; + +let cache = null; + +export async function universe() { + if (!cache) { + const raw = await api.get("/data/symbols", { limit: 2000 }); + const bySymbol = new Map(raw.symbols.map((s) => [s.symbol, s])); + cache = { ...raw, rows: raw.symbols, bySymbol }; + } + return cache; +} + +export function clearUniverse() { cache = null; } + +export function info(symbol) { return cache?.bySymbol.get(symbol) || null; } + +export function name(symbol) { return info(symbol)?.name || symbol; } + +export function sector(symbol) { return info(symbol)?.sector || "Unknown"; } + +export function price(symbol) { return info(symbol)?.price ?? null; } + +/** Filter the cached rows. `{region, sector, country, search}` -- all optional, all case-insensitive. */ +export function filterRows(rows, f = {}) { + const q = (f.search || "").trim().toLowerCase(); + return rows.filter((r) => + (!f.region || r.region === f.region) && + (!f.sector || (r.sector || "Unknown") === f.sector) && + (!f.country || r.country === f.country) && + (!q || r.symbol.toLowerCase().includes(q) || (r.name || "").toLowerCase().includes(q))); +} + +/** The `n` largest by market capitalisation under the filter. */ +export function topSymbols(rows, f, n) { + return filterRows(rows, f).slice(0, n).map((r) => r.symbol); +} diff --git a/quantcore/api/ui/styles.css b/quantcore/api/ui/styles.css new file mode 100644 index 0000000..a161b85 --- /dev/null +++ b/quantcore/api/ui/styles.css @@ -0,0 +1,254 @@ +/* quantcore test bench -- design tokens and layout. + Colours: the validated reference palette (categorical slots 1-8, a blue sequential ramp and a + blue<->red diverging pair with a neutral grey midpoint). Dark mode is a selected set of steps + for the dark surface, not an automatic flip. */ + +:root { + color-scheme: light; + --surface-0: #f2f1ee; /* page */ + --surface-1: #fcfcfb; /* card / chart surface */ + --surface-2: #f6f5f2; /* inset: table heads, wells */ + --border: #e3e2dd; + --border-strong: #cfcec8; + --text-primary: #14130f; + --text-secondary: #52514e; + --text-muted: #85837c; + + --series-1: #2a78d6; --series-2: #eb6834; --series-3: #1baf7a; --series-4: #eda100; + --series-5: #e87ba4; --series-6: #008300; --series-7: #4a3aa7; --series-8: #e34948; + + --seq-100: #cde2fb; --seq-250: #86b6ef; --seq-400: #3987e5; --seq-550: #1c5cab; --seq-700: #0d366b; + --div-pos: #2a78d6; /* diverging: cool pole */ + --div-neg: #e34948; /* diverging: warm pole */ + --div-mid: #f0efec; /* neutral midpoint */ + + --good: #0f7a4a; --warning: #b26a00; --serious: #c2410c; --critical: #c02626; + --accent: #2a78d6; + --shadow: 0 1px 2px rgba(20, 19, 15, .05), 0 4px 16px rgba(20, 19, 15, .05); + --radius: 10px; + --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; + --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif; +} + +@media (prefers-color-scheme: dark) { + :root:where(:not([data-theme="light"])) { + color-scheme: dark; + --surface-0: #121211; --surface-1: #1a1a19; --surface-2: #232321; + --border: #333331; --border-strong: #46453f; + --text-primary: #ffffff; --text-secondary: #c3c2b7; --text-muted: #8f8e85; + --series-1: #3987e5; --series-2: #d95926; --series-3: #199e70; --series-4: #c98500; + --series-5: #d55181; --series-6: #008300; --series-7: #9085e9; --series-8: #e66767; + --seq-100: #0d366b; --seq-250: #184f95; --seq-400: #256abf; --seq-550: #3987e5; --seq-700: #9ec5f4; + --div-pos: #3987e5; --div-neg: #e66767; --div-mid: #383835; + --good: #37b57d; --warning: #d9a02c; --serious: #e07a3c; --critical: #e66767; + --accent: #3987e5; + --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 4px 16px rgba(0, 0, 0, .3); + } +} +:root[data-theme="dark"] { + color-scheme: dark; + --surface-0: #121211; --surface-1: #1a1a19; --surface-2: #232321; + --border: #333331; --border-strong: #46453f; + --text-primary: #ffffff; --text-secondary: #c3c2b7; --text-muted: #8f8e85; + --series-1: #3987e5; --series-2: #d95926; --series-3: #199e70; --series-4: #c98500; + --series-5: #d55181; --series-6: #008300; --series-7: #9085e9; --series-8: #e66767; + --seq-100: #0d366b; --seq-250: #184f95; --seq-400: #256abf; --seq-550: #3987e5; --seq-700: #9ec5f4; + --div-pos: #3987e5; --div-neg: #e66767; --div-mid: #383835; + --good: #37b57d; --warning: #d9a02c; --serious: #e07a3c; --critical: #e66767; + --accent: #3987e5; + --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 4px 16px rgba(0, 0, 0, .3); +} + +* { box-sizing: border-box; } +html, body { height: 100%; } +body { + margin: 0; background: var(--surface-0); color: var(--text-primary); + font-family: var(--sans); font-size: 13px; line-height: 1.45; + -webkit-font-smoothing: antialiased; +} +h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -.01em; } +h2 { font-size: 15px; } +h3 { font-size: 13px; } +a { color: var(--accent); } + +/* ---------------------------------------------------------------- app frame */ +.app { display: flex; flex-direction: column; min-height: 100vh; } + +.topbar { + display: flex; align-items: center; gap: 16px; padding: 10px 18px; + background: var(--surface-1); border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 30; +} +.brand { display: flex; align-items: baseline; gap: 9px; } +.brand b { font-size: 15px; letter-spacing: -.02em; } +.brand span { color: var(--text-muted); font-size: 11px; } +.status-row { display: flex; gap: 6px; flex-wrap: wrap; margin-left: auto; align-items: center; } + +.pill { + display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px; border-radius: 999px; + background: var(--surface-2); border: 1px solid var(--border); font-size: 11px; color: var(--text-secondary); + white-space: nowrap; +} +.pill b { color: var(--text-primary); font-weight: 600; } +.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text-muted); flex: none; } +.dot.ok { background: var(--good); } .dot.warn { background: var(--warning); } .dot.bad { background: var(--critical); } + +.tabs { display: flex; gap: 2px; padding: 0 18px; background: var(--surface-1); border-bottom: 1px solid var(--border); + position: sticky; top: 49px; z-index: 29; overflow-x: auto; } +.tabs button { + appearance: none; background: none; border: 0; border-bottom: 2px solid transparent; cursor: pointer; + padding: 9px 13px; font: inherit; font-weight: 500; color: var(--text-secondary); white-space: nowrap; +} +.tabs button:hover { color: var(--text-primary); } +.tabs button[aria-selected="true"] { color: var(--text-primary); border-bottom-color: var(--accent); } + +.body { display: flex; align-items: flex-start; gap: 0; flex: 1; min-height: 0; } +.sidebar { + width: 302px; flex: none; background: var(--surface-1); border-right: 1px solid var(--border); + padding: 14px; position: sticky; top: 84px; max-height: calc(100vh - 84px); overflow-y: auto; +} +.main { flex: 1; min-width: 0; padding: 16px 18px 48px; } +.tabpanel[hidden] { display: none; } + +/* ---------------------------------------------------------------- cards */ +.card { + background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--radius); + padding: 14px; margin-bottom: 14px; box-shadow: var(--shadow); +} +.card > header { display: flex; align-items: baseline; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; } +.card > header .sub { color: var(--text-muted); font-size: 11px; } +.card > header .spacer { margin-left: auto; } +.grid { display: grid; gap: 14px; } +.grid.two { grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); align-items: start; } +.grid.three { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); } +@media (max-width: 900px) { .body { flex-direction: column; } .sidebar { width: 100%; position: static; max-height: none; border-right: 0; border-bottom: 1px solid var(--border); } } + +/* ---------------------------------------------------------------- forms */ +.field { display: flex; flex-direction: column; gap: 3px; margin-bottom: 9px; min-width: 0; } +.field > label { font-size: 11px; color: var(--text-secondary); font-weight: 500; } +.field .hint { font-size: 10.5px; color: var(--text-muted); } +input, select, textarea, button { font: inherit; font-size: 12.5px; color: var(--text-primary); } +input[type="text"], input[type="number"], input[type="date"], input[type="search"], select, textarea { + background: var(--surface-1); border: 1px solid var(--border-strong); border-radius: 7px; + padding: 5px 8px; width: 100%; min-width: 0; +} +input:focus-visible, select:focus-visible, textarea:focus-visible, button:focus-visible { + outline: 2px solid var(--accent); outline-offset: 1px; +} +input[type="number"] { font-variant-numeric: tabular-nums; } +.row { display: flex; gap: 8px; align-items: flex-end; flex-wrap: wrap; } +.row > .field { flex: 1; } +.row > .field.auto { flex: 0 0 auto; } +.row > .field.auto > .btnbar { height: 27px; } +.check { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-secondary); } +.check input { width: auto; } + +button.btn { + background: var(--surface-2); border: 1px solid var(--border-strong); border-radius: 7px; + padding: 5px 11px; cursor: pointer; font-weight: 500; white-space: nowrap; +} +button.btn:hover { border-color: var(--text-muted); } +button.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; } +button.btn.primary:hover { filter: brightness(1.07); } +button.btn.small { padding: 2px 7px; font-size: 11px; } +button.btn:disabled { opacity: .5; cursor: not-allowed; } +.btnbar { display: flex; gap: 7px; flex-wrap: wrap; align-items: center; } + +/* sidebar sections */ +details.section { border-top: 1px solid var(--border); padding: 9px 0; } +details.section:first-of-type { border-top: 0; } +details.section > summary { + cursor: pointer; font-weight: 600; font-size: 12px; list-style: none; display: flex; align-items: center; gap: 7px; + color: var(--text-primary); padding: 2px 0; +} +details.section > summary::-webkit-details-marker { display: none; } +details.section > summary::before { + content: ""; width: 0; height: 0; border-left: 4px solid var(--text-muted); + border-top: 3.5px solid transparent; border-bottom: 3.5px solid transparent; transition: transform .12s; +} +details.section[open] > summary::before { transform: rotate(90deg); } +details.section > summary .count { margin-left: auto; color: var(--text-muted); font-weight: 400; font-size: 11px; } +details.section > div { padding-top: 9px; } + +/* chips */ +.chips { display: flex; flex-wrap: wrap; gap: 5px; } +.chip { + display: inline-flex; align-items: center; gap: 5px; background: var(--surface-2); + border: 1px solid var(--border); border-radius: 999px; padding: 2px 4px 2px 9px; font-size: 11px; + font-family: var(--mono); +} +.chip button { appearance: none; background: none; border: 0; cursor: pointer; color: var(--text-muted); + font-size: 13px; line-height: 1; padding: 0 3px; border-radius: 50%; } +.chip button:hover { color: var(--critical); } + +/* ---------------------------------------------------------------- tables */ +.table-wrap { overflow: auto; max-height: 460px; border: 1px solid var(--border); border-radius: 8px; } +table.data { border-collapse: separate; border-spacing: 0; width: 100%; font-size: 12px; } +table.data th, table.data td { padding: 5px 9px; text-align: left; white-space: nowrap; border-bottom: 1px solid var(--border); } +table.data thead th { + position: sticky; top: 0; background: var(--surface-2); z-index: 2; font-weight: 600; font-size: 11px; + color: var(--text-secondary); cursor: default; border-bottom: 1px solid var(--border-strong); +} +table.data thead th.sortable { cursor: pointer; user-select: none; } +table.data thead th.sortable:hover { color: var(--text-primary); } +table.data thead th .arrow { color: var(--text-muted); font-size: 9px; } +table.data td.num, table.data th.num { text-align: right; font-variant-numeric: tabular-nums; font-family: var(--mono); font-size: 11.5px; } +table.data tbody tr:hover td { background: var(--surface-2); } +/* the row whose result is on screen: an accent edge plus a tint, so it reads without colour alone */ +table.data tbody tr.selected td { background: var(--surface-2); font-weight: 600; } +table.data tbody tr.selected td:first-child { box-shadow: inset 3px 0 0 var(--accent); } +table.data tbody tr:last-child td { border-bottom: 0; } +table.data td.sym { font-family: var(--mono); font-weight: 600; } +table.data td.name { max-width: 230px; overflow: hidden; text-overflow: ellipsis; } +.pos { color: var(--good); } .neg { color: var(--critical); } +.muted { color: var(--text-muted); } +.mono { font-family: var(--mono); } + +/* ---------------------------------------------------------------- stat tiles */ +.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); gap: 1px; + background: var(--border); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; } +.stat { background: var(--surface-1); padding: 9px 11px; } +.stat .label { font-size: 10.5px; color: var(--text-secondary); } +.stat .value { font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: -.02em; margin-top: 1px; } +.stat .delta { font-size: 10.5px; font-variant-numeric: tabular-nums; } +.hero { font-size: 30px; font-weight: 600; letter-spacing: -.03em; font-variant-numeric: tabular-nums; } + +/* ---------------------------------------------------------------- misc */ +.chart { width: 100%; min-width: 0; } +.chart svg { margin: 0 auto; } +.chart svg { display: block; width: 100%; height: auto; overflow: visible; } +.legend { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 6px 0 2px; font-size: 11px; color: var(--text-secondary); } +.legend .item { display: inline-flex; align-items: center; gap: 5px; cursor: default; } +.legend .key { width: 12px; height: 2px; border-radius: 1px; flex: none; } +.legend .key.rect { height: 9px; width: 9px; border-radius: 2px; } + +#tooltip { + position: fixed; z-index: 100; pointer-events: none; opacity: 0; transition: opacity .08s; + background: var(--surface-1); border: 1px solid var(--border-strong); border-radius: 7px; + box-shadow: var(--shadow); padding: 7px 9px; font-size: 11.5px; max-width: 280px; +} +#tooltip .tt-title { font-weight: 600; margin-bottom: 3px; } +#tooltip .tt-row { display: flex; align-items: center; gap: 7px; } +#tooltip .tt-row .key { width: 10px; height: 2px; border-radius: 1px; flex: none; } +#tooltip .tt-row .nm { color: var(--text-secondary); } +#tooltip .tt-row .v { margin-left: auto; font-variant-numeric: tabular-nums; font-weight: 600; } + +.note { border-left: 2px solid var(--border-strong); padding: 2px 0 2px 9px; color: var(--text-secondary); font-size: 11.5px; } +.warn-box { border-left: 2px solid var(--warning); background: var(--surface-2); padding: 7px 10px; border-radius: 0 7px 7px 0; + font-size: 11.5px; color: var(--text-secondary); margin-bottom: 8px; } +.error-box { border-left: 2px solid var(--critical); background: var(--surface-2); padding: 7px 10px; border-radius: 0 7px 7px 0; + font-size: 11.5px; color: var(--text-primary); margin-bottom: 8px; white-space: pre-wrap; } +.kv { display: grid; grid-template-columns: max-content 1fr; gap: 2px 12px; font-size: 11.5px; } +.kv dt { color: var(--text-secondary); } +.kv dd { margin: 0; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; min-width: 0; } +.empty { color: var(--text-muted); font-size: 12px; padding: 18px; text-align: center; border: 1px dashed var(--border-strong); border-radius: 8px; } +.busy { opacity: .45; transition: opacity .1s; } +.spinner { width: 12px; height: 12px; border: 2px solid var(--border-strong); border-top-color: var(--accent); + border-radius: 50%; animation: spin .7s linear infinite; display: inline-block; vertical-align: -2px; } +@keyframes spin { to { transform: rotate(360deg); } } +.seg { display: inline-flex; align-self: flex-start; flex: none; border: 1px solid var(--border-strong); + border-radius: 7px; overflow: hidden; } +.seg button { appearance: none; flex: none; background: var(--surface-1); border: 0; border-left: 1px solid var(--border); + padding: 4px 10px; cursor: pointer; color: var(--text-secondary); font-size: 12px; } +.seg button:first-child { border-left: 0; } +.seg button[aria-pressed="true"] { background: var(--accent); color: #fff; } +.tabbar-sub { display: flex; gap: 4px; margin-bottom: 10px; flex-wrap: wrap; } diff --git a/quantcore/apicli.py b/quantcore/apicli.py new file mode 100644 index 0000000..9be3784 --- /dev/null +++ b/quantcore/apicli.py @@ -0,0 +1,672 @@ +"""Command-line client of the quant-core API: ``quantcore-api ``. + +``quantcore`` (the other entry point) runs the services *in this process* and needs the data +directory locally. ``quantcore-api`` talks to a *running* service over HTTP and needs only its URL, +so it works from any machine on the network and is the quickest way to see what the API returns -- +and, with ``selftest``, to check that a deployment answers correctly on every endpoint. + + quantcore-api --url http://spark-6d4a:5001 health + quantcore-api selftest # exercise every endpoint, pass/fail table + quantcore-api endpoints # the API surface, from its own OpenAPI schema + + quantcore-api universe --region Europe --sector "Health Care" + quantcore-api prices NESN.SW AAPL --freq weekly --tail 5 + quantcore-api risk NESN.SW AAPL --horizon week --drift zero + quantcore-api correlation NESN.SW NOVN.SW ROG.SW + quantcore-api expected-returns --max-assets 50 --view AAPL:0.6:0.4 + quantcore-api scenarios NESN.SW AAPL --model gbm --n-scenarios 4096 + quantcore-api optimize --cash 1000000 --cvar-limit 40000 --horizon week --risk-drift zero + quantcore-api optimize --request my_request.json --json + quantcore-api update --wait # start the data-refresh job and follow it + +The base URL comes from ``--url``, else ``QUANTCORE_API_URL``, else ``http://127.0.0.1:5001``. +Every command takes ``--json`` to print the raw answer instead of a table; that is the form to pipe +into ``jq`` or to save as a fixture. +""" + +from __future__ import annotations + +import argparse +import json +import pathlib +import sys +import time +from typing import Any + +from quantcore.client import QuantcoreClient, QuantcoreError, default_url + +WIDE = {"display.width": 200, "display.max_columns": 40, "display.max_rows": 200} + + +# ------------------------------------------------------------------------------------------------ +# output helpers +# ------------------------------------------------------------------------------------------------ +def _json(obj: Any) -> int: + print(json.dumps(obj, indent=2, default=str)) + return 0 + + +def _records(rows: list[dict], transpose: bool = False, round_to: int = 4) -> int: + """A list of JSON objects as a table; transposed when the rows are wide (risk tables).""" + import pandas as pd + + if not rows: + print("(no rows)") + return 0 + df = pd.DataFrame(rows) + if transpose: + df = df.set_index(df.columns[0]).T + with pd.option_context(*[x for kv in WIDE.items() for x in kv]): + print(df.round(round_to).to_string()) + return 0 + + +def _frame(body: dict, tail: int | None = None) -> int: + """A ``{"index", "columns", "data"}`` answer as a table, optionally only its last rows.""" + import pandas as pd + + df = pd.DataFrame(body.get("data") or [], index=body.get("index") or [], columns=body.get("columns") or []) + head = {k: v for k, v in body.items() if k not in ("index", "columns", "data")} + if head: + print(" · ".join(f"{k}={v}" for k, v in head.items())) + print(f"{len(df)} rows x {len(df.columns)} columns" + (f", last {tail}" if tail and len(df) > tail else "")) + if tail: + df = df.tail(tail) + with pd.option_context(*[x for kv in WIDE.items() for x in kv]): + print(df.round(6).to_string()) + return 0 + + +def _matrix(body: dict, key: str) -> int: + inner = body.get(key, body) + return _frame(inner if isinstance(inner, dict) and "data" in inner else body) + + +def _client(args: argparse.Namespace) -> QuantcoreClient: + return QuantcoreClient(args.url, timeout=args.timeout) + + +def _views(specs: list[str] | None) -> list[dict]: + """``--view SYMBOL:SENTIMENT[:CONFIDENCE[:STRENGTH]]`` -> the API's view objects. + + ``--view AAPL:0.6:0.4`` reads "positive news on Apple (sentiment 0.6), moderate confidence". + """ + views = [] + for spec in specs or []: + parts = spec.split(":") + if len(parts) < 2: + raise SystemExit(f"--view needs SYMBOL:SENTIMENT[:CONFIDENCE[:STRENGTH]], got {spec!r}") + view: dict[str, Any] = {"symbol": parts[0], "sentiment": float(parts[1])} + if len(parts) > 2: + view["confidence"] = float(parts[2]) + if len(parts) > 3: + view["strength"] = float(parts[3]) + views.append(view) + return views + + +# ------------------------------------------------------------------------------------------------ +# commands +# ------------------------------------------------------------------------------------------------ +def cmd_health(args) -> int: + body = _client(args).health() + if args.json: + return _json(body) + store, cuopt = body.get("store") or {}, body.get("cuopt") or {} + print(f"{args.url or default_url()} version {body.get('version')} currency {body.get('base_currency')}") + print(f"store : {store.get('n_universe')} stocks in the universe, {store.get('n_symbols')} with prices, " + f"{store.get('n_days')} days, last {store.get('last_price_date')}") + print(f"cuOpt : {'reachable, ' + str(cuopt.get('version')) if cuopt.get('reachable') else 'NOT reachable'}" + f"{'' if cuopt.get('reachable') else ' - optimisations fall back to HiGHS on the CPU'}") + return 0 + + +def cmd_endpoints(args) -> int: + schema = _client(args).openapi() + rows = [{"method": method.upper(), "path": path, "summary": op.get("summary") or op.get("operationId", ""), + "tag": (op.get("tags") or [""])[0]} + for path, ops in sorted((schema.get("paths") or {}).items()) + for method, op in ops.items() if method in ("get", "post", "put", "delete")] + if args.json: + return _json(rows) + print(f"{schema.get('info', {}).get('title', 'quantcore')} -- {len(rows)} endpoints " + f"(interactive docs at {(args.url or default_url()).rstrip('/')}/docs)") + return _records(rows, round_to=0) + + +def cmd_universe(args) -> int: + rows = _client(args).universe(region=args.region, sector=args.sector, country=args.country) + if args.json: + return _json(rows) + keep = ["symbol", "name", "region", "country", "gics_sector", "listing_currency", "market_cap_base"] + rows = sorted(rows, key=lambda r: -(r.get("market_cap_base") or 0))[: args.limit] + print(f"{len(rows)} of the matching stocks, largest first") + return _records([{k: r.get(k) for k in keep} for r in rows], round_to=0) + + +def cmd_symbol(args) -> int: + return _json(_client(args).symbol(args.symbol)) + + +def cmd_prices(args) -> int: + body = _client(args).prices(args.symbols, start=args.start, end=args.end, currency=args.currency, + freq=args.freq) + return _json(body) if args.json else _frame(body, args.tail) + + +def cmd_returns(args) -> int: + body = _client(args).returns(args.symbols, start=args.start, end=args.end, currency=args.currency, + freq=args.freq) + return _json(body) if args.json else _frame(body, args.tail) + + +def cmd_market(args) -> int: + qc = _client(args) + if args.what == "weights": + body = qc.market_weights(max_assets=args.max_assets) + if args.json: + return _json(body) + weights = body.get("weights") or {} + print(f"as of {body.get('as_of')}, {len(weights)} stocks, sum {sum(weights.values()):.6f}") + return _records([{"symbol": k, "weight": v} for k, v in list(weights.items())[: args.limit]], round_to=6) + body = qc.market_index(start=args.start, end=args.end, freq=args.freq) + if args.json: + return _json(body) + print(f"cap-weighted index, {body.get('freq')}: mean_ann {body.get('mean_ann'):.4%}, " + f"vol_ann {body.get('vol_ann'):.4%}, {body.get('n')} observations") + return 0 + + +def cmd_risk(args) -> int: + rows = _client(args).asset_risk(args.symbols, horizon=args.horizon, freq=args.freq, window=args.window, + lam=args.lam, currency=args.currency, drift=args.drift, space=args.space, + confidence=args.confidence) + if args.json: + return _json(rows) + print(f"horizon {args.horizon} · {args.freq} · window {args.window} · lambda {args.lam} · " + f"drift {args.drift} · space {args.space}") + return _records(rows, transpose=True, round_to=6) + + +def cmd_risk_portfolio(args) -> int: + body = _client(args).portfolio_risk(json.loads(args.holdings), cash=args.cash, horizon=args.horizon, + method=args.method, confidence=args.confidence) + return _json(body) + + +def cmd_matrix(args) -> int: + qc = _client(args) + call = qc.correlation if args.command == "correlation" else qc.covariance + body = call(args.symbols, method=args.method, window=args.window, lam=args.lam, freq=args.freq) + return _json(body) if args.json else _frame(body) + + +def cmd_performance(args) -> int: + rows = _client(args).asset_performance(args.symbols, freq=args.freq, currency=args.currency, start=args.start) + return _json(rows) if args.json else _records(rows, transpose=True) + + +def cmd_expected_returns(args) -> int: + body = _client(args).expected_returns(max_assets=args.max_assets, symbols=args.symbols, tau=args.tau, + years=args.years, omega_method=args.omega_method, + views=_views(args.view)) + if args.json: + return _json(body) + rows = body.get("expected_returns") or body.get("returns") or [] + for k, v in body.items(): + if isinstance(v, (int, float, str)): + print(f"{k:24} {v}") + if isinstance(rows, dict): + rows = [{"symbol": k, **(v if isinstance(v, dict) else {"value": v})} for k, v in rows.items()] + return _records(rows[: args.limit] if isinstance(rows, list) else [], round_to=6) + + +def cmd_model_covariance(args) -> int: + body = _client(args).model_covariance(args.symbols, freq=args.freq, horizon=args.horizon, + covariance=_cov_spec(args)) + if args.json: + return _json(body) + print(f"model {body.get('model')} · as of {body.get('as_of')} · shrinkage applied " + f"{body.get('shrinkage_applied')}") + print("conditioning:", json.dumps(body.get("conditioning"), default=str)) + return _matrix(body, "covariance") + + +def cmd_scenarios(args) -> int: + body = _client(args).scenarios(args.symbols, model=args.model, n_scenarios=args.n_scenarios, + horizon=args.horizon, freq=args.freq, seed=args.seed, + sampling=args.sampling, covariance=_cov_spec(args), + expected_returns=args.expected_returns, + importance_sampling=args.importance_sampling, + holdings=json.loads(args.holdings) if args.holdings else None, + value=args.value, confidence=args.confidence) + if args.json: + return _json(body) + desc, port = body.get("description") or {}, body.get("portfolio") or {} + print(f"{desc.get('model')} · {desc.get('n_scenarios')} scenarios x {desc.get('n_assets')} assets · " + f"horizon {desc.get('horizon_periods')} periods · seed {desc.get('seed')}") + print(f"hash {desc.get('hash')} · shocks {desc.get('shock_hash')} · weighted {desc.get('weighted')} · " + f"effective sample size {desc.get('effective_sample_size')}") + if port: + print(f"portfolio: invested {port.get('invested'):,.0f}") + for level, fig in sorted((port.get("risk") or {}).items()): + print(f" {float(level):.0%} VaR {fig.get('var'):>14,.2f} CVaR {fig.get('cvar'):>14,.2f}") + return _records(body.get("summary") or [], transpose=True, round_to=6) + + +def cmd_optimize(args) -> int: + body: dict[str, Any] = json.loads(pathlib.Path(args.request).read_text()) if args.request else {} + flags = {"cash": args.cash, "max_assets": args.max_assets, "horizon": args.horizon, + "confidence": args.confidence, "cvar_limit": args.cvar_limit, "objective": args.objective, + "scenario_model": args.scenario_model, "n_scenarios": args.n_scenarios, "seed": args.seed, + "solver": args.solver, "time_limit": args.time_limit, "risk_drift": args.risk_drift, + "expected_returns": args.expected_returns, "importance_sampling": args.importance_sampling, + "row_generation": args.row_generation, "freq": args.freq, "symbols": args.symbols} + for k, v in flags.items(): # the request file wins over a flag default + if v is not None and k not in body: + body[k] = v + if args.holdings: + body["holdings"] = json.loads(args.holdings) + body.setdefault("mode", "rebalance") + if args.view: + body["views"] = _views(args.view) + if args.max_weight is not None or args.max_sector_weight is not None: + constraints = dict(body.get("constraints") or {}) + if args.max_weight is not None: + constraints["max_weight"] = args.max_weight + if args.max_sector_weight is not None: + constraints["max_sector_weight"] = args.max_sector_weight + body["constraints"] = constraints + + t0 = time.monotonic() + res = _client(args).optimize(**body) + elapsed = time.monotonic() - t0 + if args.json: + return _json(res) + print(f"status {res.get('status')} · solver {res.get('solver')} · reproducibility " + f"{res.get('reproducibility')} · {elapsed:.1f} s wall clock") + info = res.get("solver_info") or {} + if info.get("mip_gap") is not None: + print(f"mip gap {info['mip_gap']:.2e}" + (f" · {info.get('message')}" if info.get("message") else "")) + key = (res.get("scenario_key") or {}).get("id") + print(f"scenario key {key} · hash {res.get('scenario_hash')} · timings {res.get('timings')}") + print(f"wealth {res.get('wealth', 0):,.0f} · invested {res.get('invested', 0):,.0f} · " + f"cash left {res.get('cash_end', 0):,.0f} · cost {res.get('total_cost', 0):,.0f} · " + f"turnover {res.get('turnover', 0):,.0f}") + print(f"E[P&L] {res.get('expected_pnl', 0):,.0f} ({res.get('expected_return', 0):.3%}) · " + f"VaR {res.get('var', 0):,.0f} · CVaR {res.get('cvar', 0):,.0f} " + f"(limit {res.get('cvar_limit')}) at {res.get('confidence', 0):.0%} over {res.get('horizon')}") + for warning in res.get("warnings") or []: + print(f" ! {warning}") + positions = res.get("positions") or [] + if positions and not args.quiet: + keep = [k for k in ("symbol", "name", "sector", "price", "shares", "trade", "value", "weight") + if k in positions[0]] + _records([{k: p.get(k) for k in keep} for p in positions], round_to=3) + print("sectors:", {k: round(v, 3) for k, v in (res.get("sector_exposure") or {}).items()}) + return 0 if res.get("status") in ("Optimal", "FeasibleFound") else 2 + + +def cmd_jobs(args) -> int: + qc = _client(args) + if args.job_id: + return _json(qc.job(args.job_id)) + return _json(qc.jobs()) + + +def cmd_setup(args) -> int: + return _start_job(args, _client(args).start_setup(offline=args.offline, max_per_region=args.max_per_region)) + + +def cmd_update(args) -> int: + return _start_job(args, _client(args).start_update(full=args.full)) + + +def _start_job(args, started: dict) -> int: + job_id = started.get("job_id") or started.get("id") + print(f"job {job_id} started ({started.get('status', 'queued')})") + if not args.wait: + print(f"follow it with: quantcore-api --url {args.url or default_url()} jobs --job-id {job_id}") + return 0 + record = _client(args).wait_for_job(job_id, poll=args.poll, + on_poll=lambda r: print(f" ... {r.get('status')}", flush=True)) + _json(record) + return 0 if record.get("status") == "done" else 2 + + +def cmd_snapshots(args) -> int: + qc = _client(args) + if args.create: + return _json(qc.create_snapshot(args.name)) + return _json(qc.snapshots()) + + +def _cov_spec(args) -> dict: + return {"method": args.cov_method, "lam": args.lam, "window": args.window, "shrinkage": args.shrinkage} + + +# ------------------------------------------------------------------------------------------------ +# selftest -- exercise every endpoint against a running service +# ------------------------------------------------------------------------------------------------ +def cmd_selftest(args) -> int: + """Call every endpoint with a small request and report what each one answered. + + Read-only by default: the two background jobs and the snapshot endpoint only run with + ``--write``, because they download data and write to the store. Exit code 1 if any probe failed. + """ + qc = _client(args) + results: list[dict] = [] + + def probe(name: str, call: Any, evidence: Any = None, skip: str = "") -> Any: + """Run one endpoint. ``call()`` performs it, ``evidence(value)`` says what came back.""" + if skip: + results.append({"endpoint": name, "result": "skipped", "ms": 0, "evidence": skip}) + return None + t0 = time.monotonic() + try: + value = call() + note = evidence(value) if evidence else "" + except QuantcoreError as exc: + note, value, ok = str(exc)[:120], None, False + except Exception as exc: # noqa: BLE001 (a probe's own assertion, or a malformed answer) + note, value, ok = f"{type(exc).__name__}: {exc}"[:120], None, False + else: + ok = True + results.append({"endpoint": name, "result": "ok" if ok else "FAIL", + "ms": round((time.monotonic() - t0) * 1000), "evidence": note}) + return value + + def health() -> dict: + body = qc.health() + if not (body.get("store") or {}).get("prices"): + raise AssertionError("the store has no prices - run the setup job first") + return body + + body = probe("GET /health", health, lambda b: ( + f"{b['store'].get('n_universe')} stocks, last {b['store'].get('last_price_date')}, " + f"cuOpt {'up' if (b.get('cuopt') or {}).get('reachable') else 'down'}")) + if body is None: + _records(results, round_to=0) + print("\nthe service is not usable - stopping here") + return 1 + + rows = probe("GET /universe", qc.universe, lambda r: f"{len(r)} rows") or [] + syms = args.symbols or [r["symbol"] for r in + sorted(rows, key=lambda r: -(r.get("market_cap_base") or 0))[: args.n_symbols]] + if not syms: + print("no symbols to probe with") + return 1 + print(f"probing with {', '.join(syms)}\n") + holdings = {syms[0]: 10} + + probe("GET /openapi.json", qc.openapi, lambda d: f"{len(d.get('paths') or {})} paths") + probe("GET /universe/{symbol}", lambda: qc.symbol(syms[0]), + lambda d: f"{d.get('symbol')}: {d.get('name')}") + probe("GET /data/symbols", lambda: qc.symbols_table(limit=10), + lambda d: f"{len(d.get('symbols') or [])} of {d.get('n_matched')} rows, " + f"facets {list((d.get('facets') or {}).keys())}") + probe("GET /prices", lambda: qc.prices(syms, start=args.start), + lambda d: f"{len(d.get('index') or [])} dates x {len(d.get('columns') or [])} symbols") + probe("GET /returns", lambda: qc.returns(syms, start=args.start), + lambda d: f"{d.get('kind')}, {len(d.get('index') or [])} dates") + probe("GET /market/index", qc.market_index, + lambda d: f"mean_ann {d.get('mean_ann'):.2%}, vol_ann {d.get('vol_ann'):.2%}") + probe("GET /market/weights", lambda: qc.market_weights(max_assets=20), + lambda d: f"{len(d.get('weights') or {})} weights, sum {sum((d.get('weights') or {}).values()):.4f}") + + probe("POST /risk/assets", lambda: qc.asset_risk(syms, horizon="week"), + lambda r: f"{len(r)} rows, cvar95_hist {r[0].get('cvar95_hist'):.4f}") + probe("POST /risk/assets (drift=zero, space=value)", + lambda: qc.asset_risk(syms, horizon="week", drift="zero", space="value"), + lambda r: f"cvar95_normal_ewm {r[0].get('cvar95_normal_ewm'):.4f}") + probe("POST /risk/series", lambda: qc.risk_series(syms[0], horizon="week"), + lambda d: f"{len(d.get('index') or [])} dates") + probe("POST /risk/covariance", lambda: qc.covariance(syms), + lambda d: f"{len(d.get('columns') or [])} assets") + probe("POST /risk/correlation", lambda: qc.correlation(syms), + lambda d: f"diagonal {d['data'][0][0]:.3f}") + probe("POST /risk/portfolio", lambda: qc.portfolio_risk(holdings, cash=10_000, horizon="week"), + lambda d: f"value {d.get('value', 0):,.0f}, " + f"CVaR95 {((d.get('confidence') or {}).get('0.95') or {}).get('cvar_money', 0):,.0f}") + + probe("POST /performance/assets", lambda: qc.asset_performance(syms), + lambda r: f"{len(r)} rows, sharpe {r[0].get('sharpe')}") + probe("POST /performance/rolling", lambda: qc.rolling_performance(syms[0], window=252), + lambda d: f"{len(d.get('index') or [])} dates") + probe("POST /performance/portfolio", lambda: qc.portfolio_performance(holdings), + lambda d: f"{len(d)} figures") + + probe("POST /model/expected-returns", + lambda: qc.expected_returns(max_assets=args.n_symbols, symbols=args.symbols), + lambda d: f"market return {(d.get('market_return_ann') or 0):.2%}") + probe("POST /model/covariance", lambda: qc.model_covariance(syms), + lambda d: f"shrinkage {d.get('shrinkage_applied')}") + probe("POST /model/scenarios", + lambda: qc.scenarios(syms, model="gbm", n_scenarios=1024, holdings=holdings), + lambda d: f"{(d.get('description') or {}).get('n_scenarios')} scenarios, " + f"hash {(d.get('description') or {}).get('hash')}") + + def optimise() -> dict: + body = {"cash": 1_000_000, "max_assets": max(len(syms), 12), "horizon": "week", + "symbols": args.symbols, "solver": args.solver, "time_limit": args.time_limit} + body.update({"scenario_model": "gbm", "n_scenarios": 4096, "cvar_limit_fraction": 0.05} + if args.full else {"scenario_model": "gaussian_exact"}) + res = qc.optimize(**{k: v for k, v in body.items() if v is not None}) + if res.get("status") not in ("Optimal", "FeasibleFound"): + raise AssertionError(f"status {res.get('status')}: {'; '.join(res.get('warnings') or [])}"[:120]) + return res + + probe(f"POST /optimize ({'gbm, 4096 scenarios' if args.full else 'gaussian_exact'})", optimise, + lambda r: f"{r.get('status')} on {r.get('solver')}, {r.get('n_positions')} positions, " + f"CVaR {r.get('cvar', 0):,.0f}") + + probe("GET /jobs", qc.jobs, lambda r: f"{len(r)} jobs") + probe("GET /snapshots", qc.snapshots, lambda r: f"{len(r)} snapshots") + probe("POST /snapshots", lambda: qc.create_snapshot(f"selftest-{int(time.time())}"), + lambda d: str(d.get("path"))[:60], + skip="" if args.write else "writes to the store (--write)") + probe("POST /jobs/update", qc.start_update, lambda d: f"job {d.get('job_id')}", + skip="" if args.write else "downloads data (--write)") + probe("POST /jobs/setup", lambda: qc.start_setup(offline=True), lambda d: f"job {d.get('job_id')}", + skip="" if args.write else "rebuilds the universe (--write)") + + if args.json: + return _json(results) + _records(results, round_to=0) + failed = [r for r in results if r["result"] == "FAIL"] + ok = [r for r in results if r["result"] == "ok"] + skipped = [r for r in results if r["result"] == "skipped"] + print(f"\n{len(ok)} ok, {len(failed)} failed, {len(skipped)} skipped " + f"in {sum(r['ms'] for r in results) / 1000:.1f} s against {qc.base_url}") + for r in failed: + print(f" FAIL {r['endpoint']}: {r['evidence']}") + return 1 if failed else 0 + + +# ------------------------------------------------------------------------------------------------ +# parser +# ------------------------------------------------------------------------------------------------ +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(prog="quantcore-api", description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("--url", default=None, help=f"API base URL (default: $QUANTCORE_API_URL or {default_url()})") + p.add_argument("--timeout", type=float, default=900.0, help="seconds per request") + p.add_argument("--json", action="store_true", help="print the raw JSON answer instead of a table") + sub = p.add_subparsers(dest="command", required=True) + + # the same three options after the subcommand, where anyone would try them first + # ('quantcore-api optimize --json'). SUPPRESS keeps the value parsed before the subcommand + # unless the flag is repeated after it. + common = argparse.ArgumentParser(add_help=False) + common.add_argument("--url", default=argparse.SUPPRESS) + common.add_argument("--timeout", type=float, default=argparse.SUPPRESS) + common.add_argument("--json", action="store_true", default=argparse.SUPPRESS) + + def add(name: str, help_: str, func: Any) -> argparse.ArgumentParser: + sp = sub.add_parser(name, help=help_, parents=[common]) + sp.set_defaults(func=func) + return sp + + def symbols_arg(sp: argparse.ArgumentParser) -> argparse.ArgumentParser: + sp.add_argument("symbols", nargs="+", help="Yahoo symbols, e.g. NESN.SW AAPL ASML.AS") + return sp + + def window_args(sp: argparse.ArgumentParser) -> argparse.ArgumentParser: + sp.add_argument("--window", type=int, default=200, help="moving-window length in periods") + sp.add_argument("--lam", type=float, default=0.94, help="EWM decay") + return sp + + add("health", "store status, cuOpt reachability, version", cmd_health) + add("endpoints", "list every endpoint from the service's own OpenAPI schema", cmd_endpoints) + + up = add("universe", "the investable universe (filter by region, sector, country)", cmd_universe) + up.add_argument("--region"), up.add_argument("--sector"), up.add_argument("--country") + up.add_argument("--limit", type=int, default=25) + + sy = add("symbol", "one universe row", cmd_symbol) + sy.add_argument("symbol") + + for name, func, what in (("prices", cmd_prices, "adjusted closes"), + ("returns", cmd_returns, "log-returns")): + sp = symbols_arg(add(name, what, func)) + sp.add_argument("--freq", default="daily", choices=["daily", "weekly", "monthly"]) + sp.add_argument("--currency", default="base", choices=["base", "local"]) + sp.add_argument("--start"), sp.add_argument("--end") + sp.add_argument("--tail", type=int, default=10, help="print only the last N rows (0: all)") + + mk = add("market", "the cap-weighted broad market: 'index' or 'weights'", cmd_market) + mk.add_argument("what", nargs="?", default="index", choices=["index", "weights"]) + mk.add_argument("--freq", default="weekly", choices=["daily", "weekly", "monthly"]) + mk.add_argument("--start"), mk.add_argument("--end") + mk.add_argument("--max-assets", type=int, default=None), mk.add_argument("--limit", type=int, default=25) + + rk = window_args(symbols_arg(add("risk", "per-stock volatility, VaR and CVaR", cmd_risk))) + rk.add_argument("--horizon", default="week", help="'day' | 'week' | 'month' | 'quarter' | 'year' | periods") + rk.add_argument("--freq", default="daily", choices=["daily", "weekly", "monthly"]) + rk.add_argument("--currency", default="base", choices=["base", "local"]) + rk.add_argument("--drift", default="estimated", choices=["estimated", "zero"], + help="'zero': volatility only, the market-risk convention") + rk.add_argument("--space", default="log", choices=["log", "value"], + help="'value': as a fraction of the position's value, which cannot exceed 100 %%") + rk.add_argument("--confidence", type=float, nargs="+", default=[0.95, 0.99]) + + rp = add("risk-portfolio", "VaR/CVaR of a portfolio held in share numbers", cmd_risk_portfolio) + rp.add_argument("--holdings", required=True, help='JSON: {"NESN.SW": 500, "AAPL": 100}') + rp.add_argument("--cash", type=float, default=0.0) + rp.add_argument("--horizon", default="week") + rp.add_argument("--method", default="normal", choices=["normal", "historical"]) + rp.add_argument("--confidence", type=float, nargs="+", default=[0.95, 0.99]) + + for name in ("correlation", "covariance"): + sp = window_args(symbols_arg(add(name, f"{name} matrix as measured", cmd_matrix))) + sp.add_argument("--method", default="ewm", choices=["rolling", "ewm"]) + sp.add_argument("--freq", default="daily", choices=["daily", "weekly", "monthly"]) + + pf = symbols_arg(add("performance", "annualised return, Sharpe, drawdown, skew, kurtosis", cmd_performance)) + pf.add_argument("--freq", default="weekly", choices=["daily", "weekly", "monthly"]) + pf.add_argument("--currency", default="base", choices=["base", "local"]) + pf.add_argument("--start") + + er = add("expected-returns", "Black-Litterman equilibrium and posterior returns", cmd_expected_returns) + er.add_argument("--symbols", nargs="*", default=None) + er.add_argument("--max-assets", type=int, default=100) + er.add_argument("--tau", type=float, default=0.05) + er.add_argument("--years", type=int, default=10) + er.add_argument("--omega-method", default="idzorek", choices=["idzorek", "he_litterman"]) + er.add_argument("--view", action="append", metavar="SYM:SENTIMENT[:CONF[:STRENGTH]]", + help="a sentiment view; repeatable") + er.add_argument("--limit", type=int, default=25) + + mc = window_args(symbols_arg(add("model-covariance", "the covariance the models use, with its " + "conditioning", cmd_model_covariance))) + mc.add_argument("--cov-method", default="ewm", choices=["rolling", "ewm"]) + mc.add_argument("--shrinkage", default="auto") + mc.add_argument("--freq", default="daily", choices=["daily", "weekly"]) + mc.add_argument("--horizon", default="week") + + sc = window_args(symbols_arg(add("scenarios", "a scenario set and its statistics", cmd_scenarios))) + sc.add_argument("--model", default="gbm", choices=["gbm", "empirical"]) + sc.add_argument("--n-scenarios", type=int, default=4096) + sc.add_argument("--horizon", default="week") + sc.add_argument("--freq", default="daily", choices=["daily", "weekly"]) + sc.add_argument("--seed", type=int, default=42) + sc.add_argument("--sampling", default="sobol", choices=["sobol", "mc", "antithetic", "lhs", "balanced"]) + sc.add_argument("--cov-method", default="ewm", choices=["rolling", "ewm"]) + sc.add_argument("--shrinkage", default="auto") + sc.add_argument("--expected-returns", default="zero", choices=["zero", "historical", "black_litterman"]) + sc.add_argument("--importance-sampling", default="auto", choices=["auto", "market", "adaptive", "off"]) + sc.add_argument("--holdings", help='JSON: {"NESN.SW": 500}; else the set is reported per stock') + sc.add_argument("--value", type=float, default=1_000_000.0) + sc.add_argument("--confidence", type=float, nargs="+", default=[0.95, 0.99]) + + op = add("optimize", "mean-CVaR MILP in integer shares", cmd_optimize) + op.add_argument("--request", help="JSON file with the full request; its fields win over the flags") + op.add_argument("--cash", type=float, default=1_000_000.0) + op.add_argument("--holdings", help='JSON dict -> rebalancing mode') + op.add_argument("--symbols", nargs="*", default=None, help="candidates; default: largest by market cap") + op.add_argument("--max-assets", type=int, default=100) + op.add_argument("--horizon", default="week") + op.add_argument("--freq", default="daily", choices=["daily", "weekly"]) + op.add_argument("--confidence", type=float, default=0.95) + op.add_argument("--cvar-limit", type=float, default=None, help="max CVaR in CHF over the horizon") + op.add_argument("--objective", default="max_return", choices=["max_return", "min_cvar", "utility"]) + op.add_argument("--scenario-model", default="empirical", choices=["gbm", "empirical", "gaussian_exact"]) + op.add_argument("--n-scenarios", type=int, default=None, + help="default: 16 per candidate within [4096, 16384], rounded up to a power of two") + op.add_argument("--seed", type=int, default=42) + op.add_argument("--expected-returns", default="black_litterman", + choices=["black_litterman", "historical", "zero"]) + op.add_argument("--risk-drift", default="model", choices=["model", "zero"], + help="'zero': measure the CVaR on centred scenarios, so a bullish forecast " + "cannot relax the risk limit") + op.add_argument("--importance-sampling", default="auto", choices=["auto", "market", "adaptive", "off"]) + op.add_argument("--row-generation", default="auto", choices=["auto", "on", "off"]) + op.add_argument("--max-weight", type=float, default=None, help="cap per stock (default 0.10)") + op.add_argument("--max-sector-weight", type=float, default=None, help="cap per GICS sector (default 0.25)") + op.add_argument("--view", action="append", metavar="SYM:SENTIMENT[:CONF[:STRENGTH]]", help="repeatable") + op.add_argument("--solver", default="auto", choices=["auto", "cuopt", "highs"]) + op.add_argument("--time-limit", type=float, default=300.0, help="seconds for the whole request") + op.add_argument("--quiet", action="store_true", help="summary only, no position table") + + jb = add("jobs", "list background jobs, or one with --job-id", cmd_jobs) + jb.add_argument("--job-id", default=None) + + st = add("setup", "start the universe/history build (background job)", cmd_setup) + st.add_argument("--offline", action="store_true", help="use the candidate lists shipped in the package") + st.add_argument("--max-per-region", type=int, default=None) + st.add_argument("--wait", action="store_true"), st.add_argument("--poll", type=float, default=10.0) + + ud = add("update", "start the price/FX refresh (background job)", cmd_update) + ud.add_argument("--full", action="store_true") + ud.add_argument("--wait", action="store_true"), ud.add_argument("--poll", type=float, default=5.0) + + sn = add("snapshots", "list snapshots, or --create one", cmd_snapshots) + sn.add_argument("--create", action="store_true"), sn.add_argument("--name", default=None) + + se = add("selftest", "call every endpoint and report what it answered", cmd_selftest) + se.add_argument("--full", action="store_true", help="optimise on a real scenario set, not gaussian_exact") + se.add_argument("--write", action="store_true", help="also probe the endpoints that write or download") + se.add_argument("--symbols", nargs="*", default=None, help="default: the largest few in the universe") + se.add_argument("--n-symbols", type=int, default=3) + se.add_argument("--start", default=None, help="first date for the price/return probes") + se.add_argument("--solver", default="auto", choices=["auto", "cuopt", "highs"]) + se.add_argument("--time-limit", type=float, default=120.0) + return p + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + try: + return args.func(args) + except QuantcoreError as exc: + print(f"error: {exc}", file=sys.stderr) + if exc.unreachable: + print("is the service running? start it with 'quantcore serve', or pass --url", file=sys.stderr) + return 1 + except KeyboardInterrupt: + print("interrupted", file=sys.stderr) + return 130 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/quantcore/cli.py b/quantcore/cli.py new file mode 100644 index 0000000..3dd245b --- /dev/null +++ b/quantcore/cli.py @@ -0,0 +1,253 @@ +"""Command-line interface: ``quantcore ``. + + quantcore setup [--offline] [--max-per-region N] build the universe once (candidates, + metadata, full price + FX history) + quantcore reselect re-run only the selection step + quantcore clean re-apply price cleaning to stored data + quantcore update [--full] fetch the newest prices / FX + quantcore snapshot [--name NAME] freeze the data for reproducible demos + quantcore status what the store holds, cuOpt health + quantcore risk SYMBOL [SYMBOL ...] [--horizon week] [--freq daily] [--window 200] [--lam 0.94] + quantcore performance SYMBOL [SYMBOL ...] [--freq weekly] + quantcore optimize [--cash 1e6] [--cvar-limit 50000] [--confidence 0.95] [--horizon week] ... + quantcore serve [--host 127.0.0.1] [--port 5001] run the HTTP API (+ the test bench at /ui/) + +These commands run the services in this process and need the data directory locally. To drive a +*running* service over HTTP instead -- from any machine, without the data -- use ``quantcore-api`` +(:mod:`quantcore.apicli`), whose ``selftest`` also checks a deployment endpoint by endpoint. +""" + +from __future__ import annotations + +import argparse +import json +import logging +import sys + +import pandas as pd + +from quantcore.config import Settings, get_settings + +log = logging.getLogger("quantcore.cli") + + +def _settings(args) -> Settings: + s = get_settings() + if getattr(args, "data_dir", None): + s = s.model_copy(update={"data_dir": args.data_dir}) + if getattr(args, "cuopt_url", None): + s = s.model_copy(update={"cuopt_url": args.cuopt_url}) + return s + + +def cmd_setup(args) -> int: + from quantcore.universe.setup_service import SetupService + + report = SetupService(_settings(args)).run(allow_web=not args.offline, max_per_region=args.max_per_region) + print(json.dumps(report.as_dict(), indent=2, default=str)) + return 0 + + +def cmd_reselect(args) -> int: + from quantcore.universe.setup_service import SetupService + + report = SetupService(_settings(args)).rebuild_selection() + print(json.dumps(report.as_dict(), indent=2, default=str)) + return 0 + + +def cmd_update(args) -> int: + from quantcore.data.download_service import DataDownloadService + + report = DataDownloadService(_settings(args)).update(full=args.full) + print(json.dumps(report.as_dict(), indent=2, default=str)) + return 0 + + +def cmd_clean(args) -> int: + from quantcore.data.download_service import DataDownloadService + + report = DataDownloadService(_settings(args)).clean() + print(json.dumps(report.as_dict(), indent=2, default=str)) + return 0 + + +def cmd_snapshot(args) -> int: + from quantcore.data.store import DataStore + + path = DataStore(_settings(args)).create_snapshot(args.name) + print(path) + return 0 + + +def cmd_status(args) -> int: + from quantcore.data.store import DataStore + from quantcore.optimization.solvers import CuOptSolver + + s = _settings(args) + out = {"store": DataStore(s).status(), "cuopt": CuOptSolver(s).health(), "cuopt_url": s.cuopt_url} + print(json.dumps(out, indent=2, default=str)) + return 0 + + +def cmd_risk(args) -> int: + from quantcore.data.market_data import MarketDataService + from quantcore.measurement.risk import RiskService + + md = MarketDataService(_settings(args), snapshot=args.snapshot) + table = RiskService(md).asset_risk(args.symbols, freq=args.freq, window=args.window, lam=args.lam, + horizon=args.horizon, currency=args.currency) + with pd.option_context("display.width", 200, "display.max_columns", 30): + print(table.round(5).T) + return 0 + + +def cmd_performance(args) -> int: + from quantcore.data.market_data import MarketDataService + from quantcore.measurement.performance import PerformanceService + + s = _settings(args) + md = MarketDataService(s, snapshot=args.snapshot) + table = PerformanceService(md, s.risk_free_rate).asset_performance(args.symbols, freq=args.freq, + currency=args.currency, start=args.start) + with pd.option_context("display.width", 200, "display.max_columns", 30): + print(table.round(4).T) + return 0 + + +def cmd_optimize(args) -> int: + from quantcore.data.market_data import MarketDataService + from quantcore.optimization.service import OptimizationRequest, OptimizationService + + s = _settings(args) + md = MarketDataService(s, snapshot=args.snapshot) + kwargs = {} + if args.request: + kwargs = json.loads(open(args.request).read()) + kwargs.setdefault("cash", args.cash) + kwargs.setdefault("horizon", args.horizon) + kwargs.setdefault("confidence", args.confidence) + if args.cvar_limit is not None: + kwargs["cvar_limit"] = args.cvar_limit + if args.symbols: + kwargs["symbols"] = args.symbols + kwargs.setdefault("max_assets", args.max_assets) + kwargs.setdefault("scenario_model", args.scenario_model) + if args.n_scenarios is not None: + kwargs.setdefault("n_scenarios", args.n_scenarios) + kwargs.setdefault("sampling", args.sampling) + kwargs.setdefault("importance_sampling", args.importance_sampling) + kwargs.setdefault("row_generation", args.row_generation) + kwargs.setdefault("solver", args.solver) + if args.holdings: + kwargs["holdings"] = json.loads(args.holdings) + kwargs["mode"] = "rebalance" + req = OptimizationRequest(**kwargs) + res = OptimizationService(s, md).optimize(req) + if args.json: + print(json.dumps(res.as_dict(), indent=2, default=str)) + else: + print(f"status={res.status} solver={res.solver} reproducible={res.reproducibility} scenarios={res.scenario_hash} " + f"key={res.scenario_key['id'] if res.scenario_key else None} " + f"wealth={res.wealth:,.0f} invested={res.invested:,.0f} cash_end={res.cash_end:,.0f} cost={res.total_cost:,.0f}") + print(f"E[P&L]={res.expected_pnl:,.0f} ({res.expected_return:.3%}) VaR={res.var:,.0f} CVaR={res.cvar:,.0f} " + f"limit={res.cvar_limit} @ {res.confidence:.0%} over {res.horizon} ({res.horizon_periods} periods)") + if res.positions: + df = pd.DataFrame(res.positions)[["symbol", "name", "sector", "price", "shares", "trade", "value", "weight"]] + with pd.option_context("display.width", 200, "display.max_rows", 500): + print(df.round(3).to_string(index=False)) + print("sectors:", {k: round(v, 3) for k, v in res.sector_exposure.items()}) + print("timings:", res.timings, "warnings:", res.warnings) + return 0 if res.status in ("Optimal", "FeasibleFound") else 2 + + +def cmd_serve(args) -> int: + import uvicorn + + s = _settings(args) + uvicorn.run("quantcore.api.app:app", host=args.host or s.api_host, port=args.port or s.api_port, reload=False) + return 0 + + +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(prog="quantcore", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("--data-dir", help="override QUANTCORE_DATA_DIR") + p.add_argument("--cuopt-url", help="override QUANTCORE_CUOPT_URL") + sub = p.add_subparsers(dest="command", required=True) + + sp = sub.add_parser("setup", help="build the universe and download all history (run once)") + sp.add_argument("--offline", action="store_true", help="use the static candidate lists shipped with the package") + sp.add_argument("--max-per-region", type=int, default=None, help="limit candidates per region (testing)") + sp.set_defaults(func=cmd_setup) + + sub.add_parser("reselect", help="re-run only the universe selection on stored data").set_defaults(func=cmd_reselect) + + up = sub.add_parser("update", help="fetch the newest prices and FX rates") + up.add_argument("--full", action="store_true", help="re-download the complete history") + up.set_defaults(func=cmd_update) + + sub.add_parser("clean", help="re-apply the price cleaning to the stored history").set_defaults(func=cmd_clean) + + sn = sub.add_parser("snapshot", help="freeze universe/prices/fx under snapshots/") + sn.add_argument("--name", default=None) + sn.set_defaults(func=cmd_snapshot) + + sub.add_parser("status", help="store status and cuOpt health").set_defaults(func=cmd_status) + + rk = sub.add_parser("risk", help="per-asset volatility, VaR and CVaR") + rk.add_argument("symbols", nargs="+") + rk.add_argument("--freq", default="daily", choices=["daily", "weekly", "monthly"]) + rk.add_argument("--horizon", default="day") + rk.add_argument("--window", type=int, default=200) + rk.add_argument("--lam", type=float, default=0.94) + rk.add_argument("--currency", default="base", choices=["base", "local"]) + rk.add_argument("--snapshot", default=None) + rk.set_defaults(func=cmd_risk) + + pf = sub.add_parser("performance", help="per-asset performance statistics") + pf.add_argument("symbols", nargs="+") + pf.add_argument("--freq", default="daily", choices=["daily", "weekly", "monthly"]) + pf.add_argument("--currency", default="base", choices=["base", "local"]) + pf.add_argument("--start", default=None) + pf.add_argument("--snapshot", default=None) + pf.set_defaults(func=cmd_performance) + + op = sub.add_parser("optimize", help="mean-CVaR MILP optimisation") + op.add_argument("--request", help="JSON file with an OptimizationRequest (fields override the flags)") + op.add_argument("--cash", type=float, default=1_000_000.0) + op.add_argument("--holdings", help='JSON dict {"NESN.SW": 100, ...} -> rebalancing mode') + op.add_argument("--symbols", nargs="*", default=None) + op.add_argument("--max-assets", type=int, default=100) + op.add_argument("--horizon", default="week") + op.add_argument("--confidence", type=float, default=0.95) + op.add_argument("--cvar-limit", type=float, default=None, help="max CVaR in CHF over the horizon") + op.add_argument("--scenario-model", default="empirical", choices=["gbm", "empirical", "gaussian_exact"]) + op.add_argument("--n-scenarios", type=int, default=None, + help="default: 16 per candidate within [4096, 16384]; rounded up to a power of two") + op.add_argument("--sampling", default="sobol", choices=["sobol", "mc", "antithetic", "lhs", "balanced"]) + from quantcore.optimization.service import IMPORTANCE_SAMPLING_MODES # lazy, like every command's + + op.add_argument("--importance-sampling", default="auto", + choices=list(IMPORTANCE_SAMPLING_MODES), + help="'market': tilt towards the tail of the cap-weighted portfolio; " + "'adaptive': towards a first-pass solution") + op.add_argument("--row-generation", default="auto", choices=["auto", "on", "off"]) + op.add_argument("--solver", default="auto", choices=["auto", "cuopt", "highs"]) + op.add_argument("--snapshot", default=None) + op.add_argument("--json", action="store_true", help="print the full result as JSON") + op.set_defaults(func=cmd_optimize) + + sv = sub.add_parser("serve", help="run the HTTP API and the browser test bench (/ui/)") + sv.add_argument("--host", default=None) + sv.add_argument("--port", type=int, default=None) + sv.set_defaults(func=cmd_serve) + return p + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + return args.func(args) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/quantcore/client.py b/quantcore/client.py new file mode 100644 index 0000000..d8e583d --- /dev/null +++ b/quantcore/client.py @@ -0,0 +1,314 @@ +"""HTTP client of the quant-core API -- the package seen from the outside. + +The services of :mod:`quantcore` can be used in two ways. In-process, by importing +``MarketDataService``, ``RiskService``, ``OptimizationService`` and friends, which needs the data +directory on the same machine. Or over HTTP, against a running ``quantcore serve`` (or the container +on the DGX Spark), which needs nothing but a URL -- this module. + +:class:`QuantcoreClient` has one method per endpoint, passes the request bodies through unchanged +(the field names are exactly those of ``quantcore.api.schemas``, so the interactive docs at +``/docs`` are its reference) and returns the parsed JSON. It adds no logic and no defaults of its +own beyond the ones the API already applies: what comes back is what the services computed. + + from quantcore.client import QuantcoreClient + + qc = QuantcoreClient("http://spark-6d4a:5001") + qc.health()["store"]["last_price_date"] + qc.asset_risk(["NESN.SW", "AAPL"], horizon="week", drift="zero") + res = qc.optimize(cash=1_000_000, cvar_limit=40_000, horizon="week", risk_drift="zero") + res["status"], res["cvar"], len(res["positions"]) + +Errors: any answer that is not 2xx raises :class:`QuantcoreError` carrying the status code and the +API's ``detail`` message; an unreachable server raises it too, naming the URL that was tried. The +methods that start background work (``start_setup``, ``start_update``) return immediately with a job +id -- :meth:`QuantcoreClient.wait_for_job` polls it to completion. + +The transport is injectable (``session=``): anything with a ``request(method, url, params=, +json=, timeout=)`` method that returns an object with ``status_code``, ``json()`` and ``text``. +``requests.Session`` is the default; ``fastapi.testclient.TestClient`` satisfies it too, which is +how the tests exercise this client against the app in-process. +""" + +from __future__ import annotations + +import os +import time +from typing import Any, Self + +import requests + +DEFAULT_URL = "http://127.0.0.1:5001" +DEFAULT_TIMEOUT = 900.0 # an optimisation may legitimately run for minutes +JOB_STATES_DONE = ("done", "failed") + + +def default_url() -> str: + """``QUANTCORE_API_URL`` if set, else ``http://127.0.0.1:5001``.""" + return os.environ.get("QUANTCORE_API_URL", DEFAULT_URL) + + +class QuantcoreError(RuntimeError): + """An API answer that is not 2xx, or a server that could not be reached. + + ``status_code`` is ``None`` when the request never got an answer (connection refused, timeout, + DNS failure) -- distinguishing "the service said no" from "there is no service there". + """ + + def __init__(self, message: str, *, status_code: int | None = None, method: str = "", path: str = ""): + super().__init__(message) + self.status_code = status_code + self.method = method + self.path = path + + @property + def unreachable(self) -> bool: + return self.status_code is None + + +def _retrying_session() -> requests.Session: + """A session that retries an idempotent request (GET/HEAD/OPTIONS) when the connection drops + before the answer arrives -- typically a kept-alive connection that the server closed between two + calls: uvicorn closes idle connections after 5 s, which is also the default job poll interval. + POSTs are never retried after they were sent: an optimisation or a job must not run twice.""" + from requests.adapters import HTTPAdapter + from urllib3.util.retry import Retry + + retry = Retry(total=2, connect=2, read=2, status=0, redirect=0, other=0, backoff_factor=0.2, + allowed_methods=frozenset({"GET", "HEAD", "OPTIONS"}), raise_on_status=False) + session = requests.Session() + adapter = HTTPAdapter(max_retries=retry) + session.mount("http://", adapter) + session.mount("https://", adapter) + return session + + +class QuantcoreClient: + """Client of one quant-core API instance. + + ``base_url``: e.g. ``http://127.0.0.1:5001`` or ``http://spark-6d4a:5001``; a trailing slash and + a trailing ``/ui`` are stripped, so a URL copied out of the browser works. + ``timeout``: seconds per request, overridable per call (``timeout=`` on any method). + """ + + def __init__(self, base_url: str | None = None, timeout: float = DEFAULT_TIMEOUT, session: Any = None): + self.base_url = (base_url or default_url()).rstrip("/").removesuffix("/ui") + self.timeout = float(timeout) + self._session = session + self._owns_session = session is None + + # -- transport ------------------------------------------------------------------------------ + @property + def session(self) -> Any: + if self._session is None: + self._session = _retrying_session() + return self._session + + def close(self) -> None: + if self._owns_session and self._session is not None: + self._session.close() + self._session = None + + def __enter__(self) -> Self: + return self + + def __exit__(self, *exc: object) -> None: + self.close() + + def request(self, method: str, path: str, *, params: dict | None = None, json: Any = None, + timeout: float | None = None) -> Any: + """One call against the API. ``params``/``json`` entries that are ``None`` are dropped, so a + method can pass every optional argument and let the API apply its own defaults.""" + if params: + params = {k: v for k, v in params.items() if v is not None} + if isinstance(json, dict): + json = {k: v for k, v in json.items() if v is not None} + url = f"{self.base_url}{path}" + try: + res = self.session.request(method, url, params=params, json=json, + timeout=timeout if timeout is not None else self.timeout) + except requests.RequestException as exc: + raise QuantcoreError(f"cannot reach the quant-core API at {url}: {exc}", + method=method, path=path) from exc + if not 200 <= res.status_code < 300: + raise QuantcoreError(f"{method} {path} -> {res.status_code}: {_detail(res)}", + status_code=res.status_code, method=method, path=path) + return res.json() + + def get(self, path: str, **kw: Any) -> Any: + return self.request("GET", path, **kw) + + def post(self, path: str, **kw: Any) -> Any: + return self.request("POST", path, **kw) + + # -- system --------------------------------------------------------------------------------- + def health(self, **kw: Any) -> dict: + """Store status (symbols, days, last price date), cuOpt reachability, base currency.""" + return self.get("/health", **kw) + + def openapi(self, **kw: Any) -> dict: + """The OpenAPI schema -- the machine-readable list of every endpoint and field.""" + return self.get("/openapi.json", **kw) + + # -- data ----------------------------------------------------------------------------------- + def universe(self, region: str | None = None, sector: str | None = None, country: str | None = None, + **kw: Any) -> list[dict]: + """The investable universe, one row per stock (metadata, market cap, coverage).""" + return self.get("/universe", params={"region": region, "sector": sector, "country": country}, **kw) + + def symbol(self, symbol: str, **kw: Any) -> dict: + """One universe row.""" + return self.get(f"/universe/{symbol}", **kw) + + def symbols_table(self, region: str | None = None, sector: str | None = None, country: str | None = None, + search: str | None = None, limit: int = 200, sort: str = "market_cap", **kw: Any) -> dict: + """The universe as a table plus the filter facets (counts per region, sector, country) -- + one call for a stock picker.""" + return self.get("/data/symbols", params={"region": region, "sector": sector, "country": country, + "search": search, "limit": limit, "sort": sort}, **kw) + + def prices(self, symbols: list[str], start: str | None = None, end: str | None = None, + currency: str = "base", freq: str = "daily", **kw: Any) -> dict: + """Adjusted closes as ``{"index": [...], "columns": [...], "data": [[...]]}``.""" + return self.get("/prices", params={"symbols": ",".join(symbols), "start": start, "end": end, + "currency": currency, "freq": freq}, **kw) + + def returns(self, symbols: list[str], start: str | None = None, end: str | None = None, + currency: str = "base", freq: str = "daily", **kw: Any) -> dict: + """Log-returns in the same frame layout as :meth:`prices`.""" + return self.get("/returns", params={"symbols": ",".join(symbols), "start": start, "end": end, + "currency": currency, "freq": freq}, **kw) + + def market_index(self, start: str | None = None, end: str | None = None, freq: str = "weekly", + **kw: Any) -> dict: + """The cap-weighted broad-market index: its return series and annualised mean/volatility.""" + return self.get("/market/index", params={"start": start, "end": end, "freq": freq}, **kw) + + def market_weights(self, max_assets: int | None = None, **kw: Any) -> dict: + """Current market-capitalisation weights (the Black-Litterman equilibrium portfolio).""" + return self.get("/market/weights", params={"max_assets": max_assets}, **kw) + + # -- risk ----------------------------------------------------------------------------------- + def asset_risk(self, symbols: list[str], **body: Any) -> list[dict]: + """Per-stock volatility, VaR and CVaR (normal and historical, rolling and EWM). + + ``horizon``, ``freq``, ``window``, ``lam``, ``confidence``, ``currency``, and: + ``drift="zero"`` leaves the estimated mean out of the parametric figures (the market-risk + convention, and the only sane choice at long horizons), ``space="value"`` reports them as a + fraction of the position's value instead of as log-returns. + """ + return self.post("/risk/assets", json={"symbols": symbols, **body}) + + def risk_series(self, symbol: str, **body: Any) -> dict: + """Rolling and EWM volatility / VaR / CVaR through time for one stock.""" + return self.post("/risk/series", json={"symbol": symbol, **body}) + + def covariance(self, symbols: list[str], **body: Any) -> dict: + """Covariance matrix as measured (``method="rolling"|"ewm"``, ``window``, ``lam``, + ``shrinkage``). For the estimator the *models* use, see :meth:`model_covariance`.""" + return self.post("/risk/covariance", json={"symbols": symbols, **body}) + + def correlation(self, symbols: list[str], **body: Any) -> dict: + """Correlation matrix, same parameters as :meth:`covariance`.""" + return self.post("/risk/correlation", json={"symbols": symbols, **body}) + + def portfolio_risk(self, holdings: dict[str, float], **body: Any) -> dict: + """VaR/CVaR of a portfolio held in *share numbers* (plus ``cash``), normal or historical.""" + return self.post("/risk/portfolio", json={"holdings": holdings, **body}) + + # -- performance ---------------------------------------------------------------------------- + def asset_performance(self, symbols: list[str], **body: Any) -> list[dict]: + """Annualised return, volatility, Sharpe, Sortino, drawdown, Calmar, skew, kurtosis.""" + return self.post("/performance/assets", json={"symbols": symbols, **body}) + + def rolling_performance(self, symbol: str, **body: Any) -> dict: + """Rolling (``window``) or exponentially weighted (``lam``) performance series.""" + return self.post("/performance/rolling", json={"symbol": symbol, **body}) + + def portfolio_performance(self, holdings: dict[str, float], **body: Any) -> dict: + """Performance of a share-based portfolio, rebalanced or buy-and-hold.""" + return self.post("/performance/portfolio", json={"holdings": holdings, **body}) + + # -- models --------------------------------------------------------------------------------- + def expected_returns(self, **body: Any) -> dict: + """Black-Litterman expected returns: equilibrium ``pi``, the posterior, and every + intermediate quantity (``delta``, ``tau``, the calibrated ``omega``). ``views`` are the same + objects ``/optimize`` takes, so a view can be inspected before it is optimised on.""" + return self.post("/model/expected-returns", json=body) + + def model_covariance(self, symbols: list[str], **body: Any) -> dict: + """The covariance the scenario generator and the optimiser use, with its conditioning + (eigenvalues, applied shrinkage, effective observations).""" + return self.post("/model/covariance", json={"symbols": symbols, **body}) + + def scenarios(self, symbols: list[str], **body: Any) -> dict: + """A scenario set and its statistics: per-stock moments, the P&L distribution of a + portfolio (``holdings`` or ``weights`` + ``value``), histograms, VaR/CVaR, and the + fingerprint (``hash``, ``shock_hash``) that identifies the draws.""" + return self.post("/model/scenarios", json={"symbols": symbols, **body}) + + # -- optimisation --------------------------------------------------------------------------- + def optimize(self, **body: Any) -> dict: + """Mean-CVaR MILP in integer shares. ``mode="new"`` invests ``cash``; ``mode="rebalance"`` + starts from ``holdings`` and pays transaction ``costs``, and the result carries a ``before`` + block so the two states can be compared. + + The result names the solver, the status, the MIP gap, the reproducibility level, the + scenario key and hash, and the provenance of every model input. Can run for minutes: pass + ``time_limit`` (seconds, the budget of the whole request) and a generous ``timeout=``. + """ + return self.post("/optimize", json=body) + + # -- jobs and snapshots --------------------------------------------------------------------- + def start_setup(self, offline: bool = False, max_per_region: int | None = None, **kw: Any) -> dict: + """Build the universe and download all history (~15 min). Returns a job id at once.""" + return self.post("/jobs/setup", params={"offline": offline, "max_per_region": max_per_region}, **kw) + + def start_update(self, full: bool = False, **kw: Any) -> dict: + """Fetch the newest prices and FX rates. Returns a job id at once.""" + return self.post("/jobs/update", params={"full": full}, **kw) + + def jobs(self, **kw: Any) -> list[dict]: + return self.get("/jobs", **kw) + + def job(self, job_id: str, **kw: Any) -> dict: + """One job: ``queued`` | ``running`` | ``done`` | ``failed``, with its report or error.""" + return self.get(f"/jobs/{job_id}", **kw) + + def wait_for_job(self, job_id: str, poll: float = 5.0, deadline: float | None = None, + on_poll: Any = None) -> dict: + """Poll a job until it is ``done`` or ``failed``; returns the final job record. + + ``deadline``: seconds to wait before giving up (``None``: forever). ``on_poll``: a callable + invoked with each intermediate record, for progress output. + """ + started = time.monotonic() + while True: + record = self.job(job_id) + if record.get("status") in JOB_STATES_DONE: + return record + if deadline is not None and time.monotonic() - started > deadline: + raise QuantcoreError(f"job {job_id} still {record.get('status')} after {deadline:.0f} s", + method="GET", path=f"/jobs/{job_id}") + if on_poll is not None: + on_poll(record) + time.sleep(poll) + + def snapshots(self, **kw: Any) -> list[str]: + return self.get("/snapshots", **kw) + + def create_snapshot(self, name: str | None = None, **kw: Any) -> dict: + """Freeze universe, prices and FX under a name, for reproducible demos and grading.""" + return self.post("/snapshots", json={"name": name}, **kw) + + +def _detail(res: Any) -> str: + """The API's error message: FastAPI puts it in ``detail``, validation errors in a list.""" + try: + body = res.json() + except Exception: # noqa: BLE001 (not JSON: a proxy error page, a truncated answer) + return (getattr(res, "text", "") or "")[:500] + detail = body.get("detail", body) if isinstance(body, dict) else body + if isinstance(detail, list): # 422: [{"loc": [...], "msg": ..., "type": ...}, ...] + return "; ".join(f"{'.'.join(str(p) for p in d.get('loc', []))}: {d.get('msg')}" + for d in detail if isinstance(d, dict))[:500] + return str(detail)[:500] diff --git a/quantcore/config.py b/quantcore/config.py new file mode 100644 index 0000000..71401f0 --- /dev/null +++ b/quantcore/config.py @@ -0,0 +1,125 @@ +"""Central configuration (environment variables with the ``QUANTCORE_`` prefix, or a ``.env`` file). + +Every service receives a :class:`Settings` instance -- nothing reads the environment on its own, +which keeps the services testable (tests pass a ``Settings(data_dir=tmp_path)``). +""" + +from __future__ import annotations + +import logging +from functools import lru_cache +from pathlib import Path + +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + """Runtime settings of the quant core.""" + + model_config = SettingsConfigDict(env_prefix="QUANTCORE_", env_file=".env", extra="ignore") + + # --- storage --------------------------------------------------------------------------- + data_dir: Path = Field(default=Path("data"), description="Root of all persisted data.") + + # --- universe -------------------------------------------------------------------------- + n_us: int = Field(default=500, description="Largest N US stocks (S&P 500 candidates).") + n_europe: int = Field(default=500, description="Largest N European stocks (STOXX 600 candidates).") + history_start: str = Field(default="2004-12-01", description="First date requested from Yahoo.") + required_history_from: str = Field( + default="2005-01-31", + description="A stock is kept only if it has a price on or before this date ('data since 2005').", + ) + min_price_coverage: float = Field( + default=0.90, description="Minimum share of union-calendar days with a price since the first date." + ) + max_missing_run: int = Field( + default=20, description="Longest tolerated gap (business days) in a price series since the required date." + ) + + # --- market conventions ---------------------------------------------------------------- + base_currency: str = Field(default="CHF", description="Portfolio / reporting currency.") + home_market: str = Field(default="Switzerland", description="Home market of the investor.") + risk_free_rate: float = Field(default=0.0, description="Annual risk-free rate in the base currency.") + trading_days_per_year: int = 252 + weeks_per_year: int = 52 + + # --- Yahoo access ---------------------------------------------------------------------- + yahoo_batch_size: int = Field(default=100, description="Tickers per bulk download request.") + yahoo_timeout: float = Field(default=30.0, description="Seconds per HTTP request.") + yahoo_max_retries: int = 4 + yahoo_backoff: float = Field(default=2.0, description="Base of the exponential back-off (seconds).") + yahoo_info_pause: float = Field(default=0.15, description="Pause between ticker-info requests (s).") + + # --- cuOpt ----------------------------------------------------------------------------- + cuopt_url: str = Field( + default="http://127.0.0.1:5000", + description="cuOpt REST endpoint. The default is a cuOpt container on the same machine (the " + "standalone DGX Spark setup, deploy/apptainer/); point it elsewhere for a remote one.") + cuopt_time_limit: float = Field(default=120.0, description="MILP time limit in seconds.") + cuopt_poll_interval: float = 1.0 + cuopt_timeout: float = Field(default=900.0, description="Max seconds to wait for a solution.") + cuopt_request_timeout: float = Field(default=60.0, description="Seconds per HTTP call to cuOpt.") + cuopt_upload_seconds_per_mb: float = Field( + default=1.0, description="Added to the submit timeout per MB of request body. cuOpt accepts about " + "10 MB/s, so this is a 10x margin; a client-side timeout during the upload " + "would otherwise surface as an aborted job.") + solver_fallback: bool = Field( + default=True, description="Fall back to the local HiGHS MILP solver (scipy) when cuOpt is unreachable." + ) + cuopt_threads: int | None = Field(default=None, description="CPU threads of the cuOpt MIP solver (None: server default).") + cuopt_msgpack: bool = Field(default=True, description="Send cuOpt requests as msgpack when the package is available.") + + # --- MILP tolerances (reproducibility) --------------------------------------------------- + mip_rel_gap: float = Field(default=1e-4, description="Relative MIP gap for HiGHS and cuOpt.") + mip_abs_gap: float = Field(default=0.01, description="Absolute MIP gap in money units (cuOpt).") + + # --- misc ------------------------------------------------------------------------------ + log_level: str = "INFO" + api_host: str = Field( + default="127.0.0.1", + description="Interface `quantcore serve` binds to: localhost only by default; the Docker images " + "pass 0.0.0.0 to publish the port.") + api_port: int = 5001 + enable_ui: bool = Field( + default=True, + description="Serve the static test bench at /ui/ (and redirect / to it). Set false for a " + "deployment that only exposes the JSON API and /docs -- the same image then " + "serves a front end built by someone else.", + ) + + # derived paths ------------------------------------------------------------------------- + @property + def universe_dir(self) -> Path: + return self.data_dir / "universe" + + @property + def prices_dir(self) -> Path: + return self.data_dir / "prices" + + @property + def fx_dir(self) -> Path: + return self.data_dir / "fx" + + @property + def snapshots_dir(self) -> Path: + return self.data_dir / "snapshots" + + @property + def jobs_dir(self) -> Path: + return self.data_dir / "jobs" + + def ensure_dirs(self) -> None: + for d in (self.universe_dir, self.prices_dir, self.fx_dir, self.snapshots_dir, self.jobs_dir): + d.mkdir(parents=True, exist_ok=True) + + +@lru_cache(maxsize=1) +def get_settings() -> Settings: + """Process-wide settings (cached). Tests construct :class:`Settings` directly instead.""" + settings = Settings() + logging.basicConfig( + level=getattr(logging, settings.log_level.upper(), logging.INFO), + format="%(asctime)s %(levelname)s %(name)s: %(message)s", + ) + return settings diff --git a/quantcore/measurement/__init__.py b/quantcore/measurement/__init__.py new file mode 100644 index 0000000..58647a5 --- /dev/null +++ b/quantcore/measurement/__init__.py @@ -0,0 +1,18 @@ +"""Risk and performance measurement on log-returns (moving windows and exponential weighting).""" + +from quantcore.measurement.performance import PerformanceService, performance_table +from quantcore.measurement.returns import ( + FREQUENCIES, + HORIZONS, + align_prices, + horizon_periods, + log_returns, + periods_per_year, + resample_prices, +) +from quantcore.measurement.risk import RiskService, asset_risk_table + +__all__ = [ + "FREQUENCIES", "HORIZONS", "align_prices", "horizon_periods", "log_returns", "periods_per_year", + "resample_prices", "RiskService", "asset_risk_table", "PerformanceService", "performance_table", +] diff --git a/quantcore/measurement/estimators.py b/quantcore/measurement/estimators.py new file mode 100644 index 0000000..a5f8146 --- /dev/null +++ b/quantcore/measurement/estimators.py @@ -0,0 +1,228 @@ +"""Moving-window and exponentially weighted estimators of mean, volatility, covariance, correlation. + +Two estimator families, both centred (the corresponding mean is subtracted): + +* **rolling** -- equally weighted window of ``window`` observations (sample covariance, ddof=1); +* **ewm** -- exponentially weighted with decay ``lam`` (RiskMetrics: weight of the + observation ``k`` periods back is proportional to ``lam**k``; ``lam=0.94`` daily, + ``lam=0.99`` for slow adaptation; effective window ~ 1/(1-lam)). + +Per-asset *time series* use pandas (``rolling`` / ``ewm``); full matrices are computed for a +single date (``*_cov``) or streamed date by date (``covariance_sequence``) because a +``T x N x N`` array does not fit in memory for 1000 assets. +""" + +from __future__ import annotations + +from collections.abc import Iterator + +import numpy as np +import pandas as pd + + +# --------------------------------------------------------------------------------------------- +# per-asset time series +# --------------------------------------------------------------------------------------------- +def rolling_mean(r: pd.DataFrame | pd.Series, window: int, min_periods: int | None = None): + return r.rolling(window, min_periods=min_periods or max(2, window // 2)).mean() + + +def rolling_vol(r, window: int, periods_per_year: int | None = None, min_periods: int | None = None): + """Rolling standard deviation, optionally annualised (x sqrt(periods per year)).""" + vol = r.rolling(window, min_periods=min_periods or max(2, window // 2)).std(ddof=1) + return vol * np.sqrt(periods_per_year) if periods_per_year else vol + + +def ewm_mean(r, lam: float, min_periods: int = 10): + return r.ewm(alpha=1.0 - lam, adjust=True, min_periods=min_periods).mean() + + +def ewm_vol(r, lam: float, periods_per_year: int | None = None, min_periods: int = 10): + """Exponentially weighted standard deviation (centred, weights normalised to one).""" + vol = np.sqrt(r.ewm(alpha=1.0 - lam, adjust=True, min_periods=min_periods).var(bias=True)) + return vol * np.sqrt(periods_per_year) if periods_per_year else vol + + +def rolling_quantile(r, window: int, q: float, min_periods: int | None = None): + return r.rolling(window, min_periods=min_periods or max(2, window // 2)).quantile(q) + + +# --------------------------------------------------------------------------------------------- +# matrices at one date +# --------------------------------------------------------------------------------------------- +def _window(r: pd.DataFrame, window: int | None, at=None) -> pd.DataFrame: + if at is not None: + r = r.loc[r.index <= pd.Timestamp(at)] + if window is not None: + r = r.iloc[-window:] + return r + + +def rolling_cov(r: pd.DataFrame, window: int, at=None, min_obs: int | None = None) -> pd.DataFrame: + """Sample covariance of the last ``window`` rows (rows with any NaN are dropped).""" + sub = _window(r, window, at).dropna() + min_obs = min_obs or max(3, window // 2) + if len(sub) < min_obs: + raise ValueError(f"only {len(sub)} complete observations in the window (need {min_obs})") + # atleast_2d: np.cov of a single column returns a 0-d array, which DataFrame rejects -- a + # one-asset portfolio is a legitimate request (and the simplest one a caller makes) + cov = np.atleast_2d(np.cov(sub.values, rowvar=False, ddof=1)) + return pd.DataFrame(cov, index=r.columns, columns=r.columns) + + +def ewm_weights(n: int, lam: float) -> np.ndarray: + """Normalised weights for ``n`` observations, oldest first: ``lam**(n-1-t) / sum``.""" + w = lam ** np.arange(n - 1, -1, -1, dtype=float) + return w / w.sum() + + +def ewm_cov(r: pd.DataFrame, lam: float, at=None, max_obs: int | None = None, min_obs: int = 20) -> pd.DataFrame: + """Exponentially weighted covariance at ``at`` (default: last date), centred. + + ``max_obs`` truncates the history (weights below ``lam**max_obs`` are negligible; the + default keeps ``log(1e-4)/log(lam)`` observations, ~1500 for lam=0.994, ~150 for 0.94). + """ + max_obs = max_obs or int(np.ceil(np.log(1e-4) / np.log(lam))) + sub = _window(r, max_obs, at).dropna() + if len(sub) < min_obs: + raise ValueError(f"only {len(sub)} complete observations (need {min_obs})") + x = sub.values + w = ewm_weights(len(x), lam) + m = w @ x + xc = x - m + cov = (xc * w[:, None]).T @ xc + return pd.DataFrame(cov, index=r.columns, columns=r.columns) + + +def cov_to_corr(cov: pd.DataFrame) -> pd.DataFrame: + sd = np.sqrt(np.diag(cov.values)) + with np.errstate(divide="ignore", invalid="ignore"): + corr = cov.values / np.outer(sd, sd) + corr = np.clip(np.nan_to_num(corr), -1.0, 1.0) + np.fill_diagonal(corr, 1.0) + return pd.DataFrame(corr, index=cov.index, columns=cov.columns) + + +def rolling_corr(r: pd.DataFrame, window: int, at=None) -> pd.DataFrame: + return cov_to_corr(rolling_cov(r, window, at)) + + +def ewm_corr(r: pd.DataFrame, lam: float, at=None) -> pd.DataFrame: + return cov_to_corr(ewm_cov(r, lam, at)) + + +def shrinkage_intensity(x: np.ndarray) -> float: + """Data-driven intensity of the shrinkage towards the diagonal (Schaefer & Strimmer 2005). + + For centred observations ``x`` (n x N) the optimal linear shrinkage of the sample covariance + towards its diagonal is ``lambda* = sum_{i!=j} Var(s_ij) / sum_{i!=j} s_ij^2`` (clipped to + [0, 1]) -- the Ledoit-Wolf estimator with the "diagonal, unequal variances" target. It grows + with N / n, i.e. exactly when the sample covariance becomes ill-conditioned. + """ + x = np.asarray(x, dtype=float) + n = len(x) + if n < 3: + return 1.0 + xc = x - x.mean(axis=0) + w_bar = xc.T @ xc / n # mean of the products w_kij + sq = (xc**2).T @ (xc**2) # sum_k w_kij^2 + var_s = n / (n - 1) ** 3 * (sq - n * w_bar**2) + s = n / (n - 1) * w_bar + off = ~np.eye(len(s), dtype=bool) + denom = float((s[off] ** 2).sum()) + if denom <= 0: + return 1.0 + return float(np.clip(var_s[off].sum() / denom, 0.0, 1.0)) + + +def effective_observations(method: str, window: int, lam: float) -> int: + return int(window) if method == "rolling" else int(round(3.0 / (1.0 - lam))) + + +def resolve_shrinkage(shrinkage, r: pd.DataFrame, method: str, window: int, lam: float, at=None) -> float: + """``'auto'`` -> Schaefer-Strimmer intensity on the (effective) estimation window; numbers pass through.""" + if shrinkage in (None, 0, 0.0, "none"): + return 0.0 + if isinstance(shrinkage, str): + if shrinkage != "auto": + raise ValueError("shrinkage must be a number in [0, 1] or 'auto'") + sub = _window(r, effective_observations(method, window, lam), at).dropna() + return shrinkage_intensity(sub.values) + return float(shrinkage) + + +def covariance(r: pd.DataFrame, method: str = "rolling", window: int = 200, lam: float = 0.94, at=None, + shrinkage=0.0) -> pd.DataFrame: + """Dispatch on ``method`` (``'rolling'`` | ``'ewm'``) with optional shrinkage to the diagonal. + + Linear shrinkage ``(1-s) * Sigma + s * diag(Sigma)`` keeps the variances and pulls the + correlations towards zero -- needed when N is not small against the effective window. + ``shrinkage='auto'`` estimates the intensity from the data (:func:`shrinkage_intensity`). + """ + if method == "rolling": + cov = rolling_cov(r, window, at) + elif method == "ewm": + cov = ewm_cov(r, lam, at) + else: + raise ValueError("method must be 'rolling' or 'ewm'") + return shrink(cov, resolve_shrinkage(shrinkage, r, method, window, lam, at)) + + +def shrink(cov: pd.DataFrame, intensity: float) -> pd.DataFrame: + if intensity <= 0: + return cov + if not 0 < intensity <= 1: + raise ValueError("shrinkage intensity must be in [0, 1]") + diag = np.diag(np.diag(cov.values)) + return pd.DataFrame((1 - intensity) * cov.values + intensity * diag, index=cov.index, columns=cov.columns) + + +# --------------------------------------------------------------------------------------------- +# streamed sequences of matrices (for the innovation filter) +# --------------------------------------------------------------------------------------------- +def covariance_sequence( + r: pd.DataFrame, method: str, window: int = 200, lam: float = 0.94, warmup: int | None = None, + shrinkage=0.0, +) -> Iterator[tuple[pd.Timestamp, np.ndarray, np.ndarray]]: + """Yield ``(date_t, Sigma_{t-1}, r_t)`` for every ``t`` after the warm-up. + + ``Sigma_{t-1}`` uses data **up to and including t-1** -- the information set for + standardising the return of day ``t``. The rolling estimator recomputes the window at + every step (O(w N^2) each); the EWM estimator uses the exact recursion of the centred + RiskMetrics filter, so the cost per step is O(N^2). + """ + x = r.dropna().values.astype(float) + dates = r.dropna().index + n_obs, n = x.shape + shrinkage = resolve_shrinkage(shrinkage, r, method, window, lam) # one intensity for the whole sequence + if method == "rolling": + warmup = warmup or window + if n_obs <= warmup: + raise ValueError("not enough observations for the warm-up window") + for t in range(warmup, n_obs): + block = x[t - window : t] + cov = np.atleast_2d(np.cov(block, rowvar=False, ddof=1)) + yield dates[t], shrink_array(cov, shrinkage), x[t] + elif method == "ewm": + warmup = warmup or min(n_obs - 1, max(50, int(2 / (1 - lam)))) + if n_obs <= warmup: + raise ValueError("not enough observations for the warm-up window") + # initialise with the normalised-weight estimate over the warm-up block, then recurse + w = ewm_weights(warmup, lam) + m = w @ x[:warmup] + xc = x[:warmup] - m + cov = (xc * w[:, None]).T @ xc + alpha = 1.0 - lam + for t in range(warmup, n_obs): + yield dates[t], shrink_array(cov, shrinkage), x[t] + d = x[t] - m + m = m + alpha * d + cov = lam * cov + alpha * np.outer(d, x[t] - m) # Welford-style centred update + else: + raise ValueError("method must be 'rolling' or 'ewm'") + + +def shrink_array(cov: np.ndarray, intensity: float) -> np.ndarray: + if intensity <= 0: + return cov + return (1 - intensity) * cov + intensity * np.diag(np.diag(cov)) diff --git a/quantcore/measurement/performance.py b/quantcore/measurement/performance.py new file mode 100644 index 0000000..6278af3 --- /dev/null +++ b/quantcore/measurement/performance.py @@ -0,0 +1,145 @@ +"""Performance measurement on log-returns: return, volatility, Sharpe, drawdown and friends. + +All statistics are computed from one-period log-returns at a given frequency and annualised +with the number of periods per year (252 / 52 / 12). ``sharpe`` uses the annual risk-free rate +of the base currency. Rolling and exponentially weighted variants give time series. +""" + +from __future__ import annotations + +import numpy as np +import pandas as pd +from scipy import stats + +from quantcore.measurement import estimators as est +from quantcore.measurement.returns import periods_per_year, portfolio_log_returns + + +def cumulative_return(r: pd.DataFrame | pd.Series): + """Total simple return over the sample: ``exp(sum r) - 1``.""" + return np.expm1(r.sum()) + + +def annualised_return(r, ppy: int): + """Annualised log-return (arithmetic mean of log-returns x periods per year).""" + return r.mean() * ppy + + +def annualised_volatility(r, ppy: int): + return r.std(ddof=1) * np.sqrt(ppy) + + +def sharpe_ratio(r, ppy: int, risk_free: float = 0.0): + vol = annualised_volatility(r, ppy) + return (annualised_return(r, ppy) - risk_free) / vol + + +def sortino_ratio(r, ppy: int, risk_free: float = 0.0): + downside = r.where(r < 0, 0.0) + dd = np.sqrt((downside**2).mean() * ppy) + return (annualised_return(r, ppy) - risk_free) / dd + + +def drawdown_series(r: pd.Series | pd.DataFrame): + """Drawdown (fraction below the running peak) of the cumulative value path.""" + wealth = np.exp(r.fillna(0.0).cumsum()) + return wealth / wealth.cummax() - 1.0 + + +def max_drawdown(r): + return drawdown_series(r).min() + + +def calmar_ratio(r, ppy: int): + mdd = max_drawdown(r) + ann = np.expm1(annualised_return(r, ppy)) # geometric annual return + return ann / -mdd if np.ndim(mdd) == 0 else ann / -mdd + + +def performance_table(r: pd.DataFrame, freq: str = "daily", risk_free: float = 0.0) -> pd.DataFrame: + """One row per column of ``r`` with the standard performance statistics.""" + ppy = periods_per_year(freq) + out = pd.DataFrame(index=r.columns) + out["start"] = r.apply(lambda c: c.first_valid_index()) + out["end"] = r.index[-1] + out["n_obs"] = r.notna().sum() + out["cumulative_return"] = cumulative_return(r) + out["annualised_return_log"] = annualised_return(r, ppy) + out["annualised_return"] = np.expm1(out["annualised_return_log"]) + out["annualised_volatility"] = annualised_volatility(r, ppy) + out["sharpe"] = sharpe_ratio(r, ppy, risk_free) + out["sortino"] = sortino_ratio(r, ppy, risk_free) + out["max_drawdown"] = max_drawdown(r) + out["calmar"] = np.expm1(out["annualised_return_log"]) / -out["max_drawdown"] + out["skewness"] = r.apply(lambda c: stats.skew(c.dropna())) + out["excess_kurtosis"] = r.apply(lambda c: stats.kurtosis(c.dropna())) + out["hit_ratio"] = (r > 0).sum() / r.notna().sum() + out["best_period"] = r.max() + out["worst_period"] = r.min() + return out + + +def rolling_performance(r: pd.Series, window: int, freq: str = "daily", risk_free: float = 0.0) -> pd.DataFrame: + """Rolling annualised return, volatility, Sharpe and the running drawdown.""" + ppy = periods_per_year(freq) + df = pd.DataFrame(index=r.index) + mean = est.rolling_mean(r, window) + vol = est.rolling_vol(r, window) + df["return_ann"] = mean * ppy + df["volatility_ann"] = vol * np.sqrt(ppy) + df["sharpe"] = (df["return_ann"] - risk_free) / df["volatility_ann"] + df["cumulative_window"] = np.expm1(r.rolling(window, min_periods=window // 2).sum()) + df["drawdown"] = drawdown_series(r) + return df + + +def ewm_performance(r: pd.Series, lam: float, freq: str = "daily", risk_free: float = 0.0) -> pd.DataFrame: + ppy = periods_per_year(freq) + df = pd.DataFrame(index=r.index) + df["return_ann"] = est.ewm_mean(r, lam) * ppy + df["volatility_ann"] = est.ewm_vol(r, lam) * np.sqrt(ppy) + df["sharpe"] = (df["return_ann"] - risk_free) / df["volatility_ann"] + df["drawdown"] = drawdown_series(r) + return df + + +class PerformanceService: + """Deterministic performance measurement for assets and share-based portfolios.""" + + def __init__(self, market_data, risk_free: float = 0.0): + self.md = market_data + self.risk_free = risk_free + + def asset_performance(self, symbols, freq="daily", currency="base", start=None, end=None) -> pd.DataFrame: + r = self.md.returns(symbols, start=start, end=end, currency=currency, freq=freq) + return performance_table(r, freq, self.risk_free) + + def rolling(self, symbol, window=252, freq="daily", currency="base", start=None, end=None) -> pd.DataFrame: + r = self.md.returns([symbol], start=start, end=end, currency=currency, freq=freq)[symbol].dropna() + return rolling_performance(r, window, freq, self.risk_free) + + def ewm(self, symbol, lam=0.99, freq="daily", currency="base", start=None, end=None) -> pd.DataFrame: + r = self.md.returns([symbol], start=start, end=end, currency=currency, freq=freq)[symbol].dropna() + return ewm_performance(r, lam, freq, self.risk_free) + + def portfolio_returns(self, holdings: dict[str, float], freq="daily", currency="base", start=None, end=None, + rebalanced: bool = True) -> pd.Series: + """Log-returns of a share-based portfolio. + + ``rebalanced=True`` keeps today's value weights fixed (a constant-mix benchmark); + ``False`` is buy-and-hold with the given share numbers over the whole history. + """ + symbols = list(holdings) + px = self.md.prices(symbols, start=start, end=end, currency=currency, freq=freq) + shares = pd.Series(holdings, dtype=float).reindex(symbols) + if rebalanced: + values = shares * px.ffill().iloc[-1] + return portfolio_log_returns(np.log(px).diff().iloc[1:], (values / values.sum()).values) + value = (px.ffill() * shares).sum(axis=1) + return np.log(value).diff().iloc[1:].rename("portfolio") + + def portfolio_performance(self, holdings, freq="daily", currency="base", start=None, end=None, + rebalanced=True) -> pd.Series: + rp = self.portfolio_returns(holdings, freq, currency, start, end, rebalanced) + table = performance_table(rp.to_frame("portfolio"), freq, self.risk_free) + return table.iloc[0] diff --git a/quantcore/measurement/returns.py b/quantcore/measurement/returns.py new file mode 100644 index 0000000..6bab155 --- /dev/null +++ b/quantcore/measurement/returns.py @@ -0,0 +1,116 @@ +"""Log-returns at daily / weekly / monthly frequency, calendar alignment and horizons. + +Conventions used throughout the package: + +* prices are adjusted closes, one column per symbol, indexed by calendar date (tz-naive); +* the **union calendar** of all listings is used; a holiday on one exchange is filled with + the previous close (``align_prices``), which yields a zero return on that day; +* ``log_returns`` are ``ln(P_t / P_{t-1})``; a CHF return of a foreign stock is the local + log-return plus the FX log-return (see :mod:`quantcore.data.fx`); +* weekly returns are Friday-to-Friday, monthly returns month-end to month-end; +* a *horizon* (``'day'``, ``'week'``, ``'month'`` or an integer) is expressed in periods of the + chosen frequency (``horizon_periods``), assuming i.i.d. returns that add up over time. +""" + +from __future__ import annotations + +import numpy as np +import pandas as pd + +FREQUENCIES = {"daily": 252, "weekly": 52, "monthly": 12} +HORIZONS_DAYS = {"day": 1, "week": 5, "month": 21, "quarter": 63, "year": 252} +HORIZONS = list(HORIZONS_DAYS) + + +def periods_per_year(freq: str) -> int: + try: + return FREQUENCIES[freq] + except KeyError as exc: + raise ValueError(f"unknown frequency {freq!r}; use one of {list(FREQUENCIES)}") from exc + + +def horizon_periods(horizon: str | int, freq: str = "daily") -> int: + """Number of ``freq`` periods in ``horizon`` (``'week'`` at daily frequency -> 5).""" + if isinstance(horizon, (int, np.integer)): + if horizon < 1: + raise ValueError("horizon must be >= 1 period") + return int(horizon) + try: + days = HORIZONS_DAYS[horizon] + except KeyError as exc: + raise ValueError(f"unknown horizon {horizon!r}; use one of {HORIZONS} or an integer") from exc + periods = days * periods_per_year(freq) / FREQUENCIES["daily"] + if periods < 1: + raise ValueError(f"horizon {horizon!r} is shorter than one {freq} period") + return int(round(periods)) + + +def align_prices(prices: pd.DataFrame, max_ffill: int | None = 5) -> pd.DataFrame: + """Union calendar with limited forward-fill of exchange holidays; leading NaNs stay NaN.""" + prices = prices.sort_index() + # keep business days only (Yahoo occasionally returns a weekend row for some exchanges) + prices = prices[prices.index.dayofweek < 5] + return prices.ffill(limit=max_ffill) if max_ffill else prices + + +def _month_end_alias() -> str: + """``'ME'`` from pandas 2.2 on, ``'M'`` before it (pandas 3 dropped the old spelling).""" + for alias in ("ME", "M"): + try: + pd.tseries.frequencies.to_offset(alias) + except ValueError: + continue + return alias + raise RuntimeError("no month-end frequency alias accepted by this pandas") + + +MONTH_END = _month_end_alias() + + +def resample_prices(prices: pd.DataFrame, freq: str) -> pd.DataFrame: + """Daily prices -> the last observation of each week (Fri) or month.""" + if freq == "daily": + return prices + if freq == "weekly": + return prices.resample("W-FRI").last() + if freq == "monthly": + return prices.resample(MONTH_END).last() + raise ValueError(f"unknown frequency {freq!r}") + + +def log_returns(prices: pd.DataFrame | pd.Series) -> pd.DataFrame | pd.Series: + """``ln(P_t / P_{t-1})``; the first row (all NaN) is dropped.""" + r = np.log(prices).diff() + return r.iloc[1:] if len(r) else r + + +def simple_returns(prices: pd.DataFrame | pd.Series) -> pd.DataFrame | pd.Series: + return prices.pct_change().iloc[1:] + + +def horizon_returns(r: pd.DataFrame | pd.Series, periods: int, overlapping: bool = True): + """Aggregate one-period log-returns to ``periods``-period log-returns (sums). + + Overlapping windows keep ``T - periods + 1`` observations (more data, autocorrelated); + non-overlapping blocks are independent under the i.i.d. assumption but fewer. + """ + if periods == 1: + return r + if overlapping: + return r.rolling(periods).sum().iloc[periods - 1 :] + blocks = np.arange(len(r))[::-1] // periods # block id counted from the end + grouped = r.groupby(blocks[::-1]).sum() + counts = r.groupby(blocks[::-1]).size() + return grouped[counts == periods] + + +def portfolio_log_returns(r: pd.DataFrame, weights: pd.Series | np.ndarray) -> pd.Series: + """Log-return of a portfolio with weights fixed at the start of each period. + + Exact aggregation: the simple portfolio return is the weighted sum of simple returns, + ``ln(1 + sum_i w_i (exp(r_i) - 1))``. Weights are normalised to the invested fraction; a + cash remainder (``1 - sum w``) earns zero. + """ + w = np.asarray(weights, dtype=float) + simple = np.expm1(r.fillna(0.0).values) @ w + return pd.Series(np.log1p(simple), index=r.index, name="portfolio") diff --git a/quantcore/measurement/risk.py b/quantcore/measurement/risk.py new file mode 100644 index 0000000..d099441 --- /dev/null +++ b/quantcore/measurement/risk.py @@ -0,0 +1,401 @@ +"""Value-at-Risk and Conditional Value-at-Risk (expected shortfall) on log-returns. + +Conventions +----------- +* Losses are positive numbers: for a log-return ``r`` over the horizon, the loss in + return space is ``-r``; in money it is ``V0 * (1 - exp(r))``. +* ``VaR_beta`` is the ``beta``-quantile of the loss distribution, ``CVaR_beta`` the expected + loss beyond it (``beta`` = 0.95 or 0.99). +* Horizons: i.i.d. returns scale the mean with ``h`` and the volatility with ``sqrt(h)`` + (parametric); the historical method aggregates ``h`` consecutive returns. + +Methods +------- +* ``normal`` -- Gaussian (GBM) with the chosen volatility estimator; +* ``historical`` -- empirical quantiles of the (window of) realised returns; +* time series of both, driven by moving-window or exponentially weighted volatilities. + +Money amounts ("an absolute level for a period") use the exact mapping of a log-return to a +P&L, so a 99 % CVaR of CHF 12 000 over a week reads: *in the worst 1 % of weeks the average +loss is CHF 12 000*. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np +import pandas as pd +from scipy import stats + +from quantcore.measurement import estimators as est +from quantcore.measurement.returns import horizon_periods, horizon_returns, periods_per_year, portfolio_log_returns + +DEFAULT_CONFIDENCE = (0.95, 0.99) + + +# --------------------------------------------------------------------------------------------- +# closed forms and empirical estimators (return space, positive losses) +# --------------------------------------------------------------------------------------------- +def normal_var(mu: float | np.ndarray, sigma: float | np.ndarray, beta: float): + """Gaussian VaR of the loss ``-r`` with ``r ~ N(mu, sigma^2)``.""" + return -mu + stats.norm.ppf(beta) * sigma + + +def normal_cvar(mu, sigma, beta: float): + """Gaussian expected shortfall: ``-mu + sigma * phi(z_beta) / (1 - beta)``.""" + z = stats.norm.ppf(beta) + return -mu + sigma * stats.norm.pdf(z) / (1.0 - beta) + + +def normal_value_var(mu, sigma, beta: float): + """VaR as a *fraction of the position's value* when the log-return is ``N(mu, sigma^2)``. + + A quantile transforms exactly under the monotone map ``r -> 1 - e^r``, so this is simply + ``1 - exp(-VaR_log)`` -- no Ito term is involved. + """ + return 1.0 - np.exp(-normal_var(mu, sigma, beta)) + + +def normal_value_cvar(mu, sigma, beta: float): + """CVaR as a fraction of the position's value for a lognormal price. + + A CVaR is a tail *mean*, and ``1 - e^{-x}`` is concave, so ``1 - exp(-CVaR_log)`` is not the + expected value loss -- it overstates it (0.7 % relative at sigma = 0.30). The exact figure uses + the truncated lognormal moment ``E[e^r 1{r <= q}] = e^{mu + sigma^2/2} Phi(z_{1-beta} - sigma)``:: + + CVaR_value = 1 - e^{mu + sigma^2/2} Phi(z_{1-beta} - sigma) / (1 - beta) + + which is the same ``sigma^2/2`` correction that turns a log drift into an arithmetic one. + """ + mu = np.asarray(mu, dtype=float) + sigma = np.asarray(sigma, dtype=float) + z = stats.norm.ppf(1.0 - beta) + return 1.0 - np.exp(mu + 0.5 * sigma ** 2) * stats.norm.cdf(z - sigma) / (1.0 - beta) + + +def historical_var(r: pd.Series | pd.DataFrame | np.ndarray, beta: float): + """Empirical VaR: minus the ``(1 - beta)``-quantile of the returns.""" + if isinstance(r, (pd.Series, pd.DataFrame)): + return -r.quantile(1.0 - beta) + return -np.quantile(np.asarray(r), 1.0 - beta, axis=0) + + +def historical_cvar(r: pd.Series | pd.DataFrame | np.ndarray, beta: float): + """Empirical CVaR: minus the mean of the returns at or below the ``(1 - beta)``-quantile.""" + if isinstance(r, pd.DataFrame): + return pd.Series({c: historical_cvar(r[c].dropna(), beta) for c in r.columns}) + x = np.asarray(r.dropna() if isinstance(r, pd.Series) else r, dtype=float) + if x.ndim == 2: + return np.array([historical_cvar(x[:, j], beta) for j in range(x.shape[1])]) + if len(x) == 0: + return np.nan + q = np.quantile(x, 1.0 - beta) + tail = x[x <= q] + return -tail.mean() if len(tail) else -q + + +def scenario_cvar(losses: np.ndarray, beta: float, probabilities: np.ndarray | None = None) -> tuple[float, float]: + """(VaR, CVaR) of a discrete loss sample, consistent with the Rockafellar-Uryasev LP. + + With ``S`` equiprobable scenarios the CVaR is the mean of the ``(1-beta)S`` largest losses + (fractional tail weight handled exactly), which the LP formulation reproduces. With scenario + ``probabilities`` (importance sampling) the tail is the set of largest losses whose probability + adds up to ``1-beta``, the last one taken fractionally. + """ + losses = np.asarray(losses, dtype=float) + if probabilities is None: + losses = np.sort(losses)[::-1] + s = len(losses) + k = (1.0 - beta) * s + n_full = int(np.floor(k + 1e-9)) # tolerance: (1 - 0.95) * 1000 is 50.00000000000004 in floating point + frac = k - n_full if k - n_full > 1e-9 else 0.0 + var = losses[min(n_full, s - 1)] if frac > 0 or n_full == 0 else losses[n_full - 1] + tail = losses[:n_full].sum() + (frac * losses[n_full] if n_full < s else 0.0) + return float(var), float(tail / k) if k > 0 else float(losses[0]) + p = np.asarray(probabilities, dtype=float) + p = p / p.sum() + order = np.argsort(losses)[::-1] + sorted_l, w = losses[order], p[order] + tail_mass = 1.0 - beta + cum = np.cumsum(w) + k = int(np.searchsorted(cum, tail_mass - 1e-12, side="left")) + k = min(k, len(sorted_l) - 1) + var = float(sorted_l[k]) + full_mass = cum[k - 1] if k > 0 else 0.0 + frac = max(tail_mass - full_mass, 0.0) + cvar = (sorted_l[:k] @ w[:k] + frac * sorted_l[k]) / tail_mass + return var, float(cvar) + + +def weighted_quantile(x, q, weights: np.ndarray | None = None) -> np.ndarray: + """Quantiles of a sample that carries probability ``weights`` (importance sampling). + + Column-wise for a 2-D ``x``. Without weights this is ``np.quantile``; with them the weighted + CDF is interpolated at ``c_i = (sum_{j<=i} w_j - w_i / 2) / sum w``, the usual convention that + reproduces the unweighted result for equal weights. + """ + x = np.asarray(x, dtype=float) + q = np.atleast_1d(np.asarray(q, dtype=float)) + flat = x.ndim == 1 + xx = x[:, None] if flat else x + if weights is None: + out = np.quantile(xx, q, axis=0) + else: + w = np.asarray(weights, dtype=float) + order = np.argsort(xx, axis=0) + sorted_x = np.take_along_axis(xx, order, axis=0) + sorted_w = w[order] + cum = np.cumsum(sorted_w, axis=0) - 0.5 * sorted_w + cum = cum / sorted_w.sum(axis=0) + out = np.stack([[np.interp(qq, cum[:, j], sorted_x[:, j]) for j in range(xx.shape[1])] for qq in q]) + return out[:, 0] if flat else out + + +def loss_to_money(loss_return, value: float): + """Exact money loss for a loss in log-return space: ``V0 * (1 - exp(-loss))``.""" + return value * (1.0 - np.exp(-loss_return)) + + +def value_loss(loss_return): + """A loss in log-return space as a fraction of the position's value: ``1 - exp(-x)``. + + A VaR of 0.30 in log terms is a loss of 25.9 % of the value, never 30 %; the difference is + negligible for a day and material for a quarter. It can never reach 100 %. + """ + return 1.0 - np.exp(-np.asarray(loss_return, dtype=float)) + + +RETURN_SPACES = ("log", "value") +DRIFT_MODES = ("estimated", "zero") + + +# --------------------------------------------------------------------------------------------- +# per-asset table at one date +# --------------------------------------------------------------------------------------------- +def asset_risk_table( + r: pd.DataFrame, + freq: str = "daily", + window: int = 200, + lam: float = 0.94, + confidence=DEFAULT_CONFIDENCE, + horizon: str | int = "day", + hist_window: int | None = None, + at=None, + space: str = "log", + drift: str = "estimated", +) -> pd.DataFrame: + """Volatility, VaR and CVaR per asset for one horizon (rows: assets). + + Columns: ``vol_rolling_ann``, ``vol_ewm_ann`` (annualised), then for every confidence + level ``var{cc}_normal_rolling``, ``cvar{cc}_normal_rolling``, ``var{cc}_normal_ewm``, + ``cvar{cc}_normal_ewm``, ``var{cc}_hist``, ``cvar{cc}_hist`` (positive = loss). + + ``space='log'`` reports them in log-return units (the modelling space); ``'value'`` as a + fraction of the position's value, which cannot reach 100 %. The value figures are *computed* + in that space -- the normal ones from the lognormal closed forms, the historical ones from the + simple returns of the same window -- because only the VaR survives the map ``1 - e^{-x}`` + unchanged; a CVaR is a tail mean and would be overstated by converting it afterwards. + + ``drift='estimated'`` carries the window's mean into the parametric VaR/CVaR (``-mu h``); + harmless for a day, dominant for a quarter, where the mean is estimated far less precisely + than the volatility -- a stock in a strong rally then shows a *smaller* VaR than a quiet one. + ``'zero'`` is the usual market-risk convention: volatility only. The historical columns always + carry the drift of their window. + """ + if space not in RETURN_SPACES: + raise ValueError(f"space must be one of {RETURN_SPACES}") + if drift not in DRIFT_MODES: + raise ValueError(f"drift must be one of {DRIFT_MODES}") + ppy = periods_per_year(freq) + h = horizon_periods(horizon, freq) + if at is not None: + r = r.loc[r.index <= pd.Timestamp(at)] + hist_window = hist_window or max(window * 5, 750) + out = pd.DataFrame(index=r.columns) + roll_sd = r.rolling(window, min_periods=window // 2).std(ddof=1).iloc[-1] + roll_mu = r.rolling(window, min_periods=window // 2).mean().iloc[-1] + ewm_sd = est.ewm_vol(r, lam).iloc[-1] + ewm_mu = est.ewm_mean(r, lam).iloc[-1] + out["mean_rolling_ann"] = roll_mu * ppy + out["vol_rolling_ann"] = roll_sd * np.sqrt(ppy) + out["vol_ewm_ann"] = ewm_sd * np.sqrt(ppy) + out["horizon_periods"] = h + hist = horizon_returns(r.iloc[-hist_window:], h, overlapping=True) + var_fn, cvar_fn = (normal_var, normal_cvar) if space == "log" else (normal_value_var, normal_value_cvar) + if drift == "zero": + roll_mu, ewm_mu = roll_mu * 0.0, ewm_mu * 0.0 + hist_sample = hist if space == "log" else np.expm1(hist) # monotone: the same tail, in value units + for beta in confidence: + cc = f"{int(round(beta * 100)):02d}" + out[f"var{cc}_normal_rolling"] = var_fn(roll_mu * h, roll_sd * np.sqrt(h), beta) + out[f"cvar{cc}_normal_rolling"] = cvar_fn(roll_mu * h, roll_sd * np.sqrt(h), beta) + out[f"var{cc}_normal_ewm"] = var_fn(ewm_mu * h, ewm_sd * np.sqrt(h), beta) + out[f"cvar{cc}_normal_ewm"] = cvar_fn(ewm_mu * h, ewm_sd * np.sqrt(h), beta) + out[f"var{cc}_hist"] = historical_var(hist_sample, beta) + out[f"cvar{cc}_hist"] = historical_cvar(hist_sample, beta) + out["n_obs"] = r.notna().sum() + out["as_of"] = r.index[-1] + return out + + +def risk_series( + r: pd.Series, freq: str = "daily", window: int = 200, lam: float = 0.94, confidence=DEFAULT_CONFIDENCE, + horizon: str | int = "day", +) -> pd.DataFrame: + """Time series of rolling / EWM volatility and Gaussian VaR/CVaR of one return series. + + Historical VaR/CVaR use rolling quantiles / tail means over ``window`` (1-period horizon, + scaled with sqrt(h) as an approximation for longer horizons). + """ + ppy = periods_per_year(freq) + h = horizon_periods(horizon, freq) + df = pd.DataFrame(index=r.index) + roll_sd = est.rolling_vol(r, window) + roll_mu = est.rolling_mean(r, window) + e_sd = est.ewm_vol(r, lam) + e_mu = est.ewm_mean(r, lam) + df["vol_rolling_ann"] = roll_sd * np.sqrt(ppy) + df["vol_ewm_ann"] = e_sd * np.sqrt(ppy) + for beta in confidence: + cc = f"{int(round(beta * 100)):02d}" + df[f"var{cc}_normal_rolling"] = normal_var(roll_mu * h, roll_sd * np.sqrt(h), beta) + df[f"cvar{cc}_normal_rolling"] = normal_cvar(roll_mu * h, roll_sd * np.sqrt(h), beta) + df[f"var{cc}_normal_ewm"] = normal_var(e_mu * h, e_sd * np.sqrt(h), beta) + df[f"cvar{cc}_normal_ewm"] = normal_cvar(e_mu * h, e_sd * np.sqrt(h), beta) + q = est.rolling_quantile(r, window, 1.0 - beta) + df[f"var{cc}_hist"] = -q * np.sqrt(h) + tail_mean = r.rolling(window, min_periods=window // 2).apply( + lambda x, b=beta: -x[x <= np.quantile(x, 1.0 - b)].mean(), raw=True + ) + df[f"cvar{cc}_hist"] = tail_mean * np.sqrt(h) + return df + + +# --------------------------------------------------------------------------------------------- +# portfolio risk +# --------------------------------------------------------------------------------------------- +@dataclass +class PortfolioRisk: + """Risk of a share-based portfolio over one horizon, in return space and in money.""" + + as_of: str + value: float + horizon: str | int + horizon_periods: int + freq: str + method: str + confidence: dict # {"0.95": {"var": .., "cvar": .., "var_money": .., "cvar_money": ..}, ...} + volatility_ann: float + expected_return_horizon: float + weights: dict + n_obs: int + covariance_model: dict + + def as_dict(self) -> dict: + return self.__dict__.copy() + + +def portfolio_risk_from_returns( + r: pd.DataFrame, weights: pd.Series, value: float, freq: str, horizon, confidence, method: str, + window: int, lam: float, cov_method: str = "rolling", shrinkage: float = 0.0, scenarios: np.ndarray | None = None, +) -> PortfolioRisk: + """Portfolio VaR/CVaR from asset returns and weights (fraction of ``value`` per asset). + + ``method``: ``'normal'`` (Gaussian with the rolling/EWM covariance), ``'historical'`` + (realised portfolio returns over the last ``window*5`` periods, aggregated to the horizon) + or ``'scenarios'`` (a ``S x N`` array of simulated horizon log-returns, e.g. from + :mod:`quantcore.models.simulation`). + """ + ppy = periods_per_year(freq) + h = horizon_periods(horizon, freq) + w = weights.reindex(r.columns).fillna(0.0) + cov = est.covariance(r, cov_method, window=window, lam=lam, shrinkage=shrinkage) + var_p = float(w.values @ cov.values @ w.values) + mu_p = float(w.values @ (r.iloc[-window:].mean().fillna(0.0).values)) + sd_h, mu_h = np.sqrt(var_p * h), mu_p * h + levels: dict = {} + if method == "normal": + for beta in confidence: + var_r, cvar_r = normal_var(mu_h, sd_h, beta), normal_cvar(mu_h, sd_h, beta) + levels[f"{beta:.2f}"] = _level(var_r, cvar_r, value) + n_obs = int(r.iloc[-window:].dropna().shape[0]) + elif method == "historical": + rp = portfolio_log_returns(r.iloc[-max(window * 5, 750):], w.values) + rp_h = horizon_returns(rp, h, overlapping=True) + for beta in confidence: + levels[f"{beta:.2f}"] = _level(float(historical_var(rp_h, beta)), float(historical_cvar(rp_h, beta)), value) + n_obs = int(len(rp_h)) + elif method == "scenarios": + if scenarios is None: + raise ValueError("method='scenarios' needs a scenario array") + pnl = value * (np.expm1(scenarios) @ w.values) # money P&L per scenario + for beta in confidence: + var_m, cvar_m = scenario_cvar(-pnl, beta) + levels[f"{beta:.2f}"] = { + "var": float(-np.log1p(-var_m / value)) if var_m < value else np.inf, + "cvar": float(-np.log1p(-cvar_m / value)) if cvar_m < value else np.inf, + "var_money": var_m, "cvar_money": cvar_m, + } + n_obs = int(len(scenarios)) + else: + raise ValueError("method must be 'normal', 'historical' or 'scenarios'") + return PortfolioRisk( + as_of=str(r.index[-1].date()), value=float(value), horizon=horizon, horizon_periods=h, freq=freq, + method=method, confidence=levels, volatility_ann=float(np.sqrt(var_p * ppy)), + expected_return_horizon=mu_h, weights=w.round(6).to_dict(), n_obs=n_obs, + covariance_model={"method": cov_method, "window": window, "lambda": lam, "shrinkage": shrinkage}, + ) + + +def _level(var_r: float, cvar_r: float, value: float) -> dict: + return { + "var": float(var_r), "cvar": float(cvar_r), + "var_money": float(loss_to_money(var_r, value)), "cvar_money": float(loss_to_money(cvar_r, value)), + } + + +# --------------------------------------------------------------------------------------------- +# service facade +# --------------------------------------------------------------------------------------------- +class RiskService: + """Deterministic risk measurement on top of :class:`~quantcore.data.market_data.MarketDataService`.""" + + def __init__(self, market_data): + self.md = market_data + + def asset_risk(self, symbols, freq="daily", currency="base", window=200, lam=0.94, + confidence=DEFAULT_CONFIDENCE, horizon="day", start=None, end=None, + space: str = "log", drift: str = "estimated") -> pd.DataFrame: + r = self.md.returns(symbols, start=start, end=end, currency=currency, freq=freq) + return asset_risk_table(r, freq, window, lam, confidence, horizon, space=space, drift=drift) + + def risk_series(self, symbol, freq="daily", currency="base", window=200, lam=0.94, + confidence=DEFAULT_CONFIDENCE, horizon="day", start=None, end=None) -> pd.DataFrame: + r = self.md.returns([symbol], start=start, end=end, currency=currency, freq=freq)[symbol].dropna() + return risk_series(r, freq, window, lam, confidence, horizon) + + def covariance(self, symbols, freq="daily", currency="base", method="rolling", window=200, lam=0.94, + shrinkage=0.0, start=None, end=None) -> pd.DataFrame: + r = self.md.returns(symbols, start=start, end=end, currency=currency, freq=freq) + return est.covariance(r, method, window=window, lam=lam, shrinkage=shrinkage) + + def correlation(self, symbols, freq="daily", currency="base", method="rolling", window=200, lam=0.94, + start=None, end=None) -> pd.DataFrame: + return est.cov_to_corr(self.covariance(symbols, freq, currency, method, window, lam, 0.0, start, end)) + + def portfolio_risk(self, holdings: dict[str, float], cash: float = 0.0, freq="daily", currency="base", + horizon="day", confidence=DEFAULT_CONFIDENCE, method="normal", cov_method="rolling", + window=200, lam=0.94, shrinkage=0.0, start=None, end=None, + scenarios: np.ndarray | None = None) -> PortfolioRisk: + """VaR / CVaR of a portfolio given in **numbers of shares** (plus cash), in money terms.""" + symbols = list(holdings) + px = self.md.latest_prices(symbols, currency=currency) + values = pd.Series(holdings, dtype=float) * px + total = float(values.sum() + cash) + if total <= 0: + raise ValueError("portfolio value must be positive") + weights = values / total + r = self.md.returns(symbols, start=start, end=end, currency=currency, freq=freq) + return portfolio_risk_from_returns(r, weights, total, freq, horizon, confidence, method, window, lam, + cov_method, shrinkage, scenarios) diff --git a/quantcore/models/__init__.py b/quantcore/models/__init__.py new file mode 100644 index 0000000..3886825 --- /dev/null +++ b/quantcore/models/__init__.py @@ -0,0 +1,10 @@ +"""Stochastic models: covariance estimation, price-scenario simulation, Black-Litterman returns.""" + +from quantcore.models.black_litterman import BlackLittermanModel, BlackLittermanResult, View, view_from_sentiment +from quantcore.models.covariance import CovarianceModel +from quantcore.models.simulation import EmpiricalInnovationSimulator, GBMSimulator, ScenarioSet + +__all__ = [ + "BlackLittermanModel", "BlackLittermanResult", "View", "view_from_sentiment", "CovarianceModel", + "EmpiricalInnovationSimulator", "GBMSimulator", "ScenarioSet", +] diff --git a/quantcore/models/black_litterman.py b/quantcore/models/black_litterman.py new file mode 100644 index 0000000..28c3b9f --- /dev/null +++ b/quantcore/models/black_litterman.py @@ -0,0 +1,231 @@ +"""Black-Litterman expected returns with Idzorek's confidence-based view calibration. + +Model (per period, excess returns over the risk-free rate): + +* **Equilibrium**: the market portfolio (capitalisation weights ``w``) is mean-variance + optimal for a risk aversion ``delta`` = (mu_M - r_f) / (w' Sigma w), where ``mu_M`` is the + average return of the capitalisation-weighted broad market index. The implied equilibrium + excess returns are ``pi = delta * Sigma * w``. +* **Views**: ``P mu = Q + e``, ``e ~ N(0, Omega)`` with a diagonal ``Omega``. Each view ``k`` has a + *confidence* ``C_k`` in (0, 1] (Idzorek 2005): the view's uncertainty ``omega_k`` is the one + for which the posterior portfolio moves the fraction ``C_k`` of the way from the market + weights to the weights the view would imply at 100 % confidence. For a single view that + calibration has the closed form ``omega_k = (1 - C_k) / C_k * p_k (tau Sigma) p_k'`` + (Walters 2014), which is what :func:`idzorek_omega` uses -- the tilt-matching minimisation + of the paper is available as :func:`idzorek_omega_numeric` for verification. +* **Posterior**: ``mu_BL = pi + tau Sigma P' (P tau Sigma P' + Omega)^-1 (Q - P pi)`` and + ``Sigma_BL = Sigma + tau Sigma - tau Sigma P' (P tau Sigma P' + Omega)^-1 P tau Sigma``. + +Units: the covariance is per period (weekly here), ``q`` of a view is stated as an **annual +total** return and converted internally. Results are given per period and annualised. + +Reference: T. Idzorek, *A step-by-step guide to the Black-Litterman model, incorporating +user-specified confidence levels*, 2005. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +import numpy as np +import pandas as pd +from scipy.optimize import minimize_scalar + + +@dataclass +class View: + """A (possibly relative) view: ``sum_i assets[i] * mu_i = q`` with Idzorek confidence.""" + + assets: dict[str, float] + q: float # annual total return of the view portfolio (log-return units) + confidence: float = 0.5 # (0, 1] + label: str = "" + source: str = "" # provenance, e.g. the insight id that produced the view + + def __post_init__(self) -> None: + if not self.assets: + raise ValueError("a view needs at least one asset") + if not 0 < self.confidence <= 1: + raise ValueError("confidence must be in (0, 1]") + + def row(self, symbols: list[str]) -> np.ndarray: + p = np.zeros(len(symbols)) + index = {s: i for i, s in enumerate(symbols)} + for asset, weight in self.assets.items(): + if asset not in index: + raise KeyError(f"view asset {asset!r} is not in the model universe") + p[index[asset]] = weight + return p + + @property + def is_relative(self) -> bool: + return abs(sum(self.assets.values())) < 1e-12 + + +@dataclass +class BlackLittermanResult: + symbols: list[str] + periods_per_year: int + tau: float + delta: float + market_return_ann: float + market_volatility_ann: float + risk_free_ann: float + market_weights: pd.Series + prior: pd.Series # equilibrium excess returns per period + posterior: pd.Series # posterior excess returns per period + posterior_cov: pd.DataFrame # per period + omega: np.ndarray + views: list[dict] = field(default_factory=list) + + @property + def prior_ann(self) -> pd.Series: + return self.prior * self.periods_per_year + self.risk_free_ann + + @property + def posterior_ann(self) -> pd.Series: + return self.posterior * self.periods_per_year + self.risk_free_ann + + @property + def posterior_excess_ann(self) -> pd.Series: + return self.posterior * self.periods_per_year + + def expected_return_per_period(self, freq_periods_per_year: int) -> pd.Series: + """Posterior total expected log-return per period of another frequency (e.g. daily).""" + return self.posterior_ann / freq_periods_per_year + + def unconstrained_weights(self) -> pd.Series: + """Mean-variance weights ``(delta Sigma_BL)^-1 mu_BL`` implied by the posterior.""" + w = np.linalg.solve(self.delta * self.posterior_cov.values, self.posterior.values) + return pd.Series(w, index=self.symbols) + + def as_dict(self) -> dict: + return { + "periods_per_year": self.periods_per_year, "tau": self.tau, "delta": self.delta, + "market_return_ann": self.market_return_ann, "market_volatility_ann": self.market_volatility_ann, + "risk_free_ann": self.risk_free_ann, "omega": self.omega.tolist(), "views": self.views, + "prior_ann": self.prior_ann.round(6).to_dict(), "posterior_ann": self.posterior_ann.round(6).to_dict(), + "market_weights": self.market_weights.round(6).to_dict(), + } + + +def idzorek_omega(p: np.ndarray, tau_sigma: np.ndarray, confidence: float) -> float: + """Closed-form Idzorek uncertainty: ``(1 - C) / C * p (tau Sigma) p'`` (C=1 -> 0).""" + c = float(np.clip(confidence, 1e-9, 1.0)) + base = float(p @ tau_sigma @ p) + return (1.0 - c) / c * base + + +def idzorek_omega_numeric(p: np.ndarray, q: float, pi: np.ndarray, sigma: np.ndarray, tau: float, delta: float, + confidence: float) -> float: + """Idzorek's original tilt-matching calibration of one view (for verification). + + Finds ``omega`` minimising ``|| w(omega) - (w_mkt + C * (w_100 - w_mkt)) ||`` where + ``w(omega) = (delta Sigma)^-1 mu_BL(omega)`` and ``w_100`` is the weight vector at 100 % + confidence (``omega = 0``). + """ + ts = tau * sigma + w_mkt = np.linalg.solve(delta * sigma, pi) + denom_100 = float(p @ ts @ p) + mu_100 = pi + ts @ p * (q - p @ pi) / denom_100 + w_100 = np.linalg.solve(delta * sigma, mu_100) + target = w_mkt + confidence * (w_100 - w_mkt) + + def objective(log_omega: float) -> float: + omega = np.exp(log_omega) + mu = pi + ts @ p * (q - p @ pi) / (denom_100 + omega) + w = np.linalg.solve(delta * sigma, mu) + return float(np.sum((w - target) ** 2)) + + res = minimize_scalar(objective, bounds=(np.log(denom_100) - 25, np.log(denom_100) + 25), method="bounded", + options={"xatol": 1e-10}) + return float(np.exp(res.x)) + + +class BlackLittermanModel: + """Equilibrium returns from market weights and a broad-market average, plus views.""" + + def __init__(self, cov: pd.DataFrame, market_weights: pd.Series, market_return: float, risk_free: float = 0.0, + tau: float = 0.05, periods_per_year: int = 52): + self.symbols = list(cov.index) + self.sigma = cov.values.astype(float) + w = market_weights.reindex(self.symbols).fillna(0.0).astype(float) + if w.sum() <= 0: + raise ValueError("market weights must have a positive sum") + self.w = (w / w.sum()).values + self.market_return = float(market_return) # per period, total + self.risk_free = float(risk_free) # per period + self.tau = float(tau) + self.ppy = int(periods_per_year) + self.market_var = float(self.w @ self.sigma @ self.w) + self.delta = (self.market_return - self.risk_free) / self.market_var + self.pi = self.delta * self.sigma @ self.w # equilibrium excess returns per period + + # -- helpers --------------------------------------------------------------------------------- + @property + def prior(self) -> pd.Series: + return pd.Series(self.pi, index=self.symbols) + + @property + def prior_ann(self) -> pd.Series: + return self.prior * self.ppy + self.risk_free * self.ppy + + def volatility_ann(self) -> pd.Series: + return pd.Series(np.sqrt(np.diag(self.sigma) * self.ppy), index=self.symbols) + + def _views_to_matrices(self, views: list[View], omega_method: str): + k, n = len(views), len(self.symbols) + P = np.zeros((k, n)) + Q = np.zeros(k) + omega = np.zeros(k) + ts = self.tau * self.sigma + rf_ann = self.risk_free * self.ppy + for i, v in enumerate(views): + P[i] = v.row(self.symbols) + # annual total -> per-period excess (the risk-free part cancels for relative views) + Q[i] = (v.q - rf_ann * P[i].sum()) / self.ppy + if omega_method == "idzorek": + omega[i] = idzorek_omega(P[i], ts, v.confidence) + elif omega_method == "he_litterman": + omega[i] = float(P[i] @ ts @ P[i]) + else: + raise ValueError("omega_method must be 'idzorek' or 'he_litterman'") + return P, Q, omega + + # -- posterior ------------------------------------------------------------------------------- + def posterior(self, views: list[View] | None = None, omega_method: str = "idzorek") -> BlackLittermanResult: + views = list(views or []) + ts = self.tau * self.sigma + if not views: + mu, cov_post, omega = self.pi.copy(), self.sigma + ts, np.zeros(0) + else: + P, Q, omega = self._views_to_matrices(views, omega_method) + M = P @ ts @ P.T + np.diag(omega) + adj = np.linalg.solve(M, Q - P @ self.pi) + mu = self.pi + ts @ P.T @ adj + cov_post = self.sigma + ts - ts @ P.T @ np.linalg.solve(M, P @ ts) + return BlackLittermanResult( + symbols=self.symbols, periods_per_year=self.ppy, tau=self.tau, delta=self.delta, + market_return_ann=self.market_return * self.ppy, market_volatility_ann=float(np.sqrt(self.market_var * self.ppy)), + risk_free_ann=self.risk_free * self.ppy, market_weights=pd.Series(self.w, index=self.symbols), + prior=pd.Series(self.pi, index=self.symbols), posterior=pd.Series(mu, index=self.symbols), + posterior_cov=pd.DataFrame(cov_post, index=self.symbols, columns=self.symbols), omega=omega, + views=[{"label": v.label, "assets": v.assets, "q_ann": v.q, "confidence": v.confidence, "source": v.source, + "omega": float(omega[i])} for i, v in enumerate(views)], + ) + + +def view_from_sentiment(model: BlackLittermanModel, symbol: str, sentiment: float, confidence: float, + strength: float = 0.25, label: str = "", source: str = "") -> View: + """Translate a signed assessment into an absolute view on one stock. + + The AI layer delivers *positive / negative* assessments with a degree of belief; the view + return is the equilibrium return shifted by ``sentiment * strength`` annual volatilities: + ``q = pi_ann + r_f + sentiment * strength * sigma_ann``. ``sentiment`` in [-1, 1], + ``confidence`` in (0, 1] (Idzorek), ``strength`` is a modelling parameter (0.25 by default). + """ + if not -1.0 <= sentiment <= 1.0: + raise ValueError("sentiment must be in [-1, 1]") + q = float(model.prior_ann[symbol] + sentiment * strength * model.volatility_ann()[symbol]) + return View({symbol: 1.0}, q=q, confidence=confidence, label=label or f"{symbol} sentiment {sentiment:+.2f}", + source=source) diff --git a/quantcore/models/covariance.py b/quantcore/models/covariance.py new file mode 100644 index 0000000..39c65c9 --- /dev/null +++ b/quantcore/models/covariance.py @@ -0,0 +1,145 @@ +"""Covariance model specification and matrix square roots. + +:class:`CovarianceModel` is the *named* estimator the other services refer to, e.g. +``CovarianceModel("rolling", window=200)`` (200-day equally weighted) or +``CovarianceModel("ewm", lam=0.99)``; it delegates to :mod:`quantcore.measurement.estimators`. + +Square roots: the **symmetric** root ``Sigma^(1/2) = V diag(sqrt(l)) V'`` (from the +eigendecomposition; used for the innovation filter, as in the model specification) and the +**Cholesky** factor (faster, order dependent). Both satisfy ``A A' = Sigma``. +""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass + +import numpy as np +import pandas as pd + +from quantcore.measurement import estimators as est + + +@dataclass(frozen=True) +class CovarianceModel: + method: str = "rolling" # 'rolling' | 'ewm' + window: int = 200 # rolling window length (observations) + lam: float = 0.94 # EWM decay + shrinkage: float | str = "auto" # linear shrinkage towards the diagonal: number in [0, 1] or 'auto' + + def __post_init__(self) -> None: + if self.method not in ("rolling", "ewm"): + raise ValueError("method must be 'rolling' or 'ewm'") + if not 0 < self.lam < 1: + raise ValueError("lam must be in (0, 1)") + if isinstance(self.shrinkage, str): + if self.shrinkage != "auto": + raise ValueError("shrinkage must be a number in [0, 1] or 'auto'") + elif not 0 <= self.shrinkage <= 1: + raise ValueError("shrinkage must be in [0, 1]") + + def estimate(self, r: pd.DataFrame, at=None) -> pd.DataFrame: + return est.covariance(r, self.method, window=self.window, lam=self.lam, at=at, shrinkage=self.shrinkage) + + def intensity(self, r: pd.DataFrame, at=None) -> float: + """The shrinkage intensity actually applied for ``r`` (resolves ``'auto'``).""" + return est.resolve_shrinkage(self.shrinkage, r, self.method, self.window, self.lam, at) + + def sequence(self, r: pd.DataFrame, warmup: int | None = None): + return est.covariance_sequence(r, self.method, window=self.window, lam=self.lam, warmup=warmup, + shrinkage=self.shrinkage) + + @property + def effective_window(self) -> float: + return float(self.window) if self.method == "rolling" else 1.0 / (1.0 - self.lam) + + @property + def effective_observations(self) -> int: + """Observations the estimator effectively averages over (``3/(1-lambda)`` for EWM).""" + return est.effective_observations(self.method, self.window, self.lam) + + def conditioning(self, r: pd.DataFrame, at=None) -> dict: + """Is this estimator able to support ``N`` assets? + + With fewer effective observations than assets the sample covariance is singular (a + portfolio in its null space looks riskless, which an optimiser will happily exploit). + Shrinkage towards the diagonal repairs it; this report says by how much. ``ratio`` is the + smallest eigenvalue as a fraction of the average variance -- below ~1e-6 the matrix is + numerically singular, the automatic shrinkage keeps it above 2 %. + """ + cov = self.estimate(r, at=at).values + vals = np.linalg.eigvalsh(cov) + scale = float(np.mean(np.diag(cov))) or 1.0 + n = cov.shape[0] + t_eff = self.effective_observations + return { + "n_assets": n, "effective_observations": t_eff, "observations": int(len(r.dropna())), + "shrinkage_applied": round(self.intensity(r, at), 4), + "min_eigenvalue_ratio": float(vals.min() / scale), + "condition_number": float(vals.max() / vals.min()) if vals.min() > 0 else float("inf"), + "rank_deficient": bool(vals.min() <= 1e-10 * scale), + "under_determined": bool(t_eff < n), + } + + def describe(self) -> dict: + d = asdict(self) + d["effective_window"] = round(self.effective_window, 4) # rounded: this dict is part of the scenario key + return d + + @classmethod + def from_dict(cls, d: dict | None) -> "CovarianceModel": + if d is None: + return cls() + if isinstance(d, CovarianceModel): + return d + return cls(**{k: v for k, v in d.items() if k in ("method", "window", "lam", "shrinkage")}) + + +# --------------------------------------------------------------------------------------------- +# matrix roots +# --------------------------------------------------------------------------------------------- +def _as_array(cov) -> np.ndarray: + return cov.values if isinstance(cov, pd.DataFrame) else np.asarray(cov, dtype=float) + + +def symmetric_sqrt(cov, eps_rel: float = 1e-10) -> np.ndarray: + """``Sigma^(1/2)`` via eigendecomposition; tiny/negative eigenvalues are floored.""" + a = _as_array(cov) + vals, vecs = np.linalg.eigh((a + a.T) / 2) + floor = eps_rel * max(vals.max(), 1e-300) + vals = np.maximum(vals, floor) + return (vecs * np.sqrt(vals)) @ vecs.T + + +def symmetric_inv_sqrt(cov, eps_rel: float = 1e-10) -> np.ndarray: + """``Sigma^(-1/2)`` (pseudo-inverse for directions with negligible variance).""" + a = _as_array(cov) + vals, vecs = np.linalg.eigh((a + a.T) / 2) + floor = eps_rel * max(vals.max(), 1e-300) + inv = np.where(vals > floor, 1.0 / np.sqrt(np.maximum(vals, floor)), 0.0) + return (vecs * inv) @ vecs.T + + +def cholesky_factor(cov, jitter_rel: float = 1e-10) -> np.ndarray: + """Lower-triangular ``L`` with ``L L' = Sigma`` (adds a relative jitter if needed).""" + a = _as_array(cov) + a = (a + a.T) / 2 + scale = np.trace(a) / len(a) + for k in range(6): + try: + return np.linalg.cholesky(a + np.eye(len(a)) * scale * jitter_rel * (10**k) * (k > 0)) + except np.linalg.LinAlgError: + continue + raise np.linalg.LinAlgError("covariance matrix is not positive definite even after jitter") + + +def matrix_root(cov, kind: str = "symmetric") -> np.ndarray: + if kind == "symmetric": + return symmetric_sqrt(cov) + if kind == "cholesky": + return cholesky_factor(cov) + raise ValueError("kind must be 'symmetric' or 'cholesky'") + + +def is_positive_definite(cov, tol: float = 0.0) -> bool: + vals = np.linalg.eigvalsh(_as_array(cov)) + return bool(vals.min() > tol) diff --git a/quantcore/models/sampling.py b/quantcore/models/sampling.py new file mode 100644 index 0000000..1ad3f2e --- /dev/null +++ b/quantcore/models/sampling.py @@ -0,0 +1,255 @@ +"""Scenario sampling: quasi-Monte-Carlo draws, moment matching, importance sampling, resampling. + +Why this module exists. The mean-CVaR optimiser is a sample-average approximation: it sees the +model only through the scenario set. With a plain Monte-Carlo set of ``S`` scenarios the *sample +mean* of every asset is off by ``sigma / sqrt(S)`` -- with ``S = 2000`` and weekly volatilities of +3-11 % that is 3-13 % per year, the same size as the expected returns themselves -- and the +optimiser maximises that noise. Measured on the real universe (30 stocks, 8 seeds): two seeds gave +portfolios differing by 100 % of wealth, an in-sample expected return 26 % too high and a true +expected return 10 % below the optimum. The remedies implemented here remove that noise almost +entirely: + +* **quasi-Monte-Carlo** (scrambled Sobol' points, fixed seed) instead of pseudo-random draws -- + ``S`` is rounded up to a power of two, the first Sobol' coordinates are mapped to the largest + principal components (:func:`pca_root`); +* **moment matching** -- the (weighted) sample is centred and whitened so that its first two + moments are *exactly* the model's; only the tail shape remains sampled; +* **importance sampling** -- an exponential tilt of the sampling distribution along a loss + direction (a defensive mixture with the nominal density, so the likelihood ratios stay bounded + by ``1/(1-sum(mixture))``) puts a quarter of the scenarios in the tail instead of ``1-beta`` of + them; scenarios then carry probabilities ``p_s`` that the Rockafellar-Uryasev rows use instead + of ``1/S``. Several shifts can be mixed in one set (e.g. one block at ``z_{0.95}`` and one at + ``z_{0.99}``), which makes the set usable at any confidence level; +* **resampling schemes** for the empirical-innovation model: plain bootstrap, balanced bootstrap + and Sobol' draws on the ranks of the market-factor innovation. + +Everything is deterministic given the seed; :func:`scenario_hash` fingerprints a scenario set so +that two runs can be verified to have used the same one. +""" + +from __future__ import annotations + +import hashlib + +import numpy as np +from scipy.optimize import brentq +from scipy.stats import norm, qmc + +SAMPLING_METHODS = ("sobol", "mc", "antithetic", "lhs") +RESAMPLING_METHODS = ("sobol", "mc", "balanced") + + +def next_power_of_two(n: int) -> int: + return 1 << max(int(n) - 1, 0).bit_length() if n > 1 else 1 + + +# --------------------------------------------------------------------------------------------- +# base samples +# --------------------------------------------------------------------------------------------- +def standard_normal_sample(n: int, dim: int, method: str = "sobol", seed: int | None = 42) -> np.ndarray: + """``n x dim`` standard-normal draws. + + ``'sobol'``: scrambled Sobol' points through the inverse normal CDF (``n`` must be a power of + two -- use :func:`next_power_of_two`); ``'lhs'``: Latin hypercube; ``'antithetic'``: ``n/2`` + pseudo-random draws and their negatives; ``'mc'``: pseudo-random (PCG64). + """ + if method == "sobol": + if n & (n - 1): + raise ValueError("Sobol' sampling needs a power-of-two sample size") + u = qmc.Sobol(d=dim, scramble=True, seed=seed).random(n) + return norm.ppf(np.clip(u, 1e-12, 1 - 1e-12)) + if method == "lhs": + u = qmc.LatinHypercube(d=dim, scramble=True, seed=seed).random(n) + return norm.ppf(np.clip(u, 1e-12, 1 - 1e-12)) + rng = np.random.default_rng(seed) + if method == "mc": + return rng.standard_normal((n, dim)) + if method == "antithetic": + half = rng.standard_normal(((n + 1) // 2, dim)) + return np.vstack([half, -half])[:n] + raise ValueError(f"sampling must be one of {SAMPLING_METHODS}") + + +def pca_root(cov: np.ndarray) -> np.ndarray: + """``A = V diag(sqrt(l))`` with eigenvalues descending, so ``A A' = cov`` and the first + coordinate of a draw maps to the largest principal component (best use of the first Sobol' + dimensions).""" + vals, vecs = np.linalg.eigh(np.asarray(cov, dtype=float)) + order = np.argsort(vals)[::-1] + vals = np.maximum(vals[order], 0.0) + return vecs[:, order] * np.sqrt(vals) + + +# --------------------------------------------------------------------------------------------- +# moment matching +# --------------------------------------------------------------------------------------------- +def moment_match(z: np.ndarray, probabilities: np.ndarray | None = None, target_variance: float = 1.0) -> np.ndarray: + """Affine transform of the rows of ``z`` so that the (weighted) mean is exactly 0 and the + (weighted) covariance exactly ``target_variance * I``. Needs more rows than columns.""" + z = np.asarray(z, dtype=float) + n, d = z.shape + if n <= d: + raise ValueError(f"moment matching needs more scenarios ({n}) than dimensions ({d})") + p = np.full(n, 1.0 / n) if probabilities is None else np.asarray(probabilities, dtype=float) / np.sum(probabilities) + zc = z - p @ z + cov = (zc * p[:, None]).T @ zc + vals, vecs = np.linalg.eigh(cov) + vals = np.maximum(vals, 1e-14 * vals.max()) + inv_root = (vecs / np.sqrt(vals)) @ vecs.T + return zc @ inv_root * np.sqrt(target_variance) + + +def effective_sample_size(probabilities: np.ndarray) -> float: + p = np.asarray(probabilities, dtype=float) + p = p / p.sum() + return float(1.0 / np.sum(p * p)) + + +# --------------------------------------------------------------------------------------------- +# importance sampling (exponential tilt along a loss direction) +# --------------------------------------------------------------------------------------------- +def loss_direction(root: np.ndarray, exposure: np.ndarray) -> np.ndarray: + """Unit vector in draw space along which the loss of the portfolio with money exposure + ``exposure`` (shares x price) increases fastest: ``-(A' v) / ||A' v||`` for ``r = mu + A z``.""" + a = np.asarray(root, dtype=float).T @ np.asarray(exposure, dtype=float) + norm_a = np.linalg.norm(a) + if norm_a <= 0: + raise ValueError("exposure has no risk - cannot tilt") + return -a / norm_a + + +def _as_blocks(shift, mixture) -> list[tuple[float, float]]: + """``[(shift, mixture), ...]`` from scalars or sequences; validates the total mixture.""" + shifts = [float(shift)] if np.isscalar(shift) else [float(x) for x in shift] + mixtures = [float(mixture)] * len(shifts) if np.isscalar(mixture) else [float(x) for x in mixture] + if len(mixtures) != len(shifts): + raise ValueError("shift and mixture must have the same length") + if not shifts or sum(mixtures) <= 0 or sum(mixtures) >= 1 or min(mixtures) <= 0: + raise ValueError("mixtures must be positive and sum to less than 1") + return list(zip(shifts, mixtures)) + + +def tilt_normal_sample(z: np.ndarray, direction: np.ndarray, shift, mixture=0.5 + ) -> tuple[np.ndarray, np.ndarray]: + """Defensive-mixture importance sampling for standard-normal draws. + + A share ``mixture`` of the rows is shifted by ``theta = shift * direction`` (the loss of the + reference portfolio is then centred at its VaR when ``shift = z_beta``); the rest stay + nominal. ``shift`` and ``mixture`` may be sequences, in which case one block of rows is + shifted per entry -- a set tilted at ``z_{0.95}`` and ``z_{0.99}`` at once serves both + confidence levels. Returns the shifted draws and the self-normalised probabilities + ``p_s ~ f(z_s) / g(z_s)`` with ``g = (1-sum e_k) f + sum_k e_k f(. - theta_k)``, whose + likelihood ratios are bounded by ``1 / (1 - sum e_k)``. + """ + blocks = _as_blocks(shift, mixture) + z = np.array(z, dtype=float, copy=True) + n = len(z) + direction = np.asarray(direction, dtype=float) + thetas = [sh * direction for sh, _ in blocks] + start = 0 + for theta, (_, eps) in zip(thetas, blocks): + m = int(round(eps * n)) + z[start : start + m] += theta + start += m + density = (1.0 - sum(e for _, e in blocks)) * np.ones(n) + for theta, (_, eps) in zip(thetas, blocks): + density = density + eps * np.exp(np.clip(z @ theta - theta @ theta / 2.0, -700, 700)) + weights = 1.0 / density + return z, weights / weights.sum() + + +def tilted_day_probabilities(scores: np.ndarray, target_mean: float) -> tuple[np.ndarray, float]: + """Exponential tilt of the empirical distribution of ``scores`` (one per observation) so that + the tilted mean equals ``target_mean``: ``q_t ~ exp(theta * score_t)``. Returns ``(q, theta)``.""" + s = np.asarray(scores, dtype=float) + s0 = s - s.mean() + if target_mean <= s.mean(): + return np.full(len(s), 1.0 / len(s)), 0.0 + if target_mean >= s.max(): + raise ValueError("target mean beyond the largest score - tilt not feasible") + + def tilted_mean(theta): + w = np.exp(theta * (s0 - s0.max())) + return float((w * s).sum() / w.sum()) - target_mean + + hi = 1.0 + while tilted_mean(hi) < 0: + hi *= 2.0 + if hi > 1e6: + raise ValueError("tilt did not converge") + theta = brentq(tilted_mean, 0.0, hi, xtol=1e-10) + w = np.exp(theta * (s0 - s0.max())) + return w / w.sum(), float(theta) + + +# --------------------------------------------------------------------------------------------- +# resampling of historical innovations +# --------------------------------------------------------------------------------------------- +def resample_indices(n_obs: int, n_scenarios: int, horizon: int, method: str = "sobol", seed: int | None = 42, + order: np.ndarray | None = None) -> np.ndarray: + """``n_scenarios x horizon`` observation indices. + + ``'mc'``: uniform bootstrap; ``'balanced'``: every observation used equally often (random + permutation of the tiled index set); ``'sobol'``: scrambled Sobol' points in ``horizon`` + dimensions mapped to ranks -- with ``order`` the observations sorted by, e.g., the market-factor + innovation, so that every day of the horizon covers the loss distribution evenly. + """ + S, h = int(n_scenarios), int(horizon) + if method == "mc": + return np.random.default_rng(seed).integers(0, n_obs, size=(S, h)) + if method == "balanced": + rng = np.random.default_rng(seed) + full, rem = divmod(S * h, n_obs) + pool = [rng.permutation(n_obs) for _ in range(full)] + [rng.permutation(n_obs)[:rem]] + return rng.permutation(np.concatenate(pool)).reshape(S, h) # every observation full or full + 1 times + if method == "sobol": + if S & (S - 1): + raise ValueError("Sobol' resampling needs a power-of-two sample size") + u = qmc.Sobol(d=h, scramble=True, seed=seed).random(S) + ranks = np.minimum((u * n_obs).astype(int), n_obs - 1) + return ranks if order is None else np.asarray(order)[ranks] + raise ValueError(f"resampling must be one of {RESAMPLING_METHODS}") + + +def tilted_resample(scores: np.ndarray, n_scenarios: int, horizon: int, target_mean, seed: int | None = 42, + mixture=0.5) -> tuple[np.ndarray, np.ndarray, list[float]]: + """Importance-sampled bootstrap: one block of scenarios per ``target_mean`` draws its + ``horizon`` observations from the correspondingly tilted day distribution ``q`` + (:func:`tilted_day_probabilities` on ``scores`` = loss-direction innovations), the remaining + scenarios uniformly. ``target_mean`` and ``mixture`` may be sequences (one block each). + Returns ``(indices, probabilities, thetas)`` with self-normalised likelihood ratios + ``1 / ((1-sum e_k) + sum_k e_k prod_j T q^k_{t_j})``.""" + blocks = _as_blocks(target_mean, mixture) + T = len(scores) + rng = np.random.default_rng(seed) + S, h = int(n_scenarios), int(horizon) + qs, thetas = [], [] + for target, _ in blocks: + q, theta = tilted_day_probabilities(scores, target) + qs.append(q) + thetas.append(theta) + idx = np.empty((S, h), dtype=int) + start = 0 + for q, (_, eps) in zip(qs, blocks): + m = int(round(eps * S)) + idx[start : start + m] = rng.choice(T, size=(m, h), p=q) + start += m + idx[start:] = rng.integers(0, T, size=(S - start, h)) + density = (1.0 - sum(e for _, e in blocks)) * np.ones(S) + for q, (_, eps) in zip(qs, blocks): + density = density + eps * np.exp(np.clip(np.log(T * q[idx]).sum(axis=1), -700, 700)) + weights = 1.0 / density + return idx, weights / weights.sum(), thetas + + +# --------------------------------------------------------------------------------------------- +# fingerprint +# --------------------------------------------------------------------------------------------- +def scenario_hash(scenarios: np.ndarray, probabilities: np.ndarray | None = None) -> str: + """SHA-256 of the scenario matrix (rounded to 1e-12) and its probabilities -- two optimisations + with the same hash used identical inputs.""" + h = hashlib.sha256() + h.update(np.ascontiguousarray(np.round(np.asarray(scenarios, dtype=float), 12)).tobytes()) + if probabilities is not None: + h.update(np.ascontiguousarray(np.round(np.asarray(probabilities, dtype=float), 15)).tobytes()) + return h.hexdigest()[:16] diff --git a/quantcore/models/simulation.py b/quantcore/models/simulation.py new file mode 100644 index 0000000..5ecaccc --- /dev/null +++ b/quantcore/models/simulation.py @@ -0,0 +1,403 @@ +"""Stock-price models: forward simulation of horizon log-returns. + +Both models assume i.i.d. (no autocorrelation) daily or weekly log-returns, so an ``h``-period +return is the sum of ``h`` one-period draws. + +**Geometric Brownian motion** (multivariate normal):: + + r_h ~ N(h * m, h * Sigma_T) Sigma_T from the chosen covariance model + +**Empirical innovations** (filtered historical simulation):: + + eps_t = Sigma_{t-1}^(-1/2) r_t standardised with the covariance known at t-1 + r(j | T) = h * m + Sigma_T^(1/2) sum_{k=1..h} eps_{j_k}, j_k drawn at random from history + +The innovations keep the empirical shape of the return distribution (fat tails, skew, +co-movements beyond correlation) while the scale comes from the *latest* covariance. + +**Drift convention.** The expected-return model (Black-Litterman) delivers *arithmetic* expected +returns ``mu`` (mean-variance and reverse optimisation are statements about simple returns). The +log-drift is therefore ``m = ln(1 + mu_h) / h - diag(Sigma) / 2`` (Ito correction), so that the +expected simple return over the horizon is exactly ``mu_h = mu * h``; with the ``'log'`` convention +``mu`` is used as the log-drift directly and the expected simple return carries the ``sigma^2/2`` +bonus (up to +30 % p.a. for the most volatile stocks -- not what a mean-variance model means). + +**Sampling.** Draws come from :mod:`quantcore.models.sampling`: scrambled Sobol' points by +default, moment matching so that the scenario mean and covariance are exactly the model's, an +optional importance-sampling tilt along the loss direction of a reference portfolio (scenarios +then carry probabilities), and a fingerprint of the final set. +""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass, field, replace + +import numpy as np +import pandas as pd +from scipy.stats import norm + +from quantcore.measurement.risk import scenario_cvar, weighted_quantile +from quantcore.models import sampling as smp +from quantcore.models.covariance import CovarianceModel, matrix_root, symmetric_inv_sqrt + +log = logging.getLogger(__name__) + +DRIFT_CONVENTIONS = ("arithmetic", "log") + + +@dataclass +class TiltSpec: + """Importance-sampling tilt along the loss direction of the portfolio with money ``exposure`` + (shares x price per asset). + + ``shift`` is given in standard deviations of that portfolio's loss: a block of ``mixture`` + scenarios is moved by that much, so a shift of ``z_beta`` centres the loss at its VaR. Both + may be sequences -- the default tilts one block at ``z_{0.95}`` and one at ``z_{0.99}``, which + makes one scenario set serve both confidence levels, so that the set does not depend on the + ``confidence`` of a particular optimisation run. + """ + + exposure: np.ndarray + shift: tuple[float, ...] = (1.6448536269514722, 2.3263478740408408) # z_0.95, z_0.99 + mixture: tuple[float, ...] = (0.25, 0.25) + label: str = "market" # provenance: which portfolio defined the direction + + @classmethod + def at_confidence(cls, exposure, confidence: float, mixture: float = 0.5, label: str = "adaptive") -> "TiltSpec": + """A single block shifted to the VaR of ``confidence`` (depends on that confidence).""" + return cls(exposure=exposure, shift=(float(norm.ppf(confidence)),), mixture=(float(mixture),), label=label) + + def describe(self) -> dict: + return {"shift_sd": list(self.shift), "mixture": list(self.mixture), "direction": self.label} + + +@dataclass +class ScenarioSet: + """``S x N`` matrix of simulated horizon log-returns plus its provenance.""" + + log_returns: np.ndarray + symbols: list[str] + horizon_periods: int + freq: str + model: str + covariance_model: dict + as_of: str + seed: int | None = None + drift: np.ndarray | None = None # horizon log-drift per asset + expected_simple: np.ndarray | None = None # model expectation of the horizon simple return + probabilities: np.ndarray | None = None # None = equiprobable + horizon_variance: np.ndarray | None = None # diag of h * Sigma_T (for :meth:`with_drift`) + extra: dict = field(default_factory=dict) + + @property + def n_scenarios(self) -> int: + return int(self.log_returns.shape[0]) + + @property + def weights(self) -> np.ndarray: + if self.probabilities is None: + return np.full(self.n_scenarios, 1.0 / self.n_scenarios) + return self.probabilities + + @property + def hash(self) -> str: + """Fingerprint of the scenario matrix and its probabilities.""" + return smp.scenario_hash(self.log_returns, self.probabilities) + + @property + def shock_hash(self) -> str: + """Fingerprint of the *shocks* only (the drift removed). Two sets with the same shock hash + differ at most in the expected-return model, so portfolios optimised on them see identical + risk; a changed view or risk-free rate shifts the scenarios but not this hash.""" + drift = np.zeros(self.log_returns.shape[1]) if self.drift is None else self.drift + return smp.scenario_hash(self.log_returns - drift, self.probabilities) + + @property + def shocks(self) -> np.ndarray: + """The scenario matrix with the drift removed: the random part of the model.""" + return self.log_returns if self.drift is None else self.log_returns - self.drift + + def with_drift(self, mu_period, convention: str = "arithmetic") -> "ScenarioSet": + """The same shocks with a different expected-return model. + + Changing views, the risk-free rate or the expected-return model shifts every scenario by a + constant per asset; the random part is untouched. This lets the service keep one scenario + set for a whole family of runs (and skips the expensive innovation filter). + """ + if self.horizon_variance is None: + raise ValueError("this ScenarioSet has no horizon variance - cannot re-drift") + log_drift, expected_simple = horizon_drift( + _mu_vector(mu_period, self.symbols), np.diag(self.horizon_variance) / self.horizon_periods, + self.horizon_periods, convention) + return replace(self, log_returns=self.shocks + log_drift, drift=log_drift, + expected_simple=expected_simple, extra={**self.extra, "drift_convention": convention}) + + def simple_returns(self) -> np.ndarray: + return np.expm1(self.log_returns) + + def mean_simple_returns(self) -> np.ndarray: + """Probability-weighted scenario mean of the simple returns (the sample counterpart of + :attr:`expected_simple`).""" + return self.weights @ self.simple_returns() + + def summary(self) -> pd.DataFrame: + """Per-asset moments and quantiles, in both spaces. + + The unprefixed columns are **log**-returns (the space the model works in, where an ``h``-period + return is a sum of ``h`` draws); the ``_simple`` columns are the same scenarios as price + returns, ``e^x - 1``, which is what a position actually gains or loses and cannot fall below + -100 %. A quarterly log-return of -2.0 is a price return of -86 %, so the two must never be + confused when the horizon is long. + """ + r = self.log_returns + w = self.weights + mean = w @ r + std = np.sqrt(w @ (r - mean) ** 2) + qs = weighted_quantile(r, (0.01, 0.05, 0.50, 0.95), self.probabilities) + simple = np.expm1(r) + mean_s = w @ simple + std_s = np.sqrt(w @ (simple - mean_s) ** 2) + qs_s = np.expm1(qs) # a monotone map, so the quantiles carry over exactly + return pd.DataFrame( + { + "mean": mean, "std": std, + "q01": qs[0], "q05": qs[1], "q50": qs[2], "q95": qs[3], + "min": r.min(axis=0), "max": r.max(axis=0), + "mean_simple": mean_s, "std_simple": std_s, + "q01_simple": qs_s[0], "q05_simple": qs_s[1], "q50_simple": qs_s[2], "q95_simple": qs_s[3], + "min_simple": np.expm1(r.min(axis=0)), "max_simple": np.expm1(r.max(axis=0)), + "expected_simple": self.expected_simple if self.expected_simple is not None else np.expm1(mean), + }, + index=self.symbols, + ) + + def portfolio_pnl(self, exposure) -> np.ndarray: + """Money P&L per scenario of a portfolio held with money ``exposure`` per asset.""" + return self.simple_returns() @ np.asarray(exposure, dtype=float) + + def distribution(self, exposure, bins: int = 60, confidence=(0.95, 0.99), + quantiles=(0.01, 0.05, 0.25, 0.50, 0.75, 0.95, 0.99)) -> dict: + """The horizon P&L distribution of a portfolio: moments, quantiles, VaR/CVaR, histogram. + + Everything is probability-weighted, so an importance-sampled set (whose raw sample is *not* + the model distribution) reports the figures of the model. + """ + pnl = self.portfolio_pnl(exposure) + w = self.weights + mean = float(w @ pnl) + var = float(w @ (pnl - mean) ** 2) + sd = float(np.sqrt(max(var, 0.0))) + skew = float(w @ (pnl - mean) ** 3 / sd ** 3) if sd > 0 else 0.0 + kurt = float(w @ (pnl - mean) ** 4 / sd ** 4) if sd > 0 else 0.0 + mass, edges = np.histogram(pnl, bins=bins, weights=w) + risk = {} + for beta in confidence: + var_m, cvar_m = scenario_cvar(-pnl, float(beta), self.probabilities) + risk[f"{float(beta):g}"] = {"var": float(var_m), "cvar": float(cvar_m)} + return { + "n_scenarios": self.n_scenarios, "weighted": self.probabilities is not None, + "effective_sample_size": round(smp.effective_sample_size(w), 1), + "expected_pnl": mean, "volatility": sd, "skewness": skew, "kurtosis": kurt, + "min": float(pnl.min()), "max": float(pnl.max()), + "quantiles": {f"{q:g}": float(v) for q, v in + zip(quantiles, weighted_quantile(pnl, quantiles, self.probabilities))}, + "risk": risk, + "histogram": {"edges": edges.tolist(), "mass": mass.tolist()}, + } + + def describe(self) -> dict: + return { + "model": self.model, "n_scenarios": self.n_scenarios, "n_assets": len(self.symbols), + "horizon_periods": self.horizon_periods, "freq": self.freq, "as_of": self.as_of, "seed": self.seed, + "weighted": self.probabilities is not None, + "effective_sample_size": round(smp.effective_sample_size(self.weights), 1), + "hash": self.hash, "shock_hash": self.shock_hash, "covariance_model": self.covariance_model, + **self.extra, + } + + +# --------------------------------------------------------------------------------------------- +# helpers +# --------------------------------------------------------------------------------------------- +def _mu_vector(mu, symbols) -> np.ndarray: + if mu is None: + return np.zeros(len(symbols)) + if isinstance(mu, pd.Series): + mu = mu.reindex(symbols).fillna(0.0).values + return np.asarray(mu, dtype=float) + + +def horizon_drift(mu_period, cov_period, horizon_periods: int, convention: str = "arithmetic" + ) -> tuple[np.ndarray, np.ndarray]: + """``(log_drift, expected_simple)`` over the horizon for per-period expected returns ``mu``. + + ``'arithmetic'``: ``mu`` is the expected simple return per period; the log-drift gets the + Ito correction so that ``E[exp(r_h) - 1] = h mu`` exactly under normality. + ``'log'``: ``mu`` is the log-drift; the expected simple return is ``exp(h mu + h sigma^2/2) - 1``. + """ + if convention not in DRIFT_CONVENTIONS: + raise ValueError(f"drift_convention must be one of {DRIFT_CONVENTIONS}") + mu = np.asarray(mu_period, dtype=float) + var_h = np.diag(np.asarray(cov_period, dtype=float)) * horizon_periods + mu_h = mu * horizon_periods + if convention == "arithmetic": + mu_h = np.maximum(mu_h, -0.99) + return np.log1p(mu_h) - 0.5 * var_h, mu_h + return mu_h, np.expm1(mu_h + 0.5 * var_h) + + +def _finalise(z_scaled: np.ndarray, root: np.ndarray, log_drift: np.ndarray) -> np.ndarray: + return log_drift + z_scaled @ root.T + + +# --------------------------------------------------------------------------------------------- +# GBM +# --------------------------------------------------------------------------------------------- +class GBMSimulator: + """Multivariate normal horizon returns with the latest covariance of ``cov_model``. + + ``root``: ``'pca'`` (eigen-root, largest component first -- the right choice for Sobol' + points), ``'symmetric'`` or ``'cholesky'``; the distribution is the same for all three. + """ + + def __init__(self, cov_model: CovarianceModel | None = None, root: str = "pca"): + self.cov_model = cov_model or CovarianceModel() + self.root = root + + def _root(self, cov) -> np.ndarray: + return smp.pca_root(cov) if self.root == "pca" else matrix_root(cov, self.root) + + def simulate(self, r: pd.DataFrame, n_scenarios: int = 4096, horizon_periods: int = 1, mu=None, + seed: int | None = 42, freq: str = "daily", sampling: str = "sobol", moment_match: bool = True, + tilt: TiltSpec | None = None, drift_convention: str = "arithmetic") -> ScenarioSet: + symbols = list(r.columns) + cov = self.cov_model.estimate(r).values + a_h = self._root(cov) * np.sqrt(horizon_periods) + log_drift, expected_simple = horizon_drift(_mu_vector(mu, symbols), cov, horizon_periods, drift_convention) + n_req = int(n_scenarios) + S = smp.next_power_of_two(n_req) if sampling == "sobol" else n_req + z = smp.standard_normal_sample(S, len(symbols), sampling, seed) + probabilities = None + info: dict = {"sampling": sampling, "moment_matched": bool(moment_match), "n_requested": n_req, + "drift_convention": drift_convention, "root": self.root} + if tilt is not None: + direction = smp.loss_direction(a_h, tilt.exposure) + z, probabilities = smp.tilt_normal_sample(z, direction, tilt.shift, tilt.mixture) + info["importance_sampling"] = tilt.describe() + if moment_match: + z = smp.moment_match(z, probabilities) + scen = _finalise(z, a_h, log_drift) + desc = self.cov_model.describe() + desc["shrinkage_applied"] = self.cov_model.intensity(r) + return ScenarioSet(scen, symbols, horizon_periods, freq, "gbm", desc, str(r.index[-1].date()), seed, log_drift, + expected_simple, probabilities, np.diag(cov) * horizon_periods, info) + + +# --------------------------------------------------------------------------------------------- +# empirical innovations +# --------------------------------------------------------------------------------------------- +class EmpiricalInnovationSimulator: + """Bootstrap of standardised historical innovations rescaled with the latest covariance.""" + + def __init__(self, cov_model: CovarianceModel | None = None, root: str = "symmetric", center: bool = True, + max_history: int | None = None, clip: float | None = 8.0, standardize: str | None = "diagonal"): + self.cov_model = cov_model or CovarianceModel() + self.root = root + self.center = center + self.max_history = max_history + self.clip = clip # winsorise standardised innovations at +/- clip (a guard, not a model choice) + # re-standardisation of the innovation sample before resampling: 'diagonal' (unit variance per + # asset), 'full' (whitening with the sample covariance of the innovations, needs T >> N) or None. + # A lagged volatility filter leaves the innovations with variance > 1 on average (the forecast + # is too low after every shock); rescaling makes the scenario covariance reproduce Sigma_T. + if standardize not in (None, "diagonal", "full"): + raise ValueError("standardize must be None, 'diagonal' or 'full'") + self.standardize = standardize + + def innovations(self, r: pd.DataFrame) -> pd.DataFrame: + """``eps_t = Sigma_{t-1}^(-1/2) r_t`` for every date after the estimator's warm-up.""" + r = r.dropna() + if self.max_history: + r = r.iloc[-self.max_history :] + rows, dates = [], [] + for date, cov_prev, ret in self.cov_model.sequence(r): + if self.root == "cholesky": + from scipy.linalg import solve_triangular + + from quantcore.models.covariance import cholesky_factor + + eps = solve_triangular(cholesky_factor(cov_prev), ret, lower=True) + else: + eps = symmetric_inv_sqrt(cov_prev) @ ret + rows.append(eps) + dates.append(date) + if not rows: + raise ValueError("no innovations - history shorter than the estimator warm-up") + eps = np.vstack(rows) + if self.clip: + n_clipped = int((np.abs(eps) > self.clip).sum()) + if n_clipped: + log.info("innovations: %d values winsorised at +/-%.0f", n_clipped, self.clip) + eps = np.clip(eps, -self.clip, self.clip) + return pd.DataFrame(eps, index=pd.DatetimeIndex(dates), columns=r.columns) + + def simulate(self, r: pd.DataFrame, n_scenarios: int = 4096, horizon_periods: int = 1, mu=None, + seed: int | None = 42, freq: str = "daily", innovations: pd.DataFrame | None = None, + sampling: str = "sobol", moment_match: bool = True, tilt: TiltSpec | None = None, + drift_convention: str = "arithmetic") -> ScenarioSet: + symbols = list(r.columns) + eps = innovations if innovations is not None else self.innovations(r) + e = eps.values - (eps.values.mean(axis=0) if self.center else 0.0) + e, scale_info = self._restandardize(e) + cov = self.cov_model.estimate(r).values + a = matrix_root(cov, self.root) + log_drift, expected_simple = horizon_drift(_mu_vector(mu, symbols), cov, horizon_periods, drift_convention) + h = int(horizon_periods) + n_req = int(n_scenarios) + S = smp.next_power_of_two(n_req) if sampling == "sobol" else n_req + # market-factor ordering of the observations (projection on the dominant direction of Sigma_T) + w1 = np.linalg.eigh(cov)[1][:, -1] + order = np.argsort(e @ (a @ w1)) + probabilities = None + info: dict = {"n_innovations": len(e), "root": self.root, "standardize": self.standardize, + "sampling": sampling, "moment_matched": bool(moment_match), "n_requested": n_req, + "drift_convention": drift_convention, **scale_info} + if tilt is not None: + direction = smp.loss_direction(a, tilt.exposure) # in innovation space + scores = e @ direction + targets = tuple(sh / np.sqrt(h) for sh in tilt.shift) # per-day mean of the tilted score + idx, probabilities, thetas = smp.tilted_resample(scores, S, h, targets, seed, tilt.mixture) + info["importance_sampling"] = {**tilt.describe(), "theta": [round(t, 4) for t in thetas]} + else: + idx = smp.resample_indices(len(e), S, h, sampling, seed, order) + summed = e[idx].sum(axis=1) # S x N sums of h innovations, covariance ~ h I + if moment_match: + summed = smp.moment_match(summed, probabilities, target_variance=float(h)) + scen = _finalise(summed, a, log_drift) + desc = self.cov_model.describe() + desc["shrinkage_applied"] = self.cov_model.intensity(r) + return ScenarioSet(scen, symbols, horizon_periods, freq, "empirical", desc, str(r.index[-1].date()), seed, + log_drift, expected_simple, probabilities, np.diag(cov) * horizon_periods, info) + + def _restandardize(self, e: np.ndarray) -> tuple[np.ndarray, dict]: + """Rescale the centred innovation sample to unit variance (diagonal) or identity covariance (full).""" + if self.standardize is None: + return e, {} + var = e.var(axis=0, ddof=1) + info = {"innovation_variance_median": float(np.median(var))} + if self.standardize == "diagonal": + return e / np.sqrt(np.where(var > 0, var, 1.0)), info + n_obs, n = e.shape + if n_obs < 3 * n: + log.warning("full whitening needs T >= 3N (T=%d, N=%d) - using diagonal rescaling", n_obs, n) + return e / np.sqrt(np.where(var > 0, var, 1.0)), info + return e @ symmetric_inv_sqrt(np.cov(e, rowvar=False)), info + + +def make_simulator(model: str, cov_model: CovarianceModel | None = None, **kwargs): + if model == "gbm": + return GBMSimulator(cov_model, **kwargs) + if model in ("empirical", "innovations", "fhs"): + return EmpiricalInnovationSimulator(cov_model, **kwargs) + raise ValueError("model must be 'gbm' or 'empirical'") diff --git a/quantcore/optimization/__init__.py b/quantcore/optimization/__init__.py new file mode 100644 index 0000000..2c6bdfb --- /dev/null +++ b/quantcore/optimization/__init__.py @@ -0,0 +1,17 @@ +"""Mean-CVaR portfolio optimisation in integer share numbers (MILP on cuOpt, HiGHS fallback).""" + +from quantcore.optimization.problem import ( + MeanCVaRProblem, + MILPModel, + OptimizationConstraints, + ProblemSpec, + TransactionCosts, +) +from quantcore.optimization.service import OptimizationRequest, OptimizationResult, OptimizationService +from quantcore.optimization.solvers import CuOptSolver, HighsSolver, SolverResult, solve_with_fallback + +__all__ = [ + "MeanCVaRProblem", "MILPModel", "OptimizationConstraints", "ProblemSpec", "TransactionCosts", + "OptimizationRequest", "OptimizationResult", "OptimizationService", "CuOptSolver", "HighsSolver", + "SolverResult", "solve_with_fallback", +] diff --git a/quantcore/optimization/gaussian_exact.py b/quantcore/optimization/gaussian_exact.py new file mode 100644 index 0000000..01b7d0c --- /dev/null +++ b/quantcore/optimization/gaussian_exact.py @@ -0,0 +1,95 @@ +"""Exact mean-CVaR optimisation under the Gaussian model by outer approximation. + +If the horizon returns are normal, ``R ~ N(m, C)``, the money P&L of ``v = P * n`` is normal and + + CVaR_beta(n) = TC(n) - m'v + k_beta * sqrt(v' C v), k_beta = phi(z_beta) / (1 - beta). + +This is the mean-variance problem in disguise (for elliptical returns every portfolio on the +mean-CVaR frontier is on the mean-variance frontier), but in integer shares it is a convex +mixed-integer second-order-cone programme, which neither cuOpt nor HiGHS solves directly. The +convex function is instead outer-approximated by tangent cuts + + t >= TC(n) + (k_beta C v_k / sigma_k - m)' v (sigma_k = sqrt(v_k' C v_k)) + +on the same MILP as the scenario problem (:class:`MeanCVaRProblem` in Gaussian mode). Starting +from a cut at a reference portfolio, the MILP is solved, the true CVaR of its solution is +compared with the epigraph value ``t``, and a new cut is added at the solution until they agree. +Because the integer feasible set is finite and each cut removes the current infeasible point, +the loop terminates at the exact optimum (within the solver's MIP gap); measured: 20-70 cuts, +identical to Gurobi's direct MISOCP solution to the cent. No scenarios, no sampling error, fully +deterministic. +""" + +from __future__ import annotations + +import logging +import time +from collections.abc import Callable +from dataclasses import dataclass, field, replace + +from quantcore.optimization.problem import MeanCVaRProblem, MILPModel, ProblemSpec +from quantcore.optimization.rowgen import reference_exposure +from quantcore.optimization.solvers import SolverResult + +log = logging.getLogger(__name__) + +SolveFn = Callable[[MILPModel], SolverResult] + + +@dataclass +class OuterApproximationInfo: + iterations: int = 0 + cuts: int = 0 + gaps: list[float] = field(default_factory=list) # true CVaR - epigraph value per iteration + solve_times: list[float] = field(default_factory=list) + total_time: float = 0.0 + converged: bool = False + + def as_dict(self) -> dict: + return {"iterations": self.iterations, "cuts": self.cuts, "gaps": [round(g, 4) for g in self.gaps], + "solve_times_s": [round(t, 3) for t in self.solve_times], "total_time_s": round(self.total_time, 3), + "converged": self.converged} + + +def solve_gaussian_exact(spec: ProblemSpec, solve: SolveFn, tol: float = 1e-6, max_iter: int = 200 + ) -> tuple[SolverResult, MeanCVaRProblem, OuterApproximationInfo]: + """Outer-approximation loop for a Gaussian-mode :class:`ProblemSpec`. + + ``tol`` is relative to the CVaR scale (the limit, or the CVaR itself). Returns the last + solver result (its ``x`` decodes with the returned problem), the problem with all cuts, and + the iteration log. + """ + if not spec.is_gaussian: + raise ValueError("solve_gaussian_exact needs gaussian_mean and gaussian_cov") + t0 = time.monotonic() + info = OuterApproximationInfo() + problem = MeanCVaRProblem(spec) + problem.add_cut(reference_exposure(spec)) + result: SolverResult | None = None + for it in range(1, max_iter + 1): + t1 = time.monotonic() + result = solve(problem.build()) + info.solve_times.append(time.monotonic() - t1) + info.iterations = it + info.cuts = len(problem.cuts) + if not result.ok: + log.warning("outer approximation: solver returned %s at iteration %d", result.status, it) + break + d = problem.decode(result.x) + gap = d["cvar"] - d["cvar_model"] # true CVaR of the solution minus the epigraph value + info.gaps.append(float(gap)) + scale = max(abs(spec.cvar_limit or 0.0), abs(d["cvar"]), spec.wealth * 1e-4) + if gap <= tol * scale: + info.converged = True + result = replace(result, message=(result.message + f" (outer approximation: {it} iterations, " + f"{len(problem.cuts)} cuts)").strip()) + break + if d["sigma"] <= 0: # riskless solution whose cut cannot be formed: the model is exact there + info.converged = True + break + problem.add_cut(d["value"]) + else: + log.warning("outer approximation did not converge in %d iterations", max_iter) + result = replace(result, status="FeasibleFound", message=(result.message + " (outer approximation: iteration limit)").strip()) + info.total_time = time.monotonic() - t0 + return result, problem, info diff --git a/quantcore/optimization/problem.py b/quantcore/optimization/problem.py new file mode 100644 index 0000000..4edb049 --- /dev/null +++ b/quantcore/optimization/problem.py @@ -0,0 +1,544 @@ +"""Mean-CVaR optimisation as a mixed-integer linear programme. + +Decision variables (all share numbers are integers -- a stock can only be bought whole): + +* ``n_i`` final number of shares of stock ``i`` (integer, >= 0: no short positions) +* ``b_i``, ``s_i`` shares bought / sold (integers, rebalancing mode only): ``n_i = n0_i + b_i - s_i`` +* ``alpha`` the VaR level (free) and ``u_s >= 0`` the scenario excess losses of the + Rockafellar-Uryasev linearisation of CVaR (scenario mode), or the epigraph variable ``t`` + of the CVaR (Gaussian mode, see below) +* ``y_i`` binary "position open" indicators (only with a cardinality / minimum-position rule) + +**Scenario mode.** With ``S`` scenarios of horizon returns ``R_{s,i}`` (simple returns in the +base currency) with probabilities ``p_s`` (``1/S`` unless importance sampling was used), current +prices ``P_i`` and transaction costs ``TC``, the loss of scenario ``s`` is +``L_s = TC - sum_i P_i R_{s,i} n_i`` and + + CVaR_beta = min_alpha alpha + 1/(1-beta) sum_s p_s max(L_s - alpha, 0) + +which is linear once ``u_s >= L_s - alpha`` is added. The expected P&L in the objective uses the +*model* expected returns ``mu_i`` (``sum_i P_i mu_i n_i - TC``) when the spec provides them, +otherwise the probability-weighted scenario mean -- the scenarios then only shape the tail. + +**Gaussian mode** (``gaussian_mean``/``gaussian_cov`` given, no scenarios): horizon returns are +``R ~ N(m, C)``, the P&L is normal and ``CVaR_beta = TC - m'v + k_beta sqrt(v'Cv)`` with +``v = P * n`` and ``k_beta = phi(z_beta)/(1-beta)``. The convex function is represented by an +epigraph variable ``t`` and tangent cuts at exposures ``v_k`` (``t >= TC + (k_beta C v_k / sigma_k +- m)' v``), added by an outer-approximation loop (:mod:`quantcore.optimization.gaussian_exact`). +No scenarios, no sampling error: this is the mean-variance-equivalent exact solution. + +Objectives: + +* ``max_return``: maximise expected P&L subject to ``CVaR_beta <= cvar_limit`` (money); +* ``min_cvar``: minimise ``CVaR_beta`` subject to expected P&L ``>= target_pnl``; +* ``utility``: maximise ``expected P&L - risk_aversion * CVaR_beta``. + +Constraints: budget (cash cannot go negative), maximum weight per stock and per sector +(fractions of total wealth), optional transaction-cost and turnover limits, optional +cardinality / minimum position size. Weight limits are tight *variable bounds* +(``n_i <= floor(w_max W / P_i)``), which helps the MILP solver considerably; ``alpha`` and the +``u_s`` are bounded by the worst gain and loss attainable within those caps (see +``_loss_bounds``) -- free variables make the CVaR objective numerically hard. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +import numpy as np +import scipy.sparse as sp +from scipy.stats import norm + +from quantcore.measurement.risk import scenario_cvar + + +@dataclass +class OptimizationConstraints: + max_weight: float = 0.10 # per stock, fraction of total wealth + max_sector_weight: float | dict[str, float] = 0.25 # per GICS sector (scalar = same for all) + max_positions: int | None = None # cardinality limit (adds binaries) + min_position_value: float = 0.0 # minimum value of an open position (adds binaries) + max_turnover: float | None = None # fraction of wealth traded (buys + sells) + max_transaction_cost: float | None = None # absolute money amount + min_invested: float = 0.0 # minimum fraction of wealth invested in stocks + max_invested: float = 1.0 # maximum fraction invested (1.0 = fully, remainder is cash) + + def sector_limit(self, sector: str) -> float | None: + if isinstance(self.max_sector_weight, dict): + return self.max_sector_weight.get(sector, self.max_sector_weight.get("*")) + return self.max_sector_weight + + def uses_binaries(self) -> bool: + return self.max_positions is not None or self.min_position_value > 0 + + +@dataclass +class TransactionCosts: + proportional_buy: float = 0.001 # 10 bp of the traded value + proportional_sell: float = 0.001 + fixed_per_trade: float = 0.0 # money per stock traded (adds trade binaries) + + def uses_binaries(self) -> bool: + return self.fixed_per_trade > 0 + + +def cvar_multiplier(beta: float) -> float: + """``k_beta = phi(z_beta) / (1 - beta)``: Gaussian CVaR in standard deviations.""" + return float(norm.pdf(norm.ppf(beta)) / (1.0 - beta)) + + +@dataclass +class ProblemSpec: + """Everything the builder needs, as plain arrays (built by the service, tested directly).""" + + symbols: list[str] + prices: np.ndarray # current price per share, base currency + scenarios: np.ndarray | None # S x N simple returns over the horizon (None in Gaussian mode) + sectors: list[str] + cash: float # initial cash + holdings: np.ndarray | None = None # initial shares (None or zeros: new portfolio) + confidence: float = 0.95 + cvar_limit: float | None = None # money + objective: str = "max_return" # 'max_return' | 'min_cvar' | 'utility' + risk_aversion: float = 1.0 # for 'utility' + target_pnl: float | None = None # for 'min_cvar' (money over the horizon) + constraints: OptimizationConstraints = field(default_factory=OptimizationConstraints) + costs: TransactionCosts = field(default_factory=TransactionCosts) + probabilities: np.ndarray | None = None # scenario probabilities (None = 1/S); may sum to < 1 for a subset + expected_returns: np.ndarray | None = None # model expected simple return per asset over the horizon + gaussian_mean: np.ndarray | None = None # Gaussian mode: mean of the horizon simple returns + gaussian_cov: np.ndarray | None = None # Gaussian mode: covariance of the horizon simple returns + + def __post_init__(self) -> None: + self.prices = np.asarray(self.prices, dtype=float) + n = len(self.symbols) + if self.prices.shape != (n,): + raise ValueError("prices must have one entry per symbol") + if len(self.sectors) != n: + raise ValueError("sectors must have one entry per symbol") + if self.holdings is None: + self.holdings = np.zeros(n) + self.holdings = np.asarray(self.holdings, dtype=float) + if np.any(self.holdings < 0): + raise ValueError("initial holdings cannot be negative") + if not 0 < self.confidence < 1: + raise ValueError("confidence must be in (0, 1)") + if self.objective == "max_return" and self.cvar_limit is None: + raise ValueError("objective 'max_return' needs a cvar_limit") + if self.objective == "min_cvar" and self.target_pnl is None: + raise ValueError("objective 'min_cvar' needs a target_pnl") + if self.objective not in ("max_return", "min_cvar", "utility"): + raise ValueError("objective must be 'max_return', 'min_cvar' or 'utility'") + if self.is_gaussian: + self.gaussian_mean = np.asarray(self.gaussian_mean, dtype=float) + self.gaussian_cov = np.asarray(self.gaussian_cov, dtype=float) + if self.gaussian_mean.shape != (n,) or self.gaussian_cov.shape != (n, n): + raise ValueError("gaussian_mean must be N and gaussian_cov N x N") + self.scenarios = np.zeros((0, n)) + if self.expected_returns is None: + self.expected_returns = self.gaussian_mean + else: + if self.scenarios is None: + raise ValueError("scenarios are required unless gaussian_mean/gaussian_cov are given") + self.scenarios = np.asarray(self.scenarios, dtype=float) + if self.scenarios.ndim != 2 or self.scenarios.shape[1] != n or self.scenarios.shape[0] == 0: + raise ValueError("scenarios must be S x N with S >= 1") + if self.probabilities is not None: + self.probabilities = np.asarray(self.probabilities, dtype=float) + if self.probabilities.shape != (self.scenarios.shape[0],): + raise ValueError("probabilities must have one entry per scenario") + if np.any(self.probabilities < 0) or self.probabilities.sum() > 1.0 + 1e-9: + raise ValueError("probabilities must be non-negative and sum to at most 1") + if self.expected_returns is not None: + self.expected_returns = np.asarray(self.expected_returns, dtype=float) + if self.expected_returns.shape != (n,): + raise ValueError("expected_returns must have one entry per symbol") + + @property + def is_gaussian(self) -> bool: + return self.gaussian_mean is not None and self.gaussian_cov is not None + + @property + def n_scenarios(self) -> int: + return int(self.scenarios.shape[0]) + + @property + def weights(self) -> np.ndarray: + """Scenario probabilities (``1/S`` when none were given).""" + if self.probabilities is None: + return np.full(self.n_scenarios, 1.0 / max(self.n_scenarios, 1)) + return self.probabilities + + @property + def mean_returns(self) -> np.ndarray: + """Expected simple return per asset used in the objective.""" + if self.expected_returns is not None: + return self.expected_returns + w = self.weights + return (w @ self.scenarios) / w.sum() + + @property + def rebalancing(self) -> bool: + return bool(np.any(self.holdings > 0)) + + @property + def wealth(self) -> float: + return float(self.cash + self.holdings @ self.prices) + + +@dataclass +class MILPModel: + """Solver-agnostic MILP: ``min/max c'x s.t. row_lb <= A x <= row_ub, var_lb <= x <= var_ub``.""" + + c: np.ndarray + A: sp.csr_matrix + row_lb: np.ndarray + row_ub: np.ndarray + var_lb: np.ndarray + var_ub: np.ndarray + var_types: list[str] # 'I' | 'C' (binaries are 'I' with bounds [0, 1]) + var_names: list[str] + row_names: list[str] + maximize: bool + offset: float = 0.0 + + @property + def n_vars(self) -> int: + return len(self.c) + + @property + def n_rows(self) -> int: + return self.A.shape[0] + + @property + def nnz(self) -> int: + return int(self.A.nnz) + + def describe(self) -> dict: + return {"n_vars": self.n_vars, "n_int": sum(t == "I" for t in self.var_types), "n_rows": self.n_rows, + "nnz": self.nnz, "maximize": self.maximize} + + +class MeanCVaRProblem: + """Builds the MILP for a :class:`ProblemSpec` and decodes solver vectors.""" + + def __init__(self, spec: ProblemSpec): + self.spec = spec + self.n = len(spec.symbols) + self.gaussian = spec.is_gaussian + self.S = 0 if self.gaussian else spec.n_scenarios + self.rebalancing = spec.rebalancing + self.use_pos_bin = spec.constraints.uses_binaries() + self.use_trade_bin = spec.costs.uses_binaries() + self.cuts: list[np.ndarray] = [] # Gaussian mode: exposures v_k of the tangent cuts + self._layout() + + # -- variable layout ----------------------------------------------------------------------- + def _layout(self) -> None: + n = self.n + self.i_n = np.arange(0, n) + pos = n + if self.rebalancing: + self.i_b = np.arange(pos, pos + n) + pos += n + self.i_s = np.arange(pos, pos + n) + pos += n + else: + self.i_b = self.i_s = None + if self.gaussian: + self.i_alpha = None + self.i_u = np.arange(0) + self.i_cv = pos # epigraph variable of the CVaR + pos += 1 + else: + self.i_alpha = pos + pos += 1 + self.i_u = np.arange(pos, pos + self.S) + pos += self.S + self.i_cv = None + if self.use_pos_bin: + self.i_y = np.arange(pos, pos + n) + pos += n + else: + self.i_y = None + if self.use_trade_bin: + self.i_t = np.arange(pos, pos + n) + pos += n + else: + self.i_t = None + self.n_vars = pos + + # -- helpers -------------------------------------------------------------------------------- + def _max_shares(self) -> np.ndarray: + """Tight upper bound per stock: ``floor(min(w_max, f_max) * W / P_i)`` (the weight limit, or the + invested fraction if that is smaller). A legacy holding above the cap has to be sold down.""" + w = self.spec.wealth + lim = min(self.spec.constraints.max_weight, self.spec.constraints.max_invested) * w + ub = np.floor(lim / self.spec.prices + 1e-9) + return np.maximum(ub, 0.0) + + def _loss_bounds(self, ub_shares: np.ndarray) -> tuple[float, float]: + """Valid finite bounds ``(max_gain, max_loss)`` over the box ``0 <= n_i <= ub_shares_i``. + + Any feasible portfolio's money P&L lies in ``[-max_loss, max_gain]``, so ``alpha`` and the + ``u_s`` can be bounded: free variables with infinite bounds make the CVaR objective + numerically hard for the solvers (cuOpt's dual simplex aborts on such models). + """ + p = self.spec.prices + cost_ub = float(self.spec.costs.proportional_buy + self.spec.costs.proportional_sell) * float( + (ub_shares * p).sum()) + float(self.spec.costs.fixed_per_trade) * self.n + if self.gaussian: + v_max = ub_shares * p + mean = np.abs(self.spec.gaussian_mean) @ v_max + sigma = float(np.sqrt(max(v_max @ self.spec.gaussian_cov @ v_max, 0.0))) + spread = mean + cvar_multiplier(self.spec.confidence) * sigma + cost_ub + return spread, spread + gain = self.spec.scenarios * p[None, :] # money gain per share, per scenario + max_gain = float(np.maximum(gain.max(axis=0), 0.0) @ ub_shares) + max_loss = float(np.maximum(-gain.min(axis=0), 0.0) @ ub_shares) + cost_ub + return max_gain, max_loss + + def _cost_terms(self): + """Coefficients of the transaction cost ``TC`` per variable (b, s, t or n).""" + p, c = self.spec.prices, self.spec.costs + terms = [] # (indices, coefficients) + if self.rebalancing: + terms.append((self.i_b, c.proportional_buy * p)) + terms.append((self.i_s, c.proportional_sell * p)) + else: + terms.append((self.i_n, c.proportional_buy * p)) + if self.use_trade_bin: + terms.append((self.i_t, np.full(self.n, c.fixed_per_trade))) + return terms + + def _cvar_expr(self) -> tuple[np.ndarray, np.ndarray]: + """Indices and coefficients of the linear expression equal to ``CVaR_beta``.""" + if self.gaussian: + return np.array([self.i_cv]), np.array([1.0]) + tail = self.spec.weights / (1.0 - self.spec.confidence) + return np.concatenate([[self.i_alpha], self.i_u]), np.concatenate([[1.0], tail]) + + # -- Gaussian mode ----------------------------------------------------------------------- + def gaussian_risk(self, exposure: np.ndarray, total_cost: float = 0.0) -> tuple[float, float, float]: + """``(VaR, CVaR, sigma)`` of the money P&L for exposure ``v`` under the Gaussian model.""" + v = np.asarray(exposure, dtype=float) + sigma = float(np.sqrt(max(v @ self.spec.gaussian_cov @ v, 0.0))) + mean = float(self.spec.gaussian_mean @ v) - total_cost + z = float(norm.ppf(self.spec.confidence)) + return -mean + z * sigma, -mean + cvar_multiplier(self.spec.confidence) * sigma, sigma + + def add_cut(self, exposure: np.ndarray) -> None: + """Add a tangent cut of the Gaussian CVaR at money exposure ``v_k`` (``v_k`` must carry risk).""" + if not self.gaussian: + raise ValueError("cuts only exist in Gaussian mode") + v = np.asarray(exposure, dtype=float) + if v @ self.spec.gaussian_cov @ v <= 0: + raise ValueError("cut exposure has no risk") + self.cuts.append(v) + + # -- build ------------------------------------------------------------------------------------ + def build(self) -> MILPModel: + sp_ = self.spec + n, S = self.n, self.S + p = sp_.prices + W = sp_.wealth + mean_gain = sp_.mean_returns * p # expected money gain per share + cost_terms = self._cost_terms() + ub_shares = self._max_shares() + + rows, cols, vals = [], [], [] + row_lb, row_ub, row_names = [], [], [] + r = 0 + + def add_row(idx, coef, lb, ub, name): + nonlocal r + idx = np.asarray(idx) + coef = np.asarray(coef, dtype=float) + keep = coef != 0 + rows.extend([r] * int(keep.sum())) + cols.extend(idx[keep].tolist()) + vals.extend(coef[keep].tolist()) + row_lb.append(lb) + row_ub.append(ub) + row_names.append(name) + r += 1 + + cost_idx = np.concatenate([i for i, _ in cost_terms]) + cost_coef = np.concatenate([c for _, c in cost_terms]) + + if self.gaussian: + # 1g. tangent cuts: TC + (k C v_k / sigma_k - m)' (P n) - t <= 0 + if not self.cuts: + raise ValueError("Gaussian mode needs at least one cut (use gaussian_exact.solve)") + k = cvar_multiplier(sp_.confidence) + for j, v_k in enumerate(self.cuts): + sigma_k = np.sqrt(v_k @ sp_.gaussian_cov @ v_k) + grad = k * (sp_.gaussian_cov @ v_k) / sigma_k - sp_.gaussian_mean + idx = np.concatenate([self.i_n, cost_idx, [self.i_cv]]) + coef = np.concatenate([grad * p, cost_coef, [-1.0]]) + add_row(idx, coef, -np.inf, 0.0, f"cut_{j}") + else: + # 1. CVaR scenario rows: sum_i gain_si n_i - TC + alpha + u_s >= 0 + gain = sp_.scenarios * p[None, :] # money gain per share in each scenario + for s in range(S): + idx = np.concatenate([self.i_n, cost_idx, [self.i_alpha, self.i_u[s]]]) + coef = np.concatenate([gain[s], -cost_coef, [1.0, 1.0]]) + add_row(idx, coef, 0.0, np.inf, f"cvar_scen_{s}") + + # 2. CVaR expression rows (limit for max_return, objective otherwise) + cvar_idx, cvar_coef = self._cvar_expr() + if sp_.objective == "max_return" or sp_.cvar_limit is not None: + add_row(cvar_idx, cvar_coef, -np.inf, float(sp_.cvar_limit), "cvar_limit") + + # 3. budget: cash_end = cash0 + sum p s - sum p b - TC >= 0 (duplicate indices are summed) + if self.rebalancing: + add_row(np.concatenate([self.i_b, self.i_s, cost_idx]), np.concatenate([p, -p, cost_coef]), -np.inf, + sp_.cash, "budget") + # 4. linking n - b + s = n0 + for i in range(n): + add_row([self.i_n[i], self.i_b[i], self.i_s[i]], [1.0, -1.0, 1.0], sp_.holdings[i], sp_.holdings[i], + f"link_{i}") + else: + add_row(np.concatenate([self.i_n, cost_idx]), np.concatenate([p, cost_coef]), -np.inf, sp_.cash, "budget") + + # 5. invested fraction bounds (min / max) + cons = sp_.constraints + lo = cons.min_invested * W if cons.min_invested > 0 else -np.inf + hi = cons.max_invested * W if cons.max_invested < 1.0 else np.inf + if np.isfinite(lo) or np.isfinite(hi): + add_row(self.i_n, p, lo, hi, "invested") + + # 6. sector limits + sectors = np.asarray(sp_.sectors) + for sec in sorted(set(sectors)): + lim = cons.sector_limit(sec) + if lim is None or lim >= cons.max_invested: + continue + members = np.where(sectors == sec)[0] + add_row(self.i_n[members], p[members], -np.inf, lim * W, f"sector_{sec}") + + # 7. transaction cost limit and 8. turnover + if cons.max_transaction_cost is not None: + add_row(cost_idx, cost_coef, -np.inf, cons.max_transaction_cost, "max_cost") + if cons.max_turnover is not None and self.rebalancing: + add_row(np.concatenate([self.i_b, self.i_s]), np.concatenate([p, p]), -np.inf, cons.max_turnover * W, + "turnover") + + # 9. cardinality / minimum position size via binaries y_i + if self.use_pos_bin: + for i in range(n): + add_row([self.i_n[i], self.i_y[i]], [1.0, -max(ub_shares[i], 1.0)], -np.inf, 0.0, f"open_{i}") + if cons.min_position_value > 0: + add_row([self.i_n[i], self.i_y[i]], [p[i], -cons.min_position_value], 0.0, np.inf, f"minpos_{i}") + if cons.max_positions is not None: + add_row(self.i_y, np.ones(n), -np.inf, float(cons.max_positions), "cardinality") + + # 10. trade indicators for fixed costs: (b_i + s_i) <= M t_i (new mode: n_i <= M t_i) + if self.use_trade_bin: + big_m = np.maximum(ub_shares, sp_.holdings) + 1.0 + for i in range(n): + if self.rebalancing: + add_row([self.i_b[i], self.i_s[i], self.i_t[i]], [1.0, 1.0, -big_m[i]], -np.inf, 0.0, f"trade_{i}") + else: + add_row([self.i_n[i], self.i_t[i]], [1.0, -big_m[i]], -np.inf, 0.0, f"trade_{i}") + + # 11. target expected P&L for min_cvar + pnl_idx = np.concatenate([self.i_n, cost_idx]) + pnl_coef = np.concatenate([mean_gain, -cost_coef]) + if sp_.objective == "min_cvar": + add_row(pnl_idx, pnl_coef, float(sp_.target_pnl), np.inf, "target_pnl") + + # objective + c = np.zeros(self.n_vars) + if sp_.objective == "max_return": + np.add.at(c, pnl_idx, pnl_coef) + maximize = True + elif sp_.objective == "min_cvar": + np.add.at(c, cvar_idx, cvar_coef) + maximize = False + else: # utility + np.add.at(c, pnl_idx, pnl_coef) + np.add.at(c, cvar_idx, -sp_.risk_aversion * cvar_coef) + maximize = True + + # variable bounds and types + var_lb = np.zeros(self.n_vars) + var_ub = np.full(self.n_vars, np.inf) + types = ["C"] * self.n_vars + names = [""] * self.n_vars + for i in range(n): + types[self.i_n[i]] = "I" + names[self.i_n[i]] = f"n[{sp_.symbols[i]}]" + var_ub[self.i_n[i]] = ub_shares[i] + if self.rebalancing: + for i in range(n): + types[self.i_b[i]] = types[self.i_s[i]] = "I" + names[self.i_b[i]] = f"buy[{sp_.symbols[i]}]" + names[self.i_s[i]] = f"sell[{sp_.symbols[i]}]" + var_ub[self.i_b[i]] = ub_shares[i] + var_ub[self.i_s[i]] = sp_.holdings[i] + max_gain, max_loss = self._loss_bounds(ub_shares) + if self.gaussian: + var_lb[self.i_cv], var_ub[self.i_cv] = -max_gain, max_loss + names[self.i_cv] = "cvar" + else: + var_lb[self.i_alpha], var_ub[self.i_alpha] = -max_gain, max_loss + names[self.i_alpha] = "alpha" + for s in range(S): + var_ub[self.i_u[s]] = max_loss + max_gain # u_s <= L_s - alpha over the box + names[self.i_u[s]] = f"u[{s}]" + if self.use_pos_bin: + for i in range(n): + types[self.i_y[i]] = "I" + var_ub[self.i_y[i]] = 1.0 + names[self.i_y[i]] = f"y[{sp_.symbols[i]}]" + if self.use_trade_bin: + for i in range(n): + types[self.i_t[i]] = "I" + var_ub[self.i_t[i]] = 1.0 + names[self.i_t[i]] = f"t[{sp_.symbols[i]}]" + + A = sp.csr_matrix((vals, (rows, cols)), shape=(r, self.n_vars)) + A.sum_duplicates() + A.sort_indices() + return MILPModel(c=c, A=A, row_lb=np.array(row_lb, dtype=float), row_ub=np.array(row_ub, dtype=float), + var_lb=var_lb, var_ub=var_ub, var_types=types, var_names=names, row_names=row_names, + maximize=maximize) + + # -- decode ----------------------------------------------------------------------------------- + def decode(self, x: np.ndarray) -> dict: + """Turn a solution vector into shares, trades, cash, costs and the realised CVaR.""" + sp_ = self.spec + p = sp_.prices + shares = np.rint(x[self.i_n]).astype(int) + if self.rebalancing: + buys = np.rint(x[self.i_b]).astype(int) + sells = np.rint(x[self.i_s]).astype(int) + else: + buys, sells = shares.copy(), np.zeros(self.n, dtype=int) + trade_value_buy = buys * p + trade_value_sell = sells * p + cost = (sp_.costs.proportional_buy * trade_value_buy + sp_.costs.proportional_sell * trade_value_sell) + if self.use_trade_bin: + cost = cost + sp_.costs.fixed_per_trade * ((buys + sells) > 0) + total_cost = float(cost.sum()) + cash_end = float(sp_.cash + trade_value_sell.sum() - trade_value_buy.sum() - total_cost) + value = shares * p + expected_pnl = float(sp_.mean_returns @ value - total_cost) # the objective's expectation + out = { + "shares": shares, "buys": buys, "sells": sells, "trades": buys - sells, "value": value, + "cost": cost, "total_cost": total_cost, "cash_end": cash_end, "invested": float(value.sum()), + "expected_pnl": expected_pnl, "weights": value / sp_.wealth, + "turnover": float((trade_value_buy.sum() + trade_value_sell.sum()) / sp_.wealth), + } + if self.gaussian: + var_m, cvar_m, sigma = self.gaussian_risk(value, total_cost) + out.update({"var": var_m, "cvar": cvar_m, "sigma": sigma, "alpha": var_m, + "cvar_model": float(x[self.i_cv]), "pnl_scenarios": None}) + else: + pnl = sp_.scenarios @ value - total_cost # money P&L per scenario + var_m, cvar_m = scenario_cvar(-pnl, sp_.confidence, sp_.probabilities) + w = sp_.weights + out.update({"var": var_m, "cvar": cvar_m, "alpha": float(x[self.i_alpha]), "pnl_scenarios": pnl, + "expected_pnl_scenarios": float((w @ pnl) / w.sum())}) + return out diff --git a/quantcore/optimization/rowgen.py b/quantcore/optimization/rowgen.py new file mode 100644 index 0000000..db0348c --- /dev/null +++ b/quantcore/optimization/rowgen.py @@ -0,0 +1,151 @@ +"""Tail-row generation for large scenario sets (Kuenzi-Bay & Mayer, 2006). + +In the Rockafellar-Uryasev MILP only the scenarios in the tail have ``u_s > 0`` at the optimum; +every other scenario row is slack. So the problem is solved on a *working set* of scenarios +(initially the worst ``initial_multiple * (1 - beta) * S`` scenarios of a reference portfolio), +the losses of all ``S`` scenarios are evaluated for the solution, the scenarios whose loss exceeds +the solution's ``alpha`` are added, and the loop repeats. The restricted problem is a relaxation +(dropped rows, ``u_s = 0``), so its objective bounds the full one; when no omitted scenario is +violated the solution is optimal for the full scenario set (within the MIP gap). + +Measured on the real universe (30 stocks, beta = 0.95): working sets of ~11 % of ``S``, two to +three iterations, 6-7x faster than the full MILP at 32 768-100 000 scenarios -- and only the +working-set rows are sent to the solver. +""" + +from __future__ import annotations + +import logging +import time +from collections.abc import Callable +from dataclasses import dataclass, field, replace + +import numpy as np + +from quantcore.optimization.problem import MeanCVaRProblem, MILPModel, ProblemSpec +from quantcore.optimization.solvers import SolverResult + +log = logging.getLogger(__name__) + +SolveFn = Callable[[MILPModel], SolverResult] + + +@dataclass +class RowGenerationInfo: + iterations: int = 0 + working_set_sizes: list[int] = field(default_factory=list) + violated: list[int] = field(default_factory=list) + solve_times: list[float] = field(default_factory=list) + total_time: float = 0.0 + converged: bool = False + + def as_dict(self) -> dict: + return {"iterations": self.iterations, "working_set_sizes": self.working_set_sizes, "violated": self.violated, + "rows_final": self.working_set_sizes[-1] if self.working_set_sizes else 0, + "solve_times_s": [round(t, 3) for t in self.solve_times], "total_time_s": round(self.total_time, 3), + "converged": self.converged} + + +def reference_exposure(spec: ProblemSpec) -> np.ndarray: + """Money exposure of the reference portfolio: the current holdings, or (new portfolio) an + equal-weight allocation within the per-stock cap.""" + if spec.rebalancing: + return spec.holdings * spec.prices + n = len(spec.symbols) + w = min(spec.constraints.max_weight, spec.constraints.max_invested, 1.0 / n) + return np.full(n, w * spec.wealth) + + +def scenario_losses(spec: ProblemSpec, exposure: np.ndarray, total_cost: float = 0.0) -> np.ndarray: + """Money loss of every scenario for a given exposure (``TC - sum_i R_si v_i``).""" + return total_cost - spec.scenarios @ np.asarray(exposure, dtype=float) + + +def restricted_spec(spec: ProblemSpec, work: np.ndarray) -> ProblemSpec: + """The spec on a subset of scenarios: probabilities keep their *full-set* values (the omitted + scenarios contribute ``u_s = 0``), the objective keeps the full-set expected returns.""" + return replace(spec, scenarios=spec.scenarios[work], probabilities=spec.weights[work], + expected_returns=spec.mean_returns) + + +def expand_solution(problem_r: MeanCVaRProblem, x_r: np.ndarray, problem_full: MeanCVaRProblem) -> np.ndarray: + """Map a restricted solution to the full layout: ``u_s = max(L_s - alpha, 0)`` for every scenario.""" + x = np.zeros(problem_full.n_vars) + x[problem_full.i_n] = x_r[problem_r.i_n] + if problem_full.rebalancing: + x[problem_full.i_b] = x_r[problem_r.i_b] + x[problem_full.i_s] = x_r[problem_r.i_s] + alpha = float(x_r[problem_r.i_alpha]) + x[problem_full.i_alpha] = alpha + d = problem_r.decode(x_r) + losses = scenario_losses(problem_full.spec, d["value"], d["total_cost"]) + x[problem_full.i_u] = np.maximum(losses - alpha, 0.0) + if problem_full.use_pos_bin: + x[problem_full.i_y] = x_r[problem_r.i_y] + if problem_full.use_trade_bin: + x[problem_full.i_t] = x_r[problem_r.i_t] + return x + + +def solve_by_row_generation(spec: ProblemSpec, solve: SolveFn, initial_multiple: float = 2.0, max_iter: int = 30, + tol: float = 1e-7, deadline: float | None = None + ) -> tuple[SolverResult, MeanCVaRProblem, RowGenerationInfo]: + """Solve the scenario MILP of ``spec`` by tail-row generation. + + ``solve`` maps a :class:`MILPModel` to a :class:`SolverResult`. Returns the result in the + *full* problem's layout (so :meth:`MeanCVaRProblem.decode` of the returned problem works), the + full problem and the iteration log. + + ``deadline`` is a :func:`time.monotonic` timestamp: the *request's* time budget. Each pass is a + MILP of its own, so without it a limit of ``T`` seconds would allow ``T`` per pass instead of + ``T`` in total. Once it passes, the current (feasible, not proven optimal) solution is returned. + """ + if spec.is_gaussian: + raise ValueError("row generation applies to scenario problems") + t0 = time.monotonic() + info = RowGenerationInfo() + S = spec.n_scenarios + full = MeanCVaRProblem(spec) + losses0 = scenario_losses(spec, reference_exposure(spec)) + k0 = int(min(S, max(np.ceil(initial_multiple * (1.0 - spec.confidence) * S), 2))) + work = np.sort(np.argsort(losses0)[-k0:]) + scale = max(abs(spec.cvar_limit or 0.0), spec.wealth * 1e-3, 1.0) + result: SolverResult | None = None + for it in range(1, max_iter + 1): + problem_r = MeanCVaRProblem(restricted_spec(spec, work)) + t1 = time.monotonic() + result = solve(problem_r.build()) + info.solve_times.append(time.monotonic() - t1) + info.working_set_sizes.append(len(work)) + info.iterations = it + if not result.ok: + log.warning("row generation: solver returned %s at iteration %d", result.status, it) + info.total_time = time.monotonic() - t0 + return result, full, info + d = problem_r.decode(result.x) + alpha = float(result.x[problem_r.i_alpha]) + losses = scenario_losses(spec, d["value"], d["total_cost"]) + violated = np.setdiff1d(np.where(losses > alpha + tol * scale)[0], work) + info.violated.append(len(violated)) + log.info("row generation: iteration %d, working set %d of %d, %d violated", it, len(work), S, len(violated)) + if len(violated) > 0 and deadline is not None and time.monotonic() >= deadline: + log.warning("row generation: time budget spent after %d iterations (%d rows still violated)", + it, len(violated)) + x_full = expand_solution(problem_r, result.x, full) + info.total_time = time.monotonic() - t0 + return replace(result, x=x_full, status="FeasibleFound", + message=(result.message + f" (row generation: time limit after {it} iterations, " + f"{len(work)} of {S} rows)").strip()), full, info + if len(violated) == 0: + info.converged = True + x_full = expand_solution(problem_r, result.x, full) + result = replace(result, x=x_full, message=(result.message + f" (row generation: {it} iterations, " + f"{len(work)} of {S} rows)").strip()) + info.total_time = time.monotonic() - t0 + return result, full, info + work = np.union1d(work, violated) + log.warning("row generation did not converge in %d iterations", max_iter) + x_full = expand_solution(problem_r, result.x, full) + info.total_time = time.monotonic() - t0 + return replace(result, x=x_full, status="FeasibleFound", + message=(result.message + " (row generation: iteration limit)").strip()), full, info diff --git a/quantcore/optimization/service.py b/quantcore/optimization/service.py new file mode 100644 index 0000000..35395a1 --- /dev/null +++ b/quantcore/optimization/service.py @@ -0,0 +1,763 @@ +"""``OptimizationService`` -- from a request to an integer-share portfolio proposal. + +Pipeline (deterministic given the seed; the result carries the scenario hash and a +``reproducible`` flag): + +1. candidate stocks (a list or the largest ``max_assets`` of the universe by market cap); +2. expected returns: Black-Litterman on 10 years of weekly CHF returns with market-cap + weights, the broad-market average as the equilibrium return, and Idzorek-confidence views + (or plain historical means / zero drift) -- *arithmetic* annual expected returns; +3. horizon scenarios from the chosen stock-price model (GBM or empirical innovations) with + the chosen covariance estimator: scrambled Sobol' draws, moment matching (exact mean and + covariance), optionally an importance-sampling tilt along the loss direction of a first-pass + solution (``importance_sampling``), or -- ``scenario_model='gaussian_exact'`` -- no scenarios + at all but the exact Gaussian (mean-variance-equivalent) problem by outer approximation; +4. the mean-CVaR MILP (:mod:`quantcore.optimization.problem`) in "new" or "rebalancing" mode, + for large scenario sets by tail-row generation (:mod:`quantcore.optimization.rowgen`); +5. solved on cuOpt (HiGHS fallback), decoded into shares, trades, cash, costs, risk figures. +""" + +from __future__ import annotations + +import json +import logging +import time +from collections import OrderedDict +from dataclasses import asdict, dataclass, field + +import numpy as np +import pandas as pd +from scipy.stats import norm + +from quantcore.config import Settings +from quantcore.data.market_data import MarketDataService +from quantcore.measurement.returns import horizon_periods, periods_per_year +from quantcore.measurement.risk import scenario_cvar +from quantcore.models.black_litterman import ( + BlackLittermanModel, + BlackLittermanResult, + View, + view_from_sentiment, +) +from quantcore.models.covariance import CovarianceModel +from quantcore.models.sampling import next_power_of_two, scenario_hash +from quantcore.models.simulation import ScenarioSet, TiltSpec, make_simulator +from quantcore.optimization.gaussian_exact import solve_gaussian_exact +from quantcore.optimization.problem import ( + MeanCVaRProblem, + OptimizationConstraints, + ProblemSpec, + TransactionCosts, + cvar_multiplier, +) +from quantcore.optimization.rowgen import solve_by_row_generation +from quantcore.optimization.solvers import HighsSolver, SolverResult, solve_with_fallback + +log = logging.getLogger(__name__) + +SCENARIO_MODELS = ("gbm", "empirical", "gaussian_exact") +IMPORTANCE_SAMPLING_MODES = ("auto", "market", "adaptive", "off") +ROW_GENERATION_MIN_SCENARIOS = 8192 # 'auto': row generation from this size on +IMPORTANCE_SAMPLING_MIN_TAIL = 128 # 'auto': tilt when fewer than this many scenarios would be in the tail +STAGE1_SCENARIOS = 2048 # 'adaptive': first-pass solve that provides the tilt direction +TILT_ALIGNMENT_WARN = 0.3 # warn below this correlation between the solution's loss and the tilt's +SCENARIO_CACHE_SIZE = 8 # scenario sets kept per service instance +# The sample-average approximation searches an N-dimensional space, so the scenario count has to +# grow with the number of candidates. Measured on the real universe (GBM model against the exact +# optimum): the *reported figures* are stable at any size (the standard deviation of the expected +# P&L is below 0.1 % from 2 048 scenarios on), but the *composition* keeps moving between +# near-equivalent portfolios -- at 2 048 scenarios two seeds differ by 3.5 % of wealth for 30 +# candidates and 12.5 % for 100. About 64 scenarios per candidate brings that near 5 %, which is +# too expensive as a default (N = 300 would mean 32 768 scenarios and minutes per solve), so the +# default is 16 per candidate and the result says when the ratio is low. +SCENARIOS_PER_ASSET = 16 +MIN_DEFAULT_SCENARIOS = 4096 +MAX_DEFAULT_SCENARIOS = 16384 # beyond this prefer 'gaussian_exact' (no scenarios) or set it explicitly +SCENARIOS_PER_ASSET_WARN = 8 # warn below this ratio + + +def _view_fingerprint(views) -> list: + """Views as plain sorted data (for the adaptive-tilt part of the scenario key).""" + out = [] + for v in views: + d = v if isinstance(v, dict) else {"assets": dict(v.assets), "q": v.q, "confidence": v.confidence} + out.append({k: d[k] for k in sorted(d) if k not in ("label", "source")}) + return out + + +@dataclass +class OptimizationRequest: + mode: str = "new" # 'new' (all from cash) | 'rebalance' (existing holdings + cash) + cash: float = 1_000_000.0 + holdings: dict[str, int] = field(default_factory=dict) + symbols: list[str] | None = None # candidate stocks; default: largest `max_assets` by market cap + max_assets: int = 100 + horizon: str | int = "week" + freq: str = "daily" + confidence: float = 0.95 + cvar_limit: float | None = None # money over the horizon + cvar_limit_fraction: float | None = None # alternative: fraction of total wealth + objective: str = "max_return" # 'max_return' | 'min_cvar' | 'utility' + risk_aversion: float = 1.0 + target_return: float | None = None # fraction of wealth over the horizon (objective 'min_cvar') + scenario_model: str = "empirical" # 'gbm' | 'empirical' | 'gaussian_exact' + n_scenarios: int | None = None # None: 16 per candidate, within [4096, 16384] (see scenario_count) + seed: int = 42 + sampling: str = "sobol" # 'sobol' | 'mc' | 'antithetic' | 'lhs' (gbm); 'sobol' | 'mc' | 'balanced' (empirical) + moment_match: bool = True # exact scenario mean and covariance + # 'market': tilt along the loss direction of the cap-weighted candidate portfolio, in blocks at + # z_0.95 and z_0.99 -- independent of the risk limit, the constraints and the confidence, so a + # family of runs shares one scenario set; 'adaptive': tilt towards the tail of a first-pass + # solution (slightly sharper, but the set then depends on those parameters); 'auto': 'market' + # when the tail would otherwise be thin; 'off': no tilt. + importance_sampling: str = "auto" + is_mixture: float = 0.25 # share of scenarios per tilted block ('adaptive': of the single block) + drift_convention: str = "arithmetic" # 'arithmetic' (Ito-corrected log-drift) | 'log' + # 'model': the CVaR is measured on the scenarios as they are, drift included -- over a long + # horizon a large expected return then shifts the whole tail into profit and the risk limit + # stops binding. 'zero': the risk rows use the centred (driftless) scenarios while the + # objective keeps the expected return, which is the usual market-risk convention. + risk_drift: str = "model" # 'model' | 'zero' + row_generation: str = "auto" # 'auto' | 'on' | 'off': solve on the tail rows only + covariance: dict = field(default_factory=lambda: {"method": "ewm", "lam": 0.99, "shrinkage": "auto"}) + expected_returns: str = "black_litterman" # 'black_litterman' | 'historical' | 'zero' + views: list[dict] = field(default_factory=list) + bl_tau: float = 0.05 + bl_years: int = 10 + bl_omega_method: str = "idzorek" # 'idzorek' | 'he_litterman' + constraints: OptimizationConstraints = field(default_factory=OptimizationConstraints) + costs: TransactionCosts = field(default_factory=TransactionCosts) + solver: str = "auto" # 'auto' | 'cuopt' | 'highs' + time_limit: float | None = None + innovation_history: int | None = 2500 # observations used for the empirical innovations + innovation_standardize: str | None = "diagonal" # 'diagonal' | 'full' | None (see simulation module) + + def __post_init__(self) -> None: + if self.mode not in ("new", "rebalance"): + raise ValueError("mode must be 'new' or 'rebalance'") + if self.mode == "new" and self.holdings: + raise ValueError("mode 'new' starts from cash only - pass no holdings") + if self.scenario_model not in SCENARIO_MODELS: + raise ValueError(f"scenario_model must be one of {SCENARIO_MODELS}") + if self.importance_sampling not in IMPORTANCE_SAMPLING_MODES: + raise ValueError(f"importance_sampling must be one of {IMPORTANCE_SAMPLING_MODES}") + if self.row_generation not in ("auto", "on", "off"): + raise ValueError("row_generation must be 'auto', 'on' or 'off'") + if self.risk_drift not in ("model", "zero"): + raise ValueError("risk_drift must be 'model' or 'zero'") + if isinstance(self.constraints, dict): + self.constraints = OptimizationConstraints(**self.constraints) + if isinstance(self.costs, dict): + self.costs = TransactionCosts(**self.costs) + if self.cvar_limit is None and self.cvar_limit_fraction is None and self.objective == "max_return": + self.cvar_limit_fraction = 0.05 + log.info("no CVaR limit given - using 5%% of wealth over the horizon") + + @property + def horizon_periods(self) -> int: + return horizon_periods(self.horizon, self.freq) + + def scenario_count(self, n_assets: int) -> int: + """Scenarios to draw for ``n_assets`` candidates (the request's value, or the default rule).""" + if self.n_scenarios is not None: + return int(self.n_scenarios) + target = SCENARIOS_PER_ASSET * max(int(n_assets), 1) + return int(min(max(next_power_of_two(target), MIN_DEFAULT_SCENARIOS), MAX_DEFAULT_SCENARIOS)) + + def uses_row_generation(self, n_scenarios: int) -> bool: + if self.row_generation == "on": + return True + return self.row_generation == "auto" and n_scenarios >= ROW_GENERATION_MIN_SCENARIOS + + def tilt_mode(self, n_scenarios: int) -> str: + """``'market'``, ``'adaptive'`` or ``'off'`` for this request. + + ``'auto'`` is ``'market'``: the tilt blocks cover both usual confidence levels, it costs + nothing in accuracy at 95 % and roughly halves the tail noise at 99 % -- and, unlike a + rule that switches on the tail count, it keeps the scenario set independent of + ``confidence``, so runs at different risk levels stay comparable. + """ + if self.importance_sampling in ("market", "adaptive", "off"): + return self.importance_sampling + return "market" + + +@dataclass +class OptimizationResult: + status: str + solver: str + mode: str + as_of: str + wealth: float + cash_start: float + cash_end: float + invested: float + total_cost: float + turnover: float + expected_pnl: float + expected_return: float # fraction of wealth over the horizon + var: float # money + cvar: float # money + cvar_limit: float | None + confidence: float + horizon: str | int + horizon_periods: int + n_positions: int + n_candidates: int + positions: list[dict] + sector_exposure: dict + model: dict + solver_info: dict + timings: dict + reproducible: bool = False # optimality proven -> same inputs give the same figures + reproducibility: str = "none" # 'exact' (HiGHS: identical shares) | 'within_gap' (cuOpt: same value, ties may differ) + scenario_hash: str | None = None + scenario_key: dict | None = None # the inputs the scenario set depends on (see OptimizationService.scenario_key) + before: dict | None = None # rebalancing: the starting portfolio measured on the same scenarios + warnings: list[str] = field(default_factory=list) + + def as_dict(self) -> dict: + return asdict(self) + + def positions_frame(self) -> pd.DataFrame: + return pd.DataFrame(self.positions).set_index("symbol") if self.positions else pd.DataFrame() + + +class OptimizationService: + def __init__(self, settings: Settings, market_data: MarketDataService | None = None): + self.settings = settings + self.md = market_data or MarketDataService(settings) + self._scenario_cache: OrderedDict[str, ScenarioSet] = OrderedDict() + + # -- building blocks -------------------------------------------------------------------------- + def candidate_symbols(self, req: OptimizationRequest) -> list[str]: + if req.symbols: + symbols = list(dict.fromkeys(req.symbols)) + else: + caps = self.md.market_caps().sort_values(ascending=False) + symbols = caps.index[: req.max_assets].tolist() + for s in req.holdings: # existing holdings must be in the candidate set (they can be sold) + if s not in symbols: + symbols.append(s) + for v in req.views: # so must the assets a view refers to + assets = [v["symbol"]] if isinstance(v, dict) and "symbol" in v else ( + list(v["assets"]) if isinstance(v, dict) else list(v.assets)) + for s in assets: + if s not in symbols: + symbols.append(s) + unknown = [s for s in symbols if s not in self.md.universe.index] + if unknown: + raise KeyError(f"symbols not in the universe: {unknown}") + return symbols + + def expected_returns(self, req: OptimizationRequest, symbols: list[str]) -> tuple[pd.Series, dict, BlackLittermanResult | None]: + """Annual (arithmetic) expected returns per symbol and a description of the model.""" + last = self.md.last_date() + start = (last - pd.DateOffset(years=req.bl_years)).date().isoformat() + if req.expected_returns == "zero": + return pd.Series(0.0, index=symbols), {"type": "zero"}, None + weekly = self.md.returns(symbols, start=start, currency="base", freq="weekly").dropna() + if req.expected_returns == "historical": + mu = weekly.mean() * 52 + 0.5 * weekly.var() * 52 # arithmetic: log mean plus sigma^2/2 + return mu, {"type": "historical", "years": req.bl_years, "n_weeks": len(weekly)}, None + if req.expected_returns != "black_litterman": + raise ValueError("expected_returns must be 'black_litterman', 'historical' or 'zero'") + cov_w = weekly.cov() + market_w = self.md.market_weights(symbols) + index_r = self.md.cap_weighted_index(start=start, freq="weekly") + market_return = float(index_r.mean() + 0.5 * index_r.var()) # arithmetic weekly market return + bl = BlackLittermanModel(cov_w, market_w, market_return, risk_free=self.settings.risk_free_rate / 52, + tau=req.bl_tau, periods_per_year=52) + views = self._views(req, bl) + result = bl.posterior(views, omega_method=req.bl_omega_method) + desc = { + "type": "black_litterman", "years": req.bl_years, "n_weeks": len(weekly), + "market_return_ann": result.market_return_ann, "market_volatility_ann": result.market_volatility_ann, + "delta": result.delta, "tau": result.tau, "views": result.views, "convention": "arithmetic", + "omega_method": req.bl_omega_method, + } + return result.posterior_ann, desc, result + + @staticmethod + def _views(req: OptimizationRequest, bl: BlackLittermanModel) -> list[View]: + views = [] + for v in req.views: + if isinstance(v, View): + views.append(v) + elif "sentiment" in v: + views.append(view_from_sentiment(bl, v["symbol"], float(v["sentiment"]), float(v.get("confidence", 0.5)), + float(v.get("strength", 0.25)), v.get("label", ""), v.get("source", ""))) + else: + views.append(View(assets=v["assets"], q=float(v["q"]), confidence=float(v.get("confidence", 0.5)), + label=v.get("label", ""), source=v.get("source", ""))) + return views + + def _returns_and_cov_model(self, req: OptimizationRequest, symbols: list[str]) -> tuple[pd.DataFrame, CovarianceModel]: + r = self.md.returns(symbols, currency="base", freq=req.freq) + return r.dropna(), CovarianceModel.from_dict(req.covariance) # complete rows only for the estimators + + def market_exposure(self, symbols: list[str]) -> np.ndarray: + """Money exposure of the cap-weighted portfolio of the candidates (scale is irrelevant: only + its *direction* is used). This is the reference portfolio of the ``'market'`` tilt -- it + depends on the universe and the market caps, never on the request's risk parameters.""" + w = self.md.market_weights(symbols).reindex(symbols).fillna(0.0).values + if w.sum() <= 0: + w = np.ones(len(symbols)) + return w / w.sum() + + def scenario_key(self, req: OptimizationRequest, symbols: list[str], tilt_mode: str | None = None, + n_scenarios: int | None = None) -> dict: + """Exactly the inputs the scenario *shocks* depend on. + + Two requests with the same key share their scenario set: the risk limit, the confidence + level, the constraints, the transaction costs, the objective, the cash and the holdings do + **not** appear here, so a whole family of runs is compared on identical scenarios. Neither + does the expected-return model: the drift is applied afterwards + (:meth:`ScenarioSet.with_drift`), so changing views shifts the scenarios without touching + their random part. The one exception is ``importance_sampling="adaptive"``, whose tilt + direction comes from a first-pass solution; the key then records the parameters of that + solve and runs with different risk parameters no longer share a set. + """ + S = int(n_scenarios or req.scenario_count(len(symbols))) + mode = tilt_mode if tilt_mode is not None else req.tilt_mode(S) + key = { + "symbols": list(symbols), "freq": req.freq, "horizon_periods": req.horizon_periods, + "model": req.scenario_model, "n_scenarios": S, "seed": req.seed, + "sampling": req.sampling, "moment_match": bool(req.moment_match), + "covariance": CovarianceModel.from_dict(req.covariance).describe(), + "importance_sampling": mode, "as_of": str(self.md.last_date().date()), + } + if mode != "off": + key["is_mixture"] = req.is_mixture + if mode == "adaptive": # the tilt depends on the optimisation parameters of the first pass + key["adaptive_tilt"] = {"confidence": req.confidence, "cvar_limit": req.cvar_limit, + "cvar_limit_fraction": req.cvar_limit_fraction, "objective": req.objective, + "cash": req.cash, "holdings": dict(sorted(req.holdings.items())), + "constraints": asdict(req.constraints), "costs": asdict(req.costs), + "expected_returns": req.expected_returns, "views": _view_fingerprint(req.views)} + if req.scenario_model in ("empirical", "innovations", "fhs"): + key["innovations"] = {"history": req.innovation_history, "standardize": req.innovation_standardize} + key["id"] = scenario_hash(np.frombuffer(json.dumps(key, sort_keys=True, default=str).encode(), dtype=np.uint8)) + return key + + def _generate_scenarios(self, req: OptimizationRequest, symbols: list[str], tilt: TiltSpec | None, + n_scenarios: int) -> ScenarioSet: + """A drift-free scenario set (the expensive part); the drift is applied by the caller.""" + r, cov_model = self._returns_and_cov_model(req, symbols) + kwargs = {} + if req.scenario_model in ("empirical", "innovations", "fhs"): + kwargs = {"max_history": req.innovation_history, "root": "symmetric" if len(symbols) <= 250 else "cholesky", + "standardize": req.innovation_standardize} + sim = make_simulator(req.scenario_model, cov_model, **kwargs) + scen = sim.simulate(r, n_scenarios=n_scenarios, horizon_periods=req.horizon_periods, mu=None, seed=req.seed, + freq=req.freq, sampling=req.sampling, moment_match=req.moment_match, tilt=tilt, + drift_convention=req.drift_convention) + scen.covariance_model["conditioning"] = cov_model.conditioning(r) + return scen + + def scenarios(self, req: OptimizationRequest, symbols: list[str], mu_ann: pd.Series, tilt: TiltSpec | None = None, + n_scenarios: int | None = None, tilt_mode: str | None = None) -> ScenarioSet: + """The scenario set for this request, with the expected returns applied as the drift. + + The shocks are cached per :meth:`scenario_key`, so a series of runs that varies risk limits, + constraints, costs or views reuses one set (and pays the innovation filter only once). + """ + if req.scenario_model == "gaussian_exact": + raise ValueError("scenario_model 'gaussian_exact' does not generate scenarios") + S = int(n_scenarios or req.scenario_count(len(symbols))) + mode = tilt_mode if tilt_mode is not None else ("adaptive" if tilt is not None else req.tilt_mode(S)) + if mode == "market" and tilt is None: + tilt = TiltSpec(exposure=self.market_exposure(symbols), mixture=(req.is_mixture,) * 2, label="market") + key = self.scenario_key(req, symbols, mode, S) + base = self._scenario_cache.get(key["id"]) + hit = base is not None + if hit: + log.info("scenario cache hit (%s, %d scenarios)", key["id"], base.n_scenarios) + else: + base = self._generate_scenarios(req, symbols, tilt, S) + self._cache_scenarios(key["id"], base) + scen = base.with_drift(mu_ann / periods_per_year(req.freq), req.drift_convention) + scen.extra["key"] = key + scen.extra["cached"] = hit + return scen + + @staticmethod + def _sampling_warnings(n_assets: int, n_scenarios: int, cov_desc: dict) -> list[str]: + """Scenario count and covariance conditioning against the size of the candidate set.""" + out = [] + if n_scenarios < SCENARIOS_PER_ASSET_WARN * n_assets: + out.append( + f"{n_scenarios} scenarios for {n_assets} candidates ({n_scenarios / n_assets:.0f} per asset): " + f"the expected P&L and CVaR are still reliable, but the composition will move between " + f"near-equivalent portfolios when the seed changes - raise n_scenarios (about 64 per " + f"candidate), reduce max_assets, or use scenario_model='gaussian_exact' for an exact answer") + cond = cov_desc.get("conditioning") or {} + if cond.get("rank_deficient"): + out.append( + f"the covariance estimate is singular ({cond['effective_observations']} effective " + f"observations for {cond['n_assets']} assets, shrinkage {cond['shrinkage_applied']}): some " + f"portfolios appear riskless to the optimiser - use shrinkage='auto' or a longer window") + elif cond.get("under_determined") and cond.get("min_eigenvalue_ratio", 1.0) < 1e-3: + out.append( + f"the covariance estimate is ill-conditioned (smallest eigenvalue " + f"{cond['min_eigenvalue_ratio']:.1e} of the average variance, {cond['effective_observations']} " + f"effective observations for {cond['n_assets']} assets) - consider shrinkage='auto'") + return out + + def _cache_scenarios(self, key_id: str, scen: ScenarioSet) -> None: + self._scenario_cache[key_id] = scen + self._scenario_cache.move_to_end(key_id) + while len(self._scenario_cache) > SCENARIO_CACHE_SIZE: + self._scenario_cache.popitem(last=False) + + def clear_scenario_cache(self) -> None: + self._scenario_cache.clear() + + def _spec_common(self, req: OptimizationRequest, symbols: list[str]) -> dict: + prices = self.md.latest_prices(symbols, currency="base").reindex(symbols) + if prices.isna().any(): + raise ValueError(f"no current price for {prices.index[prices.isna()].tolist()}") + holdings = np.array([float(req.holdings.get(s, 0)) for s in symbols]) + wealth = req.cash + float(holdings @ prices.values) + cvar_limit = req.cvar_limit + if cvar_limit is None and req.cvar_limit_fraction is not None: + cvar_limit = req.cvar_limit_fraction * wealth + target_pnl = None if req.target_return is None else req.target_return * wealth + sectors = self.md.sectors(symbols).fillna("Unknown").tolist() + return dict(symbols=symbols, prices=prices.values, sectors=sectors, cash=req.cash, holdings=holdings, + confidence=req.confidence, cvar_limit=cvar_limit, objective=req.objective, + risk_aversion=req.risk_aversion, target_pnl=target_pnl, constraints=req.constraints, costs=req.costs) + + def build_spec(self, req: OptimizationRequest, symbols: list[str], scen: ScenarioSet) -> ProblemSpec: + # risk_drift='zero': the CVaR rows see the shocks without the drift; the objective still + # uses the model's expected returns, so the forecast can never relax the risk limit + scenarios = np.expm1(scen.shocks) if req.risk_drift == "zero" else scen.simple_returns() + return ProblemSpec(scenarios=scenarios, probabilities=scen.probabilities, + expected_returns=scen.expected_simple, **self._spec_common(req, symbols)) + + def build_gaussian_spec(self, req: OptimizationRequest, symbols: list[str], mu_ann: pd.Series) -> tuple[ProblemSpec, dict]: + """Gaussian mode: horizon simple returns ``N(mu * h / ppy, h * Sigma_T)``.""" + ppy = periods_per_year(req.freq) + h = req.horizon_periods + r, cov_model = self._returns_and_cov_model(req, symbols) + cov = cov_model.estimate(r).values * h + mean = mu_ann.reindex(symbols).fillna(0.0).values / ppy * h + cov_desc = cov_model.describe() + cov_desc["conditioning"] = cov_model.conditioning(r) + desc = {"model": "gaussian_exact", "n_scenarios": 0, "n_assets": len(symbols), "horizon_periods": h, + "freq": req.freq, "as_of": str(r.index[-1].date()), "covariance_model": cov_desc, + "shrinkage_applied": cov_model.intensity(r), "drift_convention": "arithmetic"} + risk_mean = np.zeros_like(mean) if req.risk_drift == "zero" else mean + return ProblemSpec(scenarios=None, gaussian_mean=risk_mean, gaussian_cov=cov, expected_returns=mean, + **self._spec_common(req, symbols)), desc + + def _solve_fn(self, req: OptimizationRequest, prefer_local: bool = False, deadline: float | None = None): + """A solve function that spends at most the request's remaining time budget. + + ``time_limit`` is the budget of the *whole* request. Row generation and the Gaussian outer + approximation call the solver repeatedly, so each call gets what is left of it (at least a + second, since a solver cannot be given zero). + """ + def limit() -> float | None: + if deadline is None: + return req.time_limit + return max(1.0, deadline - time.monotonic()) + + # `prefer_local` covers the models cuOpt cannot be trusted with; see _solve_scenarios for + # the CVaR-objective case and `optimize` for the Gaussian cut MILPs. An explicit + # solver='cuopt' is still honoured -- the caller asked for it and gets cuOpt's own answer. + if prefer_local and req.solver in ("auto", "highs"): + return lambda model: HighsSolver(self.settings).solve(model, limit()) + return lambda model: solve_with_fallback(model, self.settings, solver=req.solver, time_limit=limit()) + + def _deadline(self, req: OptimizationRequest) -> float | None: + return None if req.time_limit is None else time.monotonic() + float(req.time_limit) + + def _solve_scenarios(self, req: OptimizationRequest, spec: ProblemSpec) -> tuple[SolverResult, MeanCVaRProblem, dict]: + deadline = self._deadline(req) + info: dict = {} + # cuOpt 26.08 aborts when the CVaR sits in the *objective* and the scenarios carry + # probabilities: the objective coefficients are then p_s/(1-beta), which importance sampling + # spreads over about six orders of magnitude (measured 1.1e-06 .. 1.0, ratio 8.8e5), and the + # LP relaxation aborts within five seconds -- with the integers relaxed as well, so it is the + # LP and not the branch and bound. The same model with uniform weights (ratio 41) solves in + # 3.8 s, five times out of five, matching HiGHS to 7e-5. With CVaR as a *constraint* the + # weights sit in a matrix row instead, where cuOpt's scaling copes, and it solves either way. + weighted_cvar_objective = req.objective == "min_cvar" and spec.probabilities is not None + if weighted_cvar_objective and req.solver == "auto": + info["solver_routing"] = "highs: CVaR objective with probability-weighted scenarios" + solve = self._solve_fn(req, prefer_local=weighted_cvar_objective, deadline=deadline) + if req.uses_row_generation(spec.n_scenarios): + sol, problem, rg = solve_by_row_generation(spec, solve, deadline=deadline) + return sol, problem, {**info, "row_generation": rg.as_dict()} + problem = MeanCVaRProblem(spec) + return solve(problem.build()), problem, info + + # -- main entry point ------------------------------------------------------------------------- + def optimize(self, req: OptimizationRequest) -> OptimizationResult: + timings: dict = {} + t0 = time.monotonic() + symbols = self.candidate_symbols(req) + mu_ann, er_desc, _bl = self.expected_returns(req, symbols) + timings["expected_returns_s"] = round(time.monotonic() - t0, 3) + extra_info: dict = {} + warnings: list[str] = [] + + if req.scenario_model == "gaussian_exact": + t1 = time.monotonic() + spec, scen_desc = self.build_gaussian_spec(req, symbols, mu_ann) + warnings.extend(self._sampling_warnings(len(symbols), 10 ** 9, scen_desc["covariance_model"])) + timings["model_s"] = round(time.monotonic() - t1, 3) + t3 = time.monotonic() + # the cut MILPs are tiny (N integers, a few dozen rows): HiGHS locally unless cuOpt is requested + sol, problem, oa = solve_gaussian_exact(spec, self._solve_fn(req, prefer_local=True, + deadline=self._deadline(req))) + extra_info["outer_approximation"] = oa.as_dict() + timings["solve_s"] = round(time.monotonic() - t3, 3) + exp_simple = spec.gaussian_mean + scen_hash = None + else: + t1 = time.monotonic() + S = req.scenario_count(len(symbols)) + mode = req.tilt_mode(S) + tilt = None + if mode == "adaptive": + # stage 1: an untilted (moment-matched) set gives the loss direction for the tilt + scen1 = self.scenarios(req, symbols, mu_ann, n_scenarios=min(S, STAGE1_SCENARIOS), tilt_mode="off") + spec1 = self.build_spec(req, symbols, scen1) + sol1, problem1, _ = self._solve_scenarios(req, spec1) + if sol1.ok: + exposure = problem1.decode(sol1.x)["value"] + if exposure @ spec1.prices > 0: + tilt = TiltSpec.at_confidence(exposure, req.confidence, req.is_mixture * 2) + extra_info["importance_sampling"] = {"stage1_scenarios": scen1.n_scenarios, + "stage1_status": sol1.status} + if tilt is None: + mode = "off" + warnings.append("importance sampling skipped: no first-pass portfolio") + scen = self.scenarios(req, symbols, mu_ann, tilt=tilt, tilt_mode=mode) + warnings.extend(self._sampling_warnings(len(symbols), scen.n_scenarios, scen.covariance_model)) + timings["scenarios_s"] = round(time.monotonic() - t1, 3) + t2 = time.monotonic() + spec = self.build_spec(req, symbols, scen) + timings["build_s"] = round(time.monotonic() - t2, 3) + t3 = time.monotonic() + sol, problem, rg = self._solve_scenarios(req, spec) + extra_info.update(rg) + timings["solve_s"] = round(time.monotonic() - t3, 3) + scen_desc = scen.describe() + exp_simple = scen.expected_simple + scen_hash = scen.hash + extra_info["scenario_cache"] = {"key": scen.extra["key"]["id"], "reused": bool(scen.extra.get("cached"))} + timings["total_s"] = round(time.monotonic() - t0, 3) + key = None if req.scenario_model == "gaussian_exact" else scen.extra["key"] + return self._result(req, symbols, spec, problem, sol, scen_desc, scen_hash, exp_simple, er_desc, mu_ann, + timings, extra_info, warnings, key) + + def _result(self, req, symbols, spec, problem, sol, scen_desc, scen_hash, exp_simple, er_desc, mu_ann, timings, + extra_info, warnings, scenario_key=None) -> OptimizationResult: + h = req.horizon_periods + warnings = list(warnings) + if sol.status == "FeasibleFound": + # FeasibleFound can come from the solver's own limit *or* from an iteration cap in the + # outer approximation / row generation, which is not a time limit at all + oa, rg = extra_info.get("outer_approximation") or {}, extra_info.get("row_generation") or {} + if oa and not oa.get("converged"): + why = f"the Gaussian outer approximation stopped after {oa.get('iterations')} cuts" + elif rg and not rg.get("converged"): + why = f"the tail-row generation stopped after {rg.get('iterations')} passes" + else: + why = "the solver stopped at its limit" + warnings.append(f"{why}; solution feasible but not proven optimal (gap {sol.mip_gap})") + if "fallback" in sol.message: + warnings.append(sol.message) + if extra_info.get("solver_routing"): + warnings.append( + "solved on HiGHS (CPU) although cuOpt is reachable: cuOpt 26.08 aborts when the CVaR " + "is the objective *and* the scenarios are probability-weighted, because importance " + "sampling spreads the objective coefficients over six orders of magnitude. Set " + "importance_sampling='off' to use the GPU with this objective, or keep the weights " + "and the CPU - HiGHS solves it in a couple of seconds either way") + milp_desc = {"n_vars": problem.n_vars, "n_scenarios": spec.n_scenarios, "gaussian": problem.gaussian, + "weighted_scenarios": spec.probabilities is not None} + base_kwargs = dict( + status=sol.status, solver=sol.solver, mode=req.mode, as_of=scen_desc.get("as_of", ""), wealth=spec.wealth, + cash_start=req.cash, cvar_limit=spec.cvar_limit, confidence=req.confidence, horizon=req.horizon, + horizon_periods=h, n_candidates=len(symbols), + model={"expected_returns": er_desc, "scenarios": scen_desc, "milp": milp_desc, + "constraints": asdict(req.constraints), "costs": asdict(req.costs), "objective": req.objective, + "sampling": {"sampling": req.sampling, "moment_match": req.moment_match, + "importance_sampling": req.importance_sampling, "row_generation": req.row_generation, + "drift_convention": req.drift_convention, "risk_drift": req.risk_drift}}, + solver_info={"status": sol.status, "solver": sol.solver, "solve_time_s": sol.solve_time, + "mip_gap": sol.mip_gap, "objective": sol.objective, "message": sol.message, **sol.raw, + **extra_info}, + timings=timings, reproducible=sol.reproducible, scenario_hash=scen_hash, scenario_key=scenario_key, + before=(self.portfolio_figures(spec, spec.holdings, req.cash) if spec.rebalancing else None), + warnings=warnings, + # HiGHS' branch-and-bound is deterministic: identical inputs give identical shares. cuOpt (26.08) + # ignores gap settings over REST and its GPU heuristics stop within ~1e-4 of the optimum: identical + # objective, CVaR and expected P&L up to that gap, but near-tied share counts can differ. + reproducibility=("none" if not sol.reproducible else "exact" if sol.solver == "highs" else "within_gap"), + ) + if not sol.ok: + warnings.append(f"no solution: {sol.status} {sol.message}".strip()) + return OptimizationResult(cash_end=req.cash, invested=0.0, total_cost=0.0, turnover=0.0, expected_pnl=0.0, + expected_return=0.0, var=float("nan"), cvar=float("nan"), n_positions=0, + positions=[], sector_exposure={}, **base_kwargs) + d = problem.decode(sol.x) + if not problem.gaussian and spec.probabilities is not None and d["invested"] > 0: + alignment = self.tilt_alignment(spec, d["value"], self.market_exposure(symbols)) + base_kwargs["solver_info"]["tilt_alignment"] = round(alignment, 3) + if np.isfinite(alignment) and alignment < TILT_ALIGNMENT_WARN: + warnings.append( + f"the solution's loss is barely correlated with the importance-sampling reference " + f"(alignment {alignment:.2f}); the tilt did not help here - consider " + f"importance_sampling='adaptive' or 'off'") + if spec.cvar_limit is not None and d["cvar"] > spec.cvar_limit * (1 + 1e-6): + rg = extra_info.get("row_generation") or {} + oa = extra_info.get("outer_approximation") or {} + over = d["cvar"] / spec.cvar_limit - 1 + # enough precision for a small overshoot: "by 0.0%" reads as no breach at all + amount = f"{d['cvar'] - spec.cvar_limit:,.0f}" if over >= 5e-4 else f"{d['cvar'] - spec.cvar_limit:,.2f}" + # name the mechanism that left the gap, and the remedy that applies to *it* + if rg and not rg.get("converged"): + cause = (f": the tail-row generation stopped after {rg.get('iterations')} passes " + f"({rg.get('rows_final')} of {spec.n_scenarios} rows), so scenarios outside the " + f"working set are not covered - raise time_limit or set row_generation='off'") + elif oa and not oa.get("converged"): + cause = (f": the Gaussian outer approximation stopped after {oa.get('iterations')} cuts " + f"without converging, and it bounds the CVaR from below until it does - raise " + f"time_limit, or use scenario_model='gbm' for an exactly enforced limit") + else: + cause = (": the solver stopped within its MIP gap rather than at the true optimum - raise " + "time_limit, or lower n_scenarios / max_assets") + warnings.append( + f"the proposal BREACHES the CVaR limit by {amount} ({over:.3%}: {d['cvar']:,.0f} against " + f"{spec.cvar_limit:,.0f})" + cause) + # Minimising CVaR puts as little at risk as it is allowed to: cash has a CVaR of zero, so + # without a target return or a floor on the invested fraction the answer is trivially cash, + # and with a small target it is a sliver of the wealth. Both are correct and both look broken. + if sol.ok and req.objective == "min_cvar" and req.constraints.min_invested <= 0: + share = d["invested"] / spec.wealth if spec.wealth else 0.0 + if share < 0.5: + target = "no target return" if not req.target_return else f"a target return of {req.target_return:.3%}" + warnings.append( + f"only {share:.1%} of the wealth is invested: minimising CVaR holds as little risk " + f"as the constraints allow, and cash has a CVaR of zero. With {target} and no " + f"min_invested floor this is the true optimum - raise target_return, or set " + f"constraints.min_invested (e.g. 0.95) to make it deploy the money") + if sol.ok and d["invested"] <= 0: + if req.objective == "min_cvar": + pass # already explained above, in more useful detail + elif req.objective == "utility": + # E[P&L] and CVaR are both money over the horizon, so lambda is dimensionless and + # its natural scale is E[P&L]/CVaR at the optimum -- of the order of 0.02 over a + # week. At lambda = 1 every franc of expected shortfall costs a franc of expected + # return and staying in cash is genuinely optimal, which is easy to mistake for a bug. + warnings.append( + f"nothing was invested: with risk_aversion={req.risk_aversion:g} the penalty " + f"{req.risk_aversion:g} x CVaR exceeds the expected P&L of every feasible " + f"portfolio, so cash maximises the utility. Over {h} periods the trade-off is of " + f"the order of 0.02; lower risk_aversion, or use objective='max_return' with a " + f"cvar_limit") + elif req.objective != "min_cvar": + warnings.append("nothing was invested: the constraints leave no position worth taking") + if spec.cvar_limit is not None and d["invested"] > 0: + if d["cvar"] <= 0: + warnings.append( + f"the {req.confidence:.0%} CVaR of the proposal is a *gain* ({-d['cvar']:,.0f}): over " + f"{h} periods the expected return ({d['expected_pnl'] / spec.wealth:.1%} of wealth) is larger " + f"than the tail of the return distribution, so the risk limit constrains nothing and the " + f"portfolio is driven by the return forecast and the weight caps alone - use " + f"risk_drift='zero', a shorter horizon, or a smaller limit") + elif d["cvar"] < 0.25 * spec.cvar_limit: + warnings.append( + f"the CVaR limit is far from binding ({d['cvar']:,.0f} against a limit of " + f"{spec.cvar_limit:,.0f}): the proposal is set by the weight and sector caps, not by risk") + held_mu = np.array([mu_ann.get(s, 0.0) for i, s in enumerate(symbols) if d["shares"][i] > 0]) + if held_mu.size and np.nanmax(held_mu) > 0.60: + warnings.append( + f"the expected-return model gives up to {np.nanmax(held_mu):.0%} p.a. to a held stock " + f"({er_desc.get('type')}): an optimiser maximising expected return will concentrate in exactly " + f"those names - check the model before reading the proposal") + u = self.md.universe + positions = [] + for i, s in enumerate(symbols): + if d["shares"][i] == 0 and d["trades"][i] == 0: + continue + positions.append({ + "symbol": s, "name": u.at[s, "name"] if s in u.index else s, "sector": spec.sectors[i], + "currency": u.at[s, "listing_currency"] if s in u.index else None, + "price": float(spec.prices[i]), "shares": int(d["shares"][i]), + "shares_before": int(spec.holdings[i]), "trade": int(d["trades"][i]), + "value": float(d["value"][i]), "weight": float(d["weights"][i]), + "trade_value": float(d["trades"][i] * spec.prices[i]), "cost": float(d["cost"][i]), + "expected_return_ann": float(mu_ann.get(s, np.nan)), + "expected_return_horizon": float(exp_simple[i]), + }) + positions.sort(key=lambda p: -p["value"]) + sector_exposure = pd.Series(d["value"]).groupby(pd.Series(spec.sectors)).sum() / spec.wealth + return OptimizationResult( + cash_end=d["cash_end"], invested=d["invested"], total_cost=d["total_cost"], turnover=d["turnover"], + expected_pnl=d["expected_pnl"], expected_return=d["expected_pnl"] / spec.wealth, var=d["var"], + cvar=d["cvar"], n_positions=int((d["shares"] > 0).sum()), positions=positions, + sector_exposure={k: float(v) for k, v in sector_exposure.round(6).items() if v > 0}, **base_kwargs, + ) + + @staticmethod + def tilt_alignment(spec: ProblemSpec, exposure: np.ndarray, reference: np.ndarray) -> float: + """Correlation between the losses of the solution and of the tilt's reference portfolio. + + The importance-sampling tilt enriches the tail *of the reference portfolio*; a solution + whose loss is highly correlated with it gets the full variance reduction (measured: 2.3x + to 5.3x at beta = 0.99 for alignments of 0.9 to 1.0), one that is uncorrelated gets none + and one that is anti-correlated is estimated slightly *less* precisely than without the + tilt. Long-only portfolios under single-stock and sector caps are market-like in practice + (alignment around 0.9), but the figure is reported so that the exception is visible. + """ + w = spec.weights # both arguments are money exposures per asset (any common scale) + a = spec.scenarios @ np.asarray(exposure, dtype=float) + b = spec.scenarios @ np.asarray(reference, dtype=float) + a = a - w @ a + b = b - w @ b + denom = np.sqrt((w @ (a * a)) * (w @ (b * b))) + return float((w @ (a * b)) / denom) if denom > 0 else float("nan") + + @staticmethod + def portfolio_figures(spec: ProblemSpec, shares: np.ndarray, cash: float, total_cost: float = 0.0) -> dict: + """Expected P&L, VaR, CVaR and exposures of a share portfolio under this spec's model. + + The same quantities the solution reports, so that the starting portfolio of a rebalancing + can be compared with the proposal on identical scenarios (or, in Gaussian mode, on the + identical normal model). + """ + shares = np.asarray(shares, dtype=float) + value = shares * spec.prices + invested = float(value.sum()) + expected_pnl = float(spec.mean_returns @ value) - total_cost + if spec.is_gaussian: + sigma = float(np.sqrt(max(value @ spec.gaussian_cov @ value, 0.0))) + z = float(norm.ppf(spec.confidence)) + var_m, cvar_m = -expected_pnl + z * sigma, -expected_pnl + cvar_multiplier(spec.confidence) * sigma + else: + pnl = spec.scenarios @ value - total_cost + var_m, cvar_m = scenario_cvar(-pnl, spec.confidence, spec.probabilities) + sector = (pd.Series(value).groupby(pd.Series(spec.sectors)).sum() / spec.wealth).round(6) + return { + "invested": invested, "cash": float(cash), "wealth": float(cash + invested), + "n_positions": int((shares > 0).sum()), "expected_pnl": expected_pnl, + "expected_return": expected_pnl / spec.wealth if spec.wealth else 0.0, + "var": float(var_m), "cvar": float(cvar_m), + "cvar_fraction": float(cvar_m) / spec.wealth if spec.wealth else 0.0, + "weights": {s: float(value[i] / spec.wealth) for i, s in enumerate(spec.symbols) if value[i] != 0}, + "sector_exposure": {k: float(v) for k, v in sector.items() if v > 0}, + } + + # -- verification helper: risk of an arbitrary share portfolio on the same scenario set ----------- + @staticmethod + def portfolio_risk_on_scenarios(spec: ProblemSpec, shares: np.ndarray, confidence: float) -> dict: + pnl = spec.scenarios @ (np.asarray(shares) * spec.prices) + var_m, cvar_m = scenario_cvar(-pnl, confidence, spec.probabilities) + w = spec.weights + return {"expected_pnl": float((w @ pnl) / w.sum()), "var": var_m, "cvar": cvar_m} diff --git a/quantcore/optimization/solvers.py b/quantcore/optimization/solvers.py new file mode 100644 index 0000000..9897814 --- /dev/null +++ b/quantcore/optimization/solvers.py @@ -0,0 +1,317 @@ +"""MILP solvers behind one interface: NVIDIA cuOpt (REST, on the DGX Spark) and HiGHS (scipy). + +The cuOpt server solves LP/MILP from a JSON body with the constraint matrix in CSR form and +answers asynchronously (submit -> poll). :class:`CuOptSolver` wraps this with timeouts, retry +with back-off on connection errors and a circuit breaker; :func:`solve_with_fallback` uses +the local HiGHS solver when cuOpt is unreachable (deterministic analysis must keep working +when an external dependency is down -- and the result says which solver produced it). +""" + +from __future__ import annotations + +import logging +import time +from dataclasses import dataclass, field + +import numpy as np +import requests + +from quantcore.config import Settings +from quantcore.data.yahoo import CircuitBreaker, CircuitOpenError +from quantcore.optimization.problem import MILPModel + +log = logging.getLogger(__name__) + +_HEALTH_CACHE: dict[str, tuple[float, bool]] = {} # base_url -> (checked_at, reachable) + +CUOPT_HEADERS = {"CLIENT-VERSION": "custom", "Accept": "application/json", "Content-Type": "application/json"} +OPTIMAL_STATUSES = {"Optimal", "FeasibleFound"} + +# cuOpt 26.08 aborts on some of these models rather than solving them, and which of its LP methods +# copes depends on the model. Measured on a 4 137-variable / 4 106-row mean-CVaR MILP (40 candidates, +# 4 096 scenarios, beta = 99 %): with CVaR in the *objective* (objective='min_cvar') the default +# concurrent method, the dual simplex (2) and method 3 all abort within ~5 s, while method 1 (PDLP) +# returns the optimum in 4.9 s, matching HiGHS to 7e-5. With CVaR as a *constraint* the default +# solves it. So an abort is retried down this ladder before the caller is told cuOpt could not do it. +CUOPT_RETRY_METHODS = (1, 2) + + +@dataclass +class SolverResult: + status: str # Optimal | FeasibleFound | Infeasible | Unbounded | TimeLimit | Error + x: np.ndarray | None + objective: float | None + solver: str + solve_time: float + mip_gap: float | None = None + message: str = "" + raw: dict = field(default_factory=dict) + + @property + def ok(self) -> bool: + return self.x is not None and self.status in OPTIMAL_STATUSES + + @property + def reproducible(self) -> bool: + """True when optimality was proven within the MIP gap: with the same model (same scenario + hash) any solver run returns the same objective value, and the same shares up to exact ties. + A ``FeasibleFound`` (time limit) result depends on the solver's timing and is not reproducible.""" + return self.ok and self.status == "Optimal" + + +def _bound(v: float) -> float | str: + if np.isposinf(v): + return "inf" + if np.isneginf(v): + return "ninf" + return float(v) + + +def cuopt_solver_config(settings: Settings, time_limit: float, extra_config: dict | None = None) -> dict: + """cuOpt ``solver_config``: time limit, MIP gaps (reproducibility), threads, no console log.""" + cfg = {"time_limit": float(time_limit), "log_to_console": False, + "tolerances": {"mip_relative_gap": settings.mip_rel_gap, "mip_absolute_gap": settings.mip_abs_gap}} + if settings.cuopt_threads: + cfg["num_cpu_threads"] = int(settings.cuopt_threads) + if extra_config: + tol = {**cfg["tolerances"], **(extra_config.get("tolerances") or {})} + cfg = {**cfg, **extra_config, "tolerances": tol} + return cfg + + +def to_cuopt_payload(model: MILPModel, time_limit: float, extra_config: dict | None = None, + settings: Settings | None = None) -> dict: + """The cuOpt LP/MILP request body for a :class:`MILPModel`.""" + A = model.A.tocsr() + payload = { + "csr_constraint_matrix": { + "offsets": A.indptr.tolist(), "indices": A.indices.tolist(), "values": A.data.tolist(), + }, + "constraint_bounds": { + "upper_bounds": [_bound(v) for v in model.row_ub], "lower_bounds": [_bound(v) for v in model.row_lb], + }, + "objective_data": {"coefficients": model.c.tolist(), "scalability_factor": 1.0, "offset": model.offset}, + "variable_bounds": { + "upper_bounds": [_bound(v) for v in model.var_ub], "lower_bounds": [_bound(v) for v in model.var_lb], + }, + "variable_types": list(model.var_types), + "variable_names": list(model.var_names), + "maximize": bool(model.maximize), + "solver_config": (cuopt_solver_config(settings, time_limit, extra_config) if settings is not None + else {"time_limit": float(time_limit), **(extra_config or {})}), + } + return payload + + +def encode_payload(payload: dict, use_msgpack: bool) -> tuple[bytes, str]: + """Serialise the request body: msgpack (about half the size of JSON, faster to parse) when + available and enabled, JSON otherwise.""" + if use_msgpack: + try: + import msgpack + + return msgpack.packb(payload, use_bin_type=True), "application/vnd.msgpack" + except ImportError: # pragma: no cover + pass + import json + + return json.dumps(payload).encode(), "application/json" + + +class CuOptSolver: + """Client of the cuOpt self-hosted server (``/cuopt/request`` + ``/cuopt/solution/{id}``).""" + + name = "cuopt" + + def __init__(self, settings: Settings, breaker: CircuitBreaker | None = None): + self.settings = settings + self.base_url = settings.cuopt_url.rstrip("/") + self.breaker = breaker or CircuitBreaker(threshold=3, cooldown=120.0, name="cuOpt") + + # -- health ---------------------------------------------------------------------------------- + def reachable(self, ttl: float = 30.0) -> bool: + """Is the server up? Cached for ``ttl`` seconds (a solve by row generation asks repeatedly). + + Without this an unreachable server costs the connect timeout on every attempt before the + HiGHS fallback takes over -- minutes of apparent hanging for ``solver='auto'``. + """ + now = time.monotonic() + hit = _HEALTH_CACHE.get(self.base_url) + if hit is not None and now - hit[0] < ttl: + return hit[1] + ok = bool(self.health(timeout=2.0).get("reachable")) + _HEALTH_CACHE[self.base_url] = (now, ok) + return ok + + def health(self, timeout: float = 5.0) -> dict: + try: + r = requests.get(f"{self.base_url}/cuopt/health", headers=CUOPT_HEADERS, timeout=timeout) + r.raise_for_status() + return {"reachable": True, **r.json()} + except Exception as exc: # noqa: BLE001 + return {"reachable": False, "error": str(exc)} + + # -- solve ------------------------------------------------------------------------------------- + def _post_with_retry(self, payload: dict) -> str: + body, content_type = encode_payload(payload, self.settings.cuopt_msgpack) + headers = {**CUOPT_HEADERS, "Content-Type": content_type} + # the POST blocks until the server has read and parsed the whole model, so the timeout has + # to scale with the payload: a client-side timeout here surfaces as an "aborted" request + # (connect, read): a big body needs a long *read* timeout, never a long connect wait -- and + # cuOpt serves one request at a time, so a busy server refuses the connection rather than stalling + # one scalar timeout: urllib3 governs the body write with the connect timeout, so a short + # connect timeout aborts large uploads mid-flight + timeout = self.settings.cuopt_request_timeout + self.settings.cuopt_upload_seconds_per_mb * len(body) / 1e6 + delay = 1.0 + last: Exception | None = None + for attempt in range(1, 4): + # a fresh connection per attempt: a timed-out POST leaves a partially written body in a + # pooled connection, and the retry would append a second body -- the server then reports + # "unpack received extra data" (422) or aborts the job + with requests.Session() as session: + try: + r = session.post(f"{self.base_url}/cuopt/request", data=body, + headers={**headers, "Connection": "close"}, timeout=timeout) + if r.status_code in (400, 422): # schema or decoding error: no point retrying + raise ValueError(f"cuOpt rejected the model ({r.status_code}): {r.text[:500]}") + r.raise_for_status() + return r.json()["reqId"] + except (requests.ConnectionError, requests.Timeout) as exc: + last = exc + log.warning("cuOpt submit failed (attempt %d/3): %s", attempt, exc) + time.sleep(delay) + delay *= 2 + raise ConnectionError( + f"cuOpt unreachable at {self.base_url} (payload {len(body) / 1e6:.1f} MB, timeout " + f"{timeout:.0f} s per attempt)") from last + + def solve(self, model: MILPModel, time_limit: float | None = None, extra_config: dict | None = None, + _retry: bool = True) -> SolverResult: + """Submit, poll, parse. An aborted solve is retried down :data:`CUOPT_RETRY_METHODS`.""" + self.breaker.check() + time_limit = time_limit or self.settings.cuopt_time_limit + payload = to_cuopt_payload(model, time_limit, extra_config, self.settings) + t0 = time.monotonic() + try: + req_id = self._post_with_retry(payload) + except (ConnectionError, ValueError): + self.breaker.failure() + raise + deadline = t0 + self.settings.cuopt_timeout + url = f"{self.base_url}/cuopt/solution/{req_id}" + try: + while time.monotonic() < deadline: + try: + r = requests.get(url, headers=CUOPT_HEADERS, timeout=self.settings.cuopt_request_timeout) + except (requests.ConnectionError, requests.Timeout) as exc: + # the server went away mid-solve (e.g. its solver process died and took the web + # server with it): raise the builtin ConnectionError that solve_with_fallback + # turns into a HiGHS solve -- requests' own class is not a subclass of it + self.breaker.failure() + raise ConnectionError(f"cuOpt connection lost during the solve at {self.base_url}: " + f"{str(exc)[:120]}") from exc + body = r.json() + if body.get("error_result"): + message = str(body.get("error")) + if _retry and "abort" in message.lower(): + for method in CUOPT_RETRY_METHODS: + if (extra_config or {}).get("method") == method: + continue + log.warning("cuOpt aborted (%s) - retrying with method=%d", message[:60], method) + retried = self.solve(model, time_limit, {**(extra_config or {}), "method": method}, + _retry=False) + if retried.ok: + retried.message = (f"{retried.message} (cuOpt aborted with its default method; " + f"solved with method={method})").strip() + return retried + message = (f"{message} - and every method in {CUOPT_RETRY_METHODS} aborted too") + self.breaker.failure() + return SolverResult("Error", None, None, self.name, time.monotonic() - t0, message=message, + raw=body) + if "response" in body: + self.breaker.success() + return self._parse(body["response"]["solver_response"], time.monotonic() - t0) + time.sleep(self.settings.cuopt_poll_interval) + finally: + try: + requests.delete(url, headers=CUOPT_HEADERS, timeout=10) + except requests.RequestException: + pass + return SolverResult("TimeLimit", None, None, self.name, time.monotonic() - t0, message="client-side timeout") + + @staticmethod + def _parse(resp: dict, elapsed: float) -> SolverResult: + status = str(resp.get("status", "Unknown")) + sol = resp.get("solution") or {} + x = sol.get("primal_solution") + stats = sol.get("milp_statistics") or {} + return SolverResult( + status=status, x=None if x is None else np.asarray(x, dtype=float), + objective=sol.get("primal_objective"), solver="cuopt", solve_time=float(sol.get("solver_time", elapsed)), + mip_gap=stats.get("mip_gap"), raw={"status": status, "milp_statistics": stats, + "problem_category": sol.get("problem_category")}, + ) + + +class HighsSolver: + """Local MILP solver (HiGHS via ``scipy.optimize.milp``) -- the fallback and the test oracle.""" + + name = "highs" + + def __init__(self, settings: Settings | None = None): + self.settings = settings + + def solve(self, model: MILPModel, time_limit: float | None = None, mip_gap: float | None = None) -> SolverResult: + from scipy.optimize import Bounds, LinearConstraint, milp + + if mip_gap is None: + mip_gap = self.settings.mip_rel_gap if self.settings is not None else 1e-4 + t0 = time.monotonic() + c = -model.c if model.maximize else model.c + integrality = np.array([1 if t == "I" else 0 for t in model.var_types]) + cons = LinearConstraint(model.A, model.row_lb, model.row_ub) + bounds = Bounds(model.var_lb, model.var_ub) + options = {"disp": False, "mip_rel_gap": mip_gap} + if time_limit: + options["time_limit"] = float(time_limit) + res = milp(c, constraints=cons, integrality=integrality, bounds=bounds, options=options) + elapsed = time.monotonic() - t0 + status_map = {0: "Optimal", 1: "IterationLimit", 2: "Infeasible", 3: "Unbounded", 4: "Error"} + status = status_map.get(res.status, "Unknown") + if res.status == 1 and res.x is not None: + status = "FeasibleFound" + x = None if res.x is None else np.asarray(res.x, dtype=float) + obj = None if x is None else float(model.c @ x + model.offset) + return SolverResult(status, x, obj, self.name, elapsed, mip_gap=getattr(res, "mip_gap", None), + message=str(res.message), raw={"status_code": int(res.status)}) + + +def solve_with_fallback(model: MILPModel, settings: Settings, solver: str = "auto", + time_limit: float | None = None) -> SolverResult: + """``solver``: ``'cuopt'``, ``'highs'`` or ``'auto'`` (cuOpt, HiGHS if cuOpt is unreachable).""" + if solver == "highs": + return HighsSolver(settings).solve(model, time_limit) + cu = CuOptSolver(settings) + if solver == "auto" and settings.solver_fallback and not cu.reachable(): + log.info("cuOpt is not reachable at %s - solving with HiGHS", settings.cuopt_url) + result = HighsSolver(settings).solve(model, time_limit) + result.message = (result.message + " (fallback: cuOpt unreachable)").strip() + return result + reason = "cuOpt unavailable" + try: + result = cu.solve(model, time_limit) + if result.status != "Error" or solver == "cuopt": + return result + log.warning("cuOpt returned an error (%s)", result.message) + # the server is up and answered -- say so, or the reader sees a green health pill next to a + # result that claims cuOpt was "unavailable" and concludes the GPU is being ignored + reason = ("cuOpt is reachable but aborted on this model" if "abort" in result.message.lower() + else f"cuOpt returned an error: {result.message[:80]}") + except (ConnectionError, CircuitOpenError, ValueError) as exc: + if solver == "cuopt" or not settings.solver_fallback: + raise + log.warning("cuOpt unavailable (%s) - falling back to HiGHS", exc) + reason = f"cuOpt unreachable: {str(exc)[:80]}" + result = HighsSolver(settings).solve(model, time_limit) + result.message = f"{result.message} (fallback to HiGHS: {reason})".strip() + return result diff --git a/quantcore/universe/__init__.py b/quantcore/universe/__init__.py new file mode 100644 index 0000000..3c31226 --- /dev/null +++ b/quantcore/universe/__init__.py @@ -0,0 +1,18 @@ +"""Investment universe: candidate sources, Yahoo symbol mapping and the one-off ``SetupService``.""" + +from quantcore.universe.setup_service import SetupService, UniverseBuildReport +from quantcore.universe.sources import ( + fetch_sp500_wikipedia, + fetch_stoxx600_ishares, + load_static_candidates, +) +from quantcore.universe.symbols import to_yahoo_symbol + +__all__ = [ + "SetupService", + "UniverseBuildReport", + "fetch_sp500_wikipedia", + "fetch_stoxx600_ishares", + "load_static_candidates", + "to_yahoo_symbol", +] diff --git a/quantcore/universe/setup_service.py b/quantcore/universe/setup_service.py new file mode 100644 index 0000000..0f35364 --- /dev/null +++ b/quantcore/universe/setup_service.py @@ -0,0 +1,209 @@ +"""``SetupService`` -- builds the investment universe once and persists everything. + +Steps (each is a method, so parts can be re-run): + +1. **candidates**: S&P 500 (US) and STOXX Europe 600 (Europe) constituents, see + :mod:`quantcore.universe.sources`. +2. **metadata**: Yahoo ``Ticker.info`` for every candidate -- listing currency, exchange, + domicile, Yahoo sector/industry, market capitalisation, shares outstanding, first trade date. +3. **prices**: complete adjusted-close history since ``settings.history_start`` for all + candidates (so later universe changes need no re-download); FX rates to the base currency. +4. **selection**: one listing per company, rank by market capitalisation in the base currency, + keep the largest ``n_us`` / ``n_europe``, then drop stocks without prices since 2005 + (``settings.required_history_from``) or with poor coverage -- which is why the final + universe has fewer than 1000 names. +5. **persist**: ``universe/universe.csv`` with ticker, company name, country, listing currency, + exchange, GICS sector, Yahoo sector/industry, market cap (local and base), first price date. +""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass, field +from datetime import datetime, timezone + +import numpy as np +import pandas as pd + +from quantcore.config import Settings +from quantcore.data.cleaning import clean_prices, longest_missing_run +from quantcore.data.fx import download_fx +from quantcore.data.store import DataStore +from quantcore.data.yahoo import YahooClient, apply_price_scale, coverage_since, first_valid_dates +from quantcore.universe.sources import EUROPE_COUNTRIES, fetch_candidates +from quantcore.universe.symbols import normalise_share_class_name + +log = logging.getLogger(__name__) + +UNIVERSE_COLUMNS = [ + "symbol", "name", "region", "country", "listing_currency", "exchange", "exchange_code", "gics_sector", + "gics_sub_industry", "yahoo_sector", "yahoo_industry", "market_cap", "market_cap_base", + "shares_outstanding", "last_price", "first_price_date", "price_coverage", "max_missing_run", "index_weight", + "source", "rank_in_region", +] + + +@dataclass +class UniverseBuildReport: + started_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat(timespec="seconds")) + n_candidates: int = 0 + n_resolved: int = 0 + n_with_prices: int = 0 + n_selected: int = 0 + n_universe: int = 0 + by_region: dict = field(default_factory=dict) + by_country: dict = field(default_factory=dict) + by_sector: dict = field(default_factory=dict) + dropped_no_history: list[str] = field(default_factory=list) + dropped_unresolved: list[str] = field(default_factory=list) + first_price_date: str | None = None + last_price_date: str | None = None + finished_at: str | None = None + + def as_dict(self) -> dict: + return self.__dict__.copy() + + +class SetupService: + """One-off construction of the investment universe (idempotent; re-run to refresh).""" + + def __init__(self, settings: Settings, store: DataStore | None = None, client: YahooClient | None = None): + self.settings = settings + self.store = store or DataStore(settings) + self.client = client or YahooClient(settings) + + # -- step 1 -------------------------------------------------------------------------------- + def candidates(self, allow_web: bool = True, max_per_region: int | None = None) -> pd.DataFrame: + us = fetch_candidates("US", allow_web=allow_web) + eu = fetch_candidates("Europe", allow_web=allow_web) + if max_per_region: + us, eu = us.head(max_per_region), eu.head(max_per_region) + df = pd.concat([us, eu], ignore_index=True).drop_duplicates(subset="symbol").reset_index(drop=True) + self.store.save_candidates(df) + log.info("candidates: %d US + %d Europe = %d", len(us), len(eu), len(df)) + return df + + # -- step 2 -------------------------------------------------------------------------------- + def metadata(self, symbols: list[str]) -> pd.DataFrame: + def _progress(i, n): + log.info("metadata %d/%d", i, n) + + meta = self.client.ticker_infos(symbols, progress=_progress) + self.store.save_metadata(meta) + return meta + + # -- step 3 -------------------------------------------------------------------------------- + def prices_and_fx(self, symbols: list[str], meta: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]: + hist = self.client.download_history(symbols, start=self.settings.history_start)["Close"] + prices = clean_prices(apply_price_scale(hist, meta["price_scale"])) + self.store.save_prices(prices) + currencies = meta.loc[meta.index.isin(prices.columns), "currency"].dropna().unique().tolist() + fx = download_fx(self.client, currencies, self.settings.base_currency, self.settings.history_start) + self.store.save_fx(fx) + return prices, fx + + # -- step 4 -------------------------------------------------------------------------------- + def select(self, candidates: pd.DataFrame, meta: pd.DataFrame, prices: pd.DataFrame, fx: pd.DataFrame, + report: UniverseBuildReport | None = None) -> pd.DataFrame: + report = report or UniverseBuildReport() + s = self.settings + df = candidates.merge(meta, left_on="symbol", right_index=True, how="left") + resolved = df["quote_type"].eq("EQUITY") & df["currency"].notna() + report.dropped_unresolved = df.loc[~resolved, "symbol"].tolist() + df = df[resolved].copy() + report.n_resolved = len(df) + + # price statistics + have = df["symbol"].isin(prices.columns) + df = df[have].copy() + report.n_with_prices = len(df) + first = first_valid_dates(prices[df["symbol"]]) + cov = coverage_since(prices[df["symbol"]], pd.Timestamp(s.required_history_from)) + gaps = longest_missing_run(prices[df["symbol"]], pd.Timestamp(s.required_history_from)) + df["first_price_date"] = df["symbol"].map(first).map(lambda d: None if pd.isna(d) else pd.Timestamp(d).date().isoformat()) + df["price_coverage"] = df["symbol"].map(cov).round(4) + df["max_missing_run"] = df["symbol"].map(gaps) + last_px = prices[df["symbol"]].ffill().iloc[-1] + df["last_price"] = df["last_price"].where(df["last_price"].notna(), df["symbol"].map(last_px)) + + # market capitalisation in the base currency + fx_last = fx.ffill().iloc[-1] + rate = df["currency"].map(fx_last).astype(float) + cap_local = df["market_cap"].astype(float) + est = df["shares_outstanding"].astype(float) * df["last_price"].astype(float) + cap_local = cap_local.where(cap_local.notna() & (cap_local > 0), est) + df["market_cap"] = cap_local + df["market_cap_base"] = cap_local * rate + df["listing_currency"] = df["currency"] + df["name"] = df["long_name"].where(df["long_name"].notna(), df["name"]) + df["country"] = df["yahoo_country"].where(df["yahoo_country"].notna(), df["country"]) + + # Europe = EU + CH + NO + UK by domicile of the *source* (STOXX country), US = S&P 500 members + eu_mask = (df["region"] == "Europe") & ~candidates.set_index("symbol")["country"].reindex(df["symbol"]).isin(EUROPE_COUNTRIES).values + df = df[~eu_mask].copy() + + # one listing per company: keep the largest market cap among share classes + df["company_key"] = df["region"] + "|" + df["name"].map(normalise_share_class_name) + df = df.sort_values("market_cap_base", ascending=False).drop_duplicates("company_key") + + # rank within region and keep the largest N + df = df[df["market_cap_base"].notna() & (df["market_cap_base"] > 0)].copy() + df["rank_in_region"] = df.groupby("region")["market_cap_base"].rank(ascending=False, method="first").astype(int) + limit = df["region"].map({"US": s.n_us, "Europe": s.n_europe}) + selected = df[df["rank_in_region"] <= limit].copy() + report.n_selected = len(selected) + + # history filter: prices since 2005 and adequate coverage + has_history = ( + (pd.to_datetime(selected["first_price_date"]) <= pd.Timestamp(s.required_history_from)) + & (selected["price_coverage"] >= s.min_price_coverage) + & (selected["max_missing_run"] <= s.max_missing_run) + ) + report.dropped_no_history = selected.loc[~has_history, "symbol"].tolist() + universe = selected[has_history].sort_values(["region", "rank_in_region"]).reset_index(drop=True) + for col in UNIVERSE_COLUMNS: + if col not in universe.columns: + universe[col] = np.nan + universe = universe[UNIVERSE_COLUMNS] + report.n_universe = len(universe) + report.by_region = universe["region"].value_counts().to_dict() + report.by_country = universe["country"].value_counts().to_dict() + report.by_sector = universe["gics_sector"].value_counts().to_dict() + return universe + + # -- orchestration ---------------------------------------------------------------------------- + def run(self, allow_web: bool = True, max_per_region: int | None = None) -> UniverseBuildReport: + """Full setup: candidates -> metadata -> prices/FX -> selection -> persist.""" + report = UniverseBuildReport() + self.store.ensure_dirs() + cands = self.candidates(allow_web=allow_web, max_per_region=max_per_region) + report.n_candidates = len(cands) + meta = self.metadata(cands["symbol"].tolist()) + ok = meta.index[meta["quote_type"].eq("EQUITY")].tolist() + prices, fx = self.prices_and_fx(ok, meta) + universe = self.select(cands, meta, prices, fx, report) + self.store.save_universe(universe) + report.first_price_date = prices.index.min().date().isoformat() + report.last_price_date = prices.index.max().date().isoformat() + report.finished_at = datetime.now(timezone.utc).isoformat(timespec="seconds") + self.store.update_manifest( + setup_at=report.finished_at, n_candidates=report.n_candidates, n_universe=report.n_universe, + by_region=report.by_region, first_price_date=report.first_price_date, + last_price_date=report.last_price_date, n_symbols=len(prices.columns), + base_currency=self.settings.base_currency, required_history_from=self.settings.required_history_from, + ) + log.info("universe built: %d stocks (%s)", report.n_universe, report.by_region) + return report + + def rebuild_selection(self) -> UniverseBuildReport: + """Re-run only the selection step on stored data (e.g. after changing ``n_us``).""" + report = UniverseBuildReport() + cands = self.store.load_candidates() + meta = self.store.load_metadata() + prices = self.store.load_prices() + fx = self.store.load_fx() + report.n_candidates = len(cands) + universe = self.select(cands, meta, prices, fx, report) + self.store.save_universe(universe) + self.store.update_manifest(n_universe=report.n_universe, by_region=report.by_region) + return report diff --git a/quantcore/universe/sources.py b/quantcore/universe/sources.py new file mode 100644 index 0000000..561e767 --- /dev/null +++ b/quantcore/universe/sources.py @@ -0,0 +1,302 @@ +"""Candidate lists for the investment universe. + +Two curated index memberships serve as the *candidates* for "the largest 500 US and European +stocks": + +* **US**: the S&P 500 constituents (Wikipedia, ``List of S&P 500 companies``). +* **Europe** (EU, CH, NO, UK): the STOXX Europe 600 constituents, read from the daily holdings + file of the iShares STOXX Europe 600 UCITS ETF (complete list with country, exchange, + currency, GICS sector and index weight). Wikipedia's STOXX 600 page is a partial fallback. + +Every fetch is snapshotted to ``quantcore/data/static/`` so that the setup can be reproduced +offline (``load_static_candidates``). All frames share the same columns, see +:data:`CANDIDATE_COLUMNS`. +""" + +from __future__ import annotations + +import io +import logging +from datetime import date +from importlib import resources +from pathlib import Path + +import pandas as pd +import requests + +from quantcore.universe.symbols import to_yahoo_symbol + +log = logging.getLogger(__name__) + +CANDIDATE_COLUMNS = [ + "symbol", # Yahoo symbol + "source_ticker", # ticker as written by the source + "name", + "region", # 'US' | 'Europe' + "country", # domicile as given by the source (Yahoo's view is added later) + "exchange", # exchange name as given by the source + "listing_currency", # currency of the listing as given by the source (may be empty) + "gics_sector", # GICS sector, English + "gics_sub_industry", # only for the S&P 500 + "index_weight", # index / ETF weight in %, if the source gives one + "source", + "source_date", +] + +USER_AGENT = "Mozilla/5.0 (compatible; quantcore-setup/0.1; +https://fhgr.ch)" + +SP500_URL = "https://en.wikipedia.org/wiki/List_of_S%26P_500_companies" +STOXX600_WIKI_URL = "https://en.wikipedia.org/wiki/STOXX_Europe_600" +# iShares STOXX Europe 600 UCITS ETF (DE), holdings download of the German retail site +ISHARES_STOXX600_URL = ( + "https://www.ishares.com/de/privatanleger/de/produkte/251931/" + "ishares-stoxx-europe-600-ucits-etf-de-fund/1478358465952.ajax" + "?fileType=csv&fileName=EXSA_holdings&dataType=fund" +) + +# German labels of the iShares file -> English +ISHARES_SECTOR_EN = { + "IT": "Information Technology", + "Finanzwesen": "Financials", + "Gesundheitsversorgung": "Health Care", + "Energie": "Energy", + "Nichtzyklische Konsumgüter": "Consumer Staples", + "Zyklische Konsumgüter": "Consumer Discretionary", + "Industrie": "Industrials", + "Materialien": "Materials", + "Grundstoffe": "Materials", + "Kommunikation": "Communication Services", + "Versorger": "Utilities", + "Immobilien": "Real Estate", +} +ISHARES_COUNTRY_EN = { + "Vereinigtes Königreich": "United Kingdom", + "Frankreich": "France", + "Deutschland": "Germany", + "Schweiz": "Switzerland", + "Schweden": "Sweden", + "Italien": "Italy", + "Niederlande": "Netherlands", + "Spanien": "Spain", + "Norwegen": "Norway", + "Dänemark": "Denmark", + "Finnland": "Finland", + "Polen": "Poland", + "Belgien": "Belgium", + "Österreich": "Austria", + "Griechenland": "Greece", + "Irland": "Ireland", + "Portugal": "Portugal", + "Luxemburg": "Luxembourg", + "Tschechische Republik": "Czech Republic", + "Ungarn": "Hungary", + "Vereinigte Staaten": "United States", + "Bermuda": "Bermuda", + "Israel": "Israel", +} + +# The countries that make up "Europe" for this universe: EU members + CH + NO + UK. +# Domiciles outside (Bermuda, Israel, ...) are excluded even if they are STOXX 600 members. +EUROPE_COUNTRIES = { + "Austria", "Belgium", "Bulgaria", "Croatia", "Cyprus", "Czech Republic", "Denmark", "Estonia", + "Finland", "France", "Germany", "Greece", "Hungary", "Ireland", "Italy", "Latvia", "Lithuania", + "Luxembourg", "Malta", "Netherlands", "Poland", "Portugal", "Romania", "Slovakia", "Slovenia", + "Spain", "Sweden", "Switzerland", "Norway", "United Kingdom", +} + + +_GERMAN_MONTHS = {"jan": 1, "feb": 2, "mär": 3, "mar": 3, "apr": 4, "mai": 5, "jun": 6, "jul": 7, "aug": 8, + "sep": 9, "okt": 10, "nov": 11, "dez": 12} + + +def _parse_german_date(text: str) -> str | None: + """``'18.Sept.2026'`` -> ``'2026-09-18'`` (``None`` when unparseable).""" + parts = [p for p in text.replace(" ", "").split(".") if p] + if len(parts) != 3: + return None + try: + day, year = int(parts[0]), int(parts[2]) + month = _GERMAN_MONTHS.get(parts[1][:3].lower()) + return date(year, month, day).isoformat() if month else None + except ValueError: + return None + + +def _get(url: str, timeout: float = 30.0) -> bytes: + resp = requests.get(url, headers={"User-Agent": USER_AGENT}, timeout=timeout) + resp.raise_for_status() + return resp.content + + +def _empty() -> pd.DataFrame: + return pd.DataFrame(columns=CANDIDATE_COLUMNS) + + +# ---------------------------------------------------------------------------------------- +# US: S&P 500 from Wikipedia +# ---------------------------------------------------------------------------------------- +def fetch_sp500_wikipedia(timeout: float = 30.0) -> pd.DataFrame: + """S&P 500 constituents with GICS sector and sub-industry (Wikipedia).""" + html = _get(SP500_URL, timeout) + tables = pd.read_html(io.BytesIO(html)) + table = next(t for t in tables if "Symbol" in t.columns and "GICS Sector" in t.columns) + today = date.today().isoformat() + df = pd.DataFrame( + { + "symbol": [to_yahoo_symbol(s, country="United States") for s in table["Symbol"].astype(str)], + "source_ticker": table["Symbol"].astype(str), + "name": table["Security"].astype(str), + "region": "US", + "country": "United States", + "exchange": "", + "listing_currency": "USD", + "gics_sector": table["GICS Sector"].astype(str), + "gics_sub_industry": table["GICS Sub-Industry"].astype(str), + "index_weight": float("nan"), + "source": "wikipedia_sp500", + "source_date": today, + } + ) + log.info("S&P 500 candidates from Wikipedia: %d", len(df)) + return df[CANDIDATE_COLUMNS] + + +# ---------------------------------------------------------------------------------------- +# Europe: STOXX Europe 600 from the iShares ETF holdings file +# ---------------------------------------------------------------------------------------- +def parse_ishares_holdings(text: str, source_date: str | None = None) -> pd.DataFrame: + """Parse the German iShares holdings CSV (``Fondsposition per ...`` header, ``;``-free).""" + lines = text.lstrip("").splitlines() + if source_date is None: + first = lines[0] if lines else "" + raw_date = first.split(",", 1)[1].strip().strip('"') if "," in first else "" + source_date = _parse_german_date(raw_date) or date.today().isoformat() + try: + header_idx = next(i for i, line in enumerate(lines) if line.startswith("Emittententicker")) + except StopIteration as exc: + raise ValueError("iShares holdings file: header line not found") from exc + body = "\n".join(lines[header_idx:]) + raw = pd.read_csv(io.StringIO(body), sep=",", quotechar='"', thousands=".", decimal=",") + eq = raw[raw["Anlageklasse"] == "Aktien"].copy() + eq = eq[eq["Emittententicker"].notna() & (eq["Emittententicker"].astype(str).str.strip() != "-")] + + def _symbol(row: pd.Series) -> str: + try: + return to_yahoo_symbol(str(row["Emittententicker"]), exchange=str(row["Börse"])) + except ValueError: + country = ISHARES_COUNTRY_EN.get(str(row["Standort"]), str(row["Standort"])) + return to_yahoo_symbol(str(row["Emittententicker"]), country=country) + + clean = lambda v: str(v).replace("\xa0", " ").strip() # noqa: E731 + df = pd.DataFrame( + { + "symbol": eq.apply(_symbol, axis=1), + "source_ticker": eq["Emittententicker"].astype(str).map(clean), + "name": eq["Name"].astype(str).map(clean).str.title(), + "region": "Europe", + "country": eq["Standort"].map(lambda c: ISHARES_COUNTRY_EN.get(clean(c), clean(c))), + "exchange": eq["Börse"].astype(str).map(clean), + "listing_currency": eq["Marktwährung"].astype(str).map(clean), + "gics_sector": eq["Sektor"].map(lambda s: ISHARES_SECTOR_EN.get(clean(s), clean(s))), + "gics_sub_industry": "", + "index_weight": pd.to_numeric(eq["Gewichtung (%)"], errors="coerce"), + "source": "ishares_stoxx600", + "source_date": source_date, + } + ) + df = df.drop_duplicates(subset="symbol").reset_index(drop=True) + return df[CANDIDATE_COLUMNS] + + +def fetch_stoxx600_ishares(timeout: float = 30.0) -> pd.DataFrame: + """STOXX Europe 600 constituents from the iShares ETF holdings (complete, with weights).""" + raw = _get(ISHARES_STOXX600_URL, timeout).decode("utf-8-sig", errors="replace") + df = parse_ishares_holdings(raw) + log.info("STOXX 600 candidates from iShares: %d", len(df)) + return df + + +def fetch_stoxx600_wikipedia(timeout: float = 30.0) -> pd.DataFrame: + """Partial STOXX 600 list from Wikipedia (fallback; no exchange, so the suffix is by country).""" + html = _get(STOXX600_WIKI_URL, timeout) + tables = pd.read_html(io.BytesIO(html)) + table = next(t for t in tables if "Ticker" in t.columns and "Country" in t.columns) + today = date.today().isoformat() + rows = [] + for _, r in table.iterrows(): + try: + symbol = to_yahoo_symbol(str(r["Ticker"]), country=str(r["Country"])) + except ValueError: + continue + rows.append( + { + "symbol": symbol, + "source_ticker": str(r["Ticker"]), + "name": str(r["Company"]), + "region": "Europe", + "country": str(r["Country"]), + "exchange": "", + "listing_currency": "", + "gics_sector": "", + "gics_sub_industry": str(r.get("ICB Sector", "")), + "index_weight": float("nan"), + "source": "wikipedia_stoxx600", + "source_date": today, + } + ) + df = pd.DataFrame(rows, columns=CANDIDATE_COLUMNS) + log.info("STOXX 600 candidates from Wikipedia: %d", len(df)) + return df + + +# ---------------------------------------------------------------------------------------- +# Static snapshots shipped with the package +# ---------------------------------------------------------------------------------------- +def _static_path(region: str) -> Path: + name = "candidates_us.csv" if region.upper() == "US" else "candidates_europe.csv" + return Path(str(resources.files("quantcore.data").joinpath(f"static/{name}"))) + + +def load_static_candidates(region: str) -> pd.DataFrame: + """Candidates snapshotted into the package (offline fallback).""" + path = _static_path(region) + if not path.exists(): + log.warning("No static candidate file for region %s (%s)", region, path) + return _empty() + df = pd.read_csv(path, dtype={"symbol": str, "source_ticker": str}) + log.info("Static %s candidates: %d (%s)", region, len(df), path.name) + return df[CANDIDATE_COLUMNS] + + +def save_static_candidates(df: pd.DataFrame, region: str, path: Path | None = None) -> Path: + """Refresh the shipped snapshot (called by the setup after a successful web fetch).""" + path = path or _static_path(region) + path.parent.mkdir(parents=True, exist_ok=True) + df[CANDIDATE_COLUMNS].to_csv(path, index=False) + return path + + +def fetch_candidates(region: str, allow_web: bool = True, timeout: float = 30.0) -> pd.DataFrame: + """Best-effort candidate list: web sources first, then the static snapshot. + + Every web source is wrapped: a failing source is logged and the next one is tried + (timeout, retry and fallback -- the resilience the platform requires on external calls). + """ + region = "US" if region.upper() == "US" else "Europe" + fetchers = [fetch_sp500_wikipedia] if region == "US" else [fetch_stoxx600_ishares, fetch_stoxx600_wikipedia] + if allow_web: + for fetch in fetchers: + try: + df = fetch(timeout) + except Exception as exc: # noqa: BLE001 - any failure -> next source + log.warning("Candidate source %s failed: %s", fetch.__name__, exc) + continue + if len(df) >= 100: + if region == "Europe": + df = df[df["country"].isin(EUROPE_COUNTRIES)].reset_index(drop=True) + try: + save_static_candidates(df, region) + except OSError as exc: # read-only install + log.debug("Could not refresh static snapshot: %s", exc) + return df + return load_static_candidates(region) diff --git a/quantcore/universe/symbols.py b/quantcore/universe/symbols.py new file mode 100644 index 0000000..4e85b56 --- /dev/null +++ b/quantcore/universe/symbols.py @@ -0,0 +1,129 @@ +"""Mapping of exchange-local ticker codes to Yahoo Finance symbols. + +Yahoo identifies a listing by ```` where the suffix encodes the exchange +(``.SW`` SIX, ``.L`` London, ``.DE`` Xetra, ...). Share classes are written with a dash +(``NOVO-B.CO``, ``INVE-B.ST``, ``BRK-B``), and London codes lose their trailing dot +(``RR.`` -> ``RR.L``, ``BT.A`` -> ``BT-A.L``). + +Known exceptions that no rule catches live in ``data/static/symbol_overrides.json`` and are +applied last. +""" + +from __future__ import annotations + +import json +import re +from functools import lru_cache +from importlib import resources + +# iShares exchange names (STOXX 600 holdings file) -> Yahoo suffix +EXCHANGE_SUFFIX: dict[str, str] = { + "London Stock Exchange": ".L", + "Xetra": ".DE", + "Deutsche Börse AG": ".DE", + "Deutsche Boerse AG": ".DE", + "Nyse Euronext - Euronext Paris": ".PA", + "Euronext Paris": ".PA", + "SIX Swiss Exchange": ".SW", + "Nasdaq Omx Nordic": ".ST", # Stockholm + "Nasdaq Stockholm": ".ST", + "Borsa Italiana": ".MI", + "Euronext Amsterdam": ".AS", + "Bolsa De Madrid": ".MC", + "Oslo Bors Asa": ".OL", + "Omx Nordic Exchange Copenhagen A/S": ".CO", + "Nasdaq Omx Helsinki Ltd.": ".HE", + "Warsaw Stock Exchange/Equities/Main Market": ".WA", + "Nyse Euronext - Euronext Brussels": ".BR", + "Wiener Boerse Ag": ".VI", + "Athens Exchange S.A. Cash Market": ".AT", + "Irish Stock Exchange - All Market": ".IR", + "Nyse Euronext - Euronext Lisbon": ".LS", + "Prague Stock Exchange": ".PR", + "Budapest Stock Exchange": ".BD", +} + +# Country of an exchange (used when a source only gives the country of the listing) +COUNTRY_SUFFIX: dict[str, str] = { + "United Kingdom": ".L", + "Germany": ".DE", + "France": ".PA", + "Switzerland": ".SW", + "Sweden": ".ST", + "Italy": ".MI", + "Netherlands": ".AS", + "Spain": ".MC", + "Norway": ".OL", + "Denmark": ".CO", + "Finland": ".HE", + "Poland": ".WA", + "Belgium": ".BR", + "Austria": ".VI", + "Greece": ".AT", + "Ireland": ".IR", + "Portugal": ".LS", + "Czech Republic": ".PR", + "Hungary": ".BD", + "Luxembourg": ".PA", + "United States": "", +} + + +@lru_cache(maxsize=1) +def symbol_overrides() -> dict[str, str]: + """Hand-maintained corrections ``{rule_based_symbol: actual_yahoo_symbol}``.""" + try: + text = resources.files("quantcore.data").joinpath("static/symbol_overrides.json").read_text() + except FileNotFoundError: + return {} + data = json.loads(text) + return {k: v for k, v in data.items() if not k.startswith("_")} + + +def _clean_code(code: str) -> str: + code = code.strip().upper() + code = re.sub(r"\s+", "-", code) # "NOVO B" -> "NOVO-B" + code = code.rstrip(".") # "RR." -> "RR" + code = code.replace(".", "-") # "BT.A" -> "BT-A" + return code + + +def to_yahoo_symbol(source_ticker: str, exchange: str | None = None, country: str | None = None) -> str: + """Build the Yahoo symbol for an exchange-local ticker code. + + >>> to_yahoo_symbol("NOVO B", "Omx Nordic Exchange Copenhagen A/S") + 'NOVO-B.CO' + >>> to_yahoo_symbol("BT.A", "London Stock Exchange") + 'BT-A.L' + >>> to_yahoo_symbol("BRK.B", country="United States") + 'BRK-B' + """ + code = _clean_code(source_ticker) + suffix = None + if exchange is not None: + suffix = EXCHANGE_SUFFIX.get(exchange.strip()) + if suffix is None and country is not None: + suffix = COUNTRY_SUFFIX.get(country.strip()) + if suffix is None: + raise ValueError(f"Unknown exchange/country for ticker {source_ticker!r}: {exchange!r}/{country!r}") + symbol = f"{code}{suffix}" + return symbol_overrides().get(symbol, symbol) + + +def normalise_share_class_name(name: str) -> str: + """Reduce a company name to a key that is equal for its share classes. + + Used to keep only one listing per company (``Alphabet A`` / ``Alphabet C``, + ``Investor A`` / ``Investor B``). The heuristic strips legal forms and class markers. + """ + s = name.upper() + s = re.sub(r"[^A-Z0-9 ]", " ", s) + tokens = s.split() + drop = { + "INC", "CORP", "CORPORATION", "PLC", "AG", "SA", "SE", "NV", "N", "V", "AB", "ASA", "AS", "SPA", + "OYJ", "LTD", "LIMITED", "CO", "COMPANY", "GROUP", "HOLDING", "HOLDINGS", "THE", "CLASS", "CL", + "SER", "SERIES", "SHS", "SHARE", "SHARES", "ORD", "REG", "REGISTERED", "BEARER", "PS", "GS", + "A", "B", "C", "N", "R", "NEW", "KGAA", "SDB", "DI", "NPV", "ADR", "OY", "ORDINARY", + } + kept = [t for t in tokens if t not in drop] + return " ".join(kept[:3]) if kept else s.strip() diff --git a/scripts/experiments/README.md b/scripts/experiments/README.md new file mode 100644 index 0000000..57402e6 --- /dev/null +++ b/scripts/experiments/README.md @@ -0,0 +1,16 @@ +# Experiments behind the scenario-sampling section + +These scripts produced the tables in `docs/optimization_model.pdf`, Section "Scenario sets: +sampling error, variance reduction and large sets". They run against the local data store +(`quantcore setup` first) and print their tables; they are not part of the package or the tests. + +| script | what it measures | +|---|---| +| `scenario_noise.py N S SEEDS [cont\|int] [kinds] [beta]` | GBM model: plain MC vs antithetic / moment matching / LHS / Sobol' at equal `S`, continuous (weights) or integer problem, against the exact Gaussian optimum (cutting planes) — Table "Sampling noise" | +| `empirical_noise.py N S SEEDS BETA [kinds]` | empirical-innovation model: bootstrap vs moment-matched / balanced / rank-stratified resampling, evaluated on a 500 000-draw set | +| `is_noise.py N SIZES SEEDS BETAS` | importance sampling (two-stage tilt, defensive mixture) vs moment-matched Sobol' — Table "Importance sampling" | +| `scaling.py N SIZES highs\|cuopt` | solve time versus the number of scenarios (HiGHS locally / cuOpt on the Spark) — Table "Solve times" | +| `rowgen.py N SIZES [full]` | tail-row generation versus the full MILP | +| `gurobi_ref.py N BETA` | the exact Gaussian MISOCP solved directly with Gurobi (needs `gurobipy` and a licence large enough for `N`) — validation of the cutting-plane reference | + +Example: `python scripts/experiments/scenario_noise.py 30 2048 8 cont mc,antithetic,mc+mm,sobol,sobol+mm 0.95` diff --git a/scripts/experiments/empirical_noise.py b/scripts/experiments/empirical_noise.py new file mode 100644 index 0000000..4544111 --- /dev/null +++ b/scripts/experiments/empirical_noise.py @@ -0,0 +1,136 @@ +"""Sampling noise of the mean-CVaR LP (weights) under the empirical-innovation model with different resampling schemes. + +'truth' = a common evaluation set of 500k plain bootstrap draws (sampling error of CVaR for a fixed portfolio ~0.1%). +""" +import sys, time +import numpy as np, pandas as pd +from scipy.stats import qmc + +from quantcore.config import get_settings +from quantcore.data.market_data import MarketDataService +from quantcore.optimization.service import OptimizationService, OptimizationRequest +from quantcore.optimization.problem import MeanCVaRProblem, ProblemSpec, OptimizationConstraints, TransactionCosts +from quantcore.optimization.solvers import HighsSolver +from quantcore.models.covariance import CovarianceModel, symmetric_sqrt, symmetric_inv_sqrt +from quantcore.models.simulation import EmpiricalInnovationSimulator +from quantcore.measurement.risk import scenario_cvar + +N_ASSETS = int(sys.argv[1]) if len(sys.argv) > 1 else 30 +S = int(sys.argv[2]) if len(sys.argv) > 2 else 2048 +N_SEEDS = int(sys.argv[3]) if len(sys.argv) > 3 else 8 +BETA = float(sys.argv[4]) if len(sys.argv) > 4 else 0.95 +W, H = 1_000_000.0, 5 +LAMBDA = 0.05 * W +S_EVAL = 500_000 + +settings = get_settings(); md = MarketDataService(settings); svc = OptimizationService(settings, md) +req = OptimizationRequest(max_assets=N_ASSETS, solver="highs", cash=W) +symbols = svc.candidate_symbols(req) +mu_ann, _, _ = svc.expected_returns(req, symbols) +r = md.returns(symbols, currency="base", freq="daily").dropna() +cov_model = CovarianceModel.from_dict(req.covariance) +sim = EmpiricalInnovationSimulator(cov_model, max_history=req.innovation_history, standardize=req.innovation_standardize) +t0 = time.time() +eps = sim.innovations(r) +e = eps.values - eps.values.mean(axis=0) +e, info = sim._restandardize(e) +T, N = e.shape +cov = cov_model.estimate(r).values +A = symmetric_sqrt(cov) +mu_h = (mu_ann / 252).reindex(symbols).values * H +prices = md.latest_prices(symbols, currency="base").reindex(symbols).values +sectors = md.sectors(symbols).fillna("Unknown").tolist() +print(f"N={N} assets, T={T} innovations ({time.time()-t0:.0f}s), S={S}, h={H}, beta={BETA}, {info}") +# market-factor ordering of the innovation dates (projection on the first eigenvector of Sigma_T) +w1 = np.linalg.eigh(cov)[1][:, -1] +factor = e @ (A @ w1) # innovation of the dominant direction +rank_order = np.argsort(factor) # dates sorted from worst to best market innovation + + +def moment_match(x, target_var): + x = x - x.mean(axis=0) + c = x.T @ x / len(x) + return x @ symmetric_inv_sqrt(c) * np.sqrt(target_var) + + +def summed_innovations(kind, seed): + rng = np.random.default_rng(seed) + if kind == "bootstrap": # current implementation + idx = rng.integers(0, T, size=(S, H)) + return e[idx].sum(axis=1) + if kind == "bootstrap+mm": + idx = rng.integers(0, T, size=(S, H)) + return moment_match(e[idx].sum(axis=1), H) + if kind == "balanced+mm": # every date used equally often + reps = int(np.ceil(S * H / T)) + idx = rng.permutation(np.tile(np.arange(T), reps))[: S * H].reshape(S, H) + return moment_match(e[idx].sum(axis=1), H) + if kind == "lhs-ranks+mm": # stratified on the market-factor rank, per day + u = qmc.LatinHypercube(d=H, scramble=True, seed=seed).random(S) + idx = rank_order[np.minimum((u * T).astype(int), T - 1)] + return moment_match(e[idx].sum(axis=1), H) + if kind == "sobol-ranks+mm": + u = qmc.Sobol(d=H, scramble=True, seed=seed).random(S) + idx = rank_order[np.minimum((u * T).astype(int), T - 1)] + return moment_match(e[idx].sum(axis=1), H) + if kind == "windows+mm": # deterministic: all overlapping h-day windows + idx = np.arange(T - H + 1)[:, None] + np.arange(H)[None, :] + return moment_match(e[idx].sum(axis=1), H) + if kind == "windows": + idx = np.arange(T - H + 1)[:, None] + np.arange(H)[None, :] + return e[idx].sum(axis=1) + raise ValueError(kind) + + +def scenarios(kind, seed): + return mu_h + summed_innovations(kind, seed) @ A.T + + +def solve_lp(scen): + spec = ProblemSpec(symbols=symbols, prices=prices, scenarios=scen, sectors=sectors, cash=W, holdings=np.zeros(N), + confidence=BETA, cvar_limit=LAMBDA, objective="max_return", constraints=OptimizationConstraints(), + costs=TransactionCosts(proportional_buy=0.0, proportional_sell=0.0)) + prob = MeanCVaRProblem(spec) + model = prob.build() + model.var_types = ["C"] * len(model.var_types) + sol = HighsSolver().solve(model, time_limit=300) + n = sol.x[prob.i_n] + pnl = scen @ (n * prices) + return n, float(pnl.mean()), scenario_cvar(-pnl, BETA)[1] + + +# common evaluation set (plain bootstrap, independent seed) +rng_eval = np.random.default_rng(987654321) +eval_scen = mu_h + e[rng_eval.integers(0, T, size=(S_EVAL, H))].sum(axis=1) @ A.T +print(f"evaluation set: {S_EVAL} draws, mean of column means {eval_scen.mean():.2e}") + + +def true_stats(n): + pnl = eval_scen @ (n * prices) + return float(pnl.mean()), scenario_cvar(-pnl, BETA)[1] + + +results = {} +KINDS = sys.argv[5].split(",") if len(sys.argv) > 5 else ["bootstrap", "bootstrap+mm", "balanced+mm", "lhs-ranks+mm", "sobol-ranks+mm"] +for kind in KINDS: + rows, t0 = [], time.time() + for seed in range(1 if kind.startswith("windows") else N_SEEDS): + n, epnl_in, cvar_in = solve_lp(scenarios(kind, seed)) + epnl_t, cvar_t = true_stats(n) + rows.append({"epnl_in": epnl_in, "cvar_in": cvar_in, "epnl_true": epnl_t, "cvar_true": cvar_t, "n_pos": int((n > 1e-6).sum()), "w": n * prices / W}) + df = pd.DataFrame(rows) + ws = np.vstack(df["w"].values) + pair = [np.abs(ws[i] - ws[j]).sum() for i in range(len(ws)) for j in range(i + 1, len(ws))] + results[kind] = { + "E[P&L] in-sample mean": df.epnl_in.mean(), "E[P&L] in-sample sd": df.epnl_in.std(), + "E[P&L] true mean": df.epnl_true.mean(), "E[P&L] true sd": df.epnl_true.std(), + "true CVaR mean": df.cvar_true.mean(), "true CVaR sd": df.cvar_true.std(), + "CVaR excess over limit (%)": 100 * (df.cvar_true.mean() / LAMBDA - 1), + "positions": df.n_pos.mean(), "turnover between seeds (%)": 100 * np.mean(pair) if pair else float("nan"), "seconds": time.time() - t0, + } + print(f"{kind:15s} done in {time.time()-t0:.0f}s") + +out = pd.DataFrame(results).T +pd.set_option("display.width", 250); pd.set_option("display.float_format", lambda v: f"{v:10.2f}") +print(f"\nEMPIRICAL model, continuous (weights) problem, CVaR limit {LAMBDA:.0f} CHF, beta={BETA}") +print(out.T.to_string()) diff --git a/scripts/experiments/gurobi_ref.py b/scripts/experiments/gurobi_ref.py new file mode 100644 index 0000000..b524de2 --- /dev/null +++ b/scripts/experiments/gurobi_ref.py @@ -0,0 +1,46 @@ +"""Exact Gaussian mean-CVaR problem in integer shares as a MISOCP with Gurobi (validation of the cutting planes).""" +import sys, time +import numpy as np, gurobipy as gp +from scipy.stats import norm +from quantcore.config import get_settings +from quantcore.data.market_data import MarketDataService +from quantcore.optimization.service import OptimizationService, OptimizationRequest +from quantcore.optimization.problem import OptimizationConstraints +from quantcore.models.covariance import CovarianceModel + +N_ASSETS = int(sys.argv[1]) if len(sys.argv) > 1 else 30 +BETA = float(sys.argv[2]) if len(sys.argv) > 2 else 0.95 +W, H = 1_000_000.0, 5 +LAMBDA = 0.05 * W +settings = get_settings(); md = MarketDataService(settings); svc = OptimizationService(settings, md) +req = OptimizationRequest(max_assets=N_ASSETS, scenario_model="gbm", solver="highs", cash=W) +symbols = svc.candidate_symbols(req) +mu_ann, _, _ = svc.expected_returns(req, symbols) +r = md.returns(symbols, currency="base", freq="daily").dropna() +cov_h = CovarianceModel.from_dict(req.covariance).estimate(r).values * H +mu_h = (mu_ann / 252).reindex(symbols).values * H +prices = md.latest_prices(symbols, currency="base").reindex(symbols).values +sectors = md.sectors(symbols).fillna("Unknown").tolist() +N = len(symbols); k = norm.pdf(norm.ppf(BETA)) / (1 - BETA) +L = np.linalg.cholesky(cov_h) + +for integer in (True, False): + t0 = time.time() + m = gp.Model(); m.Params.OutputFlag = 0; m.Params.MIPGap = 1e-9; m.Params.MIPGapAbs = 1e-6 + ub = np.floor(0.10 * W / prices) + n = m.addMVar(N, vtype="I" if integer else "C", lb=0, ub=ub) + t = m.addVar(lb=0) + y = m.addMVar(N, lb=-gp.GRB.INFINITY) # y = L' v -> ||y|| = sqrt(v' Sigma v) + v = prices * n + m.addConstr(y == L.T @ v) + m.addConstr(y @ y <= t * t) # second-order cone + m.addConstr(k * t <= LAMBDA + mu_h @ v) # Gaussian CVaR <= Lambda + m.addConstr(prices @ n <= W) + for sec in sorted(set(sectors)): + mask = np.array([s == sec for s in sectors], dtype=float) + m.addConstr((prices * mask) @ n <= OptimizationConstraints().max_sector_weight * W) + m.setObjective(mu_h @ v, gp.GRB.MAXIMIZE) + m.optimize() + nn = n.X; vv = prices * nn; sd = np.sqrt(vv @ cov_h @ vv) + print(f"{'integer' if integer else 'continuous':10s} N={N} beta={BETA}: status {m.Status}, E[P&L]={mu_h @ vv:.2f} CHF, " + f"CVaR={-mu_h @ vv + k * sd:.2f}, positions={(nn > 0.5).sum()}, {time.time()-t0:.1f}s") diff --git a/scripts/experiments/is_noise.py b/scripts/experiments/is_noise.py new file mode 100644 index 0000000..085e3fd --- /dev/null +++ b/scripts/experiments/is_noise.py @@ -0,0 +1,173 @@ +"""Importance sampling for the mean-CVaR LP (weights): two-stage exponential tilt along the loss direction of a +first-pass solution, defensive mixture with the nominal density, self-normalised weights, weighted moment matching, +exact mean in the objective. Compared with Sobol+mm at equal S against the exact Gaussian optimum.""" +import sys, time +import numpy as np, pandas as pd +import scipy.sparse as sps +from scipy.optimize import milp, LinearConstraint, Bounds +from scipy.stats import norm, qmc +from quantcore.config import get_settings +from quantcore.data.market_data import MarketDataService +from quantcore.optimization.service import OptimizationService, OptimizationRequest +from quantcore.optimization.problem import OptimizationConstraints +from quantcore.models.covariance import CovarianceModel, symmetric_inv_sqrt + +N_ASSETS = int(sys.argv[1]); SIZES = [int(x) for x in sys.argv[2].split(",")]; N_SEEDS = int(sys.argv[3]) +BETAS = [float(x) for x in sys.argv[4].split(",")] +W = 1_000_000.0; LAMBDA = 0.05 * W; H = 5; WMAX = 0.10; SMAX = 0.25 +settings = get_settings(); md = MarketDataService(settings); svc = OptimizationService(settings, md) +req = OptimizationRequest(max_assets=N_ASSETS, solver="highs", cash=W) +symbols = svc.candidate_symbols(req) +mu_ann, _, _ = svc.expected_returns(req, symbols) +r = md.returns(symbols, currency="base", freq="daily").dropna() +cov_h = CovarianceModel.from_dict(req.covariance).estimate(r).values * H +mu_h = (mu_ann / 252).reindex(symbols).values * H +prices = md.latest_prices(symbols, currency="base").reindex(symbols).values +sectors = md.sectors(symbols).fillna("Unknown").tolist() +N = len(symbols) +vals, vecs = np.linalg.eigh(cov_h); o = np.argsort(vals)[::-1]; A = vecs[:, o] * np.sqrt(vals[o]) +sector_rows = [(np.array([s == sec for s in sectors], dtype=float)) for sec in sorted(set(sectors))] + + +def k_of(beta): + return norm.pdf(norm.ppf(beta)) / (1 - beta) + + +def true_stats(n, beta): + v = n * prices; mean = v @ mu_h; sd = np.sqrt(v @ cov_h @ v) + return mean, -mean + k_of(beta) * sd, sd + + +def weighted_mm(z, p): + m = p @ z + zc = z - m + c = (zc * p[:, None]).T @ zc + return zc @ symmetric_inv_sqrt(c) + + +def solve_lp(scen, p, beta): + """max mu_h'(n*P) s.t. RU-CVaR with scenario probabilities p <= Lambda, budget, caps, sectors; n continuous.""" + S = len(scen) + nv = N + 1 + S + c = np.zeros(nv); c[:N] = -(prices * mu_h) # exact model mean in the objective + G = scen * prices[None, :] + rows = [np.repeat(np.arange(S), N), np.arange(S), np.arange(S)] + cols = [np.tile(np.arange(N), S), np.full(S, N), N + 1 + np.arange(S)] + vals_ = [G.ravel(), np.ones(S), np.ones(S)] + lb = [np.zeros(S)]; ub = [np.full(S, np.inf)] + ri = S + rows.append(np.concatenate([[ri], np.full(S, ri)])); cols.append(np.concatenate([[N], N + 1 + np.arange(S)])) + vals_.append(np.concatenate([[1.0], p / (1 - beta)])); lb.append([-np.inf]); ub.append([LAMBDA]); ri += 1 + rows.append(np.full(N, ri)); cols.append(np.arange(N)); vals_.append(prices); lb.append([-np.inf]); ub.append([W]); ri += 1 + for mask in sector_rows: + rows.append(np.full(N, ri)); cols.append(np.arange(N)); vals_.append(prices * mask); lb.append([-np.inf]); ub.append([SMAX * W]); ri += 1 + Am = sps.csr_matrix((np.concatenate(vals_), (np.concatenate(rows), np.concatenate(cols))), shape=(ri, nv)) + vlb = np.zeros(nv); vub = np.full(nv, np.inf); vub[:N] = WMAX * W / prices; vlb[N] = -np.inf + res = milp(c, constraints=LinearConstraint(Am, np.concatenate(lb), np.concatenate(ub)), integrality=np.zeros(nv), + bounds=Bounds(vlb, vub)) + n = res.x[:N]; alpha = res.x[N] + return n, alpha + + +def scenarios_sobol(S, seed): + z = norm.ppf(qmc.Sobol(d=N, scramble=True, seed=seed).random(S)) + p = np.full(S, 1.0 / S) + return mu_h + weighted_mm(z, p) @ A.T, p + + +def scenarios_is(S, seed, beta, n1, eps=0.5, shift=None): + """Mixture proposal: first eps*S Sobol points shifted by theta along the loss direction of n1, rest nominal.""" + v = n1 * prices + a = A.T @ v # loss = -v'mu - a'Z + d = -a / np.linalg.norm(a) # unit direction of increasing loss in Z-space + theta = (shift if shift is not None else norm.ppf(beta)) * d + z = norm.ppf(qmc.Sobol(d=N, scramble=True, seed=seed).random(S)) + m = int(round(eps * S)) + z[:m] += theta + # likelihood ratio f/g with g = (1-eps) f + eps f(.-theta): 1 / ((1-eps) + eps exp(theta'z - |theta|^2/2)) + lr = 1.0 / ((1 - eps) + eps * np.exp(z @ theta - theta @ theta / 2)) + p = lr / lr.sum() + ess = 1.0 / (p ** 2).sum() + return mu_h + weighted_mm(z, p) @ A.T, p, ess + + +def scenarios_is_multi(S, seed, n1, shifts=(1.6449, 2.3263), eps=(0.25, 0.25)): + """Multi-component defensive mixture: nominal + one shifted block per shift, all along the loss direction of n1.""" + v = n1 * prices + a = A.T @ v + d = -a / np.linalg.norm(a) + z = norm.ppf(qmc.Sobol(d=N, scramble=True, seed=seed).random(S)) + thetas = [sh * d for sh in shifts] + start = 0 + for th, e in zip(thetas, eps): + m = int(round(e * S)); z[start:start + m] += th; start += m + dens = (1 - sum(eps)) * np.ones(S) + for th, e in zip(thetas, eps): + dens = dens + e * np.exp(z @ th - th @ th / 2) + lr = 1.0 / dens + p = lr / lr.sum() + return mu_h + weighted_mm(z, p) @ A.T, p, 1.0 / (p ** 2).sum() + + +def exact_reference(beta, tol=1e-7): + k = k_of(beta) + cuts = [] + ub = WMAX * W / prices + c = -(prices * mu_h) + base_rows = [prices] + [prices * m for m in sector_rows] + base_ub = [W] + [SMAX * W] * len(sector_rows) + for it in range(500): + Am = np.vstack(base_rows + [g for g, _ in cuts]); hi = np.array(base_ub + [b for _, b in cuts]) + res = milp(c, constraints=LinearConstraint(Am, np.full(len(hi), -np.inf), hi), integrality=np.zeros(N), bounds=Bounds(np.zeros(N), ub)) + x = res.x; mean, cvar, sd = true_stats(x, beta) + if cvar <= LAMBDA * (1 + tol): + return x, mean + v = x * prices + grad = -prices * mu_h + k * prices * (cov_h @ v) / sd + cuts.append((grad, LAMBDA - cvar + grad @ x)) + raise RuntimeError + + +market_w = md.market_weights(symbols).reindex(symbols).fillna(0.0).values +market_n = market_w * W / prices # cap-weighted candidate portfolio (continuous shares) +KINDS = sys.argv[5].split(",") if len(sys.argv) > 5 else ["sobol+mm", "sobol+mm+IS(eps=.5)", "IS-market(z_b)", "IS-market(2.33)"] +pd.set_option("display.width", 250); pd.set_option("display.float_format", lambda v: f"{v:10.2f}") +for beta in BETAS: + x_ref, mean_ref = exact_reference(beta) + print(f"\n===== beta={beta}: exact optimum E[P&L]={mean_ref:.2f} CHF, CVaR limit {LAMBDA:.0f}") + for S in SIZES: + results = {} + for kind in KINDS: + rows = [] + t0 = time.time() + for seed in range(N_SEEDS): + if kind == "sobol+mm": + scen, p = scenarios_sobol(S, seed); ess = S + n, alpha = solve_lp(scen, p, beta) + elif kind == "IS-market(mix)": + scen, p, ess = scenarios_is_multi(S, seed + 1000, market_n) + n, alpha = solve_lp(scen, p, beta) + elif kind.startswith("IS-market"): + shift = norm.ppf(beta) if "z_b" in kind else 2.33 + scen, p, ess = scenarios_is(S, seed + 1000, beta, market_n, eps=0.5, shift=shift) + n, alpha = solve_lp(scen, p, beta) + else: + eps = 0.5 if ".5" in kind else 0.3 + scen1, p1 = scenarios_sobol(S, seed) # stage 1: direction from a plain solve + n1, _ = solve_lp(scen1, p1, beta) + scen, p, ess = scenarios_is(S, seed + 1000, beta, n1, eps=eps) + n, alpha = solve_lp(scen, p, beta) + mean_t, cvar_t, _ = true_stats(n, beta) + L = -(scen * prices) @ n + rows.append({"epnl_true": mean_t, "cvar_true": cvar_t, "w": n * prices / W, "ess": ess, + "tail_pts": int((L > alpha).sum())}) + df = pd.DataFrame(rows); ws = np.vstack(df["w"].values) + pair = [np.abs(ws[i] - ws[j]).sum() for i in range(len(ws)) for j in range(i + 1, len(ws))] + results[kind] = { + "true E[P&L] mean": df.epnl_true.mean(), "loss vs optimum (CHF)": mean_ref - df.epnl_true.mean(), + "true CVaR excess (%)": 100 * (df.cvar_true.mean() / LAMBDA - 1), "true CVaR sd (% of limit)": 100 * df.cvar_true.std() / LAMBDA, + "turnover between seeds (%)": 100 * np.mean(pair), "turnover to optimum (%)": 100 * np.mean(np.abs(ws - x_ref * prices / W).sum(axis=1)), + "scenarios beyond VaR": df.tail_pts.mean(), "ESS": df.ess.mean(), "seconds": time.time() - t0, + } + print(f"--- S={S}") + print(pd.DataFrame(results).to_string()) diff --git a/scripts/experiments/rank_and_size.py b/scripts/experiments/rank_and_size.py new file mode 100644 index 0000000..4089104 --- /dev/null +++ b/scripts/experiments/rank_and_size.py @@ -0,0 +1,125 @@ +"""Two questions about large candidate sets. + +A. Conditioning: with ``N`` stocks and an estimator whose effective window is ``T_eff < N`` the + covariance is (numerically) singular. Does the optimiser exploit the near-null directions -- + does it build a portfolio that looks riskless in sample and is not? + +B. How many scenarios does a problem with ``N`` assets need? Measured against the exact Gaussian + optimum (no scenarios) for several ``N`` and ``S``. + +Usage: python scripts/experiments/rank_and_size.py [A|B] [sizes] +""" +import sys +import time + +import numpy as np +import pandas as pd + +from quantcore.config import get_settings +from quantcore.data.market_data import MarketDataService +from quantcore.measurement.estimators import effective_observations +from quantcore.models.covariance import CovarianceModel +from quantcore.optimization.service import OptimizationRequest, OptimizationService + +PART = sys.argv[1] if len(sys.argv) > 1 else "A" +SIZES = [int(x) for x in sys.argv[2].split(",")] if len(sys.argv) > 2 else [30, 100, 300, 745] +W, H, BETA = 1_000_000.0, 5, 0.95 + +settings = get_settings() +md = MarketDataService(settings) +svc = OptimizationService(settings, md) +caps = md.market_caps().sort_values(ascending=False) +pd.set_option("display.width", 240) + + +def universe(n): + return caps.index[:n].tolist() + + +# ============================================================================================= +if PART == "A": + rows = [] + for n in SIZES: + symbols = universe(n) + r = md.returns(symbols, currency="base", freq="daily").dropna() + for label, model in (("ewm l=0.99", CovarianceModel("ewm", lam=0.99, shrinkage=0.0)), + ("ewm l=0.99 + auto", CovarianceModel("ewm", lam=0.99, shrinkage="auto")), + ("rolling 200", CovarianceModel("rolling", window=200, shrinkage=0.0)), + ("rolling 200 + auto", CovarianceModel("rolling", window=200, shrinkage="auto"))): + t0 = time.time() + cov = model.estimate(r).values + vals = np.linalg.eigvalsh(cov) + t_eff = effective_observations(model.method, model.window, model.lam) + scale = np.diag(cov).mean() + rows.append({ + "N": n, "estimator": label, "T_eff": round(t_eff), + "shrinkage": round(model.intensity(r), 3), + "min eig / mean var": vals.min() / scale, "cond": vals.max() / max(vals.min(), 1e-300), + "eigs < 1e-6 of max": int((vals < 1e-6 * vals.max()).sum()), + "s": round(time.time() - t0, 1), + }) + print("A1. conditioning of the covariance estimate\n") + print(pd.DataFrame(rows).to_string(index=False, float_format=lambda v: f"{v:.3g}")) + + # does a singular estimate let the optimiser build a 'riskless' portfolio? + print("\nA2. minimum-variance portfolio under the caps: in-sample vs out-of-sample volatility") + out = [] + for n in SIZES: + symbols = universe(n) + r = md.returns(symbols, currency="base", freq="daily").dropna() + truth = CovarianceModel("rolling", window=len(r), shrinkage=0.0).estimate(r).values # all history + for label, model in (("ewm l=0.99", CovarianceModel("ewm", lam=0.99, shrinkage=0.0)), + ("ewm l=0.99 + auto", CovarianceModel("ewm", lam=0.99, shrinkage="auto"))): + cov = model.estimate(r).values + # long-only, fully invested, at most 10 % per name: minimise w'Cw by projected gradient + w = np.full(n, 1.0 / n) + cap = max(0.10, 1.5 / n) + for _ in range(4000): + g = cov @ w + w = w - g / (np.abs(g).max() + 1e-18) * 0.01 + w = np.clip(w, 0, cap) + if w.sum() <= 0: + w = np.full(n, 1.0 / n) + w = w / w.sum() + vol_in = np.sqrt(w @ cov @ w * 252) * 100 + vol_out = np.sqrt(w @ truth @ w * 252) * 100 + out.append({"N": n, "estimator": label, "in-sample vol (%)": vol_in, + "long-history vol (%)": vol_out, "understated by": f"{vol_out / vol_in:.2f}x", + "positions > 0.1 %": int((w > 0.001).sum())}) + print(pd.DataFrame(out).to_string(index=False, float_format=lambda v: f"{v:.2f}")) + +# ============================================================================================= +if PART == "B": + scen_sizes = [int(x) for x in sys.argv[3].split(",")] if len(sys.argv) > 3 else [512, 2048, 8192] + seeds = int(sys.argv[4]) if len(sys.argv) > 4 else 6 + solver = sys.argv[5] if len(sys.argv) > 5 else "highs" + rows = [] + for n in SIZES: + symbols = universe(n) + base = dict(symbols=symbols, cash=W, horizon="week", cvar_limit=0.05 * W, scenario_model="gbm", + solver=solver, confidence=BETA, costs={"proportional_buy": 0.0, "proportional_sell": 0.0}) + t0 = time.time() + exact = svc.optimize(OptimizationRequest(**{**base, "scenario_model": "gaussian_exact", "solver": "highs"})) + t_exact = time.time() - t0 + ref_w = {p["symbol"]: p["weight"] for p in exact.positions} + for S in scen_sizes: + ws, pnls = [], [] + t1 = time.time() + for seed in range(seeds): + res = svc.optimize(OptimizationRequest(**{**base, "n_scenarios": S, "seed": seed})) + if res.status != "Optimal": + continue + ws.append({p["symbol"]: p["weight"] for p in res.positions}) + pnls.append(res.expected_pnl) + def turn(a, b): + keys = set(a) | set(b) + return sum(abs(a.get(k, 0) - b.get(k, 0)) for k in keys) + pair = [turn(ws[i], ws[j]) for i in range(len(ws)) for j in range(i + 1, len(ws))] + rows.append({"N": n, "S": S, "S/N": S / n, + "turnover between seeds (%)": 100 * np.mean(pair), + "turnover to exact (%)": 100 * np.mean([turn(w, ref_w) for w in ws]), + "E[P&L] mean": np.mean(pnls), "E[P&L] sd": np.std(pnls, ddof=1), + "exact E[P&L]": exact.expected_pnl, "s per solve": (time.time() - t1) / max(len(ws), 1)}) + print(pd.DataFrame(rows[-1:]).to_string(index=False, float_format=lambda v: f"{v:.2f}"), flush=True) + print("\nB. scenarios needed as the candidate set grows (exact Gaussian reference)\n") + print(pd.DataFrame(rows).to_string(index=False, float_format=lambda v: f"{v:.2f}")) diff --git a/scripts/experiments/rowgen.py b/scripts/experiments/rowgen.py new file mode 100644 index 0000000..3eb2a73 --- /dev/null +++ b/scripts/experiments/rowgen.py @@ -0,0 +1,115 @@ +"""Tail-row generation for the RU mean-CVaR MILP: solve on a working set of scenarios, add violated ones, repeat. + +Full problem (new mode, proportional buy costs c): max (g_full - c p)'n s.t. + g_s'n - c p'n + alpha + u_s >= 0 (all s) [u_s >= L_s - alpha, L_s = -(g_s - c p)'n] + alpha + sum_s u_s / ((1-beta) S) <= Lambda + (1 + c) p'n <= W, p_i n_i <= w_max W, sector caps, n integer >= 0 +Restricted problem: rows only for s in the working set (u_s = 0 elsewhere) -> a relaxation; optimal for the +full problem as soon as no omitted scenario has L_s > alpha*. +""" +import sys, time +import numpy as np +import scipy.sparse as sps +from scipy.optimize import milp, LinearConstraint, Bounds +from scipy.stats import norm, qmc +from quantcore.config import get_settings +from quantcore.data.market_data import MarketDataService +from quantcore.optimization.service import OptimizationService, OptimizationRequest +from quantcore.optimization.problem import MeanCVaRProblem, ProblemSpec, OptimizationConstraints, TransactionCosts +from quantcore.optimization.solvers import HighsSolver +from quantcore.models.covariance import CovarianceModel, symmetric_inv_sqrt + +N_ASSETS = int(sys.argv[1]); SIZES = [int(x) for x in sys.argv[2].split(",")] +FULL = len(sys.argv) > 3 and sys.argv[3] == "full" +BETA = 0.95; W = 1_000_000.0; LAMBDA = 0.05 * W; H = 5; C = 0.001; WMAX = 0.10; SMAX = 0.25; GAP = 1e-4 +settings = get_settings(); md = MarketDataService(settings); svc = OptimizationService(settings, md) +req = OptimizationRequest(max_assets=N_ASSETS, solver="highs", cash=W) +symbols = svc.candidate_symbols(req) +mu_ann, _, _ = svc.expected_returns(req, symbols) +r = md.returns(symbols, currency="base", freq="daily").dropna() +cov_h = CovarianceModel.from_dict(req.covariance).estimate(r).values * H +mu_h = (mu_ann / 252).reindex(symbols).values * H +prices = md.latest_prices(symbols, currency="base").reindex(symbols).values +sectors = md.sectors(symbols).fillna("Unknown").tolist() +N = len(symbols) +vals, vecs = np.linalg.eigh(cov_h); o = np.argsort(vals)[::-1]; A = vecs[:, o] * np.sqrt(vals[o]) + + +def scen_sobol_mm(S, seed=0): + z = norm.ppf(qmc.Sobol(d=N, scramble=True, seed=seed).random(S)) + z = z - z.mean(0); z = z @ symmetric_inv_sqrt(z.T @ z / S) + return np.expm1(mu_h + z @ A.T) # simple returns, as in production + + +def solve_restricted(gain, work, S_full, mean_gain): + """MILP on the working set `work` (indices into gain rows). Returns n, alpha, objective, seconds.""" + K = len(work) + p = prices + # variables: n (N), alpha (1), u (K) + nv = N + 1 + K + c = np.zeros(nv); c[:N] = -(mean_gain - C * p) # minimise negative expected P&L (net of costs) + rows, cols, vals_ = [], [], [] + lb, ub = [], [] + ri = 0 + G = gain[work] - C * p[None, :] # net gain per share in the working scenarios + # scenario rows: G_s' n + alpha + u_s >= 0 + rows.extend(np.repeat(np.arange(K), N)); cols.extend(np.tile(np.arange(N), K)); vals_.extend(G.ravel()) + rows.extend(np.arange(K)); cols.extend([N] * K); vals_.extend([1.0] * K) + rows.extend(np.arange(K)); cols.extend(N + 1 + np.arange(K)); vals_.extend([1.0] * K) + lb.extend([0.0] * K); ub.extend([np.inf] * K); ri = K + # cvar row: alpha + sum u / ((1-beta) S_full) <= Lambda + rows.append(ri); cols.append(N); vals_.append(1.0) + rows.extend([ri] * K); cols.extend(N + 1 + np.arange(K)); vals_.extend([1.0 / ((1 - BETA) * S_full)] * K) + lb.append(-np.inf); ub.append(LAMBDA); ri += 1 + # budget (1+c) p'n <= W + rows.extend([ri] * N); cols.extend(range(N)); vals_.extend((1 + C) * p); lb.append(-np.inf); ub.append(W); ri += 1 + # sector caps + for sec in sorted(set(sectors)): + idx = [i for i, s in enumerate(sectors) if s == sec] + rows.extend([ri] * len(idx)); cols.extend(idx); vals_.extend(p[idx]); lb.append(-np.inf); ub.append(SMAX * W); ri += 1 + Am = sps.csr_matrix((vals_, (rows, cols)), shape=(ri, nv)) + vlb = np.zeros(nv); vub = np.full(nv, np.inf); vub[:N] = np.floor(WMAX * W / p + 1e-9); vlb[N] = -np.inf + integrality = np.zeros(nv); integrality[:N] = 1 + t0 = time.time() + res = milp(c, constraints=LinearConstraint(Am, np.array(lb), np.array(ub)), integrality=integrality, + bounds=Bounds(vlb, vub), options={"mip_rel_gap": GAP, "time_limit": 1800}) + n = np.rint(res.x[:N]); alpha = res.x[N] + return n, alpha, -res.fun, time.time() - t0 + + +def row_generation(gain, verbose=True): + S = len(gain) + mean_gain = gain.mean(0) + p = prices + # initial working set: the worst 2 (1-beta) S scenarios of the equal-weight (cap-respecting) portfolio + n0 = np.floor(W / N / p) + L0 = -(gain - C * p) @ n0 + K0 = int(2 * (1 - BETA) * S) + work = np.argsort(L0)[-K0:] + t_solve = 0.0 + for it in range(1, 30): + n, alpha, obj, dt = solve_restricted(gain, work, S, mean_gain) + t_solve += dt + L = -(gain - C * p) @ n + viol = np.setdiff1d(np.where(L > alpha + 1e-9)[0], work) + if verbose: + print(f" iter {it}: |work|={len(work)}, MILP {dt:.1f}s, obj {obj:.2f}, alpha {alpha:.1f}, violated {len(viol)}", flush=True) + if len(viol) == 0: + u = np.maximum(L - alpha, 0) + cvar = alpha + u.sum() / ((1 - BETA) * S) + return n, obj, cvar, it, t_solve + work = np.union1d(work, viol) + raise RuntimeError("no convergence") + + +print(f"N={N}, beta={BETA}, gap={GAP}") +for S in SIZES: + gain = scen_sobol_mm(S) * prices[None, :] + t0 = time.time() + n, obj, cvar, iters, t_solve = row_generation(gain) + t_rg = time.time() - t0 + print(f"S={S:>7}: row generation {iters} iterations, {t_rg:.1f}s total ({t_solve:.1f}s in MILPs), E[P&L]={obj:.2f}, CVaR={cvar:.1f}, positions={(n>0).sum()}", flush=True) + if FULL: + t0 = time.time() + n_f, alpha_f, obj_f, dt = solve_restricted(gain, np.arange(S), S, gain.mean(0)) + print(f"S={S:>7}: full MILP {dt:.1f}s, E[P&L]={obj_f:.2f}, same shares: {np.array_equal(n, n_f)}, max |dn| = {np.abs(n - n_f).max():.0f}", flush=True) diff --git a/scripts/experiments/scaling.py b/scripts/experiments/scaling.py new file mode 100644 index 0000000..1b71b09 --- /dev/null +++ b/scripts/experiments/scaling.py @@ -0,0 +1,54 @@ +"""Time scaling of the mean-CVaR LP / MILP in the number of scenarios S (HiGHS locally, optionally cuOpt).""" +import sys, time, json, zlib +import numpy as np +from scipy.stats import norm, qmc +from quantcore.config import get_settings +from quantcore.data.market_data import MarketDataService +from quantcore.optimization.service import OptimizationService, OptimizationRequest +from quantcore.optimization.problem import MeanCVaRProblem, ProblemSpec, OptimizationConstraints, TransactionCosts +from quantcore.optimization.solvers import HighsSolver, CuOptSolver, to_cuopt_payload +from quantcore.models.covariance import CovarianceModel, symmetric_inv_sqrt + +N_ASSETS = int(sys.argv[1]); SIZES = [int(x) for x in sys.argv[2].split(",")]; MODE = sys.argv[3] # highs | cuopt +BETA = 0.95; W = 1_000_000.0; LAMBDA = 0.05 * W; H = 5 +settings = get_settings(); md = MarketDataService(settings); svc = OptimizationService(settings, md) +req = OptimizationRequest(max_assets=N_ASSETS, solver="highs", cash=W) +symbols = svc.candidate_symbols(req) +mu_ann, _, _ = svc.expected_returns(req, symbols) +r = md.returns(symbols, currency="base", freq="daily").dropna() +cov_h = CovarianceModel.from_dict(req.covariance).estimate(r).values * H +mu_h = (mu_ann / 252).reindex(symbols).values * H +prices = md.latest_prices(symbols, currency="base").reindex(symbols).values +sectors = md.sectors(symbols).fillna("Unknown").tolist() +N = len(symbols) +vals, vecs = np.linalg.eigh(cov_h); o = np.argsort(vals)[::-1]; A = vecs[:, o] * np.sqrt(vals[o]) + +def scen_sobol_mm(S, seed=0): + z = norm.ppf(qmc.Sobol(d=N, scramble=True, seed=seed).random(S)) + z = z - z.mean(0); z = z @ symmetric_inv_sqrt(z.T @ z / S) + return mu_h + z @ A.T + +print(f"N={N}, beta={BETA}, mode={MODE}") +print(f"{'S':>8} {'nnz':>10} {'gen s':>7} {'build s':>8} {'LP s':>8} {'MILP s':>8} {'status':>10} {'E[P&L]':>9} {'json MB':>8} {'cuopt s':>8} {'cu solve':>9}") +for S in SIZES: + t0 = time.time(); scen = scen_sobol_mm(S); t_gen = time.time() - t0 + spec = ProblemSpec(symbols=symbols, prices=prices, scenarios=scen, sectors=sectors, cash=W, holdings=np.zeros(N), + confidence=BETA, cvar_limit=LAMBDA, objective="max_return", constraints=OptimizationConstraints(), + costs=TransactionCosts()) + prob = MeanCVaRProblem(spec) + t0 = time.time(); model = prob.build(); t_build = time.time() - t0 + nnz = model.A.nnz + row = f"{S:>8} {nnz:>10} {t_gen:>7.1f} {t_build:>8.1f}" + if MODE == "highs": + lp = MeanCVaRProblem(spec).build(); lp.var_types = ["C"] * len(lp.var_types) + t0 = time.time(); sol_lp = HighsSolver().solve(lp, time_limit=300); t_lp = time.time() - t0 + t0 = time.time(); sol = HighsSolver().solve(model, time_limit=300, mip_gap=1e-4); t_mip = time.time() - t0 + obj = prob.decode(sol.x)["expected_pnl"] if sol.x is not None else float("nan") + row += f" {t_lp:>8.1f} {t_mip:>8.1f} {sol.status:>10} {obj:>9.1f}" + else: + t0 = time.time(); payload = to_cuopt_payload(model, 300.0, {"mip_relative_gap": 1e-4, "log_to_console": False}) + body = json.dumps(payload); mb = len(body) / 1e6; t_json = time.time() - t0 + t0 = time.time(); sol = CuOptSolver(settings).solve(model, time_limit=300.0, extra_config={"mip_relative_gap": 1e-4, "log_to_console": False}); t_cu = time.time() - t0 + obj = prob.decode(sol.x)["expected_pnl"] if sol.x is not None else float("nan") + row += f" {'':>8} {'':>8} {sol.status:>10} {obj:>9.1f} {mb:>8.1f} {t_cu:>8.1f} {sol.solve_time:>9.1f}" + print(row, flush=True) diff --git a/scripts/experiments/scenario_noise.py b/scripts/experiments/scenario_noise.py new file mode 100644 index 0000000..efaa5d0 --- /dev/null +++ b/scripts/experiments/scenario_noise.py @@ -0,0 +1,177 @@ +"""Sampling noise of the mean-CVaR MILP under different scenario generators (GBM model). + +Reference: the exact Gaussian problem max mu'x s.t. -mu'x + k_beta * sqrt(x'Sigma x) <= Lambda, +caps, budget, integer shares -- solved with an outer-approximation (Kelley cutting planes) on the +same MILP (no scenarios at all). Under a normal model this is the mean-variance solution. +""" +import sys, time, json +import numpy as np, pandas as pd +from scipy.stats import norm, qmc +from scipy.optimize import milp, LinearConstraint, Bounds +import scipy.sparse as sps + +from quantcore.config import get_settings +from quantcore.data.market_data import MarketDataService +from quantcore.optimization.service import OptimizationService, OptimizationRequest +from quantcore.optimization.problem import MeanCVaRProblem, ProblemSpec, OptimizationConstraints, TransactionCosts +from quantcore.optimization.solvers import HighsSolver +from quantcore.models.covariance import CovarianceModel, symmetric_inv_sqrt + +N_ASSETS = int(sys.argv[1]) if len(sys.argv) > 1 else 30 +S = int(sys.argv[2]) if len(sys.argv) > 2 else 2048 +N_SEEDS = int(sys.argv[3]) if len(sys.argv) > 3 else 8 +INTEGER = (sys.argv[4] == "int") if len(sys.argv) > 4 else False +KINDS = sys.argv[5].split(",") if len(sys.argv) > 5 else ["mc", "antithetic", "mc+mm", "lhs", "sobol", "sobol+mm"] +BETA = float(sys.argv[6]) if len(sys.argv) > 6 else 0.95 +W = 1_000_000.0 +LAMBDA = 0.05 * W +H = 5 + +settings = get_settings() +md = MarketDataService(settings) +svc = OptimizationService(settings, md) +req = OptimizationRequest(max_assets=N_ASSETS, scenario_model="gbm", n_scenarios=S, solver="highs", cash=W) +symbols = svc.candidate_symbols(req) +mu_ann, _, _ = svc.expected_returns(req, symbols) +r = md.returns(symbols, currency="base", freq="daily").dropna() +cov_model = CovarianceModel.from_dict(req.covariance) +cov_d = cov_model.estimate(r).values +mu_h = (mu_ann / 252).reindex(symbols).values * H +cov_h = cov_d * H +prices = md.latest_prices(symbols, currency="base").reindex(symbols).values +sectors = md.sectors(symbols).fillna("Unknown").tolist() +N = len(symbols) +k_beta = norm.pdf(norm.ppf(BETA)) / (1 - BETA) +print(f"N={N} assets, S={S} scenarios, horizon {H} days, beta={BETA}, k_beta={k_beta:.4f}") +print("weekly vol range (%%): %.2f .. %.2f" % (100 * np.sqrt(np.diag(cov_h)).min(), 100 * np.sqrt(np.diag(cov_h)).max())) + +# eigen root (PCA ordering) and Cholesky root +vals, vecs = np.linalg.eigh(cov_h) +order = np.argsort(vals)[::-1] +A_pca = vecs[:, order] * np.sqrt(vals[order]) # columns = principal directions, largest first +A_chol = np.linalg.cholesky(cov_h) + + +def moment_match(z): + z = z - z.mean(axis=0) + c = z.T @ z / len(z) + return z @ symmetric_inv_sqrt(c) + + +def base_sample(kind, seed): + rng = np.random.default_rng(seed) + if kind == "mc": + return rng.standard_normal((S, N)), A_chol + if kind == "antithetic": + z = rng.standard_normal((S // 2, N)) + return np.vstack([z, -z]), A_chol + if kind == "mc+mm": + return moment_match(rng.standard_normal((S, N))), A_chol + if kind == "lhs": + u = qmc.LatinHypercube(d=N, scramble=True, seed=seed).random(S) + return norm.ppf(u), A_pca + if kind == "sobol": + u = qmc.Sobol(d=N, scramble=True, seed=seed).random(S) + return norm.ppf(u), A_pca + if kind == "sobol+mm": + u = qmc.Sobol(d=N, scramble=True, seed=seed).random(S) + return moment_match(norm.ppf(u)), A_pca + raise ValueError(kind) + + +def scenarios(kind, seed): + z, a = base_sample(kind, seed) + return mu_h + z @ a.T # S x N horizon log-returns (used linearly: exact Gaussian P&L) + + +def solve_milp(scen, integer): + spec = ProblemSpec(symbols=symbols, prices=prices, scenarios=scen, sectors=sectors, cash=W, + holdings=np.zeros(N), confidence=BETA, cvar_limit=LAMBDA, objective="max_return", + constraints=OptimizationConstraints(), costs=TransactionCosts(proportional_buy=0.0, proportional_sell=0.0)) + prob = MeanCVaRProblem(spec) + model = prob.build() + if not integer: + model.var_types = ["C"] * len(model.var_types) + sol = HighsSolver().solve(model, time_limit=300, mip_gap=1e-5) + n = np.rint(sol.x[prob.i_n]) if integer else sol.x[prob.i_n] + pnl = scen @ (n * prices) + from quantcore.measurement.risk import scenario_cvar + _, cvar_in = scenario_cvar(-pnl, BETA) + return n, float(pnl.mean()), cvar_in, sol + + +def true_stats(n): + v = n * prices + mean = v @ mu_h + sd = np.sqrt(v @ cov_h @ v) + return mean, -mean + k_beta * sd, sd + + +# ---- exact Gaussian reference by outer approximation on the same integer feasible set ---------------- +def exact_reference(tol=1e-6, max_iter=200, integer=True): + ub = np.floor(np.minimum(0.10, 1.0) * W / prices) + c = -(prices * mu_h) # minimise -E[P&L] + rows = [prices] # budget + lb = [-np.inf]; ubs = [W] + for sec in sorted(set(sectors)): # sector caps (defaults of OptimizationConstraints) + mask = np.array([s == sec for s in sectors], dtype=float) + rows.append(prices * mask); lb.append(-np.inf); ubs.append(OptimizationConstraints().max_sector_weight * W) + cuts = [] + n_prev = None + for it in range(max_iter): + A = np.vstack(rows + [cut[0] for cut in cuts]) + lo = np.array(lb + [-np.inf] * len(cuts)); hi = np.array(ubs + [cut[1] for cut in cuts]) + res = milp(c, constraints=LinearConstraint(A, lo, hi), integrality=np.ones(N) if integer else np.zeros(N), + bounds=Bounds(np.zeros(N), ub), options={"mip_rel_gap": 1e-9}) + n = np.round(res.x) if integer else res.x + mean, cvar, sd = true_stats(n) + if cvar <= LAMBDA + tol * LAMBDA: + return n, mean, cvar, it + 1 + v = n * prices + grad = -prices * mu_h + k_beta * prices * (cov_h @ v) / sd # d CVaR / d n + # cut: cvar(n_k) + grad'(n - n_k) <= Lambda -> grad'n <= Lambda - cvar(n_k) + grad'n_k + cuts.append((grad, LAMBDA - cvar + grad @ n)) + raise RuntimeError("no convergence") + + +t0 = time.time() +n_ref, mean_ref, cvar_ref, iters = exact_reference() +x_rel, mean_rel, cvar_rel, iters_rel = exact_reference(integer=False) +print(f"\nexact Gaussian reference (integer): E[P&L]={mean_ref:.2f} CHF, CVaR={cvar_ref:.2f}, " + f"{int((n_ref > 0).sum())} positions, {iters} cuts, {time.time()-t0:.1f}s") +print(f"continuous relaxation: E[P&L]={mean_rel:.2f} CHF, CVaR={cvar_rel:.2f}") + +results = {} +for kind in KINDS: + rows = [] + t0 = time.time() + for seed in range(N_SEEDS): + n, epnl, cvar_in, sol = solve_milp(scenarios(kind, seed), INTEGER) + mean_t, cvar_t, _ = true_stats(n) + rows.append({"seed": seed, "epnl_in": epnl, "cvar_in": cvar_in, "epnl_true": mean_t, "cvar_true": cvar_t, + "n_pos": int((n > 0).sum()), "shares": n, "status": sol.status}) + df = pd.DataFrame(rows) + shares = np.vstack(df["shares"].values) + # dissimilarity between seeds: mean over pairs of (value traded to go from one to the other) / W + pair_turn = [] + for i in range(len(shares)): + for j in range(i + 1, len(shares)): + pair_turn.append(np.abs(shares[i] - shares[j]) @ prices / W) + dist_ref = np.abs(shares - (n_ref if INTEGER else x_rel)) @ prices / W + results[kind] = { + "E[P&L] in-sample mean": df.epnl_in.mean(), "E[P&L] in-sample sd": df.epnl_in.std(), + "E[P&L] true mean": df.epnl_true.mean(), "E[P&L] true sd": df.epnl_true.std(), + "loss vs ref (CHF)": (mean_ref if INTEGER else mean_rel) - df.epnl_true.mean(), + "true CVaR mean": df.cvar_true.mean(), "true CVaR sd": df.cvar_true.std(), + "CVaR excess over limit (%)": 100 * (df.cvar_true.mean() / LAMBDA - 1), + "positions": df.n_pos.mean(), + "turnover between seeds (%)": 100 * np.mean(pair_turn), "turnover to ref (%)": 100 * dist_ref.mean(), + "seconds": time.time() - t0, + } + print(f"{kind:10s} done in {time.time()-t0:.0f}s statuses={df.status.unique().tolist()}") + +out = pd.DataFrame(results).T +pd.set_option("display.width", 250); pd.set_option("display.float_format", lambda v: f"{v:10.2f}") +print("\n%s problem; reference E[P&L] = %.2f CHF, CVaR limit = %.0f CHF (5%% of wealth), weekly" % ("INTEGER" if INTEGER else "CONTINUOUS (weights)", mean_ref if INTEGER else mean_rel, LAMBDA)) +print(out.T.to_string()) +out.to_csv(f"{'int' if INTEGER else 'cont'}_scenario_noise_N{N}_S{S}.csv") diff --git a/scripts/experiments/tilt_correlation.py b/scripts/experiments/tilt_correlation.py new file mode 100644 index 0000000..5ed11de --- /dev/null +++ b/scripts/experiments/tilt_correlation.py @@ -0,0 +1,155 @@ +"""What the importance-sampling tilt does with the correlation structure. + +1. The per-asset shift is proportional to ``Sigma v`` (covariance with the reference portfolio), + not to the assets' own volatilities. +2. The tilted set keeps the full covariance (an exponential tilt of a Gaussian is a mean shift; + moment matching makes it exact), so correlations are untouched. +3. How much tail coverage does a portfolio get whose loss direction differs from the reference's? + Measured for portfolios ranging from market-like to deliberately orthogonal. + +Usage: python scripts/experiments/tilt_correlation.py [N] [S] [SEEDS] [BETA] +""" +import sys + +import numpy as np +import pandas as pd +from scipy.stats import norm + +from quantcore.config import get_settings +from quantcore.data.market_data import MarketDataService +from quantcore.measurement.risk import scenario_cvar +from quantcore.models import sampling as smp +from quantcore.models.covariance import CovarianceModel +from quantcore.models.simulation import GBMSimulator, TiltSpec +from quantcore.optimization.service import OptimizationRequest, OptimizationService + +N_ASSETS = int(sys.argv[1]) if len(sys.argv) > 1 else 30 +S = int(sys.argv[2]) if len(sys.argv) > 2 else 4096 +N_SEEDS = int(sys.argv[3]) if len(sys.argv) > 3 else 12 +BETA = float(sys.argv[4]) if len(sys.argv) > 4 else 0.95 +W, H = 1_000_000.0, 5 + +settings = get_settings() +md = MarketDataService(settings) +svc = OptimizationService(settings, md) +req = OptimizationRequest(max_assets=N_ASSETS, scenario_model="gbm", n_scenarios=S, horizon="week", cvar_limit=0.05 * W) +symbols = svc.candidate_symbols(req) +mu_ann, _, _ = svc.expected_returns(req, symbols) +r = md.returns(symbols, currency="base", freq="daily").dropna() +cov_model = CovarianceModel.from_dict(req.covariance) +cov_h = cov_model.estimate(r).values * H +prices = md.latest_prices(symbols, currency="base").reindex(symbols).values +sectors = md.sectors(symbols).fillna("Unknown") +N = len(symbols) +vol_h = np.sqrt(np.diag(cov_h)) +market_w = svc.market_exposure(symbols) + +print(f"N={N}, S={S}, beta={BETA}, weekly horizon; average correlation " + f"{np.mean((cov_h / np.outer(vol_h, vol_h))[np.triu_indices(N, 1)]):.2f}") + +# --------------------------------------------------------------------------------------------- +# 1. what the tilt does per asset +# --------------------------------------------------------------------------------------------- +A = smp.pca_root(cov_h) +v_ref = market_w * W +d = smp.loss_direction(A, v_ref) +shift_returns = norm.ppf(BETA) * (A @ d) # the return shift of the tilted block +sigma_ref = np.sqrt(v_ref @ cov_h @ v_ref) +implied = -norm.ppf(BETA) * (cov_h @ v_ref) / sigma_ref # -z_beta * Sigma v / sigma +beta_i = (cov_h @ market_w) / (market_w @ cov_h @ market_w) # beta of each asset to the reference +table = pd.DataFrame({ + "weight (%)": market_w * 100, "weekly vol (%)": vol_h * 100, "beta to ref": beta_i, + "shift (%)": shift_returns * 100, "shift / own vol": shift_returns / vol_h, +}, index=symbols) +print("\n1. per-asset shift of the tilted block (z_beta along the loss direction)") +print(" max |A d - (-z Sigma v / sigma)| = %.2e (the shift *is* -z_beta Sigma v / sigma)" + % np.abs(shift_returns - implied).max()) +print(" corr(shift, beta) = %.4f, corr(shift, own vol) = %.4f" + % (np.corrcoef(shift_returns, beta_i)[0, 1], np.corrcoef(shift_returns, vol_h)[0, 1])) +show = table.sort_values("beta to ref") +print(pd.concat([show.head(4), show.tail(4)]).round(3).to_string()) + +# --------------------------------------------------------------------------------------------- +# 2. the tilted set keeps the covariance +# --------------------------------------------------------------------------------------------- +sim = GBMSimulator(cov_model) +plain = sim.simulate(r, n_scenarios=S, horizon_periods=H, mu=mu_ann / 252, seed=1) +tilted = sim.simulate(r, n_scenarios=S, horizon_periods=H, mu=mu_ann / 252, seed=1, + tilt=TiltSpec(exposure=v_ref, label="market")) +w = tilted.weights +centred = tilted.log_returns - w @ tilted.log_returns +cov_t = (centred * w[:, None]).T @ centred +corr_t = cov_t / np.sqrt(np.outer(np.diag(cov_t), np.diag(cov_t))) +corr_m = cov_h / np.outer(vol_h, vol_h) +# unweighted (i.e. ignoring the likelihood ratios) for comparison +cov_u = np.cov(tilted.log_returns, rowvar=False) +corr_u = cov_u / np.sqrt(np.outer(np.diag(cov_u), np.diag(cov_u))) +print("\n2. covariance of the tilted set (weighted with the likelihood ratios)") +print(" max |weighted cov - h Sigma| = %.2e" % np.abs(cov_t - cov_h).max()) +print(" max |weighted corr - model corr| = %.2e" % np.abs(corr_t - corr_m).max()) +print(" max |UNweighted corr - model| = %.2e (the raw tilted sample is not the model)" + % np.abs(corr_u - corr_m).max()) + +# --------------------------------------------------------------------------------------------- +# 3. coverage of portfolios that are not market-like +# --------------------------------------------------------------------------------------------- +def portfolio(kind: str) -> np.ndarray: + if kind == "market": + return market_w.copy() + if kind == "equal weight": + return np.full(N, 1.0 / N) + if kind == "optimised": + res = svc.optimize(OptimizationRequest(max_assets=N_ASSETS, scenario_model="gbm", n_scenarios=S, + horizon="week", cvar_limit=0.05 * W, cash=W, solver="highs", + confidence=BETA)) + held = {p["symbol"]: p["value"] for p in res.positions} + v = np.array([held.get(s, 0.0) for s in symbols]) + return v / v.sum() + if kind == "highest beta": + idx = np.argsort(beta_i)[-5:] + v = np.zeros(N); v[idx] = 1 / len(idx); return v + if kind == "lowest beta": + idx = np.argsort(beta_i)[:5] + v = np.zeros(N); v[idx] = 1 / len(idx); return v + if kind == "min variance": + inv = np.linalg.pinv(cov_h) @ np.ones(N) + v = np.maximum(inv, 0); return v / v.sum() + if kind == "least market-like": # maximise the angle to the reference loss direction, long only + c = cov_h @ market_w + v = np.zeros(N); v[np.argsort(c / vol_h)[:3]] = 1 / 3; return v + raise ValueError(kind) + + +kinds = ["market", "equal weight", "optimised", "highest beta", "min variance", "lowest beta", "least market-like"] +rows = [] +for kind in kinds: + wgt = portfolio(kind) + v = wgt * W + d_p = smp.loss_direction(A, v) + cos = float(d_p @ d) # alignment of the loss directions + losses_p, losses_t, cvars_p, cvars_t = [], [], [], [] + for seed in range(N_SEEDS): + pl = sim.simulate(r, n_scenarios=S, horizon_periods=H, mu=mu_ann / 252, seed=seed) + ti = sim.simulate(r, n_scenarios=S, horizon_periods=H, mu=mu_ann / 252, seed=seed, + tilt=TiltSpec(exposure=v_ref, label="market")) + lp = -(pl.simple_returns() @ v) + lt = -(ti.simple_returns() @ v) + cvars_p.append(scenario_cvar(lp, BETA)[1]) + cvars_t.append(scenario_cvar(lt, BETA, ti.probabilities)[1]) + var_p = np.quantile(lp, BETA) + losses_p.append((lp > var_p).sum()) + losses_t.append((lt > var_p).sum()) + exact = -(v @ (np.expm1(mu_ann.reindex(symbols).values / 252 * H))) + \ + norm.pdf(norm.ppf(BETA)) / (1 - BETA) * np.sqrt(v @ cov_h @ v) + rows.append({ + "cos to ref direction": cos, "tail points plain": np.mean(losses_p), "tail points tilted": np.mean(losses_t), + "CVaR sd plain (%)": 100 * np.std(cvars_p, ddof=1) / np.mean(cvars_p), + "CVaR sd tilted (%)": 100 * np.std(cvars_t, ddof=1) / np.mean(cvars_t), + "noise reduction": np.std(cvars_p, ddof=1) / max(np.std(cvars_t, ddof=1), 1e-12), + "CVaR bias tilted (%)": 100 * (np.mean(cvars_t) / exact - 1), + "CVaR bias plain (%)": 100 * (np.mean(cvars_p) / exact - 1), + }) +out = pd.DataFrame(rows, index=kinds) +pd.set_option("display.width", 220) +print(f"\n3. tail coverage by portfolio type ({N_SEEDS} seeds; 'cos' = alignment of loss directions)") +print(out.round(2).to_string()) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..8bf7505 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,62 @@ +"""Shared fixtures: a synthetic data store with 12 stocks in four currencies.""" + +from __future__ import annotations + +import numpy as np +import pandas as pd +import pytest + +from quantcore.config import Settings +from quantcore.data.market_data import MarketDataService +from quantcore.data.store import DataStore + +N, T = 12, 3000 +SYMBOLS = [f"S{i}" for i in range(N)] +CURRENCIES = ["CHF", "USD", "EUR", "GBP"] * 3 +SECTORS = ["Information Technology", "Financials", "Health Care", "Energy"] * 3 + + +def make_prices(seed: int = 0) -> tuple[pd.DataFrame, np.ndarray]: + rng = np.random.default_rng(seed) + dates = pd.bdate_range("2013-01-01", periods=T) + f = rng.standard_normal((T, 2)) * 0.01 + beta = rng.uniform(0.5, 1.5, size=(N, 2)) + eps = rng.standard_t(5, size=(T, N)) * 0.01 + r = f @ beta.T + eps + 0.0002 + return pd.DataFrame(100 * np.exp(np.cumsum(r, axis=0)), index=dates, columns=SYMBOLS), r + + +@pytest.fixture(scope="session") +def settings(tmp_path_factory) -> Settings: + return Settings(data_dir=tmp_path_factory.mktemp("data"), cuopt_url="http://127.0.0.1:9", solver_fallback=True) + + +@pytest.fixture(scope="session") +def store(settings) -> DataStore: + store = DataStore(settings) + store.ensure_dirs() + prices, _ = make_prices() + rng = np.random.default_rng(1) + uni = pd.DataFrame({ + "symbol": SYMBOLS, "name": [f"Company {s}" for s in SYMBOLS], "region": ["US"] * 6 + ["Europe"] * 6, + "country": ["United States"] * 6 + ["Switzerland"] * 6, "listing_currency": CURRENCIES, "exchange": "", + "exchange_code": "", "gics_sector": SECTORS, "gics_sub_industry": "", "yahoo_sector": "", "yahoo_industry": "", + "market_cap": 1e9, "market_cap_base": rng.uniform(1e9, 1e11, N), "shares_outstanding": rng.uniform(1e7, 1e9, N), + "last_price": prices.iloc[-1].values, "first_price_date": "2013-01-01", "price_coverage": 1.0, + "index_weight": np.nan, "source": "synthetic", "rank_in_region": range(1, N + 1), + }) + store.save_universe(uni) + store.save_prices(prices) + fx = pd.DataFrame({ + "USD": 0.85 * np.exp(np.cumsum(rng.standard_normal(T) * 0.004)), + "EUR": 0.95 * np.exp(np.cumsum(rng.standard_normal(T) * 0.003)), + "GBP": 1.10 * np.exp(np.cumsum(rng.standard_normal(T) * 0.004)), "CHF": 1.0, + }, index=prices.index) + store.save_fx(fx) + store.update_manifest(last_price_date=str(prices.index[-1].date()), n_universe=N) + return store + + +@pytest.fixture(scope="session") +def md(settings, store) -> MarketDataService: + return MarketDataService(settings, store) diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..00bf676 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,209 @@ +import numpy as np +import pytest +from fastapi.testclient import TestClient + +import quantcore.api.app as app_module + + +@pytest.fixture(scope="module") +def client(settings, store): + app_module.state = app_module.State(settings) + with TestClient(app_module.app) as c: + yield c + + +def test_health_and_universe(client): + h = client.get("/health").json() + assert h["status"] == "ok" and h["store"]["prices"] is True and h["cuopt"]["reachable"] is False + u = client.get("/universe", params={"region": "Europe"}).json() + assert len(u) == 6 and all(row["region"] == "Europe" for row in u) + assert client.get("/universe/S0").json()["symbol"] == "S0" + assert client.get("/universe/NOPE").status_code == 404 + + +def test_prices_returns_and_market(client): + p = client.get("/prices", params={"symbols": "S0,S1", "freq": "weekly", "start": "2020-01-01"}).json() + assert p["columns"] == ["S0", "S1"] and len(p["index"]) > 100 + r = client.get("/returns", params={"symbols": "S0", "currency": "local"}).json() + assert r["kind"] == "log_returns" + m = client.get("/market/index", params={"freq": "weekly"}).json() + assert "mean_ann" in m and m["n"] > 100 + w = client.get("/market/weights").json()["weights"] + assert abs(sum(w.values()) - 1) < 1e-6 + + +def test_risk_and_performance_endpoints(client): + body = {"symbols": ["S0", "S1", "S2"], "horizon": "week", "confidence": [0.95, 0.99]} + rows = client.post("/risk/assets", json=body).json() + assert len(rows) == 3 and rows[0]["cvar99_hist"] > rows[0]["var99_hist"] + corr = client.post("/risk/correlation", json={"symbols": ["S0", "S1"], "method": "ewm", "lam": 0.99}).json() + assert corr["data"][0][0] == pytest.approx(1.0) + pr = client.post("/risk/portfolio", json={"holdings": {"S0": 10, "S3": 5}, "cash": 500, "horizon": "month"}).json() + assert pr["confidence"]["0.95"]["cvar_money"] > 0 and pr["horizon_periods"] == 21 + series = client.post("/risk/series", json={"symbol": "S0", "window": 100}).json() + assert "vol_ewm_ann" in series["columns"] + perf = client.post("/performance/assets", json={"symbols": ["S0"], "freq": "weekly"}).json() + assert "sharpe" in perf[0] + pp = client.post("/performance/portfolio", json={"holdings": {"S0": 10, "S1": 20}}).json() + assert "max_drawdown" in pp + roll = client.post("/performance/rolling", json={"symbol": "S1", "window": 60}).json() + assert "sharpe" in roll["columns"] + assert client.post("/risk/assets", json={"symbols": ["NOPE"]}).status_code == 422 + + +def test_models_and_optimize(client): + er = client.post("/model/expected-returns", json={"symbols": ["S0", "S1", "S2", "S3"], + "views": [{"symbol": "S1", "sentiment": 0.8, "confidence": 0.6}]}).json() + assert er["posterior_ann"]["S1"] > er["prior_ann"]["S1"] and len(er["views"]) == 1 + sc = client.post("/model/scenarios", json={"symbols": ["S0", "S1"], "model": "gbm", "n_scenarios": 500}).json() + assert sc["description"]["n_scenarios"] == 512 and sc["description"]["n_requested"] == 500 # Sobol': power of two + assert sc["description"]["sampling"] == "sobol" and sc["description"]["moment_matched"] and len(sc["summary"]) == 2 + assert len(sc["description"]["hash"]) == 16 + body = {"mode": "new", "cash": 200000, "symbols": ["S0", "S1", "S2", "S3", "S4", "S5"], "cvar_limit": 8000, + "n_scenarios": 400, "scenario_model": "gbm", "solver": "highs", "horizon": "week", + "importance_sampling": "off"} + res = client.post("/optimize", json=body).json() + assert res["status"] == "Optimal" and res["solver"] == "highs" and res["reproducible"] is True + assert res["cvar"] <= 8000 + 1e-3 and res["cash_end"] >= 0 + assert all(p["shares"] >= 0 for p in res["positions"]) + # identical request -> identical scenario hash and identical portfolio + again = client.post("/optimize", json=body).json() + assert again["scenario_hash"] == res["scenario_hash"] and again["positions"] == res["positions"] + # importance sampling and the exact Gaussian mode are accepted and give proposals of the same kind + tilted = client.post("/optimize", json={**body, "importance_sampling": "market"}).json() + assert tilted["status"] == "Optimal" and tilted["model"]["scenarios"]["weighted"] is True + assert tilted["model"]["scenarios"]["importance_sampling"]["direction"] == "market" + adaptive = client.post("/optimize", json={**body, "importance_sampling": "adaptive"}).json() + assert adaptive["status"] == "Optimal" + assert adaptive["solver_info"]["importance_sampling"]["stage1_status"] == "Optimal" + # the market tilt does not depend on the risk limit: same scenario key and hash at another limit + other = client.post("/optimize", json={**body, "importance_sampling": "market", "cvar_limit": 5000}).json() + assert other["scenario_key"]["id"] == tilted["scenario_key"]["id"] + assert other["scenario_hash"] == tilted["scenario_hash"] and other["cvar"] <= 5000 + 1e-3 + exact = client.post("/optimize", json={**body, "scenario_model": "gaussian_exact"}).json() + assert exact["status"] == "Optimal" and exact["reproducible"] and exact["scenario_hash"] is None + assert exact["solver_info"]["outer_approximation"]["converged"] and exact["cvar"] <= 8000 * (1 + 1e-5) + hold = {p["symbol"]: p["shares"] for p in res["positions"]} + reb = client.post("/optimize", json={**body, "mode": "rebalance", "holdings": hold, "cash": res["cash_end"], + "cvar_limit": 5000}).json() + assert reb["status"] == "Optimal" and reb["mode"] == "rebalance" + # the starting portfolio is reported on the same scenarios, so before and after are comparable + before = reb["before"] + assert res["before"] is None # a new portfolio has nothing to compare against + assert before["n_positions"] == sum(1 for v in hold.values() if v > 0) + assert before["invested"] == pytest.approx(res["invested"], rel=1e-9) + assert before["cvar"] > 5000 > reb["cvar"] # the rebalancing is what brings the risk under the new limit + assert before["expected_pnl"] > reb["expected_pnl"] # ... at the cost of expected return + assert sum(before["sector_exposure"].values()) == pytest.approx(before["invested"] / reb["wealth"], rel=1e-6) + + +def test_jobs_and_snapshots(client): + snaps = client.get("/snapshots").json() + assert isinstance(snaps, list) + created = client.post("/snapshots", json={"name": "test-snap"}) + assert created.status_code == 201 + assert "test-snap" in client.get("/snapshots").json() + assert client.get("/jobs").json() == [] + assert client.get("/jobs/nope").status_code == 404 + + +def test_asset_risk_in_value_space(client): + body = {"symbols": ["S0", "S1"], "horizon": "month", "confidence": [0.95, 0.99]} + log = client.post("/risk/assets", json=body).json() + val = client.post("/risk/assets", json={**body, "space": "value"}).json() + for a, b in zip(log, val): + # a VaR is a quantile: it carries over through 1 - e^(-x) (up to the interpolation between + # order statistics, which does not commute with a non-linear map) + assert b["var95_hist"] < a["var95_hist"] + assert b["var95_hist"] == pytest.approx(1 - np.exp(-a["var95_hist"]), rel=1e-4) + assert b["var95_normal_ewm"] == pytest.approx(1 - np.exp(-a["var95_normal_ewm"]), rel=1e-8) + # a CVaR is a tail mean: computed in value space it is strictly below the naive conversion + for col in ("cvar95_normal_ewm", "cvar99_normal_ewm"): + assert b[col] < 1 - np.exp(-a[col]) + # this fixture drifts upwards, so at a monthly horizon the estimated mean can push the + # parametric VaR below zero -- the reason the UI asks for drift="zero" + assert a["var95_normal_ewm"] < 0 + assert b["cvar99_hist"] < 1.0 # a share of the value can never reach 100 % + assert b["vol_ewm_ann"] == a["vol_ewm_ann"] # only the loss columns change + # the market-risk convention: no drift extrapolated into the horizon + zero = client.post("/risk/assets", json={**body, "drift": "zero"}).json() + for a, z in zip(log, zero): + assert z["var95_normal_ewm"] != a["var95_normal_ewm"] + assert z["var95_hist"] == a["var95_hist"] # historical columns keep their window + + +def test_symbol_table_and_facets(client): + t = client.get("/data/symbols", params={"limit": 5}).json() + assert t["n_universe"] == 12 and t["n_matched"] == 12 and len(t["symbols"]) == 5 + caps = [row["market_cap"] for row in t["symbols"]] + assert caps == sorted(caps, reverse=True) # largest first + assert set(t["facets"]["regions"]) == {"US", "Europe"} and sum(t["facets"]["regions"].values()) == 12 + row = t["symbols"][0] + assert row["price"] > 0 and 0 < row["market_weight"] <= 1 and row["sector"] + eu = client.get("/data/symbols", params={"region": "Europe"}).json() + assert eu["n_matched"] == 6 and all(r["region"] == "Europe" for r in eu["symbols"]) + one = client.get("/data/symbols", params={"search": "S1"}).json() + assert {r["symbol"] for r in one["symbols"]} >= {"S1", "S10", "S11"} + + +def test_model_covariance_endpoint(client): + body = {"symbols": ["S0", "S1", "S2", "S3"], "freq": "daily", "horizon": "week", + "covariance": {"method": "ewm", "lam": 0.99, "shrinkage": "auto"}} + c = client.post("/model/covariance", json=body).json() + assert c["correlation"]["columns"] == body["symbols"] + assert c["correlation"]["data"][0][0] == pytest.approx(1.0) + assert 0 <= c["shrinkage_applied"] <= 1 and c["conditioning"]["n_assets"] == 4 + assert c["conditioning"]["rank_deficient"] is False + assert sum(c["eigenvalues"]) == pytest.approx(1.0) + # the covariance is scaled to the horizon, the volatility to the year + vol_h = c["volatility_horizon"]["S0"] + assert c["covariance"]["data"][0][0] == pytest.approx(vol_h ** 2, rel=1e-9) + assert c["volatility_ann"]["S0"] == pytest.approx(vol_h * (252 / 5) ** 0.5, rel=1e-9) + + +def test_scenario_distribution_of_a_portfolio(client): + body = {"symbols": ["S0", "S1", "S2"], "model": "gbm", "n_scenarios": 1024, "horizon": "week", + "weights": {"S0": 0.5, "S1": 0.3, "S2": 0.2}, "value": 1_000_000, "bins": 30, + "focus": ["S0"], "importance_sampling": "off"} + d = client.post("/model/scenarios", json=body).json() + p = d["portfolio"] + assert p["value"] == 1_000_000 and p["invested"] == pytest.approx(1_000_000) + assert len(p["histogram"]["mass"]) == 30 and sum(p["histogram"]["mass"]) == pytest.approx(1.0) + assert p["risk"]["0.99"]["cvar"] > p["risk"]["0.95"]["cvar"] > p["risk"]["0.95"]["var"] + # the quantiles and the VaR are the same statistic seen from two sides (the VaR is the order + # statistic the Rockafellar-Uryasev rows use, the quantile interpolates - one rank apart) + assert p["quantiles"]["0.05"] == pytest.approx(-p["risk"]["0.95"]["var"], rel=1e-2) + assert p["min"] <= p["quantiles"]["0.01"] <= p["quantiles"]["0.5"] <= p["quantiles"]["0.99"] <= p["max"] + assert sum(d["asset_histograms"]["S0"]["mass"]) == pytest.approx(1.0) + # a tilted set reports the same model quantities, with probability weights + tilted = client.post("/model/scenarios", json={**body, "importance_sampling": "market"}).json() + assert tilted["description"]["weighted"] is True + assert tilted["description"]["effective_sample_size"] < tilted["description"]["n_scenarios"] + assert tilted["portfolio"]["risk"]["0.95"]["cvar"] == pytest.approx(p["risk"]["0.95"]["cvar"], rel=0.05) + # shares instead of weights + shares = client.post("/model/scenarios", json={**body, "weights": {}, "holdings": {"S0": 10, "S1": 5}}).json() + assert shares["portfolio"]["invested"] > 0 and len(shares["portfolio"]["exposure"]) == 2 + + +def test_the_ui_is_served(client): + page = client.get("/ui/") + assert page.status_code == 200 and "quantcore" in page.text + assert client.get("/ui/js/main.js").status_code == 200 + assert client.get("/", follow_redirects=False).status_code in (307, 308) + + +def test_the_ui_can_be_switched_off(settings): + """``QUANTCORE_ENABLE_UI=false`` ships the same image with the JSON API and /docs only.""" + from fastapi import FastAPI + + off = FastAPI() + assert app_module.mount_test_bench(off, settings.model_copy(update={"enable_ui": False})) is False + with TestClient(off) as c: + assert c.get("/ui/").status_code == 404 + assert c.get("/", follow_redirects=False).status_code == 404 + + on = FastAPI() + assert app_module.mount_test_bench(on, settings.model_copy(update={"enable_ui": True})) is True + with TestClient(on) as c: + assert c.get("/ui/").status_code == 200 + assert c.get("/", follow_redirects=False).headers["location"] == "/ui/" diff --git a/tests/test_black_litterman.py b/tests/test_black_litterman.py new file mode 100644 index 0000000..9d9b07f --- /dev/null +++ b/tests/test_black_litterman.py @@ -0,0 +1,91 @@ +import numpy as np +import pandas as pd +import pytest + +from quantcore.models.black_litterman import ( + BlackLittermanModel, View, idzorek_omega, idzorek_omega_numeric, view_from_sentiment, +) + + +@pytest.fixture +def model(): + symbols = ["A", "B", "C", "D"] + vols = np.array([0.20, 0.25, 0.15, 0.30]) / np.sqrt(52) + corr = np.array([[1, .5, .3, .2], [.5, 1, .4, .1], [.3, .4, 1, .3], [.2, .1, .3, 1]]) + cov = pd.DataFrame(np.outer(vols, vols) * corr, index=symbols, columns=symbols) + w = pd.Series([0.4, 0.3, 0.2, 0.1], index=symbols) + return BlackLittermanModel(cov, w, market_return=0.07 / 52, risk_free=0.01 / 52, tau=0.05, periods_per_year=52) + + +def test_equilibrium_is_consistent(model): + # the market portfolio earns exactly the market excess return under the implied returns + assert model.w @ model.pi == pytest.approx(model.market_return - model.risk_free) + assert model.delta > 0 + res = model.posterior([]) + pd.testing.assert_series_equal(res.posterior, res.prior) + # market weights are recovered as the unconstrained MV weights of the prior (tau-adjusted cov ignored here) + w_mv = np.linalg.solve(model.delta * model.sigma, model.pi) + np.testing.assert_allclose(w_mv, model.w) + + +def test_idzorek_closed_form_equals_numeric(model): + p = np.array([0.0, 1.0, 0.0, 0.0]) + q = (0.12 - 0.01) / 52 + for c in (0.1, 0.35, 0.5, 0.8, 0.99): + omega_cf = idzorek_omega(p, model.tau * model.sigma, c) + omega_num = idzorek_omega_numeric(p, q, model.pi, model.sigma, model.tau, model.delta, c) + assert omega_cf == pytest.approx(omega_num, rel=1e-4) + + +def test_confidence_moves_weights_by_fraction(model): + view = View({"B": 1.0}, q=0.15, confidence=0.4) + res = model.posterior([view]) + w_post = np.linalg.solve(model.delta * model.sigma, res.posterior.values) + full = model.posterior([View({"B": 1.0}, q=0.15, confidence=1.0)]) + w_full = np.linalg.solve(model.delta * model.sigma, full.posterior.values) + tilt_partial = w_post - model.w + tilt_full = w_full - model.w + np.testing.assert_allclose(tilt_partial, 0.4 * tilt_full, atol=1e-10) + + +def test_full_confidence_reproduces_view(model): + res = model.posterior([View({"B": 1.0}, q=0.15, confidence=1.0)]) + assert res.posterior_ann["B"] == pytest.approx(0.15, abs=1e-9) + + +def test_positive_view_raises_asset_and_relative_view(model): + res = model.posterior([View({"A": 1.0}, q=0.20, confidence=0.6)]) + assert res.posterior_ann["A"] > res.prior_ann["A"] + rel = model.posterior([View({"C": 1.0, "D": -1.0}, q=0.05, confidence=0.5, label="C beats D")]) + assert (rel.posterior_ann["C"] - rel.posterior_ann["D"]) > (rel.prior_ann["C"] - rel.prior_ann["D"]) + assert rel.views[0]["label"] == "C beats D" + assert rel.posterior_cov.shape == (4, 4) + + +def test_view_from_sentiment(model): + v = view_from_sentiment(model, "C", sentiment=1.0, confidence=0.7, strength=0.25) + assert v.q == pytest.approx(model.prior_ann["C"] + 0.25 * 0.15, rel=1e-6) + neg = view_from_sentiment(model, "C", sentiment=-1.0, confidence=0.7) + assert neg.q < model.prior_ann["C"] + with pytest.raises(ValueError): + View({"A": 1.0}, q=0.1, confidence=0.0) + + +def test_documentation_example_numbers(model): + """docs/measurement_and_models.tex, Example 7.2 (the fixture is that four-asset model).""" + assert model.market_var * 52 == pytest.approx(0.02502, abs=1e-5) + assert model.delta == pytest.approx(2.3986, abs=1e-4) + np.testing.assert_allclose(model.prior_ann.values, [0.0736, 0.0880, 0.0435, 0.0550], atol=5e-5) + assert model.w @ model.prior_ann.values == pytest.approx(0.07, abs=1e-9) + res = model.posterior([View({"B": 1.0}, q=0.15, confidence=0.4)]) + p = np.array([0.0, 1.0, 0.0, 0.0]) + base = p @ (model.tau * model.sigma) @ p + assert base == pytest.approx(6.0096e-5, rel=1e-4) + assert res.omega[0] == pytest.approx(1.5 * base, rel=1e-12) + assert res.omega[0] == pytest.approx(9.0144e-5, rel=1e-4) + np.testing.assert_allclose(res.posterior_ann.values, [0.0835, 0.1128, 0.0494, 0.0580], atol=5e-5) + w_post = np.linalg.solve(model.delta * model.sigma, res.posterior.values) + full = model.posterior([View({"B": 1.0}, q=0.15, confidence=1.0)]) + w_100 = np.linalg.solve(model.delta * model.sigma, full.posterior.values) + assert w_100[1] == pytest.approx(0.7139, abs=5e-5) and w_post[1] == pytest.approx(0.4656, abs=5e-5) + np.testing.assert_allclose(w_post[[0, 2, 3]], model.w[[0, 2, 3]], atol=1e-10) diff --git a/tests/test_cleaning.py b/tests/test_cleaning.py new file mode 100644 index 0000000..60c0e22 --- /dev/null +++ b/tests/test_cleaning.py @@ -0,0 +1,132 @@ +import numpy as np +import pandas as pd +import pytest + +from quantcore.data.cleaning import ( + CleaningReport, + clean_fx, + clean_prices, + find_spike_blocks, + fx_scale_exponent, + longest_missing_run, +) + + +def test_spike_blocks_and_genuine_moves(): + idx = pd.bdate_range("2024-01-01", periods=40) + p = pd.Series(100.0, index=idx) + p.iloc[5] = 200.0 # single wrong tick (2x, reverses next day) + p.iloc[10:12] = 210.0 # two-day block + p.iloc[20:] = 40.0 # genuine crash that stays + p.iloc[30:33] = 45.0 # ordinary small moves + assert find_spike_blocks(p) == [5, 10, 11] + + +def test_clean_prices_report_and_gaps(): + idx = pd.bdate_range("2024-01-01", periods=40) + a = pd.Series(np.linspace(50, 60, 40), index=idx) + b = a.copy() + b.iloc[3:8] = -1.0 # broken adjustment stretch + b.iloc[25] = 300.0 # spike + from quantcore.data.cleaning import CleaningReport + + rep = CleaningReport() + out = clean_prices(pd.DataFrame({"A": a, "B": b}), report=rep) + assert rep.n_nonpositive == 5 and rep.symbols_nonpositive == {"B": 5} + assert rep.n_spikes == 1 and rep.symbols_spikes == {"B": 1} + assert out["A"].notna().all() + assert out["B"].isna().sum() == 6 + runs = longest_missing_run(out) + assert runs["A"] == 0 and runs["B"] == 5 + + +# --------------------------------------------------------------------------------------------- +# FX quoting-unit changes (the NOK/CHF artefact: 96 observations returned 100x too small) +# --------------------------------------------------------------------------------------------- +def _fx_series(n=500, seed=0): + rng = np.random.default_rng(seed) + idx = pd.bdate_range("2016-01-01", periods=n) + return pd.Series(0.12 * np.exp(np.cumsum(rng.standard_normal(n) * 0.004)), index=idx) + + +def test_a_quoting_unit_change_is_undone_wherever_it_sits(): + """A stretch quoted 100x too small is repaired, whatever part of the series it covers.""" + good = _fx_series() + for lo, hi in ((70, 140), (0, 60), (440, 500)): # middle, start, end + broken = good.copy() + broken.iloc[lo:hi] /= 100.0 + fixed = clean_fx(broken.to_frame("NOK"))["NOK"] + assert np.allclose(fixed.values, good.values), (lo, hi) + # the artefact is what made the volatility absurd; the repair takes it back + vol = lambda x: float(np.log(x).diff().std() * np.sqrt(252)) + assert vol(broken) > 2.0 and vol(fixed) == pytest.approx(vol(good), rel=1e-9) + + +def test_several_unit_changes_and_other_powers_of_ten(): + good = _fx_series() + broken = good.copy() + broken.iloc[30:60] /= 100.0 + broken.iloc[200:210] *= 10.0 + broken.iloc[300:301] /= 1000.0 + fixed = clean_fx(broken.to_frame("NOK"))["NOK"] + assert np.allclose(fixed.values, good.values) + assert set(fx_scale_exponent(broken).unique()) == {0, -2, 1, -3} + + +def test_genuine_moves_are_never_rescaled(): + """A devaluation is a price move, not a unit change: nothing short of a factor of ten is touched. + + The 6.1x case is real: ABVX.PA gained 510 % on its trial results (2025-07-23). A loose tolerance + reads that as a decimal shift and rewrites the year of history that follows it. + """ + good = _fx_series() + for factor in (0.75, 1.4, 0.5, 2.0, 3.0, 6.101, 6.421, 1 / 6.101, 8.0): + moved = good.copy() + moved.iloc[250:] *= factor + fixed = clean_fx(moved.to_frame("TRY"))["TRY"] + assert np.allclose(fixed.values, moved.values), factor + + +def test_clean_fx_is_idempotent_and_reports_per_currency(): + good = _fx_series() + frame = pd.DataFrame({"CHF": 1.0, "EUR": good * 8, "NOK": good}) + frame.iloc[100:180, frame.columns.get_loc("NOK")] /= 100.0 + report = CleaningReport() + fixed = clean_fx(frame, report=report) + assert report.n_fx_rescaled == 80 and report.fx_rescaled == {"NOK": 80} + assert np.allclose(fixed["NOK"].values, good.values) + assert fixed["EUR"].equals(frame["EUR"]) and fixed["CHF"].equals(frame["CHF"]) + again = CleaningReport() + assert clean_fx(fixed, report=again).equals(fixed) and again.n_fx_rescaled == 0 + + +def test_a_short_series_and_missing_values_are_handled(): + idx = pd.bdate_range("2024-01-01", periods=6) + assert fx_scale_exponent(pd.Series([0.1], index=idx[:1])).empty is False + gappy = pd.Series([0.1, np.nan, 0.001, 0.001, np.nan, 0.1], index=idx) + fixed = clean_fx(gappy.to_frame("NOK"))["NOK"] + assert fixed.dropna().round(6).tolist() == [0.1, 0.1, 0.1, 0.1] + assert fixed.isna().tolist() == [False, True, False, False, True, False] + + +def test_fx_spikes_are_removed_but_a_real_regime_break_is_kept(): + """A reversing tick is a quote error; 2015-01-16 (the SNB floor) is not, and must survive.""" + good = _fx_series(n=400) + broken = good.copy() + broken.iloc[120] *= 1.09 # one-day spike, reverses next day + broken.iloc[250:] *= 0.81 # a permanent 19 % repricing: never reversed + fixed = clean_fx(broken.to_frame("EUR"))["EUR"] + assert np.isnan(fixed.iloc[120]) # the tick is gone + assert fixed.iloc[249] == broken.iloc[249] and fixed.iloc[250] == broken.iloc[250] + assert fixed.drop(fixed.index[120]).equals(broken.drop(broken.index[120])) + + +def test_the_unit_repair_runs_before_the_spike_pass(): + """A 100x stretch must be rescaled, not mistaken for two enormous spikes at its edges.""" + good = _fx_series(n=300) + broken = good.copy() + broken.iloc[100:160] /= 100.0 + report = CleaningReport() + fixed = clean_fx(broken.to_frame("NOK"), report=report)["NOK"] + assert report.n_fx_rescaled == 60 and report.n_fx_spikes == 0 + assert np.allclose(fixed.values, good.values) diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..08dcce3 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,222 @@ +"""The HTTP client and the ``quantcore-api`` CLI, against the app running in this process. + +``TestClient`` satisfies the transport contract of :class:`QuantcoreClient` (``request(method, url, +params=, json=, timeout=)``), so the same code paths that talk to the Spark are exercised here +without a server: the URLs, the query parameters, the request bodies and the error handling. +""" + +from __future__ import annotations + +import pytest +from fastapi.testclient import TestClient + +import quantcore.api.app as app_module +from quantcore import apicli +from quantcore.client import QuantcoreClient, QuantcoreError + + +@pytest.fixture(scope="module") +def api(settings, store): + app_module.state = app_module.State(settings) + with TestClient(app_module.app) as transport: + yield QuantcoreClient("http://testserver", session=transport) + + +@pytest.fixture +def cli(api, monkeypatch): + """Run the CLI against the in-process app: every command builds its client through ``_client``.""" + monkeypatch.setattr(apicli, "_client", lambda args: api) + return apicli.main + + +# ------------------------------------------------------------------------------------------------ +# client +# ------------------------------------------------------------------------------------------------ +def test_the_client_covers_the_api(api): + health = api.health() + assert health["status"] == "ok" and health["store"]["prices"] is True + + assert len(api.universe(region="Europe")) == 6 + assert api.symbol("S0")["symbol"] == "S0" + table = api.symbols_table(limit=5) + assert len(table["symbols"]) == 5 and "sectors" in table["facets"] + assert table["n_universe"] >= table["n_matched"] >= 5 + + px = api.prices(["S0", "S1"], freq="weekly") + assert px["columns"] == ["S0", "S1"] and px["freq"] == "weekly" + assert api.returns(["S0"])["kind"] == "log_returns" + assert api.market_index(freq="weekly")["n"] > 100 + assert sum(api.market_weights()["weights"].values()) == pytest.approx(1.0) + + rows = api.asset_risk(["S0", "S1"], horizon="week") + assert len(rows) == 2 and rows[0]["cvar95_hist"] > rows[0]["var95_hist"] + assert api.risk_series("S0", horizon="week")["index"] + assert api.covariance(["S0", "S1"])["columns"] == ["S0", "S1"] + assert api.correlation(["S0", "S1"])["data"][0][0] == pytest.approx(1.0) + assert api.portfolio_risk({"S0": 100, "S1": 50}, cash=1000.0, horizon="week")["value"] > 0 + + assert len(api.asset_performance(["S0", "S1"])) == 2 + assert api.rolling_performance("S0", window=252)["index"] + assert api.portfolio_performance({"S0": 100}) + + assert api.expected_returns(max_assets=6)["market_return_ann"] != 0 + assert api.model_covariance(["S0", "S1"])["conditioning"]["n_assets"] == 2 + scen = api.scenarios(["S0", "S1"], model="gbm", n_scenarios=1024, holdings={"S0": 10}) + assert scen["description"]["n_scenarios"] == 1024 and scen["portfolio"]["invested"] > 0 + + # order-independent: another module may have created a snapshot on the session-scoped store + assert isinstance(api.jobs(), list) and isinstance(api.snapshots(), list) + + +def test_the_client_optimises(api): + res = api.optimize(cash=1_000_000, max_assets=6, horizon="week", scenario_model="gaussian_exact", + solver="highs", time_limit=120) + assert res["status"] in ("Optimal", "FeasibleFound") + assert res["solver"] == "highs" and res["positions"] + # a rebalancing run carries the starting point, so before/after can be compared + holdings = {p["symbol"]: p["shares"] for p in res["positions"][:2]} + reb = api.optimize(mode="rebalance", holdings=holdings, cash=10_000, max_assets=6, horizon="week", + scenario_model="gaussian_exact", solver="highs", time_limit=120) + assert reb["before"] is not None and reb["before"]["invested"] > 0 + + +def test_none_arguments_are_left_to_the_api(api): + """Optional arguments that are ``None`` must not be sent, or the API would see "None".""" + sent: dict = {} + original = api.session.request + + def spy(method, url, **kw): + sent.update({"params": kw.get("params"), "json": kw.get("json")}) + return original(method, url, **kw) + + api.session.request = spy + try: + api.prices(["S0"], start=None, end=None) + assert "start" not in (sent["params"] or {}) and "end" not in (sent["params"] or {}) + api.expected_returns(symbols=None, max_assets=6) + assert "symbols" not in (sent["json"] or {}) + finally: + api.session.request = original + + +def test_the_client_reports_api_errors(api): + with pytest.raises(QuantcoreError) as exc: + api.symbol("NOPE") + assert exc.value.status_code == 404 and not exc.value.unreachable + + with pytest.raises(QuantcoreError) as exc: # 422: the field name must reach the caller + api.asset_risk(["S0"], window=1) + assert exc.value.status_code == 422 and "window" in str(exc.value) + assert "greater than or equal to 10" in str(exc.value) + + +def test_the_client_reports_an_unreachable_server(): + with QuantcoreClient("http://127.0.0.1:9", timeout=2.0) as qc, pytest.raises(QuantcoreError) as exc: + qc.health() + assert exc.value.unreachable and "127.0.0.1:9" in str(exc.value) + + +def test_the_client_retries_dropped_gets_but_never_posts(): + """A poll on a kept-alive connection the server just closed must not abort `update --wait`; + a POST (optimisation, job start) must never be sent twice.""" + with QuantcoreClient("http://127.0.0.1:9") as qc: + retry = qc.session.get_adapter("http://127.0.0.1:9/health").max_retries + assert retry.read >= 1 and "GET" in retry.allowed_methods and "POST" not in retry.allowed_methods + + +def test_wait_for_job_polls_until_the_job_ends(): + states = iter(["queued", "running", "running", "done"]) + + class Stub: + def request(self, method, url, **kw): + class Res: + status_code = 200 + + @staticmethod + def json(): + return {"job_id": "abc", "status": next(states)} + return Res() + + qc = QuantcoreClient("http://stub", session=Stub()) + seen: list[str] = [] + record = qc.wait_for_job("abc", poll=0.0, on_poll=lambda r: seen.append(r["status"])) + assert record["status"] == "done" and seen == ["queued", "running", "running"] + + with pytest.raises(QuantcoreError): # a job that never finishes hits the deadline + QuantcoreClient("http://stub", session=type("S", (), { + "request": lambda self, m, u, **k: type("R", (), { + "status_code": 200, "json": staticmethod(lambda: {"status": "running"})})()})() + ).wait_for_job("abc", poll=0.0, deadline=-1.0) + + +# ------------------------------------------------------------------------------------------------ +# CLI +# ------------------------------------------------------------------------------------------------ +def test_every_documented_command_parses(): + parser = apicli.build_parser() + for argv in ( + ["health"], ["endpoints"], ["universe", "--region", "Europe", "--sector", "Health Care"], + ["symbol", "S0"], ["prices", "S0", "S1", "--freq", "weekly", "--tail", "5"], + ["returns", "S0", "--currency", "local"], ["market", "weights", "--max-assets", "10"], + ["risk", "S0", "S1", "--horizon", "week", "--drift", "zero", "--space", "value"], + ["risk-portfolio", "--holdings", '{"S0": 10}'], ["correlation", "S0", "S1"], + ["covariance", "S0", "S1", "--method", "rolling"], ["performance", "S0"], + ["expected-returns", "--max-assets", "50", "--view", "S0:0.6:0.4"], + ["model-covariance", "S0", "S1"], ["scenarios", "S0", "--model", "gbm", "--n-scenarios", "1024"], + ["optimize", "--cash", "1000000", "--cvar-limit", "40000", "--risk-drift", "zero"], + ["optimize", "--request", "r.json", "--json"], ["jobs", "--job-id", "abc"], + ["setup", "--offline", "--wait"], ["update", "--full", "--wait"], + ["snapshots", "--create", "--name", "demo"], ["selftest", "--full", "--write"], + ): + args = parser.parse_args(argv) + assert callable(args.func), argv + + +def test_sentiment_views_are_parsed(): + assert apicli._views(["S0:0.6"]) == [{"symbol": "S0", "sentiment": 0.6}] + assert apicli._views(["S0:0.6:0.4:0.3"]) == [ + {"symbol": "S0", "sentiment": 0.6, "confidence": 0.4, "strength": 0.3}] + with pytest.raises(SystemExit): + apicli._views(["S0"]) + + +@pytest.mark.parametrize("argv", [ + ["health"], ["endpoints"], ["universe", "--region", "Europe"], ["symbol", "S0"], + ["prices", "S0", "S1", "--tail", "3"], ["returns", "S0", "--tail", "3"], ["market", "index"], + ["market", "weights"], ["risk", "S0", "S1", "--drift", "zero"], + ["risk-portfolio", "--holdings", '{"S0": 10}'], ["correlation", "S0", "S1"], + ["covariance", "S0", "S1"], ["performance", "S0"], ["expected-returns", "--max-assets", "6"], + ["model-covariance", "S0", "S1"], ["scenarios", "S0", "S1", "--n-scenarios", "1024"], + ["jobs"], ["snapshots"], +]) +def test_the_cli_runs_every_read_only_command(cli, capsys, argv): + assert cli(argv) == 0 + assert capsys.readouterr().out.strip() + + +def test_the_cli_optimises_and_prints_a_summary(cli, capsys): + assert cli(["optimize", "--max-assets", "6", "--scenario-model", "gaussian_exact", + "--solver", "highs", "--time-limit", "120"]) == 0 + out = capsys.readouterr().out + assert "status Optimal" in out and "E[P&L]" in out and "CVaR" in out + + +def test_the_selftest_passes_against_a_healthy_service(cli, capsys): + """The selftest is the deployment check: every endpoint, one table, exit 0 when all answered.""" + assert cli(["selftest", "--n-symbols", "2"]) == 0 + out = capsys.readouterr().out + assert "FAIL" not in out + assert "0 failed" in out and "3 skipped" in out # the writing endpoints stay untouched + for endpoint in ("GET /health", "POST /risk/portfolio", "POST /optimize", "GET /snapshots"): + assert endpoint in out + + +def test_the_selftest_fails_loudly_when_the_service_is_down(capsys, monkeypatch): + monkeypatch.setattr(apicli, "_client", lambda args: QuantcoreClient("http://127.0.0.1:9", timeout=1.0)) + assert apicli.main(["selftest"]) == 1 + assert "not usable" in capsys.readouterr().out + + +def test_the_cli_exits_non_zero_when_the_api_is_unreachable(capsys): + assert apicli.main(["--url", "http://127.0.0.1:9", "--timeout", "1", "health"]) == 1 + assert "is the service running?" in capsys.readouterr().err diff --git a/tests/test_data.py b/tests/test_data.py new file mode 100644 index 0000000..eb78305 --- /dev/null +++ b/tests/test_data.py @@ -0,0 +1,81 @@ +import numpy as np +import pandas as pd +import pytest + +from quantcore.data.download_service import splice_adjusted +from quantcore.data.fx import FXConverter +from quantcore.data.yahoo import apply_price_scale, normalise_info +from quantcore.universe.sources import parse_ishares_holdings +from quantcore.universe.symbols import normalise_share_class_name, to_yahoo_symbol + + +def test_fx_conversion_and_log_return_identity(): + idx = pd.bdate_range("2024-01-01", periods=5) + fx = pd.DataFrame({"USD": [0.85, 0.86, 0.84, 0.85, 0.87], "CHF": 1.0}, index=idx) + conv = FXConverter(fx, base="CHF") + px = pd.DataFrame({"A": [10.0, 11.0, 10.5, 10.8, 11.2], "B": [100.0, 101.0, 99.0, 98.0, 100.0]}, index=idx) + cur = pd.Series({"A": "USD", "B": "CHF"}) + chf = conv.convert_prices(px, cur) + np.testing.assert_allclose(chf["A"], px["A"] * fx["USD"]) + np.testing.assert_allclose(chf["B"], px["B"]) + # CHF log-return = local log-return + FX log-return + r_chf = np.log(chf).diff().iloc[1:] + r_loc = np.log(px).diff().iloc[1:] + r_fx = conv.fx_log_returns(cur, idx).iloc[1:] + np.testing.assert_allclose(r_chf.values, (r_loc + r_fx).values) + with pytest.raises(KeyError): + conv.convert_prices(px, pd.Series({"A": "JPY", "B": "CHF"})) + + +def test_gbp_pence_normalisation(): + info = normalise_info("HSBA.L", {"currency": "GBp", "marketCap": 1e11, "regularMarketPrice": 1533.8, + "firstTradeDateMilliseconds": 584089200000, "quoteType": "EQUITY"}) + assert info["currency"] == "GBP" and info["price_scale"] == 100.0 + assert info["last_price"] == pytest.approx(15.338) + assert info["first_trade_date"] == "1988-07-05" + px = pd.DataFrame({"HSBA.L": [1500.0, 1600.0], "NESN.SW": [80.0, 81.0]}) + scaled = apply_price_scale(px, pd.Series({"HSBA.L": 100.0})) + assert scaled["HSBA.L"].iloc[0] == 15.0 and scaled["NESN.SW"].iloc[1] == 81.0 + + +def test_splice_rescales_history_on_dividend(): + idx = pd.bdate_range("2024-01-01", periods=10) + stored = pd.DataFrame({"A": np.linspace(100, 109, 10), "B": np.linspace(50, 59, 10)}, index=idx) + fresh_idx = pd.bdate_range("2024-01-08", periods=7) # overlaps the last 5 stored days, adds 2 new + fresh = pd.DataFrame({"A": np.concatenate([stored["A"].iloc[5:].values * 0.98, [110 * 0.98, 111 * 0.98]]), + "B": np.concatenate([stored["B"].iloc[5:].values, [60.0, 61.0]])}, index=fresh_idx) + out, rescaled = splice_adjusted(stored, fresh) + assert rescaled == ["A"] + assert len(out) == 12 + np.testing.assert_allclose(out["A"].iloc[:5], stored["A"].iloc[:5] * 0.98) # history rescaled + np.testing.assert_allclose(out["A"].iloc[5:], fresh["A"]) # fresh window authoritative + np.testing.assert_allclose(out["B"], np.concatenate([stored["B"].values, [60.0, 61.0]])) + + +def test_symbol_mapping_rules(): + assert to_yahoo_symbol("NOVO B", "Omx Nordic Exchange Copenhagen A/S") == "NOVO-B.CO" + assert to_yahoo_symbol("BT.A", "London Stock Exchange") == "BT-A.L" + assert to_yahoo_symbol("RR.", "London Stock Exchange") == "RR.L" + assert to_yahoo_symbol("NESN", "SIX Swiss Exchange") == "NESN.SW" + assert to_yahoo_symbol("BRK.B", country="United States") == "BRK-B" + assert to_yahoo_symbol("NDA FI", "Nasdaq Omx Helsinki Ltd.") == "NDA-FI.HE" + with pytest.raises(ValueError): + to_yahoo_symbol("X", "Mars Exchange") + assert normalise_share_class_name("Alphabet Inc. Class A") == normalise_share_class_name("Alphabet Inc. Class C") + assert normalise_share_class_name("Investor AB ser. B") == normalise_share_class_name("Investor Class A") + + +def test_parse_ishares_holdings(): + text = ('Fondsposition per,"18.Sept.2026"\n \nEmittententicker,Name,Sektor,Anlageklasse,Marktwert,' + 'Gewichtung (%),Nominalwert,Nominale,Kurs,Standort,Börse,Marktwährung\n' + '"ASML","ASML HOLDING","IT","Aktien","438.829.188,00","4,23","438.829.188,00","303.478,00","1.446,00",' + '"Niederlande","Euronext Amsterdam","EUR"\n' + '"NOVO B","NOVO NORDISK CLASS B","Gesundheitsversorgung","Aktien","1,00","1,00","1,00","1,00","1,00",' + '"Dänemark","Omx Nordic Exchange Copenhagen A/S","DKK"\n' + '"XEUR","EUR CASH","Barmittel und/oder Derivate","Geldmarkt","1,00","0,10","1,00","1,00","1,00",' + '"Deutschland","-","EUR"\n') + df = parse_ishares_holdings(text) + assert list(df["symbol"]) == ["ASML.AS", "NOVO-B.CO"] + assert df.iloc[0]["gics_sector"] == "Information Technology" and df.iloc[0]["country"] == "Netherlands" + assert df.iloc[0]["index_weight"] == pytest.approx(4.23) + assert df.iloc[0]["source_date"] == "2026-09-18" diff --git a/tests/test_estimators.py b/tests/test_estimators.py new file mode 100644 index 0000000..b9f8149 --- /dev/null +++ b/tests/test_estimators.py @@ -0,0 +1,54 @@ +import numpy as np +import pandas as pd +import pytest + +from quantcore.measurement import estimators as est + + +@pytest.fixture +def r(): + rng = np.random.default_rng(3) + x = rng.standard_normal((600, 4)) @ np.array([[1, .5, .2, 0], [0, 1, .3, 0], [0, 0, 1, .4], [0, 0, 0, 1]]) * 0.01 + return pd.DataFrame(x, index=pd.bdate_range("2020-01-01", periods=600), columns=list("ABCD")) + + +def test_rolling_cov_matches_numpy(r): + cov = est.rolling_cov(r, 100) + np.testing.assert_allclose(cov.values, np.cov(r.values[-100:], rowvar=False, ddof=1)) + corr = est.rolling_corr(r, 100) + np.testing.assert_allclose(np.diag(corr.values), 1.0) + np.testing.assert_allclose(corr.values, np.corrcoef(r.values[-100:], rowvar=False), atol=1e-12) + + +def test_ewm_cov_diagonal_matches_pandas(r): + lam = 0.94 + cov = est.ewm_cov(r, lam, max_obs=len(r)) + pd_var = r.ewm(alpha=1 - lam, adjust=True).var(bias=True).iloc[-1] + np.testing.assert_allclose(np.diag(cov.values), pd_var.values, rtol=1e-8) + assert est.ewm_weights(10, lam).sum() == pytest.approx(1.0) + + +def test_ewm_sequence_recursion_converges_to_direct(r): + lam = 0.97 + last = None + for date, cov_prev, _ in est.covariance_sequence(r, "ewm", lam=lam, warmup=100): + last = (date, cov_prev) + # Sigma_{T-1} from the recursion vs. the direct (normalised-weight) estimate at T-1 + direct = est.ewm_cov(r.iloc[:-1], lam, max_obs=len(r)) + assert last[0] == r.index[-1] + np.testing.assert_allclose(last[1], direct.values, rtol=0.05, atol=1e-7) + + +def test_rolling_sequence_matches_cov(r): + seq = list(est.covariance_sequence(r, "rolling", window=50)) + date, cov_prev, ret = seq[-1] + assert date == r.index[-1] + np.testing.assert_allclose(cov_prev, np.cov(r.values[-51:-1], rowvar=False, ddof=1)) + np.testing.assert_allclose(ret, r.values[-1]) + + +def test_shrinkage_keeps_variances(r): + cov = est.rolling_cov(r, 200) + sh = est.shrink(cov, 0.3) + np.testing.assert_allclose(np.diag(sh.values), np.diag(cov.values)) + assert abs(sh.values[0, 1]) == pytest.approx(0.7 * abs(cov.values[0, 1])) diff --git a/tests/test_problem.py b/tests/test_problem.py new file mode 100644 index 0000000..c9a4034 --- /dev/null +++ b/tests/test_problem.py @@ -0,0 +1,166 @@ +import itertools + +import numpy as np +import pytest + +from quantcore.config import Settings +from quantcore.measurement.risk import scenario_cvar +from quantcore.optimization.problem import MeanCVaRProblem, OptimizationConstraints, ProblemSpec, TransactionCosts +from quantcore.optimization.solvers import HighsSolver + +SETTINGS = Settings(cuopt_url="http://127.0.0.1:9") + + +def make_spec(seed=0, n=6, S=300, **kw): + rng = np.random.default_rng(seed) + symbols = [f"S{i}" for i in range(n)] + prices = rng.uniform(20, 400, n) + scen = rng.standard_normal((S, n)) * 0.03 + rng.uniform(-0.002, 0.006, n) + sectors = ["Tech", "Fin", "Health"] * (n // 3) + ["Tech"] * (n % 3) + base = dict(symbols=symbols, prices=prices, scenarios=scen, sectors=sectors, cash=100_000.0, confidence=0.95, + cvar_limit=4_000.0) + base.update(kw) + return ProblemSpec(**base) + + +def solve(spec): + problem = MeanCVaRProblem(spec) + model = problem.build() + res = HighsSolver(SETTINGS).solve(model, time_limit=60) + assert res.ok, res + return problem, model, res, problem.decode(res.x) + + +def test_new_portfolio_respects_all_constraints(): + # with a 4000 limit the 10 % caps bind first (CVaR ~1040); at 800 the CVaR limit must bind + spec = make_spec(cvar_limit=800.0, constraints=OptimizationConstraints(max_weight=0.10, max_sector_weight=0.25)) + problem, model, res, d = solve(spec) + W = spec.wealth + assert (d["shares"] >= 0).all() and np.issubdtype(d["shares"].dtype, np.integer) + assert d["cash_end"] >= -1e-6 + assert (d["weights"] <= 0.10 + 1e-9).all() + for sec in set(spec.sectors): + assert d["value"][np.array(spec.sectors) == sec].sum() <= 0.25 * W + 1e-6 + # the CVaR of the decoded portfolio (recomputed from the scenarios) is within the limit and binding + # up to the granularity of whole shares; alpha is *a* minimiser of the RU function, not necessarily the VaR + assert d["cvar"] <= spec.cvar_limit + 1e-6 + assert d["cvar"] >= 0.98 * spec.cvar_limit + assert d["var"] <= d["cvar"] + # objective equals expected P&L net of costs + assert res.objective == pytest.approx(d["expected_pnl"], rel=1e-6) + assert d["total_cost"] == pytest.approx(0.001 * d["invested"], rel=1e-9) + + +def test_brute_force_optimality_two_assets(): + spec = make_spec(n=2, S=80, cash=5_000.0, cvar_limit=300.0, + constraints=OptimizationConstraints(max_weight=0.6, max_sector_weight=1.0)) + _, _, res, d = solve(spec) + p, scen = spec.prices, spec.scenarios + best, best_obj = None, -np.inf + max_n = (0.6 * spec.wealth / p).astype(int) + 1 + for n0, n1 in itertools.product(range(max_n[0] + 1), range(max_n[1] + 1)): + n = np.array([n0, n1]) + cost = 0.001 * (n * p).sum() + if (n * p).sum() + cost > spec.cash + 1e-9 or (n * p > 0.6 * spec.wealth + 1e-9).any(): + continue + pnl = scen @ (n * p) - cost + _, cvar = scenario_cvar(-pnl, 0.95) + if cvar <= 300.0 + 1e-6 and pnl.mean() > best_obj: + best, best_obj = n, pnl.mean() + assert best_obj == pytest.approx(res.objective, rel=1e-6) + assert (d["shares"] == best).all() + + +def test_rebalancing_links_trades_and_costs(): + holdings = np.array([50, 0, 20, 0, 10, 0], dtype=float) + spec = make_spec(cash=20_000.0, holdings=holdings, cvar_limit=3_000.0, + costs=TransactionCosts(0.002, 0.003, fixed_per_trade=25.0), + constraints=OptimizationConstraints(max_weight=0.15, max_sector_weight=0.4, max_turnover=0.5)) + problem, model, res, d = solve(spec) + assert problem.rebalancing and problem.use_trade_bin + np.testing.assert_array_equal(d["shares"], holdings.astype(int) + d["buys"] - d["sells"]) + assert not ((d["buys"] > 0) & (d["sells"] > 0)).any() # never buy and sell the same stock + n_trades = int(((d["buys"] + d["sells"]) > 0).sum()) + expected_cost = 0.002 * (d["buys"] * spec.prices).sum() + 0.003 * (d["sells"] * spec.prices).sum() + 25.0 * n_trades + assert d["total_cost"] == pytest.approx(expected_cost) + assert d["cash_end"] == pytest.approx(spec.cash + (d["sells"] * spec.prices).sum() - (d["buys"] * spec.prices).sum() - d["total_cost"]) + assert d["cash_end"] >= -1e-6 + assert d["turnover"] <= 0.5 + 1e-9 + assert d["cvar"] <= 3_000.0 + 1e-6 + + +def test_objectives_min_cvar_and_utility(): + spec = make_spec(objective="min_cvar", cvar_limit=None, target_pnl=40.0) + _, _, res, d = solve(spec) + assert d["expected_pnl"] >= 40.0 - 1e-6 + assert res.objective == pytest.approx(d["cvar"], rel=1e-6) + spec_u = make_spec(objective="utility", cvar_limit=None, risk_aversion=0.5) + _, _, res_u, d_u = solve(spec_u) + assert res_u.objective == pytest.approx(d_u["expected_pnl"] - 0.5 * d_u["cvar"], rel=1e-6) + + +def test_cardinality_and_min_position(): + spec = make_spec(cvar_limit=5_000.0, constraints=OptimizationConstraints( + max_weight=0.3, max_sector_weight=1.0, max_positions=2, min_position_value=5_000.0)) + problem, model, res, d = solve(spec) + assert problem.use_pos_bin + held = d["value"] > 0 + assert held.sum() <= 2 + assert (d["value"][held] >= 5_000.0 - 1e-6).all() + + +def test_spec_validation(): + with pytest.raises(ValueError): + make_spec(cvar_limit=None) # max_return needs a limit + with pytest.raises(ValueError): + make_spec(holdings=np.array([-1, 0, 0, 0, 0, 0])) + with pytest.raises(ValueError): + make_spec(objective="min_cvar", cvar_limit=None) + + +def test_legacy_position_above_cap_is_sold_down_and_invested_bound(): + holdings = np.zeros(6) + holdings[0] = 1000.0 # far above the 10 % cap + spec = make_spec(cash=10_000.0, holdings=holdings, cvar_limit=50_000.0, + constraints=OptimizationConstraints(max_weight=0.10, max_sector_weight=1.0)) + _, model, res, d = solve(spec) + assert d["weights"][0] <= 0.10 + 1e-9 and d["sells"][0] > 0 + # the single-stock bound is min(max_weight, max_invested) * W, not their product + spec2 = make_spec(cvar_limit=50_000.0, constraints=OptimizationConstraints(max_weight=0.10, max_sector_weight=1.0, + max_invested=0.5)) + problem = MeanCVaRProblem(spec2) + np.testing.assert_array_equal(problem._max_shares(), np.floor(0.10 * spec2.wealth / spec2.prices + 1e-9)) + spec3 = make_spec(cvar_limit=50_000.0, constraints=OptimizationConstraints(max_weight=0.6, max_sector_weight=1.0, + max_invested=0.5)) + np.testing.assert_array_equal(MeanCVaRProblem(spec3)._max_shares(), np.floor(0.5 * spec3.wealth / spec3.prices + 1e-9)) + + +def test_documentation_example_rounding_fails(): + """The two-stock instance of docs/optimization_model.tex, Example 4.1 (numbers as printed there).""" + from scipy.optimize import Bounds, LinearConstraint, milp + + prices = np.array([572.0, 2208.0]) + scen = np.array([[0.005, -0.082], [0.014, -0.052], [-0.069, -0.020], [-0.002, 0.055], + [-0.009, 0.083], [0.048, 0.064], [0.044, 0.028], [-0.011, 0.042]]) + spec = ProblemSpec(symbols=["A", "B"], prices=prices, scenarios=scen, sectors=["X", "Y"], cash=10_000.0, + confidence=0.75, cvar_limit=250.0, costs=TransactionCosts(0.0, 0.0), + constraints=OptimizationConstraints(max_weight=0.7, max_sector_weight=1.0)) + problem = MeanCVaRProblem(spec) + model = problem.build() + # LP relaxation: (2.6605, 1.7876), objective 62.02, CVaR binding + lp = milp(-model.c, constraints=LinearConstraint(model.A, model.row_lb, model.row_ub), + integrality=np.zeros(model.n_vars), bounds=Bounds(model.var_lb, model.var_ub)) + np.testing.assert_allclose(lp.x[:2], [2.6605, 1.7876], atol=1e-4) + assert -lp.fun == pytest.approx(62.02, abs=0.01) + + def evaluate(n): + pnl = scen @ (n * prices) + return pnl.mean(), scenario_cvar(-pnl, 0.75)[1] + + assert evaluate(np.array([3, 2]))[1] == pytest.approx(280.13, abs=0.01) # nearest rounding: infeasible + assert evaluate(np.array([2, 1]))[0] == pytest.approx(35.43, abs=0.01) # rounding down: feasible, poor + res = HighsSolver(SETTINGS).solve(model) + assert res.ok + d = problem.decode(res.x) + np.testing.assert_array_equal(d["shares"], [7, 1]) # the integer optimum has a different structure + assert d["expected_pnl"] == pytest.approx(42.58, abs=0.01) and d["cvar"] == pytest.approx(240.74, abs=0.01) diff --git a/tests/test_returns.py b/tests/test_returns.py new file mode 100644 index 0000000..71e4eda --- /dev/null +++ b/tests/test_returns.py @@ -0,0 +1,67 @@ +import numpy as np +import pandas as pd +import pytest + +from quantcore.measurement.returns import ( + align_prices, horizon_periods, horizon_returns, log_returns, periods_per_year, portfolio_log_returns, resample_prices, +) + + +def test_log_returns_reference(): + p = pd.DataFrame({"A": [100.0, 110.0, 99.0]}, index=pd.bdate_range("2024-01-01", periods=3)) + r = log_returns(p) + assert len(r) == 2 + assert r["A"].iloc[0] == pytest.approx(np.log(1.1)) + assert r["A"].iloc[1] == pytest.approx(np.log(0.9)) + + +def test_horizon_periods(): + assert horizon_periods("day", "daily") == 1 + assert horizon_periods("week", "daily") == 5 + assert horizon_periods("month", "daily") == 21 + assert horizon_periods("week", "weekly") == 1 + assert horizon_periods("month", "weekly") == 4 + assert horizon_periods(10, "daily") == 10 + with pytest.raises(ValueError): + horizon_periods("day", "weekly") + assert periods_per_year("weekly") == 52 + + +def test_align_and_resample(): + idx = pd.bdate_range("2024-01-01", periods=10) + p = pd.DataFrame({"A": np.arange(10.0) + 1, "B": np.arange(10.0) + 1}, index=idx) + p.loc[idx[3], "B"] = np.nan # holiday on B's exchange + a = align_prices(p) + assert a.loc[idx[3], "B"] == a.loc[idx[2], "B"] # forward-filled + w = resample_prices(a, "weekly") + assert all(d.dayofweek == 4 for d in w.index) + assert w["A"].iloc[0] == 5.0 # Friday of the first week + + +def test_horizon_returns_sum(): + r = pd.Series(np.arange(1.0, 11.0) / 100, index=pd.bdate_range("2024-01-01", periods=10)) + h = horizon_returns(r, 5, overlapping=True) + assert len(h) == 6 and h.iloc[0] == pytest.approx(0.15) + nb = horizon_returns(r, 5, overlapping=False) + assert len(nb) == 2 and nb.iloc[-1] == pytest.approx(0.40) + + +def test_portfolio_log_returns_exact(): + r = pd.DataFrame({"A": [np.log(1.1), np.log(0.9)], "B": [np.log(1.0), np.log(1.2)]}) + rp = portfolio_log_returns(r, np.array([0.5, 0.5])) + assert rp.iloc[0] == pytest.approx(np.log(1 + 0.5 * 0.1 + 0.5 * 0.0)) + assert rp.iloc[1] == pytest.approx(np.log(1 + 0.5 * (-0.1) + 0.5 * 0.2)) + + +def test_monthly_resampling_works_on_both_pandas_spellings(): + """pandas renamed the month-end alias 'M' -> 'ME' in 2.2 and dropped 'M' in 3.0; the Mac runs + 2.1 and the container 3.x, so the alias is resolved at import time.""" + import pandas as pd + from quantcore.measurement.returns import MONTH_END, resample_prices + + assert MONTH_END in ("M", "ME") + pd.tseries.frequencies.to_offset(MONTH_END) # accepted by the installed pandas + px = pd.DataFrame({"A": np.arange(400.0)}, index=pd.bdate_range("2024-01-01", periods=400)) + monthly = resample_prices(px, "monthly") + assert len(monthly) == 19 and monthly.index.is_month_end.all() + assert (monthly["A"].diff().dropna() > 0).all() # month-end closes, in order diff --git a/tests/test_risk.py b/tests/test_risk.py new file mode 100644 index 0000000..fd1b0a1 --- /dev/null +++ b/tests/test_risk.py @@ -0,0 +1,208 @@ +import numpy as np +import pandas as pd +import pytest +from scipy import stats + +from quantcore.measurement import risk as rk + + +def test_normal_var_cvar_reference(): + # sigma = 1 %, mu = 0: VaR95 = 1.6449 %, CVaR95 = 2.0627 %, VaR99 = 2.3263 %, CVaR99 = 2.6652 % + assert rk.normal_var(0.0, 0.01, 0.95) == pytest.approx(0.016449, abs=1e-6) + assert rk.normal_cvar(0.0, 0.01, 0.95) == pytest.approx(0.020627, abs=1e-6) + assert rk.normal_var(0.0, 0.01, 0.99) == pytest.approx(0.023263, abs=1e-6) + assert rk.normal_cvar(0.0, 0.01, 0.99) == pytest.approx(0.026652, abs=1e-6) + # a positive mean lowers the loss quantile one-for-one + assert rk.normal_var(0.001, 0.01, 0.95) == pytest.approx(0.015449, abs=1e-6) + + +def test_historical_var_cvar_reference(): + r = pd.Series(np.arange(-0.10, 0.10, 0.001)) # 200 equally spaced returns + var95 = rk.historical_var(r, 0.95) + assert var95 == pytest.approx(-np.quantile(r, 0.05)) + cvar95 = rk.historical_cvar(r, 0.95) + tail = r[r <= np.quantile(r, 0.05)] + assert cvar95 == pytest.approx(-tail.mean()) + assert cvar95 > var95 + + +def test_scenario_cvar_is_tail_mean(): + losses = np.array([1.0, 5.0, 3.0, 9.0, 7.0, 2.0, 8.0, 4.0, 6.0, 10.0]) # S = 10 + var, cvar = rk.scenario_cvar(losses, 0.80) # k = 2 -> mean of the two largest + assert cvar == pytest.approx((10 + 9) / 2) + assert var == pytest.approx(9.0) + var, cvar = rk.scenario_cvar(losses, 0.95) # k = 0.5 -> fractional tail: 0.5 * 10 / 0.5 + assert cvar == pytest.approx(10.0) + var, cvar = rk.scenario_cvar(losses, 0.85) # k = 1.5 -> (10 + 0.5 * 9) / 1.5 + assert cvar == pytest.approx((10 + 0.5 * 9) / 1.5) + assert var == pytest.approx(9.0) + + +def test_scenario_cvar_matches_ru_minimisation(): + rng = np.random.default_rng(0) + losses = rng.standard_normal(500) + beta = 0.95 + _, cvar = rk.scenario_cvar(losses, beta) + grid = np.linspace(-1, 4, 20001) + ru = np.min([a + np.maximum(losses - a, 0).mean() / (1 - beta) for a in grid]) + assert cvar == pytest.approx(ru, abs=1e-3) + + +def test_loss_to_money(): + assert rk.loss_to_money(0.0, 1000.0) == 0.0 + assert rk.loss_to_money(np.log(1 / 0.9), 1000.0) == pytest.approx(100.0) + + +def test_asset_risk_table_columns_and_scaling(): + rng = np.random.default_rng(1) + r = pd.DataFrame(rng.standard_normal((1500, 2)) * 0.01, index=pd.bdate_range("2019-01-01", periods=1500), columns=["A", "B"]) + day = rk.asset_risk_table(r, "daily", 200, 0.94, (0.95,), "day") + week = rk.asset_risk_table(r, "daily", 200, 0.94, (0.95,), "week") + assert {"vol_rolling_ann", "vol_ewm_ann", "var95_normal_rolling", "cvar95_hist"} <= set(day.columns) + assert week["horizon_periods"].iloc[0] == 5 + # sqrt(5) scaling of the Gaussian VaR for a zero-mean window (approximately, mean is small) + ratio = week["var95_normal_rolling"] / day["var95_normal_rolling"] + assert ratio.between(2.0, 2.5).all() + assert (day["cvar95_normal_ewm"] > day["var95_normal_ewm"]).all() + + +def test_portfolio_risk_normal_vs_scenarios(md): + svc = rk.RiskService(md) + holdings = {"S0": 100, "S1": 50, "S2": 200} + pr = svc.portfolio_risk(holdings, cash=1000.0, horizon="week", method="normal") + assert pr.value > 0 and set(pr.confidence) == {"0.95", "0.99"} + assert pr.confidence["0.99"]["cvar_money"] > pr.confidence["0.95"]["cvar_money"] > 0 + # historical estimate is of the same order of magnitude + ph = svc.portfolio_risk(holdings, cash=1000.0, horizon="week", method="historical") + assert 0.3 < ph.confidence["0.95"]["cvar_money"] / pr.confidence["0.95"]["cvar_money"] < 3.0 + # scenarios: Gaussian scenarios with the same covariance reproduce the normal CVaR + rng = np.random.default_rng(0) + r = md.returns(list(holdings), currency="base", freq="daily") + from quantcore.measurement import estimators as est + cov = est.rolling_cov(r, 200) + mu_h = r.iloc[-200:].mean().values * 5 + scen = rng.multivariate_normal(mu_h, cov.values * 5, size=200_000) + ps = svc.portfolio_risk(holdings, cash=1000.0, horizon="week", method="scenarios", scenarios=scen) + # differences: sampling noise and the exact money mapping (exp) vs. the log-return closed form + assert ps.confidence["0.95"]["cvar_money"] == pytest.approx(pr.confidence["0.95"]["cvar_money"], rel=0.06) + + +def test_a_one_asset_portfolio_is_a_valid_request(md): + """``np.cov`` of a single column returns a 0-d array; the rolling estimator has to keep it 2-d. + + The simplest call anyone makes -- the risk of a single position -- went through this path. + """ + svc = rk.RiskService(md) + from quantcore.measurement import estimators as est + + r = md.returns(["S0"], currency="base", freq="daily") + assert est.rolling_cov(r, 200).shape == (1, 1) + assert est.ewm_cov(r, 0.94).shape == (1, 1) + for method in ("normal", "historical"): + for cov_method in ("rolling", "ewm"): + pr = svc.portfolio_risk({"S0": 100}, cash=1000.0, horizon="week", method=method, + cov_method=cov_method) + assert pr.value > 0 and pr.confidence["0.95"]["cvar_money"] > 0 + # a one-asset portfolio is the asset: the parametric VaR is z * sd_h - mean_h exactly + one = svc.portfolio_risk({"S0": 100}, cash=0.0, horizon="week", method="normal", cov_method="rolling") + sd_week = float(np.sqrt(est.rolling_cov(r, 200).iloc[0, 0] * 5)) + mean_week = float(r.iloc[-200:].mean().iloc[0] * 5) + assert one.confidence["0.95"]["var"] == pytest.approx(1.6448536269514722 * sd_week - mean_week, rel=1e-9) + + +def test_weekly_reference_values_from_documentation(): + """docs/measurement_and_models.tex, Example 4.1: sigma = 1 % daily, one week, V0 = 100 000.""" + s_h = 0.01 * np.sqrt(5) + assert rk.normal_var(0.0, s_h, 0.95) == pytest.approx(0.036780, abs=1e-6) + assert rk.normal_cvar(0.0, s_h, 0.95) == pytest.approx(0.046124, abs=1e-6) + assert rk.normal_var(0.0, s_h, 0.99) == pytest.approx(0.052019, abs=1e-6) + assert rk.normal_cvar(0.0, s_h, 0.99) == pytest.approx(0.059596, abs=1e-6) + v0 = 100_000.0 + assert rk.loss_to_money(rk.normal_var(0.0, s_h, 0.95), v0) == pytest.approx(3611.2, abs=0.1) + assert rk.loss_to_money(rk.normal_cvar(0.0, s_h, 0.95), v0) == pytest.approx(4507.6, abs=0.1) + assert rk.loss_to_money(rk.normal_var(0.0, s_h, 0.99), v0) == pytest.approx(5068.9, abs=0.1) + assert rk.loss_to_money(rk.normal_cvar(0.0, s_h, 0.99), v0) == pytest.approx(5785.5, abs=0.1) + + +def test_weighted_quantile_matches_numpy_and_handles_weights(): + rng = np.random.default_rng(3) + x = rng.standard_normal(5000) + qs = (0.01, 0.05, 0.5, 0.95) + np.testing.assert_allclose(rk.weighted_quantile(x, qs), np.quantile(x, qs), rtol=1e-12) + # equal weights reproduce the unweighted result (the weighted convention places the + # observations at the midpoints of their probability steps, so the tails differ by half a rank) + w = np.full(len(x), 1 / len(x)) + np.testing.assert_allclose(rk.weighted_quantile(x, qs, w), np.quantile(x, qs), atol=0.01) + # column-wise on a matrix + m = rng.standard_normal((2000, 3)) + np.testing.assert_allclose(rk.weighted_quantile(m, (0.1, 0.9)), np.quantile(m, (0.1, 0.9), axis=0), rtol=1e-12) + # a sample that over-represents the left tail, corrected by its weights, recovers the standard normal + tilted = np.concatenate([rng.standard_normal(4000), rng.standard_normal(4000) - 2.0]) + p = np.concatenate([np.full(4000, 1.0), stats.norm.pdf(tilted[4000:]) / stats.norm.pdf(tilted[4000:] + 2.0)]) + p = p / p.sum() + for q in (0.01, 0.05, 0.5): + assert rk.weighted_quantile(tilted, q, p)[0] == pytest.approx(stats.norm.ppf(q), abs=0.12) + + +def test_value_loss_maps_a_log_loss_to_a_share_of_the_position(): + assert rk.value_loss(0.0) == pytest.approx(0.0) + assert rk.value_loss(0.30) == pytest.approx(1 - np.exp(-0.30)) # 25.9 %, not 30 % + assert rk.value_loss(5.0) < 1.0 # can never reach 100 % + np.testing.assert_allclose(rk.value_loss([0.01, 0.1]), [1 - np.exp(-0.01), 1 - np.exp(-0.1)]) + # a money loss is the value-space loss times the position + assert rk.loss_to_money(0.30, 1_000.0) == pytest.approx(1_000.0 * rk.value_loss(0.30)) + + +def test_value_space_var_transforms_exactly_but_cvar_needs_the_sigma_squared_term(): + """``1 - e^{-x}`` is monotone, so a VaR carries over unchanged; a CVaR is a tail *mean* and + the concavity costs a ``sigma^2 / 2`` term -- the same correction as the Ito drift.""" + m, s, beta = 0.01, 0.30, 0.95 + r = np.random.default_rng(1).normal(m, s, 2_000_000) + loss = 1 - np.exp(r) + q = np.quantile(loss, beta) + assert rk.normal_value_var(m, s, beta) == pytest.approx(q, abs=2e-3) + assert rk.normal_value_var(m, s, beta) == pytest.approx(rk.value_loss(rk.normal_var(m, s, beta)), rel=1e-12) + exact = rk.normal_value_cvar(m, s, beta) + assert exact == pytest.approx(loss[loss >= q].mean(), abs=2e-3) + naive = rk.value_loss(rk.normal_cvar(m, s, beta)) # what converting afterwards would give + assert naive > exact # concavity: the naive figure overstates + assert 0.002 < naive - exact < 0.005 + # the correction vanishes as sigma -> 0 and grows with sigma + assert rk.value_loss(rk.normal_cvar(m, 0.01, beta)) - rk.normal_value_cvar(m, 0.01, beta) < 1e-5 + assert rk.value_loss(rk.normal_cvar(m, 0.6, beta)) - rk.normal_value_cvar(m, 0.6, beta) > 0.005 + assert rk.normal_value_cvar(m, 3.0, beta) < 1.0 # never a total loss, whatever the volatility + + +def test_asset_risk_table_in_value_space(): + rng = np.random.default_rng(5) + r = pd.DataFrame(rng.standard_normal((1500, 2)) * np.array([0.012, 0.03]), + index=pd.bdate_range("2019-01-01", periods=1500), columns=["A", "B"]) + log = rk.asset_risk_table(r, "daily", horizon="month", confidence=(0.95,)) + val = rk.asset_risk_table(r, "daily", horizon="month", confidence=(0.95,), space="value") + for col in ("var95_normal_ewm", "var95_hist"): + np.testing.assert_allclose(val[col], rk.value_loss(log[col]), rtol=1e-7) # quantiles: exact + for col in ("cvar95_normal_ewm", "cvar95_hist"): + assert (val[col] < rk.value_loss(log[col])).all() # tail means: corrected + assert (val[col] < 1.0).all() and (val[col] > 0).all() + np.testing.assert_allclose(val["vol_ewm_ann"], log["vol_ewm_ann"]) # only the losses move + with pytest.raises(ValueError): + rk.asset_risk_table(r, "daily", space="nonsense") + + +def test_zero_drift_removes_the_extrapolated_mean_from_the_parametric_risk(): + """At a quarterly horizon the estimated mean dominates the VaR: a stock with a strong recent + rally would otherwise look *less* risky than a quiet one.""" + rng = np.random.default_rng(11) + r = pd.DataFrame({"trend": rng.standard_normal(1500) * 0.02 + 0.004, # strong drift + "quiet": rng.standard_normal(1500) * 0.02}, + index=pd.bdate_range("2019-01-01", periods=1500)) + est_ = rk.asset_risk_table(r, "daily", horizon="quarter", confidence=(0.95,)) + zero = rk.asset_risk_table(r, "daily", horizon="quarter", confidence=(0.95,), drift="zero") + assert est_.loc["trend", "var95_normal_rolling"] < est_.loc["quiet", "var95_normal_rolling"] + assert zero.loc["trend", "var95_normal_rolling"] == pytest.approx( + zero.loc["quiet", "var95_normal_rolling"], rel=0.05) # same volatility -> same risk + assert zero.loc["trend", "var95_normal_rolling"] > est_.loc["trend", "var95_normal_rolling"] + # the historical columns keep the drift of their window either way + assert zero.loc["trend", "var95_hist"] == est_.loc["trend", "var95_hist"] + with pytest.raises(ValueError): + rk.asset_risk_table(r, "daily", drift="wishful") diff --git a/tests/test_rowgen_gaussian.py b/tests/test_rowgen_gaussian.py new file mode 100644 index 0000000..7b84d85 --- /dev/null +++ b/tests/test_rowgen_gaussian.py @@ -0,0 +1,275 @@ +"""Weighted scenarios, tail-row generation and the exact Gaussian (outer-approximation) mode.""" + +import itertools + +import numpy as np +import pytest +from scipy.stats import norm + +from quantcore.config import Settings +from quantcore.models import sampling as smp +from quantcore.optimization.gaussian_exact import solve_gaussian_exact +from quantcore.optimization.problem import ( + MeanCVaRProblem, + OptimizationConstraints, + ProblemSpec, + TransactionCosts, + cvar_multiplier, +) +from quantcore.optimization.rowgen import restricted_spec, solve_by_row_generation +from quantcore.optimization.solvers import HighsSolver + +SETTINGS = Settings(cuopt_url="http://127.0.0.1:9", mip_rel_gap=1e-6) + + +def highs(model): + return HighsSolver(SETTINGS).solve(model, time_limit=120) + + +def gaussian_inputs(seed=0, n=6): + rng = np.random.default_rng(seed) + symbols = [f"S{i}" for i in range(n)] + prices = rng.uniform(20, 400, n) + vol = rng.uniform(0.02, 0.06, n) + corr = 0.4 * np.ones((n, n)) + 0.6 * np.eye(n) + cov = corr * np.outer(vol, vol) + mean = rng.uniform(0.0, 0.006, n) + sectors = ["Tech", "Fin", "Health"] * (n // 3) + ["Tech"] * (n % 3) + return symbols, prices, mean, cov, sectors + + +def gaussian_scenarios(mean, cov, S, seed=1): + z = smp.moment_match(smp.standard_normal_sample(S, len(mean), "sobol", seed)) + return mean + z @ smp.pca_root(cov).T + + +def test_weighted_scenarios_equal_duplicated_scenarios(): + symbols, prices, mean, cov, sectors = gaussian_inputs() + scen = gaussian_scenarios(mean, cov, 256) + dup = np.vstack([scen, scen[:64]]) # the first 64 scenarios counted twice + p = np.ones(256) + p[:64] = 2 + p /= p.sum() + common = dict(symbols=symbols, prices=prices, sectors=sectors, cash=100_000.0, cvar_limit=3_000.0) + a = MeanCVaRProblem(ProblemSpec(scenarios=dup, **common)) + b = MeanCVaRProblem(ProblemSpec(scenarios=scen, probabilities=p, **common)) + ra, rb = highs(a.build()), highs(b.build()) + assert ra.ok and rb.ok + da, db = a.decode(ra.x), b.decode(rb.x) + assert abs(ra.objective - rb.objective) < 1e-6 * abs(ra.objective) + np.testing.assert_array_equal(da["shares"], db["shares"]) + assert abs(da["cvar"] - db["cvar"]) < 1e-6 and abs(da["expected_pnl"] - db["expected_pnl"]) < 1e-6 + + +def test_expected_returns_drive_the_objective(): + symbols, prices, mean, cov, sectors = gaussian_inputs() + scen = gaussian_scenarios(mean, cov, 256) + common = dict(symbols=symbols, prices=prices, scenarios=scen, sectors=sectors, cash=100_000.0, cvar_limit=3_000.0, + costs=TransactionCosts(proportional_buy=0.0)) + shifted = mean + np.array([0.01, 0, 0, 0, 0, 0]) # the model says S0 earns 1 % more than the scenarios show + prob = MeanCVaRProblem(ProblemSpec(expected_returns=shifted, **common)) + res = highs(prob.build()) + d = prob.decode(res.x) + assert abs(d["expected_pnl"] - (shifted @ d["value"])) < 1e-6 # objective uses the model mean ... + assert d["expected_pnl"] > d["expected_pnl_scenarios"] + 0.005 * d["value"][0] - 1e-6 # ... not the sample mean + assert d["shares"][0] > 0 + with pytest.raises(ValueError): + ProblemSpec(expected_returns=np.zeros(3), **common) + + +def test_row_generation_matches_full_milp(): + symbols, prices, mean, cov, sectors = gaussian_inputs(seed=2) + scen = gaussian_scenarios(mean, cov, 2048) + spec = ProblemSpec(symbols=symbols, prices=prices, scenarios=scen, sectors=sectors, cash=100_000.0, + cvar_limit=3_000.0, constraints=OptimizationConstraints(max_weight=0.3, max_sector_weight=0.6)) + full = MeanCVaRProblem(spec) + res_full = highs(full.build()) + res_rg, problem_rg, info = solve_by_row_generation(spec, highs) + assert res_rg.ok and info.converged and info.iterations >= 1 + assert max(info.working_set_sizes) < 0.5 * spec.n_scenarios # solved on a fraction of the rows + d_full, d_rg = full.decode(res_full.x), problem_rg.decode(res_rg.x) + assert abs(d_rg["expected_pnl"] - d_full["expected_pnl"]) <= 1e-5 * abs(d_full["expected_pnl"]) + 1e-6 + assert d_rg["cvar"] <= 3_000.0 * (1 + 1e-7) + # the expanded solution vector is feasible for the full model: u_s >= L_s - alpha for every scenario + m = full.build() + Ax = m.A @ res_rg.x + assert (Ax >= m.row_lb - 1e-6).all() and (Ax <= m.row_ub + 1e-6).all() + # a restricted spec keeps the full-set probabilities and expected returns + sub = restricted_spec(spec, np.arange(10)) + assert sub.n_scenarios == 10 and abs(sub.probabilities.sum() - 10 / 2048) < 1e-12 + np.testing.assert_allclose(sub.expected_returns, spec.mean_returns) + + +def test_row_generation_rebalancing_mode(): + symbols, prices, mean, cov, sectors = gaussian_inputs(seed=3) + scen = gaussian_scenarios(mean, cov, 1024) + holdings = np.array([40, 0, 10, 0, 5, 0], dtype=float) + spec = ProblemSpec(symbols=symbols, prices=prices, scenarios=scen, sectors=sectors, cash=20_000.0, + holdings=holdings, cvar_limit=2_500.0, constraints=OptimizationConstraints(max_weight=0.3, max_sector_weight=0.6)) + res, problem, info = solve_by_row_generation(spec, highs) + assert res.ok and info.converged + d = problem.decode(res.x) + np.testing.assert_array_equal(d["shares"], holdings + d["buys"] - d["sells"]) + assert d["cvar"] <= 2_500.0 * (1 + 1e-7) and d["cash_end"] >= -1e-6 + + +def brute_force_gaussian(prices, mean, cov, sectors, cash, limit, beta, max_weight, costs): + """Enumerate all integer share vectors within the caps (small instance).""" + ub = np.floor(max_weight * cash / prices).astype(int) + k = cvar_multiplier(beta) + best, best_n = -np.inf, None + for shares in itertools.product(*[range(u + 1) for u in ub]): + s = np.array(shares, dtype=float) + v = s * prices + tc = costs * v.sum() + if v.sum() + tc > cash + 1e-9: + continue + pnl = mean @ v - tc + cvar = -pnl + k * np.sqrt(v @ cov @ v) + if cvar <= limit + 1e-9 and pnl > best: + best, best_n = pnl, s + return best, best_n + + +def test_gaussian_exact_matches_brute_force(): + prices = np.array([400.0, 650.0, 900.0]) + vol = np.array([0.03, 0.05, 0.04]) + corr = np.array([[1, 0.5, 0.2], [0.5, 1, 0.3], [0.2, 0.3, 1]]) + cov = corr * np.outer(vol, vol) + mean = np.array([0.003, 0.006, 0.004]) + sectors = ["A", "B", "C"] + cash, limit, beta = 10_000.0, 500.0, 0.95 + costs = TransactionCosts(proportional_buy=0.002) + spec = ProblemSpec(symbols=list("XYZ"), prices=prices, scenarios=None, sectors=sectors, cash=cash, cvar_limit=limit, + confidence=beta, gaussian_mean=mean, gaussian_cov=cov, costs=costs, + constraints=OptimizationConstraints(max_weight=0.6, max_sector_weight=1.0)) + res, problem, info = solve_gaussian_exact(spec, highs, tol=1e-9) + assert res.ok and info.converged and info.cuts >= 2 + d = problem.decode(res.x) + best, best_n = brute_force_gaussian(prices, mean, cov, sectors, cash, limit, beta, 0.6, 0.002) + np.testing.assert_array_equal(d["shares"], best_n) + assert abs(d["expected_pnl"] - best) < 1e-6 + assert d["cvar"] <= limit * (1 + 1e-9) + # closed-form risk figures of the decoded portfolio + v = d["value"] + sigma = np.sqrt(v @ cov @ v) + assert abs(d["sigma"] - sigma) < 1e-9 + assert abs(d["var"] - (-(mean @ v - d["total_cost"]) + norm.ppf(beta) * sigma)) < 1e-9 + assert res.reproducible and "outer approximation" in res.message + + +def test_gaussian_exact_other_objectives_and_validation(): + symbols, prices, mean, cov, sectors = gaussian_inputs(seed=4) + base = dict(symbols=symbols, prices=prices, scenarios=None, sectors=sectors, cash=100_000.0, + gaussian_mean=mean, gaussian_cov=cov) + # min CVaR at a target expected P&L: converges, meets the target, epigraph equals the true CVaR + spec = ProblemSpec(objective="min_cvar", target_pnl=150.0, **base) + res, problem, info = solve_gaussian_exact(spec, highs) + d = problem.decode(res.x) + assert info.converged and d["expected_pnl"] >= 150.0 - 1e-6 + assert abs(d["cvar"] - d["cvar_model"]) <= 1e-6 * max(d["cvar"], 1.0) + # utility objective + spec_u = ProblemSpec(objective="utility", risk_aversion=0.05, **base) # at 0.5 staying in cash is optimal + res_u, problem_u, info_u = solve_gaussian_exact(spec_u, highs) + du = problem_u.decode(res_u.x) + assert info_u.converged and du["invested"] > 0 + # the CVaR-limited solution is on the mean-variance frontier: no feasible portfolio with the same + # expected P&L has a smaller sigma (checked on the continuous relaxation via the cut model) + spec_m = ProblemSpec(cvar_limit=3_000.0, **base) + res_m, problem_m, _ = solve_gaussian_exact(spec_m, highs) + dm = problem_m.decode(res_m.x) + assert dm["cvar"] <= 3_000.0 * (1 + 1e-6) + with pytest.raises(ValueError): + ProblemSpec(symbols=symbols, prices=prices, scenarios=None, sectors=sectors, cash=1.0, cvar_limit=1.0) + with pytest.raises(ValueError): + MeanCVaRProblem(spec_m).build() # no cuts yet + with pytest.raises(ValueError): + solve_by_row_generation(spec_m, highs) + + +def test_moment_matched_sobol_scenarios_reproduce_the_exact_optimum(): + """The sample-average approximation with Gaussian scenarios (used linearly) converges to the exact + mean-variance-equivalent optimum; plain Monte Carlo at the same size does not.""" + symbols, prices, mean, cov, sectors = gaussian_inputs(seed=6, n=8) + common = dict(symbols=symbols, prices=prices, sectors=sectors, cash=1_000_000.0, cvar_limit=30_000.0, + costs=TransactionCosts(proportional_buy=0.0), + constraints=OptimizationConstraints(max_weight=0.25, max_sector_weight=0.6)) + exact_spec = ProblemSpec(scenarios=None, gaussian_mean=mean, gaussian_cov=cov, **common) + res_e, prob_e, _info = solve_gaussian_exact(exact_spec, highs, tol=1e-8) + d_e = prob_e.decode(res_e.x) + k = cvar_multiplier(0.95) + + def true_cvar(v): + return -(mean @ v) + k * np.sqrt(v @ cov @ v) + + # moment-matched Sobol' scenarios, model mean in the objective + scen = gaussian_scenarios(mean, cov, 4096, seed=3) + prob_s = MeanCVaRProblem(ProblemSpec(scenarios=scen, expected_returns=mean, **common)) + d_s = prob_s.decode(highs(prob_s.build()).x) + assert abs(d_s["expected_pnl"] - d_e["expected_pnl"]) <= 0.01 * d_e["expected_pnl"] + assert true_cvar(d_s["value"]) <= 30_000.0 * 1.02 + # plain Monte Carlo of the same size with the sample mean in the objective (the old default) + rng = np.random.default_rng(3) + mc = mean + rng.standard_normal((4096, 8)) @ np.linalg.cholesky(cov).T + prob_m = MeanCVaRProblem(ProblemSpec(scenarios=mc, **common)) + d_m = prob_m.decode(highs(prob_m.build()).x) + true_pnl_mc = mean @ d_m["value"] + assert abs(true_pnl_mc - d_e["expected_pnl"]) > abs(d_s["expected_pnl"] - d_e["expected_pnl"]) + assert d_m["expected_pnl"] > true_pnl_mc # in-sample optimism of the plain MC solution + + +def test_row_generation_stops_at_the_time_budget(): + """The time limit is the budget of the whole request, not of each pass. + + Every pass is a MILP of its own, so a deadline that has already passed must end the loop with + the incumbent (feasible, not proven optimal) instead of starting another solve. + """ + import time + + symbols, prices, mean, cov, sectors = gaussian_inputs(seed=2) + scen = gaussian_scenarios(mean, cov, 2048) + spec = ProblemSpec(symbols=symbols, prices=prices, scenarios=scen, sectors=sectors, cash=100_000.0, + cvar_limit=3_000.0, constraints=OptimizationConstraints(max_weight=0.3, max_sector_weight=0.6)) + calls = [] + + def counted(model): + calls.append(1) + return highs(model) + + res, problem, info = solve_by_row_generation(spec, counted, deadline=time.monotonic() - 1.0) + assert len(calls) == 1 and info.iterations == 1 and not info.converged + assert res.status == "FeasibleFound" and "time limit" in res.message + d = problem.decode(res.x) # the incumbent is a real portfolio, measured on all scenarios + assert d["shares"].sum() > 0 and np.isfinite(d["cvar"]) + # with a budget in the future the loop runs to convergence as before + res2, _, info2 = solve_by_row_generation(spec, highs, deadline=time.monotonic() + 600) + assert info2.converged and res2.status == "Optimal" + + +def test_the_solve_function_hands_on_the_remaining_budget(): + from quantcore.optimization.service import OptimizationRequest, OptimizationService + + svc = OptimizationService.__new__(OptimizationService) # no data needed for this + svc.settings = SETTINGS + req = OptimizationRequest(time_limit=30.0, solver="highs", cvar_limit=1.0) + limits = [] + deadline = __import__("time").monotonic() + 30.0 + + class Recorder: + def __init__(self, settings): + pass + + def solve(self, model, time_limit=None): + limits.append(time_limit) + return None + + import quantcore.optimization.service as service_module + original = service_module.HighsSolver + service_module.HighsSolver = Recorder + try: + fn = svc._solve_fn(req, prefer_local=True, deadline=deadline) + fn(None) + fn(None) + finally: + service_module.HighsSolver = original + assert 0 < limits[1] <= limits[0] <= 30.0 # each call gets what is left, never the full limit again diff --git a/tests/test_sampling.py b/tests/test_sampling.py new file mode 100644 index 0000000..fac1ffa --- /dev/null +++ b/tests/test_sampling.py @@ -0,0 +1,122 @@ +import numpy as np +import pytest +from scipy.stats import norm + +from quantcore.models import sampling as smp + + +def test_next_power_of_two(): + assert [smp.next_power_of_two(n) for n in (1, 2, 3, 2000, 2048, 2049, 100_000)] == [1, 2, 4, 2048, 2048, 4096, 131072] + + +def test_standard_normal_sample_methods(): + for method in smp.SAMPLING_METHODS: + z = smp.standard_normal_sample(1024, 5, method, seed=3) + assert z.shape == (1024, 5) and np.isfinite(z).all() + anti = smp.standard_normal_sample(1000, 3, "antithetic", seed=1) + np.testing.assert_allclose(anti[:500], -anti[500:], atol=1e-15) # exact zero mean by construction + with pytest.raises(ValueError): + smp.standard_normal_sample(1000, 3, "sobol") # not a power of two + with pytest.raises(ValueError): + smp.standard_normal_sample(1024, 3, "bogus") + # Sobol' points are deterministic for a seed and stratify the marginals far better than MC + a = smp.standard_normal_sample(2048, 4, "sobol", seed=7) + b = smp.standard_normal_sample(2048, 4, "sobol", seed=7) + np.testing.assert_array_equal(a, b) + lhs = smp.standard_normal_sample(4096, 2, "lhs", seed=7) + counts = np.histogram(norm.cdf(lhs[:, 0]), bins=64, range=(0, 1))[0] + assert counts.min() == counts.max() == 64 # exactly 64 points per stratum + + +def test_pca_root(): + rng = np.random.default_rng(0) + L = rng.standard_normal((6, 6)) + cov = L @ L.T + a = smp.pca_root(cov) + np.testing.assert_allclose(a @ a.T, cov, atol=1e-10) + vals, vecs = np.linalg.eigh(cov) + # first column = largest-variance direction, columns orthogonal + assert abs(abs(a[:, 0] @ vecs[:, -1]) - np.sqrt(vals[-1])) < 1e-9 + np.testing.assert_allclose(a.T @ a, np.diag(np.sort(vals)[::-1]), atol=1e-9) + + +def test_moment_match_exact_and_weighted(): + rng = np.random.default_rng(1) + z = rng.standard_normal((500, 7)) * 1.3 + 0.2 + m = smp.moment_match(z) + np.testing.assert_allclose(m.mean(axis=0), 0.0, atol=1e-13) + np.testing.assert_allclose(m.T @ m / len(m), np.eye(7), atol=1e-12) + p = rng.uniform(0.5, 2.0, 500) + p /= p.sum() + mw = smp.moment_match(z, p, target_variance=5.0) + np.testing.assert_allclose(p @ mw, 0.0, atol=1e-12) + np.testing.assert_allclose((mw * p[:, None]).T @ mw, 5.0 * np.eye(7), atol=1e-11) + with pytest.raises(ValueError): + smp.moment_match(rng.standard_normal((5, 7))) + + +def test_tilt_normal_sample_weights_bounded_and_tail_enriched(): + rng = np.random.default_rng(2) + n, d = 4096, 8 + z = smp.standard_normal_sample(n, d, "sobol", seed=5) + cov = np.eye(d) * 0.01 + root = smp.pca_root(cov) + exposure = rng.uniform(1, 3, d) * 1000 + direction = smp.loss_direction(root, exposure) + assert abs(np.linalg.norm(direction) - 1) < 1e-12 + beta = 0.95 + zt, p = smp.tilt_normal_sample(z, direction, norm.ppf(beta), mixture=0.5) + assert p.shape == (n,) and abs(p.sum() - 1) < 1e-12 and (p > 0).all() + assert p.max() <= 1.0 / (1 - 0.5) / n * 1.1 # likelihood ratios bounded by 1/(1-mixture), self-normalised + ess = smp.effective_sample_size(p) + assert 0.5 * n < ess < n + # the loss of the reference exposure exceeds its VaR far more often than 5 % of the time + loss_nominal = -(z @ root.T) @ exposure + loss_tilted = -(zt @ root.T) @ exposure + var = np.quantile(loss_nominal, beta) + assert (loss_tilted > var).mean() > 0.2 + # ... while the weighted tail probability is still ~ 1 - beta (unbiased) + assert abs(p[loss_tilted > var].sum() - (1 - beta)) < 0.01 + with pytest.raises(ValueError): + smp.tilt_normal_sample(z, direction, 1.0, mixture=1.0) + + +def test_tilted_day_probabilities_and_resampling(): + rng = np.random.default_rng(3) + scores = rng.standard_t(5, 2000) + q, theta = smp.tilted_day_probabilities(scores, target_mean=0.7) + assert theta > 0 and abs(q @ scores - 0.7) < 1e-8 and abs(q.sum() - 1) < 1e-12 + q0, theta0 = smp.tilted_day_probabilities(scores, target_mean=-1.0) + assert theta0 == 0.0 and np.allclose(q0, 1 / 2000) + with pytest.raises(ValueError): + smp.tilted_day_probabilities(scores, target_mean=scores.max() + 1) + idx, p, _theta = smp.tilted_resample(scores, 1000, 5, 0.7, seed=1, mixture=0.5) + assert idx.shape == (1000, 5) and abs(p.sum() - 1) < 1e-12 + assert p.max() <= 2.0 / 1000 * 1.1 # likelihood ratios <= 1/(1-mixture), self-normalised + # tilted half draws high scores more often + assert scores[idx[:500]].mean() > scores[idx[500:]].mean() + 0.3 + + +def test_resample_indices_methods(): + T, S, h = 300, 1024, 5 + mc = smp.resample_indices(T, S, h, "mc", seed=0) + assert mc.shape == (S, h) and mc.min() >= 0 and mc.max() < T + bal = smp.resample_indices(T, S, h, "balanced", seed=0) + counts = np.bincount(bal.ravel(), minlength=T) + assert counts.max() - counts.min() <= 1 # every observation used equally often + order = np.argsort(np.random.default_rng(1).standard_normal(T)) + sob = smp.resample_indices(T, S, h, "sobol", seed=0, order=order) + assert sob.shape == (S, h) and set(np.unique(sob)) <= set(range(T)) + ranks = smp.resample_indices(T, S, h, "sobol", seed=0) + np.testing.assert_array_equal(order[ranks], sob) + with pytest.raises(ValueError): + smp.resample_indices(T, 1000, h, "sobol") + + +def test_scenario_hash(): + rng = np.random.default_rng(4) + x = rng.standard_normal((50, 3)) + h = smp.scenario_hash(x) + assert len(h) == 16 and h == smp.scenario_hash(x.copy()) + assert h != smp.scenario_hash(x + 1e-9) + assert h != smp.scenario_hash(x, np.full(50, 0.02)) diff --git a/tests/test_service_equivalence.py b/tests/test_service_equivalence.py new file mode 100644 index 0000000..2f7ee46 --- /dev/null +++ b/tests/test_service_equivalence.py @@ -0,0 +1,281 @@ +"""Service-level checks: the scenario optimiser reproduces the exact Gaussian (mean-variance-equivalent) +optimum when the scenarios are moment-matched Sobol' points, the drift convention is arithmetic, and +importance sampling / row generation leave the optimum unchanged.""" + +import numpy as np +import pandas as pd +import pytest + +from quantcore.optimization.problem import MeanCVaRProblem +from quantcore.optimization.service import OptimizationRequest, OptimizationService +from tests.conftest import SYMBOLS + +BASE = dict(symbols=SYMBOLS, cash=500_000.0, cvar_limit=12_000.0, solver="highs", horizon="week", + costs={"proportional_buy": 0.0, "proportional_sell": 0.0}, importance_sampling="off", + constraints={"max_weight": 0.15, "max_sector_weight": 0.4}) + + +@pytest.fixture(scope="module") +def svc(settings, md): + return OptimizationService(settings, md) + + +def gaussian_cvar_of(svc, req, result): + """Closed-form CVaR of a proposal under the Gaussian model of the request.""" + symbols = svc.candidate_symbols(req) + mu_ann, _, _ = svc.expected_returns(req, symbols) + spec, _ = svc.build_gaussian_spec(req, symbols, mu_ann) + shares = {p["symbol"]: p["shares"] for p in result.positions} + value = np.array([shares.get(s, 0) for s in symbols]) * spec.prices + return MeanCVaRProblem(spec).gaussian_risk(value, result.total_cost) + + +def test_sobol_moment_matched_scenarios_reproduce_the_exact_optimum(svc): + exact = svc.optimize(OptimizationRequest(scenario_model="gaussian_exact", **BASE)) + assert exact.status == "Optimal" and exact.reproducible and exact.solver_info["outer_approximation"]["converged"] + scen = svc.optimize(OptimizationRequest(scenario_model="gbm", n_scenarios=4096, **BASE)) + assert scen.status == "Optimal" and scen.model["scenarios"]["moment_matched"] + # The GBM scenarios are lognormal in simple returns while the exact mode is normal: at weekly + # volatilities of 4-5 % the lognormal left tail is ~2-3 % thinner, so the scenario optimum takes + # ~3 % more exposure. Within that model difference the two agree (the sharp test of the sampling + # itself, with Gaussian scenarios used linearly, is in test_rowgen_gaussian.py). + assert abs(scen.expected_pnl - exact.expected_pnl) <= 0.05 * exact.expected_pnl + _, cvar_true, _ = gaussian_cvar_of(svc, OptimizationRequest(scenario_model="gaussian_exact", **BASE), scen) + assert cvar_true <= BASE["cvar_limit"] * 1.05 + # the expected horizon return reported per position is the model's (arithmetic) expectation + mu_ann, _, _ = svc.expected_returns(OptimizationRequest(**BASE), SYMBOLS) + for p in scen.positions: + assert abs(p["expected_return_horizon"] - mu_ann[p["symbol"]] / 252 * 5) < 1e-12 + # plain Monte Carlo without moment matching: the sample mean disagrees with the model mean + mc = svc.optimize(OptimizationRequest(scenario_model="gbm", n_scenarios=4096, sampling="mc", moment_match=False, **BASE)) + assert mc.status == "Optimal" + dev_mc = abs(mc.solver_info["objective"] - mc.expected_pnl) # objective uses the model mean either way + assert dev_mc < 1e-6 # (the objective is always the model expectation) ... + scen_mean_mc = svc.build_spec(OptimizationRequest(scenario_model="gbm", n_scenarios=4096, sampling="mc", + moment_match=False, **BASE), SYMBOLS, + svc.scenarios(OptimizationRequest(scenario_model="gbm", n_scenarios=4096, sampling="mc", + moment_match=False, **BASE), SYMBOLS, mu_ann)) + # ... but the sample mean of a plain MC set is visibly off the model mean, a moment-matched one is not + err_mc = np.abs(scen_mean_mc.scenarios.mean(axis=0) - scen_mean_mc.expected_returns).max() + sobol_spec = svc.build_spec(OptimizationRequest(scenario_model="gbm", n_scenarios=4096, **BASE), SYMBOLS, + svc.scenarios(OptimizationRequest(scenario_model="gbm", n_scenarios=4096, **BASE), SYMBOLS, mu_ann)) + err_sobol = np.abs(sobol_spec.scenarios.mean(axis=0) - sobol_spec.expected_returns).max() + assert err_sobol < 0.1 * err_mc + + +def test_importance_sampling_and_row_generation_keep_the_optimum(svc): + plain = svc.optimize(OptimizationRequest(scenario_model="gbm", n_scenarios=4096, **BASE)) + tilted = svc.optimize(OptimizationRequest(scenario_model="gbm", n_scenarios=4096, + **{**BASE, "importance_sampling": "market"})) + assert tilted.status == "Optimal" and tilted.model["scenarios"]["weighted"] + assert tilted.model["scenarios"]["effective_sample_size"] > 2000 + assert abs(tilted.expected_pnl - plain.expected_pnl) <= 0.02 * plain.expected_pnl + exact_req = OptimizationRequest(scenario_model="gaussian_exact", **BASE) + assert gaussian_cvar_of(svc, exact_req, tilted)[1] <= BASE["cvar_limit"] * 1.05 + rg = svc.optimize(OptimizationRequest(scenario_model="gbm", n_scenarios=4096, **{**BASE, "row_generation": "on"})) + assert rg.status == "Optimal" and rg.solver_info["row_generation"]["converged"] + assert rg.scenario_hash == plain.scenario_hash # same scenario set ... + assert abs(rg.expected_pnl - plain.expected_pnl) <= 1e-4 * plain.expected_pnl # ... same optimum + assert max(rg.solver_info["row_generation"]["working_set_sizes"]) < 4096 + + +def test_empirical_model_options(svc): + req = OptimizationRequest(scenario_model="empirical", n_scenarios=2048, **{**BASE, "importance_sampling": "market"}) + res = svc.optimize(req) + assert res.status == "Optimal" and res.model["scenarios"]["model"] == "empirical" + assert all(t > 0 for t in res.model["scenarios"]["importance_sampling"]["theta"]) + assert res.model["scenarios"]["importance_sampling"]["direction"] == "market" + assert res.cvar <= BASE["cvar_limit"] * (1 + 1e-6) + bal = svc.optimize(OptimizationRequest(scenario_model="empirical", n_scenarios=1000, sampling="balanced", **BASE)) + assert bal.status == "Optimal" and bal.model["scenarios"]["n_scenarios"] == 1000 + with pytest.raises(ValueError): + OptimizationRequest(scenario_model="bogus", **BASE) + with pytest.raises(ValueError): + OptimizationRequest(**{**BASE, "importance_sampling": "maybe"}) + + +def test_one_scenario_set_for_a_family_of_runs(svc): + """Risk level, constraints, costs, objective, cash and holdings must not change the scenarios; + a different expected-return model must change only their drift, not their shocks.""" + svc.clear_scenario_cache() + base = dict(BASE, importance_sampling="market", scenario_model="gbm", n_scenarios=2048) + ref = svc.optimize(OptimizationRequest(**base)) + variants = { + "other CVaR limit": dict(base, cvar_limit=6_000.0), + "limit as a fraction": dict(base, cvar_limit=None, cvar_limit_fraction=0.02), + "confidence 99 %": dict(base, confidence=0.99, cvar_limit=20_000.0), + "tighter caps": dict(base, constraints={"max_weight": 0.10, "max_sector_weight": 0.3}), + "with costs": dict(base, costs={"proportional_buy": 0.002, "proportional_sell": 0.002}), + "min-CVaR objective": dict(base, objective="min_cvar", cvar_limit=None, target_return=0.001), + "utility objective": dict(base, objective="utility", cvar_limit=None, risk_aversion=0.2), + "more cash": dict(base, cash=2_000_000.0), + "rebalancing": dict(base, mode="rebalance", cash=50_000.0, + holdings={p["symbol"]: p["shares"] for p in ref.positions[:3]}), + } + for label, kwargs in variants.items(): + res = svc.optimize(OptimizationRequest(**kwargs)) + assert res.status == "Optimal", label + assert res.scenario_key["id"] == ref.scenario_key["id"], label + assert res.scenario_hash == ref.scenario_hash, label + assert res.solver_info["scenario_cache"]["reused"], label # and it was not regenerated + # a different expected-return model shifts the scenarios but leaves the shocks alone + symbols = svc.candidate_symbols(OptimizationRequest(**base)) + req = OptimizationRequest(**base) + mu_a, _, _ = svc.expected_returns(req, symbols) + scen_a = svc.scenarios(req, symbols, mu_a) + req_views = OptimizationRequest(**dict(base, views=[{"symbol": SYMBOLS[0], "sentiment": 0.9, "confidence": 0.8}])) + mu_b, _, _ = svc.expected_returns(req_views, symbols) + scen_b = svc.scenarios(req_views, symbols, mu_b) + assert scen_b.hash != scen_a.hash and scen_b.shock_hash == scen_a.shock_hash + np.testing.assert_allclose(scen_b.shocks, scen_a.shocks, atol=1e-15) + assert (scen_b.expected_simple[0] > scen_a.expected_simple[0]) # the view moved the drift + # what *must* change the set: symbols, horizon, covariance model, seed, sampling, scenario count + for label, kwargs in { + "fewer symbols": dict(base, symbols=SYMBOLS[:8]), + "monthly horizon": dict(base, horizon="month"), + "other covariance": dict(base, covariance={"method": "rolling", "window": 250}), + "other seed": dict(base, seed=7), + "plain Monte Carlo": dict(base, sampling="mc"), + "more scenarios": dict(base, n_scenarios=4096), + "no tilt": dict(base, importance_sampling="off"), + }.items(): + other = OptimizationRequest(**kwargs) + assert svc.scenario_key(other, svc.candidate_symbols(other))["id"] != ref.scenario_key["id"], label + # the adaptive tilt is honest about depending on the risk parameters + ad = OptimizationRequest(**dict(base, importance_sampling="adaptive")) + ad_other = OptimizationRequest(**dict(base, importance_sampling="adaptive", cvar_limit=6_000.0)) + assert svc.scenario_key(ad, symbols)["id"] != svc.scenario_key(ad_other, symbols)["id"] + assert "adaptive_tilt" in svc.scenario_key(ad, symbols) + + +def test_tilt_is_driven_by_the_covariance_and_reports_its_alignment(svc): + """The tilt shifts each asset by its covariance with the reference portfolio (not by its own + volatility), leaves the model correlations exactly intact, and the result says how well the + solution's loss lines up with the tilted direction.""" + from scipy.stats import norm + + from quantcore.models import sampling as smp + from quantcore.models.covariance import CovarianceModel + from quantcore.models.simulation import GBMSimulator, TiltSpec + from quantcore.optimization.problem import OptimizationConstraints, ProblemSpec, TransactionCosts + + req = OptimizationRequest(**dict(BASE, scenario_model="gbm", n_scenarios=2048, importance_sampling="market")) + symbols = svc.candidate_symbols(req) + r, cov_model = svc._returns_and_cov_model(req, symbols) + h = req.horizon_periods + cov_h = cov_model.estimate(r).values * h + root = smp.pca_root(cov_h) + reference = svc.market_exposure(symbols) * 1e6 + + # 1. the shift of a tilted block is exactly -z_beta * Sigma v / sigma: proportional to each + # asset's covariance with the reference, i.e. to its beta, not to its own volatility + z = norm.ppf(0.95) + direction = smp.loss_direction(root, reference) + shift = z * (root @ direction) + sigma = np.sqrt(reference @ cov_h @ reference) + np.testing.assert_allclose(shift, -z * (cov_h @ reference) / sigma, atol=1e-12) + beta_i = (cov_h @ reference) / (reference @ cov_h @ reference) + assert np.corrcoef(shift, beta_i)[0, 1] < -0.999 # perfectly ordered by beta + if np.std(np.sqrt(np.diag(cov_h))) > 0: # ... and only loosely by own volatility + assert np.corrcoef(shift, np.sqrt(np.diag(cov_h)))[0, 1] > -0.999 + + # 2. the weighted tilted sample has exactly the model covariance (and correlations) + mu_ann, _, _ = svc.expected_returns(req, symbols) + sim = GBMSimulator(cov_model) + tilted = sim.simulate(r, n_scenarios=2048, horizon_periods=h, mu=mu_ann / 252, seed=1, + tilt=TiltSpec(exposure=reference)) + w = tilted.weights + centred = tilted.log_returns - w @ tilted.log_returns + cov_t = (centred * w[:, None]).T @ centred + np.testing.assert_allclose(cov_t, cov_h, rtol=1e-8, atol=1e-14) + unweighted = np.cov(tilted.log_returns, rowvar=False) + assert np.abs(unweighted - cov_h).max() > 1e-6 # the raw tilted sample is *not* the model + + # 3. alignment: 1 for the reference itself, negative for a portfolio built to oppose it + spec = ProblemSpec(scenarios=tilted.simple_returns(), probabilities=tilted.probabilities, + expected_returns=tilted.expected_simple, **svc._spec_common(req, symbols)) + assert abs(svc.tilt_alignment(spec, reference, svc.market_exposure(symbols)) - 1) < 1e-9 # itself + opposed = np.zeros(len(symbols)) + opposed[np.argsort(beta_i)[:2]] = 1e5 # the two assets least exposed to the reference + assert svc.tilt_alignment(spec, opposed, svc.market_exposure(symbols)) < 0.9 + # a real run reports the figure and stays well above the warning threshold + res = svc.optimize(req) + assert res.solver_info["tilt_alignment"] > 0.5 and not res.warnings + + +def test_risk_drift_zero_keeps_the_limit_binding_when_the_forecast_is_large(md, settings): + """Over a long horizon a big expected return shifts the whole tail into profit, and a CVaR limit + measured on the drifted scenarios stops constraining anything. The zero-drift convention measures + risk on the centred scenarios -- so the risk figure does not depend on the return forecast at all + -- while the objective still uses it.""" + svc = OptimizationService(settings, md) + base = dict(symbols=SYMBOLS[:8], cash=1_000_000.0, horizon=252, freq="daily", confidence=0.99, + cvar_limit_fraction=0.12, scenario_model="gbm", n_scenarios=2048, solver="highs", + expected_returns="zero", row_generation="off", importance_sampling="off") + symbols = svc.candidate_symbols(OptimizationRequest(**base)) + v = np.full(len(symbols), 125_000.0) # an equally weighted portfolio of the whole wealth + + risk = {} + for mu_val in (0.8, 1.5): # 80 % and 150 % p.a. forecasts + for mode in ("model", "zero"): + req = OptimizationRequest(**{**base, "risk_drift": mode}) + spec = svc.build_spec(req, symbols, svc.scenarios(req, symbols, pd.Series(mu_val, index=symbols))) + risk[(mu_val, mode)] = OptimizationService.portfolio_risk_on_scenarios(spec, v / spec.prices, 0.99) + if mode == "zero": + np.testing.assert_allclose(spec.mean_returns, svc.build_spec( + OptimizationRequest(**{**base, "risk_drift": "model"}), symbols, + svc.scenarios(OptimizationRequest(**base), symbols, pd.Series(mu_val, index=symbols)) + ).mean_returns) # the objective sees the forecast either way + + # the zero-drift risk is the same whatever the forecast; the drifted one is not + assert risk[(0.8, "zero")]["cvar"] == pytest.approx(risk[(1.5, "zero")]["cvar"], rel=1e-12) + assert risk[(1.5, "model")]["cvar"] < 0 < risk[(1.5, "zero")]["cvar"] # the tail became a gain + assert risk[(0.8, "model")]["cvar"] < risk[(0.8, "zero")]["cvar"] + # the drift enters multiplicatively (e^{r+d} - 1), so the gap is well below mu * V + gap = risk[(1.5, "zero")]["cvar"] - risk[(1.5, "model")]["cvar"] + assert 0.3 * 1.5e6 < gap < 1.5e6 + + res = svc.optimize(OptimizationRequest(**{**base, "expected_returns": "historical"})) + if res.cvar <= 0: # the historical forecast is large enough to make the limit vacuous + assert any("constrains nothing" in w for w in res.warnings) + + +def test_an_all_cash_utility_solution_says_why(md, settings): + """objective='utility' with a large lambda is optimal in cash -- which must not look like a bug.""" + from quantcore.optimization.service import OptimizationRequest, OptimizationService + + svc = OptimizationService(settings, md) + common = dict(cash=1_000_000.0, max_assets=6, horizon="week", scenario_model="gaussian_exact", + solver="highs", time_limit=120, objective="utility") + big = svc.optimize(OptimizationRequest(risk_aversion=50.0, **common)) + assert big.status in ("Optimal", "FeasibleFound") and big.invested == 0 + assert any("nothing was invested" in w and "risk_aversion" in w for w in big.warnings) + small = svc.optimize(OptimizationRequest(risk_aversion=0.0, **common)) + assert small.invested > 0 and not any("nothing was invested" in w for w in small.warnings) + + +def test_a_weighted_cvar_objective_is_routed_to_highs_and_says_so(md, settings): + """cuOpt aborts when the CVaR is the objective *and* the scenarios carry probabilities. + + The routing is that narrow on purpose: the same objective with uniform weights solves on the GPU, + so only the weighted case may be diverted, and only under solver='auto'. + """ + from quantcore.optimization.service import OptimizationRequest, OptimizationService + + svc = OptimizationService(settings, md) + common = dict(cash=1_000_000.0, max_assets=6, horizon="week", scenario_model="gbm", + n_scenarios=1024, solver="auto", time_limit=120, risk_drift="zero") + weighted = svc.optimize(OptimizationRequest(objective="min_cvar", target_return=0.0005, + importance_sampling="market", **common)) + assert weighted.solver == "highs" + assert any("probability-weighted" in w for w in weighted.warnings) + + # uniform weights: no diversion (cuOpt is unreachable in the tests, so it still lands on HiGHS -- + # through the fallback, which says something different) + plain = svc.optimize(OptimizationRequest(objective="min_cvar", target_return=0.0005, + importance_sampling="off", **common)) + assert not any("probability-weighted" in w for w in plain.warnings) + # the constrained form is never diverted either + other = svc.optimize(OptimizationRequest(objective="max_return", cvar_limit_fraction=0.05, + importance_sampling="market", **common)) + assert not any("probability-weighted" in w for w in other.warnings) diff --git a/tests/test_simulation.py b/tests/test_simulation.py new file mode 100644 index 0000000..1e5be9b --- /dev/null +++ b/tests/test_simulation.py @@ -0,0 +1,197 @@ +import numpy as np +import pandas as pd +import pytest + +from quantcore.models.covariance import CovarianceModel, cholesky_factor, symmetric_inv_sqrt, symmetric_sqrt +from quantcore.models.simulation import EmpiricalInnovationSimulator, GBMSimulator + + +@pytest.fixture +def r(): + rng = np.random.default_rng(7) + L = np.array([[1.0, 0, 0], [0.6, 0.8, 0], [0.3, 0.2, 0.9]]) + x = rng.standard_t(6, size=(1500, 3)) @ L.T * 0.01 + return pd.DataFrame(x, index=pd.bdate_range("2019-01-01", periods=1500), columns=list("ABC")) + + +def test_matrix_roots(r): + cov = r.cov().values + s = symmetric_sqrt(cov) + np.testing.assert_allclose(s @ s, cov, atol=1e-12) + np.testing.assert_allclose(symmetric_inv_sqrt(cov) @ s, np.eye(3), atol=1e-8) + L = cholesky_factor(cov) + np.testing.assert_allclose(L @ L.T, cov, atol=1e-12) + + +def test_gbm_scenarios_have_target_moments(r): + cm = CovarianceModel("rolling", window=500) + mu = pd.Series([0.001, 0.0, -0.001], index=list("ABC")) + scen = GBMSimulator(cm).simulate(r, n_scenarios=100_000, horizon_periods=5, mu=mu, seed=1) + target = cm.estimate(r).values * 5 + # Sobol' sets are rounded up to a power of two; moment matching makes the covariance exact + assert scen.model == "gbm" and scen.n_scenarios == 131_072 and scen.horizon_periods == 5 + assert scen.extra["n_requested"] == 100_000 and scen.extra["sampling"] == "sobol" + np.testing.assert_allclose(np.cov(scen.log_returns, rowvar=False, ddof=0), target, rtol=1e-8, atol=1e-14) + # arithmetic drift convention: the expected simple return over the horizon is mu * h (Ito correction) + np.testing.assert_allclose(scen.expected_simple, mu.values * 5) + np.testing.assert_allclose(scen.mean_simple_returns(), mu.values * 5, atol=3e-5) + # log convention: the log-drift is mu * h exactly + scen_log = GBMSimulator(cm).simulate(r, n_scenarios=4096, horizon_periods=5, mu=mu, seed=1, drift_convention="log") + np.testing.assert_allclose(scen_log.log_returns.mean(axis=0), mu.values * 5, atol=1e-12) + assert (scen_log.expected_simple > np.expm1(mu.values * 5)).all() # sigma^2/2 bonus under the log convention + # plain Monte Carlo without moment matching: the old behaviour, moments only approximately right + mc = GBMSimulator(cm).simulate(r, n_scenarios=100_000, horizon_periods=5, mu=mu, seed=1, sampling="mc", + moment_match=False) + assert mc.n_scenarios == 100_000 + np.testing.assert_allclose(np.cov(mc.log_returns, rowvar=False), target, rtol=0.05, atol=1e-6) + + +def test_innovations_are_standardised(r): + sim = EmpiricalInnovationSimulator(CovarianceModel("ewm", lam=0.97)) + eps = sim.innovations(r) + assert eps.shape[1] == 3 and len(eps) > 1000 + cov = np.cov(eps.values, rowvar=False) + np.testing.assert_allclose(cov, np.eye(3), atol=0.15) + + +def test_empirical_scenarios_match_latest_covariance(r): + cm = CovarianceModel("ewm", lam=0.97) + sim = EmpiricalInnovationSimulator(cm) + scen = sim.simulate(r, n_scenarios=60_000, horizon_periods=3, seed=2, sampling="mc", moment_match=False) + target = cm.estimate(r).values * 3 + np.testing.assert_allclose(np.cov(scen.log_returns, rowvar=False), target, rtol=0.2, atol=2e-6) + # zero expected return: the log-drift is the Ito correction, the mean simple return ~ 0 + np.testing.assert_allclose(scen.mean_simple_returns(), 0.0, atol=5e-4) + exact = sim.simulate(r, n_scenarios=60_000, horizon_periods=3, seed=2) # Sobol' ranks + moment matching + assert exact.n_scenarios == 65_536 + np.testing.assert_allclose(np.cov(exact.log_returns, rowvar=False, ddof=0), target, rtol=1e-8, atol=1e-14) + # fat tails survive the filter: kurtosis of the innovations above the Gaussian 0 + from scipy.stats import kurtosis + assert kurtosis(sim.innovations(r).values).mean() > 0.3 + + +def test_cholesky_root_variant(r): + sim = EmpiricalInnovationSimulator(CovarianceModel("rolling", window=250), root="cholesky") + eps = sim.innovations(r) + np.testing.assert_allclose(np.cov(eps.values, rowvar=False), np.eye(3), atol=0.15) + scen = sim.simulate(r, n_scenarios=5000, horizon_periods=1, seed=3, sampling="mc") + assert scen.extra["root"] == "cholesky" and scen.log_returns.shape == (5000, 3) + + +def test_restandardisation_reproduces_model_covariance(r): + cm = CovarianceModel("ewm", lam=0.97, shrinkage=0.0) + for mode in ("diagonal", "full"): + sim = EmpiricalInnovationSimulator(cm, standardize=mode) + scen = sim.simulate(r, n_scenarios=80_000, horizon_periods=1, seed=5, sampling="mc", moment_match=False) + model = cm.estimate(r).values + sc = np.cov(scen.log_returns, rowvar=False) + np.testing.assert_allclose(np.sqrt(np.diag(sc)), np.sqrt(np.diag(model)), rtol=0.03) + assert scen.extra["standardize"] == mode and scen.extra["innovation_variance_median"] > 0 + with pytest.raises(ValueError): + EmpiricalInnovationSimulator(cm, standardize="bogus") + + +def test_auto_shrinkage_intensity_properties(): + from quantcore.measurement.estimators import shrinkage_intensity + + rng = np.random.default_rng(11) + assert shrinkage_intensity(rng.standard_normal((80, 40))) == 1.0 # independent -> full shrinkage + L = np.linalg.cholesky(0.4 * np.eye(10) + 0.6) + strong = shrinkage_intensity(rng.standard_normal((2000, 10)) @ L.T) + assert 0.0 <= strong < 0.05 # strongly correlated, long sample -> little shrinkage + cm = CovarianceModel("rolling", window=100, shrinkage="auto") + x = pd.DataFrame(rng.standard_normal((300, 20)) @ np.linalg.cholesky(0.5 * np.eye(20) + 0.5).T) + assert 0 < cm.intensity(x) < 1 and cm.describe()["shrinkage"] == "auto" + + +def test_importance_sampling_tilt_both_models(r): + + from quantcore.models.simulation import TiltSpec + + cm = CovarianceModel("ewm", lam=0.97) + exposure = np.array([30_000.0, 50_000.0, 20_000.0]) + beta = 0.99 + for sim in (GBMSimulator(cm), EmpiricalInnovationSimulator(cm)): + plain = sim.simulate(r, n_scenarios=4096, horizon_periods=5, seed=9) + tilted = sim.simulate(r, n_scenarios=4096, horizon_periods=5, seed=9, + tilt=TiltSpec.at_confidence(exposure, beta, mixture=0.5)) + assert plain.probabilities is None and tilted.probabilities is not None + assert abs(tilted.probabilities.sum() - 1) < 1e-12 and tilted.describe()["effective_sample_size"] > 2000 + # exact first two moments in both cases (weighted for the tilted set) + w = tilted.weights + np.testing.assert_allclose(w @ tilted.log_returns, plain.log_returns.mean(axis=0), atol=1e-12) + cov_t = (tilted.log_returns - w @ tilted.log_returns).T @ ((tilted.log_returns - w @ tilted.log_returns) * w[:, None]) + np.testing.assert_allclose(cov_t, np.cov(plain.log_returns, rowvar=False, ddof=0), rtol=1e-8, atol=1e-14) + # many more scenarios beyond the reference portfolio's VaR, with the right total probability + loss_p = -(plain.simple_returns() @ exposure) + loss_t = -(tilted.simple_returns() @ exposure) + var = np.quantile(loss_p, beta) + assert (loss_t > var).sum() > 5 * (loss_p > var).sum() + assert abs(w[loss_t > var].sum() - (1 - beta)) < 0.01 + assert "importance_sampling" in tilted.extra and tilted.hash != plain.hash + # the default tilt mixes z_0.95 and z_0.99 blocks, so one set serves both levels + both = sim.simulate(r, n_scenarios=4096, horizon_periods=5, seed=9, tilt=TiltSpec(exposure=exposure)) + w2 = both.weights + loss_b = -(both.simple_returns() @ exposure) + for level in (0.95, 0.99): + var_l = np.quantile(loss_p, level) + assert (loss_b > var_l).sum() > 3 * (loss_p > var_l).sum() + assert abs(w2[loss_b > var_l].sum() - (1 - level)) < 0.01 + # determinism: same seed -> same hash, other seed -> different + a = GBMSimulator(cm).simulate(r, n_scenarios=512, seed=1) + b = GBMSimulator(cm).simulate(r, n_scenarios=512, seed=1) + c = GBMSimulator(cm).simulate(r, n_scenarios=512, seed=2) + assert a.hash == b.hash != c.hash + + +def test_scenario_distribution_reports_the_model_quantities(r): + sim = GBMSimulator(CovarianceModel("ewm", lam=0.97, shrinkage=0.0)) + scen = sim.simulate(r, n_scenarios=8192, horizon_periods=5, mu=pd.Series(0.0004, index=r.columns), seed=5) + v = np.array([400_000.0, 300_000.0, 300_000.0]) + d = scen.distribution(v, bins=40, confidence=(0.95, 0.99)) + cov_h = CovarianceModel("ewm", lam=0.97, shrinkage=0.0).estimate(r).values * 5 + sigma = np.sqrt(v @ cov_h @ v) + assert d["volatility"] == pytest.approx(sigma, rel=0.02) + # the reported mean is the mean of the *displayed* sample; moment matching is exact in log + # space, so it equals the model's expectation only up to the convexity of exp + assert d["expected_pnl"] == pytest.approx(float(scen.expected_simple @ v), rel=1e-4) + assert sum(d["histogram"]["mass"]) == pytest.approx(1.0) + assert len(d["histogram"]["edges"]) == 41 + # Gaussian reference: VaR = z sigma - mean, CVaR = phi(z)/(1-beta) sigma - mean + from scipy.stats import norm + for beta in (0.95, 0.99): + z = norm.ppf(beta) + assert d["risk"][f"{beta:g}"]["var"] == pytest.approx(z * sigma - d["expected_pnl"], rel=0.03) + assert d["risk"][f"{beta:g}"]["cvar"] == pytest.approx( + norm.pdf(z) / (1 - beta) * sigma - d["expected_pnl"], rel=0.03) + assert abs(d["skewness"]) < 0.25 and d["kurtosis"] == pytest.approx(3.0, abs=0.4) + + +def test_a_tilted_set_reports_the_same_distribution_as_an_untilted_one(r): + from quantcore.models.simulation import TiltSpec + sim = GBMSimulator(CovarianceModel("ewm", lam=0.97, shrinkage=0.0)) + v = np.array([400_000.0, 300_000.0, 300_000.0]) + kw = dict(n_scenarios=8192, horizon_periods=5, mu=pd.Series(0.0004, index=r.columns), seed=11) + plain = sim.simulate(r, **kw).distribution(v) + tilted = sim.simulate(r, **kw, tilt=TiltSpec(exposure=v)).distribution(v) + assert tilted["weighted"] and tilted["effective_sample_size"] < 8192 + assert tilted["expected_pnl"] == pytest.approx(plain["expected_pnl"], rel=1e-6) + assert tilted["volatility"] == pytest.approx(plain["volatility"], rel=0.02) + assert tilted["risk"]["0.99"]["cvar"] == pytest.approx(plain["risk"]["0.99"]["cvar"], rel=0.03) + for q in ("0.05", "0.5"): + assert tilted["quantiles"][q] == pytest.approx(plain["quantiles"][q], rel=0.05) + + +def test_summary_reports_price_returns_that_cannot_fall_below_minus_one(r): + """A long horizon makes the two spaces look very different: a log-return of -2 is -86 % of the + price. The table must carry both, and the price column must be bounded below by -100 %.""" + sim = GBMSimulator(CovarianceModel("ewm", lam=0.97, shrinkage=0.0)) + scen = sim.simulate(r, n_scenarios=4096, horizon_periods=63, mu=pd.Series(0.0004, index=r.columns), seed=3) + t = scen.summary() + for col in ("q01", "q05", "q50", "q95", "min", "max"): + np.testing.assert_allclose(t[f"{col}_simple"], np.expm1(t[col]), rtol=1e-12) # a monotone map + assert (t["min_simple"] > -1).all() and (t["min"] < t["min_simple"]).all() + assert (t["max_simple"] > t["max"]).all() + # the scenario mean in price space is above the log mean (Jensen) and matches the model's + assert (t["mean_simple"] > t["mean"]).all() + assert np.allclose(t["mean_simple"], t["expected_simple"], rtol=0.05) diff --git a/tests/test_solvers.py b/tests/test_solvers.py new file mode 100644 index 0000000..308ba39 --- /dev/null +++ b/tests/test_solvers.py @@ -0,0 +1,165 @@ +import numpy as np +import pytest +import scipy.sparse as sp + +from quantcore.config import Settings +from quantcore.optimization.problem import MILPModel +from quantcore.optimization.solvers import CuOptSolver, HighsSolver, solve_with_fallback, to_cuopt_payload + + +def readme_model() -> MILPModel: + # maximise 3x + 2y s.t. 2x + 3y <= 12, 4x + y <= 10, x,y >= 0 integer -> x = 2, y = 2, obj 10 + A = sp.csr_matrix(np.array([[2.0, 3.0], [4.0, 1.0]])) + return MILPModel(c=np.array([3.0, 2.0]), A=A, row_lb=np.array([-np.inf, -np.inf]), row_ub=np.array([12.0, 10.0]), + var_lb=np.zeros(2), var_ub=np.array([np.inf, np.inf]), var_types=["I", "I"], var_names=["x", "y"], + row_names=["c1", "c2"], maximize=True) + + +def test_payload_format(): + payload = to_cuopt_payload(readme_model(), time_limit=5) + assert payload["csr_constraint_matrix"] == {"offsets": [0, 2, 4], "indices": [0, 1, 0, 1], "values": [2.0, 3.0, 4.0, 1.0]} + assert payload["constraint_bounds"]["lower_bounds"] == ["ninf", "ninf"] + assert payload["variable_bounds"]["upper_bounds"] == ["inf", "inf"] + assert payload["variable_types"] == ["I", "I"] and payload["maximize"] is True + assert payload["solver_config"]["time_limit"] == 5.0 + + +def test_highs_solves_readme_example(): + res = HighsSolver().solve(readme_model()) + assert res.ok and res.status == "Optimal" + np.testing.assert_allclose(res.x, [2.0, 2.0]) + assert res.objective == pytest.approx(10.0) + + +def test_fallback_when_cuopt_unreachable(): + settings = Settings(cuopt_url="http://127.0.0.1:9", solver_fallback=True, cuopt_request_timeout=2) + res = solve_with_fallback(readme_model(), settings, solver="auto") + assert res.solver == "highs" and "fallback" in res.message and res.objective == pytest.approx(10.0) + with pytest.raises(ConnectionError): + solve_with_fallback(readme_model(), settings, solver="cuopt") + assert CuOptSolver(settings).health()["reachable"] is False + + +def test_fallback_when_cuopt_dies_during_the_solve(monkeypatch): + """cuOpt accepted the job, then its server went away while the client polled for the solution + (seen with a read-only container whose solver process crashed): 'auto' must still answer, on HiGHS.""" + import requests + + from quantcore.optimization import solvers as S + + settings = Settings(cuopt_url="http://127.0.0.1:9", solver_fallback=True, cuopt_poll_interval=0.01) + monkeypatch.setattr(S.CuOptSolver, "reachable", lambda self, ttl=30.0: True) + monkeypatch.setattr(S.CuOptSolver, "_post_with_retry", lambda self, payload: "job-1") + + def lost(*args, **kwargs): + raise requests.ConnectionError("Max retries exceeded") + + monkeypatch.setattr(S.requests, "get", lost) + monkeypatch.setattr(S.requests, "delete", lost) + res = solve_with_fallback(readme_model(), settings, solver="auto") + assert res.solver == "highs" and "connection lost" in res.message and res.objective == pytest.approx(10.0) + with pytest.raises(ConnectionError): + solve_with_fallback(readme_model(), settings, solver="cuopt") + + +@pytest.mark.skipif(not CuOptSolver(Settings()).health().get("reachable"), reason="cuOpt server not reachable") +def test_cuopt_solves_readme_example(): + res = CuOptSolver(Settings()).solve(readme_model(), time_limit=10) + assert res.ok and res.solver == "cuopt" + np.testing.assert_allclose(res.x, [2.0, 2.0]) + assert res.objective == pytest.approx(10.0) + + +def test_cuopt_retries_down_the_method_ladder_after_an_abort(monkeypatch): + """cuOpt's default method aborts on some CVaR-objective models; the client works down + ``CUOPT_RETRY_METHODS`` and reports which method finally solved it.""" + from quantcore.optimization import solvers as S + + calls = [] + + class Response: + def __init__(self, payload, status=200): + self._payload = payload + self.status_code = status + self.text = str(payload) + + def json(self): + return self._payload + + def raise_for_status(self): + pass + + def fake_post(url, data=None, headers=None, timeout=None, **kw): # noqa: ARG001 + import msgpack + + calls.append(msgpack.unpackb(data)["solver_config"]) + return Response({"reqId": f"req{len(calls)}"}) + + def fake_get(url, headers=None, timeout=None, **kw): + if len(calls) == 1: # first attempt: aborted + return Response({"error_result": True, "error": "req1 was aborted"}) + return Response({"response": {"solver_response": { + "status": "Optimal", + "solution": {"primal_solution": [1.0, 0.0], "primal_objective": 1.0, "solver_time": 0.1, + "milp_statistics": {"mip_gap": 0.0}}, + }}}) + + class FakeSession: # the client opens a fresh session per submit attempt + def __enter__(self): + return self + + def __exit__(self, *exc): + return False + + post = staticmethod(fake_post) + + monkeypatch.setattr(S.requests, "Session", FakeSession) + monkeypatch.setattr(S.requests, "get", fake_get) + monkeypatch.setattr(S.requests, "delete", lambda *a, **k: Response({})) + + model = readme_model() + res = S.CuOptSolver(Settings(cuopt_url="http://example.invalid")).solve(model, time_limit=5) + first = S.CUOPT_RETRY_METHODS[0] + assert res.status == "Optimal" and f"method={first}" in res.message + assert calls[0].get("method") is None and calls[1]["method"] == first # default, then the ladder + + +def test_cuopt_reports_when_every_method_aborts(monkeypatch): + """All rungs exhausted: the caller is told that, not just that the first attempt aborted.""" + from quantcore.optimization import solvers as S + + calls = [] + + class Response: + def __init__(self, payload): + self._payload, self.status_code, self.text = payload, 200, str(payload) + + def json(self): + return self._payload + + def raise_for_status(self): + pass + + class FakeSession: + def __enter__(self): + return self + + def __exit__(self, *exc): + return False + + @staticmethod + def post(url, data=None, headers=None, timeout=None, **kw): + import msgpack + + calls.append(msgpack.unpackb(data)["solver_config"]) + return Response({"reqId": f"req{len(calls)}"}) + + monkeypatch.setattr(S.requests, "Session", FakeSession) + monkeypatch.setattr(S.requests, "get", + lambda *a, **k: Response({"error_result": True, "error": "req was aborted"})) + monkeypatch.setattr(S.requests, "delete", lambda *a, **k: Response({})) + + res = S.CuOptSolver(Settings(cuopt_url="http://example.invalid")).solve(readme_model(), time_limit=5) + assert res.status == "Error" + assert str(S.CUOPT_RETRY_METHODS) in res.message + assert len(calls) == 1 + len(S.CUOPT_RETRY_METHODS) # the default plus every rung