Fundamentals
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).
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
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.
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. The IDE’s Fundamentals documentation page lists the full set of fields you can expect across each section.
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 |
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 ide.fundamentals_module 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
In the Test (backtest) dialog, you provide:
- A fundamentals token (the QuantCraft authorization token), and
- One or more fundamentals sources:
- a single path (one symbol),
- a list of paths (several symbols), or
- a symbols map (
{"AAPL": "fundamentals/AAPL.json.gz", ...}).
For multi-bundle runs, you can also choose a primary symbol — that bundle’s snapshot is what gets merged into each OHLCV bar’s fundamentals field by default.
If fundamentals are not configured, the engine simply passes fundamentals=None to your callbacks, and you guard for it.
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_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")What the snapshot looks like
A bar-aligned snapshot is a nested dict with only the periods that are valid as of this bar. 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 }
}
}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>.
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, or the one set byfundamentals.primary). 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 |
Notes and gotchas
- Always guard for
None. If fundamentals are not configured,fundamentalsisNone. Inside a snapshot, individual sections (e.g.earnings.Trend) can also benullfor some bars. - 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.