# Enterprise FIX 4.4 market-data profile

Profile: `depthfeed-market-data-v1`. Implementation preview; the public REST and
WebSocket endpoints remain the currently deployed interfaces until a FIX listener
and account CompIDs have been provisioned. Do not guess a FIX hostname or port.

## What the interface delivers

Aggregated order-book bids/offers for concrete Binance spot/futures, Polymarket,
Kalshi, Limitless and supported sports book channels. Clients receive an initial
snapshot and price-level changes, with full replacements after a detected upstream
sequence gap. This is useful for an existing institutional FIX market-data adapter,
book builder or research/risk system.

REST remains the discovery and historical-data interface. This FIX profile does not
implement execution, order entry, cancels/replaces, balances, historical tick replay,
SecurityList, trade prints, underlying `price:*` channels, or wildcard subscriptions.
It does not increase capture coverage or make an older recorded book fresher.

## Access and session contract

| Setting | Contract |
|---|---|
| Transport | TCP with verified TLS 1.2 or newer; FIX 4.4 tag-value messages |
| Endpoint | Individually supplied during enterprise provisioning |
| SenderCompID (49) | Your assigned CompID; one connected socket per CompID |
| TargetCompID (56) | Supplied server CompID; example `DEPTHFEED` |
| Logon (35=A) | `98=0`, `108=5..30`, `554=<account API key>` |
| Authorization | Live enterprise plan and API key; CompID bound to your account ID |
| Plan name | Desk maps to internal `enterprise`; multiplied grants are preserved |
| Revalidation | Every 25 seconds; 5-second request deadline; failed authorization closes the connection |
| Session recovery | Persistent incoming/outgoing sequence numbers; `ResendRequest` returns gap fills |

TLS protects the API key; FIX `98=0` means no additional FIX-layer encryption.
Verify the server certificate and hostname. Never put a key in a command line,
session config, source control or raw FIX log. Supply tag 554 through your FIX
engine's logon callback using a secret source. Account browser JWTs are not accepted.

An ordinary API authorization cache does not grant FIX access. `GET /v3/fix/session`
rechecks the key and current account plan, expiry and status without stale fallback.
The authoritative PostgreSQL account store is required: a missing/revoked key,
expiry, downgrade, store failure or failed database connection at startup never
falls back to a legacy key row. Legacy-only keys must first become account-managed
keys to use this new interface; their existing REST access is unchanged. The endpoint returns a short-lived normalized
stream ticket and effective limits; it is control-plane authorization, not a 1,000 RPS
data grant. API 401/403/429/5xx or timeouts fail closed at the gateway.

Each FIX session consumes one normal account WebSocket connection and its normal
subscription allowance. Sports caps still apply across the upstream account's
connections. Existing REST grants, including 200 RPS where already granted, remain
unchanged. Session authorization calls use the account's normal REST request budget.

## Instruments: Symbol (55)

Use identifiers from REST discovery, preserving their case. No fuzzy symbol lookup
or cross-venue contract equivalence is implied.

| Book | FIX Symbol |
|---|---|
| Binance | `book:binance:btc:spot` or `book:binance:btc:futures` |
| Polymarket crypto | `book:polymarket:btc:<market_id>#up` or `#down` |
| Kalshi crypto | `book:kalshi:btc:<ticker>#yes` or `#no` |
| Limitless | `book:limitless:btc:<slug>` |
| Polymarket sports | `book:sports:polymarket:<outcome_token_id>` |
| Kalshi sports | `book:sports:kalshi:<ticker>#yes` or `#no` |

Other supported coins use their existing WebSocket coin segment. A Polymarket
sports token already identifies the outcome, so it has no `#up/#down` suffix.
For Kalshi, yes offers are `1 - no bid`; no offers are `1 - yes bid`. Sports feeds
already publish a yes-side bid/offer book; the no side is its exact decimal
complement with sides exchanged. These conversions do not infer equivalent
contracts between venues. Prediction prices are probabilities in `[0,1]`, not cents.
Binance retains the normalized pair's price units; quantities retain upstream units.

## MarketDataRequest (35=V)

One concrete instrument per request; use distinct IDs for multiple instruments.
All fields below are part of this profile, including on cancellation where required
by the FIX 4.4 dictionary.

| Tag | Meaning | Value |
|---|---|---|
| 262 | MDReqID | Unique active ID, 1..64 characters |
| 263 | SubscriptionRequestType | `0` one snapshot; `1` snapshot plus updates; `2` cancel |
| 264 | MarketDepth | `1..1000` levels **per side**; `0` unlimited is unsupported |
| 265 | MDUpdateType | `1` incremental; required for subscription type 1 |
| 266 | AggregatedBook | `Y` when supplied |
| 267 | NoMDEntryTypes | `2` |
| 269 | MDEntryType | Repeating entries `0` bid and `1` offer |
| 146 | NoRelatedSym | `1` |
| 55 | Symbol | A concrete instrument from the table above |

Human-readable example (`|` stands for ASCII SOH; engines calculate 9/10/34/52):

```text
35=V|262=btc-spot-1|263=1|264=10|265=1|266=Y|267=2|269=0|269=1|146=1|55=book:binance:btc:spot|
```

MarketDepth is a maximum, not a promise that the capture contains that many levels.
A full snapshot replaces the available normalized depth; it does not certify that
the entire venue ladder was captured.

The first response is a complete normalized `W`. There is no separate successful subscribe
message. If no complete book arrives within 15 seconds, `Y` rejects that request;
check discovery and current feed coverage before retrying. Cancellation uses the
original MDReqID and a well-formed `V` with `263=2`; it stops future processing of
that request, without retracting messages already queued on the wire. Unknown
cancel IDs and duplicate active IDs are rejected. Reuse an ID after cancellation
only after accounting for any in-flight messages, or choose a new ID.

## Building and maintaining the book

`35=W` (MarketDataSnapshotFullRefresh) includes `262`, `55`, and `268` groups with
`269` side, `270` price, `271` quantity and, when present in the normalized feed,
`272` UTC source date / `273` UTC source time with milliseconds. Replace the entire
book for that request. `268=0` explicitly represents an empty book.

FIX 4.4 does not put MDEntryID in the `W` group. For this profile, assign every
snapshot level the deterministic ID:

```text
<Symbol>|<side>:<canonical decimal price>
book:binance:btc:spot|0:100.5
```

The pipe inside the computed ID is a literal character, not a FIX field delimiter;
the actual wire delimiter is ASCII SOH.

Subscription controls are coalesced at 500ms intervals into acknowledged upstream
batches, with removals before additions. This preserves the existing WebSocket
30-operations/10-seconds safeguard while allowing a normal multi-market FIX
startup. Data updates are not batched by this scheduler. A bad symbol in an atomic
upstream batch rejects its own FIX request; valid neighbours are retried. The
upstream must acknowledge an application ping before FIX Logon is accepted.

Canonical decimals remove insignificant trailing fractional zeros and exponent
notation. There is no binary-float conversion in this adapter. Precision already
lost upstream cannot be recovered. IDs are scoped by the full Symbol, so equal
prices on different instruments cannot collide. A client whose FIX library requires
explicit IDs in snapshots must implement this documented snapshot-ID convention.

`35=X` (MarketDataIncrementalRefresh) includes `262` and `268` groups:

| Tag | Meaning |
|---|---|
| 279 | `0` new, `1` change, `2` delete |
| 269 | Side: `0` bid, `1` offer |
| 278 | Deterministic level ID from the convention above |
| 55 | Full Symbol on every update group |
| 270 | Canonical decimal price |
| 271 | Full replacement quantity for new/change; absent on delete |
| 272/273 | Normalized source timestamp when available |

Apply updates by ID and side/price, not by arrival position. Delete removes a level;
change replaces its size. A depth-limited book includes deletions when a level leaves
the visible depth and new entries when another enters it. Unchanged books emit no
empty incrementals. A later `W` always replaces the previous book, even during an
active subscription.

`35=Y` rejects a market-data request with MDReqID, `281` reason and a useful `58`
message. Standard reasons distinguish unknown symbols, duplicate IDs, capacity,
unsupported depth (5), update type (6), aggregated-book mode (7) and entry types (8).
Invalid fields can receive a session reject identifying the field instead.
Unsupported application messages, including orders, receive a business
reject (`35=j`) or a dictionary/session reject for malformed messages. No order is
forwarded to a venue.

## Gaps, reconnects and timestamps

Do not apply market data beyond an unresolved FIX sequence gap. Use your engine's
standard sequence recovery. Old market-data messages are deliberately not journaled;
`ResendRequest (35=2)` receives `SequenceReset/GapFill (35=4,123=Y)`. The next complete
normalized frame produces a fresh `W` for each active request. Wait for those snapshots
before treating the book as complete; missing books time out with a request reject.
An upstream per-channel sequence gap also triggers a full replacement. A backwards
or invalid upstream sequence closes the connection rather than extending a bad chain.

After any disconnect or service restart, reconnect with exponential backoff and
resubscribe with new MDReqIDs. Subscriptions are not persisted. Sequence numbers are
persisted; there is no automatic reset on logon/logout/disconnect. Authenticated
`141=Y` explicitly requests a sequence reset and is honored; coordinate this with
your counterparty rather than enabling it on every reconnect.

A slow reader is disconnected when its bounded queues or socket deadline are
exceeded. Invalidate its local book and rebuild after reconnecting. No arbitrary
market-data messages are silently dropped while continuing the same delta chain.

FIX header `52` is the gateway's sending timestamp. Entry `272/273` preserves the
timestamp supplied by the normalized data; it may be a capture/store timestamp,
not the venue's physical event time. Missing source time is omitted, never replaced
with the current gateway time. Measure client receipt using a synchronized client
clock and report timestamp provenance and clock uncertainty. Local fixture timings,
server write completion and stored-record age are not end-user network latency.

## Profile safety limits

In addition to account entitlements: 1,000 active request IDs maximum, 40,000 retained
depth slots per session (sum of twice each requested depth), 64 KiB inbound FIX body,
100 inbound messages/second, 32 pending application FIX messages, 64 queued normalized
frames and 8 MiB of queued normalized JSON. Individual normalized frames are capped
at 4 MiB; upstream sides at 10,000 price levels. Socket writes have a 5-second
deadline; unauthenticated reads have a 10-second deadline. The deployment's separate
connection cap defaults to 16 and is configurable only within 1..128. These are safety
bounds, not promised sustained throughput or additional account grants. Initial
logons have a separate service-wide burst of eight attempts, replenishing at two
attempts/second; at most four account authorization requests run concurrently.
Reconnect with backoff when an admission attempt is closed.

## Third-party acknowledgment

This product includes software developed by
quickfixengine.org (http://www.quickfixengine.org/).

