◆ Flux

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 error

Safety 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.

FLUX
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

FLUX
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 undefined

The 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.

The division 2×2 on the asset tag 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

FLUX
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 way

Currency 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.

FLUX
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 flow

That 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

FLUX
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 strength

The 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