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.
The three storage rules
| Type | Literal | Stored as | Zero point |
|---|---|---|---|
d date | 2026.07.30 | int — days | 2000-01-01 |
t time | 10:00:00.000 | int — milliseconds | midnight (same day) |
p timestamp | 2026.07.30D09:30:00.000000000 | int64 — nanoseconds | 2000-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 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
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 -> 9709Accessors, and their q spellings
| q | Amber | Meaning |
|---|---|---|
t.hh | hh t | hour of day (0–23) |
t.mm | mm t | minute of hour (0–59) |
t.ss | sec t | second of minute — ss is string-search in Amber |
t.minute | minute t | minutes since midnight |
t.second | second t | seconds since midnight |
| (millis) | milli t | millisecond (0–999) |
| build | hms[h;m;s] | construct a time |
| parse | ptime "HH:MM:SS.mmm" | string → ms |
| format | stime t | ms → "HH:MM:SS.mmm" |
Bucketing — the bar family
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 columnThe classic 1-minute OHLCV query, exactly as a q tickerplant would write it:
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:
select vwap:wavg[sz;px] by time:1m xbar time from tradesWhy 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
/ 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 - UNIXNSAmber ↔ pandas / NumPy
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.
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-2000And the query macros come in both flavours, named so you cannot pick the wrong one by accident:
| Macro | Substituted with |
|---|---|
$__from / $__to | the panel range as milliseconds of day |
$__fromNs / $__toNs | the panel range as nanoseconds since 2000-01-01 |
$__interval_ms | the 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/
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 IPCColumns 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.