From a58595fcd73516abc6ccdafd0566a08622ff0aa9 Mon Sep 17 00:00:00 2001
From: Nelson Torres <nelson.torres.alvarado1@gmail.com>
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.