Skip to content

Commit 77b23bf

Browse files
committed
add instrument metadata helpers
1 parent d8bcbfe commit 77b23bf

4 files changed

Lines changed: 393 additions & 0 deletions

File tree

tardis_dev/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@
1111
from tardis_dev.clear_cache import clear_cache
1212
from tardis_dev.download_datasets import default_file_name, download_datasets, download_datasets_async
1313
from tardis_dev.get_exchange_details import get_exchange_details, get_exchange_details_async
14+
from tardis_dev.instrument_info import (
15+
InstrumentInfo,
16+
InstrumentInfoFilter,
17+
InstrumentSymbolSelector,
18+
InstrumentSymbols,
19+
find_instrument_symbols,
20+
find_instrument_symbols_async,
21+
get_instrument_info,
22+
get_instrument_info_async,
23+
)
1424
from tardis_dev.replay import Response, replay
1525

1626

@@ -26,6 +36,14 @@
2636
"download_datasets_async",
2737
"get_exchange_details",
2838
"get_exchange_details_async",
39+
"get_instrument_info",
40+
"get_instrument_info_async",
41+
"find_instrument_symbols",
42+
"find_instrument_symbols_async",
43+
"InstrumentInfo",
44+
"InstrumentInfoFilter",
45+
"InstrumentSymbolSelector",
46+
"InstrumentSymbols",
2947
"clear_cache",
3048
"default_file_name",
3149
]

tardis_dev/instrument_info.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import asyncio
2+
import gzip
3+
import json
4+
import urllib.error
5+
import urllib.parse
6+
from typing import Any, Dict, List, Literal, Mapping, Optional, Sequence, TypedDict, Union
7+
8+
from tardis_dev._http import create_session
9+
from tardis_dev._options import DEFAULT_ENDPOINT
10+
11+
12+
InstrumentInfo = Dict[str, Any]
13+
InstrumentInfoFilterValue = Union[str, Sequence[str]]
14+
15+
16+
class InstrumentInfoFilter(TypedDict, total=False):
17+
baseCurrency: InstrumentInfoFilterValue
18+
quoteCurrency: InstrumentInfoFilterValue
19+
type: InstrumentInfoFilterValue
20+
contractType: InstrumentInfoFilterValue
21+
underlyingType: InstrumentInfoFilterValue
22+
active: bool
23+
availableSince: str
24+
availableTo: str
25+
26+
27+
class InstrumentSymbols(TypedDict):
28+
exchange: str
29+
symbols: List[str]
30+
31+
32+
InstrumentSymbolSelector = Literal["id", "datasetId"]
33+
34+
35+
def get_instrument_info(
36+
exchange: Union[str, Sequence[str]],
37+
*,
38+
filter: Optional[Mapping[str, Any]] = None,
39+
symbol: Optional[str] = None,
40+
api_key: str = "",
41+
endpoint: str = DEFAULT_ENDPOINT,
42+
timeout: int = 60,
43+
http_proxy: Optional[str] = None,
44+
) -> Union[InstrumentInfo, List[InstrumentInfo]]:
45+
try:
46+
asyncio.get_running_loop()
47+
except RuntimeError:
48+
return asyncio.run(
49+
get_instrument_info_async(
50+
exchange=exchange,
51+
filter=filter,
52+
symbol=symbol,
53+
api_key=api_key,
54+
endpoint=endpoint,
55+
timeout=timeout,
56+
http_proxy=http_proxy,
57+
)
58+
)
59+
60+
raise RuntimeError(
61+
"get_instrument_info() cannot be called from a running event loop. Use get_instrument_info_async() instead."
62+
)
63+
64+
65+
async def get_instrument_info_async(
66+
exchange: Union[str, Sequence[str]],
67+
*,
68+
filter: Optional[Mapping[str, Any]] = None,
69+
symbol: Optional[str] = None,
70+
api_key: str = "",
71+
endpoint: str = DEFAULT_ENDPOINT,
72+
timeout: int = 60,
73+
http_proxy: Optional[str] = None,
74+
) -> Union[InstrumentInfo, List[InstrumentInfo]]:
75+
if filter is not None and symbol is not None:
76+
raise ValueError("Provide either 'filter' or 'symbol', not both.")
77+
78+
if symbol is not None and not isinstance(exchange, str):
79+
raise ValueError("'symbol' can only be used with a single exchange.")
80+
81+
async with await create_session(api_key, timeout) as session:
82+
if isinstance(exchange, str):
83+
return await _get_instrument_info(
84+
session=session,
85+
exchange=exchange,
86+
filter=filter,
87+
symbol=symbol,
88+
endpoint=endpoint,
89+
http_proxy=http_proxy,
90+
)
91+
92+
results = await asyncio.gather(
93+
*(
94+
_get_instrument_info(
95+
session=session,
96+
exchange=exchange_id,
97+
filter=filter,
98+
endpoint=endpoint,
99+
http_proxy=http_proxy,
100+
)
101+
for exchange_id in exchange
102+
)
103+
)
104+
105+
return [instrument for instruments in results for instrument in instruments]
106+
107+
108+
def find_instrument_symbols(
109+
exchanges: Sequence[str],
110+
filter: Mapping[str, Any],
111+
*,
112+
selector: InstrumentSymbolSelector = "id",
113+
api_key: str = "",
114+
endpoint: str = DEFAULT_ENDPOINT,
115+
timeout: int = 60,
116+
http_proxy: Optional[str] = None,
117+
) -> List[InstrumentSymbols]:
118+
try:
119+
asyncio.get_running_loop()
120+
except RuntimeError:
121+
return asyncio.run(
122+
find_instrument_symbols_async(
123+
exchanges=exchanges,
124+
filter=filter,
125+
selector=selector,
126+
api_key=api_key,
127+
endpoint=endpoint,
128+
timeout=timeout,
129+
http_proxy=http_proxy,
130+
)
131+
)
132+
133+
raise RuntimeError(
134+
"find_instrument_symbols() cannot be called from a running event loop. Use find_instrument_symbols_async() instead."
135+
)
136+
137+
138+
async def find_instrument_symbols_async(
139+
exchanges: Sequence[str],
140+
filter: Mapping[str, Any],
141+
*,
142+
selector: InstrumentSymbolSelector = "id",
143+
api_key: str = "",
144+
endpoint: str = DEFAULT_ENDPOINT,
145+
timeout: int = 60,
146+
http_proxy: Optional[str] = None,
147+
) -> List[InstrumentSymbols]:
148+
_validate_selector(selector)
149+
150+
async with await create_session(api_key, timeout) as session:
151+
return await asyncio.gather(
152+
*(
153+
_find_instrument_symbols_for_exchange(
154+
session=session,
155+
exchange=exchange,
156+
filter=filter,
157+
selector=selector,
158+
endpoint=endpoint,
159+
http_proxy=http_proxy,
160+
)
161+
for exchange in exchanges
162+
)
163+
)
164+
165+
166+
def _validate_selector(selector: str) -> None:
167+
if selector not in ("id", "datasetId"):
168+
raise ValueError("Invalid 'selector' argument. Supported values are 'id' and 'datasetId'.")
169+
170+
171+
async def _find_instrument_symbols_for_exchange(
172+
*,
173+
session,
174+
exchange: str,
175+
filter: Mapping[str, Any],
176+
selector: InstrumentSymbolSelector,
177+
endpoint: str,
178+
http_proxy: Optional[str],
179+
) -> InstrumentSymbols:
180+
instruments = await _get_instrument_info(
181+
session=session,
182+
exchange=exchange,
183+
filter=filter,
184+
symbol=None,
185+
endpoint=endpoint,
186+
http_proxy=http_proxy,
187+
)
188+
189+
return {
190+
"exchange": exchange,
191+
"symbols": [_get_symbol(instrument, selector) for instrument in instruments],
192+
}
193+
194+
195+
async def _get_instrument_info(
196+
*,
197+
session,
198+
exchange: str,
199+
filter: Optional[Mapping[str, Any]],
200+
symbol: Optional[str] = None,
201+
endpoint: str,
202+
http_proxy: Optional[str],
203+
) -> Union[InstrumentInfo, List[InstrumentInfo]]:
204+
url = _get_instrument_info_url(endpoint, exchange, filter, symbol)
205+
206+
async with session.get(url, proxy=http_proxy) as response:
207+
body = await response.read()
208+
if response.headers.get("Content-Encoding") == "gzip":
209+
body = gzip.decompress(body)
210+
211+
if response.status != 200:
212+
error_text = body.decode("utf-8", errors="replace")
213+
raise urllib.error.HTTPError(url, code=response.status, msg=error_text, hdrs=None, fp=None)
214+
215+
return json.loads(body.decode("utf-8"))
216+
217+
218+
def _get_instrument_info_url(
219+
endpoint: str,
220+
exchange: str,
221+
filter: Optional[Mapping[str, Any]],
222+
symbol: Optional[str],
223+
) -> str:
224+
url = f"{endpoint}/instruments/{urllib.parse.quote(exchange, safe='')}"
225+
if symbol is not None:
226+
return f"{url}/{urllib.parse.quote(symbol, safe='')}"
227+
228+
if filter is not None:
229+
encoded_filter = urllib.parse.quote(json.dumps(filter, separators=(",", ":")))
230+
return f"{url}?filter={encoded_filter}"
231+
232+
return url
233+
234+
235+
def _get_symbol(instrument: Mapping[str, Any], selector: InstrumentSymbolSelector) -> str:
236+
if selector == "datasetId":
237+
return instrument.get("datasetId") or instrument["id"]
238+
239+
return instrument["id"]

tests/test_imports.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,22 @@
55
DEFAULT_DATASETS_ENDPOINT,
66
DEFAULT_ENDPOINT,
77
Channel,
8+
InstrumentInfo,
9+
InstrumentInfoFilter,
10+
InstrumentSymbolSelector,
11+
InstrumentSymbols,
812
Response,
913
__version__,
1014
clear_cache,
1115
default_file_name,
1216
download_datasets,
1317
download_datasets_async,
18+
find_instrument_symbols,
19+
find_instrument_symbols_async,
1420
get_exchange_details,
1521
get_exchange_details_async,
22+
get_instrument_info,
23+
get_instrument_info_async,
1624
replay,
1725
)
1826

@@ -27,7 +35,15 @@ def test_public_imports_are_available():
2735
assert replay is not None
2836
assert download_datasets is not None
2937
assert download_datasets_async is not None
38+
assert find_instrument_symbols is not None
39+
assert find_instrument_symbols_async is not None
3040
assert get_exchange_details is not None
3141
assert get_exchange_details_async is not None
42+
assert get_instrument_info is not None
43+
assert get_instrument_info_async is not None
44+
assert InstrumentInfo is not None
45+
assert InstrumentInfoFilter is not None
46+
assert InstrumentSymbolSelector is not None
47+
assert InstrumentSymbols is not None
3248
assert clear_cache is not None
3349
assert default_file_name is not None

0 commit comments

Comments
 (0)