Calendar Module
The calendar module gives your strategy a company's dividend history, split history, earnings dates, IPO date, and recent SEC filings — and lets the backtest apply splits and dividends to your positions automatically.
Enable Include calendar data on the Symbol & data tab of the Run backtest modal. It is on by default, and your choice (plus the two sub-options) is remembered for the next run. Calendars load automatically for every symbol the run trades, including a screener-resolved universe — there is no per-ticker picker. An agent's Test modal has the same option — Include calendar data (dividends, splits) (see Agents).
Backtests only. Calendar data is loaded for backtests, Debug Test and Agent tests. Forward runs — chart forward tests, bulk runs in AutoTrading, and QuantCloud — never get a calendar, so calendar is always None there.
Why it matters: price bars are raw
Backtest candles are unadjusted. Without calendar data:
- A 4-for-1 split looks like a 75 % overnight price collapse, and your simulated position collapses with it.
- Dividends never arrive, which quietly understates the total return of any long-held position — roughly 2 %/year for a typical dividend payer.
The two sub-options fix both without requiring any code changes.
The calendar callback parameter
Add a calendar parameter to any callback and the handle is passed in:
def on_bar(bar_index, bar, calendar=None, symbol=None):
if calendar is None: # always guard — not every run enables calendar data
return
days = calendar.days_to_earnings(bar["t"], symbol)
if days is not None and days <= 2:
return # sit out the earnings print
divs = calendar.dividends(bar["t"], symbol)
if divs and (divs[-1].amount or 0) > 0.5: # amount can be None
...Works on on_bar, on_tick, and on_timer. The calendar argument is matched by name, so a script with no calendar parameter behaves exactly as before, and a declared calendar receives either the handle or None — never another object, whatever else your signature takes. If you also read fundamentals, the recommended signature is def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None).
You can also reach the same handle without a parameter: the script global qc_calendar, or quantcraft.process.runtime.qc_calendar.
Methods
| Method | Returns |
|---|---|
dividends(bar_time, symbol=None) | Dividends public at that bar |
splits(bar_time, symbol=None) | Splits announced by that bar |
filings(bar_time, symbol=None) | SEC filings public at that bar |
next_earnings(bar_time, symbol=None) | {date, basis, estimated, …} or None |
days_to_earnings(bar_time, symbol=None) | int or None |
ipo_date(symbol=None) | date or None |
current(bar_time, symbol=None) | A snapshot dict of everything above at that bar, or None |
dividends_between(start, end, symbol=None) | Dividends whose ex-date falls in (start, end] |
splits_between(start, end, symbol=None) | Splits whose effective date falls in (start, end] |
has(symbol=None) | Whether this symbol has calendar data at all |
symbols / primary_symbol() / get(symbol=None) | The loaded symbols, the default one, and the raw per-symbol calendar (or None) |
The _between methods use the effect dates ("when does it hit my position?"), unlike the bar-time methods above, which use the "could I have known?" dates — see the next section.
bar_time accepts bar["t"], bar.t[0], a datetime, a date, or epoch seconds.
Lists are oldest-first — dividends(...)[-1] is the most recent. This is the opposite of fundamentals, where index 0 is the newest period.
A symbol with no calendar data returns [] or None, never an error. Most tickers outside the large caps have no calendar on the server, so guard on emptiness rather than exceptions.
Two dates per event — use the right one
Every dividend and split carries two dates, and picking the wrong one introduces look-ahead:
| "Could I have known?" | "When does it affect my position?" | |
|---|---|---|
| Dividend | declaration_date | ex_date |
| Split | filing_date | effective_date |
dividends() / splits() / filings() filter on the first column — they return only what was public at your bar. A split is often visible weeks before it takes effect, because the 8-K is filed in advance:
from datetime import date
bar_date = date.fromisoformat(str(bar["t"])[:10])
splits = calendar.splits(bar["t"], symbol)
upcoming = [s for s in splits
if s.effective_date and s.effective_date > bar_date] # announced, not yet effectiveEvent fields
DividendEvent: ex_date, amount, estimated, type, declaration_date,
record_date, payment_date, period_end
SplitEvent: effective_date, ratio, type ("forward" | "reverse"), filing_date, form
FilingEvent: form, filing_date, report_date, items, category,
accession_number, primary_document, primary_document_description
IpoInfo: date, first_trade_date, sourceamount (dividends) and effective_date (splits) can be None — guard before comparing.
A filing's category is one of earnings, acquisition, officer-director, charter-amendment, agreement, regulation-fd, auditor-change, shareholder-vote, cybersecurity, other.
The estimated flag
EDGAR does not publish ex-dividend dates. Unless exchange data was available, the ex-date is estimated as the declaration date + 10 days and is accurate to about ±7 days, with estimated is True. By default the simulation still credits estimated dividends (policy best_available); a run preset (.qcs) can set dividendDatePolicy to exact_only (skip estimated ones) or off — there is no modal control for this. Filter on it in your own logic if your strategy is ex-date sensitive:
exact = [d for d in calendar.dividends(bar["t"], symbol) if not d.estimated]Corporate actions in the simulation
Both sub-options are on by default when calendar data is enabled, and both run before your callbacks each bar — so on_bar always sees an already-adjusted position.
Adjust positions for stock splits — on the effective date, share count is multiplied by the ratio and cost basis divided by it. Stop and target prices are rescaled too. Because qty × entry_price is unchanged, your equity curve stays continuous across the split.
Price bars deliberately stay unadjusted, so bar["close"] is the real historical price. An indicator computed over the raw close series will still see the split gap — adjust your own series using calendar.splits(...) if that matters.
Credit dividends to cash — on the ex-date, qty × amount is added to cash for longs and subtracted for shorts (a short seller owes the dividend to the lender). This flows into equity and every return metric, but not into realized_pnl, which stays trade-only. Read the totals from account metrics:
def on_finish(bars, symbol=None, bars_by_symbol=None):
from quantcraft.process.runtime import account
m = account.refresh_metrics({})
print(m["total_dividend_income"], m["total_splits_applied"])Where you see corporate actions
Nothing changes your position or cash silently:
- Output panel — one
[calendar]line per corporate action applied, plus a summary at the end:[calendar] 2024-01-10 AAPL — SPLIT 4:1 applied to 1 position(s): 100 -> 400 shares (cost basis unchanged; bars stay unadjusted) [calendar] 2024-02-09 AAPL — DIVIDEND $0.2400/share on 400 share(s): +$96.00 cash … [calendar] corporate actions — 1 split(s) applied; 1 dividend(s) applied (0 exact ex-date, 1 estimated +/-7d) - Test Results — a Split or Dividend row appears inline among the closed trades (the card is then titled Closed trades & events), showing the ratio or per-share amount and cash impact. An estimated ex-date is marked
est.These rows carry no#chart marker and are excluded from the CSV export.
next_earnings is point-in-time
In a backtest, next_earnings never returns the server's "next earnings date" computed as of today — that would be look-ahead at a historical bar. It is rebuilt from the last earnings filing visible at your bar plus the company's reporting cadence, so basis == "filing-cadence" and estimated is True.
It returns None when no earnings filing is visible yet — early in a run, or for a symbol whose filing history does not reach back that far. Always guard for it.
When you see no dividends
A run that applies nothing says why in the Output panel:
| Log says | Meaning |
|---|---|
loaded 3/5 symbol(s); 2 without calendar data | Printed at the start: how many symbols got a calendar |
none of 5 symbol(s) returned calendar data: … | No symbol in the run has calendar data — nothing can apply |
no calendar data on the server | The calendar job has not run for that symbol |
calendar has no dividend or split history | Non-payer, no recorded splits |
… ex-dates 2019-05-10..2020-05-08 | It pays, but not inside your tested date range |
N with no ex-date, which can never be applied | History exists but cannot be placed on the clock |
no positions were held … | Nothing was open when an action came due |
If no [calendar] lines appear at all, calendar data was not attached to the run — check the Include calendar data toggle on the Symbol & data tab.
See also
- Backtests — the Run backtest modal and the Include calendar data option
- Strategy lifecycle —
calendarcallback parameter,qc_calendarruntime global - Account model — where dividend cash and split adjustments land
- Screener — how calendars load when a screener resolves the ticker list
- Test Results — Split / Dividend rows in the closed-trades table
- Output panel —
[calendar]log lines
