Temporal mechanics

Dates, times and timestamps are first-class types in Amber with literal syntax, auto-display and type-aware arithmetic. Underneath they are plain integers — and the number they count from is 2000-01-01, not 1970-01-01. Every integration in the ecosystem gets this right explicitly, because getting it wrong is a silent thirty-year shift.

1970-01-01 Unix epoch t = 0 2000-01-01 Amber epoch d = 0 · p = 0 2026-08-21 d = 9729 10,957 days 946,684,800,000,000,000 ns int64 ns range: ±292.47 years from the epoch 1707 … 2292 (Unix ns: 1677 … 2262)

The three storage rules

TypeLiteralStored asZero point
d date2026.07.30int — days2000-01-01
t time10:00:00.000int — millisecondsmidnight (same day)
p timestamp2026.07.30D09:30:00.000000000int64 — nanoseconds2000-01-01T00:00:00

A time of day is a count of milliseconds since midnight, the same convention as kdb+'s time — it carries no date at all. A timestamp is a count of nanoseconds since the epoch instant and carries both.

The two magic numbers

The Unix epoch is 10,957 days before the Amber epoch, which is 946,684,800 seconds, which is 946,684,800,000,000,000 nanoseconds. Every conversion in this page is one of those three constants applied in one of two directions, and nothing else.

Working with them

amber>
2026.07.30                          / date         -> 2026.07.30
10:00:00.000 + 00:00:05.000         / time + time  -> 10:00:05.000
2026.08.15 - 2026.07.30             / date - date  -> 16   (days)
2026.07.30 + 16                     / date + int   -> 2026.08.15
2026.07.30D09:30:00.000000000       / timestamp (ns since 2000.01.01)

year 2026.07.30                     / 2026
month 2026.07.30                    / 7
day  2026.07.30                     / 30
dow  2026.07.30                     / day of week

"D"$"2026.12.25"                    / string casts: "D"$ (date) "T"$ (time) "P"$ (timestamp)
`i$2026.07.30                       / extract the raw numeric value -> 9709

Accessors, and their q spellings

qAmberMeaning
t.hhhh thour of day (0–23)
t.mmmm tminute of hour (0–59)
t.sssec tsecond of minute — ss is string-search in Amber
t.minuteminute tminutes since midnight
t.secondsecond tseconds since midnight
(millis)milli tmillisecond (0–999)
buildhms[h;m;s]construct a time
parseptime "HH:MM:SS.mmm"string → ms
formatstime tms → "HH:MM:SS.mmm"

Bucketing — the bar family

amber>
minbar[w; t]      / w-minute bar, returned as a time (ms)
bar[w; u; t]      / generic: w buckets of u ms — bar[5;60000;t] = 5-minute bars
xbar[w; x]        / plain bucketing of any integer column

The classic 1-minute OHLCV query, exactly as a q tickerplant would write it:

amber>
tb: +@[+trade; ,`time; minbar[1]@]        / snap the time column onto 1-minute bars
qby[tb; `sym`time;
    `open`high`low`close`vol!({first x`px};{max x`px};{min x`px};{last x`px};{sum x`sz})]

Or, at the prompt, in bare qSQL:

amber>
select vwap:wavg[sz;px] by time:1m xbar time from trades

Why 2000-01-01 at all

Three reasons, in descending order of how much they matter.

Range where you need it

An int64 nanosecond counter spans ±292.47 years. From 1970 that is 1677–2262; from 2000 it is 1707–2292. Financial data is a forward-looking problem — the thirty years bought at the far end are worth more than the thirty given up at the near end.

Smaller numbers, denser columns

A date is an int in the low ten-thousands, not the high ten-thousands. Amber stores an integer vector in the narrowest type that holds every element, so a date column of a modern trading day is physically int16-representable rather than pushed a width up.

Compatibility with q

kdb+ uses the same epoch and the same millisecond-of-day convention. A shop porting a tickerplant query does not have to rebase anything, and a stored kdb-shaped column means the same thing here.

Converting, in each direction

Amber ↔ Unix, in Amber

amber>
/ constants
UNIXD:10957                        / days from 1970-01-01 to 2000-01-01
UNIXNS:946684800000000000          / nanoseconds, same interval

/ date -> unix days -> unix seconds
ud:(`i$2026.07.30) + UNIXD         / 20299
us:86400 * ud                      / 1753833600

/ timestamp -> unix nanoseconds
un:(`i$2026.07.30D09:30:00.000000000) + UNIXNS

/ and back
`p$ un - UNIXNS

Amber ↔ pandas / NumPy

python-amber does not rebase for you — on purpose

Table.from_pandas maps datetime64 columns to int64 nanoseconds without re-basing the epoch. Silently shifting your data by thirty years would be worse than leaving it where pandas had it, so the conversion is yours to make and yours to see.

import numpy as np, pandas as pd, amber as am

AMBER_EPOCH_NS = 946_684_800_000_000_000       # 2000-01-01T00:00:00Z

# Amber timestamps -> pandas
t   = am.q("select time, px from trades")
ns  = np.asarray(t.column("time", raw=True))   # zero-copy int64 view
idx = pd.to_datetime(ns + AMBER_EPOCH_NS, utc=True)
df  = pd.DataFrame({"px": np.asarray(t.column("px", raw=True))}, index=idx)

# pandas -> Amber timestamps
back = df.index.view("int64") - AMBER_EPOCH_NS
am.set("time", back.astype("int64"))

Amber ↔ Arrow

Arrow's timestamp type carries its own epoch (always 1970) in the schema, so the conversion belongs at the boundary. amber-arrow exports the raw int64 column and leaves the field typed as an integer unless you ask otherwise — an Arrow timestamp field that claimed the Unix epoch while holding Amber counts would be worse than an honest integer.

import pyarrow as pa, amber_arrow as aa

tbl = aa.query("select time, px from trades").read_all()
ts  = pa.compute.add(tbl.column("time"), 946_684_800_000_000_000)
tbl = tbl.set_column(0, pa.field("time", pa.timestamp("ns", tz="UTC")),
                     ts.cast(pa.timestamp("ns", tz="UTC")))

Amber ↔ Grafana

Grafana's time axis is Unix milliseconds. The datasource plugin does the conversion in Go and makes the unit an explicit setting rather than a guess, because a column of nanoseconds-since-2000 and a column of milliseconds-of-day overlap in magnitude and cannot be told apart by value alone.

provisioning/datasources/amber.yaml
jsonData:
  host: amberd
  port: 5012
  # The store's `time` columns are nanoseconds since 2000-01-01 -- Amber's own
  # timestamp epoch, not the Unix one. Every panel sets this explicitly too, but
  # the default belongs here so a new panel is right before anyone touches it.
  defaultTimeUnit: ns-2000

And the query macros come in both flavours, named so you cannot pick the wrong one by accident:

MacroSubstituted with
$__from / $__tothe panel range as milliseconds of day
$__fromNs / $__toNsthe panel range as nanoseconds since 2000-01-01
$__interval_msthe panel's step, for bucketing with xbar

Storage: the same rule on disk

amber-tick's partitioned store writes time columns as int64 nanoseconds since 2000-01-01, and q/tick.k teaches amber.k's grid formatter about it without touching amber.k. The Arrow IPC copy of the store is written in the same pass from the same arrays, so the two views cannot drift.

store/
store/
  sym                       the symbol enumeration domain
  par.txt                   the partition list
  2026.08.21/               <- the partition name IS the date
    trades/{.d,sym,time,px,sz,side,venue,cond,seq,extime}
    quotes/{.d,sym,time,bid,ask,bsz,asz,bex,aex,cond}
  arrow/2026.08.21/*.arrow  the same data as Arrow IPC
Rendering does not change storage

Columns keep numeric storage, exactly as kdb does internally, so xasc and the `s attribute work unchanged and the as-of kernel still binary-searches. A column named time auto-renders as HH:MM:SS.mmm in a grid; tsym[t;c] marks any other columns you want rendered that way.