Research: YU08; Execution Algo Engine
Decision 1; a new execution-algo-engine component, warm-path, not a BLP featureβ
order-matcher IS the BLP: a single-threaded, no-GC, in-memory matching core (LMAX-BLP.md)
journaled and snapshotted for crash recovery. Slicing a parent order over a multi-second-to-minute
time window is stateful scheduling work with no place on that thread; it would mean either
blocking the matching loop on a timer (unacceptable: the BLP's whole design point is that nothing
but sequenced input events touches it) or bolting an unrelated second responsibility onto the one
component this project treats as sacrosanct. A plain Spring Boot service; the same shape as
account-service/trade-processor, not order-matcher's Disruptor/journal machinery; is the right
altitude: it owns parent-order scheduling and submits child orders exactly like any other REST
client of the existing order-entry surface.
Decision 2; children submit through order-matcher's existing POST /orders, unchangedβ
POST /orders (OrderCreateRequest: clientOrderId, accountId, security, side, quantity,
limitPrice) is the same endpoint the web front end's order ticket already calls
(order-ticket.component.ts). It already carries the full YU03 two-tier admission path; in-process
Gateway replica screening, then the BLP's authoritative sequenced accept/reject; with no gateway
bypass and no new endpoint. execution-algo-engine is simply one more caller of this endpoint;
order-matcher gains no new code, no new field, no special case for algo-sourced orders. Batch
ingress (POST /orders/batch) is not used; each child is submitted individually as its bucket
comes due, so the risk gateway's per-order idempotency/reservation bookkeeping applies exactly as it
does to a manually entered order.
Decision 3; child limit price: last price with a small aggressive offsetβ
POST /orders requires a positive limitPrice (OrderMatcherService.validateOrderCreateRequest
rejects a null/non-positive one); there is no "market order" verb on this path (that exists only
for MarketTradeRequest/POST /trades, a different flow this state does not use). Before
submitting each child, execution-algo-engine fetches the current price from
GET /prices/{ticker} on price-publisher (the same tick a screen would show) and offsets it by
10 bps against the order's side; Buy at price Γ 1.001, Sell at price Γ 0.999; so the
order is aggressively marketable against the resting book rather than a passive limit that might
never cross. 10bps sits far inside the default 50% price collar
(risk.price.collar-bps=5000) and the price is fetched immediately before submission, so it is
always within the default 30s freshness window (risk.price.max-age-ms); no risk-gateway
rejection path is exercised by this offset under normal operation.
Decision 4; own state is event-sourced over a dedicated JetStream stream, reusing the existing clientβ
Crash recovery must resume a parent order's schedule and observed fills without re-deriving them
from scratch. YU04 already established the pattern this project uses for durable, replayable state:
a JetStream stream plus the io.nats:jnats:2.20.5 client every JVM service in this lineage already
depends on (JetStreamControlFeedPublisher, ControlFeedSubscriber). execution-algo-engine reuses
the identical client and stream-bootstrap idiom for a new stream, TRADERX_ALGO_ENGINE (subject
algo.events.>, file storage): every state transition (ParentOrderCreated, ChildOrderSubmitted,
ChildOrderFillObserved, ParentOrderCompleted) is appended as one JSON message before it is
reflected in the in-memory model. On every boot, a fresh ephemeral pull consumer
(DeliverPolicy.All, AckPolicy.None) replays the entire stream from the start and rebuilds every
parent order, then the same subscription keeps delivering new events live; the same consumer
serves both roles, so there is no separate "replay mode" versus "live mode" to keep in sync. No ack
bookkeeping is needed because applying an event is a deterministic function of current state
(each event fully replaces the affected bucket/order's fields rather than incrementing a counter),
so a full replay from the start is correct regardless of when a crash happened. (A durable named
consumer with explicit acks was tried first and rejected once live kind verification surfaced the
gap: acking permanently advances that consumer's position, so a restart would only replay whatever
was left unacked; any parent order that had already fully completed before the restart would be
silently missing from the rebuilt state, contradicting FR-AE08's "every parent order." See ADR-030.)
This is deliberately not a database table: this state has no existing datastore of its own to add one to, and the append-only, replay-to-rebuild shape is exactly what JetStream already gives for free; a new table plus ORM would duplicate infrastructure the project already runs for the same purpose.
Decision 5; fill tracking: catch-all NATS subscribe + client-side filter, no new subjectβ
/accounts/<accountId>/orders already broadcasts every order lifecycle event
(orderId, status, remainingQuantity, limitPrice, lastExecutionPrice) to multiple
subscribers (the frontend blotter stream); no new subject, no publisher change.
execution-algo-engine cannot use a NATS token wildcard to select every account's subject the way
pricing.* does for tick-store (YU07 Decision 2), because these subjects use a literal / rather
than NATS's .-delimited hierarchy; order-matcher publishes the literal string
"/accounts/" + accountId + "/orders" (NatsBridgeHandler), so the entire string is one NATS
token and * cannot be embedded inside it (jnats's subject validator rejects
"/accounts/*/orders" outright: "Subject wildcard improperly placed"; discovered live during this
state's kind verification, not from documentation). Instead, execution-algo-engine subscribes to
NATS's full match-all subject (">") and filters client-side by subject prefix/suffix
(/accounts/ ... /orders); the only correct way to select every account's order subject given
the existing publisher's non-hierarchical naming, at the cost of also receiving every other subject
on the connection and discarding non-matches.
Every message on this bus is wrapped in a NatsEnvelope (topic/payload/date/from/type; messaging/nats/NatsJSONPublisher, the concrete Publisher<T> every JVM publisher on this branch
uses, including order-matcher's NatsBridgeHandler); the actual OrderResponse fields live under
payload, not the envelope's top level; also found live, not from documentation, since nothing in
this project's spec packs had previously needed to describe the wire format from a new
subscriber's point of view. execution-algo-engine unwraps payload before reading orderId etc.
Every submitted child order's returned orderId (from the synchronous POST /orders
response) is recorded in an in-memory orderId -> (parentOrderId, bucketIndex) index; an incoming
lifecycle event whose orderId matches updates that bucket's fill state and appends a
ChildOrderFillObserved event.
clientOrderId is still set on every child request (<parentOrderId>:<bucketIndex>) for
idempotency and audit-trail readability in order-matcher's own logs and risk-gateway idempotency
table, but OrderResponse (the payload rendered onto /orders//accounts/*/orders) does not carry
clientOrderId today; it is built from the output-ring's zero-allocation rendering path
(OrderResponse.from(...), Tier 2-C, order-matcher/api/OrderResponse.java), a hot-path structure
this state does not touch. orderId-based correlation via the algo-engine's own submit-time index
is sufficient for this state's own progress tracking without adding a field to that path.
Decision 6; TWAP: equal-quantity time bucketsβ
A parent order (accountId, security, side, quantity, durationSeconds, bucketSeconds)
slices into ceil(durationSeconds / bucketSeconds) buckets of floor(quantity / bucketCount) each,
with the remainder from integer division added to the last bucket so every share is scheduled
exactly once. bucketSeconds defaults to 10 (within the 5-30s range that keeps a manual demo run
watchable) and is caller-overridable.
Decision 7; VWAP: pluggable volume-profile source, synthetic by defaultβ
VolumeProfileSource.bucketWeights(security, bucketCount) returns a list of non-negative weights
summing to 1, one per bucket, used instead of TWAP's equal split. Two implementations:
SyntheticVolumeProfileSource(default,algo.volume-profile.source=synthetic): a deterministic U-shaped intraday curve; weight at bucketiofnis1 + 4 Γ ((i / (n-1) β 0.5))Β²before normalization, the textbook shape of higher participation near the open/close and a lull mid-session. Needs no external data, so VWAP has no data dependency at all when this source is selected.DuckDbVolumeProfileSource(algo.volume-profile.source=duckdb): runs one DuckDB query (viaorg.duckdb:duckdb_jdbc, the same DuckDB engine YU07'stick-storeuses from Python, here from Java) over YU07's unifiedticksParquet store (read_parquet('<algo.volume-profile.duckdb.path>/**/*.parquet', hive_partitioning=true)), groupingevent_type='trade'rows for the requestedsecurityby intraday time bucket to produce a historical volume fraction per bucket. Bulk OneDrive β GCS TAQ ingestion is still blocked (rclone pending university IT approval; CLI-triggered local hydration is a confirmed dead end; YU07's own handoff) and there is not yet enough captured live-trade volume to shape a profile either. Rather than block VWAP on that unblocking, this source falls back toSyntheticVolumeProfileSource's weights whenever the query returns zero matching rows for the requested security, logging that it did so; VWAP is fully usable today and starts reading real volume automatically the moment enough TAQ or live-trade data exists atgs://traderx-501015-tick-store, with no code change required to pick it up.
Decision 8; REST-only parent-order ingress, no front-end panelβ
A parent order is submitted via POST /algo/orders on execution-algo-engine itself (not
order-matcher; this is the algo engine's own control surface, separate from the child-order
ingress in Decision 2). GET /algo/orders/{parentOrderId} and GET /algo/orders expose progress
(schedule, buckets submitted, quantity filled, status). No web-front-end panel is added in this
state; a REST client (curl, run-state-kind's smoke check) is the only caller.
Generation; one new component, one shared-file appendβ
execution-algo-engine is a new directory in the shared component tree, like tick-store before
it; no ancestor state has a file at this path to conflict with. The one shared file this state
modifies is kubernetes-runtime/manifests/base/kustomization.yaml, already overridden by every
ancestor through YU07 (each appends its own resource entries): this state's copy starts from YU07's
current version and appends execution-algo-engine-deployment.yaml +
execution-algo-engine-service.yaml, never replacing it. Verified empirically post-generation (see
generation/implementation-status.md): every ancestor's resource entry, including YU07's own
tick-store-deployment.yaml, survives alongside the two new entries.