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). Factorrowsare keyed byYYYYMMstrings, not list indices.
What you get
Each fundamentals bundle is a single JSON object for one ticker, containing:
- Balance sheet —
quarterlyandyearlyreports. - Cash flow —
quarterlyandyearlyreports. - Income statement —
quarterlyandyearlyreports. - Earnings:
- History — reported EPS vs. estimates and surprises by report date.
- Trend — forward estimates, EPS trend, and analyst revisions.
- Annual —
epsActualper fiscal year.
- Ratios —
quarterlyandyearlyfinancial ratios (margins, returns, liquidity, leverage). - Financials extended —
quarterlyandyearlyextended metrics not present in the core income, balance-sheet, or cash-flow statements. - Per share —
quarterlyandyearlyper-share values as filed (share count on that period's split basis). - Valuation —
quarterlyandyearlymarket cap, enterprise value, multiples and yields, plus a non-datelatestobject. - Scores —
quarterlyandyearlyPiotroski F, Altman Z-prime and Beneish M, plusnotApplicableReason. - TTM — a single trailing-twelve-month object (not bucketed by period).
- Metrics provenance — flat
metricsInputs/metricsHealthobjects.metricsInputsisNoneon a bundle that predates the metrics expansion. - Filing history —
quarterlyandyearlyfiling metadata (form type, filed date, keyed by period end). Also included in bar-alignedcurrent(...)snapshots when present in the bundle; raw tables remain available viaf.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:
| Path | Meaning |
|---|---|
balanceSheet.quarterly / balanceSheet.yearly | Balance sheet snapshots |
cashFlow.quarterly / cashFlow.yearly | Cash flow statements |
incomeStatement.quarterly / incomeStatement.yearly | Income statements |
earnings.History | Reported EPS, estimates, surprise per report |
earnings.Trend | Analyst estimates, EPS trend, revisions |
earnings.Annual | Annual epsActual |
ratios.quarterly / ratios.yearly | Financial ratios (margins, returns, liquidity, leverage) |
financialsExtended.quarterly / financialsExtended.yearly | Extended financial metrics |
perShare.quarterly / perShare.yearly | Per-share values (as filed) |
valuation.quarterly / valuation.yearly | Valuation multiples and yields |
scores.quarterly / scores.yearly | Piotroski / Altman / Beneish |
filingHistory.quarterly / filingHistory.yearly | Filing 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 table | Why |
|---|---|
valuation.latest | A single object beside the date keys, not a period |
scores.notApplicableReason | A string beside the date keys |
ttm | A single object; values are scalars rather than row dicts |
metricsInputs / metricsHealth | Flat provenance objects |
companyInfo | Flat 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.rawWhat you can read off a loaded bundle:
| Attribute | What it gives you |
|---|---|
f.raw | The full decoded JSON tree. |
f.symbol | Resolved ticker (e.g. "AAPL"). |
f.tables | Dict of DateKeyedTable objects keyed by dotted path (e.g. "balanceSheet.quarterly"). |
f.table(path) | Convenience accessor for a single DateKeyedTable. |
f.lists | Flat 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, andsorted(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:
- 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.
- 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_fundamentalsargument or a raw dict instead ofBacktestFundamentals. The modern signature passesfundamentalsas the third argument toon_bar/on_tick. The module-level aliasfundamentals_as_of_barmaps 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 / member | Purpose |
|---|---|
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.symbols | Tuple 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, thebalanceSheet.quarterlysection steps back one quarter, whilebalanceSheet.yearlysteps back one fiscal year, and so on. - If a sub-table doesn’t have enough history before the current bar, its section is
nullin 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
QcFundamentalsAPI, the preferred signature isf.get_data_with_offset(bar_time, offset, *, print_on_shortfall=True, use_filing_date=False). The module-level functionget_data_with_offset(qc, bar_time, offset, *, print_on_shortfall=True)has nouse_filing_dateparameter — it always uses period-end eligibility, so call the method withuse_filing_date=True(or use the callback handle) for point-in-time data. Defaultuse_filing_date=Falsemeans period-end eligibility only (can look ahead vs filing dates). Withuse_filing_date=True, a period whosefiling_dateis missing falls back to an assumed deadline of 45 days (quarterly) / 90 days (annual) after period end. Prefer the callbackfundamentalshandle 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
symboluses the primary bundle: the only one loaded; otherwisefundamentals.primaryif 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 passsymbol=symbolthere. fundamentals.symbolslists 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
SPYwhile reading fundamentals forAAPLandMSFT.
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) / prevFundamentals field reference
Field availability depends on the symbol — not every issuer reports every line. Use
sorted(f.lists.keys())orsorted(t.columns.keys())at runtime to see what a specific bundle actually exposes. Numeric values may arrive as strings; cast withfloat(...)for math.
Balance sheet
Tables: balanceSheet.quarterly, balanceSheet.yearly
| Field | Field |
|---|---|
accountsPayable | longTermInvestments |
accumulatedAmortization | negativeGoodwill |
accumulatedDepreciation | netDebt |
accumulatedOtherComprehensiveIncome | netInvestedCapital |
additionalPaidInCapital | netReceivables |
capitalLeaseObligations | netTangibleAssets |
capitalStock | netWorkingCapital |
capitalSurpluse | noncontrollingInterestInConsolidatedEntity |
cash | nonCurrrentAssetsOther |
cashAndEquivalents | nonCurrentAssetsTotal |
cashAndShortTermInvestments | nonCurrentLiabilitiesOther |
commonStock | nonCurrentLiabilitiesTotal |
commonStockSharesOutstanding | otherAssets |
commonStockTotalEquity | otherCurrentAssets |
currency_symbol | otherCurrentLiab |
currentDeferredRevenue | otherLiab |
date | otherStockholderEquity |
deferredLongTermAssetCharges | preferredStockRedeemable |
deferredLongTermLiab | preferredStockTotalEquity |
earningAssets | propertyPlantAndEquipmentGross |
filing_date | propertyPlantAndEquipmentNet |
goodWill | propertyPlantEquipment |
intangibleAssets | retainedEarnings |
inventory | retainedEarningsTotalEquity |
liabilitiesAndStockholdersEquity | shortLongTermDebt |
longTermDebt | shortLongTermDebtTotal |
longTermDebtTotal | shortTermDebt |
shortTermInvestments | temporaryEquityRedeemableNoncontrollingInterests |
totalAssets | totalCurrentAssets |
totalCurrentLiabilities | totalLiab |
totalPermanentEquity | totalStockholderEquity |
treasuryStock | warrants |
Cash flow
Tables: cashFlow.quarterly, cashFlow.yearly
| Field | Field |
|---|---|
beginPeriodCashFlow | endPeriodCashFlow |
capitalExpenditures | exchangeRateChanges |
cashAndCashEquivalentsChanges | filing_date |
cashFlowsOtherOperating | freeCashFlow |
changeInCash | investments |
changeInWorkingCapital | issuanceOfCapitalStock |
changeReceivables | netBorrowings |
changeToAccountReceivables | netIncome |
changeToInventory | otherCashflowsFromFinancingActivities |
changeToLiabilities | otherCashflowsFromInvestingActivities |
changeToNetincome | otherNonCashItems |
changeToOperatingActivities | salePurchaseOfStock |
currency_symbol | stockBasedCompensation |
date | totalCashflowsFromInvestingActivities |
depreciation | totalCashFromFinancingActivities |
dividendsPaid | totalCashFromOperatingActivities |
Income statement
Tables: incomeStatement.quarterly, incomeStatement.yearly
| Field | Field |
|---|---|
costOfRevenue | netIncomeFromContinuingOps |
currency_symbol | netInterestIncome |
date | nonOperatingIncomeNetOther |
depreciationAndAmortization | nonRecurring |
discontinuedOperations | operatingIncome |
effectOfAccountingCharges | otherItems |
ebit | otherOperatingExpenses |
ebitda | preferredStockAndOtherAdjustments |
extraordinaryItems | reconciledDepreciation |
filing_date | researchDevelopment |
grossProfit | sellingAndMarketingExpenses |
incomeBeforeTax | sellingGeneralAdministrative |
incomeTaxExpense | taxProvision |
interestExpense | totalOperatingExpenses |
interestIncome | totalOtherIncomeExpenseNet |
minorityInterest | totalRevenue |
netIncome | |
netIncomeApplicableToCommonShares |
Earnings — History
Table: earnings.History (reported EPS vs. estimates per report date)
| Field | Description |
|---|---|
beforeAfterMarket | When the report was released relative to market hours |
currency | Reporting currency |
date | Period end date |
epsActual | Reported EPS |
epsEstimate | Consensus EPS estimate |
epsDifference | epsActual − epsEstimate |
reportDate | Date the figures were reported |
surprisePercent | EPS surprise as a percentage |
Earnings — Trend
Table: earnings.Trend (forward estimates and revisions)
| Field | Field |
|---|---|
date | epsTrendCurrent |
period | epsTrend7daysAgo |
growth | epsTrend30daysAgo |
earningsEstimateAvg | epsTrend60daysAgo |
earningsEstimateLow | epsTrend90daysAgo |
earningsEstimateHigh | epsRevisionsUpLast7days |
earningsEstimateYearAgoEps | epsRevisionsUpLast30days |
earningsEstimateNumberOfAnalysts | epsRevisionsDownLast7days |
earningsEstimateGrowth | epsRevisionsDownLast30days |
revenueEstimateAvg | |
revenueEstimateLow | |
revenueEstimateHigh | |
revenueEstimateYearAgoEps | |
revenueEstimateNumberOfAnalysts | |
revenueEstimateGrowth |
Earnings — Annual
Table: earnings.Annual (annual reported EPS)
| Field | Description |
|---|---|
date | Fiscal-year period end date |
epsActual | Reported 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.debtToEquitywas renameddebtToEquityRatio.
| Field | Field |
|---|---|
grossMargin | currentRatio |
operatingMargin | quickRatio |
netProfitMargin | cashRatio |
returnOnAssets | debtToEquityRatio |
returnOnEquity | debtToAssetsRatio |
operatingCashFlowMargin | interestCoverageRatio |
freeCashFlowMargin | assetTurnover |
freeCashFlowToNetIncome | inventoryTurnover |
receivablesTurnover |
Working capital
| Field | Notes |
|---|---|
daysSalesOutstanding | Days, 2dp |
daysInventoryOutstanding | Days, 2dp |
daysPayablesOutstanding | Days, 2dp |
cashConversionCycle | Days, 2dp — signed; negative (collect before you pay) is desirable, not an error |
operatingCycle | Days, 2dp |
payablesTurnover | Plain decimal |
workingCapitalTurnover | Plain decimal |
fixedAssetTurnover | Plain decimal |
netWorkingCapitalToAssets | Plain decimal |
defensiveIntervalRatio | Plain decimal |
Leverage and coverage
All plain decimals (4dp).
| Field | Field |
|---|---|
totalDebtToEquityRatio | netDebtToFreeCashFlowRatio |
netDebtToEquityRatio | equityMultiplier |
longTermDebtToEquityRatio | ebitdaInterestCoverageRatio |
longTermDebtToCapitalRatio | cashFlowInterestCoverageRatio |
capitalizationRatio | cashFlowToDebtRatio |
debtToEbitdaRatio | debtServiceCoverageRatio |
netDebtToEbitdaRatio |
Returns
| Field | Notes |
|---|---|
effectiveTaxRate | % |
returnOnInvestedCapital | % |
returnOnCapitalEmployed | % |
returnOnTangibleEquity | % |
cashReturnOnInvestedCapital | % |
operatingReturnOnAssets | % |
returnOnEquityDupontMargin | % |
returnOnEquityDupontTurnover | Plain decimal |
returnOnEquityDupontLeverage | Plain decimal |
nopat | Monetary string, 2dp — the odd one out in an otherwise all-numeric block |
investedCapital | Monetary string, 2dp |
capitalEmployed | Monetary string, 2dp |
Margins and capital intensity
| Field | Notes |
|---|---|
ebitMargin | % |
ebitdaMargin | % |
pretaxMargin | % |
researchAndDevelopmentToRevenue | % |
sellingGeneralAdministrativeToRevenue | % |
stockBasedCompensationToRevenue | % |
capexToRevenue | % |
capexToOperatingCashFlow | % |
accrualsRatio | % |
assetQualityRatio | % |
capexToDepreciationRatio | Plain decimal (4dp) |
cashConversionRatio | Plain 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.
| Field | Field |
|---|---|
grossProfitGrowthYoY | totalAssetsGrowthYoY |
operatingIncomeGrowthYoY | totalStockholderEquityGrowthYoY |
ebitdaGrowthYoY | totalDebtGrowthYoY |
operatingCashFlowGrowthYoY | dividendsPaidGrowthYoY |
freeCashFlowGrowthYoY | sharesDilutedGrowthYoY — negative means buybacks |
capitalExpendituresGrowthYoY | bookValuePerShareGrowthYoY |
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,evToEbitdaandevToRevenueare 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.
| Field | Notes |
|---|---|
date | Period end date |
sharesOutstanding | 2dp string |
sharesBasis | "dei" / "weightedAverageSharesDiluted" / "weightedAverageSharesBasic" |
bookValuePerShare, tangibleBookValuePerShare | |
revenuePerShare, ebitdaPerShare | |
operatingCashFlowPerShare, freeCashFlowPerShare | |
cashPerShare, netCashPerShare | |
netDebtPerShare, totalDebtPerShare | |
workingCapitalPerShare, retainedEarningsPerShare | |
dividendsPaidPerShare | |
netCurrentAssetValuePerShare | Graham NCAV |
grahamNumber |
Valuation
Tables: valuation.quarterly, valuation.yearly (plus the non-table valuation.latest)
Three rules:
- Multiples are
marketCap / aggregate, notprice / perShare. Don't recompute them fromperShare— restated comparatives make the two disagree for older periods. - A
Nonemultiple beside a signed yield is correct, not a bug. Multiples marked null on non-positive denominator below goNonewhen their denominator isn't positive, while yields andbookToMarketRatiokeep their sign. A loss-maker showspriceToEarningsRatio: NoneandearningsYield: -5.0. - Multiples are trailing-twelve-month, even on quarterly periods — a quarterly P/E is a real trailing P/E, not one quarter annualised.
Metadata
| Field | Notes |
|---|---|
date | Period end date |
sharePrice | 4dp string |
priceDate | YYYY-MM-DD |
priceLagDays | Integer |
priceBasis | "as-quoted" (latest) / "split-adjusted" / "unreconciled" — treat "unreconciled" with suspicion |
approximate | true on historical periods (residual dividend back-adjustment) |
sharesRebased | Share count was moved onto the current split basis |
sharesOutstanding | 2dp string, current split basis — differs from perShare.sharesOutstanding (as-filed) for older periods |
Size: marketCap, enterpriseValue — 2dp monetary strings.
Multiples (4dp numbers)
| Field | Null on non-positive denominator |
|---|---|
priceToEarningsRatio | yes |
priceToBookRatio | yes |
priceToTangibleBookRatio | yes |
priceToSalesRatio | |
priceToFreeCashFlowRatio | yes |
priceToOperatingCashFlowRatio | yes |
pegRatioTrailing | yes |
enterpriseValueToEbitdaRatio | yes |
enterpriseValueToEbitRatio | yes |
enterpriseValueToSalesRatio | |
enterpriseValueToFreeCashFlowRatio | yes |
enterpriseValueToOperatingCashFlowRatio | |
enterpriseValueToInvestedCapitalRatio | |
bookToMarketRatio | keeps 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
| Field | Notes |
|---|---|
date | Period end date |
piotroskiFScore | Integer 0–9 |
piotroskiComponents | Object or None — keys roaPositive, cfoPositive, roaImproved, accrualQuality, leverageImproved, currentRatioImproved, noShareIssuance, grossMarginImproved, assetTurnoverImproved, each 1, 0 or None |
altmanZScorePrivate | 4dp — Z-prime, book equity |
beneishMScore | 4dp |
beneishComponents | Object 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.notApplicableReasonis"financial-sector-sic". Treat that as "not applicable", not "no data". Read it fromqc_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, usuallytrue),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
| Class | JSON type | Decimals | Example |
|---|---|---|---|
| Monetary aggregates | string | 2 | "4519282704050.00" |
Per-share values, sharePrice | string | 4 | "309.3500" |
| Share counts | string | 2 | "14608963000.00" |
| Ratios / multiples / turnover / coverage / scores | number | 4 | 28.0462 |
| Percentages (margins, returns, yields, payout, growth, CAGR, tax rate) | number | 2 | 42.35 means 42.35% |
| Days (DSO / DIO / DPO / CCC / operating cycle) | number | 2 | 29.2 |
Counts (piotroskiFScore, priceLagDays) | number | integer | 8 |
| Dates | string | — | 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"]| Field | Type | Notes |
|---|---|---|
entityType | string | null | e.g. "operating" |
entityName | string | null | Legal entity name |
cik | string | null | SEC Central Index Key |
sic | string | null | SIC industry code |
sicDescription | string | null | Human-readable SIC category |
stateOfIncorporation | string | null | Two-letter state code |
fiscalYearEnd | string | null | "MMDD" format, e.g. "0926" = Sep 26 |
category | string | null | Filer category |
phone | string | null | Contact phone number |
exchanges | string[] | Exchange codes |
tickers | string[] | Ticker symbols |
businessAddress | object | null | street1, street2, city, stateOrCountry, zipCode |
Notes and gotchas
- Always guard for
None. If fundamentals are not configured,fundamentalsisNone. Inside a snapshot, individual sections (e.g.earnings.Trend,scores) can also benullfor 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
0is the most recent eligible period as of the bar,1is the one before it, etc. - The bar’s timestamp is the source of truth for which report is "current" — pass
bar["t"](orbar.t[0]) directly; you don’t need to parse it yourself.
