From 4c5132fd578c4e1311486df9e189e00d0965c543 Mon Sep 17 00:00:00 2001 From: Nelson Torres Date: Thu, 30 Jan 2025 02:02:04 -0300 Subject: [PATCH] Max pain daemon: - To calculate the `max_pain` first we need an expiration date, get_expiration_dates()` retrieves them and the user then enters one of the shown, then using the select expiry_date on `get_instruments()` we are good to build the `oi_by_strikes` important! - Add `update_oi_by_strikes()`. - Add `check_if_complete()`. - `get_max_pain()`: here's where all the action takes place, the `oi_by_strikes` must be complete to start the calculations, - Use `maybe_open_oi_feed` for open a oi_feed. - Add `max_pain_readme.rst` --- examples/derivs/__init__.py | 0 examples/max_pain.py | 139 +++++++++++++++++++++++++++++++++++ examples/max_pain_readme.rst | 19 +++++ 3 files changed, 158 insertions(+) create mode 100644 examples/derivs/__init__.py create mode 100644 examples/max_pain.py create mode 100644 examples/max_pain_readme.rst diff --git a/examples/derivs/__init__.py b/examples/derivs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/max_pain.py b/examples/max_pain.py new file mode 100644 index 00000000..e5deb96f --- /dev/null +++ b/examples/max_pain.py @@ -0,0 +1,139 @@ +#!/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]] + 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 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 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) diff --git a/examples/max_pain_readme.rst b/examples/max_pain_readme.rst new file mode 100644 index 00000000..c24907a6 --- /dev/null +++ b/examples/max_pain_readme.rst @@ -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.