Skip to content

Commit eea9815

Browse files
userFRMclaude
andauthored
fix(server): return ThetaData stream-verification values from the websocket bridge (#807)
* fix(server): return ThetaData stream-verification values from the websocket bridge The bundled WebSocket bridge acknowledged every stream request with `OK`, which is not one of the stream-request verification values ThetaData clients match on. A successful subscribe, unsubscribe, or stop now acknowledges with `SUBSCRIBED`. It also returned `OK` when a subscribe arrived before streaming had started, even though nothing was installed. That was a false-positive acknowledgement: the client believed it was subscribed while no feed existed. That path now returns `ERROR` with a message saying streaming is not started. The acknowledgement value is built from a small internal enum so the wire strings live in one place. ThetaData also documents `MAX_STREAMS_REACHED` and `INVALID_PERMS`, but the upstream FPSS error type does not carry enough to tell those apart from a generic failure, so both currently fall through to `ERROR` with a descriptive message; the specific values can be emitted once the error type surfaces the category. Closes #801 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(server): the websocket strike field is in dollars, not thousandths The websocket bridge parses the option strike as dollars, but the docs still showed thousandths (570000) and called it the one exception to the dollars-everywhere rule, so a client following the docs would subscribe to the wrong contract. Show the strike as a dollar number. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: the websocket strike envelope is dollars across the streaming docs The strike-dollars change made the bundled server's websocket subscribe envelope take dollars, but the per-endpoint streaming option docs and the symbology page still showed thousandths (570000) and called the websocket the one exception to dollars-everywhere. Correct the examples and prose so a websocket client sends the dollar value. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(docs): emit the websocket strike envelope in dollars from the generator The streaming-options doc generator hard-coded the websocket subscribe example with the strike as a scaled integer (570000) and prose calling it thousandths, so a client copying the published envelope subscribed to a $570,000 strike instead of the $570 option. The server's own subscribe path takes dollars, so the docs contradicted the code. Emit the strike in dollars (570) from the three option stream specs and rewrite the envelope note to match. Invert the docs-consistency guard so it bans the thousandths vocabulary and the 570000 literal on every page that carries a websocket subscribe envelope, instead of whitelisting the thousandths note as an allowed exception. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(server): publish stream-verification tokens and make WS STOP apply The WebSocket REQ_RESPONSE and DISCONNECTED frames rendered their outcome via the Rust Debug form, putting internal variant identifiers (`Subscribed`, `MaxStreamsReached`, `InvalidPerms`, and the `RemoveReason` names) onto the wire where clients expect the documented stream-verification vocabulary. Give `StreamResponseType` and `RemoveReason` an explicit `as_wire_str` mapping (the single source of `SUBSCRIBED` / `ERROR` / `MAX_STREAMS_REACHED` / `INVALID_PERMS` and the SCREAMING_SNAKE disconnect tokens) and emit that, so the variant identifier can never reach a client. `StreamResponseType` also gains a `Display` impl routed through the same mapping. STOP previously acknowledged `SUBSCRIBED` unconditionally — even when streaming was never started or no stream existed — which told the client its feed was gone when nothing changed. STOP is a removal, not a status query, so it now snapshots the live subscription set, removes it, and acknowledges success only when the removal actually applied; an inactive session or a failed unsubscribe surfaces `ERROR` with a diagnostic, matching the subscribe path's discipline. Correct the index price streaming doc example, which subscribed `Contract.stock("SPX")` for an index; SPX is an index, so the example now uses `Contract.index("SPX")` to match the other language rows. Regenerate the checked-in docs page from the corrected source. Unit tests assert each enum variant maps to its exact wire token (the regression guard against the Debug leak), the STOP error/success token contract, and that no variant identifier appears in the serialized frame. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent dcbcce6 commit eea9815

13 files changed

Lines changed: 519 additions & 60 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6969
- The CLI's raw OHLC output (`--format json-raw` / `csv`) emits the `vwap` value in its own column. The raw value row was one field short of its header set, so `vwap` was dropped and `date`, `expiration`, `strike`, and `right` rendered under the wrong column names. The typed SDK surfaces and the CLI's presentation `json` were unaffected.
7070
- Python — the standalone `StreamingClient` streaming connect, reconnect, and subscribe / unsubscribe paths release the GIL across their blocking I/O (the TLS connect and handshake, and the per-subscription wire write), so other Python threads keep running while a connect or subscribe is in flight; the typed exception raised on failure is unchanged.
7171
- The standalone Python and TypeScript streaming clients now forward the full streaming and reconnect config, so every tuning knob — including the wait strategy and consumer-core affinity, host selection, watchdog and keepalive cadences, and the reconnect backoff and replay pacing — is honored, matching the unified client and the C ABI.
72+
- The bundled WebSocket server acknowledges stream requests with ThetaData's stream-verification values: a successful subscribe, unsubscribe, or stop now returns `SUBSCRIBED` instead of `OK`, and a subscribe that arrives before streaming has started returns `ERROR` with a descriptive message instead of a false-positive `OK` that claimed success while installing nothing.
7273

7374
### Security
7475

crates/thetadatadx/build_support_bin/endpoints/docs_render/streaming.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ const STREAMS: &[StreamSpec] = &[
200200
cpp_sub: "thetadatadx::Contract::option(\"SPY\", {.expiration = \"20260618\", .strike = \"570\", .right = \"C\"}).quote()",
201201
ws_req_type: "QUOTE",
202202
ws_sec_type: "OPTION",
203-
ws_contract: Some(r#"{"symbol": "SPY", "expiration": 20260618, "strike": 570000, "right": "C"}"#),
203+
ws_contract: Some(r#"{"symbol": "SPY", "expiration": 20260618, "strike": 570, "right": "C"}"#),
204204
group: "Options",
205205
label: "Quote",
206206
},
@@ -216,7 +216,7 @@ const STREAMS: &[StreamSpec] = &[
216216
cpp_sub: "thetadatadx::Contract::option(\"SPY\", {.expiration = \"20260618\", .strike = \"570\", .right = \"C\"}).trade()",
217217
ws_req_type: "TRADE",
218218
ws_sec_type: "OPTION",
219-
ws_contract: Some(r#"{"symbol": "SPY", "expiration": 20260618, "strike": 570000, "right": "C"}"#),
219+
ws_contract: Some(r#"{"symbol": "SPY", "expiration": 20260618, "strike": 570, "right": "C"}"#),
220220
group: "Options",
221221
label: "Trade",
222222
},
@@ -232,7 +232,7 @@ const STREAMS: &[StreamSpec] = &[
232232
cpp_sub: "thetadatadx::Contract::option(\"SPY\", {.expiration = \"20260618\", .strike = \"570\", .right = \"C\"}).open_interest()",
233233
ws_req_type: "OPEN_INTEREST",
234234
ws_sec_type: "OPTION",
235-
ws_contract: Some(r#"{"symbol": "SPY", "expiration": 20260618, "strike": 570000, "right": "C"}"#),
235+
ws_contract: Some(r#"{"symbol": "SPY", "expiration": 20260618, "strike": 570, "right": "C"}"#),
236236
group: "Options",
237237
label: "Open Interest",
238238
},
@@ -275,7 +275,7 @@ const STREAMS: &[StreamSpec] = &[
275275
prose: "Streams every index value update. Indices publish price prints through the trade feed, so each update delivers a `Trade` event whose `price` field carries the index value. Indices have no full-stream broadcast; subscribe per index.",
276276
event: "Trade",
277277
rust_sub: "Contract::index(\"SPX\").trade()",
278-
python_sub: "Contract.stock(\"SPX\").trade()",
278+
python_sub: "Contract.index(\"SPX\").trade()",
279279
ts_sub: "Contract.index('SPX').trade()",
280280
cpp_sub: "thetadatadx::Contract::index(\"SPX\").trade()",
281281
ws_req_type: "TRADE",
@@ -417,7 +417,7 @@ fn http_tab(spec: &StreamSpec) -> String {
417417
);
418418
if spec.ws_contract.is_some_and(|c| c.contains("strike")) {
419419
out.push_str(
420-
"\nThe WebSocket envelope takes the strike in thousandths of a dollar (`570000` = $570.00); the SDKs take dollars.\n",
420+
"\nThe WebSocket envelope takes the strike in dollars (`570` = $570.00), the same as the SDKs.\n",
421421
);
422422
}
423423
out

crates/thetadatadx/src/tdbe/types/enums.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,33 @@ pub enum StreamResponseType {
603603
InvalidPerms = 3,
604604
}
605605

606+
impl StreamResponseType {
607+
/// Stream-request verification token this outcome is published as on
608+
/// the WebSocket wire.
609+
///
610+
/// The wire vocabulary is fixed by the stream-verification contract
611+
/// every client matches on: `SUBSCRIBED` / `ERROR` /
612+
/// `MAX_STREAMS_REACHED` / `INVALID_PERMS`. This mapping is the single
613+
/// source of those tokens, so the Rust variant identifier can never
614+
/// reach the wire — emitting the debug form of the variant would break
615+
/// every client written against the documented vocabulary.
616+
#[must_use]
617+
pub const fn as_wire_str(self) -> &'static str {
618+
match self {
619+
Self::Subscribed => "SUBSCRIBED",
620+
Self::Error => "ERROR",
621+
Self::MaxStreamsReached => "MAX_STREAMS_REACHED",
622+
Self::InvalidPerms => "INVALID_PERMS",
623+
}
624+
}
625+
}
626+
627+
impl core::fmt::Display for StreamResponseType {
628+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
629+
f.write_str(self.as_wire_str())
630+
}
631+
}
632+
606633
/// Disconnect reason codes.
607634
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
608635
#[repr(i16)]
@@ -699,6 +726,37 @@ impl RemoveReason {
699726
Self::InvalidCredentialsNullUser => "InvalidCredentialsNullUser",
700727
}
701728
}
729+
730+
/// Stable SCREAMING_SNAKE_CASE token published as the disconnect
731+
/// `reason` on the WebSocket wire.
732+
///
733+
/// This mapping is the single source of the wire reason vocabulary, so
734+
/// the Rust variant identifier can never reach a client: emitting the
735+
/// debug form of the variant would leak an internal type name in place
736+
/// of the documented status token.
737+
#[must_use]
738+
pub const fn as_wire_str(self) -> &'static str {
739+
match self {
740+
Self::Unspecified => "UNSPECIFIED",
741+
Self::InvalidCredentials => "INVALID_CREDENTIALS",
742+
Self::InvalidLoginValues => "INVALID_LOGIN_VALUES",
743+
Self::InvalidLoginSize => "INVALID_LOGIN_SIZE",
744+
Self::GeneralValidationError => "GENERAL_VALIDATION_ERROR",
745+
Self::TimedOut => "TIMED_OUT",
746+
Self::ClientForcedDisconnect => "CLIENT_FORCED_DISCONNECT",
747+
Self::AccountAlreadyConnected => "ACCOUNT_ALREADY_CONNECTED",
748+
Self::SessionTokenExpired => "SESSION_TOKEN_EXPIRED",
749+
Self::InvalidSessionToken => "INVALID_SESSION_TOKEN",
750+
Self::FreeAccount => "FREE_ACCOUNT",
751+
Self::TooManyRequests => "TOO_MANY_REQUESTS",
752+
Self::NoStartDate => "NO_START_DATE",
753+
Self::LoginTimedOut => "LOGIN_TIMED_OUT",
754+
Self::ServerRestarting => "SERVER_RESTARTING",
755+
Self::SessionTokenNotFound => "SESSION_TOKEN_NOT_FOUND",
756+
Self::ServerUserDoesNotExist => "SERVER_USER_DOES_NOT_EXIST",
757+
Self::InvalidCredentialsNullUser => "INVALID_CREDENTIALS_NULL_USER",
758+
}
759+
}
702760
}
703761

704762
/// Market-calendar day classification.
@@ -783,3 +841,74 @@ impl std::fmt::Display for CalendarStatus {
783841
}
784842

785843
include!("generated/enums_endpoint.rs");
844+
845+
#[cfg(test)]
846+
mod wire_token_tests {
847+
use super::{RemoveReason, StreamResponseType};
848+
849+
/// Every stream-response outcome maps to its exact stream-verification
850+
/// token. This is the single source the WebSocket surface publishes,
851+
/// so a drift here would leak the Rust variant name onto the wire and
852+
/// break clients matching on the documented vocabulary.
853+
#[test]
854+
fn stream_response_wire_tokens_are_exact() {
855+
assert_eq!(StreamResponseType::Subscribed.as_wire_str(), "SUBSCRIBED");
856+
assert_eq!(StreamResponseType::Error.as_wire_str(), "ERROR");
857+
assert_eq!(
858+
StreamResponseType::MaxStreamsReached.as_wire_str(),
859+
"MAX_STREAMS_REACHED"
860+
);
861+
assert_eq!(
862+
StreamResponseType::InvalidPerms.as_wire_str(),
863+
"INVALID_PERMS"
864+
);
865+
// `Display` routes through the same mapping.
866+
assert_eq!(StreamResponseType::Subscribed.to_string(), "SUBSCRIBED");
867+
}
868+
869+
/// Every disconnect reason maps to a stable SCREAMING_SNAKE token; the
870+
/// Rust variant identifier must never reach the wire.
871+
#[test]
872+
fn remove_reason_wire_tokens_are_exact() {
873+
for (reason, token) in [
874+
(RemoveReason::Unspecified, "UNSPECIFIED"),
875+
(RemoveReason::InvalidCredentials, "INVALID_CREDENTIALS"),
876+
(RemoveReason::InvalidLoginValues, "INVALID_LOGIN_VALUES"),
877+
(RemoveReason::InvalidLoginSize, "INVALID_LOGIN_SIZE"),
878+
(
879+
RemoveReason::GeneralValidationError,
880+
"GENERAL_VALIDATION_ERROR",
881+
),
882+
(RemoveReason::TimedOut, "TIMED_OUT"),
883+
(
884+
RemoveReason::ClientForcedDisconnect,
885+
"CLIENT_FORCED_DISCONNECT",
886+
),
887+
(
888+
RemoveReason::AccountAlreadyConnected,
889+
"ACCOUNT_ALREADY_CONNECTED",
890+
),
891+
(RemoveReason::SessionTokenExpired, "SESSION_TOKEN_EXPIRED"),
892+
(RemoveReason::InvalidSessionToken, "INVALID_SESSION_TOKEN"),
893+
(RemoveReason::FreeAccount, "FREE_ACCOUNT"),
894+
(RemoveReason::TooManyRequests, "TOO_MANY_REQUESTS"),
895+
(RemoveReason::NoStartDate, "NO_START_DATE"),
896+
(RemoveReason::LoginTimedOut, "LOGIN_TIMED_OUT"),
897+
(RemoveReason::ServerRestarting, "SERVER_RESTARTING"),
898+
(
899+
RemoveReason::SessionTokenNotFound,
900+
"SESSION_TOKEN_NOT_FOUND",
901+
),
902+
(
903+
RemoveReason::ServerUserDoesNotExist,
904+
"SERVER_USER_DOES_NOT_EXIST",
905+
),
906+
(
907+
RemoveReason::InvalidCredentialsNullUser,
908+
"INVALID_CREDENTIALS_NULL_USER",
909+
),
910+
] {
911+
assert_eq!(reason.as_wire_str(), token, "{reason:?}");
912+
}
913+
}
914+
}

docs-site/docs/articles/symbology.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ An option contract is always the four-tuple **(symbol, expiration, strike, right
2222

2323
Strikes are **human dollars everywhere on the SDK surface** — never scaled integers. Endpoints that take an optional `strike` / `right` treat omission as a wildcard: all strikes, both rights. Wildcard responses identify each row's contract via the `expiration`, `strike`, and `right` response fields.
2424

25-
The one place a scaled strike survives: the server's [WebSocket subscribe envelope](/server/websocket) takes thousandths of a dollar (`570000` = $570.00), matching the upstream wire convention.
25+
Strikes are dollars across every surface, including the bundled server's [WebSocket subscribe envelope](/server/websocket) (`570` = $570.00). The scaled-integer form only exists on the raw upstream wire, which the SDK and the server both hide.
2626

2727
## Dates and times in
2828

docs-site/docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6969
- The CLI's raw OHLC output (`--format json-raw` / `csv`) emits the `vwap` value in its own column. The raw value row was one field short of its header set, so `vwap` was dropped and `date`, `expiration`, `strike`, and `right` rendered under the wrong column names. The typed SDK surfaces and the CLI's presentation `json` were unaffected.
7070
- Python — the standalone `StreamingClient` streaming connect, reconnect, and subscribe / unsubscribe paths release the GIL across their blocking I/O (the TLS connect and handshake, and the per-subscription wire write), so other Python threads keep running while a connect or subscribe is in flight; the typed exception raised on failure is unchanged.
7171
- The standalone Python and TypeScript streaming clients now forward the full streaming and reconnect config, so every tuning knob — including the wait strategy and consumer-core affinity, host selection, watchdog and keepalive cadences, and the reconnect backoff and replay pacing — is honored, matching the unified client and the C ABI.
72+
- The bundled WebSocket server acknowledges stream requests with ThetaData's stream-verification values: a successful subscribe, unsubscribe, or stop now returns `SUBSCRIBED` instead of `OK`, and a subscribe that arrives before streaming has started returns `ERROR` with a descriptive message instead of a false-positive `OK` that claimed success while installing nothing.
7273

7374
### Security
7475

docs-site/docs/server/websocket.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,19 @@ The server bridges [streaming](/streaming/) onto a local WebSocket at `ws://127.
2828
| `id` | Your request id; echoed in the acknowledgement |
2929
| `contract` | Omit for `FULL_*` streams |
3030

31-
Option contracts carry the four-tuple, with the strike in **thousandths of a dollar** (the one wire-format exception to the dollars-everywhere rule — see [Symbology](/articles/symbology)):
31+
Option contracts carry the four-tuple, with the strike in **dollars** (a JSON number, e.g. `570` or `570.0`):
3232

3333
```json
34-
{"symbol": "SPY", "expiration": 20250321, "strike": 570000, "right": "C"}
34+
{"symbol": "SPY", "expiration": 20250321, "strike": 570, "right": "C"}
3535
```
3636

37-
`{"msg_type": "STOP", "id": 2}` removes every active stream at once. Each command is acknowledged:
37+
`{"msg_type": "STOP", "id": 2}` removes every active stream at once. Each command is acknowledged with a stream-request verification value in the `response` field:
3838

3939
```json
40-
{ "header": { "type": "REQ_RESPONSE", "response": "OK", "req_id": 1 } }
40+
{ "header": { "type": "REQ_RESPONSE", "response": "SUBSCRIBED", "req_id": 1 } }
4141
```
4242

43-
Invalid commands answer with `"response": "ERROR"` and a message naming the offending field.
43+
`SUBSCRIBED` confirms the request was accepted; it acknowledges subscribe (`add: true`), unsubscribe (`add: false`), and `STOP`, since there is no removal-specific value. Rejected commands answer with `"response": "ERROR"` and an `error` field naming the cause — a bad envelope, an offending field, or a subscribe sent before streaming has started (which installs nothing, so it is never acknowledged as a success). The values `MAX_STREAMS_REACHED` and `INVALID_PERMS` are also part of the verification vocabulary.
4444

4545
## Event messages
4646

@@ -50,7 +50,7 @@ Events arrive as JSON with a `header.type` of `QUOTE`, `TRADE`, `OHLC`, or `OPEN
5050

5151
```bash
5252
websocat ws://127.0.0.1:25520/v1/events
53-
{"msg_type": "STREAM", "sec_type": "OPTION", "req_type": "TRADE", "id": 1, "add": true, "contract": {"symbol": "SPY", "expiration": 20250321, "strike": 570000, "right": "C"}}
53+
{"msg_type": "STREAM", "sec_type": "OPTION", "req_type": "TRADE", "id": 1, "add": true, "contract": {"symbol": "SPY", "expiration": 20250321, "strike": 570, "right": "C"}}
5454
```
5555

5656
## Limits

docs-site/docs/streaming/indices/price.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def on_event(event):
4545

4646
client.stream.start_streaming(on_event)
4747

48-
sub = Contract.stock("SPX").trade()
48+
sub = Contract.index("SPX").trade()
4949
client.stream.subscribe(sub)
5050

5151
# Remove this stream; the session stays open for other subscriptions.

docs-site/docs/streaming/options/open-interest.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,10 @@ WebSocket streaming from the bundled [server binary](/server/). Send one JSON en
106106

107107
```bash
108108
websocat ws://127.0.0.1:25520/v1/events
109-
{"msg_type": "STREAM", "sec_type": "OPTION", "req_type": "OPEN_INTEREST", "id": 1, "add": true, "contract": {"symbol": "SPY", "expiration": 20260618, "strike": 570000, "right": "C"}}
109+
{"msg_type": "STREAM", "sec_type": "OPTION", "req_type": "OPEN_INTEREST", "id": 1, "add": true, "contract": {"symbol": "SPY", "expiration": 20260618, "strike": 570, "right": "C"}}
110110
```
111111

112-
The WebSocket envelope takes the strike in thousandths of a dollar (`570000` = $570.00); the SDKs take dollars.
112+
The WebSocket envelope takes the strike in dollars (`570` = $570.00), the same as the SDKs.
113113

114114
</template>
115115

docs-site/docs/streaming/options/quote.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,10 @@ WebSocket streaming from the bundled [server binary](/server/). Send one JSON en
106106

107107
```bash
108108
websocat ws://127.0.0.1:25520/v1/events
109-
{"msg_type": "STREAM", "sec_type": "OPTION", "req_type": "QUOTE", "id": 1, "add": true, "contract": {"symbol": "SPY", "expiration": 20260618, "strike": 570000, "right": "C"}}
109+
{"msg_type": "STREAM", "sec_type": "OPTION", "req_type": "QUOTE", "id": 1, "add": true, "contract": {"symbol": "SPY", "expiration": 20260618, "strike": 570, "right": "C"}}
110110
```
111111

112-
The WebSocket envelope takes the strike in thousandths of a dollar (`570000` = $570.00); the SDKs take dollars.
112+
The WebSocket envelope takes the strike in dollars (`570` = $570.00), the same as the SDKs.
113113

114114
</template>
115115

docs-site/docs/streaming/options/trade.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,10 @@ WebSocket streaming from the bundled [server binary](/server/). Send one JSON en
106106

107107
```bash
108108
websocat ws://127.0.0.1:25520/v1/events
109-
{"msg_type": "STREAM", "sec_type": "OPTION", "req_type": "TRADE", "id": 1, "add": true, "contract": {"symbol": "SPY", "expiration": 20260618, "strike": 570000, "right": "C"}}
109+
{"msg_type": "STREAM", "sec_type": "OPTION", "req_type": "TRADE", "id": 1, "add": true, "contract": {"symbol": "SPY", "expiration": 20260618, "strike": 570, "right": "C"}}
110110
```
111111

112-
The WebSocket envelope takes the strike in thousandths of a dollar (`570000` = $570.00); the SDKs take dollars.
112+
The WebSocket envelope takes the strike in dollars (`570` = $570.00), the same as the SDKs.
113113

114114
</template>
115115

0 commit comments

Comments
 (0)