Compare commits
	
		
			4 Commits 
		
	
	
		
			8165c684e5
			...
			1568a19cc0
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 1568a19cc0 | |
|  | 28b45232b5 | |
|  | 9e644e278d | |
|  | 0d2547fc43 | 
|  | @ -51,6 +51,7 @@ __brokers__: list[str] = [ | ||||||
|     'ib', |     'ib', | ||||||
|     'kraken', |     'kraken', | ||||||
|     'kucoin', |     'kucoin', | ||||||
|  |     'deribit', | ||||||
| 
 | 
 | ||||||
|     # broken but used to work |     # broken but used to work | ||||||
|     # 'questrade', |     # 'questrade', | ||||||
|  | @ -61,7 +62,6 @@ __brokers__: list[str] = [ | ||||||
|     # wstrade |     # wstrade | ||||||
|     # iex |     # iex | ||||||
| 
 | 
 | ||||||
|     # deribit |  | ||||||
|     # bitso |     # bitso | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -25,6 +25,7 @@ from .api import ( | ||||||
|     get_client, |     get_client, | ||||||
| ) | ) | ||||||
| from .feed import ( | from .feed import ( | ||||||
|  |     get_mkt_info, | ||||||
|     open_history_client, |     open_history_client, | ||||||
|     open_symbol_search, |     open_symbol_search, | ||||||
|     stream_quotes, |     stream_quotes, | ||||||
|  | @ -34,15 +35,20 @@ from .feed import ( | ||||||
|     # open_trade_dialog, |     # open_trade_dialog, | ||||||
|     # norm_trade_records, |     # norm_trade_records, | ||||||
| # ) | # ) | ||||||
|  | from .venues import ( | ||||||
|  |     OptionPair, | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| log = get_logger(__name__) | log = get_logger(__name__) | ||||||
| 
 | 
 | ||||||
| __all__ = [ | __all__ = [ | ||||||
|     'get_client', |     'get_client', | ||||||
| #    'trades_dialogue', | #    'trades_dialogue', | ||||||
|  |     'get_mkt_info', | ||||||
|     'open_history_client', |     'open_history_client', | ||||||
|     'open_symbol_search', |     'open_symbol_search', | ||||||
|     'stream_quotes', |     'stream_quotes', | ||||||
|  |     'OptionPair', | ||||||
| #    'norm_trade_records', | #    'norm_trade_records', | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,10 +19,14 @@ Deribit backend. | ||||||
| 
 | 
 | ||||||
| ''' | ''' | ||||||
| import asyncio | import asyncio | ||||||
|  | from collections import ChainMap | ||||||
| from contextlib import ( | from contextlib import ( | ||||||
|     asynccontextmanager as acm, |     asynccontextmanager as acm, | ||||||
| ) | ) | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
|  | from decimal import ( | ||||||
|  |     Decimal, | ||||||
|  | ) | ||||||
| from functools import partial | from functools import partial | ||||||
| import time | import time | ||||||
| from typing import ( | from typing import ( | ||||||
|  | @ -31,7 +35,7 @@ from typing import ( | ||||||
|     Callable, |     Callable, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| import pendulum | from pendulum import now | ||||||
| import trio | import trio | ||||||
| from trio_typing import TaskStatus | from trio_typing import TaskStatus | ||||||
| from rapidfuzz import process as fuzzy | from rapidfuzz import process as fuzzy | ||||||
|  | @ -51,7 +55,25 @@ from cryptofeed.defines import ( | ||||||
|     OPTION, CALL, PUT |     OPTION, CALL, PUT | ||||||
| ) | ) | ||||||
| from cryptofeed.symbols import Symbol | from cryptofeed.symbols import Symbol | ||||||
| 
 | # types for managing the cb callbacks. | ||||||
|  | # from cryptofeed.types import L1Book | ||||||
|  | from .venues import ( | ||||||
|  |     _ws_url, | ||||||
|  |     MarketType, | ||||||
|  |     PAIRTYPES, | ||||||
|  |     Pair, | ||||||
|  |     OptionPair, | ||||||
|  |     JSONRPCResult, | ||||||
|  |     JSONRPCChannel, | ||||||
|  |     KLinesResult, | ||||||
|  |     Trade, | ||||||
|  |     LastTradesResult, | ||||||
|  | ) | ||||||
|  | from piker.accounting import ( | ||||||
|  |     Asset, | ||||||
|  |     digits_to_dec, | ||||||
|  |     MktPair, | ||||||
|  | ) | ||||||
| from piker.data import ( | from piker.data import ( | ||||||
|     def_iohlcv_fields, |     def_iohlcv_fields, | ||||||
|     match_from_pairs, |     match_from_pairs, | ||||||
|  | @ -74,57 +96,6 @@ _spawn_kwargs = { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| _url = 'https://www.deribit.com' |  | ||||||
| _ws_url = 'wss://www.deribit.com/ws/api/v2' |  | ||||||
| _testnet_ws_url = 'wss://test.deribit.com/ws/api/v2' |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class JSONRPCResult(Struct): |  | ||||||
|     jsonrpc: str = '2.0' |  | ||||||
|     id: int |  | ||||||
|     result: Optional[list[dict]] = None |  | ||||||
|     error: Optional[dict] = None |  | ||||||
|     usIn: int |  | ||||||
|     usOut: int |  | ||||||
|     usDiff: int |  | ||||||
|     testnet: bool |  | ||||||
| 
 |  | ||||||
| class JSONRPCChannel(Struct): |  | ||||||
|     jsonrpc: str = '2.0' |  | ||||||
|     method: str |  | ||||||
|     params: dict |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class KLinesResult(Struct): |  | ||||||
|     close: list[float] |  | ||||||
|     cost: list[float] |  | ||||||
|     high: list[float] |  | ||||||
|     low: list[float] |  | ||||||
|     open: list[float] |  | ||||||
|     status: str |  | ||||||
|     ticks: list[int] |  | ||||||
|     volume: list[float] |  | ||||||
| 
 |  | ||||||
| class Trade(Struct): |  | ||||||
|     trade_seq: int |  | ||||||
|     trade_id: str |  | ||||||
|     timestamp: int |  | ||||||
|     tick_direction: int |  | ||||||
|     price: float |  | ||||||
|     mark_price: float |  | ||||||
|     iv: float |  | ||||||
|     instrument_name: str |  | ||||||
|     index_price: float |  | ||||||
|     direction: str |  | ||||||
|     combo_trade_id: Optional[int] = 0, |  | ||||||
|     combo_id: Optional[str] = '', |  | ||||||
|     amount: float |  | ||||||
| 
 |  | ||||||
| class LastTradesResult(Struct): |  | ||||||
|     trades: list[Trade] |  | ||||||
|     has_more: bool |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # convert datetime obj timestamp to unixtime in milliseconds | # convert datetime obj timestamp to unixtime in milliseconds | ||||||
| def deribit_timestamp(when): | def deribit_timestamp(when): | ||||||
|     return int((when.timestamp() * 1000) + (when.microsecond / 1000)) |     return int((when.timestamp() * 1000) + (when.microsecond / 1000)) | ||||||
|  | @ -142,13 +113,15 @@ def str_to_cb_sym(name: str) -> Symbol: | ||||||
|     else: |     else: | ||||||
|         raise Exception("Couldn\'t parse option type") |         raise Exception("Couldn\'t parse option type") | ||||||
| 
 | 
 | ||||||
|  |     new_expiry_date = get_values_from_cb_normalized_date(expiry_date) | ||||||
|  | 
 | ||||||
|     return Symbol( |     return Symbol( | ||||||
|         base, quote, |         base=base, | ||||||
|  |         quote=quote, | ||||||
|         type=OPTION, |         type=OPTION, | ||||||
|         strike_price=strike_price, |         strike_price=strike_price, | ||||||
|         option_type=option_type, |         option_type=option_type, | ||||||
|         expiry_date=expiry_date, |         expiry_date=new_expiry_date) | ||||||
|         expiry_normalize=False) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def piker_sym_to_cb_sym(name: str) -> Symbol: | def piker_sym_to_cb_sym(name: str) -> Symbol: | ||||||
|  | @ -159,83 +132,138 @@ def piker_sym_to_cb_sym(name: str) -> Symbol: | ||||||
| 
 | 
 | ||||||
|     if option_type == 'P': |     if option_type == 'P': | ||||||
|         option_type = PUT  |         option_type = PUT  | ||||||
|     elif option_type  == 'C': |     elif option_type == 'C': | ||||||
|         option_type = CALL |         option_type = CALL | ||||||
|     else: |     else: | ||||||
|         raise Exception("Couldn\'t parse option type") |         raise Exception("Couldn\'t parse option type") | ||||||
| 
 | 
 | ||||||
|     return Symbol( |     return Symbol( | ||||||
|         base, quote, |         base=base, | ||||||
|  |         quote=quote, | ||||||
|         type=OPTION, |         type=OPTION, | ||||||
|         strike_price=strike_price, |         strike_price=strike_price, | ||||||
|         option_type=option_type, |         option_type=option_type, | ||||||
|         expiry_date=expiry_date.upper()) |         expiry_date=expiry_date) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def cb_sym_to_deribit_inst(sym: Symbol): | def cb_sym_to_deribit_inst(sym: Symbol): | ||||||
|     # cryptofeed normalized |     new_expiry_date = get_values_from_cb_normalized_date(sym.expiry_date) | ||||||
|     cb_norm = ['F', 'G', 'H', 'J', 'K', 'M', 'N', 'Q', 'U', 'V', 'X', 'Z'] |  | ||||||
| 
 |  | ||||||
|     # deribit specific  |  | ||||||
|     months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'] |  | ||||||
| 
 |  | ||||||
|     exp = sym.expiry_date |  | ||||||
| 
 |  | ||||||
|     # YYMDD |  | ||||||
|     # 01234 |  | ||||||
|     year, month, day = ( |  | ||||||
|         exp[:2], months[cb_norm.index(exp[2:3])], exp[3:]) |  | ||||||
| 
 |  | ||||||
|     otype = 'C' if sym.option_type == CALL else 'P' |     otype = 'C' if sym.option_type == CALL else 'P' | ||||||
| 
 | 
 | ||||||
|     return f'{sym.base}-{day}{month}{year}-{sym.strike_price}-{otype}' |     return f'{sym.base}-{new_expiry_date}-{sym.strike_price}-{otype}' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_values_from_cb_normalized_date(expiry_date: str) -> str: | ||||||
|  |     # deribit specific | ||||||
|  |     cb_norm = [ | ||||||
|  |         'F', 'G', 'H', 'J', | ||||||
|  |         'K', 'M', 'N', 'Q', | ||||||
|  |         'U', 'V', 'X', 'Z' | ||||||
|  |     ] | ||||||
|  |     months = [ | ||||||
|  |         'JAN', 'FEB', 'MAR', 'APR', | ||||||
|  |         'MAY', 'JUN', 'JUL', 'AUG', | ||||||
|  |         'SEP', 'OCT', 'NOV', 'DEC' | ||||||
|  |     ] | ||||||
|  |     # YYMDD | ||||||
|  |     # 01234 | ||||||
|  |     day, month, year = ( | ||||||
|  |         expiry_date[3:], | ||||||
|  |         months[cb_norm.index(expiry_date[2:3])], | ||||||
|  |         expiry_date[:2] | ||||||
|  |     ) | ||||||
|  |     return f'{day}{month}{year}' | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_config() -> dict[str, Any]: | def get_config() -> dict[str, Any]: | ||||||
| 
 | 
 | ||||||
|     conf, path = config.load() |     conf: dict | ||||||
|  |     path: Path | ||||||
| 
 | 
 | ||||||
|  |     conf, path = config.load( | ||||||
|  |         conf_name='brokers', | ||||||
|  |         touch_if_dne=True, | ||||||
|  |     ) | ||||||
|  |     section: dict = {} | ||||||
|     section = conf.get('deribit') |     section = conf.get('deribit') | ||||||
| 
 |  | ||||||
|     # TODO: document why we send this, basically because logging params for cryptofeed |  | ||||||
|     conf['log'] = {} |  | ||||||
|     conf['log']['disabled'] = True |  | ||||||
| 
 |  | ||||||
|     if section is None: |     if section is None: | ||||||
|         log.warning(f'No config section found for deribit in {path}') |         log.warning(f'No config section found for deribit in {path}') | ||||||
|  |         return {} | ||||||
| 
 | 
 | ||||||
|     return conf  |     conf_option = section.get('option', {}) | ||||||
|  |     section.clear # clear the dict to reuse it | ||||||
|  |     section['deribit'] = {} | ||||||
|  |     section['deribit']['key_id'] = conf_option.get('api_key') | ||||||
|  |     section['deribit']['key_secret'] = conf_option.get('api_secret') | ||||||
|  | 
 | ||||||
|  |     section['log'] = {} | ||||||
|  |     section['log']['filename'] = 'feedhandler.log' | ||||||
|  |     section['log']['level'] = 'DEBUG' | ||||||
|  | 
 | ||||||
|  |     return section | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Client: | class Client: | ||||||
| 
 | 
 | ||||||
|     def __init__(self, json_rpc: Callable) -> None: |     def __init__( | ||||||
|         self._pairs: dict[str, Any] = None |         self, | ||||||
|  | 
 | ||||||
|  |         json_rpc: Callable | ||||||
|  | 
 | ||||||
|  |     ) -> None: | ||||||
|  |         self._pairs: ChainMap[str, Pair] = ChainMap() | ||||||
| 
 | 
 | ||||||
|         config = get_config().get('deribit', {}) |         config = get_config().get('deribit', {}) | ||||||
| 
 | 
 | ||||||
|         if ('key_id' in config) and ('key_secret' in config): |         self._key_id = config.get('key_id') | ||||||
|             self._key_id = config['key_id'] |         self._key_secret = config.get('key_secret') | ||||||
|             self._key_secret = config['key_secret'] |  | ||||||
| 
 |  | ||||||
|         else: |  | ||||||
|             self._key_id = None |  | ||||||
|             self._key_secret = None |  | ||||||
| 
 | 
 | ||||||
|         self.json_rpc = json_rpc |         self.json_rpc = json_rpc | ||||||
| 
 | 
 | ||||||
|     @property |         self._auth_ts = None | ||||||
|     def currencies(self): |         self._auth_renew_ts = 5 # seconds to renew auth | ||||||
|         return ['btc', 'eth', 'sol', 'usd'] |  | ||||||
| 
 | 
 | ||||||
|     async def get_balances(self, kind: str = 'option') -> dict[str, float]: |     async def _json_rpc_auth_wrapper(self, *args, **kwargs) -> JSONRPCResult: | ||||||
|  |          | ||||||
|  |         """Background task that adquires a first access token and then will | ||||||
|  |         refresh the access token. | ||||||
|  | 
 | ||||||
|  |         https://docs.deribit.com/?python#authentication-2 | ||||||
|  |         """ | ||||||
|  |         access_scope = 'trade:read_write' | ||||||
|  |         current_ts = time.time() | ||||||
|  | 
 | ||||||
|  |         if not self._auth_ts or current_ts - self._auth_ts < self._auth_renew_ts: | ||||||
|  |             # if we are close to token expiry time | ||||||
|  | 
 | ||||||
|  |             params = { | ||||||
|  |                 'grant_type': 'client_credentials', | ||||||
|  |                 'client_id': self._key_id, | ||||||
|  |                 'client_secret': self._key_secret, | ||||||
|  |                 'scope': access_scope | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             resp = await self.json_rpc('public/auth', params) | ||||||
|  |             result = resp.result | ||||||
|  | 
 | ||||||
|  |             self._auth_ts = time.time() + result['expires_in'] | ||||||
|  | 
 | ||||||
|  |         return await self.json_rpc(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     async def get_balances( | ||||||
|  |         self, | ||||||
|  |         kind: str = 'option' | ||||||
|  |     ) -> dict[str, float]: | ||||||
|         """Return the set of positions for this account |         """Return the set of positions for this account | ||||||
|         by symbol. |         by symbol. | ||||||
|         """ |         """ | ||||||
|         balances = {} |         balances = {} | ||||||
| 
 | 
 | ||||||
|         for currency in self.currencies: |         for currency in self.currencies: | ||||||
|             resp = await self.json_rpc( |             resp = await self._json_rpc_auth_wrapper( | ||||||
|                 'private/get_positions', params={ |                 'private/get_positions', params={ | ||||||
|                     'currency': currency.upper(), |                     'currency': currency.upper(), | ||||||
|                     'kind': kind}) |                     'kind': kind}) | ||||||
|  | @ -244,20 +272,46 @@ class Client: | ||||||
| 
 | 
 | ||||||
|         return balances |         return balances | ||||||
| 
 | 
 | ||||||
|     async def get_assets(self) -> dict[str, float]: |     async def get_assets( | ||||||
|  |         self, | ||||||
|  |         venue: str | None = None, | ||||||
|  | 
 | ||||||
|  |     ) -> dict[str, Asset]: | ||||||
|         """Return the set of asset balances for this account |         """Return the set of asset balances for this account | ||||||
|         by symbol. |         by symbol. | ||||||
|         """ |         """ | ||||||
|         balances = {} |         assets = {} | ||||||
|  |         resp = await self._json_rpc_auth_wrapper( | ||||||
|  |             'public/get_currencies', | ||||||
|  |             params={} | ||||||
|  |         ) | ||||||
|  |         currencies = resp.result | ||||||
|  |         for currency in currencies: | ||||||
|  |             name = currency['currency'] | ||||||
|  |             tx_tick = digits_to_dec(currency['fee_precision'])  | ||||||
|  |             atype='crypto_currency' | ||||||
|  |             assets[name] = Asset( | ||||||
|  |                 name=name, | ||||||
|  |                 atype=atype, | ||||||
|  |                 tx_tick=tx_tick) | ||||||
| 
 | 
 | ||||||
|         for currency in self.currencies: |             instruments = await self.symbol_info(currency=name) | ||||||
|             resp = await self.json_rpc( |             for instrument in instruments: | ||||||
|                 'private/get_account_summary', params={ |                 pair = instruments[instrument] | ||||||
|                     'currency': currency.upper()}) |                 assets[pair.symbol] = Asset( | ||||||
|  |                     name=pair.symbol, | ||||||
|  |                     atype=pair.venue, | ||||||
|  |                     tx_tick=pair.size_tick) | ||||||
| 
 | 
 | ||||||
|             balances[currency] = resp.result['balance'] |         return assets  | ||||||
| 
 | 
 | ||||||
|         return balances |     async def get_mkt_pairs(self) -> dict[str, Pair]: | ||||||
|  |         flat: dict[str, Pair] = {} | ||||||
|  |         for key in self._pairs: | ||||||
|  |             item = self._pairs.get(key) | ||||||
|  |             flat[item.bs_fqme] = item | ||||||
|  | 
 | ||||||
|  |         return flat | ||||||
| 
 | 
 | ||||||
|     async def submit_limit( |     async def submit_limit( | ||||||
|         self, |         self, | ||||||
|  | @ -274,7 +328,7 @@ class Client: | ||||||
|             'type': 'limit', |             'type': 'limit', | ||||||
|             'price': price, |             'price': price, | ||||||
|         } |         } | ||||||
|         resp = await self.json_rpc( |         resp = await self._json_rpc_auth_wrapper( | ||||||
|             f'private/{action}', params) |             f'private/{action}', params) | ||||||
| 
 | 
 | ||||||
|         return resp.result |         return resp.result | ||||||
|  | @ -282,10 +336,32 @@ class Client: | ||||||
|     async def submit_cancel(self, oid: str): |     async def submit_cancel(self, oid: str): | ||||||
|         """Send cancel request for order id |         """Send cancel request for order id | ||||||
|         """ |         """ | ||||||
|         resp = await self.json_rpc( |         resp = await self._json_rpc_auth_wrapper( | ||||||
|             'private/cancel', {'order_id': oid}) |             'private/cancel', {'order_id': oid}) | ||||||
|         return resp.result |         return resp.result | ||||||
| 
 | 
 | ||||||
|  |     async def exch_info( | ||||||
|  |         self, | ||||||
|  |         sym: str | None = None, | ||||||
|  | 
 | ||||||
|  |         venue: MarketType = 'option', | ||||||
|  |         expiry: str | None = None, | ||||||
|  | 
 | ||||||
|  |     ) -> dict[str, Pair] | Pair: | ||||||
|  | 
 | ||||||
|  |         pair_table: dict[str, Pair] = self._pairs | ||||||
|  | 
 | ||||||
|  |         if ( | ||||||
|  |             sym | ||||||
|  |             and (cached_pair := pair_table.get(sym)) | ||||||
|  |         ): | ||||||
|  |             return cached_pair | ||||||
|  | 
 | ||||||
|  |         if sym: | ||||||
|  |             return pair_table[sym] | ||||||
|  |         else: | ||||||
|  |             return self._pairs | ||||||
|  | 
 | ||||||
|     async def symbol_info( |     async def symbol_info( | ||||||
|         self, |         self, | ||||||
|         instrument: Optional[str] = None, |         instrument: Optional[str] = None, | ||||||
|  | @ -293,7 +369,7 @@ class Client: | ||||||
|         kind: str = 'option', |         kind: str = 'option', | ||||||
|         expired: bool = False |         expired: bool = False | ||||||
| 
 | 
 | ||||||
|     ) -> dict[str, dict]: |     ) -> dict[str, Pair] | Pair: | ||||||
|         ''' |         ''' | ||||||
|         Get symbol infos. |         Get symbol infos. | ||||||
| 
 | 
 | ||||||
|  | @ -308,28 +384,65 @@ class Client: | ||||||
|             'expired': str(expired).lower() |             'expired': str(expired).lower() | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         resp: JSONRPCResult = await self.json_rpc( |         resp: JSONRPCResult = await self._json_rpc_auth_wrapper( | ||||||
|             'public/get_instruments', |             'public/get_instruments', | ||||||
|             params, |             params, | ||||||
|         ) |         ) | ||||||
|         # convert to symbol-keyed table |         # convert to symbol-keyed table | ||||||
|  |         pair_type: Type = PAIRTYPES[kind] | ||||||
|         results: list[dict] | None = resp.result |         results: list[dict] | None = resp.result | ||||||
|         instruments: dict[str, dict] = { |          | ||||||
|             item['instrument_name'].lower(): item |         instruments: dict[str, Pair] = {} | ||||||
|             for item in results |         for item in results: | ||||||
|         } |             symbol=item['instrument_name'].lower() | ||||||
|  |             try: | ||||||
|  |                 pair: Pair = pair_type( | ||||||
|  |                     symbol=symbol, | ||||||
|  |                     **item | ||||||
|  |                 ) | ||||||
|  |             except Exception as e: | ||||||
|  |                 e.add_note( | ||||||
|  |                     "\nDon't panic, prolly stupid deribit changed their symbology schema again..\n" | ||||||
|  |                     'Check out their API docs here:\n\n' | ||||||
|  |                     'https://docs.deribit.com/?python#deribit-api-v2-1-1' | ||||||
|  |                 ) | ||||||
|  |                 raise | ||||||
|  | 
 | ||||||
|  |             instruments[symbol] = pair | ||||||
| 
 | 
 | ||||||
|         if instrument is not None: |         if instrument is not None: | ||||||
|             return instruments[instrument] |             return instruments[instrument.lower()] | ||||||
|         else: |         else: | ||||||
|             return instruments |             return instruments | ||||||
| 
 | 
 | ||||||
|     async def cache_symbols( |     async def cache_symbols( | ||||||
|         self, |         self, | ||||||
|     ) -> dict: |         venue: MarketType = 'option', | ||||||
| 
 | 
 | ||||||
|         if not self._pairs: |     ) -> None: | ||||||
|             self._pairs = await self.symbol_info() |         # lookup internal mkt-specific pair table to update | ||||||
|  |         pair_table: dict[str, Pair] = self._pairs | ||||||
|  | 
 | ||||||
|  |         # make API request(s) | ||||||
|  |         mkt_pairs = await self.symbol_info() | ||||||
|  | 
 | ||||||
|  |         if not mkt_pairs: | ||||||
|  |             raise SymbolNotFound(f'No market pairs found!?:\n{resp}') | ||||||
|  | 
 | ||||||
|  |         pairs_view_subtable: dict[str, Pair] = {} | ||||||
|  | 
 | ||||||
|  |         for instrument in mkt_pairs: | ||||||
|  |             pair_type: Type = PAIRTYPES[venue] | ||||||
|  | 
 | ||||||
|  |             pair: Pair = pair_type(**mkt_pairs[instrument].to_dict()) | ||||||
|  | 
 | ||||||
|  |             pair_table[pair.symbol.upper()] = pair | ||||||
|  | 
 | ||||||
|  |             # update an additional top-level-cross-venue-table | ||||||
|  |             # `._pairs: ChainMap` for search B0 | ||||||
|  |             pairs_view_subtable[pair.bs_fqme] = pair | ||||||
|  | 
 | ||||||
|  |         self._pairs.maps.append(pairs_view_subtable) | ||||||
| 
 | 
 | ||||||
|         return self._pairs |         return self._pairs | ||||||
| 
 | 
 | ||||||
|  | @ -337,37 +450,35 @@ class Client: | ||||||
|         self, |         self, | ||||||
|         pattern: str, |         pattern: str, | ||||||
|         limit: int = 30, |         limit: int = 30, | ||||||
|     ) -> dict[str, Any]: |     ) -> dict[str, Pair]: | ||||||
|         ''' |         ''' | ||||||
|         Fuzzy search symbology set for pairs matching `pattern`. |         Fuzzy search symbology set for pairs matching `pattern`. | ||||||
| 
 | 
 | ||||||
|         ''' |         ''' | ||||||
|         pairs: dict[str, Any] = await self.symbol_info() |         pairs: dict[str, Pair] = await self.exch_info() | ||||||
|         matches: dict[str, Pair] = match_from_pairs( | 
 | ||||||
|  |         return match_from_pairs( | ||||||
|             pairs=pairs, |             pairs=pairs, | ||||||
|             query=pattern.upper(), |             query=pattern.upper(), | ||||||
|             score_cutoff=35, |             score_cutoff=35, | ||||||
|             limit=limit |             limit=limit | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|        # repack in name-keyed table |  | ||||||
|         return { |  | ||||||
|             pair['instrument_name'].lower(): pair |  | ||||||
|             for pair in matches.values() |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     async def bars( |     async def bars( | ||||||
|         self, |         self, | ||||||
|         symbol: str, |         mkt: MktPair, | ||||||
|  | 
 | ||||||
|         start_dt: Optional[datetime] = None, |         start_dt: Optional[datetime] = None, | ||||||
|         end_dt: Optional[datetime] = None, |         end_dt: Optional[datetime] = None, | ||||||
|  | 
 | ||||||
|         limit: int = 1000, |         limit: int = 1000, | ||||||
|         as_np: bool = True, |         as_np: bool = True, | ||||||
|     ) -> dict: | 
 | ||||||
|         instrument = symbol |     ) -> list[tuple] | np.ndarray: | ||||||
|  |         instrument: str = mkt.bs_fqme.split('.')[0] | ||||||
| 
 | 
 | ||||||
|         if end_dt is None: |         if end_dt is None: | ||||||
|             end_dt = pendulum.now('UTC') |             end_dt = now('UTC') | ||||||
| 
 | 
 | ||||||
|         if start_dt is None: |         if start_dt is None: | ||||||
|             start_dt = end_dt.start_of( |             start_dt = end_dt.start_of( | ||||||
|  | @ -377,7 +488,7 @@ class Client: | ||||||
|         end_time = deribit_timestamp(end_dt) |         end_time = deribit_timestamp(end_dt) | ||||||
| 
 | 
 | ||||||
|         # https://docs.deribit.com/#public-get_tradingview_chart_data |         # https://docs.deribit.com/#public-get_tradingview_chart_data | ||||||
|         resp = await self.json_rpc( |         resp = await self._json_rpc_auth_wrapper( | ||||||
|             'public/get_tradingview_chart_data', |             'public/get_tradingview_chart_data', | ||||||
|             params={ |             params={ | ||||||
|                 'instrument_name': instrument.upper(), |                 'instrument_name': instrument.upper(), | ||||||
|  | @ -387,36 +498,34 @@ class Client: | ||||||
|             }) |             }) | ||||||
| 
 | 
 | ||||||
|         result = KLinesResult(**resp.result) |         result = KLinesResult(**resp.result) | ||||||
|         new_bars = [] |         new_bars: list[tuple] = [] | ||||||
|         for i in range(len(result.close)): |         for i in range(len(result.close)): | ||||||
| 
 | 
 | ||||||
|             _open = result.open[i] |  | ||||||
|             high = result.high[i] |  | ||||||
|             low = result.low[i] |  | ||||||
|             close = result.close[i] |  | ||||||
|             volume = result.volume[i] |  | ||||||
| 
 |  | ||||||
|             row = [  |             row = [  | ||||||
|                 (start_time + (i * (60 * 1000))) / 1000.0,  # time |                 (start_time + (i * (60 * 1000))) / 1000.0,  # time | ||||||
|                 result.open[i], |                 result.open[i], | ||||||
|                 result.high[i], |                 result.high[i], | ||||||
|                 result.low[i], |                 result.low[i], | ||||||
|                 result.close[i], |                 result.close[i], | ||||||
|                 result.volume[i], |                 result.volume[i] | ||||||
|                 0 |  | ||||||
|             ] |             ] | ||||||
| 
 | 
 | ||||||
|             new_bars.append((i,) + tuple(row)) |             new_bars.append((i,) + tuple(row)) | ||||||
| 
 | 
 | ||||||
|         array = np.array(new_bars, dtype=def_iohlcv_fields) if as_np else klines |         if not as_np: | ||||||
|         return array |             return result | ||||||
|  | 
 | ||||||
|  |         return np.array( | ||||||
|  |             new_bars, | ||||||
|  |             dtype=def_iohlcv_fields | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|     async def last_trades( |     async def last_trades( | ||||||
|         self, |         self, | ||||||
|         instrument: str, |         instrument: str, | ||||||
|         count: int = 10 |         count: int = 10 | ||||||
|     ): |     ): | ||||||
|         resp = await self.json_rpc( |         resp = await self._json_rpc_auth_wrapper( | ||||||
|             'public/get_last_trades_by_instrument', |             'public/get_last_trades_by_instrument', | ||||||
|             params={ |             params={ | ||||||
|                 'instrument_name': instrument, |                 'instrument_name': instrument, | ||||||
|  | @ -428,78 +537,17 @@ class Client: | ||||||
| 
 | 
 | ||||||
| @acm | @acm | ||||||
| async def get_client( | async def get_client( | ||||||
|     is_brokercheck: bool = False |     is_brokercheck: bool = False, | ||||||
|  |     venue: MarketType = 'option', | ||||||
| ) -> Client: | ) -> Client: | ||||||
| 
 | 
 | ||||||
|     async with ( |     async with ( | ||||||
|         trio.open_nursery() as n, |         trio.open_nursery() as n, | ||||||
|         open_jsonrpc_session( |         open_jsonrpc_session( | ||||||
|             _testnet_ws_url, dtype=JSONRPCResult) as json_rpc |             _ws_url, response_type=JSONRPCResult | ||||||
|  |         ) as json_rpc | ||||||
|     ): |     ): | ||||||
|         client = Client(json_rpc) |         client = Client(json_rpc) | ||||||
| 
 |  | ||||||
|         _refresh_token: Optional[str] = None |  | ||||||
|         _access_token: Optional[str] = None |  | ||||||
| 
 |  | ||||||
|         async def _auth_loop( |  | ||||||
|             task_status: TaskStatus = trio.TASK_STATUS_IGNORED |  | ||||||
|         ): |  | ||||||
|             """Background task that adquires a first access token and then will |  | ||||||
|             refresh the access token while the nursery isn't cancelled. |  | ||||||
| 
 |  | ||||||
|             https://docs.deribit.com/?python#authentication-2 |  | ||||||
|             """ |  | ||||||
|             renew_time = 10 |  | ||||||
|             access_scope = 'trade:read_write' |  | ||||||
|             _expiry_time = time.time() |  | ||||||
|             got_access = False |  | ||||||
|             nonlocal _refresh_token |  | ||||||
|             nonlocal _access_token |  | ||||||
| 
 |  | ||||||
|             while True: |  | ||||||
|                 if time.time() - _expiry_time < renew_time: |  | ||||||
|                     # if we are close to token expiry time |  | ||||||
| 
 |  | ||||||
|                     if _refresh_token != None: |  | ||||||
|                         # if we have a refresh token already dont need to send |  | ||||||
|                         # secret |  | ||||||
|                         params = { |  | ||||||
|                             'grant_type': 'refresh_token', |  | ||||||
|                             'refresh_token': _refresh_token, |  | ||||||
|                             'scope': access_scope |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                     else: |  | ||||||
|                         # we don't have refresh token, send secret to initialize |  | ||||||
|                         params = { |  | ||||||
|                             'grant_type': 'client_credentials', |  | ||||||
|                             'client_id': client._key_id, |  | ||||||
|                             'client_secret': client._key_secret, |  | ||||||
|                             'scope': access_scope |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                     resp = await json_rpc('public/auth', params) |  | ||||||
|                     result = resp.result |  | ||||||
| 
 |  | ||||||
|                     _expiry_time = time.time() + result['expires_in'] |  | ||||||
|                     _refresh_token = result['refresh_token'] |  | ||||||
| 
 |  | ||||||
|                     if 'access_token' in result: |  | ||||||
|                         _access_token = result['access_token'] |  | ||||||
| 
 |  | ||||||
|                     if not got_access: |  | ||||||
|                         # first time this loop runs we must indicate task is |  | ||||||
|                         # started, we have auth |  | ||||||
|                         got_access = True |  | ||||||
|                         task_status.started() |  | ||||||
| 
 |  | ||||||
|                 else: |  | ||||||
|                     await trio.sleep(renew_time / 2) |  | ||||||
| 
 |  | ||||||
|         # if we have client creds launch auth loop |  | ||||||
|         if client._key_id is not None: |  | ||||||
|             await n.start(_auth_loop) |  | ||||||
| 
 |  | ||||||
|         await client.cache_symbols() |         await client.cache_symbols() | ||||||
|         yield client |         yield client | ||||||
|         n.cancel_scope.cancel() |         n.cancel_scope.cancel() | ||||||
|  | @ -523,7 +571,7 @@ async def maybe_open_feed_handler() -> trio.abc.ReceiveStream: | ||||||
| 
 | 
 | ||||||
| async def aio_price_feed_relay( | async def aio_price_feed_relay( | ||||||
|     fh: FeedHandler, |     fh: FeedHandler, | ||||||
|     instrument: Symbol, |     instrument: str, | ||||||
|     from_trio: asyncio.Queue, |     from_trio: asyncio.Queue, | ||||||
|     to_trio: trio.abc.SendChannel, |     to_trio: trio.abc.SendChannel, | ||||||
| ) -> None: | ) -> None: | ||||||
|  | @ -542,21 +590,33 @@ async def aio_price_feed_relay( | ||||||
|             'symbol': cb_sym_to_deribit_inst( |             'symbol': cb_sym_to_deribit_inst( | ||||||
|                 str_to_cb_sym(data.symbol)).lower(), |                 str_to_cb_sym(data.symbol)).lower(), | ||||||
|             'ticks': [ |             'ticks': [ | ||||||
|                 {'type': 'bid', |                 { | ||||||
|                     'price': float(data.bid_price), 'size': float(data.bid_size)}, |                     'type': 'bid', | ||||||
|                 {'type': 'bsize', |                     'price': float(data.bid_price), | ||||||
|                     'price': float(data.bid_price), 'size': float(data.bid_size)}, |                     'size': float(data.bid_size) | ||||||
|                 {'type': 'ask', |                 }, | ||||||
|                     'price': float(data.ask_price), 'size': float(data.ask_size)}, |                 { | ||||||
|                 {'type': 'asize', |                     'type': 'bsize', | ||||||
|                     'price': float(data.ask_price), 'size': float(data.ask_size)} |                     'price': float(data.bid_price), | ||||||
|  |                     'size': float(data.bid_size) | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     'type': 'ask', | ||||||
|  |                     'price': float(data.ask_price), | ||||||
|  |                     'size': float(data.ask_size) | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     'type': 'asize', | ||||||
|  |                     'price': float(data.ask_price), | ||||||
|  |                     'size': float(data.ask_size) | ||||||
|  |                 } | ||||||
|             ] |             ] | ||||||
|         })) |         })) | ||||||
| 
 |     sym: Symbol = piker_sym_to_cb_sym(instrument) | ||||||
|     fh.add_feed( |     fh.add_feed( | ||||||
|         DERIBIT, |         DERIBIT, | ||||||
|         channels=[TRADES, L1_BOOK], |         channels=[TRADES, L1_BOOK], | ||||||
|         symbols=[piker_sym_to_cb_sym(instrument)], |         symbols=[sym], | ||||||
|         callbacks={ |         callbacks={ | ||||||
|             TRADES: _trade, |             TRADES: _trade, | ||||||
|             L1_BOOK: _l1 |             L1_BOOK: _l1 | ||||||
|  | @ -597,9 +657,9 @@ async def maybe_open_price_feed( | ||||||
|     async with maybe_open_context( |     async with maybe_open_context( | ||||||
|         acm_func=open_price_feed, |         acm_func=open_price_feed, | ||||||
|         kwargs={ |         kwargs={ | ||||||
|             'instrument': instrument |             'instrument': instrument.split('.')[0] | ||||||
|         }, |         }, | ||||||
|         key=f'{instrument}-price', |         key=f'{instrument.split('.')[0]}-price', | ||||||
|     ) as (cache_hit, feed): |     ) as (cache_hit, feed): | ||||||
|         if cache_hit: |         if cache_hit: | ||||||
|             yield broadcast_receiver(feed, 10) |             yield broadcast_receiver(feed, 10) | ||||||
|  | @ -664,10 +724,10 @@ async def maybe_open_order_feed( | ||||||
|     async with maybe_open_context( |     async with maybe_open_context( | ||||||
|         acm_func=open_order_feed, |         acm_func=open_order_feed, | ||||||
|         kwargs={ |         kwargs={ | ||||||
|             'instrument': instrument, |             'instrument': instrument.split('.')[0], | ||||||
|             'fh': fh |             'fh': fh | ||||||
|         }, |         }, | ||||||
|         key=f'{instrument}-order', |         key=f'{instrument.split('.')[0]}-order', | ||||||
|     ) as (cache_hit, feed): |     ) as (cache_hit, feed): | ||||||
|         if cache_hit: |         if cache_hit: | ||||||
|             yield broadcast_receiver(feed, 10) |             yield broadcast_receiver(feed, 10) | ||||||
|  |  | ||||||
|  | @ -21,18 +21,33 @@ Deribit backend. | ||||||
| from contextlib import asynccontextmanager as acm | from contextlib import asynccontextmanager as acm | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from typing import Any, Optional, Callable | from typing import Any, Optional, Callable | ||||||
|  | from pprint import pformat | ||||||
| import time | import time | ||||||
| 
 | 
 | ||||||
| import trio | import trio | ||||||
| from trio_typing import TaskStatus | from trio_typing import TaskStatus | ||||||
| import pendulum | from pendulum import ( | ||||||
|  |     from_timestamp, | ||||||
|  |     now, | ||||||
|  | ) | ||||||
| from rapidfuzz import process as fuzzy | from rapidfuzz import process as fuzzy | ||||||
| import numpy as np | import numpy as np | ||||||
| import tractor | import tractor | ||||||
| 
 | 
 | ||||||
| from piker.brokers import open_cached_client | from piker.accounting import ( | ||||||
|  |     MktPair, | ||||||
|  |     unpack_fqme, | ||||||
|  | ) | ||||||
|  | from piker.brokers import ( | ||||||
|  |     open_cached_client, | ||||||
|  |     NoData, | ||||||
|  | ) | ||||||
|  | from piker._cacheables import ( | ||||||
|  |     async_lifo_cache, | ||||||
|  | ) | ||||||
| from piker.log import get_logger, get_console_log | from piker.log import get_logger, get_console_log | ||||||
| from piker.data import ShmArray | from piker.data import ShmArray | ||||||
|  | from piker.data.validate import FeedInit | ||||||
| from piker.brokers._util import ( | from piker.brokers._util import ( | ||||||
|     BrokerError, |     BrokerError, | ||||||
|     DataUnavailable, |     DataUnavailable, | ||||||
|  | @ -47,9 +62,13 @@ from cryptofeed.symbols import Symbol | ||||||
| from .api import ( | from .api import ( | ||||||
|     Client, Trade, |     Client, Trade, | ||||||
|     get_config, |     get_config, | ||||||
|     str_to_cb_sym, piker_sym_to_cb_sym, cb_sym_to_deribit_inst, |     piker_sym_to_cb_sym, cb_sym_to_deribit_inst, | ||||||
|     maybe_open_price_feed |     maybe_open_price_feed | ||||||
| ) | ) | ||||||
|  | from .venues import ( | ||||||
|  |     Pair, | ||||||
|  |     OptionPair, | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| _spawn_kwargs = { | _spawn_kwargs = { | ||||||
|     'infect_asyncio': True, |     'infect_asyncio': True, | ||||||
|  | @ -64,36 +83,107 @@ async def open_history_client( | ||||||
|     mkt: MktPair, |     mkt: MktPair, | ||||||
| ) -> tuple[Callable, int]: | ) -> tuple[Callable, int]: | ||||||
| 
 | 
 | ||||||
|     fnstrument: str = mkt.bs_fqme |  | ||||||
|     # TODO implement history getter for the new storage layer. |     # TODO implement history getter for the new storage layer. | ||||||
|     async with open_cached_client('deribit') as client: |     async with open_cached_client('deribit') as client: | ||||||
| 
 | 
 | ||||||
|         async def get_ohlc( |         async def get_ohlc( | ||||||
|             end_dt: Optional[datetime] = None, |             timeframe: float, | ||||||
|             start_dt: Optional[datetime] = None, |             end_dt: datetime | None = None, | ||||||
|  |             start_dt: datetime | None = None, | ||||||
| 
 | 
 | ||||||
|         ) -> tuple[ |         ) -> tuple[ | ||||||
|             np.ndarray, |             np.ndarray, | ||||||
|             datetime,  # start |             datetime,  # start | ||||||
|             datetime,  # end |             datetime,  # end | ||||||
|         ]: |         ]: | ||||||
|  |             if timeframe != 60: | ||||||
|  |                 raise DataUnavailable('Only 1m bars are supported') | ||||||
| 
 | 
 | ||||||
|             array = await client.bars( |             array: np.ndarray = await client.bars( | ||||||
|                 instrument, |                 mkt, | ||||||
|                 start_dt=start_dt, |                 start_dt=start_dt, | ||||||
|                 end_dt=end_dt, |                 end_dt=end_dt, | ||||||
|             ) |             ) | ||||||
|             if len(array) == 0: |             if len(array) == 0: | ||||||
|                 raise DataUnavailable |                 raise NoData( | ||||||
|  |                     f'No frame for {start_dt} -> {end_dt}\n' | ||||||
|  |                 ) | ||||||
| 
 | 
 | ||||||
|             start_dt = pendulum.from_timestamp(array[0]['time']) |             start_dt = from_timestamp(array[0]['time']) | ||||||
|             end_dt = pendulum.from_timestamp(array[-1]['time']) |             end_dt = from_timestamp(array[-1]['time']) | ||||||
|  | 
 | ||||||
|  |             times = array['time'] | ||||||
|  |             if not times.any(): | ||||||
|  |                 raise ValueError( | ||||||
|  |                     'Bad frame with null-times?\n\n' | ||||||
|  |                     f'{times}' | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |             if end_dt is None: | ||||||
|  |                 inow: int = round(time.time()) | ||||||
|  |                 if (inow - times[-1]) > 60: | ||||||
|  |                     await tractor.pause() | ||||||
| 
 | 
 | ||||||
|             return array, start_dt, end_dt |             return array, start_dt, end_dt | ||||||
| 
 | 
 | ||||||
|         yield get_ohlc, {'erlangs': 3, 'rate': 3} |         yield get_ohlc, {'erlangs': 3, 'rate': 3} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @async_lifo_cache() | ||||||
|  | async def get_mkt_info( | ||||||
|  |     fqme: str, | ||||||
|  | 
 | ||||||
|  | ) -> tuple[MktPair, Pair] | None: | ||||||
|  | 
 | ||||||
|  |     # uppercase since kraken bs_mktid is always upper | ||||||
|  |     if 'deribit' not in fqme.lower(): | ||||||
|  |         fqme += '.deribit' | ||||||
|  | 
 | ||||||
|  |     mkt_mode: str = '' | ||||||
|  |     broker, mkt_ep, venue, expiry = unpack_fqme(fqme) | ||||||
|  | 
 | ||||||
|  |     # NOTE: we always upper case all tokens to be consistent with | ||||||
|  |     # binance's symbology style for pairs, like `BTCUSDT`, but in | ||||||
|  |     # theory we could also just keep things lower case; as long as | ||||||
|  |     # we're consistent and the symcache matches whatever this func | ||||||
|  |     # returns, always! | ||||||
|  |     expiry: str = expiry.upper() | ||||||
|  |     venue: str = venue.upper() | ||||||
|  |     venue_lower: str = venue.lower() | ||||||
|  | 
 | ||||||
|  |     mkt_mode: str = 'option' | ||||||
|  | 
 | ||||||
|  |     async with open_cached_client( | ||||||
|  |         'deribit', | ||||||
|  |     ) as client: | ||||||
|  | 
 | ||||||
|  |         assets: dict[str, Asset] = await client.get_assets() | ||||||
|  |         pair_str: str = mkt_ep.lower() | ||||||
|  | 
 | ||||||
|  |         pair: Pair = await client.exch_info( | ||||||
|  |             sym=pair_str, | ||||||
|  |         ) | ||||||
|  |         mkt_mode = pair.venue | ||||||
|  |         client.mkt_mode = mkt_mode | ||||||
|  | 
 | ||||||
|  |         dst: Asset | None = assets.get(pair.bs_dst_asset) | ||||||
|  |         src: Asset | None = assets.get(pair.bs_src_asset) | ||||||
|  | 
 | ||||||
|  |         mkt = MktPair( | ||||||
|  |             dst=dst, | ||||||
|  |             src=src, | ||||||
|  |             price_tick=pair.price_tick, | ||||||
|  |             size_tick=pair.size_tick, | ||||||
|  |             bs_mktid=pair.symbol, | ||||||
|  |             expiry=pair.expiry, | ||||||
|  |             venue=mkt_mode, | ||||||
|  |             broker='deribit', | ||||||
|  |             _atype=mkt_mode, | ||||||
|  |             _fqme_without_src=True, | ||||||
|  |         ) | ||||||
|  |         return mkt, pair | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| async def stream_quotes( | async def stream_quotes( | ||||||
| 
 | 
 | ||||||
|     send_chan: trio.abc.SendChannel, |     send_chan: trio.abc.SendChannel, | ||||||
|  | @ -108,31 +198,26 @@ async def stream_quotes( | ||||||
|     # XXX: required to propagate ``tractor`` loglevel to piker logging |     # XXX: required to propagate ``tractor`` loglevel to piker logging | ||||||
|     get_console_log(loglevel or tractor.current_actor().loglevel) |     get_console_log(loglevel or tractor.current_actor().loglevel) | ||||||
| 
 | 
 | ||||||
|     sym = symbols[0] |     sym = symbols[0].split('.')[0] | ||||||
|  | 
 | ||||||
|  |     init_msgs: list[FeedInit] = [] | ||||||
| 
 | 
 | ||||||
|     async with ( |     async with ( | ||||||
|         open_cached_client('deribit') as client, |         open_cached_client('deribit') as client, | ||||||
|         send_chan as send_chan |         send_chan as send_chan | ||||||
|     ): |     ): | ||||||
| 
 | 
 | ||||||
|         init_msgs = { |         mkt, pair = await get_mkt_info(sym) | ||||||
|             # pass back token, and bool, signalling if we're the writer |  | ||||||
|             # and that history has been written |  | ||||||
|             sym: { |  | ||||||
|                 'symbol_info': { |  | ||||||
|                     'asset_type': 'option', |  | ||||||
|                     'price_tick_size': 0.0005 |  | ||||||
|                 }, |  | ||||||
|                 'shm_write_opts': {'sum_tick_vml': False}, |  | ||||||
|                 'fqsn': sym, |  | ||||||
|             }, |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|  |         # build out init msgs according to latest spec | ||||||
|  |         init_msgs.append( | ||||||
|  |             FeedInit(mkt_info=mkt) | ||||||
|  |         ) | ||||||
|         nsym = piker_sym_to_cb_sym(sym) |         nsym = piker_sym_to_cb_sym(sym) | ||||||
| 
 | 
 | ||||||
|         async with maybe_open_price_feed(sym) as stream: |         async with maybe_open_price_feed(sym) as stream: | ||||||
| 
 | 
 | ||||||
|             cache = await client.cache_symbols() |             cache = client._pairs | ||||||
| 
 | 
 | ||||||
|             last_trades = (await client.last_trades( |             last_trades = (await client.last_trades( | ||||||
|                 cb_sym_to_deribit_inst(nsym), count=1)).trades |                 cb_sym_to_deribit_inst(nsym), count=1)).trades | ||||||
|  | @ -174,12 +259,21 @@ async def open_symbol_search( | ||||||
|     async with open_cached_client('deribit') as client: |     async with open_cached_client('deribit') as client: | ||||||
| 
 | 
 | ||||||
|         # load all symbols locally for fast search |         # load all symbols locally for fast search | ||||||
|         cache = await client.cache_symbols() |         cache = client._pairs | ||||||
|         await ctx.started() |         await ctx.started() | ||||||
| 
 | 
 | ||||||
|         async with ctx.open_stream() as stream: |         async with ctx.open_stream() as stream: | ||||||
| 
 | 
 | ||||||
|  |             pattern: str | ||||||
|             async for pattern in stream: |             async for pattern in stream: | ||||||
|                 # repack in dict form |                 # NOTE: pattern fuzzy-matching is done within | ||||||
|                 await stream.send( |                 # the methd impl. | ||||||
|                     await client.search_symbols(pattern)) |                 pairs: dict[str, Pair] = await client.search_symbols( | ||||||
|  |                     pattern, | ||||||
|  |                 ) | ||||||
|  |                 # repack in fqme-keyed table | ||||||
|  |                 byfqme: dict[str, Pair] = {} | ||||||
|  |                 for pair in pairs.values(): | ||||||
|  |                     byfqme[pair.bs_fqme] = pair | ||||||
|  | 
 | ||||||
|  |                 await stream.send(byfqme) | ||||||
|  |  | ||||||
|  | @ -0,0 +1,191 @@ | ||||||
|  | # piker: trading gear for hackers | ||||||
|  | # Copyright (C) Tyler Goodlet (in stewardship for pikers) | ||||||
|  | 
 | ||||||
|  | # This program is free software: you can redistribute it and/or modify | ||||||
|  | # it under the terms of the GNU Affero General Public License as published by | ||||||
|  | # the Free Software Foundation, either version 3 of the License, or | ||||||
|  | # (at your option) any later version. | ||||||
|  | 
 | ||||||
|  | # This program is distributed in the hope that it will be useful, | ||||||
|  | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | # GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | # You should have received a copy of the GNU Affero General Public License | ||||||
|  | # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | """ | ||||||
|  | Per market data-type definitions and schemas types. | ||||||
|  | 
 | ||||||
|  | """ | ||||||
|  | from __future__ import annotations | ||||||
|  | import pendulum | ||||||
|  | from typing import ( | ||||||
|  |     Literal, | ||||||
|  | ) | ||||||
|  | from decimal import Decimal | ||||||
|  | 
 | ||||||
|  | from msgspec import field | ||||||
|  | 
 | ||||||
|  | from piker.types import Struct | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # API endpoint paths by venue / sub-API | ||||||
|  | _domain: str = 'deribit.com' | ||||||
|  | _url = f'https://www.{_domain}' | ||||||
|  | 
 | ||||||
|  | # WEBsocketz | ||||||
|  | _ws_url: str = f'wss://www.{_domain}/ws/api/v2' | ||||||
|  | 
 | ||||||
|  | # test nets | ||||||
|  | _testnet_ws_url: str = f'wss://test.{_domain}/ws/api/v2' | ||||||
|  | 
 | ||||||
|  | MarketType = Literal[ | ||||||
|  |     'option' | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_api_eps(venue: MarketType) -> tuple[str, str]: | ||||||
|  |     ''' | ||||||
|  |     Return API ep root paths per venue. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     return { | ||||||
|  |         'option': ( | ||||||
|  |             _ws_url, | ||||||
|  |         ), | ||||||
|  |     }[venue] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Pair(Struct, frozen=True, kw_only=True): | ||||||
|  | 
 | ||||||
|  |     symbol: str | ||||||
|  | 
 | ||||||
|  |     # src | ||||||
|  |     quote_currency: str # 'BTC' | ||||||
|  | 
 | ||||||
|  |     # dst | ||||||
|  |     base_currency: str # "BTC", | ||||||
|  | 
 | ||||||
|  |     tick_size: float # 0.0001 # [{'above_price': 0.005, 'tick_size': 0.0005}] | ||||||
|  |     tick_size_steps: list[dict[str, float]]  | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def price_tick(self) -> Decimal: | ||||||
|  |         return Decimal(str(self.tick_size_steps[0]['above_price'])) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def size_tick(self) -> Decimal: | ||||||
|  |         return Decimal(str(self.tick_size)) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def bs_fqme(self) -> str: | ||||||
|  |         return f'{self.symbol}' | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def bs_mktid(self) -> str: | ||||||
|  |         return f'{self.symbol}.{self.venue}' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class OptionPair(Pair, frozen=True): | ||||||
|  | 
 | ||||||
|  |     taker_commission: float # 0.0003 | ||||||
|  |     strike: float # 5000.0 | ||||||
|  |     settlement_period: str # 'day' | ||||||
|  |     settlement_currency: str # "BTC", | ||||||
|  |     rfq: bool # false | ||||||
|  |     price_index: str # 'btc_usd' | ||||||
|  |     option_type: str # 'call' | ||||||
|  |     min_trade_amount: float # 0.1 | ||||||
|  |     maker_commission: float # 0.0003 | ||||||
|  |     kind: str # 'option' | ||||||
|  |     is_active: bool # true | ||||||
|  |     instrument_type: str # 'reversed' | ||||||
|  |     instrument_name: str # 'BTC-1SEP24-55000-C' | ||||||
|  |     instrument_id: int # 364671 | ||||||
|  |     expiration_timestamp: int # 1725177600000 | ||||||
|  |     creation_timestamp: int # 1724918461000 | ||||||
|  |     counter_currency: str # 'USD'  | ||||||
|  |     contract_size: float # '1.0' | ||||||
|  |     block_trade_tick_size: float # '0.0001' | ||||||
|  |     block_trade_min_trade_amount: int # '25' | ||||||
|  |     block_trade_commission: float # '0.003' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     # NOTE: see `.data._symcache.SymbologyCache.load()` for why | ||||||
|  |     ns_path: str = 'piker.brokers.deribit:OptionPair' | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def expiry(self) -> str: | ||||||
|  |         iso_date = pendulum.from_timestamp(self.expiration_timestamp / 1000).isoformat() | ||||||
|  |         return iso_date  | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def venue(self) -> str: | ||||||
|  |         return 'option' | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def bs_fqme(self) -> str: | ||||||
|  |         return f'{self.symbol}' | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def bs_src_asset(self) -> str: | ||||||
|  |         return f'{self.quote_currency}' | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def bs_dst_asset(self) -> str: | ||||||
|  |         return f'{self.symbol}' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | PAIRTYPES: dict[MarketType, Pair] = { | ||||||
|  |     'option': OptionPair, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class JSONRPCResult(Struct): | ||||||
|  |     id: int | ||||||
|  |     usIn: int | ||||||
|  |     usOut: int | ||||||
|  |     usDiff: int | ||||||
|  |     testnet: bool | ||||||
|  |     jsonrpc: str = '2.0' | ||||||
|  |     error: Optional[dict] = None | ||||||
|  |     result: Optional[list[dict]] = None | ||||||
|  | 
 | ||||||
|  | class JSONRPCChannel(Struct): | ||||||
|  |     method: str | ||||||
|  |     params: dict | ||||||
|  |     jsonrpc: str = '2.0' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class KLinesResult(Struct): | ||||||
|  |     low: list[float] | ||||||
|  |     cost: list[float] | ||||||
|  |     high: list[float] | ||||||
|  |     open: list[float] | ||||||
|  |     close: list[float] | ||||||
|  |     ticks: list[int] | ||||||
|  |     status: str | ||||||
|  |     volume: list[float] | ||||||
|  | 
 | ||||||
|  | class Trade(Struct): | ||||||
|  |     iv: float | ||||||
|  |     price: float | ||||||
|  |     amount: float | ||||||
|  |     trade_id: str | ||||||
|  |     contracts: float | ||||||
|  |     direction: str | ||||||
|  |     trade_seq: int | ||||||
|  |     timestamp: int | ||||||
|  |     mark_price: float | ||||||
|  |     index_price: float | ||||||
|  |     tick_direction: int | ||||||
|  |     instrument_name: str | ||||||
|  |     combo_id: Optional[str] = '', | ||||||
|  |     combo_trade_id: Optional[int] = 0, | ||||||
|  |     block_trade_id: Optional[str] = '', | ||||||
|  |     block_trade_leg_count: Optional[int] = 0, | ||||||
|  | 
 | ||||||
|  | class LastTradesResult(Struct): | ||||||
|  |     trades: list[Trade] | ||||||
|  |     has_more: bool | ||||||
							
								
								
									
										224
									
								
								pyproject.toml
								
								
								
								
							
							
						
						
									
										224
									
								
								pyproject.toml
								
								
								
								
							|  | @ -15,8 +15,8 @@ | ||||||
| # You should have received a copy of the GNU Affero General Public License | # You should have received a copy of the GNU Affero General Public License | ||||||
| # along with this program.  If not, see <https://www.gnu.org/licenses/>. | # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
| [build-system] | [build-system] | ||||||
| requires = ["hatchling"] | requires = ["poetry-core"] | ||||||
| build-backend = "hatchling.build" | build-backend = "poetry.core.masonry.api" | ||||||
| 
 | 
 | ||||||
| # ------ - ------ | # ------ - ------ | ||||||
| 
 | 
 | ||||||
|  | @ -34,114 +34,122 @@ ignore = [] | ||||||
| 
 | 
 | ||||||
| # ------ - ------ | # ------ - ------ | ||||||
| 
 | 
 | ||||||
| [project] | [tool.poetry] | ||||||
| name = "piker" | name = "piker" | ||||||
| version = "0.1.0a0dev0" | version = "0.1.0.alpha0.dev0" | ||||||
| description = "trading gear for hackers" | description = "trading gear for hackers" | ||||||
| authors = [{ name = "Tyler Goodlet", email = "goodboy_foss@protonmail.com" }] | authors = ["Tyler Goodlet <goodboy_foss@protonmail.com>"] | ||||||
| requires-python = ">=3.12, <3.13" | license = "AGPLv3" | ||||||
| license = "AGPL-3.0-or-later" |  | ||||||
| readme = "README.rst" | readme = "README.rst" | ||||||
| keywords = [ | 
 | ||||||
|     "async", | # ------ - ------ | ||||||
|     "trading", | 
 | ||||||
|     "finance", | [tool.poetry.dependencies] | ||||||
|     "quant", | async-generator = "^1.10" | ||||||
|     "charting", | attrs = "^23.1.0" | ||||||
|  | bidict = "^0.22.1" | ||||||
|  | colorama = "^0.4.6" | ||||||
|  | colorlog = "^6.7.0" | ||||||
|  | cython = "^3.0.0" | ||||||
|  | greenback = "^1.1.1" | ||||||
|  | ib-insync = "^0.9.86" | ||||||
|  | msgspec = "^0.18.0" | ||||||
|  | numba = "^0.59.0" | ||||||
|  | numpy = "^1.25" | ||||||
|  | polars = "^0.18.13" | ||||||
|  | pygments = "^2.16.1" | ||||||
|  | python = ">=3.11, <3.13" | ||||||
|  | rich = "^13.5.2" | ||||||
|  | # setuptools = "^68.0.0" | ||||||
|  | tomli = "^2.0.1" | ||||||
|  | tomli-w = "^1.0.0" | ||||||
|  | trio-util = "^0.7.0" | ||||||
|  | trio-websocket = "^0.10.3" | ||||||
|  | typer = "^0.9.0" | ||||||
|  | rapidfuzz = "^3.5.2" | ||||||
|  | pdbp = "^1.5.0" | ||||||
|  | trio = "^0.24" | ||||||
|  | pendulum = "^3.0.0" | ||||||
|  | httpx = "^0.27.0" | ||||||
|  | 
 | ||||||
|  | [tool.poetry.dependencies.tractor] | ||||||
|  | develop = true | ||||||
|  | git = 'https://github.com/goodboy/tractor.git' | ||||||
|  | branch = 'asyncio_debugger_support' | ||||||
|  | # path = "../tractor" | ||||||
|  | 
 | ||||||
|  | [tool.poetry.dependencies.asyncvnc] | ||||||
|  | git = 'https://github.com/pikers/asyncvnc.git' | ||||||
|  | branch = 'main' | ||||||
|  | 
 | ||||||
|  | [tool.poetry.dependencies.tomlkit] | ||||||
|  | develop = true | ||||||
|  | git = 'https://github.com/pikers/tomlkit.git' | ||||||
|  | branch = 'piker_pin' | ||||||
|  | # path = "../tomlkit/" | ||||||
|  | 
 | ||||||
|  | [tool.poetry.group.uis] | ||||||
|  | optional = true | ||||||
|  | [tool.poetry.group.uis.dependencies] | ||||||
|  | # https://python-poetry.org/docs/managing-dependencies/#dependency-groups | ||||||
|  | # TODO: make sure the levenshtein shit compiles on nix.. | ||||||
|  | # rapidfuzz = {extras = ["speedup"], version = "^0.18.0"} | ||||||
|  | rapidfuzz = "^3.2.0" | ||||||
|  | qdarkstyle = ">=3.0.2" | ||||||
|  | pyqtgraph = { git = 'https://github.com/pikers/pyqtgraph.git' } | ||||||
|  | 
 | ||||||
|  | # ------ - ------ | ||||||
|  | pyqt6 = "^6.7.0" | ||||||
|  | 
 | ||||||
|  | [tool.poetry.group.dev] | ||||||
|  | optional = true | ||||||
|  | [tool.poetry.group.dev.dependencies] | ||||||
|  | # testing / CI | ||||||
|  | pytest = "^6.0.0" | ||||||
|  | elasticsearch = "^8.9.0" | ||||||
|  | xonsh = "^0.14.2" | ||||||
|  | prompt-toolkit = "3.0.40" | ||||||
|  | 
 | ||||||
|  | # console ehancements and eventually remote debugging | ||||||
|  | # extras/helpers. | ||||||
|  | # TODO: add a toolset that makes debugging a `pikerd` service | ||||||
|  | # (tree) easy to hack on directly using more or less the local env: | ||||||
|  | # - xonsh + xxh | ||||||
|  | # - rsyscall + pdbp | ||||||
|  | # - actor runtime control console like BEAM/OTP | ||||||
|  | 
 | ||||||
|  | # ------ - ------ | ||||||
|  | 
 | ||||||
|  | # TODO: add an `--only daemon` group for running non-ui / pikerd | ||||||
|  | # service tree in distributed mode B) | ||||||
|  | # https://python-poetry.org/docs/managing-dependencies/#installing-group-dependencies | ||||||
|  | # [tool.poetry.group.daemon.dependencies] | ||||||
|  | 
 | ||||||
|  | [tool.poetry.scripts] | ||||||
|  | piker = 'piker.cli:cli' | ||||||
|  | pikerd = 'piker.cli:pikerd' | ||||||
|  | ledger = 'piker.accounting.cli:ledger' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | [project] | ||||||
|  | name="piker" | ||||||
|  | keywords=[ | ||||||
|  |   "async", | ||||||
|  |   "trading", | ||||||
|  |   "finance", | ||||||
|  |   "quant", | ||||||
|  |   "charting", | ||||||
| ] | ] | ||||||
| classifiers = [ | classifiers=[ | ||||||
|     "Development Status :: 3 - Alpha", |   'Development Status :: 3 - Alpha', | ||||||
|     "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", |   "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", | ||||||
|     "Operating System :: POSIX :: Linux", |   'Operating System :: POSIX :: Linux', | ||||||
|     "Programming Language :: Python :: Implementation :: CPython", |   "Programming Language :: Python :: Implementation :: CPython", | ||||||
|     "Programming Language :: Python :: 3 :: Only", |   "Programming Language :: Python :: 3 :: Only", | ||||||
|     "Programming Language :: Python :: 3.11", |   "Programming Language :: Python :: 3.11", | ||||||
|     "Programming Language :: Python :: 3.12", |   "Programming Language :: Python :: 3.12", | ||||||
|     "Intended Audience :: Financial and Insurance Industry", |   'Intended Audience :: Financial and Insurance Industry', | ||||||
|     "Intended Audience :: Science/Research", |   'Intended Audience :: Science/Research', | ||||||
|     "Intended Audience :: Developers", |   'Intended Audience :: Developers', | ||||||
|     "Intended Audience :: Education", |   'Intended Audience :: Education', | ||||||
| ] | ] | ||||||
| dependencies = [ |  | ||||||
|     "async-generator >=1.10, <2.0.0", |  | ||||||
|     "attrs >=23.1.0, <24.0.0", |  | ||||||
|     "bidict >=0.22.1, <0.23.0", |  | ||||||
|     "colorama >=0.4.6, <0.5.0", |  | ||||||
|     "colorlog >=6.7.0, <7.0.0", |  | ||||||
|     "ib-insync >=0.9.86, <0.10.0", |  | ||||||
|     "numba >=0.59.0, <0.60.0", |  | ||||||
|     "numpy >=1.25, <2.0", |  | ||||||
|     "polars >=0.18.13, <0.19.0", |  | ||||||
|     "pygments >=2.16.1, <3.0.0", |  | ||||||
|     "rich >=13.5.2, <14.0.0", |  | ||||||
|     "tomli >=2.0.1, <3.0.0", |  | ||||||
|     "tomli-w >=1.0.0, <2.0.0", |  | ||||||
|     "trio-util >=0.7.0, <0.8.0", |  | ||||||
|     "trio-websocket >=0.10.3, <0.11.0", |  | ||||||
|     "typer >=0.9.0, <1.0.0", |  | ||||||
|     "rapidfuzz >=3.5.2, <4.0.0", |  | ||||||
|     "pdbp >=1.5.0, <2.0.0", |  | ||||||
|     "trio >=0.24, <0.25", |  | ||||||
|     "pendulum >=3.0.0, <4.0.0", |  | ||||||
|     "httpx >=0.27.0, <0.28.0", |  | ||||||
|     "cryptofeed >=2.4.0, <3.0.0", |  | ||||||
|     "pyarrow >=17.0.0, <18.0.0", |  | ||||||
|     "websockets ==12.0", |  | ||||||
|     "msgspec", |  | ||||||
|     "tractor", |  | ||||||
|     "asyncvnc", |  | ||||||
|     "tomlkit", |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [project.optional-dependencies] |  | ||||||
| uis = [ |  | ||||||
|     # https://docs.astral.sh/uv/concepts/projects/dependencies/#optional-dependencies |  | ||||||
|     # TODO: make sure the levenshtein shit compiles on nix.. |  | ||||||
|     # rapidfuzz = {extras = ["speedup"], version = "^0.18.0"} |  | ||||||
|     "rapidfuzz >=3.2.0, <4.0.0", |  | ||||||
|     "qdarkstyle >=3.0.2, <4.0.0", |  | ||||||
|     "pyqt6 >=6.7.0, <7.0.0", |  | ||||||
|     "pyqtgraph", |  | ||||||
| 
 |  | ||||||
|     # ------ - ------ |  | ||||||
| 
 |  | ||||||
|     # TODO: add an `--only daemon` group for running non-ui / pikerd |  | ||||||
|     # service tree in distributed mode B) |  | ||||||
|     # https://docs.astral.sh/uv/concepts/projects/dependencies/#optional-dependencies |  | ||||||
|     # [project.optional-dependencies] |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [dependency-groups] |  | ||||||
| dev = [ |  | ||||||
|     "pytest >=6.0.0, <7.0.0", |  | ||||||
|     "elasticsearch >=8.9.0, <9.0.0", |  | ||||||
|     "xonsh >=0.14.2, <0.15.0", |  | ||||||
|     "prompt-toolkit ==3.0.40", |  | ||||||
|     "cython >=3.0.0, <4.0.0", |  | ||||||
|     "greenback >=1.1.1, <2.0.0", |  | ||||||
|     # console ehancements and eventually remote debugging |  | ||||||
|     # extras/helpers. |  | ||||||
|     # TODO: add a toolset that makes debugging a `pikerd` service |  | ||||||
|     # (tree) easy to hack on directly using more or less the local env: |  | ||||||
|     # - xonsh + xxh |  | ||||||
|     # - rsyscall + pdbp |  | ||||||
|     # - actor runtime control console like BEAM/OTP |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [project.scripts] |  | ||||||
| piker = "piker.cli:cli" |  | ||||||
| pikerd = "piker.cli:pikerd" |  | ||||||
| ledger = "piker.accounting.cli:ledger" |  | ||||||
| 
 |  | ||||||
| [tool.hatch.build.targets.sdist] |  | ||||||
| include = ["piker"] |  | ||||||
| 
 |  | ||||||
| [tool.hatch.build.targets.wheel] |  | ||||||
| include = ["piker"] |  | ||||||
| 
 |  | ||||||
| [tool.uv.sources] |  | ||||||
| pyqtgraph = { git = "https://github.com/pikers/pyqtgraph.git" } |  | ||||||
| asyncvnc = { git = "https://github.com/pikers/asyncvnc.git", branch = "main" } |  | ||||||
| tomlkit = { git = "https://github.com/pikers/tomlkit.git", branch ="piker_pin" } |  | ||||||
| msgspec = { git = "https://github.com/jcrist/msgspec.git" } |  | ||||||
| tractor = { path = "../tractor" } |  | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue