diff --git a/piker/brokers/deribit/__init__.py b/piker/brokers/deribit/__init__.py index 4c0c1850..7499cd35 100644 --- a/piker/brokers/deribit/__init__.py +++ b/piker/brokers/deribit/__init__.py @@ -34,6 +34,9 @@ from .feed import ( # open_trade_dialog, # norm_trade_records, # ) +from .venues import ( + OptionPair, +) log = get_logger(__name__) @@ -43,6 +46,7 @@ __all__ = [ 'open_history_client', 'open_symbol_search', 'stream_quotes', + 'OptionPair', # 'norm_trade_records', ] diff --git a/piker/brokers/deribit/api.py b/piker/brokers/deribit/api.py index 03cc301e..b9b98ad9 100644 --- a/piker/brokers/deribit/api.py +++ b/piker/brokers/deribit/api.py @@ -19,10 +19,14 @@ Deribit backend. ''' import asyncio +from collections import ChainMap from contextlib import ( asynccontextmanager as acm, ) from datetime import datetime +from decimal import ( + Decimal, +) from functools import partial import time from typing import ( @@ -31,7 +35,7 @@ from typing import ( Callable, ) -import pendulum +from pendulum import now import trio from trio_typing import TaskStatus from rapidfuzz import process as fuzzy @@ -51,7 +55,25 @@ from cryptofeed.defines import ( OPTION, CALL, PUT ) 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 ( def_iohlcv_fields, 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 def deribit_timestamp(when): return int((when.timestamp() * 1000) + (when.microsecond / 1000)) diff --git a/piker/brokers/deribit/feed.py b/piker/brokers/deribit/feed.py index 821aab87..bd2fbe06 100644 --- a/piker/brokers/deribit/feed.py +++ b/piker/brokers/deribit/feed.py @@ -21,11 +21,15 @@ Deribit backend. from contextlib import asynccontextmanager as acm from datetime import datetime from typing import Any, Optional, Callable +from pprint import pformat import time import trio from trio_typing import TaskStatus -import pendulum +from pendulum import ( + from_timestamp, + now, +) from rapidfuzz import process as fuzzy import numpy as np import tractor @@ -50,6 +54,10 @@ from .api import ( str_to_cb_sym, piker_sym_to_cb_sym, cb_sym_to_deribit_inst, maybe_open_price_feed ) +from .venues import ( + Pair, + OptionPair, +) _spawn_kwargs = { 'infect_asyncio': True, diff --git a/piker/brokers/deribit/venues.py b/piker/brokers/deribit/venues.py new file mode 100644 index 00000000..91a1583f --- /dev/null +++ b/piker/brokers/deribit/venues.py @@ -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 . + +""" +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