Naming
Every pubsub topic, state key and CloudEvent type, subject and source
in Virtufin is built by one library, Virtufin.Naming. The same library
exists in Python (virtufin-naming) and TypeScript (@virtufin/naming).
Code never writes a name by hand: it asks an entity for its topic or key.
The rules are normative in the pubsub-topics spec. The naming reference lists every family that every repo defines, generated from the published fragments. This section explains the model and how to use it.
The model in one picture
An entity is a stem (scope + path), an identity, an entity type and a set of events. Every name is derived from those parts, so the topic, the key and the subject of one entity can never disagree:
flowchart LR
subgraph Entity["Entity: Binance order book"]
S["scope<br/><code>act</code> or <code>hyp.<WORLD></code>"]
P["path<br/><code>exchange.binance.orderbook</code>"]
I["identity<br/><code>BTC_USDT</code>"]
E["events<br/><code>changed</code>, <code>gap</code>, …"]
end
S --> T["topic<br/><code>act.exchange.binance.orderbook.changed.BTC_USDT</code>"]
P --> T
E --> T
I --> T
S --> K["state key<br/><code>act.exchange.binance.orderbook.BTC_USDT</code>"]
P --> K
I --> K
I --> SU["ce-subject<br/><code>currency_pair/BTC_USDT</code>"]
P --> CT["ce-type<br/><code>com.virtufin.exchange.binance.orderbook.changed</code>"]
E --> CT
Derivation rules
| Name | Rule | Example (Binance order book, BTC_USDT) |
|---|---|---|
| topic | scope.path.event[.identity]; the identity is appended only if the event is partitioned |
act.exchange.binance.orderbook.changed.BTC_USDT |
| state key | scope.path.identity; every event of the entity shares it |
act.exchange.binance.orderbook.BTC_USDT |
| index key | scope.path.index; the list of instances |
cfg.scenario.index |
| ce-subject | entity_type/identity |
currency_pair/BTC_USDT |
| ce-type | com.virtufin.path.event; the scope and identity are dropped |
com.virtufin.exchange.binance.orderbook.changed |
| ce-source | urn:com.virtufin:<service>[:<instance>]; workers use urn:com.virtufin:worker:<type>[:<id>] |
urn:com.virtufin:worker:binanceorderbook |
| dead letter | dlq.<topic>; the pubsub component's own is dlq.pubsub |
dlq.sc.LIVE.trading.order.submitted |
| experimental | x.<segments>; outside the contract, for demos and scratch work |
x.demo.events |
An unpartitioned event (partitioned: false) has one topic for all
instances, and its identity travels in the subject. Trading events work
this way: sc.LIVE.trading.order.filled with subject order/<id>.
Scopes
The first segment of every name is a scope. Only virtufin.core defines
infrastructure scopes; trading scopes come from the fragments that own them.
| Scope | Template | Owner fragment | Purpose | Example |
|---|---|---|---|---|
act |
act.<path>… |
virtufin.worlds |
actual market data | act.exchange.binance.candle.closed.BTC_USDT.1m |
hyp |
hyp.<WORLD>.<path>… |
virtufin.worlds |
hypothetical market data, one named world | hyp.CRASH_2020.exchange.binance.orderbook.changed.BTC_USDT |
sc |
sc.<SCENARIO>.<path>… |
virtufin.worlds |
scenario-scoped trading, indicators, positions | sc.LIVE.indicator.sma-20.changed.BTC_USDT |
sys |
sys.<service>.<entity>… |
virtufin.core |
one platform service's own names | sys.workmanager.worker.lifecycle |
wrk |
wrk.<worker_type>.<event>[.<worker_id>] |
virtufin.core |
worker commands, responses, errors | wrk.httpgetworker.command.{worker_id} |
rpc |
rpc.<caller>.reply.<run_id> |
virtufin.core |
per-run request-reply topics | rpc.notebook.reply.9f8e7d6c |
cfg |
cfg.<registry>.<ID> |
virtufin.worlds and services |
registry and configuration keys (never topics) | cfg.scenario.LIVE |
dlq |
dlq.<topic> |
virtufin.core |
dead letters | dlq.pubsub |
x |
x.<segments> |
none | experimental, outside the contract | x.demo.notebook.events |
Segment classes
Every segment belongs to exactly one class, and the letter case tells you
which. Vocabulary is lowercase and identifiers are uppercase, so a reserved
word such as index can never collide with an identity.
| Class | Alphabet | Used for | Valid | Invalid |
|---|---|---|---|---|
vocab |
[a-z][a-z0-9-]* |
domain, entity, event, venue, service, worker type, indicator instance | orderbook, load-failed, sma-20 |
OrderBook, load_failed, 20sma |
identifier |
[A-Z0-9_]{1,32}, at least one letter |
scenario, world, universe, strategy name | LIVE, CRASH_2020 |
live, 123 |
pair |
BASE_QUOTE, uppercase |
currency pair | BTC_USDT, 1INCH_USD |
BTCUSDT, btc_usdt, BTC/USDT |
guid |
lowercase canonical GUID | worker, order, connection id | 3f2a8c1e-0b4d-4e6f-9a7b-1c2d3e4f5a6b |
uppercase or braced GUIDs |
interval |
1s 1m 3m 5m 15m 30m 1h 2h 4h 6h 8h 12h 1d 3d 1w 1mo |
candle interval | 1m, 1mo |
1M, 60s |
token |
[a-z0-9][a-z0-9-]* |
opaque id, e.g. a run id | 9f8e7d6c |
Run_1 |
any |
[A-Za-z0-9_-]+ |
experimental (x.) names only |
demo |
a.b |
A segment is at most 64 characters, and a whole name at most 249 (Kafka's limit, the strictest backend we might use). See Validity.
Who owns which names
Names are defined in fragments: small YAML files that live in the repo that owns the names, and compose with the fragments they import. There is no central list. See Fragments.
| Fragment | Lives in | Defines |
|---|---|---|
virtufin.core |
lib (Virtufin.Naming) |
segment classes; the sys, wrk, rpc, cfg, dlq, x scopes; worker and reply families |
virtufin.worlds |
strategy-devkit (Virtufin.Behavior.DevKit) |
act/hyp.<WORLD>, sc.<SCENARIO>; the scenario, world and strategy registries |
virtufin.market |
market-data-devkit | venue-generic order book, candle, trade, instrument |
virtufin.indicator |
indicator-devkit | sc.<S>.indicator.<name> |
virtufin.trading |
strategy-devkit | sc.<S>.trading.order, sc.<S>.position |
virtufin.venue.<venue> |
the engine repo of the first worker for that venue | the venue, its symbol mapping, its bound market families |
virtufin.worker.<type> |
each worker's repo | its worker type and what it consumes and produces |
virtufin.service.<service> |
each service repo | its sys.* and cfg.* names |
Using it
C
using Virtufin.Naming;
// Composes virtufin.core plus every fragment embedded in this assembly
// and the assemblies it references.
var names = NameRegistry.Load(typeof(MyWorker).Assembly);
var book = names.GetEntity("virtufin.venue.binance/orderbook");
var pair = CurrencyPair.Parse("BTC_USDT");
book.Topic("changed", World.Act, pair); // act.exchange.binance.orderbook.changed.BTC_USDT
book.Key(World.Act, pair); // act.exchange.binance.orderbook.BTC_USDT
book.Subject(pair); // currency_pair/BTC_USDT
book.CeType("changed"); // com.virtufin.exchange.binance.orderbook.changed
names.TryParseTopic("act.exchange.binance.orderbook.gap.BTC_USDT", out var parsed);
// parsed.Entity.Id == "virtufin.venue.binance/orderbook", parsed.Event == "gap"
Python
from virtufin_naming import ACT, CurrencyPair, NameRegistry
names = NameRegistry.load() # core + fragments of installed packages
book = names.entity("virtufin.venue.binance/orderbook")
pair = CurrencyPair.parse("BTC_USDT")
book.topic("changed", ACT, pair) # act.exchange.binance.orderbook.changed.BTC_USDT
book.key(ACT, pair) # act.exchange.binance.orderbook.BTC_USDT
names.parse_topic("act.exchange.binance.orderbook.gap.BTC_USDT").event # "gap"
TypeScript
import { ACT, CurrencyPair, FragmentDocument, NameRegistry } from "@virtufin/naming";
const names = NameRegistry.compose([FragmentDocument.parse(marketYaml), FragmentDocument.parse(binanceYaml)]);
const book = names.entity("virtufin.venue.binance/orderbook");
book.topic("changed", ACT, CurrencyPair.parse("BTC_USDT"));
Builders validate their arguments: a lowercase pair, a missing world or an
unknown event throws NamingException / NamingError in the caller's
process. Infrastructure services (api, WorkManager, WebSocketManager) pass
names through unchanged and never validate them.
In this section
- Interfaces and combinators: the building blocks and their laws.
- Fragments: the YAML format, ownership, discovery and conflict checks.
- Adding a venue: the Alpaca order-book walkthrough.
- Validity: why every name is a valid Redis stream, Dapr state key, Kafka topic and NATS subject.
- Migration: old name → new name.
- Core families: the families
virtufin.coredefines, generated from the fragment.