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 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 |
n timespan | 0D01:00:00.000000000 | int64, nanoseconds | a length of time, no zero point |
u minute | 09:30 | int, minutes | midnight |
v second | 09:30:15 | int, seconds | midnight |
m month | 2026.03m | int, months | 2000.01 |
Since 2.9.0 all of q's temporal kinds are in, each with its typed null (0Nd 0Nt 0Np 0Nn 0Nu 0Nv 0Nm), and a list of any one kind is its own vector type, held as its numbers like q's, so sorting, grouping and comparing them runs at int speed.
A time of day is a count of milliseconds since midnight, the same convention as q'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.01.01 2026.01.02 / A strand of dates is a list (2.5)
2026.07.30 * 2 / Anything else is 'type; date+0N is 0Nd
2026.07.30D10:00 - 2026.07.30D09:30 / timestamp - timestamp -> 0D00:30:00.000000000 (a timespan)
2026.07.30 + 0D09:30 / date + timespan -> a timestamp
09:30 + 15 / minute + int -> 09:45
2026.03m + 1 / month + int -> 2026.04m
09:30:00 / a second, as q; a time keeps its ms: 09:30:00.000
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"$ "T"$ "P"$, and "M"$ "U"$ "V"$ "N"$ (2.9.0)
`i$2026.07.30 / extract the raw numeric value -> 9707Accessors, 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:
gentq 100000 / sets the globals: trades, quotes
tb: +@[+trades; ,`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:minbar[1;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, so 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
q 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 q-shaped column means the same thing here.
The offsets themselves (day counts, millisecond-of-day, the nanosecond scale, and what goes wrong when a foreign timestamp arrives on a different epoch) are worked through in why Amber counts days and nanoseconds from 2000-01-01.
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 / 20664
us:86400 * ud / 1785369600
/ timestamp -> unix nanoseconds
un:(`i$2026.07.30D09:30:00.000000000) + UNIXNS
/ and back: `mkp builds a timestamp from raw ns, and so does q's `timestamp$
/ (the other constructors are `mkd `mkt)
`mkp 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 q 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.