asset & currency — the instrument tag
A price is a rate: so many units of a quote currency per unit of a base instrument. Once you say that out loud, a whole class of bugs becomes a type error — because “BTC priced in dollars” and “BTC priced in euros” are then visibly different things, and adding them is visibly nonsense.
That is the entire pillar. A structured asset tag rides on the price-dimension kinds, the
operators gate on it, and fx and money fall out as tagged versions of kinds that already
existed. Zero new sorts.
This page is the FDK reference for the asset tag: its components, the operator behaviour it
drives, and the fx/money/pv kinds it produces. The tag mechanism it reuses — the affine
substrate, the join lattice, the two-regime kind system — is specified in
Kinds; the full ±/×/÷ rules and the FX role rule live in
Operators. Here we state how those rules read when the tag names an
instrument and a currency.
New here? Start with Guide §5 — Kinds →
A note on the samples. Flux has no expression-statements, so a bare expression is not a program. The lines marked
✗on this page are therefore expression fragments: they exist to show what the kind rules refuse, not what the parser accepts. Every unmarked line is a legal statement. The bracket notation (price[BTC,USD],fx[USD/EUR]) is how this page writes a kind in prose and in comments; it is not source syntax.
The tag
price[B, Q] level[B, Q] pv[Q] volume[B]| Component | What it is | Carried by |
|---|---|---|
base B |
the instrument | every price-dimension kind — except pv, which drops it (deliberately) |
quote Q |
the currency the price is in — the unit the value is measured in | every dimension containing price |
venue @v |
optional third component, opt-in, default off | the same kinds, when enabled |
Dimensionless kinds — ratio, osc, num, signal, dir — carry no asset tag. A relative
strength is a number; it does not belong to an instrument.
Each component has its own top (⊤base, ⊤quote, ⊤venue), and the join widens the
component that differs while preserving the one that matches:
price[BTC,USD] ⊔ price[ETH,USD] = price[⊤base, USD] // the quote survives
tag ⊔ ⊤component = ⊤component // never an errorSafety comes from the algebra, not the join
This is the design decision worth understanding, because it is counter-intuitive at first: the join is permissive (it widens), and the safety lives in the operators.
btcUsd = series("BTC-USD").close // price[BTC,USD]
ethUsd = series("ETH-USD").close // price[ETH,USD]
btcEur = series("BTC-EUR").close // price[BTC,EUR]
btcUsd + ethUsd // ✗ [ErrDim] — different bases
btcUsd + btcEur // ✗ [ErrDim] — different quotes: a dollar is not a euro
move = btcUsd - btcUsd // level[BTC,USD] ✓± demands identical tags, component by component. That single rule catches the two mistakes
that matter — mixing instruments, and mixing currencies — and it catches them at compile time,
where a silent wrong number would otherwise have been produced.
Ordering and equality gate the same way. price[BTC,USD] < price[BTC,EUR] does not compile.
Division: a 2×2, and one of its cells is an exchange rate
btcUsd = series("BTC-USD").close
ethUsd = series("ETH-USD").close
btcEur = series("BTC-EUR").close
ethEur = series("ETH-EUR").close
same = btcUsd / btcUsd // ratio — same base, same quote
rel = btcUsd / ethUsd // ratio — base differs, quote shared: relative strength; the tag is dropped
rate = btcUsd / btcEur // fx[USD/EUR] — SAME base, quote differs: this IS an exchange rate
btcUsd / ethEur // ✗ [ErrDim] — both differ: no shared axis to cancel, so the ratio is undefinedThe third cell is the interesting one. Divide the same instrument priced in two currencies and the
instrument cancels — what remains is the rate between the currencies. Flux names that:
fx[USD/EUR].
The fourth cell has nothing to cancel: a different base and a different quote share no axis, so the
division is [ErrDim] rather than a silent number. A ratio needs a common denominator — a shared
quote for a cross-base strength, a shared base for a cross-quote rate — and when neither is present
there is no defensible value to return.
Figure — the division 2×2: same base and same quote cancel to a
ratio, a shared quote gives a relative strength, a shared base gives fx[USD/EUR], and when both differ no axis cancels — [ErrDim].
There is no fxRate(a, b) primitive. An fx value is derived — by that division, or by a
feed that declares itself an exchange-rate source. That is deliberate: a rate you conjured from a
symbol name is a rate nobody checked.
Multiplication: conversion is unit cancellation
btcUsd = series("BTC-USD").close
btcEur = series("BTC-EUR").close
usdPerEur = btcUsd / btcEur // fx[USD/EUR] — derived by the division above, never conjured
eurPerUsd = btcEur / btcUsd // fx[EUR/USD] — its reciprocal
back = btcEur * usdPerEur // price[BTC,USD] ✓ — the shared quote cancels: (EUR/BTC)·(USD/EUR)
also = btcEur / eurPerUsd // price[BTC,USD] ✓ — the reciprocal converts the same wayCurrency conversion is not a special rule bolted on. It is the ordinary exponent algebra, applied to a tag that happens to name a currency — which is exactly what makes it hard to get wrong.
Cross-asset multiplication widens rather than failing (price[BTC,USD] × price[ETH,USD] is a
P² kind with widened tags): a product of two instruments is unusual but not meaningless, and the
rule that catches the real mistakes is ±, not ×.
The money-flow: pv drops the base
Multiply a price by a volume and you have a money-flow — a notional amount of currency. The base
pairs, then cancels, deliberately: a flow of money is base-agnostic, an amount in a currency rather
than a quantity of an instrument. So pv[Q] carries the quote alone.
btc = series("BTC-USD")
eth = series("ETH-USD")
btcE = series("BTC-EUR")
flowBtc = btc.close * btc.volume // pv[USD] — the base pairs, then drops
flowEth = eth.close * eth.volume // pv[USD]
flowEur = btcE.close * btcE.volume // pv[EUR]
book = flowBtc + flowEth // pv[USD] ✓ — notionals in one currency compose
flowBtc + flowEur // ✗ [ErrDim] — a dollar flow is not a euro flowThat two USD money-flows add is the point, not an oversight: a dollar of BTC notional and a dollar
of ETH notional are the same dimension, and summing them is exactly the portfolio total a book
wants — a dollar is a dollar. The mistake ± still catches on a money-flow is the currency mix,
never the base mix. Per-asset discrimination, when you want it, comes from holding the per-asset pv
in a vec or a Table keyed on the base — never from re-tagging the flow.
Dividing a flow back is the ordinary group algebra, no special rule: pv[Q] ÷ volume[B] → price[B,Q]
recovers the price, and pv[Q] ÷ price[B,Q] → volume[B] recovers the size — the same P·V exponents
that built the flow, run in reverse.
fx and money invent nothing
| Notation | Actually is |
|---|---|
fx[Q1/Q2] |
the existing ratio kind, wearing a currency-pair annotation |
money[Q] |
decimal pv[Q] — an exact fixed-point money-flow |
Zero new sorts, zero new lattice height. The pair annotation is a fourth tag axis that lives
on ratio alone — and since ratio carries no asset tag, the ceiling of three tags per kind is
preserved.
Venue and source
The venue is where a price came from. It is metadata by default — carried on the producer, not in the kind — because tagging every price with an exchange would fragment the type of every expression that touches two of them, for a safety nobody asked for.
The venue can be enabled as an opt-in third component of the tag, for the arbitrage case where two prices of the same instrument on two exchanges must not be interchangeable. It is designed, and off by default.
pinVenue — pinning a series to a venue at the type level — is specified and held inert, its name
carried so enabling it later changes no grammar.
A producer declares what it is:
| Producer kind | Meaning |
|---|---|
Index |
a computed index, not a tradable instrument |
Venue |
an exchange feed |
Fx |
an exchange-rate feed — the second way an fx value can arise |
Metric |
a non-price series (the metric[id] seam), held inert |
metric[id] (rule A15) is the identity annotation a Metric producer stamps: same-id ±
preserves, differing ids refuse, metric[A] ÷ metric[B] → ratio. The axis is held inert — no
non-price series enters ANALYSIS until the amendment is armed; the annotation rule itself is stated
in Kinds.
toSource(key) is the seam that stamps a stream’s tag onto the series it produces, and hands it
to the host for append-only causal ingestion — which is how an external feed becomes an
ordinary, repaint-free series (net).
Cross-series work
btc = series("BTC-USD")
eth = series("ETH-USD")
spread = btc.close / eth.close // ratio — plottable
corr = stat.correl(returns(btc.close), returns(eth.close), 30) // osc(-1,1)
rel = series("ALT-USD").close / btc.close // relative strengthThe foreign series is aligned onto the chart’s ordinal axis by an as-of join — the most recent
foreign bar at or before the current bar’s time. Never a nearest match, which would read the
future. Gaps hold the last known value; before the first foreign bar the value is na. No-repaint
is inherited rather than re-argued.
See also
- Kinds — the tag axes and their two regimes; canonical home of the asset-tag algebra.
- Operators — the
±,×,÷rules and the FX role rule in full. - units — the sibling tag axis, for physical quantities.
- compute — asset-tagged columns, and joins that cannot mix instruments.
- net —
toSource, and the append-only causal ingestion a feed’s tag rides in on. - Host integration — cross-series compilation and the as-of contract.