diff --git a/examples/max_pain.py b/examples/max_pain.py index e5deb96f..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]] @@ -49,6 +54,103 @@ 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 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]: @@ -74,30 +176,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, @@ -109,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) @@ -116,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():