btgsolutions-dataservices-python-client 2.11.4__py3-none-any.whl → 2.17.1__py3-none-any.whl

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.
@@ -26,6 +26,7 @@ SECURITIES = 'securities'
26
26
  TRADES = 'trades'
27
27
  PROCESSEDTRADES = 'processed-trades'
28
28
  INSTRUMENTSTATUS = 'instrument_status'
29
+ SETTLEMENTPRICES = 'settlement-price'
29
30
  BOOKS = 'books'
30
31
  INDICES = 'indices'
31
32
  CANDLES1S = 'candles-1S'
@@ -49,7 +50,8 @@ VALID_MARKET_DATA_TYPES = [
49
50
  CANDLES1S,
50
51
  CANDLES1M,
51
52
  STOPLOSS,
52
- INSTRUMENTSTATUS
53
+ INSTRUMENTSTATUS,
54
+ SETTLEMENTPRICES
53
55
  ]
54
56
  VALID_MARKET_DATA_SUBTYPES = [ALL, STOCKS, OPTIONS, DERIVATIVES]
55
57
 
@@ -134,6 +136,11 @@ market_data_socket_urls = {
134
136
  DERIVATIVES: f"{url_ws}v2/marketdata/{INSTRUMENTSTATUS}/{DERIVATIVES}",
135
137
  OPTIONS: f"{url_ws}v2/marketdata/{INSTRUMENTSTATUS}/{OPTIONS}",
136
138
  }
139
+ },
140
+ SETTLEMENTPRICES: {
141
+ REALTIME: {
142
+ ALL: f"{url_ws}v2/marketdata/{SETTLEMENTPRICES}",
143
+ }
137
144
  }
138
145
  },
139
146
  BMV: {
@@ -9,4 +9,6 @@ from .ticker_last_event import TickerLastEvent
9
9
  from .corporate_events import CorporateEvents
10
10
  from .company_data import CompanyData
11
11
  from .public_sources import PublicSources
12
- from .reference_data import ReferenceData
12
+ from .reference_data import ReferenceData
13
+ from .stock_loan import StockLoan
14
+ from .ticker_last_event_polling import TickerLastEventPolling
@@ -58,7 +58,8 @@ class IntradayCandles:
58
58
  start:int=0,
59
59
  end:int=0,
60
60
  mode:str='absolute',
61
- raw_data:bool=False
61
+ raw_data:bool=False,
62
+ cross_filter:str='',
62
63
  ):
63
64
  """
64
65
  This method provides realtime intraday candles for a given ticker.
@@ -93,6 +94,10 @@ class IntradayCandles:
93
94
  Candle mode.
94
95
  Example: 'absolute', 'relative' or 'spark'.
95
96
  Default: absolute.
97
+ cross_filter: str
98
+ Filter trades by cross status. Available only when market_type is derivatives.
99
+ Options: 'all', 'only_cross' or 'without_cross'.
100
+ Default: 'all'.
96
101
  raw_data: bool
97
102
  If false, returns data in a dict of dataframes. If true, returns raw data.
98
103
  Default: False.
@@ -101,6 +106,8 @@ class IntradayCandles:
101
106
  if market_type not in ['stocks', 'derivatives', 'options', 'indices']: raise MarketTypeError(f"Must provide a valid 'market_type' parameter. Input: '{market_type}'. Accepted values: 'stocks', 'derivatives', 'options' or 'indices'.")
102
107
 
103
108
  if delay not in ['delayed', 'realtime']: raise DelayError(f"Must provide a valid 'delay' parameter. Input: '{delay}'. Accepted values: 'delayed' or 'realtime'.")
109
+
110
+ if cross_filter and market_type != 'derivatives': raise DelayError(f"The 'cross_filter' flag is only valid for market_type 'derivatives'.")
104
111
 
105
112
  tickers = ','.join(tickers) if type(tickers) is list else tickers
106
113
 
@@ -109,6 +116,8 @@ class IntradayCandles:
109
116
  if start: url += f'&start={start}'
110
117
 
111
118
  if end: url += f'&end={end}'
119
+
120
+ if cross_filter: url += f'&cross_filter={cross_filter}'
112
121
 
113
122
  response = requests.request("GET", url, headers=self.headers)
114
123
  if response.status_code == 200:
@@ -0,0 +1,110 @@
1
+
2
+ from typing import Optional
3
+ from ..exceptions import BadResponse
4
+ import requests
5
+ from ..config import url_api_v1
6
+ import json
7
+ from .authenticator import Authenticator
8
+ import pandas as pd
9
+
10
+ class StockLoan:
11
+ """
12
+ This class provides trades related to stock loan operations.
13
+
14
+ * Main use case:
15
+
16
+ >>> from btgsolutions_dataservices import StockLoan
17
+ >>> stock_loan = StockLoan(
18
+ >>> api_key='YOUR_API_KEY',
19
+ >>> )
20
+ >>> trades = stock_loan.get_trades(
21
+ >>> ticker = 'PETR4'
22
+ >>> )
23
+ >>> trades = stock_loan.get_paginated_trades(
24
+ >>> page = 1,
25
+ >>> limit = 1000
26
+ >>> )
27
+ >>> stock_loan.get_available_tickers()
28
+
29
+ Parameters
30
+ ----------------
31
+ api_key: str
32
+ User identification key.
33
+ Field is required.
34
+ """
35
+ def __init__(
36
+ self,
37
+ api_key: Optional[str]
38
+ ):
39
+ self.api_key = api_key
40
+ self.token = Authenticator(self.api_key).token
41
+ self.headers = {"authorization": f"authorization {self.token}"}
42
+
43
+ def get_trades(
44
+ self,
45
+ ticker:Optional[str]='',
46
+ ):
47
+ """
48
+ Returns trades related to stock loan operations.
49
+
50
+ Parameters
51
+ ----------------
52
+ ticker : str, optional
53
+ The ticker symbol to be returned.
54
+ Example: 'PETR4'
55
+
56
+ """
57
+ page = 1
58
+ df = pd.DataFrame()
59
+ while True:
60
+ url = f"{url_api_v1}/marketdata/stock-loan/daily-trades?page={page}&limit=20000"
61
+ if ticker: url += f'&ticker={ticker}'
62
+ response = requests.request("GET", url, headers=self.headers)
63
+ if response.status_code != 200: raise BadResponse(response.json())
64
+ response = response.json()
65
+ df = pd.concat([df, pd.DataFrame(response['data'])])
66
+ if response['totalPages'] <= page: break
67
+ page += 1
68
+
69
+ return df.reset_index(drop=True)
70
+
71
+ def get_paginated_trades(
72
+ self,
73
+ page:int,
74
+ limit:int,
75
+ ticker:Optional[str]='',
76
+ ):
77
+ """
78
+ Returns paginated trades related to stock loan operations.
79
+
80
+ Parameters
81
+ ----------------
82
+ ticker : str, optional
83
+ The ticker symbol to be returned.
84
+ Example: 'PETR4'
85
+
86
+ page : int
87
+ Page number for paginated results.
88
+ Example: 1
89
+
90
+ limit : int
91
+ Maximum number of items to return per page.
92
+ Example: 1000
93
+
94
+ """
95
+ url = f"{url_api_v1}/marketdata/stock-loan/daily-trades?page={page}&limit={limit}"
96
+ if ticker: url += f'&ticker={ticker}'
97
+ response = requests.request("GET", url, headers=self.headers)
98
+ if response.status_code == 200: return json.loads(response.text)
99
+ raise BadResponse(response.json())
100
+
101
+ def get_available_tickers(
102
+ self,
103
+ ):
104
+ """
105
+ This method provides all tickers available for query.
106
+ """
107
+ url = f"{url_api_v1}/marketdata/stock-loan/available-tickers"
108
+ response = requests.request("GET", url, headers=self.headers)
109
+ if response.status_code == 200: return json.loads(response.text)
110
+ raise BadResponse(response.json())
@@ -1,4 +1,4 @@
1
- from typing import Optional
1
+ from typing import Optional, List
2
2
  from ..exceptions import BadResponse
3
3
  import requests
4
4
  from ..config import url_api_v1
@@ -18,10 +18,16 @@ class TickerLastEvent:
18
18
  >>> )
19
19
 
20
20
  >>> last_event.get_trades(
21
+ >>> data_type = 'equities',
21
22
  >>> ticker = 'PETR4',
22
23
  >>> raw_data = False
23
24
  >>> )
24
25
 
26
+ >>> last_event.get_tob(
27
+ >>> data_type = 'stocks',
28
+ >>> raw_data = False
29
+ >>> )
30
+
25
31
  Parameters
26
32
  ----------------
27
33
  api_key: str
@@ -33,10 +39,12 @@ class TickerLastEvent:
33
39
  api_key:Optional[str]
34
40
  ):
35
41
  self.api_key = api_key
36
- self.token = Authenticator(self.api_key).token
37
- self.headers = {"authorization": f"authorization {self.token}"}
42
+ self.authenticator = Authenticator(self.api_key)
38
43
 
39
- self.available_data_types = ['equities', 'derivatives']
44
+ self.available_data_types = {
45
+ "trades": ['equities', 'derivatives'],
46
+ "books": ['stocks', 'derivatives', 'options']
47
+ }
40
48
 
41
49
  def get_trades(self, data_type:str, ticker:str, raw_data:bool=False):
42
50
 
@@ -56,12 +64,12 @@ class TickerLastEvent:
56
64
  If false, returns data in a dataframe. If true, returns raw data.
57
65
  Field is not required. Default: False.
58
66
  """
59
- if data_type not in self.available_data_types:
60
- raise Exception("Must provide a valid data_type. Valid data types are: {self.available_data_types}")
67
+ if data_type not in self.available_data_types["trades"]:
68
+ raise Exception(f"Must provide a valid data_type. Valid data types are: {self.available_data_types['trades']}")
61
69
 
62
70
  url = f"{url_api_v1}/marketdata/last-event/trades/{data_type}?ticker={ticker}"
63
71
 
64
- response = requests.request("GET", url, headers=self.headers)
72
+ response = requests.request("GET", url, headers={"authorization": f"Bearer {self.authenticator.token}"})
65
73
  if response.status_code == 200:
66
74
  if raw_data:
67
75
  return response.json()
@@ -70,23 +78,100 @@ class TickerLastEvent:
70
78
  else:
71
79
  response = json.loads(response.text)
72
80
  raise BadResponse(f'Error: {response.get("error", "")}')
81
+
82
+ def get_tobs(self, data_type:str, raw_data:bool=False):
83
+
84
+ """
85
+ This method provides the last available top of book for all tickers of the given market type.
86
+
87
+ Parameters
88
+ ----------------
89
+ data_type: str
90
+ Market Data Type.
91
+ Field is required.
92
+ Example: 'stocks', 'derivatives', 'options'.
93
+ raw_data: bool
94
+ If false, returns data in a dataframe. If true, returns raw data.
95
+ Field is not required. Default: False.
96
+ """
97
+
98
+ if data_type not in self.available_data_types["books"]:
99
+ raise Exception(f"Must provide a valid data_type. Valid data types are: {self.available_data_types}")
100
+
101
+ url = f"{url_api_v1}/marketdata/last-event/books/top/{data_type}/batch"
102
+
103
+ response = requests.request("GET", url, headers={"authorization": f"Bearer {self.authenticator.token}"})
104
+
105
+ if response.status_code == 200:
106
+ if raw_data:
107
+ return response.json()
108
+ else:
109
+ return pd.DataFrame(response.json())
110
+ else:
111
+
112
+ response = json.loads(response.text)
113
+ raise BadResponse(f'Error: {response.get("error", "")}')
73
114
 
74
- def get_available_tickers(self, data_type:str):
115
+ def get_status(self, tickers:List[str]=None, raw_data:bool=False):
116
+
117
+ """
118
+ This method provides the current ticker trading status information.
119
+
120
+ Parameters
121
+ ----------------
122
+ tickers: List[str]
123
+ Ticker symbol.
124
+ Field is not required. Default: None
125
+ Example: ['PETR4', 'VALE3', 'DOLM25'].
126
+ If no ticker is provided, it returns trading status information for all available tickers.
127
+ raw_data: bool
128
+ If false, returns data in a dataframe. If true, returns raw data.
129
+ Field is not required. Default: False.
130
+ """
131
+
132
+ if tickers:
133
+ tickers = ','.join(tickers)
134
+ url = f"{url_api_v1}/marketdata/last-event/status/all/batch?tickers={tickers}"
135
+ else:
136
+ url = f"{url_api_v1}/marketdata/last-event/status/all/batch"
137
+
138
+ response = requests.request("GET", url, headers={"authorization": f"Bearer {self.authenticator.token}"})
139
+ if response.status_code == 200:
140
+ if raw_data:
141
+ return response.json()
142
+ else:
143
+ return pd.DataFrame(response.json())
144
+ else:
145
+ response = json.loads(response.text)
146
+ raise BadResponse(f'Error: {response}')
147
+
148
+ def get_available_tickers(self,type:str, data_type:str):
75
149
 
76
150
  """
77
151
  This method provides all the available tickers for the specific data type.
78
152
 
79
153
  Parameters
80
154
  ----------------
155
+ type: str
156
+ Data Type.
157
+ Field is required.
158
+ Example: 'trades', 'books'
81
159
  data_type: str
82
160
  Market Data Type.
83
161
  Field is required.
84
- Example: 'equities', 'derivatives'.
162
+ Example: 'equities', 'derivatives', 'options', 'stocks'.
85
163
  """
86
164
 
87
- url = f"{url_api_v1}/marketdata/last-event/trades/{data_type}/available-tickers"
165
+ if type not in self.available_data_types:
166
+ raise Exception(f"Must provide a valid type. Valid data types are: {list(self.available_data_types.keys())}")
167
+
168
+ if data_type not in self.available_data_types[type]:
169
+ raise Exception(f"Must provide a valid data_type. Valid data types are: {self.available_data_types['books']}")
170
+
171
+ url = f"{url_api_v1}/marketdata/last-event/trades/{data_type}/available-tickers" if type == "trades" else \
172
+ f"{url_api_v1}/marketdata/last-event/books/{data_type}/availables"
88
173
 
89
- response = requests.request("GET", url, headers=self.headers)
174
+ response = requests.request("GET", url, headers={"authorization": f"Bearer {self.authenticator.token}"})
90
175
  if response.status_code == 200:
91
176
  return response.json()
92
177
  else:
@@ -0,0 +1,149 @@
1
+ import time
2
+ from typing import Optional, List
3
+ from ..exceptions import BadResponse
4
+ import requests
5
+ from ..config import url_api_v1
6
+ from .authenticator import Authenticator
7
+ import pandas as pd
8
+ import json
9
+ from datetime import datetime, timezone, timedelta
10
+ import threading
11
+
12
+ class TickerLastEventPolling:
13
+ """
14
+ This class continuously polls and caches the latest ticker market data in the background for quick and up-to-date access.
15
+
16
+ * Main use case:
17
+
18
+ >>> from btgsolutions_dataservices import TickerLastEventPolling
19
+ >>> last_event = TickerLastEventPolling(
20
+ >>> api_key='YOUR_API_KEY',
21
+ >>> data_type='top-of-books',
22
+ >>> data_subtype='stocks',
23
+ >>> )
24
+
25
+ >>> last_event.get(
26
+ >>> raw_data=False
27
+ >>> )
28
+
29
+ Parameters
30
+ ----------------
31
+ api_key: str
32
+ User identification key.
33
+ Field is required.
34
+ data_type: str
35
+ Market Data type.
36
+ Options: 'top-of-books'
37
+ Field is required.
38
+ data_subtype: str
39
+ Market Data subtype.
40
+ Options: 'stocks', 'options', 'derivatives'.
41
+ Field is required.
42
+ """
43
+ def __init__(
44
+ self,
45
+ api_key:Optional[str],
46
+ data_type: str,
47
+ data_subtype: str,
48
+ interval_seconds: float = 1.0
49
+ ):
50
+ self.api_key = api_key
51
+ self.authenticator = Authenticator(self.api_key)
52
+ self.interval_seconds = interval_seconds
53
+ self.data_type = data_type
54
+
55
+ self._last_request_datetime = None
56
+ self._cache = {}
57
+
58
+ self._available_data_types = {
59
+ "top-of-books": ['stocks', 'derivatives', 'options']
60
+ }
61
+
62
+ self._available_url = {
63
+ "top-of-books": {
64
+ "data": f"{url_api_v1}/marketdata/last-event/books/top/{data_subtype}/batch",
65
+ "available": f"{url_api_v1}/marketdata/last-event/books/{data_subtype}/availables"
66
+ }
67
+ }
68
+
69
+ if data_type not in self._available_data_types:
70
+ raise Exception(f"Must provide a valid data_type. Valid data types are: {self._available_data_types}")
71
+
72
+ if data_subtype not in self._available_data_types[data_type]:
73
+ raise Exception(f"Must provide a valid data_subtype. Valid data subtypes are: {self._available_data_types[data_type]}")
74
+
75
+ self.url = self._available_url[self.data_type]["data"]
76
+
77
+ self._update_data()
78
+
79
+ threading.Thread(target=self._polling_loop, daemon=True).start()
80
+
81
+ def _polling_loop(self):
82
+
83
+ while True:
84
+ try:
85
+ self._update_data()
86
+ except Exception as e:
87
+ print("error on updating data:", e)
88
+ continue
89
+
90
+ time.sleep(self.interval_seconds)
91
+
92
+
93
+ def _update_data(self):
94
+ url = self.url + (f"?dt={(self._last_request_datetime - timedelta(seconds=60)).strftime('%Y-%m-%dT%H:%M:%S.000Z')}" if self._last_request_datetime else "")
95
+
96
+ request_datetime = datetime.now(timezone.utc)
97
+
98
+ response = requests.request("GET", url, headers={"authorization": f"Bearer {self.authenticator.token}"})
99
+
100
+ if response.status_code != 200:
101
+ return
102
+
103
+
104
+ self._last_request_datetime = request_datetime
105
+
106
+ new_data = { tob["sb"]: tob for tob in response.json() if tob.get("sb")}
107
+
108
+ self._cache.update(new_data)
109
+
110
+
111
+ def get(self, force_update: bool=False, raw_data:bool=False):
112
+
113
+ """
114
+ This method provides the last events for all tickers of the given data type and data subtype.
115
+
116
+ Parameters
117
+ ----------------
118
+ force_update: bool
119
+ If true, forces an update before returning the data. If false, returns the data.
120
+ Field is not required. Default: False.
121
+ raw_data: bool
122
+ If false, returns data in a dataframe. If true, returns raw data.
123
+ Field is not required. Default: False.
124
+ """
125
+ if force_update:
126
+ self._update_data()
127
+
128
+ if raw_data:
129
+ return list(self._cache.values())
130
+ else:
131
+ return pd.DataFrame(self._cache.values())
132
+
133
+
134
+
135
+ def get_available_tickers(self):
136
+
137
+ """
138
+ This method provides all the available tickers for the specific data type and data subtype.
139
+
140
+ """
141
+
142
+ url = self._available_url[self.data_type]["available"]
143
+
144
+ response = requests.request("GET", url, headers={"authorization": f"Bearer {self.authenticator.token}"})
145
+ if response.status_code == 200:
146
+ return response.json()
147
+ else:
148
+ response = json.loads(response.text)
149
+ raise BadResponse(f'Error: {response.get("error", "")}')
@@ -1,19 +1,30 @@
1
1
  from typing import Optional, Callable, List
2
2
  import websocket
3
3
  import time
4
- from datetime import date
4
+ from datetime import date, datetime
5
5
  import multiprocessing
6
6
  import logging
7
7
  from logging.handlers import QueueHandler, QueueListener
8
8
  import json
9
9
  import ssl
10
10
  import threading
11
+ import uuid
11
12
  from ..rest import Authenticator
12
13
  from ..config import market_data_socket_urls, market_data_feedb_socket_urls, REALTIME, B3, TRADES, BOOKS, FEED_A, FEED_B, MAX_WS_RECONNECT_RETRIES
13
14
  from .websocket_default_functions import _on_open, _on_message_already_serialized, _on_error, _on_close
14
15
 
15
16
  multiprocessing.set_start_method("spawn", force=True)
16
17
 
18
+ class LogConstFilter(logging.Filter):
19
+ def __init__(self, consts):
20
+ super().__init__()
21
+ self.consts = consts
22
+
23
+ def filter(self, record):
24
+ for key, value in self.consts.items():
25
+ setattr(record, key, value)
26
+ return True
27
+
17
28
  class MarketDataFeed:
18
29
  """
19
30
  WebSocket client that connects with BTG Solutions Data Services WebSocket servers. The servers streams realtime and delayed market data, such as trades and book events.
@@ -50,7 +61,7 @@ class MarketDataFeed:
50
61
  Field is not required. Default: 'b3'.
51
62
  data_type: str
52
63
  Market Data type.
53
- Options: 'trades', 'processed-trades', 'books', 'indices', 'securities', 'stoploss', 'candles-1S', 'candles-1M', 'instrument_status'.
64
+ Options: 'trades', 'processed-trades', 'books', 'indices', 'securities', 'stoploss', 'candles-1S', 'candles-1M', 'instrument_status', 'settlement-price'.
54
65
  Field is not required. Default: 'trades'.
55
66
  data_subtype: str
56
67
  Market Data subtype (when applicable).
@@ -136,6 +147,9 @@ class MarketDataFeed:
136
147
  self.reconnect = reconnect
137
148
  self.__nro_reconnect_retries = 0
138
149
 
150
+ client_feed = f'feed_{exchange}_{data_type}_{stream_type}_{data_subtype}'
151
+ client_id = str(uuid.uuid4())
152
+
139
153
  self.server_message_queue = multiprocessing.Queue()
140
154
  self.client_message_queue = multiprocessing.Queue()
141
155
  self.log_queue = multiprocessing.Queue()
@@ -143,8 +157,10 @@ class MarketDataFeed:
143
157
  log_level = getattr(logging, log_level)
144
158
  self.log_level = log_level
145
159
 
160
+ log_constants = {'client_feed': client_feed, 'client_id': client_id}
146
161
  log_handler = logging.FileHandler(filename=f"MarketDataFeed_{date.today().isoformat()}.log")
147
- log_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
162
+ log_handler.setFormatter(logging.Formatter('%(asctime)s - %(client_feed)s - %(client_id)s - %(levelname)s - %(message)s'))
163
+ log_handler.addFilter(LogConstFilter(log_constants))
148
164
 
149
165
  log_queue_listener = QueueListener(self.log_queue, log_handler)
150
166
  log_queue_listener.start()
@@ -156,6 +172,14 @@ class MarketDataFeed:
156
172
  self.process = None
157
173
  self.running = False
158
174
 
175
+ self.head_message_count = 0
176
+ self.head_avg_latency = 0
177
+
178
+ self.instruments = []
179
+
180
+ def set_instruments(self, instruments: List[str]):
181
+ self.instruments = instruments
182
+
159
183
  def _ws_client_process(self, server_message_queue: multiprocessing.Queue, client_message_queue: multiprocessing.Queue, log_queue: multiprocessing.Queue, log_level: int):
160
184
 
161
185
  logger = logging.getLogger("client")
@@ -163,7 +187,27 @@ class MarketDataFeed:
163
187
  logger.addHandler(QueueHandler(log_queue))
164
188
 
165
189
  def on_message(ws, message):
166
- server_message_queue.put(json.loads(message))
190
+ message = json.loads(message)
191
+ server_message_queue.put(message)
192
+
193
+ if self.log_level != logging.DEBUG:
194
+ return
195
+
196
+ msg_datetime = None
197
+ if self.data_type == BOOKS:
198
+ bid = message.get("bid")
199
+ offer = message.get("offer")
200
+ if bid:
201
+ msg_datetime = bid[0]["datetime"]
202
+ elif offer:
203
+ msg_datetime = offer[0]["datetime"]
204
+ else:
205
+ msg_datetime = message.get("tTime")
206
+ if msg_datetime:
207
+ msg_datetime = datetime.strptime(msg_datetime, "%Y-%m-%dT%H:%M:%S.%fZ")
208
+ latency = (time.time() - msg_datetime.timestamp()) * 1000
209
+ self.head_message_count += 1
210
+ self.head_avg_latency += (latency - self.head_avg_latency) / self.head_message_count
167
211
 
168
212
  def on_error(ws, error):
169
213
  logger.error(f"On Error | {error}")
@@ -182,6 +226,9 @@ class MarketDataFeed:
182
226
 
183
227
  def on_open(ws):
184
228
  logger.info(f"On Open | Connection open")
229
+ if self.instruments:
230
+ logger.info(f"On Open | Subscribing to {len(self.instruments)} instruments")
231
+ self.subscribe(self.instruments)
185
232
  self.on_open()
186
233
  self.__nro_reconnect_retries = 0
187
234
 
@@ -208,15 +255,28 @@ class MarketDataFeed:
208
255
  break
209
256
  pass
210
257
 
258
+ log_metrics_interval = 5
259
+ log_timer = time.time()
211
260
  while True:
212
261
  try:
213
262
  if not client_message_queue.empty():
214
263
  msg = client_message_queue.get()
215
264
  ws.send(json.dumps(msg))
216
- else:
217
- time.sleep(0.05)
265
+
266
+ if self.log_level != logging.DEBUG:
267
+ continue
268
+
269
+ if time.time() - log_timer >= log_metrics_interval:
270
+ if self.head_avg_latency == 0:
271
+ logger.debug(f"HEAD - (ServerQ) Relative Latency: N/A; Throughput: N/A")
272
+ else:
273
+ logger.debug(f"HEAD - (ServerQ) Relative Latency: {round(self.head_avg_latency, 1)} ms; Throughput: {round(self.head_message_count/log_metrics_interval, 1)} msg/s")
274
+ self.head_message_count = 0
275
+ self.head_avg_latency = 0
276
+ log_timer = time.time()
277
+
218
278
  except Exception as e:
219
- time.sleep(0.1)
279
+ time.sleep(0.01)
220
280
 
221
281
  run_forever_new_thread()
222
282
 
@@ -230,18 +290,47 @@ class MarketDataFeed:
230
290
 
231
291
  def run_on_new_thread(*args):
232
292
  log_timer = time.time()
293
+ log_metrics_interval = 5
294
+
295
+ message_count = 0
296
+ latency_message_count = 0
297
+ latency_average = 0
233
298
  while self.running:
234
299
  if not self.server_message_queue.empty():
235
300
  msg = self.server_message_queue.get()
236
301
  self.on_message(msg)
237
- else:
238
- time.sleep(0.01)
239
302
 
240
- if time.time() - log_timer >= 5.0:
303
+ if self.log_level != logging.DEBUG:
304
+ continue
305
+
306
+ message_count += 1
307
+ msg_datetime = None
308
+ if self.data_type == BOOKS:
309
+ bid = msg.get("bid")
310
+ offer = msg.get("offer")
311
+ if bid:
312
+ msg_datetime = bid[0]["datetime"]
313
+ elif offer:
314
+ msg_datetime = offer[0]["datetime"]
315
+ else:
316
+ msg_datetime = msg.get("tTime")
317
+ if msg_datetime:
318
+ msg_datetime = datetime.strptime(msg_datetime, "%Y-%m-%dT%H:%M:%S.%fZ")
319
+ latency = (time.time() - msg_datetime.timestamp()) * 1000
320
+ latency_message_count += 1
321
+ latency_average += (latency - latency_average) / latency_message_count
322
+
323
+ if time.time() - log_timer >= log_metrics_interval:
241
324
  server_queue_size = self.server_message_queue.qsize()
242
325
  client_queue_size = self.client_message_queue.qsize()
243
- self.logger.debug(f"Server queue: {server_queue_size} | Client queue: {client_queue_size}")
326
+ if message_count == 0:
327
+ self.logger.debug(f"TAIL - (ServerQ) Relative Latency: N/A; Throughput: N/A; Size: {server_queue_size} | (ClientQ) Size: {client_queue_size}")
328
+ else:
329
+ self.logger.debug(f"TAIL - (ServerQ) Relative Latency: {round(latency_average, 1)} ms; Throughput: {round(message_count/log_metrics_interval, 1)} msg/s; Size: {server_queue_size} | (ClientQ) Size: {client_queue_size}")
244
330
  log_timer = time.time()
331
+ message_count = 0
332
+ latency_message_count = 0
333
+ latency_average = 0
245
334
 
246
335
  threading.Thread(target=run_on_new_thread).start()
247
336
 
@@ -262,7 +351,7 @@ class MarketDataFeed:
262
351
  message = json.dumps(message)
263
352
  self.client_message_queue.put(json.loads(message))
264
353
 
265
- def subscribe(self, list_instruments: List[str], n=None):
354
+ def subscribe(self, list_instruments: List[str], n=None, initial_snapshot: bool=False):
266
355
  """
267
356
  Subscribes a list of instruments.
268
357
 
@@ -274,9 +363,17 @@ class MarketDataFeed:
274
363
  Field is not required.
275
364
  **For books data_type only.**
276
365
  Maximum book level. It must be between 1 and 10.
366
+ initial_snapshot: float
367
+ If True, client receives ticker last event (snapshot) of the provided ticker.
368
+ Field is not required.
369
+ Default: False
277
370
  """
278
- if self.data_type == BOOKS and n is not None:
279
- self._send({'action': 'subscribe', 'params': {"tickers": list_instruments, "n": n}})
371
+
372
+ if initial_snapshot or n is not None:
373
+ message = {'action': 'subscribe', 'params': {"tickers": list_instruments, "initial_snapshot": initial_snapshot}}
374
+ if n is not None:
375
+ message['params']['n'] = n
376
+ self._send(message)
280
377
  else:
281
378
  self._send({'action': 'subscribe', 'params': list_instruments})
282
379
 
@@ -2,7 +2,7 @@
2
2
  from typing import Optional, List
3
3
  from ..exceptions import WSTypeError, DelayedError, FeedError
4
4
  from ..rest import Authenticator
5
- from ..config import market_data_socket_urls, market_data_feedb_socket_urls, MAX_WS_RECONNECT_RETRIES, VALID_STREAM_TYPES, VALID_EXCHANGES, VALID_MARKET_DATA_TYPES, VALID_MARKET_DATA_SUBTYPES, REALTIME, B3, TRADES, INDICES, ALL, STOCKS, BOOKS, FEED_A, FEED_B
5
+ from ..config import market_data_socket_urls, market_data_feedb_socket_urls, MAX_WS_RECONNECT_RETRIES, VALID_STREAM_TYPES, VALID_EXCHANGES, VALID_MARKET_DATA_TYPES, VALID_MARKET_DATA_SUBTYPES, REALTIME, B3, TRADES, INDICES, ALL, STOCKS, BOOKS, FEED_A, FEED_B, SETTLEMENTPRICES
6
6
  from .websocket_default_functions import _on_open, _on_message, _on_error, _on_close
7
7
  import websocket
8
8
  import json
@@ -49,7 +49,7 @@ class MarketDataWebSocketClient:
49
49
 
50
50
  data_type: str
51
51
  Market Data type.
52
- Options: 'trades', 'processed-trades', 'books', 'indices', 'securities', 'stoploss', 'candles-1S', 'candles-1M', 'instrument_status'.
52
+ Options: 'trades', 'processed-trades', 'books', 'indices', 'securities', 'stoploss', 'candles-1S', 'candles-1M', 'instrument_status', 'settlement-price'.
53
53
  Field is not required. Default: 'trades'.
54
54
 
55
55
  data_subtype: str
@@ -93,7 +93,7 @@ class MarketDataWebSocketClient:
93
93
  self.__nro_reconnect_retries = 0
94
94
 
95
95
  if data_subtype is None:
96
- if exchange is B3 and data_type is not INDICES:
96
+ if exchange is B3 and not data_type in [INDICES, SETTLEMENTPRICES]:
97
97
  data_subtype = STOCKS
98
98
  else:
99
99
  data_subtype = ALL
@@ -267,7 +267,7 @@ class MarketDataWebSocketClient:
267
267
  """
268
268
  self.ws.close()
269
269
 
270
- def subscribe(self, list_instruments, n=None):
270
+ def subscribe(self, list_instruments, n=None, initial_snapshot: bool=False):
271
271
  """
272
272
  Subscribes a list of instruments.
273
273
 
@@ -278,12 +278,19 @@ class MarketDataWebSocketClient:
278
278
  n: int
279
279
  Field is not required.
280
280
  **For books data_type only.**
281
- Maximum book level. It must be between 1 and 10.
281
+ Maximum book level. It must be between 1 and 10.
282
+ initial_snapshot: float
283
+ If True, client receives ticker last event (snapshot) of the provided ticker.
284
+ Field is not required.
285
+ Default: False
282
286
  """
283
287
 
284
- if self.data_type == BOOKS and n is not None:
285
- self.__send({'action': 'subscribe', 'params': {"tickers": list_instruments, "n": n}})
286
- self.__print(f'Socket subscribed the following instrument(s) with n = {n}: {list_instruments}')
288
+ if initial_snapshot or n is not None:
289
+ message = {'action': 'subscribe', 'params': {"tickers": list_instruments, "initial_snapshot": initial_snapshot}}
290
+ if n is not None:
291
+ message['params']['n'] = n
292
+ self.__send(message)
293
+ self.__print(f'Socket subscribed the following instrument(s) with n = {n}: {list_instruments} and initial_snapshot = {initial_snapshot}')
287
294
  else:
288
295
  self.__send({'action': 'subscribe', 'params': list_instruments})
289
296
  self.__print(f'Socket subscribed the following instrument(s): {list_instruments}')
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: btgsolutions-dataservices-python-client
3
- Version: 2.11.4
3
+ Version: 2.17.1
4
4
  Summary: Python package containing several classes and data for extracting and manipulating market and trading data.
5
5
  Home-page: https://github.com/BTG-Pactual-Solutions/btgsolutions-dataservices-python-client
6
6
  Author: BTG Solutions Data Services powered by BTG Pactual Solutions
7
- Requires-Python: >=3.9,<3.13
7
+ Requires-Python: >=3.9,<3.14
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
10
  Requires-Dist: pandas>=2.2.2
@@ -165,11 +165,26 @@ ws.run(on_message=lambda message: print(message))
165
165
 
166
166
  ```python
167
167
  import btgsolutions_dataservices as btg
168
- ws = btg.MarketDataWebSocketClient(api_key='YOUR_API_KEY', data_type='books')
168
+ ws = btg.MarketDataWebSocketClient(api_key='YOUR_API_KEY', data_type='instrument_status', data_subtype='stocks')
169
169
  ws.run(on_message=lambda message: print(message))
170
170
  ws.instrument_status('PETR4')
171
171
  ws.instrument_status_history('PETR4')
172
172
 
173
+ ## The following is optional to keep the program running in a .py file:
174
+ # from time import sleep
175
+ # while True:
176
+ # sleep(1)
177
+ ```
178
+ ##### Settlement Price
179
+
180
+ ```python
181
+ import btgsolutions_dataservices as btg
182
+ ws = btg.MarketDataWebSocketClient(api_key='YOUR_API_KEY', data_type='settlement-price', instruments=['ABEVOU25', 'WINV25'])
183
+ ws.run(on_message=lambda message: print(message))
184
+
185
+ ## Getting the last event (settlement-price) of ABEVOU25:
186
+ # ws.get_last_event(['ABEVOU25'])
187
+
173
188
  ## The following is optional to keep the program running in a .py file:
174
189
  # from time import sleep
175
190
  # while True:
@@ -214,7 +229,7 @@ quotes = btg.Quotes(api_key='YOUR_API_KEY')
214
229
  quotes.get_quote(market_type = 'stocks', tickers = ['PETR4', 'VALE3'])
215
230
  ```
216
231
 
217
- #### Ticker Last Event
232
+ #### Ticker Last Trade
218
233
 
219
234
  ```python
220
235
  import btgsolutions_dataservices as btg
@@ -222,6 +237,31 @@ last_event = btg.TickerLastEvent(api_key='YOUR_API_KEY')
222
237
  last_event.get_trades(data_type='equities', ticker='VALE3')
223
238
  ```
224
239
 
240
+ #### Ticker Last Top of Book
241
+
242
+ ```python
243
+ import btgsolutions_dataservices as btg
244
+ last_event = btg.TickerLastEvent(api_key='YOUR_API_KEY')
245
+ last_event.get_tobs(data_type='stocks')
246
+ ```
247
+
248
+ #### Ticker Last Trading Status
249
+
250
+ ```python
251
+ import btgsolutions_dataservices as btg
252
+ last_event = btg.TickerLastEvent(api_key='YOUR_API_KEY')
253
+ last_event.get_status(tickers=['PETR4','VALE3'])
254
+ ```
255
+
256
+ #### Ticker Last Polling - Top of Books
257
+
258
+ ```python
259
+ import btgsolutions_dataservices as btg
260
+ last_event = btg.TickerLastEventPolling(api_key='YOUR_API_KEY', data_type='top-of-books', data_subtype='stocks')
261
+ last_event.get()
262
+ ```
263
+
264
+
225
265
  ### Historical Data
226
266
 
227
267
  #### Historical Candles
@@ -336,6 +376,16 @@ public_sources = btg.PublicSources(api_key='YOUR_API_KEY')
336
376
  public_sources.get_opas(start_date='2022-10-01', end_date='2024-10-01')
337
377
  ```
338
378
 
379
+ #### STOCK LOAN
380
+
381
+ ```python
382
+ import btgsolutions_dataservices as btg
383
+ stock_loan = btg.StockLoan(api_key='YOUR_API_KEY')
384
+ stock_loan.get_trades()
385
+ stock_loan.get_paginated_trades(page=1, limit=1000, ticker ='PETR4')
386
+ stock_loan.get_available_tickers()
387
+ ```
388
+
339
389
  #### Company Fundamentals
340
390
 
341
391
  ##### Company General Information
@@ -1,26 +1,28 @@
1
1
  btgsolutions_dataservices/__init__.py,sha256=reSSb9MBIp5WQ5o872fX__moJYXx0sQgkhUf3QmfEjI,93
2
- btgsolutions_dataservices/config.py,sha256=syf7x6LL4WNAM0e6CI1BKfPHnprxWoww7llzvtV0QXs,5082
2
+ btgsolutions_dataservices/config.py,sha256=9L9bIGL1j7XSAkpYMxLS8oqIu-F4CQ2EyAywIT496p8,5285
3
3
  btgsolutions_dataservices/exceptions.py,sha256=QlwQTDEohtVsTER0lHbQrDgAGvjiZBmoyDuqLkUcvh8,599
4
- btgsolutions_dataservices/rest/__init__.py,sha256=OI7KWRdAMG3oHABhaGn6nOsGCou4-FX7e0qH2rxChmE,494
4
+ btgsolutions_dataservices/rest/__init__.py,sha256=NEar641a2Mosioj-D3HrcdrJqgjaFRCwos3rftnUhq0,591
5
5
  btgsolutions_dataservices/rest/authenticator.py,sha256=uBLARz5lCRhIDc_PZaC4a2rSQtrFJajmAC4Bdg9BnX0,1363
6
6
  btgsolutions_dataservices/rest/bulk_data.py,sha256=26CrFH-IVMUbpPLx0HaN-BePs5R9A1ZUINSir7qtfhE,9032
7
7
  btgsolutions_dataservices/rest/company_data.py,sha256=yhsgR-P7g5e6TDdpoMZoyiyqI85roBCXJGyob4g1P_8,7318
8
8
  btgsolutions_dataservices/rest/corporate_events.py,sha256=04DdoUnBaX1rSAWplCLzNm-YPrKks80txOR26xubEZ0,2399
9
9
  btgsolutions_dataservices/rest/hfn.py,sha256=0E-oNw797M8XgbDfwbhmMaPaf9z-WN4rhMpMR_2dkos,11205
10
10
  btgsolutions_dataservices/rest/historical_candles.py,sha256=0iu3clwbJUtwyHotxT5pb3xsqCJvpd6kRUsrYBVNU80,7359
11
- btgsolutions_dataservices/rest/intraday_candles.py,sha256=scH2eMh3ZhtWii_Kzv3PEGcUaLAUT-CAeswCY-3pYVc,5221
11
+ btgsolutions_dataservices/rest/intraday_candles.py,sha256=WWujmvW_qoREPjYadhMoJyiVMX8Ts7zFo21KacqKhjU,5687
12
12
  btgsolutions_dataservices/rest/intraday_tick_data.py,sha256=VZZ-yPoQn9dZCJ9KnPYGwaEFq2FZi0oSnLDIIENYCNo,2469
13
13
  btgsolutions_dataservices/rest/public_sources.py,sha256=CMXsvHe8RKuEmJJ_baeVgsj28TrH2AYxE3TEfGSCdOQ,2459
14
14
  btgsolutions_dataservices/rest/quotes.py,sha256=sirlG1-_aCtE7auwYM7lTZkpMcW3UYisjtCaBWzPWuY,7508
15
15
  btgsolutions_dataservices/rest/reference_data.py,sha256=ntcHEMapKULGPHJvmzAnccUo8ZTGokt1M9bDBceV5Tk,2112
16
- btgsolutions_dataservices/rest/ticker_last_event.py,sha256=Lnm8sGS6wo6frL8yJddtBtEOEsPiodgjl4qq5Xdo0P0,2950
16
+ btgsolutions_dataservices/rest/stock_loan.py,sha256=_EYxywO-qA-zC7CeAo8a1omUb327g13P1R1B-plPKtI,3207
17
+ btgsolutions_dataservices/rest/ticker_last_event.py,sha256=1DR4fVsareRDr1iGya7E8308kDcciK5XtlS2SI56c1o,6328
18
+ btgsolutions_dataservices/rest/ticker_last_event_polling.py,sha256=wCDplal8k5xHXI4JD69D5j-djYkL16SiNiso7Of3UCI,4656
17
19
  btgsolutions_dataservices/websocket/__init__.py,sha256=Bifok6zQFenYNIA-NE8hYSooZVIQ2MCeTCjIOYnfHzI,165
18
20
  btgsolutions_dataservices/websocket/hfn_websocket_client.py,sha256=wzyPXtXFsv31cVFcTWrTA5uhFBebgM2IpIxXQvPSP54,6253
19
- btgsolutions_dataservices/websocket/market_data_feed.py,sha256=Zh3aqFLoVZPnA3LVXCRxcsPYdlQId9qifrQsBkWUWrg,11770
20
- btgsolutions_dataservices/websocket/market_data_websocket_client.py,sha256=i_1kd6mdd9fLDYD54zx_t1HJPPBUdDzE4pojs3DIPLI,13375
21
+ btgsolutions_dataservices/websocket/market_data_feed.py,sha256=HAVA4thvYyvmcVrHm4_exGaq4y9RCGjwl98yuXR8KVc,16209
22
+ btgsolutions_dataservices/websocket/market_data_websocket_client.py,sha256=ix6EH6yHJcRc69RWuvUjHxr0H17zRSM_o9YjsO6OZIw,13812
21
23
  btgsolutions_dataservices/websocket/websocket_default_functions.py,sha256=JWbOR4uX14BI9JB3zXMbwqVsF8AtfDbEC83fgs8O5PY,335
22
- btgsolutions_dataservices_python_client-2.11.4.dist-info/licenses/LICENSE,sha256=vePSp4Jry-f15vZ48xge2vljuhNgkMZD6wU0XmuFBgA,1057
23
- btgsolutions_dataservices_python_client-2.11.4.dist-info/METADATA,sha256=___fOIFhAUcvenXmiN89UeDuljLwG8JBwtmNoBq5hMc,11808
24
- btgsolutions_dataservices_python_client-2.11.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
- btgsolutions_dataservices_python_client-2.11.4.dist-info/top_level.txt,sha256=OaseSN41lI9j_Z-kHIE_XVo_m1hmGdAHE3EK1h5_Ce0,26
26
- btgsolutions_dataservices_python_client-2.11.4.dist-info/RECORD,,
24
+ btgsolutions_dataservices_python_client-2.17.1.dist-info/licenses/LICENSE,sha256=vePSp4Jry-f15vZ48xge2vljuhNgkMZD6wU0XmuFBgA,1057
25
+ btgsolutions_dataservices_python_client-2.17.1.dist-info/METADATA,sha256=cDuPuZOmolVyzvEqaHub2x0wvWAwv2kuAqeGSYPNYVg,13169
26
+ btgsolutions_dataservices_python_client-2.17.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
+ btgsolutions_dataservices_python_client-2.17.1.dist-info/top_level.txt,sha256=OaseSN41lI9j_Z-kHIE_XVo_m1hmGdAHE3EK1h5_Ce0,26
28
+ btgsolutions_dataservices_python_client-2.17.1.dist-info/RECORD,,