Compare commits
	
		
			6 Commits 
		
	
	
		
			7a638157ce
			...
			1272c3efad
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 1272c3efad | |
|  | 34b74a0c8a | |
|  | 22831fc071 | |
|  | 6219ed151d | |
|  | ac6c05431a | |
|  | de2e9a82c2 | 
							
								
								
									
										148
									
								
								default.nix
								
								
								
								
							
							
						
						
									
										148
									
								
								default.nix
								
								
								
								
							|  | @ -1,82 +1,130 @@ | |||
| with (import <nixpkgs> {}); | ||||
| with python312Packages; | ||||
| let | ||||
|   glibStorePath = lib.getLib glib; | ||||
|   qtpyStorePath = lib.getLib qtpy; | ||||
|   pyqt6StorePath = lib.getLib pyqt6; | ||||
|   pyqt6SipStorePath = lib.getLib pyqt6-sip; | ||||
|   zstdStorePath = lib.getLib zstd; | ||||
|   dbusStorePath = lib.getLib dbus; | ||||
|   libGLStorePath = lib.getLib libGL; | ||||
|   freetypeStorePath = lib.getLib freetype; | ||||
|   qt6baseStorePath = lib.getLib qt6.qtbase; | ||||
|   rapidfuzzStorePath = lib.getLib rapidfuzz; | ||||
|   qdarkstyleStorePath = lib.getLib qdarkstyle; | ||||
|   fontconfigStorePath = lib.getLib fontconfig; | ||||
|   libxkbcommonStorePath = lib.getLib libxkbcommon; | ||||
|   xcbutilcursorStorePath = lib.getLib xcb-util-cursor; | ||||
| 
 | ||||
|   qtpyStorePath = lib.getLib python312Packages.qtpy; | ||||
|   pyqt6StorePath = lib.getLib python312Packages.pyqt6; | ||||
|   pyqt6SipStorePath = lib.getLib python312Packages.pyqt6-sip; | ||||
|   rapidfuzzStorePath = lib.getLib python312Packages.rapidfuzz; | ||||
|   qdarkstyleStorePath = lib.getLib python312Packages.qdarkstyle; | ||||
| 
 | ||||
|   xorgLibX11StorePath = lib.getLib xorg.libX11; | ||||
|   xorgLibxcbStorePath = lib.getLib xorg.libxcb; | ||||
|   xorgxcbutilwmStorePath = lib.getLib xorg.xcbutilwm; | ||||
|   xorgxcbutilimageStorePath = lib.getLib xorg.xcbutilimage; | ||||
|   xorgxcbutilerrorsStorePath = lib.getLib xorg.xcbutilerrors; | ||||
|   xorgxcbutilkeysymsStorePath = lib.getLib xorg.xcbutilkeysyms; | ||||
|   xorgxcbutilrenderutilStorePath = lib.getLib xorg.xcbutilrenderutil; | ||||
| in | ||||
| stdenv.mkDerivation { | ||||
|   name = "piker-qt6-poetry-shell"; | ||||
|   name = "piker-qt6-uv"; | ||||
|   buildInputs = [ | ||||
|     # System requirements. | ||||
|     glib | ||||
|     dbus | ||||
|     zstd | ||||
|     libGL | ||||
|     freetype | ||||
|     qt6.qtbase | ||||
|     libgcc.lib | ||||
|     fontconfig | ||||
|     libxkbcommon | ||||
| 
 | ||||
|     # Xorg requirements | ||||
|     xcb-util-cursor | ||||
|     xorg.libxcb | ||||
|     xorg.libX11 | ||||
|     xorg.xcbutilwm | ||||
|     xorg.xcbutilimage | ||||
|     xorg.xcbutilerrors | ||||
|     xorg.xcbutilkeysyms | ||||
|     xorg.xcbutilrenderutil | ||||
| 
 | ||||
|     # Python requirements. | ||||
|     python312Full | ||||
|     poetry-core | ||||
|     qdarkstyle | ||||
|     rapidfuzz | ||||
|     pyqt6 | ||||
|     qtpy | ||||
|     python312Packages.uv | ||||
|     python312Packages.qdarkstyle | ||||
|     python312Packages.rapidfuzz | ||||
|     python312Packages.pyqt6 | ||||
|     python312Packages.qtpy | ||||
|   ]; | ||||
|   src = null; | ||||
|   shellHook = '' | ||||
|     set -e | ||||
| 
 | ||||
|     export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${libgcc.lib}/lib:${glibStorePath}/lib | ||||
| 
 | ||||
|     # Set the Qt plugin path | ||||
|     # export QT_DEBUG_PLUGINS=1 | ||||
| 
 | ||||
|     QTBASE_PATH="${qt6baseStorePath}" | ||||
|     echo "qtbase path:    $QTBASE_PATH" | ||||
|     echo "" | ||||
|     export QT_PLUGIN_PATH="$QTBASE_PATH/lib/qt-6/plugins" | ||||
|     export QT_QPA_PLATFORM_PLUGIN_PATH="$QT_PLUGIN_PATH/platforms" | ||||
|     echo "qt plugin path: $QT_PLUGIN_PATH" | ||||
|     echo "" | ||||
|     QTBASE_PATH="${qt6baseStorePath}/lib" | ||||
|     QT_PLUGIN_PATH="$QTBASE_PATH/qt-6/plugins" | ||||
|     QT_QPA_PLATFORM_PLUGIN_PATH="$QT_PLUGIN_PATH/platforms" | ||||
| 
 | ||||
|     # Maybe create venv & install deps | ||||
|     poetry install --with uis | ||||
|     LIB_GCC_PATH="${libgcc.lib}/lib" | ||||
|     GLIB_PATH="${glibStorePath}/lib" | ||||
|     ZSTD_PATH="${zstdStorePath}/lib" | ||||
|     DBUS_PATH="${dbusStorePath}/lib" | ||||
|     LIBGL_PATH="${libGLStorePath}/lib" | ||||
|     FREETYPE_PATH="${freetypeStorePath}/lib" | ||||
|     FONTCONFIG_PATH="${fontconfigStorePath}/lib" | ||||
|     LIB_XKB_COMMON_PATH="${libxkbcommonStorePath}/lib" | ||||
| 
 | ||||
|     # Use pyqt6 from System, patch activate script | ||||
|     ACTIVATE_SCRIPT_PATH="$(poetry env info --path)/bin/activate" | ||||
|     XCB_UTIL_CURSOR_PATH="${xcbutilcursorStorePath}/lib" | ||||
|     XORG_LIB_X11_PATH="${xorgLibX11StorePath}/lib" | ||||
|     XORG_LIB_XCB_PATH="${xorgLibxcbStorePath}/lib" | ||||
|     XORG_XCB_UTIL_IMAGE_PATH="${xorgxcbutilimageStorePath}/lib" | ||||
|     XORG_XCB_UTIL_WM_PATH="${xorgxcbutilwmStorePath}/lib" | ||||
|     XORG_XCB_UTIL_RENDER_UTIL_PATH="${xorgxcbutilrenderutilStorePath}/lib" | ||||
|     XORG_XCB_UTIL_KEYSYMS_PATH="${xorgxcbutilkeysymsStorePath}/lib" | ||||
|     XORG_XCB_UTIL_ERRORS_PATH="${xorgxcbutilerrorsStorePath}/lib" | ||||
| 
 | ||||
|     export RPDFUZZ_PATH="${rapidfuzzStorePath}/lib/python3.12/site-packages" | ||||
|     export QDRKSTYLE_PATH="${qdarkstyleStorePath}/lib/python3.12/site-packages" | ||||
|     export QTPY_PATH="${qtpyStorePath}/lib/python3.12/site-packages" | ||||
|     export PYQT6_PATH="${pyqt6StorePath}/lib/python3.12/site-packages" | ||||
|     export PYQT6_SIP_PATH="${pyqt6SipStorePath}/lib/python3.12/site-packages" | ||||
|     echo "rapidfuzz at:   $RPDFUZZ_PATH" | ||||
|     echo "qdarkstyle at:  $QDRKSTYLE_PATH" | ||||
|     echo "qtpy at:        $QTPY_PATH"  | ||||
|     echo "pyqt6 at:       $PYQT6_PATH" | ||||
|     echo "pyqt6-sip at:   $PYQT6_SIP_PATH" | ||||
|     echo "" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$QTBASE_PATH" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$QT_PLUGIN_PATH" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$QT_QPA_PLATFORM_PLUGIN_PATH" | ||||
| 
 | ||||
|     PATCH="export PYTHONPATH=\"" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$LIB_GCC_PATH" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$DBUS_PATH" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$GLIB_PATH" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$ZSTD_PATH" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$LIBGL_PATH" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$FONTCONFIG_PATH" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$FREETYPE_PATH" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$LIB_XKB_COMMON_PATH" | ||||
| 
 | ||||
|     PATCH="$PATCH\$RPDFUZZ_PATH" | ||||
|     PATCH="$PATCH:\$QDRKSTYLE_PATH" | ||||
|     PATCH="$PATCH:\$QTPY_PATH" | ||||
|     PATCH="$PATCH:\$PYQT6_PATH" | ||||
|     PATCH="$PATCH:\$PYQT6_SIP_PATH" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$XCB_UTIL_CURSOR_PATH" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$XORG_LIB_X11_PATH" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$XORG_LIB_XCB_PATH" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$XORG_XCB_UTIL_IMAGE_PATH" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$XORG_XCB_UTIL_WM_PATH" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$XORG_XCB_UTIL_RENDER_UTIL_PATH" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$XORG_XCB_UTIL_KEYSYMS_PATH" | ||||
|     LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$XORG_XCB_UTIL_ERRORS_PATH" | ||||
| 
 | ||||
|     PATCH="$PATCH\"" | ||||
|     export LD_LIBRARY_PATH | ||||
| 
 | ||||
|     if grep -q "$PATCH" "$ACTIVATE_SCRIPT_PATH"; then | ||||
|         echo "venv is already patched." | ||||
|     else | ||||
|         echo "patching $ACTIVATE_SCRIPT_PATH to use pyqt6 from nixos..." | ||||
|         sed -i "\$i$PATCH" $ACTIVATE_SCRIPT_PATH | ||||
|     fi | ||||
|     RPDFUZZ_PATH="${rapidfuzzStorePath}/lib/python3.12/site-packages" | ||||
|     QDRKSTYLE_PATH="${qdarkstyleStorePath}/lib/python3.12/site-packages" | ||||
|     QTPY_PATH="${qtpyStorePath}/lib/python3.12/site-packages" | ||||
|     PYQT6_PATH="${pyqt6StorePath}/lib/python3.12/site-packages" | ||||
|     PYQT6_SIP_PATH="${pyqt6SipStorePath}/lib/python3.12/site-packages" | ||||
| 
 | ||||
|     PATCH="$PATCH:$RPDFUZZ_PATH" | ||||
|     PATCH="$PATCH:$QDRKSTYLE_PATH" | ||||
|     PATCH="$PATCH:$QTPY_PATH" | ||||
|     PATCH="$PATCH:$PYQT6_PATH" | ||||
|     PATCH="$PATCH:$PYQT6_SIP_PATH" | ||||
| 
 | ||||
|     export PATCH  | ||||
| 
 | ||||
|     # Install deps | ||||
|     uv lock | ||||
| 
 | ||||
|     poetry shell | ||||
|   ''; | ||||
| } | ||||
|  |  | |||
|  | @ -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. | ||||
|  | @ -52,12 +52,14 @@ from cryptofeed import FeedHandler | |||
| from cryptofeed.defines import ( | ||||
|     DERIBIT, | ||||
|     L1_BOOK, TRADES, | ||||
|     OPTION, CALL, PUT | ||||
|     OPTION, CALL, PUT, | ||||
|     OPEN_INTEREST, | ||||
| ) | ||||
| from cryptofeed.symbols import Symbol | ||||
| from cryptofeed.types import ( | ||||
|     L1Book, | ||||
|     Trade, | ||||
|     OpenInterest, | ||||
| ) | ||||
| from piker.brokers import SymbolNotFound | ||||
| from .venues import ( | ||||
|  | @ -110,6 +112,10 @@ def deribit_timestamp(when: datetime) -> int: | |||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def get_timestamp_int(expiry_date: str) -> int: | ||||
|     return int(time.mktime(time.strptime(expiry_date, '%d%b%y'))) | ||||
| 
 | ||||
| 
 | ||||
| def str_to_cb_sym(name: str) -> Symbol: | ||||
|     base, strike_price, expiry_date, option_type = name.split('-') | ||||
| 
 | ||||
|  | @ -122,8 +128,9 @@ def str_to_cb_sym(name: str) -> Symbol: | |||
|     else: | ||||
|         raise Exception("Couldn\'t parse option type") | ||||
| 
 | ||||
|     new_expiry_date = get_values_from_cb_normalized_date(expiry_date) | ||||
| 
 | ||||
|     new_expiry_date: int = get_timestamp_int( | ||||
|         get_values_from_cb_normalized_date(expiry_date) | ||||
|     ) | ||||
|     return Symbol( | ||||
|         base=base, | ||||
|         quote=quote, | ||||
|  | @ -143,11 +150,12 @@ def piker_sym_to_cb_sym(name: str) -> Symbol: | |||
|     )= tuple( | ||||
|         name.upper().split('-')) | ||||
| 
 | ||||
|     new_expiry_date = get_timestamp_int(expiry_date) | ||||
|     quote: str = base | ||||
| 
 | ||||
|     if option_type == 'P': | ||||
|     if option_type == 'P' or option_type == 'PUT': | ||||
|         option_type = PUT  | ||||
|     elif option_type == 'C': | ||||
|     elif option_type == 'C' or option_type == 'CALL': | ||||
|         option_type = CALL | ||||
|     else: | ||||
|         raise Exception("Couldn\'t parse option type") | ||||
|  | @ -158,7 +166,7 @@ def piker_sym_to_cb_sym(name: str) -> Symbol: | |||
|         type=OPTION, | ||||
|         strike_price=strike_price, | ||||
|         option_type=option_type, | ||||
|         expiry_date=expiry_date | ||||
|         expiry_date=new_expiry_date | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -226,16 +234,18 @@ def get_config() -> dict[str, Any]: | |||
|         ) | ||||
| 
 | ||||
|     conf_option = section.get('option', {}) | ||||
|     section.clear # clear the dict to reuse it | ||||
|     section['deribit'] = {} | ||||
|     section['deribit']['key_id'] = conf_option.get('api_key') | ||||
|     section['deribit']['key_secret'] = conf_option.get('api_secret') | ||||
| 
 | ||||
|     section['log'] = {} | ||||
|     section['log']['filename'] = 'feedhandler.log' | ||||
|     section['log']['level'] = 'DEBUG' | ||||
| 
 | ||||
|     return section | ||||
|     conf_log = conf_option.get('log', {}) | ||||
|     return { | ||||
|         'deribit': { | ||||
|             'key_id': conf_option['key_id'], | ||||
|             'key_secret': conf_option['key_secret'], | ||||
|         }, | ||||
|         'log': { | ||||
|             'filename': conf_log['filename'], | ||||
|             'level': conf_log['level'], | ||||
|             'disabled': conf_log['disabled'], | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| class Client: | ||||
|  | @ -311,6 +321,20 @@ class Client: | |||
| 
 | ||||
|         return balances | ||||
| 
 | ||||
|     async def get_currencies( | ||||
|         self, | ||||
| 
 | ||||
|     ) -> list[dict]: | ||||
|         ''' | ||||
|         Return the set of currencies for deribit. | ||||
|         ''' | ||||
|         assets = {} | ||||
|         resp = await self._json_rpc_auth_wrapper( | ||||
|             'public/get_currencies', | ||||
|             params={} | ||||
|         ) | ||||
|         return resp.result | ||||
| 
 | ||||
|     async def get_assets( | ||||
|         self, | ||||
|         venue: str | None = None, | ||||
|  | @ -323,11 +347,7 @@ class Client: | |||
| 
 | ||||
|         ''' | ||||
|         assets = {} | ||||
|         resp = await self._json_rpc_auth_wrapper( | ||||
|             'public/get_currencies', | ||||
|             params={} | ||||
|         ) | ||||
|         currencies: list[dict] = resp.result | ||||
|         currencies = await self.get_currencies() | ||||
|         for currency in currencies: | ||||
|             name: str = currency['currency'] | ||||
|             tx_tick: Decimal = digits_to_dec(currency['fee_precision']) | ||||
|  | @ -359,6 +379,82 @@ class Client: | |||
| 
 | ||||
|         return flat | ||||
| 
 | ||||
|     async def get_instruments( | ||||
|         self, | ||||
|         currency: str = 'btc', | ||||
|         kind: str = 'option', | ||||
|         expired: bool = False, | ||||
|         expiry_date: str = None, | ||||
| 
 | ||||
|     ) -> list[Symbol]: | ||||
|         """ | ||||
|         Get instruments for cryptoFeed.FeedHandler. | ||||
|         """ | ||||
|         params: dict[str, str] = { | ||||
|             'currency': currency.upper(), | ||||
|             'kind': kind, | ||||
|             'expired': expired, | ||||
|         } | ||||
| 
 | ||||
|         r: JSONRPCResult = await self._json_rpc_auth_wrapper( | ||||
|             'public/get_instruments', | ||||
|             params, | ||||
|         ) | ||||
|         resp = r.result | ||||
|         response_list = [] | ||||
| 
 | ||||
|         for i in range(len(resp)): | ||||
|             element = resp[i] | ||||
|             name = f'{element["instrument_name"].split("-")[1]}' | ||||
|             if not expiry_date or name == expiry_date.upper(): | ||||
|                 response_list.append(piker_sym_to_cb_sym(element['instrument_name'])) | ||||
| 
 | ||||
|         return response_list | ||||
| 
 | ||||
|     async def get_expiration_dates( | ||||
|         self, | ||||
|         currency: str = 'btc', | ||||
|         kind: str = 'option', | ||||
| 
 | ||||
|     ) ->  list[str]: | ||||
|         """ | ||||
|         Get a dict with all expiration dates listed as value and currency as key. | ||||
|         """ | ||||
|          | ||||
|         params: dict[str, str] = { | ||||
|             'currency': currency.upper(), | ||||
|             'kind': kind, | ||||
|         } | ||||
| 
 | ||||
|         r: JSONRPCResult = await self._json_rpc_auth_wrapper( | ||||
|             'public/get_expirations', | ||||
|             params, | ||||
|         ) | ||||
|         resp = r.result | ||||
| 
 | ||||
|         return resp[currency][kind] | ||||
| 
 | ||||
|     def get_strikes_dict( | ||||
|         self, | ||||
|         instruments: list[Symbol], | ||||
| 
 | ||||
|     ) -> dict[str, dict[str, Decimal | None]]: | ||||
|         """ | ||||
|         Get a dict with strike prices as keys. | ||||
|         """ | ||||
| 
 | ||||
|         response: dict[str, dict[str, Decimal | None]] = {} | ||||
| 
 | ||||
|         for i in range(len(instruments)): | ||||
|             element = instruments[i] | ||||
|             strike = f'{str(element).split('-')[1]}' | ||||
|             response[f'{strike}'] = { | ||||
|                 'C': None, | ||||
|                 'P': None, | ||||
|             } | ||||
| 
 | ||||
|         return response | ||||
| 
 | ||||
|     async def submit_limit( | ||||
|         self, | ||||
|         symbol: str, | ||||
|  | @ -738,6 +834,116 @@ async def maybe_open_price_feed( | |||
|             yield feed | ||||
| 
 | ||||
| 
 | ||||
| async def aio_open_interest_feed_relay( | ||||
|     fh: FeedHandler, | ||||
|     instruments: list[Symbol], | ||||
|     from_trio: asyncio.Queue, | ||||
|     to_trio: trio.abc.SendChannel, | ||||
| ) -> None: | ||||
|     async def _trade( | ||||
|         trade: Trade,  # cryptofeed, NOT ours from `.venues`! | ||||
|         receipt_timestamp: int, | ||||
|     ) -> None: | ||||
|         ''' | ||||
|         Proxy-thru `cryptofeed.FeedHandler` "trades" to `piker`-side. | ||||
| 
 | ||||
|         ''' | ||||
|         to_trio.send_nowait(('trade', trade)) | ||||
| 
 | ||||
| 	# trade and oi are user defined functions that | ||||
| 	# will be called when trade and open interest updates are received | ||||
| 	# data type is not dict, is an object: cryptofeed.types.OpenINterest | ||||
|     async def _oi( | ||||
|         oi: OpenInterest, | ||||
|         receipt_timestamp: int, | ||||
|     ) -> None: | ||||
|         ''' | ||||
|         Proxy-thru `cryptofeed.FeedHandler` "oi" to `piker`-side. | ||||
| 
 | ||||
|         ''' | ||||
|         symbol: Symbol = str_to_cb_sym(oi.symbol) | ||||
|         piker_sym: str = cb_sym_to_deribit_inst(symbol) | ||||
|         ( | ||||
|             base, | ||||
|             expiry_date, | ||||
|             strike_price, | ||||
|             option_type | ||||
|         ) = tuple( | ||||
|             piker_sym.split('-') | ||||
|         ) | ||||
|         msg = { | ||||
|             'timestamp': oi.timestamp, | ||||
|             'strike_price': strike_price, | ||||
|             'option_type': option_type, | ||||
|             'open_interest': Decimal(oi.open_interest), | ||||
|         } | ||||
|         to_trio.send_nowait(('oi', msg)) | ||||
| 
 | ||||
| 
 | ||||
|     channels = [TRADES, OPEN_INTEREST] | ||||
|     callbacks={TRADES: _trade, OPEN_INTEREST: _oi} | ||||
| 
 | ||||
|     fh.add_feed( | ||||
|         DERIBIT, | ||||
|         channels=channels, | ||||
|         symbols=instruments, | ||||
|         callbacks=callbacks | ||||
|     ) | ||||
| 
 | ||||
|     if not fh.running: | ||||
|         fh.run( | ||||
|             start_loop=False, | ||||
|             install_signal_handlers=False | ||||
|         ) | ||||
| 
 | ||||
|     # sync with trio | ||||
|     to_trio.send_nowait(None) | ||||
| 
 | ||||
|     # run until cancelled | ||||
|     await asyncio.sleep(float('inf')) | ||||
| 
 | ||||
| 
 | ||||
| @acm | ||||
| async def open_oi_feed( | ||||
|     instruments: list[Symbol],  | ||||
| ) -> to_asyncio.LinkedTaskChannel: | ||||
| 
 | ||||
|     fh: FeedHandler | ||||
|     first: None | ||||
|     chan: to_asyncio.LinkedTaskChannel | ||||
|     async with ( | ||||
|         maybe_open_feed_handler() as fh, | ||||
|         to_asyncio.open_channel_from( | ||||
|             partial( | ||||
|                 aio_open_interest_feed_relay, | ||||
|                 fh, | ||||
|                 instruments, | ||||
|             ) | ||||
|         ) as (first, chan) | ||||
|     ): | ||||
|         yield chan | ||||
| 
 | ||||
| 
 | ||||
| @acm | ||||
| async def maybe_open_oi_feed( | ||||
|     instruments: list[Symbol],  | ||||
| ) -> trio.abc.ReceiveStream: | ||||
| 
 | ||||
|     # TODO: add a predicate to maybe_open_context | ||||
|     feed: to_asyncio.LinkedTaskChannel | ||||
|     async with maybe_open_context( | ||||
|         acm_func=open_oi_feed, | ||||
|         kwargs={ | ||||
|             'instruments': instruments | ||||
|         }, | ||||
|         key=f'{instruments[0].base}', | ||||
| 
 | ||||
|     ) as (cache_hit, feed): | ||||
|         if cache_hit: | ||||
|             yield broadcast_receiver(feed, 10) | ||||
|         else: | ||||
|             yield feed | ||||
| 
 | ||||
| 
 | ||||
| # TODO, move all to `.broker` submod! | ||||
| # async def aio_order_feed_relay( | ||||
|  |  | |||
							
								
								
									
										205
									
								
								pyproject.toml
								
								
								
								
							
							
						
						
									
										205
									
								
								pyproject.toml
								
								
								
								
							|  | @ -15,8 +15,8 @@ | |||
| # You should have received a copy of the GNU Affero General Public License | ||||
| # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| [build-system] | ||||
| requires = ["poetry-core"] | ||||
| build-backend = "poetry.core.masonry.api" | ||||
| requires = ["hatchling"] | ||||
| build-backend = "hatchling.build" | ||||
| 
 | ||||
| # ------ - ------ | ||||
| 
 | ||||
|  | @ -34,119 +34,114 @@ ignore = [] | |||
| 
 | ||||
| # ------ - ------ | ||||
| 
 | ||||
| [tool.poetry] | ||||
| name = "piker" | ||||
| version = "0.1.0.alpha0.dev0" | ||||
| description = "trading gear for hackers" | ||||
| authors = ["Tyler Goodlet <goodboy_foss@protonmail.com>"] | ||||
| license = "AGPLv3" | ||||
| readme = "README.rst" | ||||
| 
 | ||||
| # ------ - ------ | ||||
| 
 | ||||
| [tool.poetry.dependencies] | ||||
| async-generator = "^1.10" | ||||
| attrs = "^23.1.0" | ||||
| bidict = "^0.22.1" | ||||
| colorama = "^0.4.6" | ||||
| colorlog = "^6.7.0" | ||||
| ib-insync = "^0.9.86" | ||||
| msgspec = "^0.18.6" | ||||
| numba = "^0.59.0" | ||||
| numpy = "^1.25" | ||||
| polars = "^0.18.13" | ||||
| pygments = "^2.16.1" | ||||
| python = ">=3.11, <3.13" | ||||
| rich = "^13.5.2" | ||||
| # setuptools = "^68.0.0" | ||||
| tomli = "^2.0.1" | ||||
| tomli-w = "^1.0.0" | ||||
| trio-util = "^0.7.0" | ||||
| trio-websocket = "^0.10.3" | ||||
| typer = "^0.9.0" | ||||
| rapidfuzz = "^3.5.2" | ||||
| pdbp = "^1.5.0" | ||||
| trio = "^0.24" | ||||
| pendulum = "^3.0.0" | ||||
| httpx = "^0.27.0" | ||||
| cryptofeed = "^2.4.0" | ||||
| pyarrow = "^17.0.0" | ||||
| 
 | ||||
| tractor = {path = "../tractor", develop = true} | ||||
| websockets = "12.0" | ||||
| [tool.poetry.dependencies.asyncvnc] | ||||
| git = 'https://github.com/pikers/asyncvnc.git' | ||||
| branch = 'main' | ||||
| 
 | ||||
| [tool.poetry.dependencies.tomlkit] | ||||
| develop = true | ||||
| git = 'https://github.com/pikers/tomlkit.git' | ||||
| branch = 'piker_pin' | ||||
| # path = "../tomlkit/" | ||||
| 
 | ||||
| [tool.poetry.group.uis] | ||||
| optional = true | ||||
| [tool.poetry.group.uis.dependencies] | ||||
| # https://python-poetry.org/docs/managing-dependencies/#dependency-groups | ||||
| # TODO: make sure the levenshtein shit compiles on nix.. | ||||
| # rapidfuzz = {extras = ["speedup"], version = "^0.18.0"} | ||||
| rapidfuzz = "^3.2.0" | ||||
| qdarkstyle = ">=3.0.2" | ||||
| pyqtgraph = { git = 'https://github.com/pikers/pyqtgraph.git' } | ||||
| 
 | ||||
| # ------ - ------ | ||||
| pyqt6 = "^6.7.0" | ||||
| 
 | ||||
| [tool.poetry.group.dev] | ||||
| optional = true | ||||
| [tool.poetry.group.dev.dependencies] | ||||
| # testing / CI | ||||
| pytest = "^6.0.0" | ||||
| elasticsearch = "^8.9.0" | ||||
| xonsh = "^0.14.2" | ||||
| prompt-toolkit = "3.0.40" | ||||
| cython = "^3.0.0" | ||||
| greenback = "^1.1.1" | ||||
| 
 | ||||
| # console ehancements and eventually remote debugging | ||||
| # extras/helpers. | ||||
| # TODO: add a toolset that makes debugging a `pikerd` service | ||||
| # (tree) easy to hack on directly using more or less the local env: | ||||
| # - xonsh + xxh | ||||
| # - rsyscall + pdbp | ||||
| # - actor runtime control console like BEAM/OTP | ||||
| 
 | ||||
| # ------ - ------ | ||||
| 
 | ||||
| # TODO: add an `--only daemon` group for running non-ui / pikerd | ||||
| # service tree in distributed mode B) | ||||
| # https://python-poetry.org/docs/managing-dependencies/#installing-group-dependencies | ||||
| # [tool.poetry.group.daemon.dependencies] | ||||
| 
 | ||||
| [tool.poetry.scripts] | ||||
| piker = 'piker.cli:cli' | ||||
| pikerd = 'piker.cli:pikerd' | ||||
| ledger = 'piker.accounting.cli:ledger' | ||||
| 
 | ||||
| 
 | ||||
| [project] | ||||
| keywords=[ | ||||
| name = "piker" | ||||
| version = "0.1.0a0dev0" | ||||
| description = "trading gear for hackers" | ||||
| authors = [{ name = "Tyler Goodlet", email = "goodboy_foss@protonmail.com" }] | ||||
| requires-python = ">=3.12, <3.13" | ||||
| license = "AGPL-3.0-or-later" | ||||
| readme = "README.rst" | ||||
| keywords = [ | ||||
|     "async", | ||||
|     "trading", | ||||
|     "finance", | ||||
|     "quant", | ||||
|     "charting", | ||||
| ] | ||||
| classifiers=[ | ||||
|   'Development Status :: 3 - Alpha', | ||||
| classifiers = [ | ||||
|     "Development Status :: 3 - Alpha", | ||||
|     "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", | ||||
|   'Operating System :: POSIX :: Linux', | ||||
|     "Operating System :: POSIX :: Linux", | ||||
|     "Programming Language :: Python :: Implementation :: CPython", | ||||
|     "Programming Language :: Python :: 3 :: Only", | ||||
|     "Programming Language :: Python :: 3.11", | ||||
|     "Programming Language :: Python :: 3.12", | ||||
|   'Intended Audience :: Financial and Insurance Industry', | ||||
|   'Intended Audience :: Science/Research', | ||||
|   'Intended Audience :: Developers', | ||||
|   'Intended Audience :: Education', | ||||
|     "Intended Audience :: Financial and Insurance Industry", | ||||
|     "Intended Audience :: Science/Research", | ||||
|     "Intended Audience :: Developers", | ||||
|     "Intended Audience :: Education", | ||||
| ] | ||||
| dependencies = [ | ||||
|     "async-generator >=1.10, <2.0.0", | ||||
|     "attrs >=23.1.0, <24.0.0", | ||||
|     "bidict >=0.22.1, <0.23.0", | ||||
|     "colorama >=0.4.6, <0.5.0", | ||||
|     "colorlog >=6.7.0, <7.0.0", | ||||
|     "ib-insync >=0.9.86, <0.10.0", | ||||
|     "numba >=0.59.0, <0.60.0", | ||||
|     "numpy >=1.25, <2.0", | ||||
|     "polars >=0.18.13, <0.19.0", | ||||
|     "pygments >=2.16.1, <3.0.0", | ||||
|     "rich >=13.5.2, <14.0.0", | ||||
|     "tomli >=2.0.1, <3.0.0", | ||||
|     "tomli-w >=1.0.0, <2.0.0", | ||||
|     "trio-util >=0.7.0, <0.8.0", | ||||
|     "trio-websocket >=0.10.3, <0.11.0", | ||||
|     "typer >=0.9.0, <1.0.0", | ||||
|     "rapidfuzz >=3.5.2, <4.0.0", | ||||
|     "pdbp >=1.5.0, <2.0.0", | ||||
|     "trio >=0.24, <0.25", | ||||
|     "pendulum >=3.0.0, <4.0.0", | ||||
|     "httpx >=0.27.0, <0.28.0", | ||||
|     "cryptofeed >=2.4.0, <3.0.0", | ||||
|     "pyarrow >=17.0.0, <18.0.0", | ||||
|     "websockets ==12.0", | ||||
|     "msgspec", | ||||
|     "tractor", | ||||
|     "asyncvnc", | ||||
|     "tomlkit", | ||||
| ] | ||||
| 
 | ||||
| [project.optional-dependencies] | ||||
| uis = [ | ||||
|     # https://docs.astral.sh/uv/concepts/projects/dependencies/#optional-dependencies | ||||
|     # TODO: make sure the levenshtein shit compiles on nix.. | ||||
|     # rapidfuzz = {extras = ["speedup"], version = "^0.18.0"} | ||||
|     "rapidfuzz >=3.2.0, <4.0.0", | ||||
|     "qdarkstyle >=3.0.2, <4.0.0", | ||||
|     "pyqt6 >=6.7.0, <7.0.0", | ||||
|     "pyqtgraph", | ||||
| 
 | ||||
|     # ------ - ------ | ||||
| 
 | ||||
|     # TODO: add an `--only daemon` group for running non-ui / pikerd | ||||
|     # service tree in distributed mode B) | ||||
|     # https://docs.astral.sh/uv/concepts/projects/dependencies/#optional-dependencies | ||||
|     # [project.optional-dependencies] | ||||
| ] | ||||
| 
 | ||||
| [dependency-groups] | ||||
| dev = [ | ||||
|     "pytest >=6.0.0, <7.0.0", | ||||
|     "elasticsearch >=8.9.0, <9.0.0", | ||||
|     "xonsh >=0.14.2, <0.15.0", | ||||
|     "prompt-toolkit ==3.0.40", | ||||
|     "cython >=3.0.0, <4.0.0", | ||||
|     "greenback >=1.1.1, <2.0.0", | ||||
|     # console ehancements and eventually remote debugging | ||||
|     # extras/helpers. | ||||
|     # TODO: add a toolset that makes debugging a `pikerd` service | ||||
|     # (tree) easy to hack on directly using more or less the local env: | ||||
|     # - xonsh + xxh | ||||
|     # - rsyscall + pdbp | ||||
|     # - actor runtime control console like BEAM/OTP | ||||
| ] | ||||
| 
 | ||||
| [project.scripts] | ||||
| piker = "piker.cli:cli" | ||||
| pikerd = "piker.cli:pikerd" | ||||
| ledger = "piker.accounting.cli:ledger" | ||||
| 
 | ||||
| [tool.hatch.build.targets.sdist] | ||||
| include = ["piker"] | ||||
| 
 | ||||
| [tool.hatch.build.targets.wheel] | ||||
| include = ["piker"] | ||||
| 
 | ||||
| [tool.uv.sources] | ||||
| pyqtgraph = { git = "https://github.com/pikers/pyqtgraph.git" } | ||||
| asyncvnc = { git = "https://github.com/pikers/asyncvnc.git", branch = "main" } | ||||
| tomlkit = { git = "https://github.com/pikers/tomlkit.git", branch ="piker_pin" } | ||||
| msgspec = { git = "https://github.com/jcrist/msgspec.git" } | ||||
| tractor = { path = "../tractor" } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue