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 <noreply@anthropic.com>
63 lines
2.5 KiB
Python
63 lines
2.5 KiB
Python
"""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)
|