diff --git a/examples/websocket/options-admin.py b/examples/websocket/options-admin.py new file mode 100644 index 00000000..ce2e0469 --- /dev/null +++ b/examples/websocket/options-admin.py @@ -0,0 +1,18 @@ +from massive import WebSocketClient +from massive.websocket.models import WebSocketMessage, Market +from typing import List + +client = WebSocketClient(market=Market.Options, verbose=True) + +# Options Admin / FLEX messages +client.subscribe("AD.*") # all options exchanges +# client.subscribe("AD.302") +# client.subscribe("AD.323") + + +def handle_msg(msgs: List[WebSocketMessage]): + for m in msgs: + print(m) + + +client.run(handle_msg) diff --git a/massive/websocket/models/__init__.py b/massive/websocket/models/__init__.py index 629aca87..9eeac9d1 100644 --- a/massive/websocket/models/__init__.py +++ b/massive/websocket/models/__init__.py @@ -30,6 +30,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "FromDictProtocol": "Q": EquityQuote, "FMV": FairMarketValue, "LV": LaunchpadValue, + "AD": OptionsAdmin, }, Market.Indices: { "A": EquityAgg, diff --git a/massive/websocket/models/common.py b/massive/websocket/models/common.py index 7efa836e..cbfef733 100644 --- a/massive/websocket/models/common.py +++ b/massive/websocket/models/common.py @@ -54,6 +54,7 @@ class EventType(Enum): LaunchpadValue = "LV" LaunchpadAggMin = "AM" BusinessFairMarketValue = "FMV" + OptionsAdmin = "AD" FuturesTrade = "T" FuturesQuote = "Q" FuturesAgg = "A" diff --git a/massive/websocket/models/models.py b/massive/websocket/models/models.py index 6b3acd90..88cbc9cc 100644 --- a/massive/websocket/models/models.py +++ b/massive/websocket/models/models.py @@ -374,6 +374,31 @@ def from_dict(d): ) +@modelclass +class OptionsAdmin: + "OptionsAdmin contains an OPRA administrative message, such as a FLEX trade report, for a given options exchange. The message is the raw OPRA text, kept as-is since its format varies by exchange. The timestamp is the SIP timestamp in Unix nanoseconds, and retransmission is only set (True) on retransmitted messages." + + event_type: Optional[Union[str, EventType]] = None + id: Optional[str] = None + exchange: Optional[int] = None + message: Optional[str] = None + timestamp: Optional[int] = None + sequence_number: Optional[int] = None + retransmission: Optional[bool] = None + + @staticmethod + def from_dict(d): + return OptionsAdmin( + event_type=d.get("ev", None), + id=d.get("i", None), + exchange=d.get("x", None), + message=d.get("m", None), + timestamp=d.get("t", None), + sequence_number=d.get("q", None), + retransmission=d.get("rt", None), + ) + + @modelclass class FuturesTrade: event_type: Optional[str] = None @@ -470,6 +495,7 @@ def from_dict(d): IndexValue, LaunchpadValue, FairMarketValue, + OptionsAdmin, FuturesTrade, FuturesQuote, FuturesAgg, diff --git a/test_websocket/test_options_admin.py b/test_websocket/test_options_admin.py new file mode 100644 index 00000000..218112da --- /dev/null +++ b/test_websocket/test_options_admin.py @@ -0,0 +1,59 @@ +import json +import unittest +from massive.websocket import logger +from massive.websocket.models import parse, Market, OptionsAdmin + +# frames captured verbatim from wss://socket.massive.com/options (AD.*) +SINGLE_FRAME = '[{"ev":"AD","i":"1794331349111117097-2270200655","t":1794331349111117097,"q":2270200655,"x":300,"m":"FLEX E MDB LST 127888 240 P EUR 12.18.26 631 @ 6 2MDB"}]' +MULTI_FRAME = '[{"ev":"AD","i":"1794331356867045949-2270938732","t":1794331356867045949,"q":2270938732,"x":300,"m":"FLEX E PG LST 96699 135 P EUR 12.18.26 1122 @ 0.05 2PG"},{"ev":"AD","i":"1794331356867503288-2270938735","t":1794331356867503288,"q":2270938735,"x":300,"m":"FLEX E PG LST 97132 135 C EUR 12.18.26 1122 @ 35.25 2PG"}]' + + +class OptionsAdminTest(unittest.TestCase): + def test_parse_single(self): + msgs = parse(json.loads(SINGLE_FRAME), logger, Market.Options) + self.assertEqual( + msgs, + [ + OptionsAdmin( + event_type="AD", + id="1794331349111117097-2270200655", + exchange=300, + message="FLEX E MDB LST 127888 240 P EUR 12.18.26 631 @ 6 2MDB", + timestamp=1794331349111117097, + sequence_number=2270200655, + retransmission=None, + ) + ], + ) + + def test_parse_multi(self): + msgs = parse(json.loads(MULTI_FRAME), logger, Market.Options) + self.assertEqual( + msgs, + [ + OptionsAdmin( + event_type="AD", + id="1794331356867045949-2270938732", + exchange=300, + message="FLEX E PG LST 96699 135 P EUR 12.18.26 1122 @ 0.05 2PG", + timestamp=1794331356867045949, + sequence_number=2270938732, + retransmission=None, + ), + OptionsAdmin( + event_type="AD", + id="1794331356867503288-2270938735", + exchange=300, + message="FLEX E PG LST 97132 135 C EUR 12.18.26 1122 @ 35.25 2PG", + timestamp=1794331356867503288, + sequence_number=2270938735, + retransmission=None, + ), + ], + ) + + def test_parse_retransmission(self): + d = json.loads(SINGLE_FRAME) + d[0]["rt"] = True + msgs = parse(d, logger, Market.Options) + self.assertEqual(msgs[0].retransmission, True) # type: ignore