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`max_pain_deribit_backup
							parent
							
								
									b1cae45cab
								
							
						
					
					
						commit
						e1ea85ce3b
					
				|  | @ -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) | ||||
|  | @ -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. | ||||
		Loading…
	
		Reference in New Issue