millistream-mdf 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,560 @@
1
+ Metadata-Version: 2.4
2
+ Name: millistream-mdf
3
+ Version: 0.1.0
4
+ Summary: A high-level, intuitive Python wrapper for the libmdf C SDK, providing easy access to Millistream Data Feed servers for real-time financial data streaming.
5
+ Author-email: Gustav Frison <gustav.frison@millistream.com>
6
+ Keywords: millistream,mdf,data feed,financial data,real-time data
7
+ Requires-Python: >=3.13
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: rich>=14.2.0
10
+
11
+ # millistream-mdf
12
+
13
+ <div align="center">
14
+
15
+ <strong>Python wrapper for the Millistream Data Feed (MDF) C SDK</strong>
16
+
17
+ [![PyPI][pypi-badge]][pypi-url]
18
+ [![Python Version][python-badge]][python-url]
19
+ [![Documentation][docs-badge]][docs-url]
20
+
21
+ [pypi-badge]: https://img.shields.io/pypi/v/millistream-mdf.svg
22
+ [pypi-url]: https://pypi.org/project/millistream-mdf/
23
+ [python-badge]: https://img.shields.io/badge/python-3.13+-blue.svg
24
+ [python-url]: https://pypi.org/project/millistream-mdf/
25
+ [docs-badge]: https://img.shields.io/badge/docs-latest-blue.svg
26
+ [docs-url]: https://packages.millistream.com/documents/
27
+
28
+ </div>
29
+
30
+ ## Table of Contents
31
+
32
+ - [Overview](#overview)
33
+ - [Installation](#installation)
34
+ - [Install Package](#1-install-package)
35
+ - [Install Prerequisites](#2-install-prerequisites)
36
+ - [Quick Start](#quick-start)
37
+ - [API Reference](#api-reference)
38
+ - [MDF Class](#mdf-class)
39
+ - [Message Class](#message-class)
40
+ - [Usage Examples](#usage-examples)
41
+ - [News Streaming](#news-streaming)
42
+ - [Sending Data](#sending-data)
43
+ - [Manual Connection Control](#manual-connection-control)
44
+ - [Available Data Types](#available-data-types)
45
+ - [Request Classes](#request-classes)
46
+ - [Subscription Modes](#subscription-modes)
47
+ - [Error Handling](#error-handling)
48
+ - [Documentation](#documentation)
49
+ - [License](#license)
50
+ - [Support](#support)
51
+
52
+ ## Overview
53
+
54
+ A high-level Python wrapper for the libmdf C SDK, providing access to the Millistream Data Feed (MDF) for real-time financial data streaming.
55
+
56
+ ## Installation
57
+
58
+ ### 1. Install Package
59
+
60
+ Install with **[uv](https://docs.astral.sh/uv/)**:
61
+
62
+ ```bash
63
+ uv add millistream-mdf
64
+ ```
65
+
66
+ Or with **[pip](https://pip.pypa.io/)**:
67
+
68
+ ```bash
69
+ pip install millistream-mdf
70
+ ```
71
+
72
+ ### 2. Install Prerequisites
73
+
74
+ **Ubuntu/Debian**
75
+ ```bash
76
+ python -m millistream_mdf --install-deps
77
+ ```
78
+
79
+ For manual installation, refer to the [official documentation](https://packages.millistream.com/Linux/).
80
+
81
+ **macOS**
82
+
83
+ It is recommended to use the latest [libmdf installer](https://packages.millistream.com/macOS/) to install the necessary dependencies for macOS.
84
+
85
+ > **Note:** Will most likely be named `libmdf-x.x.x.pkg`.
86
+
87
+ **Windows**
88
+
89
+ It is recommended to use the latest [libmdf installer](https://packages.millistream.com/Windows/) to install the necessary dependencies for Windows.
90
+
91
+ >**Note:** Will most likely be named `libmdf-x.x.x.exe`.
92
+
93
+ ## Quick Start
94
+
95
+ ```python
96
+ from millistream_mdf import MDF, RequestClass
97
+
98
+
99
+ with MDF(
100
+ url='sandbox.millistream.com',
101
+ port=9100,
102
+ username='sandbox',
103
+ password='sandbox'
104
+ ) as session:
105
+
106
+ for message in session.subscribe(
107
+ request_classes=[RequestClass.QUOTE], # Subscrive to 'quote' data
108
+ instruments=[1146], # Volvo B
109
+ timeout=1
110
+ ):
111
+ print('raw:', message.fields) # unformatted fields
112
+ print('parsed:', message.parse_fields(remap_keys=True)) # convert types and/or format keys
113
+
114
+ print('---')
115
+ ```
116
+
117
+ or using the **asyncio** API:
118
+
119
+ ```python
120
+ from millistream_mdf import AsyncMDF, RequestClass
121
+ import asyncio
122
+
123
+
124
+ async def main():
125
+
126
+ async with AsyncMDF(
127
+ url='sandbox.millistream.com',
128
+ port=9100,
129
+ username='sandbox',
130
+ password='sandbox'
131
+ ) as session:
132
+
133
+ async for message in session.subscribe(
134
+ request_classes=[RequestClass.QUOTE], # Subscrive to 'quote' data
135
+ instruments=[1146], # Volvo B
136
+ timeout=1
137
+ ):
138
+ print('raw:', message.fields) # unformatted fields
139
+ print('parsed:', message.parse_fields(remap_keys=True)) # convert types and/or format keys
140
+
141
+ print('---')
142
+
143
+ asyncio.run(main())
144
+ ```
145
+
146
+ > **Tip:** You can use `sandbox.millistream.com:9100` for free to test the MDF with username: `sandbox` and password: `sandbox`. The data will be delayed and might not have access to the full offering.
147
+
148
+ > **Tip:** If you only want to convert the types you can use `parse_fields(remap_keys=False, convert_types=[...])`
149
+
150
+ **Example Output:**
151
+ ```
152
+ raw: {5: '272.60', 6: '272.80', 19: '1559', 20: '1988', 7: '272.80', 10: '2139312', 11: '584322621.58', 37: '7116', 8: '275.10', 9: '271.80', 39: '275', 123: '273.07232827', 367: '105139', 368: '28856646.78', 369: None, 370: None, 3: '2025-10-11', 4: '15:29:40'}
153
+ parsed: {'bidprice': 272.6, 'askprice': 272.8, 'bidquantity': 1559.0, 'askquantity': 1988.0, 'lastprice': 272.8, 'quantity': 2139312.0, 'turnover': 584322621.58, 'numtrades': 7116, 'dayhighprice': 275.1, 'daylowprice': 271.8, 'openprice': 275.0, 'vwap': 273.07232827, 'offbookquantity': '105139', 'offbookturnover': '28856646.78', 'darkquantity': None, 'darkturnover': None, 'date': datetime.date(2025, 10, 11), 'time': datetime.time(15, 29, 40)}
154
+ ---
155
+ raw: {20: '31', 4: '15:29:45'}
156
+ parsed: {'askquantity': 31.0, 'time': datetime.time(15, 29, 45)}
157
+ ---
158
+ raw: {19: '4796', 4: '15:29:57'}
159
+ parsed: {'bidquantity': 4796.0, 'time': datetime.time(15, 29, 57)}
160
+ ---
161
+ raw: {19: '3173', 20: '1432', 4: '15:30:02'}
162
+ parsed: {'bidquantity': 3173.0, 'askquantity': 1432.0, 'time': datetime.time(15, 30, 2)}
163
+ ```
164
+
165
+ > **Note:** Only the differences are broadcasted for efficacy. In the example above a full image is broadcasted at the beginning since [`subscription_mode`](#subcriptions-modes) defaults to `full` (`image` + `stream`).
166
+
167
+
168
+ ## API Reference
169
+
170
+ ### MDF Class
171
+
172
+ The main client class for connecting to MDF servers.
173
+
174
+ #### Constructor Parameters
175
+
176
+ | Name | Type | Description | Default |
177
+ |---------------------|-----------------|--------------------------------------------------|------------|
178
+ | `url` | `str` | Server URL | |
179
+ | `port` | `int` | Server port | 9100 |
180
+ | `username` | `str` | Username for authentication | |
181
+ | `password` | `str` | Password for authentication | |
182
+ | `heartbeat_interval`| `int`, `float` | Heartbeat interval in seconds | 30 |
183
+ | `connect_timeout` | `int`, `float` | Connection timeout in seconds | 10 |
184
+ | `tcp_nodelay` | `bool` | Disable TCP Nagle algorithm | True |
185
+ | `no_encryption` | `bool` | Disable encryption | False |
186
+
187
+ #### Attributes
188
+
189
+ All [constructor parameters](#constructor-parameters)
190
+
191
+ #### Properties
192
+
193
+ | Name | Type | Description | Default |
194
+ |---------------------|-----------------|--------------------------------------------------|------------|
195
+ | `is_connected` | `bool` | Whether the client is connected to the server | False |
196
+ | `is_authenticated` | `bool` | Whether the client is authenticated to the server| False |
197
+
198
+ #### Methods
199
+
200
+ ##### `connect()`
201
+ Connect to the MDF server and authenticate.
202
+
203
+ **Raises:**
204
+ - [`MDFConnectionError`](#exception-types): If connection fails
205
+ - [`MDFAuthenticationError`](#exception-types): If authentication fails
206
+
207
+ ##### `disconnect()`
208
+ Disconnect from the MDF server.
209
+
210
+ ##### `subscribe(request_classes, instruments='*', subscription_mode='full', timeout=1)`
211
+ Subscribe to data streams and yield messages.
212
+
213
+ **Parameters:**
214
+ - `request_classes`: List of request classes to subscribe to (e.g., `[RequestClass.QUOTE, RequestClass.TRADE, RequestClass.BASICDATA]`). Can be string names or integer MREF codes
215
+ - `instruments`: Instrument references to subscribe to. Can be `'*'` for all, or numeric IDs (e.g., `[1146, 1147]`)
216
+ - `subscription_mode`: Subscription mode (`'image'`, `'stream'`, or `'full'`). See [`Subscription Modes`](#subcriptions-modes) for more information.
217
+ - `timeout`: Timeout in seconds for consume operations
218
+
219
+ **Returns:** Generator yielding [`Message`](#message-class) objects
220
+
221
+ ##### `stream(timeout=1)`
222
+ Stream messages from the server.
223
+
224
+ **Parameters:**
225
+ - `timeout`: Timeout in seconds for consume operations
226
+
227
+ **Returns:** Generator yielding [`Message`](#message-class) objects
228
+
229
+ ##### `send(mref, instrument, fields, delay=0)`
230
+ Send a single message to the server with specified fields.
231
+
232
+ **Parameters:**
233
+ - `mref`: Message reference (e.g., `MessageReference.QUOTE`, `MessageReference.TRADE`)
234
+ - `instrument`: Instrument reference
235
+ - `fields`: Dictionary mapping field names to values
236
+ - `delay`: Optional delay parameter (default: `0`)
237
+
238
+ **Returns:** `True` if the message was sent successfully
239
+
240
+ **Raises:**
241
+ - [`MDFError`](#exception-types): If not connected or authenticated
242
+ - [`MDFMessageError`](#exception-types): If message construction or sending fails
243
+
244
+ **Example:**
245
+ ```python
246
+ client.send(
247
+ mref=MessageReference.QUOTE,
248
+ instrument=12345,
249
+ fields={
250
+ Field.BIDPRICE: 100.50,
251
+ Field.ASKPRICE: 100.55,
252
+ Field.BIDQUANTITY: 1000,
253
+ Field.ASKQUANTITY: 500
254
+ }
255
+ )
256
+ ```
257
+
258
+ ##### `send_batch(messages)`
259
+ Send multiple messages in a single batch for better efficiency.
260
+
261
+ **Parameters:**
262
+ - `messages`: List of message dictionaries with `'mref'`, `'instrument'`, `'fields'`, and optionally `'delay'`
263
+
264
+ **Returns:** `True` if all messages were sent successfully
265
+
266
+ **Example:**
267
+ ```python
268
+ client.send_batch([
269
+ {
270
+ 'mref': MessageReference.QUOTE,
271
+ 'instrument': 12345,
272
+ 'fields': {Field.BIDPRICE: 100.50, Field.ASKPRICE: 100.55},
273
+ },
274
+ {
275
+ 'mref': MessageReference.TRADE,
276
+ 'instrument': 12345,
277
+ 'fields': {Field.TRADEPRICE: 100.52, Field.TRADEQUANTITY: 1000},
278
+ }
279
+ ])
280
+ ```
281
+
282
+ ##### `create_message_builder()`
283
+ Create a new `MessageBuilder` for advanced message construction.
284
+
285
+ **Returns:** A new `MessageBuilder` instance (must be used as context manager)
286
+
287
+ **Example:**
288
+ ```python
289
+ with client.create_message_builder() as builder:
290
+ builder.add_message(mref=MessageReference.QUOTE, instrument=12345)
291
+ builder.add_field(Field.BIDPRICE, 100.50)
292
+ builder.add_field(Field.ASKPRICE, 100.55)
293
+ builder.send(client._handle)
294
+ ```
295
+
296
+ ### Message Class
297
+
298
+ Represents a message received from the MDF server.
299
+
300
+ #### Attributes
301
+
302
+ | Name | Type | Description | Default | Example |
303
+ |-----------------|----------------------------------------|------------------------------------------------------------------------|-----------|--------------------------------------------------|
304
+ | `ref` | `int` | What type of message it is (e.g. `MessageReference.NEWSHEADLINE`) | | `MessageReference.QUOTE` |
305
+ | `instrument` | `int` | Instrument reference ID | | `12345` |
306
+ | `fields` | `dict[int, str \| None]` | Dictionary of field: value pairs (raw values) | `{}` | `{Field.BIDPRICE: "100.50", Field.ASKPRICE: "100.55"}` |
307
+ | `delay` | `int` | Message delay type | `0` | `0` |
308
+
309
+ #### Properties
310
+
311
+ | Name | Type | Description | Default | Example |
312
+ |-----------------|----------------------------------------|------------------------------------------------------------------------|-----------|--------------------------------------------------|
313
+ | `parsed_fields` | `dict[str \| int, str \| int \| float \| date \| time \| datetime \| list[str]]` | Dictionary of field: value pairs with parsed types | | `{'bidprice': 100.50, 'askprice': 100.55}` |
314
+
315
+ > **Note:** For a list items are always `str`. The type of each item in the list is not guaranteed. For general type casting the type will have to be guessed.
316
+
317
+ #### Methods
318
+
319
+ ##### `parse_fields(remap_keys=True, convert_types=['str', 'int', 'float', 'date', 'time', 'datetime', 'list'], on_field_missing='ignore', list_delimiter=' ')`
320
+ Parse and convert field values to their proper types.
321
+
322
+ **Parameters:**
323
+ - `remap_keys`: If `True`, use lowercase field names as keys; else use field IDs
324
+ - `convert_types`: Which types to convert (`str`, `int`, `float`, `date`, `time`, `datetime`, `list`)
325
+ - `on_field_missing`: How to handle unmapped fields (`'raise'`, `'ignore'`, `'skip'`)
326
+ - `list_delimiter`: Delimiter to split list values on
327
+
328
+ **Returns:** Dictionary with converted values
329
+
330
+ ##### `get(field, default=None)`
331
+ Get field value by name with optional default.
332
+
333
+ ##### `__getitem__(field)`
334
+ Allow dict-like access to fields: `message[Field.BIDPRICE]`
335
+
336
+ ##### `__contains__(field)`
337
+ Check if field exists: `Field.BIDPRICE in message`
338
+
339
+ ## Usage Examples
340
+
341
+ ### News Streaming
342
+
343
+ ```python
344
+ from millistream_mdf import MDF, RequestClass, MessageReference, Field
345
+
346
+ with MDF(
347
+ url='sandbox.millistream.com',
348
+ port=9100,
349
+ username='sandbox',
350
+ password='sandbox'
351
+ ) as session:
352
+
353
+ for message in session.subscribe(
354
+ request_classes=[RequestClass.NEWSHEADLINE, RequestClass.NEWSCONTENT],
355
+ subscription_mode='stream',
356
+ instruments='*',
357
+ timeout=1
358
+ ):
359
+ if message.ref == MessageReference.NEWSHEADLINE:
360
+ print(f"Headline: {message.get(Field.HEADLINE)}")
361
+ print(f"Date: {message.get(Field.DATE)}")
362
+
363
+ elif message.ref == MessageReference.NEWSCONTENT:
364
+ print(f'Content: {message.get(Field.TEXTBODY, 'N/A')[:100]}...')
365
+ print('---')
366
+ ```
367
+
368
+ > **Tip:** You can use `'*'` for all available instruments.
369
+
370
+ > **Note:** [`RequestClass`](#request-classes) and [`MessageReference`](#message-reference) have overlapping names but serve different purposes and have different integer values.
371
+
372
+ **Example Output:**
373
+
374
+ ```
375
+ Headline: Antibiotics Market Size to Surpass USD 55.26 Billion by 2033, Report by DataM Intelligence
376
+ Date: 2025-10-10
377
+ Content: <?xml version="1.0" encoding="UTF-8"?><NewsItem><NewsEnvelope><TransmissionId>202509221001PR_NEWS_EU...
378
+ ---
379
+ Headline: Aventis Energy Confirms Strong Radioactivity at Corvo Uranium Project
380
+ Date: 2025-10-10
381
+ Content: <?xml version="1.0" encoding="UTF-8"?><NewsItem><NewsEnvelope><TransmissionId>A3462546</Transmission...
382
+ ---
383
+ ```
384
+
385
+ > **Note:** Headlines and content are sent in different messages so that the headline can be recieved as quick as possible. You can pair them together using the `Field.NEWSID` field.
386
+
387
+ ### Sending Data
388
+
389
+ ```python
390
+ from millistream_mdf import MDF, MessageReference, Field
391
+
392
+ # Simple message sending
393
+ with MDF(
394
+ url='server.example.com',
395
+ port=9100,
396
+ username='user',
397
+ password='pass'
398
+ ) as client:
399
+
400
+ # Send a single quote message
401
+ client.send(
402
+ mref=MessageReference.QUOTE,
403
+ instrument=12345,
404
+ fields={
405
+ Field.BIDPRICE: 100.50,
406
+ Field.ASKPRICE: 100.55,
407
+ Field.BIDQUANTITY: 1000,
408
+ Field.ASKQUANTITY: 500,
409
+ }
410
+ )
411
+
412
+ # Send multiple messages in a batch (more efficient)
413
+ client.send_batch([
414
+ {
415
+ 'mref': MessageReference.QUOTE,
416
+ 'instrument': 12345,
417
+ 'fields': {Field.BIDPRICE: 100.50, Field.ASKPRICE: 100.55},
418
+ },
419
+ {
420
+ 'mref': MessageReference.TRADE,
421
+ 'instrument': 12345,
422
+ 'fields': {Field.TRADEPRICE: 100.52, Field.TRADEQUANTITY: 1000},
423
+ }
424
+ ])
425
+ ```
426
+
427
+ ### Manual Connection Control
428
+
429
+ ```python
430
+ from millistream_mdf import MDF, MDFError
431
+
432
+ session = MDF(
433
+ url='sandbox.millistream.com',
434
+ port=9100,
435
+ username='sandbox',
436
+ password='sandbox'
437
+ )
438
+
439
+ try:
440
+ session.connect()
441
+ print("Connected!")
442
+
443
+ # Subscribe to quote data
444
+ session.subscribe(request_classes=[RequestClass.QUOTE], instruments='*')
445
+
446
+ # Stream subscribed data
447
+ for message in session.stream(timeout=1):
448
+ print(message.fields)
449
+ print("---")
450
+
451
+ except MDFError as e:
452
+ print(f"Error: {e}")
453
+ finally:
454
+ session.disconnect()
455
+ ```
456
+
457
+ ## Available Data Types
458
+
459
+ ### Request Classes
460
+
461
+ - `NEWSHEADLINE`: News headlines
462
+ - `NEWSCONTENT`: Full news content
463
+ - `QUOTE`: Market quotes (bid/ask)
464
+ - `TRADE`: Trade executions
465
+ - `ORDER`: Order book data
466
+ - `BASICDATA`: Instrument basic information
467
+ - `PRICEHISTORY`: Historical price data
468
+ - `CORPORATEACTION`: Corporate actions
469
+ - `FUNDAMENTALS`: Financial fundamentals
470
+ - `PERFORMANCE`: Performance metrics
471
+ - `KEYRATIOS`: Key financial ratios
472
+ - `ESTIMATES`: Analyst estimates
473
+ - `MIFID`: MiFID II data
474
+ - `GREEKS`: Options Greeks
475
+ - And more...
476
+
477
+ ### Message Reference
478
+
479
+ - `MESSAGESREFERENCE`: Message reference
480
+ - `LOGON`: Logon
481
+ - `LOGOFF`: Logoff
482
+ - `LOGONGREETING`: Logon greeting
483
+ - `NEWSHEADLINE`: News headline
484
+ - `QUOTE`: Quote
485
+ - `TRADE`: Trade
486
+ - `BIDLEVELINSERT`: Bid level insert
487
+ - `ASKLEVELINSERT`: Ask level insert
488
+ - `BIDLEVELDELETE`: Bid level delete
489
+ - `ASKLEVELDELETE`: Ask level delete
490
+ - `BIDLEVELUPDATE`: Bid level update
491
+ - `ASKLEVELUPDATE`: Ask level update
492
+ - `INSTRUMENTRESET`: Instrument reset
493
+ - And [more](https://packages.millistream.com/documents/MDF%20Messages%20Reference.pdf)...
494
+
495
+
496
+ ### Subscription Modes
497
+
498
+ - `image`: Snapshot of current values
499
+ - `stream`: Streaming data only
500
+ - `full`: Both image and stream
501
+
502
+ ## Error Handling
503
+
504
+ The wrapper provides a comprehensive exception hierarchy:
505
+
506
+ ```python
507
+ from millistream_mdf import (
508
+ MDFError,
509
+ MDFConnectionError,
510
+ MDFAuthenticationError,
511
+ RequestClass
512
+ )
513
+
514
+ try:
515
+ with MDF(
516
+ url='sandbox.millistream.com',
517
+ port=9100,
518
+ username='sandbox',
519
+ password='sandbox'
520
+ ) as session:
521
+ for message in session.subscribe(request_classes=[RequestClass.QUOTE], instruments='*'):
522
+ print(message)
523
+
524
+ except MDFConnectionError as e:
525
+ print(f"Connection failed: {e}")
526
+ except MDFAuthenticationError as e:
527
+ print(f"Authentication failed: {e}")
528
+ except MDFError as e:
529
+ print(f"MDF error: {e}")
530
+ ```
531
+
532
+ ### Exception Types
533
+
534
+ - `MDFError`: Base exception for all MDF-related errors
535
+ - `MDFConnectionError`: Connection failures
536
+ - `MDFAuthenticationError`: Login failures
537
+ - `MDFTimeoutError`: Timeout errors
538
+ - `MDFMessageError`: Message operation failures
539
+ - `MDFConfigurationError`: Invalid configuration
540
+ - `MDFLibraryError`: Underlying library errors
541
+
542
+
543
+ ## Documentation
544
+
545
+ For more detailed documentation, visit the [official documentation](https://packages.millistream.com/documents/) or the [millistream sandbox](https://sandbox.millistream.com/sandbox/mdf).
546
+
547
+
548
+ ## License
549
+
550
+ This wrapper is provided under the LGPL v3 license, the same as the underlying libmdf library.
551
+
552
+ ## Support
553
+
554
+ For issues with this Python wrapper:
555
+
556
+ 1. Check this documentation
557
+ 2. Check error messages and exception types
558
+ 3. [Open an issue](https://github.com/mint273/millistream-mdf/issues) on GitHub
559
+
560
+ For *libmdf* library issues, refer to the official Millistream documentation or contact tech@millistream.com.