Trade booking

Table of Contents

One service books trades. Every caller books through the same operations: the trade booking journey in the UX, the REST gateway, the ORE import and the synthetic generator. A caller prepares a request and sends it. It does not decide how the trade is stored. The trading service writes a trade and its parts in one unit of work, so a trade is whole or absent. A bond trade names a security. The security is a separate aggregate, created by its own operation before the trade. A structure groups trades that are already booked, and its booking is a design in progress.

1. Detail

1.1. The principle

Booking has one path. A caller may differ in the data it sends. It never differs in the operation it calls.

  • The trading service owns booking. It knows nothing about its callers. No operation has a flag, a reason code or a branch that exists for one caller.
  • A caller owns its source. The UX reads a form. The ORE import reads an XML document. The generator draws from a plan. Each turns its source into the same request.
  • A trade is a position. It is not repaired after the fact. A stop in the middle of a booking leaves nothing, and the caller sends the request again.
  • A trade records where it came from. Callers share one operation, so the trade itself must say which caller created it. The origin is data on the trade. It is never a branch in the service.

1.2. The booking contract

A trade is an immutable anchor with a booking and a state. Each is a row in its own table. Every row of the trade names the activity that wrote it. The booking request carries all the parts, so that no part is written alone.

Part What it holds Model
Anchor The immutable facts: trade id, party, counterparty, trade type, booking nature, entry channel, counterparty scope ores.trading.trade
Origin Which of our systems created the trade, and which run of it. See the next section. ores.trading.trade
Booking Where the trade sits: book, netting set, trade date, execution time ores.trading.trade_booking
Activity type The cause of the booking: new_booking for a live trade, draft_capture for a draft ores.trading.trade_activity
Identifiers The firm's own identifier, a venue identifier, the ORE identifier ores.trading.trade_identifier
Classification Party roles, portfolios, additional fields trade_party_role, trade_portfolio, trade_additional_field
Instrument The economic terms, as a typed batch with one array for each instrument table and child table ores.trading.trade_operations

The service creates the first state and the activity. It stamps the activity id on every other row. The caller never states an activity id. The service takes the tenant and the party from the session and the audit fields from the caller's change intent.

The caller states the trade id. The trade id is the idempotency key. A second booking of a trade id answers a conflict with the code already_exists and writes nothing. A caller that sends a request again after a stop therefore reaches the same end state.

The service stores a hash of the request content with the trade. A second booking of a trade id answers already_exists when the hash is equal. It answers content_differs when the hash is not equal, and the caller treats that as an error. An id collision or a drifted plan is therefore never taken for a retry.

The trade type routes the instrument to one table, through the trade type catalogue. The request is refused before it writes anything when the batch holds a row for another table, or when the trade type has no instrument table.

1.3. The origin of a trade

Every trade states where it came from. The origin is part of the anchor, so it is immutable. It has two fields:

  • Origin. A code from a lookup table: ux for a person booking in the app, api for an outside system on the REST gateway, ore_import for the ORE import and synthetic for generated data. A new caller adds a row to the lookup. It adds no code to the service.
  • Origin reference. An optional text that names the run in the source system: the import id, the generation config id or the API client. It lets a person find every trade of one run, and lets a rollback name its trades.

The origin is not the entry channel. The entry channel says how the deal reached the firm: manual, stp, ecn or allocation. The origin says which of our systems wrote the record. The ORE import, the REST gateway and the generator all enter through stp, and only the origin tells them apart.

The caller states the origin in the booking request. The service checks it against the lookup. It also checks that the authenticated client may state that origin, because each client is registered for the origins it may use. A client registered for synthetic cannot state ux. The service stores the origin. The service never changes its behaviour for a given origin. The origin is how a report, a clean-up or an audit separates real trades from generated ones. The reason code and the commentary are not used for this.

1.4. The common flow

Every caller follows the same six steps. A step belongs to the caller or to the service, never to both.

  1. Resolve the references. The caller finds the party, the counterparty, the book, the netting set and the portfolios. These belong to refdata. The caller reads them, and creates the missing ones through refdata's own operations first.
  2. Resolve the security. A trade type that names a security needs it in place. The caller sends the security to the security operation. The operation creates it, or answers that it exists. See the section on bonds.
  3. Build the request. The caller builds the booking request from its source.
  4. Book. The caller sends one request. The service checks the permission and the session, opens one unit of work, writes every part and commits.
  5. Read the reply. The reply carries the outcome and the activity id. A conflict means the trade is already booked, whole.
  6. Handle the outcome. A refusal writes nothing. The caller reports it or sends the request again.

A four-eyes rule for booking, if one applies, wraps the one booking operation in the service. It is not added to any caller. See the four-eyes framework. The counterparty's confirmation is a different matter. The activity type states whether it is required, and the confirmation follows the booking.

1.5. Transactionality

1.5.1. The rule

A set of rows that one service owns is written in one unit_of_work: one connection, one transaction, one commit. A set that several services own is never one transaction. The sequence between the owners is the consistency mechanism. See the bundle catalogue, the database per service pattern and the rule that writes go through the repositories.

Trading owns every table of a trade. The catalogue names the two sets as bundles B7 (the trade and its parts) and B8 (the trade's instrument and legs). The booking operation writes both in one unit of work.

1.5.2. The boundaries

Set Owner Unit of work
Trade, activity, booking, state, identifiers, classification, instrument and its child rows trading One, in the booking operation
Bond issue with its legs, amounts, rates and schedules trading One, in the security operation
Books, counterparties, netting sets, portfolios, currencies refdata Its own, before the booking
An import of many trades several None. A workflow sequences the steps.

The trade and the security are two units of work. The security is shared by many trades and has its own life, so it cannot belong to one trade's transaction. A security with no trade is a valid state. A trade with no security is not, and the database refuses it. The order is therefore fixed: the security first, the trade second.

1.5.3. What a stop leaves

The stop falls State left
Before the security operation commits Nothing
After the security commits, before the booking A whole security and no trade. The state is valid.
Inside the booking Nothing
After the booking commits A whole trade

No state in this table is half-written. For that reason the service has no repair step, and a caller has no check for a half-booked trade. A caller that finds its trade id booked treats the trade as present.

1.5.4. Reads and failures inside the unit of work

A read made with the unit of work's context runs on its transaction and does not end it, so the booking's reads see its own writes. See A repository works inside an open transaction and never holds credentials.

A refusal by the database rolls the whole unit back. The service reports the database's own words. It does not map them to a second set of codes.

1.5.5. Across owners

The ORE import writes refdata rows and trading rows. It is a workflow with one step for each owner. See workflows, failure, compensation and retry and package transport. Each trade of the import is atomic. The import as a whole is not. A user may roll an import back. That rollback is a deliberate act on whole trades, and it is not the repair of a half-booked trade. A sandbox holds an import that a person wants to examine before it goes live.

1.6. Instrument peculiarities

An instrument is not one shape. The batch carries one array for each table because a variant cannot cross the wire.

  • Header, fact row and children. A swap writes a header row, a fact row and its legs. The trade owns its legs. The booking writes all of them or none.
  • Option and schedule children. An option adds premium, exercise and payment rows. A schedule adds its date rows. They follow the same rule: the service stamps them and writes them in the same unit of work.
  • Bonds. A bond is the one family whose economics live outside the trade. See the next section.

1.6.1. Bonds and bond issues

A bond issue is the security. A bond trade is a position in it. The trade holds a reference to the security and its own size, date, counterparty and book. The security holds the terms that every holder shares: issuer, issue date, maturity, currency, coupon and schedule.

  • Two aggregates. The bond issue and its children are one aggregate. The bond instrument is part of the trade. The instrument names the issue with issue_id and states the trade's notional.
  • The issue has legs. The legs, amounts, rates, schedules and dates sit on the issue's leg, not on the trade. Call dates and conversion targets sit on the issue. A swap differs: its legs belong to the trade.
  • One table for ten trade types. Bond, ForwardBond, BondFuture, BondOption, BondRepo, BondTRS, BondPosition, CallableBond, ConvertibleBond and Ascot all route to the bond instrument table, and each names an issue.
  • Security operation. One operation creates the issue with its children in one unit of work. The issue is create-only. A second creation of an issue id answers already_exists when the terms are equal and content_differs when they are not. The stored issue stays as it was. An issue is therefore whole or absent, and its existence proves its completeness.
  • Order. A caller creates the issue before it books a trade that names it. The database refuses a bond instrument whose issue does not exist.
  • Sharing. A security belongs to the tenant. It does not belong to a book or a party. A currency belongs to the security, so every book that holds the security has that currency.
  • Resolution. A caller names a security by its security identifier and sends the issue. The service compares the terms. No caller holds comparison code. A caller refuses its trade when the answer is content_differs.
  • Concurrency. Two callers may create the same issue at the same time. One creation succeeds and the other answers already_exists. The second caller books its trade.

1.7. The callers

All four callers send the same requests. The table lists what differs, and the differences are only data.

Caller Source Entry channel Origin and reference Security step Reference step
UX A form with the tabs Trade, Instrument, Identifiers and Review manual ux, the session Choose an issue, or create one in the issue editor Pick lists read from refdata
Web API A JSON body from an outside system stp api, the API client Create the issue, then book The caller sends ids it holds
ORE import An ORE portfolio document stp ore_import, the import id Send the issue and read the answer Create portfolios, books and netting sets in an earlier step
Synthetic A plan drawn from a generation config stp synthetic, the config id Create the issues of the plan, then book Read from the config's scope

1.7.1. UX trade booking

The browser app sends the request through the BFF to the trading service. The page is one form with four tabs. Each tab fills a part of the request: the Trade tab fills the anchor and the booking, the Instrument tab fills the batch, the Identifiers tab fills identifiers and classification, and the Review tab shows the request before it is sent. The person chooses live or draft. The two choices differ in the activity type only. The person gets the refusal in the form and keeps the input. The web component owns the transport. The booking journey owns the steps.

1.7.2. Web API trade booking

The REST gateway ores.http terminates HTTP, authenticates the caller and forwards the request to the owning service over NATS. The booking operations become routes in the same way as the refdata and IAM operations. The route takes the request body unchanged. The gateway adds no booking logic. An outside system uses the trade id as its idempotency key, so a retry after a timeout is safe.

1.7.3. ORE import trade booking

The ORE component reads the XML and plans the import. The plan lists the trades, the securities and the references each needs. The execute step runs in this order:

  1. Create the missing refdata rows through refdata's operations.
  2. Create each missing security through the security operation.
  3. Book each trade through the booking operation.

The import turns an ORE trade into the request. It maps the ORE trade id to an identifier under the ORE scheme and the envelope to classification rows. It holds no code that writes a part of a trade. It states the origin ore_import and the import id as the origin reference. Its rollback closes whole bookings, and it finds them by the origin reference. The result states how many trades it booked and how many it refused. See the import journey and the export journey. The export reads back what the import wrote.

1.7.4. Synthetic trade booking

The synthetic component generates trades and nothing else. A planner draws a pool of securities and a set of trades from a config. Every trade id is derived from the config, so the plan is the same on every run. The component turns each planned security into a security request and each planned trade into a booking request. It sends them through the client that every caller uses.

Synthetic holds no booking logic. It does not decide the order of writes. It does not detect a half-booked trade, because none exists. A trade is present when its trade id is booked, so a second run books nothing and moves no version. The entry channel is stp. The origin is synthetic and the origin reference is the config id. A report that must exclude generated trades filters on the origin. The service treats the origin like any other data.

1.7.5. Extending synthetic generation

Synthetic generation covers every asset class. A generation config names one asset class, and the asset class decides the trade types and the instrument terms that the plan draws. The generator is extensible: a new asset class adds a planner and changes nothing else.

  • One planner for each asset class. The planner reads the config and draws the plan. The plan holds the securities, if the asset class has any, and the trades. A planner is a pure function of the config and of a snapshot of the references in the config's scope. A shared step reads the snapshot. The planner does not call the network and does not write.
  • One output. Every planner produces the same booking requests and the same security requests that the other callers send. A planner has no private request and no private writer.
  • One writer. A single writer takes a plan and sends it through the shared client. It holds no knowledge of any asset class. The booking service already accepts every instrument kind through the typed batch.
  • Deterministic. Each planner derives every id from the config. A second run books nothing.
  • No change to booking. A new asset class touches no service code, no booking operation and no other planner. The conformance rules apply to every planner without exception.

The shared parts are the writer, the plan type and the id derivation. The parts that differ are the draw from the config and the mapping from a drawn trade to a typed instrument batch. A model can generate the mapping when the instrument writer for every kind is generated.

1.8. Structure booking

A structure is one deal made of several trades. See trade structures and deal composition and the grouping journey. The legs are ordinary trades, and each is booked by the booking operation. The structure is a trade set that adds a product on top of them. The members link the structure to its legs through structure members, and trade links type the relations.

A structure can be arbitrarily large. One transaction that writes every leg in full, with its instrument rows, would grow without limit. The request would grow too. The design therefore does not book a structure in one transaction. It makes each leg atomic, keeps the legs out of risk until the structure is whole, and then makes the whole structure live in one transaction that holds no instrument rows.

1.8.1. The three steps

  1. Book each leg as a draft. A leg is an ordinary trade. The caller books it with the booking operation and the activity draft_capture. The leg is atomic, as every trade is. The caller may book the legs one at a time or in parallel, and a retry of a leg answers already_exists. A draft contributes to no risk and no P&L, so a half-built structure affects no number.
  2. Complete the structure. The caller sends one request that names the structure and lists its leg trade ids and their roles. It states the leg count. The request holds ids only, so its size does not depend on the size of the legs.
  3. Go live in one unit of work. The service checks that every leg exists, that every leg is still a draft held for this structure, and that the count and the roles agree. It then writes the structure, the members and the links. For each leg it writes the next state with the activity execution. All of this commits once. Either every leg is live and the structure exists, or no leg has changed.

The unit of work in step 3 writes small rows, and the instrument rows were written in step 1. Its size still grows with the leg count. The size limit of a message and of a transaction is an open decision.

1.8.2. How this follows the lifecycle

The step uses the trade lifecycle as it is. It adds no state.

  • A leg enters at draft through draft_capture. That is the machine's own entry for a trade that is recorded and not yet agreed. The lifecycle names the case: an agreement that exists with details missing is a draft, told apart by its reason.
  • A leg goes from draft to live through the event Execute, which is the activity execution. The state trigger takes the transition only from the state the leg starts from, so a leg that is not a draft cannot be activated.
  • The legs arrive live and unauthorised, as every live trade does. The authorisation and the confirmation then follow the normal queues.
  • The activity execution is confirmable and economic. A leg that goes live through it would take an external version of its own. A structure is confirmed as a whole and holds the external version, as trade structures requires. How the two fit is an open decision.
  • A caller that gives up discards each draft leg. The machine allows draft to cancelled through Discard. That is a deliberate act and not a repair.

A structure is never visible half-live. Its members do not exist until step 3, and the state of its legs changes in the one transaction.

1.8.3. What a stop leaves

The stop falls State left
Inside a leg booking Nothing of that leg
After some legs, before the completion Whole draft legs and no structure. Each state is valid.
Inside the completion Nothing. The legs stay drafts.
After the completion A whole structure, every leg live

The caller finishes the structure by booking the missing legs again and sending the completion again. Draft legs that wait too long show in the draft report, with their age and their reason.

1.8.4. What this needs

  • A draft reason. The lifecycle states that a draft carries a reason that names who must unblock it. The models have no such field. Structure booking needs a reason such as structure_incomplete, so that a report separates these drafts from a trade that waits for an agreement.
  • A hold on the legs. A draft has a reason code and an optional reason subject. For structure_incomplete the subject is the structure id. The completion refuses a leg whose reason is not structure_incomplete for that structure, so it can never execute a draft that a trader holds on purpose.
  • The completion operation. It owns step 3. It is the only operation that writes members and links for a new structure.

1.8.5. Open decisions

  • Whole or in parts. The three steps suit a strategy that confirms as one deal. A package binds nothing economic, so its legs could go live one by one and be grouped later. The rule for choosing is not set.
  • The external version. The structure table has no column for the external version. The activation must either leave the external version of each leg unmoved and create the structure's version, or use an activity that is not confirmable. The choice is open.
  • The composite trade. A composite trade holds its own legs. A package links separate trades. The two overlap, and a decision must say whether a composite trade becomes a package.
  • The reply. The structure's internal version is read from its legs and is not stored. The completion must return it.

1.9. Open decisions

These decisions are not made. Each one blocks a part of the target. The structure decisions are in the section on structure booking.

  • Size of a structure. A completion request holds leg ids, so it grows with the leg count. A message has a size limit, and a transaction has a practical one. The page does not set a maximum number of legs, and it does not say how a larger structure is split.
  • Four-eyes on booking. If a trade needs approval before it exists, a pending request must live somewhere until a second person approves it. That conflicts with a booking that is whole or absent. The four-eyes framework owns the answer.
  • The word origin. The booking journey lists an origin among the things the trade entity contributes. No field of that name exists. The journey may mean the entry channel, the booking nature or a new field. The journey and this page must agree on one meaning.
  • Reads across owners. Trading checks a book in a trigger, and refdata owns books. The page does not say how trading may read data that another service owns, or what happens when a book is removed between the reference step and the booking.

1.10. Conformance rules

These rules describe the target. A check can enforce each one, and each one can become a task.

  1. One operation creates a trade with its parts, and one operation creates a security. No other operation creates a trade or a security.
  2. The entity operations for a trade's parts act on a booked trade. They add, change or remove a part of a trade that exists, and they refuse a trade that is not booked. They never create a trade.
  3. The booking request carries the identifiers, the classification and the instrument, so that the caller makes one call for each trade.
  4. The service stamps the activity id. A request that states one is refused.
  5. The trading service holds no code and no field that names a caller.
  6. A caller holds no code that checks a trade for missing parts, repairs a trade or orders the writes of its parts.
  7. The security operation is create-only. Its answer already_exists is the caller's proof that the same security is present, and content_differs stops the trade.
  8. A database-backed test proves, for each operation, that a refused part leaves no row.
  9. Every trade states an origin from the lookup. The origin is immutable and no service code branches on it.
  10. A leg of a new structure is booked as a draft. Only the completion operation makes it live, and it does so for all legs in one unit of work.
  11. A database-backed test proves that a refused completion leaves every leg a draft and writes no member.
  12. The service refuses a second booking whose content differs from the first, and it refuses an origin that the authenticated client may not state.

Emacs 29.3 (Org mode 9.6.15)