Documentation

Fundamentals

Work in progress: Fundamentals are not fully functional yet. Using this feature in the IDE or in backtests can return inconsistent fundamentals data (missing fields, mismatched report dates, or unstable bar-aligned snapshots). Treat results as experimental until a future release marks fundamentals stable.

QuantCraft strategies can use company financial data alongside OHLCV price bars: balance sheets, cash-flow statements, income statements, and earnings history / estimates. The data is loaded once per symbol from the QuantCraft fundamentals service, organized as date-keyed tables, and made available to your strategy as bar-aligned snapshots during a backtest. Callback timing follows Strategy lifecycle; configure symbols and dates in Test (Running code).

Beyond the three statements you also get ratios (91 of them), per-share values, valuation multiples and yields, and quality scores (Piotroski F, Altman Z-prime, Beneish M) — all point-in-time, all optional. These roll out per symbol, so guard every access; raw["metricsInputs"] is None means a bundle predates them.

This page covers:

  • What financial statement data is available
  • How date-keyed tables are organized
  • How bar-aligned payloads reach your callbacks during a backtest

Related: Global Fama-French factor return datasets (not per-ticker) are a separate feature — see Fama-French factors (fama_french.qc, no callback argument). Factor rows are keyed by YYYYMM strings, not list indices.


What you get

Each fundamentals bundle is a single JSON object for one ticker, containing:

  • Balance sheet — quarterly and yearly reports.
  • Cash flow — quarterly and yearly reports.
  • Income statement — quarterly and yearly reports.
  • Earnings:
    • History — reported EPS vs. estimates and surprises by report date.
    • Trend — forward estimates, EPS trend, and analyst revisions.
    • Annual — epsActual per fiscal year.
  • Ratios — quarterly and yearly financial ratios (margins, returns, liquidity, leverage).
  • Financials extended — quarterly and yearly extended metrics not present in the core income, balance-sheet, or cash-flow statements.
  • Per share — quarterly and yearly per-share values as filed (share count on that period's split basis).
  • Valuation — quarterly and yearly market cap, enterprise value, multiples and yields, plus a non-date latest object.
  • Scores — quarterly and yearly Piotroski F, Altman Z-prime and Beneish M, plus notApplicableReason.
  • TTM — a single trailing-twelve-month object (not bucketed by period).
  • Metrics provenance — flat metricsInputs / metricsHealth objects. metricsInputs is None on a bundle that predates the metrics expansion.
  • Filing history — quarterly and yearly filing metadata (form type, filed date, keyed by period end). Also included in bar-aligned current(...) snapshots when present in the bundle; raw tables remain available via f.tables["filingHistory.quarterly"].
  • Company info — Flat entity metadata (name, CIK, SIC, address, exchanges, tickers). Not date-keyed and not in bar-aligned snapshots; access via f.raw["companyInfo"].

Each report is a row of camelCase metrics — for example totalAssets, cash, freeCashFlow, totalRevenue, netIncome, epsActual, epsEstimate, surprisePercent. Numeric values may arrive as strings in the underlying JSON, and any missing field comes back as None.

A given ticker may not expose every section. See the Fundamentals Coverage Explorer to check which fields are available for a ticker.


Date-keyed tables

For each section above, fundamentals are organized as a date-keyed table: rows are keyed by report date (YYYY-MM-DD), and metrics are exposed as parallel lists that you can index like price history.

The standard table paths are:

PathMeaning
balanceSheet.quarterly / balanceSheet.yearlyBalance sheet snapshots
cashFlow.quarterly / cashFlow.yearlyCash flow statements
incomeStatement.quarterly / incomeStatement.yearlyIncome statements
earnings.HistoryReported EPS, estimates, surprise per report
earnings.TrendAnalyst estimates, EPS trend, revisions
earnings.AnnualAnnual epsActual
ratios.quarterly / ratios.yearlyFinancial ratios (margins, returns, liquidity, leverage)
financialsExtended.quarterly / financialsExtended.yearlyExtended financial metrics
perShare.quarterly / perShare.yearlyPer-share values (as filed)
valuation.quarterly / valuation.yearlyValuation multiples and yields
scores.quarterly / scores.yearlyPiotroski / Altman / Beneish
filingHistory.quarterly / filingHistory.yearlyFiling metadata (form type, filed date)

Not tables. valuation.latest, scores.notApplicableReason, ttm, metricsInputs, metricsHealth and companyInfo are not date-keyed, so they are absent from tables, lists and every bar-time snapshot — that is intentional:

Not a tableWhy
valuation.latestA single object beside the date keys, not a period
scores.notApplicableReasonA string beside the date keys
ttmA single object; values are scalars rather than row dicts
metricsInputs / metricsHealthFlat provenance objects
companyInfoFlat entity metadata

All of these are as-of-upload rather than per-period, so keeping them out of tables also keeps them out of every bar-time snapshot — a backtest cannot accidentally read today's price off a 2019 bar. Reach them through f.raw instead. An empty block ({"quarterly": {}, "yearly": {}}, which is what a financial-sector filer's scores looks like) registers no table at all.

Index convention: newest first

Inside any of these tables, index 0 is the most recent eligible period, index 1 is the previous one, and so on. This matches how price series work in the rest of QuantCraft.

If you want to load a bundle directly (for example outside a backtest), the basic shape is:

from quantcraft.fundamentals import QcFundamentals f = QcFundamentals(token, "fundamentals/AAPL.json.gz") latest_total_assets = f.tables["balanceSheet.quarterly"]["totalAssets"][0] prior_total_assets = f.tables["balanceSheet.quarterly"]["totalAssets"][1] same = f.lists["balanceSheet.quarterly.totalAssets"][0] whole = f.raw

What you can read off a loaded bundle:

AttributeWhat it gives you
f.rawThe full decoded JSON tree.
f.symbolResolved ticker (e.g. "AAPL").
f.tablesDict of DateKeyedTable objects keyed by dotted path (e.g. "balanceSheet.quarterly").
f.table(path)Convenience accessor for a single DateKeyedTable.
f.listsFlat map keyed by "<tablePath>.<fieldName>", values aligned newest-first.

A DateKeyedTable exposes periods (the YYYY-MM-DD keys, newest first), columns (field → values), and supports table["fieldName"] indexing.

Discovering fields at runtime is easy: sorted(f.lists.keys()) shows every flat series, and sorted(t.columns.keys()) shows the columns of one table — useful when a server adds or removes fields.


Bar-aligned payloads in backtests

In a backtest, you don’t read fundamentals as raw tables — the engine selects the right report for the current bar and gives it to your callbacks as a snapshot. This is called the bar-aligned payload.

Configuring fundamentals for a backtest

On the Symbol & data tab of Run backtest:

  1. Check Uses fundamentals (select if your strategy requires fundamental data) (requires sign-in — QuantCraft sends your session token to load B2 fundamentals files). Bulk and chart forward runs label it Use fundamentals (optional). The checkbox is shown when fundamentals features are available in your build, but it is disabled (with a Premium hint) unless you have Premium or free-data access is enabled.
  2. Pick one or more fundamentals bundles from the multi-select (paths like fundamentals/AAPL.json.gz).

With a Screener symbol source (backtest) or Screener target (AutoTrading / QuantCloud bulk runs) you don't need to pick anything: turn fundamentals on and every symbol the screen selects gets its own bundle, so fundamentals.current(bar["t"], symbol=symbol) works for each one. Bundles come from the Screener's own download (nothing is fetched twice), and a symbol the Screener has not downloaded yet is fetched once and kept. In a live run the bundles follow each re-screen. Picks are still accepted as extras (e.g. a benchmark ticker). A symbol whose data can't be loaded returns None rather than failing the run — the Output panel prints how many loaded.

OHLCV symbols and fundamentals symbols are independent. You can backtest RNMBY via the broker feed while loading RHM.XETRA fundamentals — pass the correct ticker into fundamentals.current(..., symbol="RHM.XETRA") (or equivalent) in code.

When multiple bundles are selected, use explicit symbol= on fundamentals.current / get_data_with_offset when you need a specific bundle. The primary bundle used for bar-aligned snapshots defaults to the first OHLCV symbol in your symbol list unless the engine applies another rule.

For low-level table access outside callbacks, use qc_fundamentals from quantcraft.process.runtime when it is not None (e.g. qc_fundamentals.raw["companyInfo"]). It holds the primary bundle only; use the callback fundamentals handle with symbol= for any other ticker.

If fundamentals are not configured, the engine simply passes fundamentals=None to your callbacks, and you guard for it.

Point-in-time (backtest): The backtest engine and Debug Test both construct BacktestFundamentals(..., backtest_mode=True), so current / get_data_with_offset use filing-date gating (use_filing_date=True) — a period is excluded if its filing_date is after the bar time. When a period has no usable filing_date, an assumed filing deadline is used instead — 45 days after period end for quarterly tables, 90 days for annual (default_filing_lag_days) — rather than making the report visible the day its period closed. Forward runs use period-end eligibility (backtest_mode=False). Prefer the callback fundamentals handle in IDE backtests; calling raw qc_fundamentals.get_data_with_offset(...) without use_filing_date=True can look ahead relative to filing dates.

Forward runs: a live run reloads its fundamentals about every hour, and within about 5 minutes after the Screener's data updates. A failed reload keeps the previous data. Because a forward run's "now" really is today, the as-of-upload blocks (raw["ttm"], raw["valuation"]["latest"], …) are current data there, not look-ahead.

Legacy callbacks: Older strategies may use a separate qc_fundamentals argument or a raw dict instead of BacktestFundamentals. The modern signature passes fundamentals as the third argument to on_bar / on_tick. The module-level alias fundamentals_as_of_bar maps to the same bar-aligned payload shape.

The fundamentals callback parameter

When fundamentals are loaded, the engine passes a BacktestFundamentals handle as the fundamentals argument to on_bar, on_tick, and (when used on forward runs) on_timer:

def on_bar(bar_index, bar, fundamentals=None, symbol=None): if fundamentals is None: return snap = fundamentals.current(bar["t"]) bs_q = (snap.get("balanceSheet") or {}).get("quarterly") or {} total_assets = bs_q.get("totalAssets")
Method / memberPurpose
fundamentals.current(bar_time, symbol=None)As-of snapshot dict for that bar time (filing-date gated in backtest). Optional symbol selects which loaded bundle; omit for primary. Includes filingHistory when present in the bundle.
fundamentals.get_data_with_offset(bar_time, offset, *, symbol=None, print_on_shortfall=True)Nth-prior period per sub-table for the chosen bundle (same filing-date mode as current).
fundamentals.symbolsTuple of uppercase keys for every loaded bundle.
fundamentals.primary_symbol()Primary key used when symbol is omitted.
fundamentals.reload_all()Re-downloads every bundle. Forward runs call this for you; you rarely need it.

What the snapshot looks like

A bar-aligned snapshot is a nested dict with only the periods that are valid as of this bar (filing-date gated in IDE backtests). Each non-null section includes a periodEnd plus the report’s metrics:

{ "barTime": "2026-06-01T00:00:00+00:00", "symbol": "AAPL", "balanceSheet": { "quarterly": { "periodEnd": "2026-03-31", "totalAssets": 352000000.0, "cash": 51000000.0 }, "yearly": { "periodEnd": "2025-09-30", "totalAssets": 344000000.0 } }, "cashFlow": { "quarterly": { "periodEnd": "2026-03-31", "freeCashFlow": 24500000.0 } }, "incomeStatement": { "quarterly": { "periodEnd": "2026-03-31", "totalRevenue": 95000000.0, "netIncome": 23000000.0 } }, "earnings": { "History": { "periodEnd": "2026-03-31", "reportDate": "2026-05-02", "epsActual": 1.53 }, "Trend": null, "Annual": { "periodEnd": "2025-09-30", "epsActual": 6.13 } }, "ratios": { "quarterly": { "periodEnd": "2026-03-31", "grossMargin": 0.47, "operatingMargin": 0.31, "currentRatio": 1.04 }, "yearly": { "periodEnd": "2025-09-30", "grossMargin": 0.46, "currentRatio": 0.87 } }, "financialsExtended": { "quarterly": { "periodEnd": "2026-03-31", "ebitda": 38000000000.0, "ebitdaMargin": 0.40 }, "yearly": { "periodEnd": "2025-09-30", "ebitda": 134000000000.0 } }, "perShare": { "quarterly": { "periodEnd": "2026-03-31", "bookValuePerShare": 4.3, "grahamNumber": null }, "yearly": null }, "valuation": { "quarterly": { "periodEnd": "2026-03-31", "marketCap": 3812000000.0, "priceToEarningsRatio": null, "earningsYield": -5.0, "priceBasis": "split-adjusted", "approximate": true }, "yearly": null }, "scores": { "quarterly": { "periodEnd": "2026-03-31", "piotroskiFScore": 3, "beneishMScore": -1.94 }, "yearly": null }, "filingHistory": { "quarterly": { "periodEnd": "2026-03-31", "formType": "10-Q", "filedAt": "2026-05-02" }, "yearly": { "periodEnd": "2025-09-30", "formType": "10-K", "filedAt": "2025-10-31" } } }

Trend can be null when no period qualifies on or before the bar time. The priceToEarningsRatio: null beside earningsYield: -5.0 above is a loss-maker, not missing data — see Valuation.

Never in the snapshot: ttm, valuation.latest, scores.notApplicableReason, metricsInputs, metricsHealth, companyInfo. They are as-of-upload rather than per-period, so attaching them to a bar would leak future information into a backtest. Read them from qc_fundamentals.raw[...].

The same per-bar dict appears under each row’s fundamentals field in the structured backtest result, so what you see in your strategy matches what shows up in the result data.

Looking up older periods (get_data_with_offset)

Sometimes you want not the latest report as of a bar, but a previous one — the prior quarter, prior fiscal year, the previous earnings line:

prior = fundamentals.get_data_with_offset(bar["t"], 1) prev_bs_q = (prior or {}).get("balanceSheet", {}).get("quarterly")

Behavior:

  • Each sub-table is indexed independently. With offset=1, the balanceSheet.quarterly section steps back one quarter, while balanceSheet.yearly steps back one fiscal year, and so on.
  • If a sub-table doesn’t have enough history before the current bar, its section is null in the returned dict (and a one-line note may be printed).
  • The shape is the same as current(...), plus a top-level "offset": <int>.
  • On the raw QcFundamentals API, the preferred signature is f.get_data_with_offset(bar_time, offset, *, print_on_shortfall=True, use_filing_date=False). The module-level function get_data_with_offset(qc, bar_time, offset, *, print_on_shortfall=True) has no use_filing_date parameter — it always uses period-end eligibility, so call the method with use_filing_date=True (or use the callback handle) for point-in-time data. Default use_filing_date=False means period-end eligibility only (can look ahead vs filing dates). With use_filing_date=True, a period whose filing_date is missing falls back to an assumed deadline of 45 days (quarterly) / 90 days (annual) after period end. Prefer the callback fundamentals handle in IDE backtests, which uses filing-date gating automatically.

Multi-symbol fundamentals

When you load multiple bundles (for cross-sectional or comparison strategies), you can pick which one to read from with symbol="TICKER":

def on_bar(bar_index, bar, fundamentals=None, symbol=None): if fundamentals is None: return primary_snap = fundamentals.current(bar["t"]) msft_snap = fundamentals.current(bar["t"], symbol="MSFT") aapl_prior = fundamentals.get_data_with_offset(bar["t"], 1, symbol="AAPL")
  • Omitting symbol uses the primary bundle: the only one loaded; otherwise fundamentals.primary if set, else the traded symbol, else the alphabetically first loaded ticker. In a multi-symbol or Screener run that fallback can silently be the wrong company — always pass symbol=symbol there.
  • fundamentals.symbols lists every uppercase ticker key currently loaded.
  • fundamentals.primary_symbol() returns the primary key.
  • The trading symbol you’re backtesting can be different from the fundamentals symbol(s) you load — for example, trade SPY while reading fundamentals for AAPL and MSFT.

Acceptable bar times

Both current(...) and get_data_with_offset(...) accept several time formats so you can pass bar["t"], bar.t[0], or your own value:

  • datetime (naive treated as UTC),
  • date,
  • ISO string (including a trailing Z),
  • UNIX epoch seconds (int / float).

Practical patterns

A few common shapes:

def on_bar(bar_index, bar, fundamentals=None, symbol=None): if fundamentals is None: return snap = fundamentals.current(bar["t"]) if not snap: return bs_q = (snap.get("balanceSheet") or {}).get("quarterly") or {} is_q = (snap.get("incomeStatement") or {}).get("quarterly") or {} if (bs_q.get("totalAssets") or 0) > 0 and (is_q.get("netIncome") or 0) > 0: ...
def on_bar(bar_index, bar, fundamentals=None, symbol=None): if fundamentals is None: return snap = fundamentals.current(bar["t"]) or {} prior = fundamentals.get_data_with_offset(bar["t"], 1) or {} cur = ((snap.get("incomeStatement") or {}).get("quarterly") or {}).get("totalRevenue") prev = ((prior.get("incomeStatement") or {}).get("quarterly") or {}).get("totalRevenue") if cur and prev: revenue_growth = (cur - prev) / prev

Fundamentals field reference

Field availability depends on the symbol — not every issuer reports every line. Use sorted(f.lists.keys()) or sorted(t.columns.keys()) at runtime to see what a specific bundle actually exposes. Numeric values may arrive as strings; cast with float(...) for math.

Balance sheet

Tables: balanceSheet.quarterly, balanceSheet.yearly

FieldField
accountsPayablelongTermInvestments
accumulatedAmortizationnegativeGoodwill
accumulatedDepreciationnetDebt
accumulatedOtherComprehensiveIncomenetInvestedCapital
additionalPaidInCapitalnetReceivables
capitalLeaseObligationsnetTangibleAssets
capitalStocknetWorkingCapital
capitalSurplusenoncontrollingInterestInConsolidatedEntity
cashnonCurrrentAssetsOther
cashAndEquivalentsnonCurrentAssetsTotal
cashAndShortTermInvestmentsnonCurrentLiabilitiesOther
commonStocknonCurrentLiabilitiesTotal
commonStockSharesOutstandingotherAssets
commonStockTotalEquityotherCurrentAssets
currency_symbolotherCurrentLiab
currentDeferredRevenueotherLiab
dateotherStockholderEquity
deferredLongTermAssetChargespreferredStockRedeemable
deferredLongTermLiabpreferredStockTotalEquity
earningAssetspropertyPlantAndEquipmentGross
filing_datepropertyPlantAndEquipmentNet
goodWillpropertyPlantEquipment
intangibleAssetsretainedEarnings
inventoryretainedEarningsTotalEquity
liabilitiesAndStockholdersEquityshortLongTermDebt
longTermDebtshortLongTermDebtTotal
longTermDebtTotalshortTermDebt
shortTermInvestmentstemporaryEquityRedeemableNoncontrollingInterests
totalAssetstotalCurrentAssets
totalCurrentLiabilitiestotalLiab
totalPermanentEquitytotalStockholderEquity
treasuryStockwarrants

Cash flow

Tables: cashFlow.quarterly, cashFlow.yearly

FieldField
beginPeriodCashFlowendPeriodCashFlow
capitalExpendituresexchangeRateChanges
cashAndCashEquivalentsChangesfiling_date
cashFlowsOtherOperatingfreeCashFlow
changeInCashinvestments
changeInWorkingCapitalissuanceOfCapitalStock
changeReceivablesnetBorrowings
changeToAccountReceivablesnetIncome
changeToInventoryotherCashflowsFromFinancingActivities
changeToLiabilitiesotherCashflowsFromInvestingActivities
changeToNetincomeotherNonCashItems
changeToOperatingActivitiessalePurchaseOfStock
currency_symbolstockBasedCompensation
datetotalCashflowsFromInvestingActivities
depreciationtotalCashFromFinancingActivities
dividendsPaidtotalCashFromOperatingActivities

Income statement

Tables: incomeStatement.quarterly, incomeStatement.yearly

FieldField
costOfRevenuenetIncomeFromContinuingOps
currency_symbolnetInterestIncome
datenonOperatingIncomeNetOther
depreciationAndAmortizationnonRecurring
discontinuedOperationsoperatingIncome
effectOfAccountingChargesotherItems
ebitotherOperatingExpenses
ebitdapreferredStockAndOtherAdjustments
extraordinaryItemsreconciledDepreciation
filing_dateresearchDevelopment
grossProfitsellingAndMarketingExpenses
incomeBeforeTaxsellingGeneralAdministrative
incomeTaxExpensetaxProvision
interestExpensetotalOperatingExpenses
interestIncometotalOtherIncomeExpenseNet
minorityInteresttotalRevenue
netIncome
netIncomeApplicableToCommonShares

Earnings — History

Table: earnings.History (reported EPS vs. estimates per report date)

FieldDescription
beforeAfterMarketWhen the report was released relative to market hours
currencyReporting currency
datePeriod end date
epsActualReported EPS
epsEstimateConsensus EPS estimate
epsDifferenceepsActual − epsEstimate
reportDateDate the figures were reported
surprisePercentEPS surprise as a percentage

Earnings — Trend

Table: earnings.Trend (forward estimates and revisions)

FieldField
dateepsTrendCurrent
periodepsTrend7daysAgo
growthepsTrend30daysAgo
earningsEstimateAvgepsTrend60daysAgo
earningsEstimateLowepsTrend90daysAgo
earningsEstimateHighepsRevisionsUpLast7days
earningsEstimateYearAgoEpsepsRevisionsUpLast30days
earningsEstimateNumberOfAnalystsepsRevisionsDownLast7days
earningsEstimateGrowthepsRevisionsDownLast30days
revenueEstimateAvg
revenueEstimateLow
revenueEstimateHigh
revenueEstimateYearAgoEps
revenueEstimateNumberOfAnalysts
revenueEstimateGrowth

Earnings — Annual

Table: earnings.Annual (annual reported EPS)

FieldDescription
dateFiscal-year period end date
epsActualReported annual EPS

Ratios

Tables: ratios.quarterly, ratios.yearly

91 metrics since the metrics expansion (94 keys counting date, currency_symbol and filing_date). The original set is listed first; everything from Working capital down is new. A symbol mid-rollout may carry only the original set, so discover the live list with sorted(f.table("ratios.quarterly").columns.keys()).

Original set

Margin and return fields in this group (grossMargin, operatingMargin, etc.) are decimal fractions — multiply by 100 for a percentage. debtToEquity was renamed debtToEquityRatio.

FieldField
grossMargincurrentRatio
operatingMarginquickRatio
netProfitMargincashRatio
returnOnAssetsdebtToEquityRatio
returnOnEquitydebtToAssetsRatio
operatingCashFlowMargininterestCoverageRatio
freeCashFlowMarginassetTurnover
freeCashFlowToNetIncomeinventoryTurnover
receivablesTurnover

Working capital

FieldNotes
daysSalesOutstandingDays, 2dp
daysInventoryOutstandingDays, 2dp
daysPayablesOutstandingDays, 2dp
cashConversionCycleDays, 2dp — signed; negative (collect before you pay) is desirable, not an error
operatingCycleDays, 2dp
payablesTurnoverPlain decimal
workingCapitalTurnoverPlain decimal
fixedAssetTurnoverPlain decimal
netWorkingCapitalToAssetsPlain decimal
defensiveIntervalRatioPlain decimal

Leverage and coverage

All plain decimals (4dp).

FieldField
totalDebtToEquityRationetDebtToFreeCashFlowRatio
netDebtToEquityRatioequityMultiplier
longTermDebtToEquityRatioebitdaInterestCoverageRatio
longTermDebtToCapitalRatiocashFlowInterestCoverageRatio
capitalizationRatiocashFlowToDebtRatio
debtToEbitdaRatiodebtServiceCoverageRatio
netDebtToEbitdaRatio

Returns

FieldNotes
effectiveTaxRate%
returnOnInvestedCapital%
returnOnCapitalEmployed%
returnOnTangibleEquity%
cashReturnOnInvestedCapital%
operatingReturnOnAssets%
returnOnEquityDupontMargin%
returnOnEquityDupontTurnoverPlain decimal
returnOnEquityDupontLeveragePlain decimal
nopatMonetary string, 2dp — the odd one out in an otherwise all-numeric block
investedCapitalMonetary string, 2dp
capitalEmployedMonetary string, 2dp

Margins and capital intensity

FieldNotes
ebitMargin%
ebitdaMargin%
pretaxMargin%
researchAndDevelopmentToRevenue%
sellingGeneralAdministrativeToRevenue%
stockBasedCompensationToRevenue%
capexToRevenue%
capexToOperatingCashFlow%
accrualsRatio%
assetQualityRatio%
capexToDepreciationRatioPlain decimal (4dp)
cashConversionRatioPlain decimal (4dp)

Payout

All percentages (2dp): dividendPayoutRatio, freeCashFlowPayoutRatio, shareholderPayoutToFreeCashFlow, retentionRatio, sustainableGrowthRate.

Growth, year over year

All percentages (2dp). Revenue / net income / EPS growth are not duplicated here — they live in earnings.Trend.

FieldField
grossProfitGrowthYoYtotalAssetsGrowthYoY
operatingIncomeGrowthYoYtotalStockholderEquityGrowthYoY
ebitdaGrowthYoYtotalDebtGrowthYoY
operatingCashFlowGrowthYoYdividendsPaidGrowthYoY
freeCashFlowGrowthYoYsharesDilutedGrowthYoY — negative means buybacks
capitalExpendituresGrowthYoYbookValuePerShareGrowthYoY

CAGRs — yearly only

All percentages (2dp), and null on every quarterly period — a filter or rule that asks for one on a quarterly period fails every symbol.

revenueCagr3y / revenueCagr5y, netIncomeCagr3y / netIncomeCagr5y, epsCagr3y / epsCagr5y, freeCashFlowCagr3y / freeCashFlowCagr5y, dividendCagr3y / dividendCagr5y.

Financials extended

Tables: financialsExtended.quarterly, financialsExtended.yearly

Fields vary by symbol and are not enumerated here. Discover what a loaded bundle exposes at runtime:

t = f.table("financialsExtended.quarterly") if t: print(sorted(t.columns.keys()))

Common fields include extended EBITDA metrics, working capital components, and per-share data not present in the core income statement.

Retired fields: ebitda, ebitdaMargin, enterpriseValue, priceToEarnings, priceToBook, priceToSales, evToEbitda and evToRevenue are being withdrawn from Financials extended in favour of the Valuation and Ratios versions, which are computed on a trailing-twelve-month basis (so values differ).

Per share

Tables: perShare.quarterly, perShare.yearly

An as-filed record: sharesOutstanding is on whatever split basis was reported for that period, so it is not comparable across a split boundary. Use Valuation for anything comparative. Values are 4dp strings except where noted.

FieldNotes
datePeriod end date
sharesOutstanding2dp string
sharesBasis"dei" / "weightedAverageSharesDiluted" / "weightedAverageSharesBasic"
bookValuePerShare, tangibleBookValuePerShare
revenuePerShare, ebitdaPerShare
operatingCashFlowPerShare, freeCashFlowPerShare
cashPerShare, netCashPerShare
netDebtPerShare, totalDebtPerShare
workingCapitalPerShare, retainedEarningsPerShare
dividendsPaidPerShare
netCurrentAssetValuePerShareGraham NCAV
grahamNumber

Valuation

Tables: valuation.quarterly, valuation.yearly (plus the non-table valuation.latest)

Three rules:

  1. Multiples are marketCap / aggregate, not price / perShare. Don't recompute them from perShare — restated comparatives make the two disagree for older periods.
  2. A None multiple beside a signed yield is correct, not a bug. Multiples marked null on non-positive denominator below go None when their denominator isn't positive, while yields and bookToMarketRatio keep their sign. A loss-maker shows priceToEarningsRatio: None and earningsYield: -5.0.
  3. Multiples are trailing-twelve-month, even on quarterly periods — a quarterly P/E is a real trailing P/E, not one quarter annualised.

Metadata

FieldNotes
datePeriod end date
sharePrice4dp string
priceDateYYYY-MM-DD
priceLagDaysInteger
priceBasis"as-quoted" (latest) / "split-adjusted" / "unreconciled" — treat "unreconciled" with suspicion
approximatetrue on historical periods (residual dividend back-adjustment)
sharesRebasedShare count was moved onto the current split basis
sharesOutstanding2dp string, current split basis — differs from perShare.sharesOutstanding (as-filed) for older periods

Size: marketCap, enterpriseValue — 2dp monetary strings.

Multiples (4dp numbers)

FieldNull on non-positive denominator
priceToEarningsRatioyes
priceToBookRatioyes
priceToTangibleBookRatioyes
priceToSalesRatio
priceToFreeCashFlowRatioyes
priceToOperatingCashFlowRatioyes
pegRatioTrailingyes
enterpriseValueToEbitdaRatioyes
enterpriseValueToEbitRatioyes
enterpriseValueToSalesRatio
enterpriseValueToFreeCashFlowRatioyes
enterpriseValueToOperatingCashFlowRatio
enterpriseValueToInvestedCapitalRatio
bookToMarketRatiokeeps its sign
netDebtToMarketCapRatio
marketCapToTotalAssets
altmanZScore

Yields — 2dp percentages, signed: earningsYield, ebitToEnterpriseValueYield, freeCashFlowYield, operatingCashFlowYield, dividendYield, buybackYield, shareholderYield.

valuation.latest — not a period. A single object carrying every field above plus priceAsOf, priceAgeDays, priceSource, fundamentalsAsOf and ttmBasis. It sits beside the date keys, so it is not a table entry and never appears in a bar-time snapshot — reading it in a backtest would be look-ahead. Access it via qc_fundamentals.raw["valuation"]["latest"]; it is None when no price is available.

Scores

Tables: scores.quarterly, scores.yearly

FieldNotes
datePeriod end date
piotroskiFScoreInteger 0–9
piotroskiComponentsObject or None — keys roaPositive, cfoPositive, roaImproved, accrualQuality, leverageImproved, currentRatioImproved, noShareIssuance, grossMarginImproved, assetTurnoverImproved, each 1, 0 or None
altmanZScorePrivate4dp — Z-prime, book equity
beneishMScore4dp
beneishComponentsObject or None — keys dsri, gmi, aqi, sgi, depi, sgai, tata, lvgi (4dp numbers)

Two expected absences, neither of which is an error:

  • The earliest period a filer has carries no scores — they need a prior year to compare against.
  • For SIC 6000–6799 (banks, brokers, insurers, REITs) both buckets are empty and scores.notApplicableReason is "financial-sector-sic". Treat that as "not applicable", not "no data". Read it from qc_fundamentals.raw["scores"]["notApplicableReason"].

TTM

ttm is a single object, not a table — trailing-twelve-month aggregates as of the upload, not as of any bar. It never appears in a snapshot; access it via qc_fundamentals.raw["ttm"] (safe in forward runs, look-ahead in a backtest).

  • Metadata: basis ("quarterly" / "annual"), asOf, periodsUsed (list[str]), complete (bool), synthetic (bool — fiscal Q4 was reconstructed; normal, usually true), coverageDays (about 365).
  • Flows — summed over 4 quarters, 2dp strings except epsBasic / epsDiluted (4dp): totalRevenue, grossProfit, operatingIncome, ebit, ebitda, netIncome, epsBasic, epsDiluted, depreciationAndAmortization, interestExpense, incomeBeforeTax, incomeTaxExpense, costOfRevenue, sellingGeneralAdministrative, researchDevelopment, taxProvision, totalCashFromOperatingActivities, totalCashflowsFromInvestingActivities, capitalExpenditures, freeCashFlow, dividendsPaid, stockBasedCompensation, paymentsForRepurchaseOfCommonStock, proceedsFromIssuanceOfCommonStock, repaymentsOfLongTermDebt.
  • Instants — latest period, never summed, 2dp strings: totalAssets, totalLiab, totalStockholderEquity, totalCurrentAssets, totalCurrentLiabilities, totalDebt, netDebt, cashAndShortTermInvestments, goodWill, intangibleAssets, netWorkingCapital, retainedEarnings, netReceivables, inventory, accountsPayable, propertyPlantAndEquipmentNet, preferredStockTotalEquity, commonStockSharesOutstanding, minorityInterest.

Metrics provenance (metricsInputs / metricsHealth)

Flat objects describing where the metrics came from:

"metricsInputs": { "metricsVersion": 1, "priceAsOf": "2026-08-21", "priceSource": "…" | null, // source of valuation.latest "historicalPriceSource": "stooq" | null, // source of valuation.quarterly/yearly "sharesSource": "dei" | "weightedAverageSharesDiluted" | "weightedAverageSharesBasic" | null, "dividendSource": "nasdaq" | "edgar-xbrl" | "cash-flow" | null, "ttmBasis": "quarterly" | "annual" | null, "historicalValuation": true } "metricsHealth": { "prices": "ok" | "stale" | "absent", // current price for valuation.latest "historicalPrices": "ok" | "absent", "splits": "ok" | "absent", "calendar": "ok" | "absent" }

Check metricsHealth["prices"] != "absent" before relying on price-based fields; "stale" is still usable — compare against priceAsOf. metricsInputs is None means a bundle that predates the metrics expansion. Fields appear per symbol as it re-uploads, so coverage is ragged for a while: treat every block above as optional.

Type conventions for the metrics blocks

ClassJSON typeDecimalsExample
Monetary aggregatesstring2"4519282704050.00"
Per-share values, sharePricestring4"309.3500"
Share countsstring2"14608963000.00"
Ratios / multiples / turnover / coverage / scoresnumber428.0462
Percentages (margins, returns, yields, payout, growth, CAGR, tax rate)number242.35 means 42.35%
Days (DSO / DIO / DPO / CCC / operating cycle)number229.2
Counts (piotroskiFScore, priceLagDays)numberinteger8
Datesstring—YYYY-MM-DD

Every metric is independently nullable. A period object is omitted entirely when all its non-metadata keys are null. A block is {"quarterly": {}, "yearly": {}} when empty — never null, never dropped. NaN / Infinity never appear.

Company info

companyInfo is a flat object — it is not date-keyed and does not appear in current(...) / get_data_with_offset(...) snapshots. Access it directly:

info = f.raw["companyInfo"]
FieldTypeNotes
entityTypestring | nulle.g. "operating"
entityNamestring | nullLegal entity name
cikstring | nullSEC Central Index Key
sicstring | nullSIC industry code
sicDescriptionstring | nullHuman-readable SIC category
stateOfIncorporationstring | nullTwo-letter state code
fiscalYearEndstring | null"MMDD" format, e.g. "0926" = Sep 26
categorystring | nullFiler category
phonestring | nullContact phone number
exchangesstring[]Exchange codes
tickersstring[]Ticker symbols
businessAddressobject | nullstreet1, street2, city, stateOrCountry, zipCode

Notes and gotchas

  • Always guard for None. If fundamentals are not configured, fundamentals is None. Inside a snapshot, individual sections (e.g. earnings.Trend, scores) can also be null for some bars, and the metrics-expansion blocks may be missing entirely on older bundles.
  • Always pass symbol= in multi-symbol and Screener runs — the primary-bundle fallback may not be the ticker you're on.
  • Numbers may be strings. Underlying JSON often ships numeric metrics as strings; coerce with float(...) when doing math.
  • Newest-first indexing applies to date-keyed tables and to offset lookups — index 0 is the most recent eligible period as of the bar, 1 is the one before it, etc.
  • The bar’s timestamp is the source of truth for which report is "current" — pass bar["t"] (or bar.t[0]) directly; you don’t need to parse it yourself.