Skip to content

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.&lt;WORLD&gt;</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.core defines, generated from the fragment.