diff --git a/piker/brokers/ib/_util.py b/piker/brokers/ib/_util.py new file mode 100644 index 00000000..c7a49909 --- /dev/null +++ b/piker/brokers/ib/_util.py @@ -0,0 +1,184 @@ +# piker: trading gear for hackers +# Copyright (C) Tyler Goodlet (in stewardship for pikers) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +''' +``ib`` utilities and hacks suitable for use in the backend and/or as +runnable script-programs. + +''' +from typing import Literal +import subprocess + +import tractor + + +_reset_tech: Literal[ + 'vnc', + 'i3ipc_xdotool', + + # TODO: in theory we can use a different linux DE API or + # some other type of similar window scanning/mgmt client + # (on other OSs) to do the same. + +] = 'vnc' + + +async def data_reset_hack( + reset_type: str = 'data', + +) -> None: + ''' + Run key combos for resetting data feeds and yield back to caller + when complete. + + NOTE: this is a linux-only hack around! + + There are multiple "techs" you can use depending on your infra setup: + + - if running ib-gw in a container with a VNC server running the most + performant method is the `'vnc'` option. + + - if running ib-gw/tws locally, and you are using `i3` you can use + the ``i3ipc`` lib and ``xdotool`` to send the appropriate click + and key-combos automatically to your local desktop's java X-apps. + + https://interactivebrokers.github.io/tws-api/historical_limitations.html#pacing_violations + + TODOs: + - a return type that hopefully determines if the hack was + successful. + - other OS support? + - integration with ``ib-gw`` run in docker + Xorg? + - is it possible to offer a local server that can be accessed by + a client? Would be sure be handy for running native java blobs + that need to be wrangle. + + ''' + global _reset_tech + + match _reset_tech: + case 'vnc': + try: + await tractor.to_asyncio.run_task(vnc_click_hack) + except OSError: + _reset_tech = 'i3ipc_xdotool' + try: + i3ipc_xdotool_manual_click_hack() + return True + except OSError: + return False + + case 'i3ipc_xdotool': + i3ipc_xdotool_manual_click_hack() + + case _ as tech: + raise RuntimeError(f'{tech} is not supported for reset tech!?') + + # we don't really need the ``xdotool`` approach any more B) + return True + + +async def vnc_click_hack( + reset_type: str = 'data' +) -> None: + ''' + Reset the data or netowork connection for the VNC attached + ib gateway using magic combos. + + ''' + key = {'data': 'f', 'connection': 'r'}[reset_type] + + import asyncvnc + + async with asyncvnc.connect( + 'localhost', + port=3003, + # password='ibcansmbz', + ) as client: + + # move to middle of screen + # 640x1800 + client.mouse.move( + x=500, + y=500, + ) + client.mouse.click() + client.keyboard.press('Ctrl', 'Alt', key) # keys are stacked + + +def i3ipc_xdotool_manual_click_hack() -> None: + import i3ipc + + i3 = i3ipc.Connection() + t = i3.get_tree() + + orig_win_id = t.find_focused().window + + # for tws + win_names: list[str] = [ + 'Interactive Brokers', # tws running in i3 + 'IB Gateway', # gw running in i3 + # 'IB', # gw running in i3 (newer version?) + ] + + for name in win_names: + results = t.find_titled(name) + print(f'results for {name}: {results}') + if results: + con = results[0] + print(f'Resetting data feed for {name}') + win_id = str(con.window) + w, h = con.rect.width, con.rect.height + + # TODO: seems to be a few libs for python but not sure + # if they support all the sub commands we need, order of + # most recent commit history: + # https://github.com/rr-/pyxdotool + # https://github.com/ShaneHutter/pyxdotool + # https://github.com/cphyc/pyxdotool + + # TODO: only run the reconnect (2nd) kc on a detected + # disconnect? + for key_combo, timeout in [ + # only required if we need a connection reset. + # ('ctrl+alt+r', 12), + # data feed reset. + ('ctrl+alt+f', 6) + ]: + subprocess.call([ + 'xdotool', + 'windowactivate', '--sync', win_id, + + # move mouse to bottom left of window (where there should + # be nothing to click). + 'mousemove_relative', '--sync', str(w-4), str(h-4), + + # NOTE: we may need to stick a `--retry 3` in here.. + 'click', '--window', win_id, + '--repeat', '3', '1', + + # hackzorzes + 'key', key_combo, + ], + timeout=timeout, + ) + + # re-activate and focus original window + subprocess.call([ + 'xdotool', + 'windowactivate', '--sync', str(orig_win_id), + 'click', '--window', str(orig_win_id), '1', + ]) diff --git a/piker/brokers/ib/feed.py b/piker/brokers/ib/feed.py index 345e4ade..358bb066 100644 --- a/piker/brokers/ib/feed.py +++ b/piker/brokers/ib/feed.py @@ -57,6 +57,7 @@ from .api import ( RequestError, Contract, ) +from ._util import data_reset_hack # https://interactivebrokers.github.io/tws-api/tick_types.html @@ -914,65 +915,6 @@ async def stream_quotes( # last = time.time() -async def data_reset_hack( - reset_type: str = 'data', - -) -> None: - ''' - Run key combos for resetting data feeds and yield back to caller - when complete. - - This is a linux-only hack around: - - https://interactivebrokers.github.io/tws-api/historical_limitations.html#pacing_violations - - TODOs: - - a return type that hopefully determines if the hack was - successful. - - other OS support? - - integration with ``ib-gw`` run in docker + Xorg? - - is it possible to offer a local server that can be accessed by - a client? Would be sure be handy for running native java blobs - that need to be wrangle. - - ''' - - async def vnc_click_hack( - reset_type: str = 'data' - ) -> None: - ''' - Reset the data or netowork connection for the VNC attached - ib gateway using magic combos. - - ''' - key = {'data': 'f', 'connection': 'r'}[reset_type] - - import asyncvnc - - async with asyncvnc.connect( - 'localhost', - port=3003, - # password='ibcansmbz', - ) as client: - - # move to middle of screen - # 640x1800 - client.mouse.move( - x=500, - y=500, - ) - client.mouse.click() - client.keyboard.press('Ctrl', 'Alt', key) # keys are stacked - - try: - await tractor.to_asyncio.run_task(vnc_click_hack) - except OSError: - return False - - # we don't really need the ``xdotool`` approach any more B) - return True - - @tractor.context async def open_symbol_search( ctx: tractor.Context, diff --git a/scripts/ib_data_reset.py b/scripts/ib_data_reset.py index 7e328190..10ec55e5 100644 --- a/scripts/ib_data_reset.py +++ b/scripts/ib_data_reset.py @@ -17,68 +17,13 @@ ''' IB api client data feed reset hack for i3. +Delegates to ``i3ipc`` python lib to detect the correct local +window to click-activate and ``xdotool`` to send the mouse +events to said window. + ''' -import subprocess +from piker.brokers.ib._util import i3ipc_xdotool_manual_click_hack -import i3ipc -i3 = i3ipc.Connection() -t = i3.get_tree() - -orig_win_id = t.find_focused().window - -# for tws -win_names: list[str] = [ - 'Interactive Brokers', # tws running in i3 - 'IB Gateway', # gw running in i3 - # 'IB', # gw running in i3 (newer version?) -] - -for name in win_names: - results = t.find_titled(name) - print(f'results for {name}: {results}') - if results: - con = results[0] - print(f'Resetting data feed for {name}') - win_id = str(con.window) - w, h = con.rect.width, con.rect.height - - # TODO: seems to be a few libs for python but not sure - # if they support all the sub commands we need, order of - # most recent commit history: - # https://github.com/rr-/pyxdotool - # https://github.com/ShaneHutter/pyxdotool - # https://github.com/cphyc/pyxdotool - - # TODO: only run the reconnect (2nd) kc on a detected - # disconnect? - for key_combo, timeout in [ - # only required if we need a connection reset. - # ('ctrl+alt+r', 12), - # data feed reset. - ('ctrl+alt+f', 6) - ]: - subprocess.call([ - 'xdotool', - 'windowactivate', '--sync', win_id, - - # move mouse to bottom left of window (where there should - # be nothing to click). - 'mousemove_relative', '--sync', str(w-4), str(h-4), - - # NOTE: we may need to stick a `--retry 3` in here.. - 'click', '--window', win_id, - '--repeat', '3', '1', - - # hackzorzes - 'key', key_combo, - ], - timeout=timeout, - ) - -# re-activate and focus original window -subprocess.call([ - 'xdotool', - 'windowactivate', '--sync', str(orig_win_id), - 'click', '--window', str(orig_win_id), '1', -]) +if __name__ == '__main__': + i3ipc_xdotool_manual_click_hack()