"""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())