Docs

SDK and API reference

pip install stoxa, one key, every number as it was known on any date. This page covers install, keys, point-in-time semantics, the filter language, ranking systems, simulations, house strategies and every endpoint.

Getting started

Stoxa exposes everything the app does through a REST API at https://app.stoxa.io/api and a Python client, stoxa, that wraps it. Both need a personal API key from Settings → API keys; a Stoxa subscription is required.

pip install stoxa
export STOXA_API_KEY=vdk_...
from datetime import date
from stoxa import equities, macro

d = date(2026, 10, 30)
rf, src = macro.risk_free(as_of=d)                  # 10y Treasury as published on d, and where it came from
fin = equities.get_fundamentals("AAPL", as_of=d)    # only filings with filed <= d
val = equities.value_company("AAPL", as_of=d)       # the workbench's engine, sourced defaults
val.value_per_share, val.upside
g = equities.implied_growth("AAPL", as_of=d)        # reverse DCF: growth the price implies
picks = equities.screen([{"field": "pe", "op": "lt", "value": 15},
                         {"field": "roic", "op": "ge", "value": 0.15}], as_of=d)

The same calls over plain HTTP:

curl -H "Authorization: Bearer vdk_..." \
  "https://app.stoxa.io/api/companies/AAPL/fundamentals?as_of=2026-10-30"

Python ≥ 3.10. The client depends on httpx, pandas, numpy and pydantic only. Source and issues: hello@stoxa.io.

Authentication and access

Every request carries Authorization: Bearer vdk_…. A key acts as its owner: it has exactly the access your account has, so it stops working if your subscription lapses, and it is revoked the moment you revoke it in Settings. You can hold up to 10 active keys; label them per machine or agent so you can revoke one without touching the others.

StatusMeaning
401Missing, unknown, revoked or expired credential. The SDK raises stoxa.ApiError with a hint to set STOXA_API_KEY.
403Valid key whose owner has no access (no active subscription), or a non-admin calling an admin route.
404Unknown ticker, series or job. get_fundamentals returns an empty frame instead; value_company raises ValueError.
400 / 422Bad filter, operator, field name or request body. The detail field says which.

Keys are shown once at creation and stored only as a hash. Treat them like passwords: environment variables or a secrets manager, never a notebook you share. GET /api/me tells you who the server thinks you are.

Point-in-time semantics

Every data call takes as_of. With it, the answer is what was public on that date: fundamentals come only from filings with filed ≤ as_of, with restatements resolved to the latest such filing; prices stop at as_of; macro series use only vintages published by then. Without it, you get today. A screen or backtest on a date is built from a snapshot of the whole universe as it stood then, so a backtest cannot peek.

The first screen on a date Stoxa has not screened before builds that day's snapshot server-side, which takes a few minutes for the whole universe. Later screens on that date are instant. POST /api/backtest/estimate says how many dates a backtest would have to build.

get_fundamentals(..., provenance=True) appends <field>__accession and <field>__filed columns, the SEC filing each number came from, so a value can always be traced to its source.

Python SDK

Configuration

import stoxa
stoxa.configure(api_key="vdk_...")            # beats the environment; url and timeout optional
stoxa.current_config()                        # {"url": "https://app.stoxa.io", "api_key_set": True, "timeout": 60.0}

Environment: STOXA_API_KEY, and STOXA_API_URL if you are not talking to app.stoxa.io. Non-2xx answers raise stoxa.ApiError with .status and .detail.

stoxa.equities

FunctionReturns
get_universe()DataFrame of every covered ticker: ticker, cik, name, exchange, asset_type, sic, sector, industry.
get_prices(ticker, start=None, end=None)Daily split-adjusted OHLCV DataFrame.
get_returns(ticker, start=None, end=None)Daily close-to-close simple returns (Series; dividends excluded).
get_fundamentals(ticker, fields=None, as_of=None, freq="annual", provenance=False)DataFrame indexed by period end, one column per canonical field (revenue, operating_income, net_income, total_assets, …), as known on as_of. freq="quarterly" for quarters. Empty frame for an unknown ticker.
get_metrics(ticker, as_of=None)Dict of derived metrics, the same keys the screener filters on (see Filters).
screener_fields()The filterable fields with label, type and, for categorical ones, options.
screen(filters, as_of=None, sort=("market_cap", True), columns=None, limit=200, timeout=900, universe=None, funnel=False, ranking=None)DataFrame of matches; df.attrs["as_of"] and ["count"] carry the snapshot date and total count, ["funnel"] the survivors after each filter entry when funnel=True. With ranking, the rank fields become usable and the default sort is rank.
backtest(filters, start, end, rebalance="Q", horizon_days=None, limit=20, sort=None, timeout=1800, *, return_type="total", costs=None, hold_tolerance=None, benchmark="SPY", universe=None, ranking=None)Dict with periods, equity_curve, summary (CAGR, volatility, Sharpe, Sortino, max drawdown, beta, alpha, cost drag, …) and vintage. Costs default to the variable model. Synchronous; can take minutes.
rolling_backtest(filters, start, end, step="M", holding="Q", limit=20, ...)One window per step date, each held for holding: the distribution of window returns against the benchmark (mean, median, percentiles, share beating the benchmark, histogram).
list_universes(), save_universe(name, filters=None, tickers=None), delete_universe(universe_id)Built-in universes (all, liquid, large_cap, mid_cap, small_cap) and saved ones; a saved universe is a filter list or a ticker list.
list_rankings(), get_ranking(system_id), validate_ranking(definition, na_policy=None), save_ranking(definition, name=None, na_policy=None, description=None, system_id=None), delete_ranking(system_id)Ranking systems: the built-ins, then yours. validate_ranking returns the normalised and effective weights without saving; invalid definitions raise ApiError (422) naming the node path.
rank(system, as_of=None, universe=None, detail=False, na_policy=None, timeout=900)DataFrame: ticker, rank (0 to 100, 100 best), rank_pos, and with detail one column per node.
rank_performance(system, start, end, rebalance="M", buckets=10, universe=None, na_policy=None, costs=None, benchmark="SPY", min_price=None, return_type="total", timeout=1800)RankPerformance: the bucket test. to_pandas() is one row per bucket; ["statistics"] has Spearman, slope and top-minus-bottom for the whole period and each half.
validate_strategy(strategy), simulate(strategy, wait=True, timeout=3600, poll_seconds=2.0, strategy_id=None), simulation_job(job_id)Portfolio simulation of a Strategy dict. simulate queues a job and polls it; the Simulation result has equity() and trades() frames, summary and warnings.
list_strategies(), get_strategy(strategy_id), save_strategy(strategy, name=None, description=None, strategy_id=None), delete_strategy(strategy_id)Your saved strategies, stored normalised.
house_strategies(), house_strategy(slug)The eight house strategies: a DataFrame of cards with since-launch statistics, or one HouseStrategy with definition(), stats(), equity(), holdings(), trades(), yearly() and ["recommendations"].
follow(slug, capital, mode="jump_in", name=None), portfolios(), portfolio_rebalance(portfolio_id, accept=False)Follow a house strategy as a paper portfolio (jump_in or scale_in), list your portfolios with drift, read the rebalance diff or accept it at the latest close.
clone(slug, name=None)Copy a house strategy into your saved strategies (dates covering the last ten years); returns the new id.
default_assumptions(ticker, as_of=None)The sourced Assumptions the workbench starts from on that date.
value_company(ticker, as_of=None, **overrides)ValuationResult: value_per_share, price, upside, margin_of_safety, wacc_detail, bridge, per_year, warnings. Overrides are dotted paths, e.g. **{"wacc.value": 0.09}; pass assumptions= to start from your own object.
implied_growth(ticker, as_of=None, **overrides)Reverse DCF: the initial revenue growth the price implies, or None outside [-50%, 60%].
stochastic_dcf(ticker, distributions, n=2000, as_of=None, **overrides)Monte Carlo over dotted assumption paths; percentiles, p_undervalued, histogram. Specs: normal, lognormal, triangular, uniform.
portfolio_sim(tickers, weights=None, days=252, n_paths=2000, start=None, seed=None)Joint block-bootstrap portfolio simulation: mean and percentile terminal returns, VaR/CVaR, mean max drawdown, correlation matrix.
simulate_gbm, bootstrap_paths, var_cvarPure helpers (no API call); also under stoxa.simulation.

Ratios are fractions (ROIC of 15% is 0.15); money is in dollars.

stoxa.macro

FunctionReturns
list_series(category=None, geography=None, q=None)Catalogue rows: code, name, category, geography, frequency, units, source.
get_series(code, transform=None, as_of=None, start=None, end=None)List of (period, value) as known on as_of. Transforms: level, yoy, mom, mom_ann, ann3m, ann6m, z1y, z3y, z5y, z10y, diff, diff_yoy. Unknown code raises KeyError.
get_frame(codes, ...)Wide DataFrame indexed by period, one column per code.
latest(code, as_of=None, transform=None, max_age_days=None)The last observation on or before as_of, or None.
risk_free(as_of=None)(rate, provenance): the 10-year Treasury chain the valuation defaults use.
context(as_of=None)Regime as known then: ust10y, ust2y, spread_2s10s, cpi_yoy, unemployment, hy_oas, sahm, curve_state (inverted | flat | steep), recession_signal, source.
recessions()NBER recession bands.
available()Whether the macro store is answering.

Series codes look like US.RATES.UST.10Y or US.INFL.CPI.HEADLINE.SA; 15 geographies are covered. Use list_series(q="cpi") to search.

Filters

Screens and backtests share one filter shape, in the SDK and over HTTP. A filter list is a set of entries joined with and. An entry is a condition, or an or group of conditions:

[{"field": "pe", "op": "lt", "value": 15},                       # condition
 {"any": [{"field": "pe", "op": "lt", "value": 15},               # OR group: passes when any one holds
          {"field": "ev_ebit", "op": "lt", "value": 10}]},
 {"field": "sector", "op": "not_in", "value": ["Utilities"]},
 {"field": "roic", "op": "between", "value": [0.15, 0.6]}]
OperatorValueFields
lt le gt ge eq nenumber (or text for eq/ne on categorical fields)any
between[low, high], inclusivenumeric
in, not_inlist of valuescategorical (ticker, name, sector, industry, exchange, macro_curve_state, macro_recession_signal)

Groups are one level deep: a group inside a group, or an empty group, is rejected with 422. Rows whose field is null fail the condition, as in SQL.

Numeric fields. composite_score, current_ratio, debt_to_equity, dividend_yield, dividends_ttm, ebit_ttm, ebitda_ttm, eps_growth, ev, ev_ebit, ev_ebitda, ev_sales, factor_growth, factor_momentum, factor_quality, factor_risk, factor_value, fcf_margin, fcf_ttm, gross_margin, interest_coverage, market_cap, net_debt_ebitda, net_income_ttm, net_margin, operating_margin, payout_ratio, pb, pe, pfcf, price, ps, ret_12m, ret_1m, ret_3m, ret_6m, revenue_growth, revenue_ttm, roa, roe, roic, vol_1y.

Macro fields. macro_ust10y, macro_spread_2s10s, macro_cpi_yoy, macro_unemployment, macro_hy_oas, macro_sahm (numeric) and macro_curve_state, macro_recession_signal (categorical). They hold one value per snapshot date, the regime as known then, so in a backtest a macro condition keeps or drops the whole rebalance. screener_fields() or GET /api/screener/fields is the authoritative list.

Rankings, simulations, strategies

A ranking system is a tree whose root is a composite. Factor nodes rank one numeric screener field within the universe, the sector or the industry; composite nodes take the weighted mean of their children and re-rank it; conditional nodes route stocks to one of two branches. Weights are relative among siblings (all zero means equal). A rank is 0 to 100, 100 best.

{"type": "composite", "name": "Cheap quality", "children": [
  {"type": "composite", "name": "Value", "weight": 1, "children": [
    {"type": "factor", "name": "P to E", "field": "pe", "direction": "lower"},
    {"type": "factor", "name": "EV to EBITDA", "field": "ev_ebitda", "direction": "lower", "scope": "industry"}]},
  {"type": "factor", "name": "ROIC", "field": "roic", "direction": "higher", "weight": 2}]}

Limits: 4 levels below the root, 60 factor nodes, sibling names unique, no / in names. na_policy decides what a missing value scores: negative (ties just below the worst valid stock; the default for new systems), neutral (the middle of the valid scores) or skip (the node drops out of its parent's mean). With a ranking attached to a screen or backtest, the fields rank, rank_pos, rank_change_4w, rank_change_13w and node:<path> are usable in filters, columns and sort; without one they are rejected (400).

A strategy is the body of every simulation endpoint and of saved strategies. Only start and end are required; unknown keys are rejected (422).

{"name": "Quality momentum", "universe": "liquid",
 "ranking": {"system": "Core: Combination", "na_policy": "negative"},
 "buy_rules": [{"field": "price", "op": "ge", "value": 5}],          // ANDed, like screen filters
 "sell_rules": [{"kind": "rank_below", "value": 80},                 // ORed, checked at rebalances
                {"kind": "pct_from_high", "op": "le", "value": -25}],
 "positions": {"target": 20, "max": null, "min_cash_pct": 0, "sector_cap_pct": 30, "industry_cap_pct": null},
 "rebalance": "M",                                                   // W | M | Q | A
 "weights": "equal",                                                 // equal | rank
 "stops": {"trailing_pct": 20, "entry_pct": null, "time_days": null},
 "buyback_cooloff_days": 30,
 "hedge": {"vehicle": "cash", "ratio_pct": 50,
           "entry": {"kind": "benchmark_below_sma", "days": 200}, "exit": null},
 "costs": {"model": "variable"}, "commission": {"per_trade": 0, "pct": 0},
 "return_type": "total", "start": "2015-01-01", "end": "2024-12-31",
 "capital": 100000, "benchmark": "SPY"}
Sell rule kindSells when
fieldthe held stock meets a screener condition (field, op, value, or an any group); not the rank fields
rank_belowits rank is below value; needs a ranking; no rank also sells
rank_pos_aboveits rank position is above value; needs a ranking
fails_buy_rulesit no longer passes the universe and the buy rules
gain_pctpercent gain since entry meets op / value (dividends excluded)
days_heldcalendar days held meet op / value
pct_from_highpercent from the highest close since entry meets op / value

Invalid strategies answer 422 with errors: [{"path": "sell_rules[1].op", "message": "..."}]. Simulations are long only: no shorting, margin or leverage, no formula language, fills at daily closes in whole shares. House strategies use the same schema: GET /api/strategies/house/{slug} returns the definition the simulator runs. Every statistic outside the backtest block and the backtest rows of yearly is since launch.

REST API reference

Base URL https://app.stoxa.io/api. JSON in and out; errors are {"detail": "message"}. Dates are ISO YYYY-MM-DD; timestamps are UTC. The full contract with example payloads ships with the SDK docs; this is the map.

Meta and account

EndpointWhat it does
GET /health{"status": "ok", "db": "ok"}
GET /meWho the caller is: kind (apikey | firebase), uid, email, is_admin.
GET /me/keys, POST /me/keys, DELETE /me/keys/{id}List, create and revoke API keys. Signed-in session only, not from a key.

Companies

EndpointWhat it does
GET /universe?q=&limit=Search the ticker universe.
GET /companies/{ticker}Profile, price, market cap, metrics, data-quality checks.
GET /companies/{ticker}/fundamentals?freq=annual|quarterly&as_of=&provenance=Canonical statements as known on a date; provenance=true adds the filing behind every value and all vintages of it.
GET /companies/{ticker}/prices?start=&end=&adjusted=Daily candles.
GET /companies/{ticker}/metrics?as_of=All screener metrics for one company.
GET /companies/{ticker}/filings?limit=Recent SEC filings with links.
GET /companies/{ticker}/peersPeer set with headline multiples.

Screener and backtest

EndpointWhat it does
GET /screener/fieldsFilterable fields with label, type, category, unit, description and options; rank fields carry requires_ranking: true.
POST /screener/runBody: filters, as_of, sort {field, desc}, columns, limit, and optionally universe, funnel: true (survivors after each filter entry) and ranking {"system": id|name} or {"definition": {...}} with na_policy, nodes. Returns as_of, count, columns, rows (and funnel, ranking).
GET|POST|DELETE /screensSaved screens (name + run payload).
GET|POST /universes, DELETE /universes/{id}Built-in universes, then saved ones. Body {"name", "definition": {"filters": [...]} | {"tickers": [...]}} → {"id"}. Built-in and digit-only names are reserved.
GET|POST|DELETE /watchlistsWatchlists.
POST /backtestSynchronous backtest. Body: filters, start, end, rebalance (W | M | Q | A), horizon_days, limit, sort, return_type (total | price), costs ("none" | {"model": "fixed", "pct"} | "variable" | {"model": "sqrt", "capital", "k", "l"}; default variable), hold_tolerance, benchmark (ticker | "equal_weight_universe" | null), universe, ranking, mode (backtest | rolling | rank_performance). Returns periods, equity_curve, summary, vintage.
POST /backtest/rollingRolling backtest: a window starts every step date and is held for holding; returns windows, summary (mean, median, p10–p90, share beating the benchmark), histogram, vintage.
POST /backtest/estimateDates total, ready and to build, with a seconds estimate. Cheap.
POST /backtest/jobs, GET /backtest/jobs/{id}, GET /backtest/jobs, DELETE /backtest/jobs/{id}Asynchronous backtests, rolling tests, rank performance tests and simulations, with progress and ETA; jobs are private to the key owner.

Rankings

EndpointWhat it does
GET /rankingsBuilt-ins (Core: Value, Core: Quality, Core: Momentum, Core: Growth, Core: Low Risk, Core: Combination, Core: Value (industry-relative), Quality at a Reasonable Price), then your systems.
GET /rankings/{id}One system; 404 when unknown or someone else's.
POST /rankings, PUT /rankings/{id}, DELETE /rankings/{id}Body {"name", "description", "definition", "na_policy"} → {"id"}. Built-ins are read-only (403). Invalid definitions → 422 with errors: [{path, message}].
POST /rankings/validateBody {"definition", "na_policy"} → always 200: {"ok": true, "nodes": [{path, weight_share, effective_weight, ...}], "leaves", "depth"} or {"ok": false, "errors": [...]}.
POST /rankings/rankBody {"system": id|name | "definition": {...}, "as_of", "universe", "na_policy", "detail"} → columns (ticker, rank, rank_pos, node scores with detail), rows best first, per-node NA / valid / fallback counts.
GET /companies/{ticker}/ranks?system=&as_of=&universe=&na_policy=Why a stock ranks where it does: rank, position and, per node, the score, value, scope used and group size. Default system Core: Combination.
POST /rankings/{id or name}/performanceThe bucket test. Body {"start", "end", "rebalance", "buckets", "universe", "na_policy", "costs", "benchmark", "min_price", "return_type"} → per-bucket annual return, excess, volatility, Sharpe; Spearman, Pearson and slope for the whole period and each half; series; vintage. Also as a job with "mode": "rank_performance".

Simulations and saved strategies

EndpointWhat it does
POST /simulations/validateBody: a Strategy → {"ok": true, "strategy": {...}} with every default filled in, or 422 with paths. Nothing is run.
POST /simulations/estimateDates total, ready and to build, with a seconds estimate: one snapshot per rebalance date.
POST /simulations[?strategy_id=]202 {"id", "status": "queued", "estimate"}: a job in the backtest queue; poll, list and cancel through /backtest/jobs.
POST /simulations/runThe result synchronously: summary, equity, hedge_periods, trades, round_trips, holdings_by_date, periods, vintage, warnings.
GET|POST /strategies, GET|PUT|DELETE /strategies/{id}Saved strategies. Body {"name", "description", "definition": Strategy} → {"id"}; stored normalised; source records a clone's house strategy and version.

House strategies, portfolios, notifications

EndpointWhat it does
GET /strategies/houseOne card per house strategy: slug, name, aim, version, launched_at, rebalance, positions, followers, labels (risk score, trading commitment, liquidity floor, max recommended capital), since_launch statistics, sparkline.
GET /strategies/house/{slug}The card plus description, definition, versions, live equity, summary, the labelled pre-launch backtest, yearly returns (phase backtest | live), holdings with today's ranks, the latest 100 trades and the current recommendations.
POST /strategies/house/{slug}/followBody {"capital": 25000, "mode": "jump_in" | "scale_in", "name"} → a Portfolio. 404 unknown slug; 409 no prices yet.
POST /strategies/house/{slug}/cloneBody {"name"} (optional) → {"id", "source": {"house", "version"}}: the definition saved as one of your strategies.
GET /portfoliosYour followed portfolios: capital, status (following | manual), equity, cash, total_return, holdings, drift, in_sync, pending_orders.
GET /portfolios/{id}, DELETE /portfolios/{id}One portfolio with daily equity points and trades; delete it.
GET /portfolios/{id}/rebalanceThe diff against the model: orders (sell | buy | trim | add, shares and value at the latest close), drift, in_sync, excluded.
POST /portfolios/{id}/rebalance/acceptExecute the diff at the latest close (sells first) → {"trades", "portfolio"}.
POST /portfolios/{id}/unfollowKeep the portfolio as manual history, still marked nightly.
GET /notifications?limit=, POST /notifications/readRebalance notices for the strategies you follow, newest first; mark them read.

Public (no key)

EndpointWhat it does
GET /public/strategiesThe one call that needs no credential (CORS for the Stoxa site, cached 15 minutes): per house strategy the name, aim, launch date, version, rebalance, risk score, trading commitment, since-launch total return, CAGR, max drawdown and SPY return, and the tickers held 30 days ago (no share counts or weights). Since-launch numbers only.

Valuation

EndpointWhat it does
GET /valuation/{ticker}/defaults?as_of=&preset=base|streetInputs, the full sourced Assumptions object, the source of each assumption, and data-quality checks.
POST /valuation/runBody: ticker, as_of, assumptions. Returns value per share, upside, WACC detail, EV-to-equity bridge, per-year table, warnings. Models: dcf_fcff, reverse_dcf, relative, ddm, residual_income.
POST /valuation/reverseImplied revenue growth for the current price.
POST /valuation/sensitivityTwo-parameter grid of value per share.
POST /valuation/montecarloDistributions over assumption paths; n ≤ 20000.
GET|POST|DELETE /scenariosSaved assumption sets per ticker.

Macro

EndpointWhat it does
GET /macro/context?as_of=Regime as known on a date, plus risk_free and its provenance.
GET /macro/series?category=&geography=&q=Catalogue.
GET /macro/series/{code}/observations?transform=&asof=&start=&end=Observations as of a vintage date. Note the spelling asof here.
GET /macro/recessionsNBER bands.
GET /macro/status, /macro/overview, /macro/announcements, /macro/sourcesStore status, the Pulse overview, the release calendar, source list.

Errors, limits, conduct

  • Errors are JSON with a detail string; the SDK raises ApiError(status, detail). A 401 from a key you just created usually means the key was pasted with whitespace.
  • Timeouts. Most calls answer in well under a second. A screen on a new date, a backtest, or a company Stoxa has never ingested (it fetches the filings on first request) can take minutes; the SDK's long timeouts on screen and backtest reflect that.
  • Limits. Backtests run one at a time per server and queue; Monte Carlo caps at 20,000 draws; screens at 200 rows by default (limit up to the universe). There is no hard request rate today. Please keep automated clients to what a person could reasonably use; bulk re-downloading the dataset is not what the key is for.
  • Use. Your key is for your own research, notebooks and agents. Redistributing the dataset or substantial extracts, or sharing keys, is outside the terms of use. Quoting figures with attribution is fine.
  • Changes. Fields and endpoints are added without notice; removals or renames are announced by email to key holders at least 30 days ahead. The SDK is versioned on PyPI; pin it in production.

Nothing here is investment advice. See the terms of use and privacy policy.