# Trading calendars
> Say which days a market trades - its weekdays, how they changed over the years, and its holidays - and ask it in SQL and in timers.
- Category: Time-series
- Keywords: missing days, gaps, data gaps, missing bars, missing\_bars, calendar, calendars, trading calendar, trading day, trading days, market days, business days, working days, holiday, holidays, era, eras, trading week, sun-thu, mon-fri, week change, NEPSE, CREATE CALENDAR, ALTER CALENDAR, DROP CALENDAR, ADD HOLIDAY, ADD TRADING DAY, is\_trading\_day, next\_trading\_day, prev\_trading\_day, trading\_days\_between, suggest\_calendar\_eras, kdb\_calendars, trading nepse 15:05
- See also: [Date and time functions](functions-date-time.md), [Timers](timers.md), [Time and time zones](time-and-time-zones.md), [SAMPLE BY](sample-by.md)
A **trading calendar** says which days a market trades. It lives in one database, next to its tables, and SQL functions and [timers](timers.md) read it.
A market's week can change. NEPSE traded **Sunday to Thursday** until 2026-04-05, then **Monday to Friday** from 2026-04-10. So a calendar is a list of **eras**: each era says from which day on which weekdays trade. On top of the eras come single days:
| Part | Example | Meaning |
|---|---|---|
| era | `ERA FROM '2022-01-01' DAYS sun-thu` | from this day on, these weekdays trade (until the next era) |
| holiday | `HOLIDAY '2026-10-20' 'Dashain'` | no trading on a day the era trades |
| extra trading day | `TRADING DAY '2022-01-07' 'Friday session'` | a session on a day the era does not trade |
| time zone | `TIME ZONE 'Asia/Kathmandu'` | which clock a `TIMESTAMP` is read on (UTC when not given) |
- A day before the first era is not a trading day (unless it is an extra trading day).
- Weekdays: `sun mon tue wed thu fri sat` (or the full names). A range may go past Saturday: `fri-sun`. A list: `mon,wed,fri`.
- Dates are written `'yyyy-mm-dd'`.
## CREATE CALENDAR
Syntax:
```sql
CREATE CALENDAR [IF NOT EXISTS] name (
[TIME ZONE 'zone',]
ERA FROM 'date' DAYS weekdays [, ERA FROM 'date' DAYS weekdays ...]
[, HOLIDAY 'date' ['note'] ...]
[, TRADING DAY 'date' ['note'] ...]
)
```
- A calendar needs at least one era. Two eras cannot start on the same day.
- `TIME ZONE` takes any zone: `'Asia/Kathmandu'`, `'UTC'`, `'+05:45'`, or one with summer time such as `'America/New_York'` (a moment's day is on the offset in force then; [summer time](time-and-time-zones.md#summer-time)).
- **Role:** `ddl`, like `CREATE TYPE`. It is saved at once, so it cannot go inside `BEGIN` ... `COMMIT`.
**Example: NEPSE: Sunday to Thursday, then Monday to Friday**
```sql
CREATE CALENDAR nepse (
TIME ZONE 'Asia/Kathmandu',
ERA FROM '2022-01-01' DAYS sun-thu,
ERA FROM '2026-04-10' DAYS mon-fri
);
SELECT is_trading_day('nepse', '2026-04-05') AS sun_before,
is_trading_day('nepse', '2026-04-10') AS fri_after,
is_trading_day('nepse', '2026-04-12') AS sun_after;
```
Result:
`CREATE CALENDAR nepse (TIME ZONE 'Asia/Kathmandu', ERA FROM '2022-01-01' DAYS 'sun-thu', ERA FROM '2`
```text
calendar 'nepse' created: 2 era(s), 0 holiday(s), 0 extra trading day(s)
```
`SELECT is_trading_day('nepse', '2026-04-05') AS sun_before, is_trading_day('nepse', '2026-04-10') AS`
| sun_before (BOOL) | fri_after (BOOL) | sun_after (BOOL) |
|---|---|---|
| true | true | false |
(1 row)
## ALTER CALENDAR
Syntax:
```sql
ALTER CALENDAR [IF EXISTS] name ADD HOLIDAY 'date' ['note'] [, 'date' ['note'] ...]
ALTER CALENDAR [IF EXISTS] name DROP HOLIDAY 'date' [, 'date' ...]
ALTER CALENDAR [IF EXISTS] name ADD TRADING DAY 'date' ['note'] [, ...]
ALTER CALENDAR [IF EXISTS] name DROP TRADING DAY 'date' [, ...]
ALTER CALENDAR [IF EXISTS] name ADD ERA FROM 'date' DAYS weekdays
ALTER CALENDAR [IF EXISTS] name DROP ERA FROM 'date'
ALTER CALENDAR [IF EXISTS] name SET TIME ZONE 'zone'
```
| Rule | What happens |
|---|---|
| A holiday that is there already | its note is replaced |
| A day that is a holiday and is added as a trading day (or the other way) | refused: drop it first |
| A holiday on a day its era does not trade anyway (a Saturday) | kept, and the answer says so |
| `DROP HOLIDAY` of a day that is not a holiday | refused |
| `DROP ERA` of the last era | refused: a calendar needs one era |
| A day listed twice in one statement | refused |
The next query sees the change (also a [cached](performance-tips.md) result).
**Example: Holidays, one statement for many**
```sql
CREATE CALENDAR nepse (ERA FROM '2022-01-01' DAYS sun-thu, ERA FROM '2026-04-10' DAYS mon-fri);
ALTER CALENDAR nepse ADD HOLIDAY '2026-10-20' 'Dashain', '2026-10-21' 'Dashain', '2026-10-22' 'Dashain';
SELECT is_trading_day('nepse', '2026-10-21') AS holiday,
next_trading_day('nepse', '2026-10-19') AS after_dashain;
```
Result:
`CREATE CALENDAR nepse (ERA FROM '2022-01-01' DAYS 'sun-thu', ERA FROM '2026-04-10' DAYS 'mon-fri')`
```text
calendar 'nepse' created: 2 era(s), 0 holiday(s), 0 extra trading day(s)
```
`ALTER CALENDAR nepse ADD HOLIDAY '2026-10-20' 'Dashain', '2026-10-21' 'Dashain', '2026-10-22' 'Dasha`
```text
calendar 'nepse' altered: 3 holiday(s) added
```
`SELECT is_trading_day('nepse', '2026-10-21') AS holiday, next_trading_day('nepse', '2026-10-19') AS `
| holiday (BOOL) | after_dashain (DATE) |
|---|---|
| false | 2026-10-23 |
(1 row)
**Example: An extra Friday session in the Sunday-Thursday years**
```sql
CREATE CALENDAR nepse (ERA FROM '2022-01-01' DAYS sun-thu, ERA FROM '2026-04-10' DAYS mon-fri);
ALTER CALENDAR nepse ADD TRADING DAY '2022-01-07' 'Friday session';
SELECT is_trading_day('nepse', '2022-01-07') AS extra, is_trading_day('nepse', '2022-01-14') AS plain_friday;
```
Result:
`CREATE CALENDAR nepse (ERA FROM '2022-01-01' DAYS 'sun-thu', ERA FROM '2026-04-10' DAYS 'mon-fri')`
```text
calendar 'nepse' created: 2 era(s), 0 holiday(s), 0 extra trading day(s)
```
`ALTER CALENDAR nepse ADD TRADING DAY '2022-01-07' 'Friday session'`
```text
calendar 'nepse' altered: 1 extra trading day(s) added
```
`SELECT is_trading_day('nepse', '2022-01-07') AS extra, is_trading_day('nepse', '2022-01-14') AS plai`
| extra (BOOL) | plain_friday (BOOL) |
|---|---|
| true | false |
(1 row)
**Example: A holiday cannot also be a trading day** (shows an error)
Setup:
```sql
CREATE CALENDAR nepse (ERA FROM '2022-01-01' DAYS sun-thu);
ALTER CALENDAR nepse ADD HOLIDAY '2026-10-20';
```
```sql
ALTER CALENDAR nepse ADD TRADING DAY '2026-10-20';
```
Result:
Error: `'2026-10-20' is a holiday of calendar nepse; DROP HOLIDAY '2026-10-20' first`
## DROP CALENDAR
Syntax:
```sql
DROP CALENDAR [IF EXISTS] name [, name ...]
```
A timer with a `trading` schedule on a dropped calendar fails every day and says why, until you make the calendar again or change the schedule.
## The functions
The calendar is named as text. It may come first or last: `is_trading_day('nepse', ts)` and `is_trading_day(ts, 'nepse')` are the same.
| Function | Returns | Meaning |
|---|---|---|
| `is_trading_day(cal, d)` | `BOOL` | `d` is a trading day |
| `next_trading_day(cal, d)` | `DATE` | the first trading day after `d` |
| `prev_trading_day(cal, d)` | `DATE` | the last trading day before `d`; `NULL` before the first one |
| `trading_days_between(cal, from, to)` | `LONG` | trading days `d` with `from <= d < to`; negative when `to` is before `from` (the rule of NumPy's `busday_count`) |
- **Which day:** a `DATE` or `'2026-04-10'` is that day. A `TIMESTAMP` is the day it falls on in the calendar's time zone: `2026-04-10 20:00 UTC` is Saturday 01:45 in Kathmandu.
- **NULL:** any `NULL` argument gives `NULL`.
- **Errors:** a calendar that does not exist is an error, never a quiet `false`.
- `is_trading_day(t)` with **one** argument is the old fixed Sunday-to-Thursday week on the UTC date, with no holidays ([date functions](functions-date-time.md#is_trading_day)).
- **Speed:** the calendar is looked up once per query and thread; each row is a few comparisons. `trading_days_between` counts whole weeks at once, so a range of ten years costs no more than a week.
**Example: Around the week change**
```sql
CREATE CALENDAR nepse (ERA FROM '2022-01-01' DAYS sun-thu, ERA FROM '2026-04-10' DAYS mon-fri);
SELECT next_trading_day('nepse', '2026-04-02') AS after_thu,
next_trading_day('nepse', '2026-04-10') AS after_fri,
prev_trading_day('nepse', '2026-04-13') AS before_mon,
trading_days_between('nepse', '2026-04-01', '2026-04-15') AS days_1_to_14;
```
Result:
`CREATE CALENDAR nepse (ERA FROM '2022-01-01' DAYS 'sun-thu', ERA FROM '2026-04-10' DAYS 'mon-fri')`
```text
calendar 'nepse' created: 2 era(s), 0 holiday(s), 0 extra trading day(s)
```
`SELECT next_trading_day('nepse', '2026-04-02') AS after_thu, next_trading_day('nepse', '2026-04-10')`
| after_thu (DATE) | after_fri (DATE) | before_mon (DATE) | days_1_to_14 (LONG) |
|---|---|---|---|
| 2026-04-05 | 2026-04-13 | 2026-04-10 | 10 |
(1 row)
**Example: Rows that fall on a day the market was closed**
```sql
CREATE CALENDAR nepse (TIME ZONE 'Asia/Kathmandu', ERA FROM '2022-01-01' DAYS sun-thu, ERA FROM '2026-04-10' DAYS mon-fri);
CREATE TABLE ticks (ts TIMESTAMP, symbol SYMBOL, price DOUBLE) TIMESTAMP(ts) PARTITION BY DAY;
INSERT INTO ticks VALUES ('2026-04-09T05:15:00Z', 'CITY', 510), ('2026-04-10T05:15:00Z', 'CITY', 512),
('2026-04-11T05:15:00Z', 'CITY', 511), ('2026-04-12T05:15:00Z', 'CITY', 513);
SELECT ts, price FROM ticks WHERE NOT is_trading_day('nepse', ts) ORDER BY ts;
```
Result:
`CREATE CALENDAR nepse (TIME ZONE 'Asia/Kathmandu', ERA FROM '2022-01-01' DAYS 'sun-thu', ERA FROM '2`
```text
calendar 'nepse' created: 2 era(s), 0 holiday(s), 0 extra trading day(s)
```
`CREATE TABLE ticks (`
```text
table 'ticks' created
```
`INSERT INTO ticks VALUES ('2026-04-09T05:15:00Z', 'CITY', 510), ('2026-04-10T05:15:00Z', 'CITY', 512`
```text
4 row(s) inserted
```
`SELECT ts, price FROM ticks WHERE NOT is_trading_day('nepse', ts) ORDER BY ts`
| ts (TIMESTAMP) | price (DOUBLE) |
|---|---|
| 2026-04-11T05:15:00.000Z | 511 |
| 2026-04-12T05:15:00.000Z | 513 |
(2 rows)
## Find missing days
A trading day with no row is a gap in your data (a feed that stopped, a day not loaded). List the calendar's trading days between a symbol's first and last row with `generate_series`, and keep those without a row:
**Example: Trading days of CITY with no daily bar**
```sql
CREATE CALENDAR nepse (TIME ZONE 'Asia/Kathmandu', ERA FROM '2026-04-10' DAYS mon-fri, HOLIDAY '2026-09-24' 'holiday');
CREATE TABLE day_bars (time TIMESTAMP, symbol SYMBOL, close DOUBLE) TIMESERIES(time) PARTITION BY MONTH;
INSERT INTO day_bars VALUES ('2026-09-21', 'CITY', 510), ('2026-09-23', 'CITY', 512), ('2026-09-25', 'CITY', 515);
SELECT d AS missing_day
FROM generate_series((SELECT min(time) FROM day_bars WHERE symbol = 'CITY')::DATE,
(SELECT max(time) FROM day_bars WHERE symbol = 'CITY')::DATE) AS d
WHERE is_trading_day('nepse', d)
AND d NOT IN (SELECT time::DATE FROM day_bars WHERE symbol = 'CITY');
```
Result:
`CREATE CALENDAR nepse (TIME ZONE 'Asia/Kathmandu', ERA FROM '2026-04-10' DAYS 'mon-fri', HOLIDAY '20`
```text
calendar 'nepse' created: 1 era(s), 1 holiday(s), 0 extra trading day(s)
```
`CREATE TABLE day_bars (`
```text
table 'day_bars' created
```
`INSERT INTO day_bars VALUES ('2026-09-21', 'CITY', 510), ('2026-09-23', 'CITY', 512), ('2026-09-25',`
```text
3 row(s) inserted
```
`SELECT d AS missing_day FROM generate_series(CAST((SELECT min(time) FROM day_bars WHERE symbol = 'CI`
| missing_day (DATE) |
|---|
| 2026-09-22 |
(1 row)
22 September is missing; 24 September is a holiday, so it is not.
## See the calendars: kdb_calendars
`SELECT * FROM kdb_calendars` lists every calendar of the database: one row per era, holiday and extra trading day.
| Column | Meaning |
|---|---|
| `calendar` | the calendar's name |
| `time_zone` | its zone (`UTC` when none was given) |
| `kind` | `era`, `holiday` or `trading day` |
| `day` | the era's first day, or the day |
| `days` | an era's weekdays (`sun-thu`) |
| `note` | the note of a holiday or trading day |
**Example: What the calendar holds**
```sql
CREATE CALENDAR nepse (ERA FROM '2022-01-01' DAYS sun-thu, ERA FROM '2026-04-10' DAYS mon-fri);
ALTER CALENDAR nepse ADD HOLIDAY '2026-10-20' 'Dashain';
SELECT kind, day, days, note FROM kdb_calendars WHERE calendar = 'nepse' ORDER BY day;
```
Result:
`CREATE CALENDAR nepse (ERA FROM '2022-01-01' DAYS 'sun-thu', ERA FROM '2026-04-10' DAYS 'mon-fri')`
```text
calendar 'nepse' created: 2 era(s), 0 holiday(s), 0 extra trading day(s)
```
`ALTER CALENDAR nepse ADD HOLIDAY '2026-10-20' 'Dashain'`
```text
calendar 'nepse' altered: 1 holiday(s) added
```
`SELECT kind, day, days, note FROM kdb_calendars WHERE calendar = 'nepse' ORDER BY day`
| kind (STRING) | day (DATE) | days (STRING) | note (STRING) |
|---|---|---|---|
| era | 2022-01-01 | sun-thu | NULL |
| era | 2026-04-10 | mon-fri | NULL |
| holiday | 2026-10-20 | NULL | Dashain |
(3 rows)
## Find the eras in your data: suggest_calendar_eras
Syntax:
```sql
suggest_calendar_eras(time [, 'zone'])
```
An aggregate: it looks at the days that have rows and writes a report as text:
- how many days of each weekday have data, per year, and how many of the era's weekdays have **no** data (holidays, or gaps in your data);
- the eras it found, and the days with data outside their era's weekdays (extra sessions);
- a `CREATE CALENDAR` statement to start from (and `ADD TRADING DAY` for the extra sessions).
How it decides: each week gets the weekdays that have data in most of the weeks around it (4 before, 4 after), so one holiday or one extra session does not count. Weeks in a row with the same weekdays are an era; an era is at least 8 weeks. Where two eras meet, the new era starts on the day that leaves the fewest days outside their era.
- Give the zone of the market when the times are UTC and a session could cross midnight UTC. The day is the local day in that zone.
- Check the days without data before you add them as holidays: they may be gaps in your data.
**Example: The weekdays of the sample candles** (sample data: sample)
```sql
SELECT suggest_calendar_eras(time, 'Asia/Kathmandu') AS report FROM ochl_day;
```
Result:
| report (STRING) |
|---|
| year sun mon tue wed thu fri sat no_data 2025 1 1 1 1 0 0 0 0 2026 3 3 3 3 3 0 0 166 eras: 2025-12-28 sun-thu era ... |
(1 row)
## Timers on trading days
A [timer](timers.md#schedules) with the schedule `trading nepse 15:05` runs at 15:05 on the trading days of calendar `nepse` of its database only. It follows the calendar as it changes: a holiday added in the morning is skipped that afternoon. When the calendar is dropped, each run fails and says so.
## How it is stored
Calendars are part of the database's catalog, like tables and [ENUM types](column-types.md#enum): in every backup, kept across restarts, and replayed from the log after a crash. A database with a calendar needs a KDB from 2026-09-25 or newer (feature `calendars`, see [versions](versions.md)).