Metadata-Version: 2.5
Name: sobres
Version: 2.2.0
Summary: One-stop CLI for equity analysis: market data, portfolio optimization, factor models, econometrics, and goal planning.
Project-URL: Homepage, https://github.com/AI-Solutions-Lab-LLC/sobres
Project-URL: Issues, https://github.com/AI-Solutions-Lab-LLC/sobres/issues
Project-URL: Changelog, https://github.com/AI-Solutions-Lab-LLC/sobres/blob/main/CHANGELOG.md
Project-URL: Source, https://github.com/AI-Solutions-Lab-LLC/sobres
Author-email: JJ Espinoza <jj.espinoza.la@gmail.com>
License: MIT License
        
        Copyright (c) 2026 JJ Espinoza
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: cli,econometrics,fama-french,finance,fire,markowitz,portfolio
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Investment
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: numpy>=1.26
Requires-Dist: pandas>=2.2
Requires-Dist: platformdirs>=4.2
Requires-Dist: pydantic>=2.7
Requires-Dist: rich>=13.7
Requires-Dist: scipy>=1.13
Requires-Dist: sqlalchemy>=2.0
Requires-Dist: structlog>=24.1
Requires-Dist: typer>=0.12
Requires-Dist: yfinance>=0.2.40
Provides-Extra: all
Requires-Dist: arch>=7.0; extra == 'all'
Requires-Dist: cvxpy>=1.5; extra == 'all'
Requires-Dist: fastapi>=0.115; extra == 'all'
Requires-Dist: opentelemetry-api>=1.25; extra == 'all'
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.25; extra == 'all'
Requires-Dist: opentelemetry-sdk>=1.25; extra == 'all'
Requires-Dist: pandas-datareader>=0.10; extra == 'all'
Requires-Dist: pyarrow>=15; extra == 'all'
Requires-Dist: statsmodels>=0.14; extra == 'all'
Requires-Dist: uvicorn>=0.30; extra == 'all'
Provides-Extra: data
Requires-Dist: pandas-datareader>=0.10; extra == 'data'
Provides-Extra: dev
Requires-Dist: arch>=7.0; extra == 'dev'
Requires-Dist: cvxpy>=1.5; extra == 'dev'
Requires-Dist: fastapi>=0.115; extra == 'dev'
Requires-Dist: hypothesis>=6.100; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: opentelemetry-api>=1.25; extra == 'dev'
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.25; extra == 'dev'
Requires-Dist: opentelemetry-sdk>=1.25; extra == 'dev'
Requires-Dist: pandas-datareader>=0.10; extra == 'dev'
Requires-Dist: pandas-stubs; extra == 'dev'
Requires-Dist: pyarrow>=15; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.2; extra == 'dev'
Requires-Dist: pyyaml>=6.0; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Requires-Dist: shellcheck-py; extra == 'dev'
Requires-Dist: statsmodels>=0.14; extra == 'dev'
Requires-Dist: types-requests; extra == 'dev'
Requires-Dist: uvicorn>=0.30; extra == 'dev'
Provides-Extra: econ
Requires-Dist: arch>=7.0; extra == 'econ'
Requires-Dist: statsmodels>=0.14; extra == 'econ'
Provides-Extra: opt
Requires-Dist: cvxpy>=1.5; extra == 'opt'
Provides-Extra: otel
Requires-Dist: opentelemetry-api>=1.25; extra == 'otel'
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.25; extra == 'otel'
Requires-Dist: opentelemetry-sdk>=1.25; extra == 'otel'
Provides-Extra: parquet
Requires-Dist: pyarrow>=15; extra == 'parquet'
Provides-Extra: web
Requires-Dist: fastapi>=0.115; extra == 'web'
Requires-Dist: uvicorn>=0.30; extra == 'web'
Description-Content-Type: text/markdown

# sobres

[![CI](https://github.com/AI-Solutions-Lab-LLC/sobres/actions/workflows/ci.yml/badge.svg)](https://github.com/AI-Solutions-Lab-LLC/sobres/actions/workflows/ci.yml)
[![Release](https://github.com/AI-Solutions-Lab-LLC/sobres/actions/workflows/release.yml/badge.svg)](https://github.com/AI-Solutions-Lab-LLC/sobres/actions/workflows/release.yml)
[![PyPI](https://img.shields.io/pypi/v/sobres.svg)](https://pypi.org/project/sobres/)
[![Python](https://img.shields.io/pypi/pyversions/sobres.svg)](https://pypi.org/project/sobres/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

A one-stop **CLI for equity analysis** — market data, portfolio optimization, factor
models, econometrics, and real-world goal planning, in one tool.

> ⚠️ **For research and education only. Not investment advice.**

> 📍 **This repository was `espin086/Stocks`, now rebuilt as `sobres` under
> AI Solutions Lab.** The prior R linear-programming scripts and `yfinance`
> pullers were kept under `legacy_code/` while the port was in progress and
> removed once it was complete; they remain in git history. The allocation LP
> they solved is documented in
> [`docs/allocation-lp-reference.md`](docs/allocation-lp-reference.md).

```bash
# Optimize a portfolio
sobres optimize markowitz --tickers AAPL MSFT NVDA JNJ XOM GLD \
    --start 2015-01-01 --fill ffill --objective max_sharpe --max-weight 0.35

# See the whole risk/return trade-off, not one point
sobres optimize frontier --tickers AAPL MSFT --start 2020-01-01 --fill ffill --points 50 --format csv > frontier.csv

# Find out whether that optimizer actually works out-of-sample
sobres optimize backtest --tickers AAPL MSFT --start 2020-01-01 --fill ffill --rebalance quarterly --lookback 36m

# Is there alpha, or is it just factor exposure?
sobres analyze factors NVDA --model ff5

# When can I retire?
sobres plan retire --income 200000 --expenses 90000 --portfolio 400000

# How much of my international return was the company, and how much was the dollar?
sobres fx attribution --tickers NESN.SW 7203.T ASML.AS --base USD

# That FIRE number buys a US lifestyle. What does it buy in Portugal?
sobres ppp adjust-goal --goal fire --to PRT
```

## Status

**v1.0.0 implementation; publication pending.** The foundation (onboarding, the registry-generated CLI, the storage
port, keyless providers, the currency model, structured logging) and portfolio
optimization: Markowitz weights, the efficient frontier, a walk-forward backtest
and a risk panel. Persistence, the UI, the container and the remaining analytics
follow milestone by milestone below.

**Start here: [`openspec/project.md`](openspec/project.md)** for the architecture, then
the milestone plans in [`openspec/changes/`](openspec/changes/).

| # | Milestone | Ships | State |
|---|---|---|---|
| [0000](openspec/changes/0000-release-engineering/) | Release engineering | CI gate, version-gated PyPI publishing | 🔧 Upload rehearsal/activation pending (#21) |
| [0001](openspec/changes/0001-foundation-data-and-cli/) | Foundation | `init`/`doctor` onboarding, command registry, storage port, providers, currency, observability, `sobres data` | ✅ Done |
| [0002](openspec/changes/0002-portfolio-optimization/) | **Portfolio optimization** | Returns, risk, Markowitz, frontier, backtest, risk parity | ✅ Done (R script re-run deferred; see `docs/allocation-lp-reference.md`) |
| [0003](openspec/changes/0003-local-persistence/) | Local persistence | Saved portfolios, goals, run history, `sobres db` | ✅ Done |
| [0004](openspec/changes/0004-web-ui/) | Web UI | FastAPI + React SPA derived from the registry, `sobres serve`, `sobres open` | ✅ Done (real-browser e2e tracked separately) |
| [0005](openspec/changes/0005-docker-distribution/) | Docker | One image on Docker Hub, `sobres deploy` | ✅ Done |
| [0006](openspec/changes/0006-landing-page/) | Landing page | Animated dark GitHub Pages site | ✅ Done — [live](https://ai-solutions-lab-llc.github.io/sobres/) |
| [0007](openspec/changes/0007-equity-factor-analysis/) | Factor analysis | CAPM, Fama-French 3/5 + momentum | ✅ Done |
| [0008](openspec/changes/0008-goal-planning/) | Goal planning | Retirement/FIRE, house, car, education, Monte Carlo | ✅ Done |
| [0009](openspec/changes/0009-econometrics-forecasting/) | Econometrics | Joint VAR/BVAR price forecasts with held-out controls, GARCH volatility, stationarity diagnostics, robust regression | ✅ Done (elastic-net/boosted trees and the macro preset deferred) |
| [0010](openspec/changes/0010-currency-and-ppp/) | Exchange rates & PPP | FX attribution, hedging, PPP-adjusted goals | ✅ Done |
| [0011](openspec/changes/0011-rebrand-sobres/) | Rebrand | One name everywhere: `sobres` | ✅ Done (PyPI name reserved on first publish, #21) |
| [0016](openspec/changes/0016-broker-execution/) | Broker execution | Saved portfolio → whole-share orders through a broker port; Alpaca adapter; paper first, live gated | ✅ Done (real-money verification is the owner's step, #22) |
| [0018](openspec/changes/0018-pre-earnings-options-poc/) | Pre-earnings options POC | Point-in-time IV-ramp long-call backtest with a pre-registered sweep and Greeks attribution, a versioned candidate score and paper recommendations, all local | ✅ Implemented, except the real-history run (B6), which waits on the Massive data subscriptions |
| [0019](openspec/changes/0019-sec-filings-intelligence/) | SEC filings intelligence | EDGAR provider, 10-K/10-Q/8-K/Form 4 item parsing, year-over-year risk-factor and MD&A change scores, local point-in-time retrieval (sqlite-vec), an LLM labeller behind a port, a point-in-time evidence study feeding the 0018 score | 📝 Proposed, not implemented |

sobres is local-first: it runs on your machine or on a server you control. The whole tool also runs from one container — see [docs/DEPLOYING.md](docs/DEPLOYING.md):

```bash
docker run -p 8787:8787 -v sobres:/data aisolutionslab/sobres serve --host 0.0.0.0
sobres deploy compose > docker-compose.yml     # generated from your resolved configuration
sobres deploy check                            # doctor's checks plus mount, bind, token, credentials
```

## Install

```bash
pip install sobres                      # or: pipx install sobres
sobres init                             # FRED, Alpaca and Massive keys (hidden input)
sobres doctor                           # every check tells you what's wrong and how to fix it
sobres update                           # pull the newest release; your config, keys and data are kept
```

`sobres init` shows, for each key, what it unlocks and the exact page that issues
it, then verifies it with one request. All three are free to start and every one
is skippable:

| Key | Unlocks | Get it |
|---|---|---|
| FRED | Macro series and the live risk-free rate | https://fredaccount.stlouisfed.org/apikeys |
| Alpaca (paper) | `sobres trade`, on simulated money by default | https://app.alpaca.markets/signup, then https://app.alpaca.markets/paper/dashboard/overview → API Keys |
| Massive | Stocks and options data and the earnings calendar for the pre-earnings options research ([0018](openspec/changes/0018-pre-earnings-options-poc/)) | https://massive.com/dashboard/signup, then https://massive.com/dashboard/keys |

Missed one? `sobres doctor` offers every key that is still unset; `sobres doctor
--keys` re-enters them all.

To remove sobres completely, run `sobres uninstall` rather than `pip uninstall
sobres`. pip removes only the package; `sobres uninstall` also deletes the config
file holding your keys and the SQLite database, then runs pip for you
(`--dry-run` lists what it would remove). Details: [docs/INSTALLATION.md](docs/INSTALLATION.md#uninstall).

Homebrew requires a separate formula/tap; PyPI publishing alone does not enable
`brew install sobres`. Homebrew distribution is deferred.

That is the onboarding path, and it stays three commands as the tool grows.
`sobres doctor --fix` applies the safe repairs; `sobres upgrade` detects how you
installed and runs the matching upgrade. `sobres open` starts the web UI and puts
it in your browser — `sobres open doctor` goes straight to a view.

Or for development:

```bash
git clone https://github.com/AI-Solutions-Lab-LLC/sobres.git
cd sobres
pip install -e ".[dev]"
pre-commit install                  # optional: run CI's checks before each commit
```

> **One name everywhere:** the distribution, the import package, and the console
> script are all `sobres` — `pip install sobres`, `import sobres`, `sobres --help`.

## Quickstart: the data layer

```bash
sobres data prices AAPL MSFT NVDA --start 2015-01-01          # Rich table on a TTY
sobres data prices AAPL --start 2020-01-01 --format csv > p.csv  # CSV when piped
sobres data factors --model ff5 --frequency monthly            # Fama-French + RF, decimal
sobres data fx EURUSD GBP/USD --start 2024-01-01               # ECB reference rates
sobres data macro DGS10 CPIAUCSL --start 2020-01-01            # needs a free FRED key
sobres cache info                                              # what is on disk, how old
sobres commands --format json                                  # the whole registry
```

Every data-emitting command takes `--format table|json|csv` (JSON is one
document at full precision; logs never touch stdout) and `--refresh` to bypass
the cache. A second identical call is served from the local SQLite file. Prices
default to the split- and dividend-adjusted close, state their currency, and
normalize pence- and cent-quoted listings to the major unit.

## Quickstart: optimization

```bash
sobres optimize markowitz --tickers AAPL MSFT JNJ XOM GLD --start 2015-01-01 --fill ffill
sobres optimize markowitz --tickers AAPL MSFT --start 2020-01-01 --fill ffill --objective min_variance --max-weight 0.6
sobres optimize frontier  --tickers AAPL MSFT --start 2020-01-01 --fill ffill --points 50 --format csv > frontier.csv
sobres optimize backtest  --tickers AAPL MSFT --start 2020-01-01 --fill ffill --rebalance quarterly --lookback 36m
sobres optimize risk      --tickers AAPL MSFT --weights 0.6 0.4 --start 2015-01-01 --fill ffill
```

`--fill` has no default on purpose: how provider gaps are handled changes every
number, so you say `drop`, `ffill` or `raise`. Ledoit-Wolf shrinkage is the
default covariance, transaction costs default to 10 bps per unit of cash-inclusive one-way turnover,
and USD portfolios use a dated FRED Treasury proxy when a key is configured.
Other currencies and missing keys use an explicit zero fallback; `--risk-free`
overrides it with an annual simple decimal rate. Every
in-sample result is labelled as such. A multi-currency universe needs `--base`;
returns are converted before any moment is estimated. Read
[why your backtest looks too good](docs/why-your-backtest-looks-too-good.md)
before trusting the Sharpe ratio.

## Quickstart: factor analysis

```bash
sobres analyze stock NVDA --start 2015-01-01 --fill drop
sobres analyze factors NVDA --model ff5 --start 2015-01-01 --fill drop
sobres analyze factors --tickers AAPL MSFT NVDA --model ff5+mom --start 2015-01-01 --fill drop --format csv
sobres analyze factors NVDA --rolling 36 --start 2015-01-01 --fill drop
```

Excess returns (`r - RF`, with `RF` from the same Ken French file) regressed on
CAPM, Fama-French 3 or 5 factors, or 5 + momentum, monthly by default. Every
coefficient carries OLS and Newey-West standard errors, t-statistics and
p-values; alpha is annualized and, when its HAC p-value exceeds 0.05, the output
says it is not distinguishable from zero. `analyze stock` adds the price summary,
the 0002 risk panel, CAPM beta and current fundamentals with the note that they
are not point-in-time.

## Quickstart: goal planning

```bash
sobres plan retire --income 200000 --expenses 90000 --portfolio 400000 --return 0.07
sobres plan house --price 950000 --down-pct 0.20 --by 2029-06-01 --monthly 3000
sobres plan car --price 45000 --by 2027-01-01 --current 5000
sobres plan education --annual-cost 35000 --years 4 --starting 2038
sobres plan goal --target 250000 --by 2032-01-01 --monthly 1500 --simulate 10000
```

Real by default (today's dollars; the return you give is deflated by trailing
10-year CPI, or 2.5% without a FRED key) with `--nominal` as the alternative.
Every answer comes with a simulated success probability and the 10th to 90th
percentile outcomes; `--method bootstrap --history SPY` resamples real return
blocks so bad-early-years paths appear. The seed is printed. Taxes are not
modeled and the output says so.

## Quickstart: econometrics

```bash
sobres econ forecast ticker:AAPL --horizon 20             # ridge VAR on the equity-basic state
sobres econ forecast ticker:AAPL --model bvar --sector ticker:XLK
sobres econ evaluate ticker:AAPL --models var bvar --horizon 20
sobres econ volatility SPY --model garch --horizon 30
sobres econ diagnose DGS10
sobres econ regress --y AAPL --x SPY DGS10 --robust hac
```

`econ forecast` models a joint state — the stock's split-only log return, the
market return (`ticker:SPY` unless `--benchmark` says otherwise), log realized
volatility and the change in log dollar-volume activity, plus an optional
`--sector` series — with a ridge VAR (default) or a Minnesota-prior BVAR. Lag
order and shrinkage are chosen on three chronological validation blocks inside
the training window, never on the held-out dates. Every run reports a held-out
evaluation over the last 252 sessions against no-change and training-mean
controls (return and price errors, direction accuracy, out-of-sample R², 80%/95%
coverage), and the price table carries 80% and 95% bounds from a joint residual
bootstrap with parameter refits (VAR) or posterior-predictive draws (BVAR). The
point is the median price draw; the seed is printed. A negative or near-zero
skill is reported as such — a forecast is evidence, not advice. Univariate ARIMA
was removed with this revision; `--model arima` explains the replacement.
`econ evaluate` scores VAR and BVAR on identical dates and never picks a winner
for you. Elastic-net and boosted-tree forecasters and the FRED macro preset are
[deferred](openspec/changes/0009-econometrics-forecasting/tasks.md).

## Quickstart: invest a saved portfolio (paper first)

```bash
sobres doctor --keys                                    # paper keys, typed without echo and verified
sobres portfolio save core --tickers AAPL MSFT --weights 0.6 0.4
sobres trade preview core --budget 1000                 # no side effects; prints a plan hash
sobres trade execute core --budget 1000 --plan <hash>   # records the intent, then submits
sobres trade status                                     # reconciles open/unresolved orders
sobres trade positions && sobres trade history          # what the broker reports; realized/unrealized
sobres trade close AAPL --quantity 4                    # preview, confirm, submit a sell
```

The broker sits behind a Sobres-owned port (`data/brokers/base.py`); Alpaca is
the first adapter and the shared workflow, storage, accounting and commands do
not know its name. Sizing is whole shares from the saved weights, a budget,
current holdings, pending orders and fresh quotes — a 60/40 portfolio, a $1,000
budget and quotes of $100/$50 preview as 6 and 8 shares with $0 residual.
`execute` refuses a plan whose hash no longer matches (quotes moved, holdings
changed) and a plan already confirmed; a submission that times out is recorded
as `unresolved`, never as failed or filled, until `status` asks the broker.
P&L uses average cost (buy 10 @ 100, sell 4 @ 110, mark 6 @ 105 → 40 realized,
30 unrealized); deposits are cash flows, not profit. **Paper is the default.**
Live needs `alpaca_environment=live`, `trading_live_enabled=true`, `--live` and
a typed confirmation of the account id; `execute` and `close` only run from the
terminal. Credentials are secrets (0600 config, redacted, browser-locked).

## Quickstart: exchange rates and purchasing power

```bash
sobres fx rates EURUSD USDJPY --start 2015-01-01
sobres fx convert 100000 --from USD --to EUR --on 2026-09-01
sobres fx attribution --tickers NESN.SW 7203.T --base USD --start 2015-01-01 --fill drop
sobres fx hedge --tickers NESN.SW 7203.T --base USD --start 2015-01-01 --fill drop
sobres ppp compare --base USD --vs EUR GBP JPY
sobres plan retire --income 200000 --expenses 90000 --save-goal fire
sobres ppp adjust-goal --goal fire --to PRT
```

Attribution splits each asset's base-currency return into local, currency
and cross components that reconcile exactly, and reports currency risk with
the correlations that drive it. Hedged figures are a covered-interest-parity
approximation and say so. PPP is reported as a valuation gap with its
benchmark year and vintage, never as a forecast; `adjust-goal` restates a
goal at another country's price level beside the market-rate figure.

## Quickstart: saved state

```bash
sobres portfolio save core --tickers AAPL MSFT NVDA JNJ --weights 0.3 0.3 0.2 0.2
sobres optimize markowitz --portfolio core --start 2018-01-01 --fill ffill --save-run
sobres run list                          # newest first, with a one-line summary
sobres run show <id> --format json       # the complete stored record
sobres run diff <id-a> <id-b>            # two runs of the same command, side by side
sobres watchlist add tech NVDA AMD
sobres db info                           # path, schema version, size, rows per table
sobres db export --to ~/backups/sobres-$(date +%F).sqlite
```

One SQLite file holds the cache and everything you save; `sobres cache clear`
removes cached observations only and says what it preserved.

**No API key is required** for the core tool. Prices come from yfinance and factor
returns from the Ken French Data Library, both keyless. A free
[FRED key](https://fredaccount.stlouisfed.org/apikeys) unlocks macro series
and the live risk-free rate. `sobres init` asks for it and offers to verify it;
later, `sobres doctor --keys` does the same without echoing the key.

## Quickstart: the web UI

```bash
pip install "sobres[web]"
sobres open                        # starts the server if needed, opens the browser
sobres open doctor                 # straight to a view: settings, doctor, runs, run <id>, ...
sobres serve                       # http://127.0.0.1:8787, no browser
sobres serve --host 0.0.0.0        # prints a token once; required off loopback
```

Every command in the registry is an HTTP route (`POST /api/v1/<group>/<name>`,
documented at `/api/docs`) and a form in the single-page app. The form shows the
equivalent `sobres` command line as you fill it in. Long computations
(`markowitz`, `frontier`, `backtest`) run as jobs with real progress and a cancel
button; results carry the same provenance header, in-sample label and disclaimer
the CLI prints. A parity test fails the build if a command lacks a route or a
view, and the same inputs give byte-identical JSON on both surfaces.

The server binds loopback by default. Binding any other address requires a
deployment token, generated once and stored hashed; `sobres serve token rotate`
replaces it. Cookies are `HttpOnly`, `SameSite=Strict` and the token is never
accepted on a query string.

## Data sources

| Source | Key | Used for |
|---|---|---|
| [yfinance](https://github.com/ranaroussi/yfinance) | — | Prices, dividends, splits, fundamentals |
| [FRED](https://fred.stlouisfed.org/) | free | Risk-free rate, CPI, macro series |
| [Ken French Data Library](https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/data_library.html) | — | Fama-French 3/5-factor + momentum returns |
| [ECB reference rates](https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html) | — | Daily exchange rates |
| [World Bank ICP](https://data.worldbank.org/indicator/PA.NUS.PPP) / [OECD](https://data.oecd.org/conversion/purchasing-power-parities-ppp.htm) | — | PPP conversion factors and price levels |

The options research ([0018](openspec/changes/0018-pre-earnings-options-poc/)) reads options chains, IV and Greeks and
the earnings calendar from Massive (Options Advanced plus the Benzinga earnings add-on); see
[docs/INSTALLATION.md](docs/INSTALLATION.md#4-pre-earnings-options-research).

Every provider, what it supplies, and its doctor line: [DATA.md](DATA.md).

Planned, not yet implemented ([0019](openspec/changes/0019-sec-filings-intelligence/)): SEC EDGAR (keyless, contact email
in the User-Agent) for filings; Anthropic (API key) for risk-factor labelling and
change summaries.

## Architecture

One rule, and everything follows from it:

```
              registry.py — every command declared once
                    │
adapters →  cli/ (Typer)  api/ (FastAPI)  frontend/ (React)   no business logic
math     →  core/         pure, I/O-free                      no network, no disk
I/O      →  data/         providers + SQLite                  no math
```

All math lives in `core/` as pure functions. The CLI, the HTTP API, and the web UI
are three renderings of one command registry — so "the UI has every CLI feature" is
a test that fails the build, not an intention. One SQLite file holds the cache,
saved portfolios, and run history, and is also the one thing Docker mounts. Full
detail: [`openspec/project.md`](openspec/project.md).

## Landing page

<https://ai-solutions-lab-llc.github.io/sobres/> is built from `site/` and deployed
by GitHub Actions on every push to `main` that touches it. Every figure on it is
recorded by `python site/scripts/record_figures.py` from real `sobres` runs, the
version and install commands are generated at build time, and the build fails on
a bundle over 150 KB, a Lighthouse score under 95, a third-party request, or a
stale command name.

## Development

```bash
pytest -m "not network"   # full suite, offline
pytest -m network         # live provider contract tests (run deliberately)
ruff check . && ruff format --check .
mypy
```

Tests never hit the network by default. Provider payloads are recorded as fixtures;
math is tested against hand-computed and textbook values.

### CI

Every pull request runs lint, format, `mypy --strict`, and the test suite on
Python 3.11–3.13 (Linux) plus 3.12 on macOS and Windows, then builds the wheel
and sdist, installs the wheel into a clean environment and runs it, and audits
the dependency tree. CodeQL and Dependabot run alongside. One aggregated status
check, **All checks passed**, gates merges.

### Releases

Releasing is a version bump. Change `__version__` in
`src/sobres/__about__.py`, add a `CHANGELOG.md` section, merge to `main`. The
pipeline re-runs the full gate on that commit and publishes using the
organization `PYPI_PROD` token, then tags, creates the GitHub release, and
pushes the container image. TestPyPI uses `PYPI_TEST`. Token uploads do not
produce PEP 740 attestations. Any push to `main` that doesn't change the
version publishes nothing — the declared version is the only gate.

See **[docs/RELEASING.md](docs/RELEASING.md)** for the one-time setup and the
failure playbook.

## Contributing

This project is spec-driven. Behavior changes start with an OpenSpec change under
`openspec/changes/` — proposal, spec delta, design, tasks — before implementation.
The spec delta is the contract; every scenario in it gets a test.

## License

MIT — see [LICENSE](LICENSE).

## Disclaimer

sobres is a research and education tool. It is not investment advice, not a
recommendation to buy or sell any security, and carries no warranty of accuracy.
Data comes from third-party sources that may be delayed, revised, or wrong.
Backtested results are hypothetical and do not indicate future performance.
