From e8c5f7a531cc9b879ee7d16d305982f0a640c09e Mon Sep 17 00:00:00 2001 From: Nelson Torres Date: Mon, 3 Feb 2025 18:35:35 -0300 Subject: [PATCH 1/2] Extract logic from get_max_pain() All the max pain math now is in this two functions: - get_total_intrinsic_values(): calculate the total value for all strike_prices and stores then in a dict[str, Decimal] - `get_intrinsic_value_and_max_pain()` given the `intrinsic_values` dict, returns the `max_pain` strike price and the `total_intrinsic_value` for that `strike_price` --- examples/max_pain.py | 62 ++++++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/examples/max_pain.py b/examples/max_pain.py index e5deb96f..4fd2d4f8 100644 --- a/examples/max_pain.py +++ b/examples/max_pain.py @@ -49,6 +49,44 @@ async def max_pain_daemon( 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 update_oi_by_strikes(msg: tuple): nonlocal oi_by_strikes if 'oi' == msg[0]: @@ -74,30 +112,10 @@ async def max_pain_daemon( ''' 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 = get_total_intrinsic_values(oi_by_strikes) - 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 + total_intrinsic_value, max_pain = get_intrinsic_value_and_max_pain(intrinsic_values) return { 'timestamp': timestamp, -- 2.34.1 From 490aaa38746104e550153df39b1ed10ef3277b0c Mon Sep 17 00:00:00 2001 From: Nelson Torres Date: Mon, 3 Feb 2025 18:38:40 -0300 Subject: [PATCH 2/2] Add Plot Here is the `plot_graph()` that is in char of the bars, scatter and vertical line plot items. Also all the necessary code for the graph to be shown. --- examples/max_pain.py | 86 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/examples/max_pain.py b/examples/max_pain.py index 4fd2d4f8..29bbfb2f 100644 --- a/examples/max_pain.py +++ b/examples/max_pain.py @@ -10,6 +10,11 @@ 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]] @@ -87,6 +92,65 @@ async def max_pain_daemon( 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]: @@ -127,6 +191,16 @@ async def max_pain_daemon( 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) @@ -134,13 +208,21 @@ async def max_pain_daemon( 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']}') - print(f'total intrinsic value: {max_pain['total_intrinsic_value']}') + 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(): -- 2.34.1