Compare commits
16 Commits
b9321dbb49
...
490aaa3874
Author | SHA1 | Date |
---|---|---|
|
490aaa3874 | |
|
e8c5f7a531 | |
|
18e6b8beaa | |
|
cd592afed8 | |
|
d475347e53 | |
|
a4954393f9 | |
|
82be0e7935 | |
|
62c67163ec | |
|
52e45ccabf | |
|
0fcf278f8f | |
|
33387c4baa | |
|
44dcad1745 | |
|
3a65f95636 | |
|
7fdf6fb5ea | |
|
89e0e53e2f | |
|
2cdaf1ad94 |
|
@ -0,0 +1,239 @@
|
|||
#!/usr/bin/env python
|
||||
from decimal import (
|
||||
Decimal,
|
||||
)
|
||||
import trio
|
||||
import tractor
|
||||
from datetime import datetime
|
||||
from pprint import pformat
|
||||
from piker.brokers.deribit.api import (
|
||||
get_client,
|
||||
maybe_open_oi_feed,
|
||||
)
|
||||
import sys
|
||||
import pyqtgraph as pg
|
||||
from PyQt6 import QtCore
|
||||
from pyqtgraph import ScatterPlotItem, InfiniteLine
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
|
||||
def check_if_complete(
|
||||
oi: dict[str, dict[str, Decimal | None]]
|
||||
) -> bool:
|
||||
return all(
|
||||
oi[strike]['C'] is not None
|
||||
and
|
||||
oi[strike]['P'] is not None for strike in oi
|
||||
)
|
||||
|
||||
|
||||
async def max_pain_daemon(
|
||||
) -> None:
|
||||
oi_by_strikes: dict[str, dict[str, Decimal | None]]
|
||||
instruments: list[Symbol] = []
|
||||
expiry_dates: list[str]
|
||||
expiry_date: str
|
||||
currency: str = 'btc'
|
||||
kind: str = 'option'
|
||||
|
||||
async with get_client(
|
||||
) as client:
|
||||
expiry_dates: list[str] = await client.get_expiration_dates(
|
||||
currency=currency,
|
||||
kind=kind
|
||||
)
|
||||
|
||||
print(f'Available expiration dates for {currency}-{kind}:')
|
||||
print(f'{expiry_dates}')
|
||||
expiry_date = input('Please enter a valid expiration date: ').upper()
|
||||
print('Starting little daemon...')
|
||||
|
||||
oi_by_strikes: dict[str, dict[str, Decimal]]
|
||||
instruments = await client.get_instruments(
|
||||
expiry_date=expiry_date,
|
||||
)
|
||||
oi_by_strikes = client.get_strikes_dict(instruments)
|
||||
|
||||
|
||||
def get_total_intrinsic_values(
|
||||
oi_by_strikes: dict[str, dict[str, Decimal]]
|
||||
) -> dict[str, dict[str, Decimal]]:
|
||||
call_cash: Decimal = Decimal(0)
|
||||
put_cash: Decimal = Decimal(0)
|
||||
intrinsic_values: dict[str, dict[str, Decimal]] = {}
|
||||
closes: list = sorted(Decimal(close) for close in oi_by_strikes)
|
||||
|
||||
for strike, oi in oi_by_strikes.items():
|
||||
s = Decimal(strike)
|
||||
call_cash = sum(max(0, (s - c) * oi_by_strikes[str(c)]['C']) for c in closes)
|
||||
put_cash = sum(max(0, (c - s) * oi_by_strikes[str(c)]['P']) for c in closes)
|
||||
|
||||
intrinsic_values[strike] = {
|
||||
'C': call_cash,
|
||||
'P': put_cash,
|
||||
'total': call_cash + put_cash,
|
||||
}
|
||||
|
||||
return intrinsic_values
|
||||
|
||||
def get_intrinsic_value_and_max_pain(
|
||||
intrinsic_values: dict[str, dict[str, Decimal]]
|
||||
):
|
||||
# We meed to find the lowest value, so we start at
|
||||
# infinity to ensure that, and the max_pain must be
|
||||
# an amount greater than zero.
|
||||
total_intrinsic_value: Decimal = Decimal('Infinity')
|
||||
max_pain: Decimal = Decimal(0)
|
||||
|
||||
for strike, oi in oi_by_strikes.items():
|
||||
s = Decimal(strike)
|
||||
if intrinsic_values[strike]['total'] < total_intrinsic_value:
|
||||
total_intrinsic_value = intrinsic_values[strike]['total']
|
||||
max_pain = s
|
||||
|
||||
return total_intrinsic_value, max_pain
|
||||
|
||||
def plot_graph(
|
||||
oi_by_strikes: dict[str, dict[str, Decimal]],
|
||||
plot,
|
||||
):
|
||||
"""Update the bar graph with new open interest data."""
|
||||
plot.clear()
|
||||
|
||||
intrinsic_values = get_total_intrinsic_values(oi_by_strikes)
|
||||
|
||||
for strike_str in sorted(oi_by_strikes, key=lambda x: int(x)):
|
||||
strike = int(strike_str)
|
||||
calls_val = float(oi_by_strikes[strike_str]['C'])
|
||||
puts_val = float(oi_by_strikes[strike_str]['P'])
|
||||
|
||||
bar_c = pg.BarGraphItem(
|
||||
x=[strike - 100],
|
||||
height=[calls_val],
|
||||
width=200,
|
||||
pen='w',
|
||||
brush=(0, 0, 255, 150)
|
||||
)
|
||||
plot.addItem(bar_c)
|
||||
|
||||
bar_p = pg.BarGraphItem(
|
||||
x=[strike + 100],
|
||||
height=[puts_val],
|
||||
width=200,
|
||||
pen='w',
|
||||
brush=(255, 0, 0, 150)
|
||||
)
|
||||
plot.addItem(bar_p)
|
||||
|
||||
total_val = float(intrinsic_values[strike_str]['total']) / 100000
|
||||
|
||||
scatter_iv = ScatterPlotItem(
|
||||
x=[strike],
|
||||
y=[total_val],
|
||||
pen=pg.mkPen(color=(0, 255, 0), width=2),
|
||||
brush=pg.mkBrush(0, 255, 0, 150),
|
||||
size=3,
|
||||
symbol='o'
|
||||
)
|
||||
plot.addItem(scatter_iv)
|
||||
|
||||
_, max_pain = get_intrinsic_value_and_max_pain(intrinsic_values)
|
||||
|
||||
vertical_line = InfiniteLine(
|
||||
pos=max_pain,
|
||||
angle=90,
|
||||
pen=pg.mkPen(color='yellow', width=1, style=QtCore.Qt.PenStyle.DotLine),
|
||||
label=f'Max pain: {max_pain:,.0f}',
|
||||
labelOpts={
|
||||
'position': 0.85,
|
||||
'color': 'yellow',
|
||||
'movable': True
|
||||
}
|
||||
)
|
||||
plot.addItem(vertical_line)
|
||||
|
||||
def update_oi_by_strikes(msg: tuple):
|
||||
nonlocal oi_by_strikes
|
||||
if 'oi' == msg[0]:
|
||||
strike_price = msg[1]['strike_price']
|
||||
option_type = msg[1]['option_type']
|
||||
open_interest = msg[1]['open_interest']
|
||||
oi_by_strikes.setdefault(
|
||||
strike_price, {}
|
||||
).update(
|
||||
{option_type: open_interest}
|
||||
)
|
||||
|
||||
def get_max_pain(
|
||||
oi_by_strikes: dict[str, dict[str, Decimal]]
|
||||
) -> dict[str, str | Decimal]:
|
||||
'''
|
||||
This method requires only the strike_prices and oi for call
|
||||
and puts, the closes list are the same as the strike_prices
|
||||
the idea is to sum all the calls and puts cash for each strike
|
||||
and the ITM strikes from that strike, the lowest value is what we
|
||||
are looking for the intrinsic value.
|
||||
|
||||
'''
|
||||
|
||||
nonlocal timestamp
|
||||
|
||||
intrinsic_values = get_total_intrinsic_values(oi_by_strikes)
|
||||
|
||||
total_intrinsic_value, max_pain = get_intrinsic_value_and_max_pain(intrinsic_values)
|
||||
|
||||
return {
|
||||
'timestamp': timestamp,
|
||||
'expiry_date': expiry_date,
|
||||
'total_intrinsic_value': total_intrinsic_value,
|
||||
'max_pain': max_pain,
|
||||
}
|
||||
|
||||
async with maybe_open_oi_feed(
|
||||
instruments,
|
||||
) as oi_feed:
|
||||
# Initialize QApplication
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
win = pg.GraphicsLayoutWidget(show=True)
|
||||
win.setWindowTitle('Calls (blue) vs Puts (red)')
|
||||
|
||||
plot = win.addPlot(title='OI by Strikes')
|
||||
plot.showGrid(x=True, y=True)
|
||||
print('Plot initialized...')
|
||||
|
||||
async for msg in oi_feed:
|
||||
|
||||
update_oi_by_strikes(msg)
|
||||
if check_if_complete(oi_by_strikes):
|
||||
if 'oi' == msg[0]:
|
||||
timestamp = msg[1]['timestamp']
|
||||
max_pain = get_max_pain(oi_by_strikes)
|
||||
intrinsic_values = get_total_intrinsic_values(oi_by_strikes)
|
||||
|
||||
# graph here
|
||||
plot_graph(oi_by_strikes, plot)
|
||||
|
||||
print('-----------------------------------------------')
|
||||
print(f'timestamp: {datetime.fromtimestamp(max_pain['timestamp'])}')
|
||||
print(f'expiry_date: {max_pain['expiry_date']}')
|
||||
print(f'max_pain: {max_pain['max_pain']:,.0f}')
|
||||
print(f'total intrinsic value: {max_pain['total_intrinsic_value']:,.0f}')
|
||||
print('-----------------------------------------------')
|
||||
|
||||
# Process GUI events to keep the window responsive
|
||||
app.processEvents()
|
||||
|
||||
|
||||
async def main():
|
||||
|
||||
async with tractor.open_nursery() as n:
|
||||
|
||||
p: tractor.Portal = await n.start_actor(
|
||||
'max_pain_daemon',
|
||||
enable_modules=[__name__],
|
||||
infect_asyncio=True,
|
||||
)
|
||||
await p.run(max_pain_daemon)
|
||||
|
||||
if __name__ == '__main__':
|
||||
trio.run(main)
|
|
@ -0,0 +1,19 @@
|
|||
## Max Pain Calculation for Deribit Options
|
||||
|
||||
This feature, which calculates the max pain point for options traded on the Deribit exchange using cryptofeed library.
|
||||
|
||||
- Functions in the api module for fetching options data from Deribit. [commit](https://pikers.dev/pikers/piker/commit/da55856dd2876291f55a06eb0561438a912d8241)
|
||||
|
||||
- Compute the max pain point based on open interest data using deribit's api. [commit](https://pikers.dev/pikers/piker/commit/0d9d6e15ba0edeb662ec97f7599dd66af3046b94)
|
||||
|
||||
### How to test it?
|
||||
|
||||
**Before start:** in order to get this working with `uv`, you **must** use my `tractor` [fork](https://pikers.dev/ntorres/tractor/src/branch/aio_abandons) and this branch: `aio_abandons`, the reason is that I cherry-pick the `uv_migration` that guille made, for some reason that a didn't dive into, in my system y need tractor using `uv` too. quite hacky I guess.
|
||||
|
||||
1. `uv lock`
|
||||
|
||||
2. `uv run --no-dev python examples/max_pain.py`
|
||||
|
||||
3. A message should be display, enter one of the expiration date available.
|
||||
|
||||
4. The script should be up and running.
|
|
@ -51,6 +51,7 @@ __brokers__: list[str] = [
|
|||
'ib',
|
||||
'kraken',
|
||||
'kucoin',
|
||||
'deribit',
|
||||
|
||||
# broken but used to work
|
||||
# 'questrade',
|
||||
|
@ -61,7 +62,6 @@ __brokers__: list[str] = [
|
|||
# wstrade
|
||||
# iex
|
||||
|
||||
# deribit
|
||||
# bitso
|
||||
]
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ from .api import (
|
|||
get_client,
|
||||
)
|
||||
from .feed import (
|
||||
get_mkt_info,
|
||||
open_history_client,
|
||||
open_symbol_search,
|
||||
stream_quotes,
|
||||
|
@ -34,15 +35,20 @@ from .feed import (
|
|||
# open_trade_dialog,
|
||||
# norm_trade_records,
|
||||
# )
|
||||
from .venues import (
|
||||
OptionPair,
|
||||
)
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
__all__ = [
|
||||
'get_client',
|
||||
# 'trades_dialogue',
|
||||
'get_mkt_info',
|
||||
'open_history_client',
|
||||
'open_symbol_search',
|
||||
'stream_quotes',
|
||||
'OptionPair',
|
||||
# 'norm_trade_records',
|
||||
]
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -18,38 +18,59 @@
|
|||
Deribit backend.
|
||||
|
||||
'''
|
||||
from __future__ import annotations
|
||||
from contextlib import asynccontextmanager as acm
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional, Callable
|
||||
from typing import (
|
||||
# Any,
|
||||
# Optional,
|
||||
Callable,
|
||||
)
|
||||
# from pprint import pformat
|
||||
import time
|
||||
|
||||
import cryptofeed
|
||||
import trio
|
||||
from trio_typing import TaskStatus
|
||||
import pendulum
|
||||
from rapidfuzz import process as fuzzy
|
||||
from pendulum import (
|
||||
from_timestamp,
|
||||
)
|
||||
import numpy as np
|
||||
import tractor
|
||||
|
||||
from piker.brokers import open_cached_client
|
||||
from piker.log import get_logger, get_console_log
|
||||
from piker.data import ShmArray
|
||||
from piker.brokers._util import (
|
||||
BrokerError,
|
||||
from piker.accounting import (
|
||||
Asset,
|
||||
MktPair,
|
||||
unpack_fqme,
|
||||
)
|
||||
from piker.brokers import (
|
||||
open_cached_client,
|
||||
NoData,
|
||||
DataUnavailable,
|
||||
)
|
||||
|
||||
from cryptofeed import FeedHandler
|
||||
from cryptofeed.defines import (
|
||||
DERIBIT, L1_BOOK, TRADES, OPTION, CALL, PUT
|
||||
from piker._cacheables import (
|
||||
async_lifo_cache,
|
||||
)
|
||||
from cryptofeed.symbols import Symbol
|
||||
from piker.log import (
|
||||
get_logger,
|
||||
mk_repr,
|
||||
)
|
||||
from piker.data.validate import FeedInit
|
||||
|
||||
|
||||
from .api import (
|
||||
Client, Trade,
|
||||
get_config,
|
||||
str_to_cb_sym, piker_sym_to_cb_sym, cb_sym_to_deribit_inst,
|
||||
Client,
|
||||
# get_config,
|
||||
piker_sym_to_cb_sym,
|
||||
cb_sym_to_deribit_inst,
|
||||
str_to_cb_sym,
|
||||
maybe_open_price_feed
|
||||
)
|
||||
from .venues import (
|
||||
Pair,
|
||||
OptionPair,
|
||||
Trade,
|
||||
)
|
||||
|
||||
_spawn_kwargs = {
|
||||
'infect_asyncio': True,
|
||||
|
@ -64,90 +85,215 @@ async def open_history_client(
|
|||
mkt: MktPair,
|
||||
) -> tuple[Callable, int]:
|
||||
|
||||
fnstrument: str = mkt.bs_fqme
|
||||
# TODO implement history getter for the new storage layer.
|
||||
async with open_cached_client('deribit') as client:
|
||||
|
||||
pair: OptionPair = client._pairs[mkt.dst.name]
|
||||
# XXX NOTE, the cuckers use ms !!!
|
||||
creation_time_s: int = pair.creation_timestamp/1000
|
||||
|
||||
async def get_ohlc(
|
||||
end_dt: Optional[datetime] = None,
|
||||
start_dt: Optional[datetime] = None,
|
||||
timeframe: float,
|
||||
end_dt: datetime | None = None,
|
||||
start_dt: datetime | None = None,
|
||||
|
||||
) -> tuple[
|
||||
np.ndarray,
|
||||
datetime, # start
|
||||
datetime, # end
|
||||
]:
|
||||
if timeframe != 60:
|
||||
raise DataUnavailable('Only 1m bars are supported')
|
||||
|
||||
array = await client.bars(
|
||||
instrument,
|
||||
array: np.ndarray = await client.bars(
|
||||
mkt,
|
||||
start_dt=start_dt,
|
||||
end_dt=end_dt,
|
||||
)
|
||||
if len(array) == 0:
|
||||
raise DataUnavailable
|
||||
if (
|
||||
end_dt is None
|
||||
):
|
||||
raise DataUnavailable(
|
||||
'No history seems to exist yet?\n\n'
|
||||
f'{mkt}'
|
||||
)
|
||||
elif (
|
||||
end_dt
|
||||
and
|
||||
end_dt.timestamp() < creation_time_s
|
||||
):
|
||||
# the contract can't have history
|
||||
# before it was created.
|
||||
pair_type_str: str = type(pair).__name__
|
||||
create_dt: datetime = from_timestamp(creation_time_s)
|
||||
raise DataUnavailable(
|
||||
f'No history prior to\n'
|
||||
f'`{pair_type_str}.creation_timestamp: int = '
|
||||
f'{pair.creation_timestamp}\n\n'
|
||||
f'------ deribit sux ------\n'
|
||||
f'WHICH IN "NORMAL PEOPLE WHO USE EPOCH TIME" form is,\n'
|
||||
f'creation_time_s: {creation_time_s}\n'
|
||||
f'create_dt: {create_dt}\n'
|
||||
)
|
||||
raise NoData(
|
||||
f'No frame for {start_dt} -> {end_dt}\n'
|
||||
)
|
||||
|
||||
start_dt = pendulum.from_timestamp(array[0]['time'])
|
||||
end_dt = pendulum.from_timestamp(array[-1]['time'])
|
||||
start_dt = from_timestamp(array[0]['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
|
||||
|
||||
yield get_ohlc, {'erlangs': 3, 'rate': 3}
|
||||
yield (
|
||||
get_ohlc,
|
||||
{ # backfill config
|
||||
'erlangs': 3,
|
||||
'rate': 3,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@async_lifo_cache()
|
||||
async def get_mkt_info(
|
||||
fqme: str,
|
||||
|
||||
) -> tuple[MktPair, Pair|OptionPair] | 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,
|
||||
venue=mkt_mode,
|
||||
broker='deribit',
|
||||
_atype=mkt_mode,
|
||||
_fqme_without_src=True,
|
||||
|
||||
# expiry=pair.expiry,
|
||||
# XXX TODO, currently we don't use it since it's
|
||||
# already "described" in the `OptionPair.symbol: str`
|
||||
# and if we slap in the ISO repr it's kinda hideous..
|
||||
# -[ ] figure out the best either std
|
||||
)
|
||||
return mkt, pair
|
||||
|
||||
|
||||
async def stream_quotes(
|
||||
|
||||
send_chan: trio.abc.SendChannel,
|
||||
symbols: list[str],
|
||||
feed_is_live: trio.Event,
|
||||
loglevel: str = None,
|
||||
|
||||
# startup sync
|
||||
task_status: TaskStatus[tuple[dict, dict]] = trio.TASK_STATUS_IGNORED,
|
||||
|
||||
) -> None:
|
||||
# XXX: required to propagate ``tractor`` loglevel to piker logging
|
||||
get_console_log(loglevel or tractor.current_actor().loglevel)
|
||||
'''
|
||||
Open a live quote stream for the market set defined by `symbols`.
|
||||
|
||||
sym = symbols[0]
|
||||
Internally this starts a `cryptofeed.FeedHandler` inside an `asyncio`-side
|
||||
task and relays through L1 and `Trade` msgs here to our `trio.Task`.
|
||||
|
||||
'''
|
||||
sym = symbols[0].split('.')[0]
|
||||
init_msgs: list[FeedInit] = []
|
||||
|
||||
# multiline nested `dict` formatter (since rn quote-msgs are
|
||||
# just that).
|
||||
pfmt: Callable[[str], str] = mk_repr(
|
||||
# so we can see `deribit`'s delightfully mega-long bs fields..
|
||||
maxstring=100,
|
||||
)
|
||||
|
||||
async with (
|
||||
open_cached_client('deribit') as client,
|
||||
send_chan as send_chan
|
||||
):
|
||||
mkt: MktPair
|
||||
pair: Pair
|
||||
mkt, pair = await get_mkt_info(sym)
|
||||
|
||||
init_msgs = {
|
||||
# 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,
|
||||
)
|
||||
)
|
||||
# build `cryptofeed` feed-handle
|
||||
cf_sym: cryptofeed.Symbol = piker_sym_to_cb_sym(sym)
|
||||
|
||||
nsym = piker_sym_to_cb_sym(sym)
|
||||
from_cf: tractor.to_asyncio.LinkedTaskChannel
|
||||
async with maybe_open_price_feed(sym) as from_cf:
|
||||
|
||||
async with maybe_open_price_feed(sym) as stream:
|
||||
# load the "last trades" summary
|
||||
last_trades_res: cryptofeed.LastTradesResult = await client.last_trades(
|
||||
cb_sym_to_deribit_inst(cf_sym),
|
||||
count=1,
|
||||
)
|
||||
last_trades: list[Trade] = last_trades_res.trades
|
||||
|
||||
cache = await client.cache_symbols()
|
||||
# TODO, do we even need this or will the above always
|
||||
# work?
|
||||
# if not last_trades:
|
||||
# await tractor.pause()
|
||||
# async for typ, quote in from_cf:
|
||||
# if typ == 'trade':
|
||||
# last_trade = Trade(**(quote['data']))
|
||||
# break
|
||||
|
||||
last_trades = (await client.last_trades(
|
||||
cb_sym_to_deribit_inst(nsym), count=1)).trades
|
||||
# else:
|
||||
last_trade = Trade(
|
||||
**(last_trades[0])
|
||||
)
|
||||
|
||||
if len(last_trades) == 0:
|
||||
last_trade = None
|
||||
async for typ, quote in stream:
|
||||
if typ == 'trade':
|
||||
last_trade = Trade(**(quote['data']))
|
||||
break
|
||||
|
||||
else:
|
||||
last_trade = Trade(**(last_trades[0]))
|
||||
|
||||
first_quote = {
|
||||
first_quote: dict = {
|
||||
'symbol': sym,
|
||||
'last': last_trade.price,
|
||||
'brokerd_ts': last_trade.timestamp,
|
||||
|
@ -158,13 +304,84 @@ async def stream_quotes(
|
|||
'broker_ts': last_trade.timestamp
|
||||
}]
|
||||
}
|
||||
task_status.started((init_msgs, first_quote))
|
||||
task_status.started((
|
||||
init_msgs,
|
||||
first_quote,
|
||||
))
|
||||
|
||||
feed_is_live.set()
|
||||
|
||||
async for typ, quote in stream:
|
||||
topic = quote['symbol']
|
||||
await send_chan.send({topic: quote})
|
||||
# NOTE XXX, static for now!
|
||||
# => since this only handles ONE mkt feed at a time we
|
||||
# don't need a lookup table to map interleaved quotes
|
||||
# from multiple possible mkt-pairs
|
||||
topic: str = mkt.bs_fqme
|
||||
|
||||
# deliver until cancelled
|
||||
async for typ, ref in from_cf:
|
||||
match typ:
|
||||
case 'trade':
|
||||
trade: cryptofeed.types.Trade = ref
|
||||
|
||||
# TODO, re-impl this according to teh ideal
|
||||
# fqme for opts that we choose!!
|
||||
bs_fqme: str = cb_sym_to_deribit_inst(
|
||||
str_to_cb_sym(trade.symbol)
|
||||
).lower()
|
||||
|
||||
piker_quote: dict = {
|
||||
'symbol': bs_fqme,
|
||||
'last': trade.price,
|
||||
'broker_ts': time.time(),
|
||||
# ^TODO, name this `brokerd/datad_ts` and
|
||||
# use `time.time_ns()` ??
|
||||
'ticks': [{
|
||||
'type': 'trade',
|
||||
'price': float(trade.price),
|
||||
'size': float(trade.amount),
|
||||
'broker_ts': trade.timestamp,
|
||||
}],
|
||||
}
|
||||
log.info(
|
||||
f'deribit {typ!r} quote for {sym!r}\n\n'
|
||||
f'{trade}\n\n'
|
||||
f'{pfmt(piker_quote)}\n'
|
||||
)
|
||||
|
||||
case 'l1':
|
||||
book: cryptofeed.types.L1Book = ref
|
||||
|
||||
# TODO, so this is where we can possibly change things
|
||||
# and instead lever the `MktPair.bs_fqme: str` output?
|
||||
bs_fqme: str = cb_sym_to_deribit_inst(
|
||||
str_to_cb_sym(book.symbol)
|
||||
).lower()
|
||||
|
||||
piker_quote: dict = {
|
||||
'symbol': bs_fqme,
|
||||
'ticks': [
|
||||
|
||||
{'type': 'bid',
|
||||
'price': float(book.bid_price),
|
||||
'size': float(book.bid_size)},
|
||||
|
||||
{'type': 'bsize',
|
||||
'price': float(book.bid_price),
|
||||
'size': float(book.bid_size),},
|
||||
|
||||
{'type': 'ask',
|
||||
'price': float(book.ask_price),
|
||||
'size': float(book.ask_size),},
|
||||
|
||||
{'type': 'asize',
|
||||
'price': float(book.ask_price),
|
||||
'size': float(book.ask_size),}
|
||||
]
|
||||
}
|
||||
|
||||
await send_chan.send({
|
||||
topic: piker_quote,
|
||||
})
|
||||
|
||||
|
||||
@tractor.context
|
||||
|
@ -174,12 +391,21 @@ async def open_symbol_search(
|
|||
async with open_cached_client('deribit') as client:
|
||||
|
||||
# load all symbols locally for fast search
|
||||
cache = await client.cache_symbols()
|
||||
# cache = client._pairs
|
||||
await ctx.started()
|
||||
|
||||
async with ctx.open_stream() as stream:
|
||||
|
||||
pattern: str
|
||||
async for pattern in stream:
|
||||
# repack in dict form
|
||||
await stream.send(
|
||||
await client.search_symbols(pattern))
|
||||
|
||||
# NOTE: pattern fuzzy-matching is done within
|
||||
# the methd impl.
|
||||
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,196 @@
|
|||
# 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,
|
||||
Optional,
|
||||
)
|
||||
from decimal import Decimal
|
||||
|
||||
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'
|
||||
|
||||
# TODO, impl this without the MM:SS part of
|
||||
# the `'THH:MM:SS..'` etc..
|
||||
@property
|
||||
def expiry(self) -> str:
|
||||
iso_date = pendulum.from_timestamp(
|
||||
self.expiration_timestamp / 1000
|
||||
).isoformat()
|
||||
return iso_date
|
||||
|
||||
@property
|
||||
def venue(self) -> str:
|
||||
return f'{self.instrument_type}_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
|
Loading…
Reference in New Issue