Compare commits

..

No commits in common. "de352c10e380c31de7d5b6506c418827732be581" and "7cc90fb900cf47cd915336bdc66e1e46c1b25ec3" have entirely different histories.

5 changed files with 103 additions and 201 deletions

View File

@ -1,139 +0,0 @@
#!/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,
)
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]]
expiry_dates: list[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: str = input('Please enter a valid expiration date: ').upper()
print('Starting little daemon...')
instruments: list[Symbol] = []
oi_by_strikes: dict[str, dict[str, Decimal]]
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
# 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)
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,
}
if intrinsic_values[strike]['total'] < total_intrinsic_value:
total_intrinsic_value = intrinsic_values[strike]['total']
max_pain = s
return {
'timestamp': timestamp,
'expiry_date': expiry_date,
'total_intrinsic_value': total_intrinsic_value,
'max_pain': max_pain,
}
async with get_client(
) as client:
instruments = await client.get_instruments(
expiry_date=expiry_date,
)
oi_by_strikes = client.get_strikes_dict(instruments)
async with maybe_open_oi_feed(
instruments,
) as oi_feed:
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)
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']}')
print(f'total intrinsic value: {max_pain['total_intrinsic_value']}')
print('-----------------------------------------------')
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)

View File

@ -1,19 +0,0 @@
## 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.

39
max_pain.py 100644
View File

@ -0,0 +1,39 @@
#!/usr/bin/env python
import trio
import tractor
from piker.brokers.deribit.api import (
maybe_open_oi_feed,
)
async def max_pain_daemon(
) -> None:
def check_if_complete(
oi: dict[str, dict[str, Decimal | None]],
) -> bool:
for strike in oi:
if (
oi[strike]['C'] == None
or
oi[strike]['P'] == None
):
return False
return True
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)

View File

@ -234,18 +234,17 @@ def get_config() -> dict[str, Any]:
)
conf_option = section.get('option', {})
conf_log = conf_option.get('log', {})
return {
'deribit': {
'key_id': conf_option['key_id'],
'key_secret': conf_option['key_secret'],
},
'log': {
'filename': conf_log['filename'],
'level': conf_log['level'],
'disabled': conf_log['disabled'],
}
}
section = {} # 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'
section['log']['disabled'] = True
return section
def check_if_complete(
oi: dict[str, dict[str, Decimal | None]],
@ -425,29 +424,6 @@ class Client:
return response_list
async def get_expiration_dates(
self,
currency: str = 'btc',
kind: str = 'option',
) -> list[str]:
"""
Get a dict with all expiration dates listed as value and currency as key.
"""
params: dict[str, str] = {
'currency': currency.upper(),
'kind': kind,
}
r: JSONRPCResult = await self._json_rpc_auth_wrapper(
'public/get_expirations',
params,
)
resp = r.result
return resp[currency][kind]
def get_strikes_dict(
self,
instruments: list[Symbol],
@ -850,10 +826,16 @@ async def maybe_open_price_feed(
async def aio_open_interest_feed_relay(
fh: FeedHandler,
oi_by_strikes: dict[str, dict[str, Decimal]],
instruments: list[Symbol],
from_trio: asyncio.Queue,
to_trio: trio.abc.SendChannel,
) -> None:
intrinsic_values: dict[str, dict[str, Decimal]] = {}
total_intrinsic_value: Decimal = Decimal('Infinity')
max_pain: Decimal = Decimal(0)
async def _trade(
trade: Trade, # cryptofeed, NOT ours from `.venues`!
receipt_timestamp: int,
@ -875,6 +857,11 @@ async def aio_open_interest_feed_relay(
Proxy-thru `cryptofeed.FeedHandler` "oi" to `piker`-side.
'''
nonlocal intrinsic_values
nonlocal oi_by_strikes
nonlocal total_intrinsic_value
nonlocal max_pain
symbol: Symbol = str_to_cb_sym(oi.symbol)
piker_sym: str = cb_sym_to_deribit_inst(symbol)
(
@ -885,13 +872,36 @@ async def aio_open_interest_feed_relay(
) = tuple(
piker_sym.split('-')
)
msg = {
'timestamp': oi.timestamp,
'strike_price': strike_price,
'option_type': option_type,
'open_interest': Decimal(oi.open_interest),
}
to_trio.send_nowait(('oi', msg))
open_interest: Decimal = oi.open_interest
oi_by_strikes[f'{strike_price}'][f'{option_type}'] = open_interest
is_ready = check_if_complete(oi_by_strikes)
if is_ready:
for strike in oi_by_strikes:
s: Decimal = Decimal(f'{strike}')
closes: list[str] = sorted(oi_by_strikes.keys())
call_cash: Decimal = Decimal(0)
put_cash: Decimal = Decimal(0)
for close in closes:
c: Decimal = Decimal(f'{close}')
call_cash += max(0, (s - c) * oi_by_strikes[f'{close}']['C'])
put_cash += max(0, (c - s) * oi_by_strikes[f'{close}']['P'])
intrinsic_values[f'{strike}'] = {}
intrinsic_values[f'{strike}']['C'] = call_cash
intrinsic_values[f'{strike}']['P'] = put_cash
intrinsic_values[f'{strike}']['total'] = (call_cash + put_cash)
for strike in intrinsic_values:
if intrinsic_values[f'{strike}']['total'] < total_intrinsic_value:
total_intrinsic_value = intrinsic_values[f'{strike}']['total']
max_pain = strike
print('-----------------------------------------------')
print(f'expiry date: {expiry_date}')
print(f'max_pain: {max_pain}')
print(f'total intrinsic value: {total_intrinsic_value}')
print('-----------------------------------------------')
channels = [TRADES, OPEN_INTEREST]
@ -921,6 +931,16 @@ async def aio_open_interest_feed_relay(
async def open_oi_feed(
instruments: list[Symbol],
) -> to_asyncio.LinkedTaskChannel:
expiry_date: str = '6DEC24'
instruments: list[Symbol] = []
oi_by_strikes: dict[str, dict[str, Decimal]]
async with get_client(
) as client:
instruments = await client.get_instruments(
expiry_date=expiry_date,
)
oi_by_strikes = client.get_strikes_dict(instruments)
fh: FeedHandler
first: None
@ -932,6 +952,7 @@ async def open_oi_feed(
aio_open_interest_feed_relay,
fh,
instruments,
oi_by_strikes,
)
) as (first, chan)
):
@ -950,7 +971,7 @@ async def maybe_open_oi_feed(
kwargs={
'instruments': instruments
},
key=f'{instruments[0].base}',
key=f'{instruments[0]}',
) as (cache_hit, feed):
if cache_hit: