commit
						54d272ea29
					
				
							
								
								
									
										15
									
								
								README.rst
								
								
								
								
							
							
						
						
									
										15
									
								
								README.rst
								
								
								
								
							|  | @ -103,6 +103,21 @@ bet you weren't expecting this from the foss bby:: | |||
|     piker -b kraken chart XBTUSD | ||||
| 
 | ||||
| 
 | ||||
| run in distributed mode | ||||
| *********************** | ||||
| start the service daemon:: | ||||
| 
 | ||||
|     pikerd -l info | ||||
| 
 | ||||
| 
 | ||||
| connect yourt chart:: | ||||
| 
 | ||||
|     piker -b kraken chart XMRXBT | ||||
| 
 | ||||
| 
 | ||||
| enjoy persistent real-time data feeds tied to daemon lifetime. | ||||
| 
 | ||||
| 
 | ||||
| if anyone asks you what this project is about | ||||
| ********************************************* | ||||
| you don't talk about it. | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ | |||
| Structured, daemon tree service management. | ||||
| 
 | ||||
| """ | ||||
| from functools import partial | ||||
| from typing import Optional, Union | ||||
| from contextlib import asynccontextmanager | ||||
| 
 | ||||
|  | @ -87,6 +88,18 @@ async def open_pikerd( | |||
|             yield _services | ||||
| 
 | ||||
| 
 | ||||
| @asynccontextmanager | ||||
| async def maybe_open_runtime( | ||||
|     loglevel: Optional[str] = None, | ||||
|     **kwargs, | ||||
| ) -> None: | ||||
|     if not tractor.current_actor(err_on_no_runtime=False): | ||||
|         async with tractor.open_root_actor(loglevel=loglevel, **kwargs): | ||||
|             yield | ||||
|     else: | ||||
|         yield | ||||
| 
 | ||||
| 
 | ||||
| @asynccontextmanager | ||||
| async def maybe_open_pikerd( | ||||
|     loglevel: Optional[str] = None, | ||||
|  | @ -100,23 +113,23 @@ async def maybe_open_pikerd( | |||
|     if loglevel: | ||||
|         get_console_log(loglevel) | ||||
| 
 | ||||
|     try: | ||||
|     # subtle, we must have the runtime up here or portal lookup will fail | ||||
|     async with maybe_open_runtime(loglevel, **kwargs): | ||||
|         async with tractor.find_actor(_root_dname) as portal: | ||||
|             assert portal is not None | ||||
|             yield portal | ||||
|             return | ||||
|             # assert portal is not None | ||||
|             if portal is not None: | ||||
|                 yield portal | ||||
|                 return | ||||
| 
 | ||||
|     except (RuntimeError, AssertionError):  # tractor runtime not started yet | ||||
| 
 | ||||
|         # presume pikerd role | ||||
|         async with open_pikerd( | ||||
|             loglevel, | ||||
|             **kwargs, | ||||
|         ) as _: | ||||
|             # in the case where we're starting up the | ||||
|             # tractor-piker runtime stack in **this** process | ||||
|             # we return no portal to self. | ||||
|             yield None | ||||
|     # presume pikerd role | ||||
|     async with open_pikerd( | ||||
|         loglevel, | ||||
|         **kwargs, | ||||
|     ) as _: | ||||
|         # in the case where we're starting up the | ||||
|         # tractor-piker runtime stack in **this** process | ||||
|         # we return no portal to self. | ||||
|         yield None | ||||
| 
 | ||||
| 
 | ||||
| # brokerd enabled modules | ||||
|  | @ -124,7 +137,7 @@ _data_mods = [ | |||
|     'piker.brokers.core', | ||||
|     'piker.brokers.data', | ||||
|     'piker.data', | ||||
|     'piker.data._buffer' | ||||
|     'piker.data._sampling' | ||||
| ] | ||||
| 
 | ||||
| 
 | ||||
|  | @ -134,6 +147,8 @@ async def spawn_brokerd( | |||
|     **tractor_kwargs | ||||
| ) -> tractor._portal.Portal: | ||||
| 
 | ||||
|     from .data import _setup_persistent_brokerd | ||||
| 
 | ||||
|     log.info(f'Spawning {brokername} broker daemon') | ||||
| 
 | ||||
|     brokermod = get_brokermod(brokername) | ||||
|  | @ -145,13 +160,28 @@ async def spawn_brokerd( | |||
|     global _services | ||||
|     assert _services | ||||
| 
 | ||||
|     await _services.actor_n.start_actor( | ||||
|     portal = await _services.actor_n.start_actor( | ||||
|         dname, | ||||
|         enable_modules=_data_mods + [brokermod.__name__], | ||||
|         loglevel=loglevel, | ||||
|         **tractor_kwargs | ||||
|     ) | ||||
| 
 | ||||
|     # TODO: so i think this is the perfect use case for supporting | ||||
|     # a cross-actor async context manager api instead of this | ||||
|     # shoort-and-forget task spawned in the root nursery, we'd have an | ||||
|     # async exit stack that we'd register the `portal.open_context()` | ||||
|     # call with and then have the ability to unwind the call whenevs. | ||||
| 
 | ||||
|     # non-blocking setup of brokerd service nursery | ||||
|     _services.service_n.start_soon( | ||||
|         partial( | ||||
|             portal.run, | ||||
|             _setup_persistent_brokerd, | ||||
|             brokername=brokername, | ||||
|         ) | ||||
|     ) | ||||
| 
 | ||||
|     return dname | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -22,7 +22,6 @@ from typing import Dict | |||
| from contextlib import asynccontextmanager, AsyncExitStack | ||||
| 
 | ||||
| import trio | ||||
| import tractor | ||||
| 
 | ||||
| from . import get_brokermod | ||||
| from ..log import get_logger | ||||
|  | @ -30,10 +29,12 @@ from ..log import get_logger | |||
| 
 | ||||
| log = get_logger(__name__) | ||||
| 
 | ||||
| _cache: Dict[str, 'Client'] = {} | ||||
| 
 | ||||
| _cache: Dict[str, 'Client'] = {}  # noqa | ||||
| 
 | ||||
| 
 | ||||
| @asynccontextmanager | ||||
| async def get_cached_client( | ||||
| async def open_cached_client( | ||||
|     brokername: str, | ||||
|     *args, | ||||
|     **kwargs, | ||||
|  | @ -74,10 +75,11 @@ async def get_cached_client( | |||
|             client._exit_stack = exit_stack | ||||
|             clients[brokername] = client | ||||
| 
 | ||||
|             yield client | ||||
|         yield client | ||||
| 
 | ||||
|     finally: | ||||
|         client._consumers -= 1 | ||||
|         if client._consumers <= 0: | ||||
|             # teardown the client | ||||
|             await client._exit_stack.aclose() | ||||
|         if client is not None: | ||||
|             # if no more consumers, teardown the client | ||||
|             client._consumers -= 1 | ||||
|             if client._consumers <= 0: | ||||
|                 await client._exit_stack.aclose() | ||||
|  |  | |||
|  | @ -34,10 +34,11 @@ import logging | |||
| import time | ||||
| 
 | ||||
| import trio | ||||
| from trio_typing import TaskStatus | ||||
| import tractor | ||||
| from async_generator import aclosing | ||||
| from ib_insync.wrapper import RequestError | ||||
| from ib_insync.contract import Contract, ContractDetails | ||||
| from ib_insync.contract import Contract, ContractDetails, Option | ||||
| from ib_insync.order import Order | ||||
| from ib_insync.ticker import Ticker | ||||
| from ib_insync.objects import Position | ||||
|  | @ -46,14 +47,9 @@ from ib_insync.wrapper import Wrapper | |||
| from ib_insync.client import Client as ib_Client | ||||
| 
 | ||||
| from ..log import get_logger, get_console_log | ||||
| from ..data import ( | ||||
|     maybe_spawn_brokerd, | ||||
|     iterticks, | ||||
|     attach_shm_array, | ||||
|     subscribe_ohlc_for_increment, | ||||
|     _buffer, | ||||
| ) | ||||
| from ..data import maybe_spawn_brokerd | ||||
| from ..data._source import from_df | ||||
| from ..data._sharedmem import ShmArray | ||||
| from ._util import SymbolNotFound | ||||
| 
 | ||||
| 
 | ||||
|  | @ -168,6 +164,7 @@ class Client: | |||
| 
 | ||||
|         # contract cache | ||||
|         self._contracts: Dict[str, Contract] = {} | ||||
|         self._feeds: Dict[str, trio.abc.SendChannel] = {} | ||||
| 
 | ||||
|         # NOTE: the ib.client here is "throttled" to 45 rps by default | ||||
| 
 | ||||
|  | @ -384,42 +381,6 @@ class Client: | |||
|             formatDate=2,  # timezone aware UTC datetime | ||||
|         ) | ||||
| 
 | ||||
|     async def stream_ticker( | ||||
|         self, | ||||
|         symbol: str, | ||||
|         to_trio, | ||||
|         opts: Tuple[int] = ('375', '233', '236'), | ||||
|         contract: Optional[Contract] = None, | ||||
|     ) -> None: | ||||
|         """Stream a ticker using the std L1 api. | ||||
|         """ | ||||
|         contract = contract or (await self.find_contract(symbol)) | ||||
|         ticker: Ticker = self.ib.reqMktData(contract, ','.join(opts)) | ||||
| 
 | ||||
|         # define a simple queue push routine that streams quote packets | ||||
|         # to trio over the ``to_trio`` memory channel. | ||||
| 
 | ||||
|         def push(t): | ||||
|             """Push quotes to trio task. | ||||
| 
 | ||||
|             """ | ||||
|             # log.debug(t) | ||||
|             try: | ||||
|                 to_trio.send_nowait(t) | ||||
|             except trio.BrokenResourceError: | ||||
|                 # XXX: eventkit's ``Event.emit()`` for whatever redic | ||||
|                 # reason will catch and ignore regular exceptions | ||||
|                 # resulting in tracebacks spammed to console.. | ||||
|                 # Manually do the dereg ourselves. | ||||
|                 ticker.updateEvent.disconnect(push) | ||||
|                 log.error(f"Disconnected stream for `{symbol}`") | ||||
|                 self.ib.cancelMktData(contract) | ||||
| 
 | ||||
|         ticker.updateEvent.connect(push) | ||||
| 
 | ||||
|         # let the engine run and stream | ||||
|         await self.ib.disconnectedEvent | ||||
| 
 | ||||
|     async def get_quote( | ||||
|         self, | ||||
|         symbol: str, | ||||
|  | @ -613,6 +574,8 @@ async def _aio_get_client( | |||
|     client_id: Optional[int] = None, | ||||
| ) -> Client: | ||||
|     """Return an ``ib_insync.IB`` instance wrapped in our client API. | ||||
| 
 | ||||
|     Client instances are cached for later use. | ||||
|     """ | ||||
|     # first check cache for existing client | ||||
| 
 | ||||
|  | @ -652,8 +615,10 @@ async def _aio_get_client( | |||
|         # create and cache | ||||
|         try: | ||||
|             client = Client(ib) | ||||
| 
 | ||||
|             _client_cache[(host, port)] = client | ||||
|             log.debug(f"Caching client for {(host, port)}") | ||||
| 
 | ||||
|             yield client | ||||
| 
 | ||||
|         except BaseException: | ||||
|  | @ -691,13 +656,14 @@ async def _trio_run_client_method( | |||
| 
 | ||||
|     # if the method is an *async gen* stream for it | ||||
|     meth = getattr(Client, method) | ||||
|     if inspect.isasyncgenfunction(meth): | ||||
|         kwargs['_treat_as_stream'] = True | ||||
| 
 | ||||
|     # if the method is an *async func* but manually | ||||
|     # streams back results, make sure to also stream it | ||||
|     args = tuple(inspect.getfullargspec(meth).args) | ||||
|     if 'to_trio' in args: | ||||
| 
 | ||||
|     if inspect.isasyncgenfunction(meth) or ( | ||||
|         # if the method is an *async func* but manually | ||||
|         # streams back results, make sure to also stream it | ||||
|         'to_trio' in args | ||||
|     ): | ||||
|         kwargs['_treat_as_stream'] = True | ||||
| 
 | ||||
|     result = await tractor.to_asyncio.run_task( | ||||
|  | @ -780,7 +746,7 @@ def normalize( | |||
|     # convert named tuples to dicts so we send usable keys | ||||
|     new_ticks = [] | ||||
|     for tick in ticker.ticks: | ||||
|         if tick: | ||||
|         if tick and not isinstance(tick, dict): | ||||
|             td = tick._asdict() | ||||
|             td['type'] = tick_types.get(td['tickType'], 'n/a') | ||||
| 
 | ||||
|  | @ -811,36 +777,13 @@ def normalize( | |||
|     return data | ||||
| 
 | ||||
| 
 | ||||
| _local_buffer_writers = {} | ||||
| 
 | ||||
| 
 | ||||
| @asynccontextmanager | ||||
| async def activate_writer(key: str) -> (bool, trio.Nursery): | ||||
|     """Mark the current actor with module var determining | ||||
|     whether an existing shm writer task is already active. | ||||
| 
 | ||||
|     This avoids more then one writer resulting in data | ||||
|     clobbering. | ||||
|     """ | ||||
|     global _local_buffer_writers | ||||
| 
 | ||||
|     try: | ||||
|         assert not _local_buffer_writers.get(key, False) | ||||
| 
 | ||||
|         _local_buffer_writers[key] = True | ||||
| 
 | ||||
|         async with trio.open_nursery() as n: | ||||
|             yield n | ||||
|     finally: | ||||
|         _local_buffer_writers.pop(key, None) | ||||
| 
 | ||||
| 
 | ||||
| async def fill_bars( | ||||
| async def backfill_bars( | ||||
|     sym: str, | ||||
|     first_bars: list, | ||||
|     shm: 'ShmArray',  # type: ignore # noqa | ||||
|     shm: ShmArray,  # type: ignore # noqa | ||||
|     # count: int = 20,  # NOTE: any more and we'll overrun underlying buffer | ||||
|     count: int = 6,  # NOTE: any more and we'll overrun the underlying buffer | ||||
|     count: int = 10,  # NOTE: any more and we'll overrun the underlying buffer | ||||
| 
 | ||||
|     task_status: TaskStatus[trio.CancelScope] = trio.TASK_STATUS_IGNORED, | ||||
| ) -> None: | ||||
|     """Fill historical bars into shared mem / storage afap. | ||||
| 
 | ||||
|  | @ -848,41 +791,58 @@ async def fill_bars( | |||
|     https://github.com/pikers/piker/issues/128 | ||||
| 
 | ||||
|     """ | ||||
|     next_dt = first_bars[0].date | ||||
|     first_bars, bars_array = await _trio_run_client_method( | ||||
|         method='bars', | ||||
|         symbol=sym, | ||||
|     ) | ||||
| 
 | ||||
|     i = 0 | ||||
|     while i < count: | ||||
|     # write historical data to buffer | ||||
|     shm.push(bars_array) | ||||
| 
 | ||||
|         try: | ||||
|             bars, bars_array = await _trio_run_client_method( | ||||
|                 method='bars', | ||||
|                 symbol=sym, | ||||
|                 end_dt=next_dt, | ||||
|             ) | ||||
|     with trio.CancelScope() as cs: | ||||
| 
 | ||||
|             shm.push(bars_array, prepend=True) | ||||
|             i += 1 | ||||
|             next_dt = bars[0].date | ||||
|         task_status.started(cs) | ||||
| 
 | ||||
|         except RequestError as err: | ||||
|             # TODO: retreive underlying ``ib_insync`` error? | ||||
|         next_dt = first_bars[0].date | ||||
| 
 | ||||
|             if err.code == 162: | ||||
|         i = 0 | ||||
|         while i < count: | ||||
| 
 | ||||
|                 if 'HMDS query returned no data' in err.message: | ||||
|                     # means we hit some kind of historical "dead zone" | ||||
|                     # and further requests seem to always cause | ||||
|                     # throttling despite the rps being low | ||||
|                     break | ||||
|             try: | ||||
|                 bars, bars_array = await _trio_run_client_method( | ||||
|                     method='bars', | ||||
|                     symbol=sym, | ||||
|                     end_dt=next_dt, | ||||
|                 ) | ||||
| 
 | ||||
|                 else: | ||||
|                     log.exception( | ||||
|                         "Data query rate reached: Press `ctrl-alt-f` in TWS") | ||||
|                 if bars_array is None: | ||||
|                     raise SymbolNotFound(sym) | ||||
| 
 | ||||
|                     # TODO: should probably create some alert on screen | ||||
|                     # and then somehow get that to trigger an event here | ||||
|                     # that restarts/resumes this task? | ||||
|                     await tractor.breakpoint() | ||||
|                 shm.push(bars_array, prepend=True) | ||||
|                 i += 1 | ||||
|                 next_dt = bars[0].date | ||||
| 
 | ||||
|             except RequestError as err: | ||||
|                 # TODO: retreive underlying ``ib_insync`` error? | ||||
| 
 | ||||
|                 if err.code == 162: | ||||
| 
 | ||||
|                     if 'HMDS query returned no data' in err.message: | ||||
|                         # means we hit some kind of historical "dead zone" | ||||
|                         # and further requests seem to always cause | ||||
|                         # throttling despite the rps being low | ||||
|                         break | ||||
| 
 | ||||
|                     else: | ||||
|                         log.exception( | ||||
|                             "Data query rate reached: Press `ctrl-alt-f`" | ||||
|                             "in TWS" | ||||
|                         ) | ||||
| 
 | ||||
|                         # TODO: should probably create some alert on screen | ||||
|                         # and then somehow get that to trigger an event here | ||||
|                         # that restarts/resumes this task? | ||||
|                         await tractor.breakpoint() | ||||
| 
 | ||||
| 
 | ||||
| asset_type_map = { | ||||
|  | @ -904,27 +864,96 @@ asset_type_map = { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| # TODO: figure out how to share quote feeds sanely despite | ||||
| # the wacky ``ib_insync`` api. | ||||
| # @tractor.msg.pub | ||||
| @tractor.stream | ||||
| _quote_streams: Dict[str, trio.abc.ReceiveStream] = {} | ||||
| 
 | ||||
| 
 | ||||
| async def _setup_quote_stream( | ||||
|     symbol: str, | ||||
|     opts: Tuple[int] = ('375', '233', '236'), | ||||
|     contract: Optional[Contract] = None, | ||||
| ) -> None: | ||||
|     """Stream a ticker using the std L1 api. | ||||
|     """ | ||||
|     global _quote_streams | ||||
| 
 | ||||
|     async with _aio_get_client() as client: | ||||
| 
 | ||||
|         contract = contract or (await client.find_contract(symbol)) | ||||
|         ticker: Ticker = client.ib.reqMktData(contract, ','.join(opts)) | ||||
| 
 | ||||
|         # define a simple queue push routine that streams quote packets | ||||
|         # to trio over the ``to_trio`` memory channel. | ||||
|         to_trio, from_aio = trio.open_memory_channel(2**8)  # type: ignore | ||||
| 
 | ||||
|         def push(t): | ||||
|             """Push quotes to trio task. | ||||
| 
 | ||||
|             """ | ||||
|             # log.debug(t) | ||||
|             try: | ||||
|                 to_trio.send_nowait(t) | ||||
| 
 | ||||
|             except trio.BrokenResourceError: | ||||
|                 # XXX: eventkit's ``Event.emit()`` for whatever redic | ||||
|                 # reason will catch and ignore regular exceptions | ||||
|                 # resulting in tracebacks spammed to console.. | ||||
|                 # Manually do the dereg ourselves. | ||||
|                 ticker.updateEvent.disconnect(push) | ||||
|                 log.error(f"Disconnected stream for `{symbol}`") | ||||
|                 client.ib.cancelMktData(contract) | ||||
| 
 | ||||
|                 # decouple broadcast mem chan | ||||
|                 _quote_streams.pop(symbol, None) | ||||
| 
 | ||||
|         ticker.updateEvent.connect(push) | ||||
| 
 | ||||
|         return from_aio | ||||
| 
 | ||||
| 
 | ||||
| async def start_aio_quote_stream( | ||||
|     symbol: str, | ||||
|     contract: Optional[Contract] = None, | ||||
| ) -> trio.abc.ReceiveStream: | ||||
| 
 | ||||
|     global _quote_streams | ||||
| 
 | ||||
|     from_aio = _quote_streams.get(symbol) | ||||
|     if from_aio: | ||||
| 
 | ||||
|         # if we already have a cached feed deliver a rx side clone to consumer | ||||
|         return from_aio.clone() | ||||
| 
 | ||||
|     else: | ||||
| 
 | ||||
|         from_aio = await tractor.to_asyncio.run_task( | ||||
|             _setup_quote_stream, | ||||
|             symbol=symbol, | ||||
|             contract=contract, | ||||
|         ) | ||||
| 
 | ||||
|         # cache feed for later consumers | ||||
|         _quote_streams[symbol] = from_aio | ||||
| 
 | ||||
|         return from_aio | ||||
| 
 | ||||
| 
 | ||||
| async def stream_quotes( | ||||
|     ctx: tractor.Context, | ||||
| 
 | ||||
|     send_chan: trio.abc.SendChannel, | ||||
|     symbols: List[str], | ||||
|     shm_token: Tuple[str, str, List[tuple]], | ||||
|     shm: ShmArray, | ||||
|     feed_is_live: trio.Event, | ||||
|     loglevel: str = None, | ||||
| 
 | ||||
|     # compat for @tractor.msg.pub | ||||
|     topics: Any = None, | ||||
|     get_topics: Callable = None, | ||||
| ) -> AsyncIterator[Dict[str, Any]]: | ||||
|     # startup sync | ||||
|     task_status: TaskStatus[Tuple[Dict, Dict]] = trio.TASK_STATUS_IGNORED, | ||||
| 
 | ||||
| ) -> None: | ||||
|     """Stream symbol quotes. | ||||
| 
 | ||||
|     This is a ``trio`` callable routine meant to be invoked | ||||
|     once the brokerd is up. | ||||
|     """ | ||||
|     # XXX: required to propagate ``tractor`` loglevel to piker logging | ||||
|     get_console_log(loglevel or tractor.current_actor().loglevel) | ||||
| 
 | ||||
|     # TODO: support multiple subscriptions | ||||
|     sym = symbols[0] | ||||
|  | @ -934,130 +963,67 @@ async def stream_quotes( | |||
|         symbol=sym, | ||||
|     ) | ||||
| 
 | ||||
|     stream = await _trio_run_client_method( | ||||
|         method='stream_ticker', | ||||
|         contract=contract,  # small speedup | ||||
|         symbol=sym, | ||||
|     ) | ||||
|     stream = await start_aio_quote_stream(symbol=sym, contract=contract) | ||||
| 
 | ||||
|     shm = None | ||||
|     async with trio.open_nursery() as ln: | ||||
|         # check if a writer already is alive in a streaming task, | ||||
|         # otherwise start one and mark it as now existing | ||||
|     # pass back some symbol info like min_tick, trading_hours, etc. | ||||
|     syminfo = asdict(details) | ||||
|     syminfo.update(syminfo['contract']) | ||||
| 
 | ||||
|         key = shm_token['shm_name'] | ||||
|     # TODO: more consistent field translation | ||||
|     atype = syminfo['asset_type'] = asset_type_map[syminfo['secType']] | ||||
| 
 | ||||
|         writer_already_exists = _local_buffer_writers.get(key, False) | ||||
|     # for stocks it seems TWS reports too small a tick size | ||||
|     # such that you can't submit orders with that granularity? | ||||
|     min_tick = 0.01 if atype == 'stock' else 0 | ||||
| 
 | ||||
|         # maybe load historical ohlcv in to shared mem | ||||
|         # check if shm has already been created by previous | ||||
|         # feed initialization | ||||
|         if not writer_already_exists: | ||||
|             _local_buffer_writers[key] = True | ||||
|     syminfo['price_tick_size'] = max(syminfo['minTick'], min_tick) | ||||
| 
 | ||||
|             shm = attach_shm_array( | ||||
|                 token=shm_token, | ||||
|     # for "traditional" assets, volume is normally discreet, not a float | ||||
|     syminfo['lot_tick_size'] = 0.0 | ||||
| 
 | ||||
|                 # we are the buffer writer | ||||
|                 readonly=False, | ||||
|             ) | ||||
| 
 | ||||
|             # async def retrieve_and_push(): | ||||
|             start = time.time() | ||||
| 
 | ||||
|             bars, bars_array = await _trio_run_client_method( | ||||
|                 method='bars', | ||||
|                 symbol=sym, | ||||
| 
 | ||||
|             ) | ||||
| 
 | ||||
|             log.info(f"bars_array request: {time.time() - start}") | ||||
| 
 | ||||
|             if bars_array is None: | ||||
|                 raise SymbolNotFound(sym) | ||||
| 
 | ||||
|             # write historical data to buffer | ||||
|             shm.push(bars_array) | ||||
|             shm_token = shm.token | ||||
| 
 | ||||
|             # TODO: generalize this for other brokers | ||||
|             # start bar filler task in bg | ||||
|             ln.start_soon(fill_bars, sym, bars, shm) | ||||
| 
 | ||||
|             times = shm.array['time'] | ||||
|             delay_s = times[-1] - times[times != times[-1]][-1] | ||||
|             subscribe_ohlc_for_increment(shm, delay_s) | ||||
| 
 | ||||
|         # pass back some symbol info like min_tick, trading_hours, etc. | ||||
|         # con = asdict(contract) | ||||
|         # syminfo = contract | ||||
|         syminfo = asdict(details) | ||||
|         syminfo.update(syminfo['contract']) | ||||
| 
 | ||||
|         # TODO: more consistent field translation | ||||
|         atype = syminfo['asset_type'] = asset_type_map[syminfo['secType']] | ||||
| 
 | ||||
|         # for stocks it seems TWS reports too small a tick size | ||||
|         # such that you can't submit orders with that granularity? | ||||
|         min_tick = 0.01 if atype == 'stock' else 0 | ||||
| 
 | ||||
|         syminfo['price_tick_size'] = max(syminfo['minTick'], min_tick) | ||||
| 
 | ||||
|         # for "traditional" assets, volume is normally discreet, not a float | ||||
|         syminfo['lot_tick_size'] = 0.0 | ||||
| 
 | ||||
|         # TODO: for loop through all symbols passed in | ||||
|         init_msgs = { | ||||
|             # pass back token, and bool, signalling if we're the writer | ||||
|             # and that history has been written | ||||
|             sym: { | ||||
|                 'is_shm_writer': not writer_already_exists, | ||||
|                 'shm_token': shm_token, | ||||
|                 'symbol_info': syminfo, | ||||
|             } | ||||
|     # TODO: for loop through all symbols passed in | ||||
|     init_msgs = { | ||||
|         # pass back token, and bool, signalling if we're the writer | ||||
|         # and that history has been written | ||||
|         sym: { | ||||
|             'symbol_info': syminfo, | ||||
|         } | ||||
|         await ctx.send_yield(init_msgs) | ||||
|     } | ||||
| 
 | ||||
|         # check for special contract types | ||||
|         if type(first_ticker.contract) not in (ibis.Commodity, ibis.Forex): | ||||
|             suffix = 'exchange' | ||||
|             # should be real volume for this contract | ||||
|             calc_price = False | ||||
|         else: | ||||
|             # commodities and forex don't have an exchange name and | ||||
|             # no real volume so we have to calculate the price | ||||
|             suffix = 'secType' | ||||
|             calc_price = True | ||||
|             # ticker = first_ticker | ||||
|     # check for special contract types | ||||
|     if type(first_ticker.contract) not in (ibis.Commodity, ibis.Forex): | ||||
|         suffix = 'exchange' | ||||
|         # should be real volume for this contract | ||||
|         calc_price = False | ||||
|     else: | ||||
|         # commodities and forex don't have an exchange name and | ||||
|         # no real volume so we have to calculate the price | ||||
|         suffix = 'secType' | ||||
|         calc_price = True | ||||
| 
 | ||||
|         # pass first quote asap | ||||
|         quote = normalize(first_ticker, calc_price=calc_price) | ||||
|         con = quote['contract'] | ||||
|         topic = '.'.join((con['symbol'], con[suffix])).lower() | ||||
|         quote['symbol'] = topic | ||||
|     # pass first quote asap | ||||
|     quote = normalize(first_ticker, calc_price=calc_price) | ||||
|     con = quote['contract'] | ||||
|     topic = '.'.join((con['symbol'], con[suffix])).lower() | ||||
|     quote['symbol'] = topic | ||||
| 
 | ||||
|         first_quote = {topic: quote} | ||||
|     first_quote = {topic: quote} | ||||
| 
 | ||||
|         # yield first quote asap | ||||
|         await ctx.send_yield(first_quote) | ||||
|     # ugh, clear ticks since we've consumed them | ||||
|     # (ahem, ib_insync is stateful trash) | ||||
|     first_ticker.ticks = [] | ||||
| 
 | ||||
|         # ticker.ticks = [] | ||||
|     log.debug(f"First ticker received {quote}") | ||||
| 
 | ||||
|         # ugh, clear ticks since we've consumed them | ||||
|         # (ahem, ib_insync is stateful trash) | ||||
|         first_ticker.ticks = [] | ||||
|     task_status.started((init_msgs,  first_quote)) | ||||
| 
 | ||||
|         log.debug(f"First ticker received {quote}") | ||||
|     if type(first_ticker.contract) not in (ibis.Commodity, ibis.Forex): | ||||
|         suffix = 'exchange' | ||||
|         calc_price = False  # should be real volume for contract | ||||
| 
 | ||||
|         if type(first_ticker.contract) not in (ibis.Commodity, ibis.Forex): | ||||
|             suffix = 'exchange' | ||||
| 
 | ||||
|             calc_price = False  # should be real volume for contract | ||||
| 
 | ||||
|         # with trio.move_on_after(10) as cs: | ||||
|         # wait for real volume on feed (trading might be closed) | ||||
| 
 | ||||
|         async with aclosing(stream): | ||||
| 
 | ||||
|             async for ticker in stream: | ||||
| 
 | ||||
|                 # for a real volume contract we rait for the first | ||||
|  | @ -1072,108 +1038,45 @@ async def stream_quotes( | |||
|                     # (ahem, ib_insync is truly stateful trash) | ||||
|                     ticker.ticks = [] | ||||
| 
 | ||||
|                     # tell incrementer task it can start | ||||
|                     _buffer.shm_incrementing(key).set() | ||||
| 
 | ||||
|                     # XXX: this works because we don't use | ||||
|                     # ``aclosing()`` above? | ||||
|                     break | ||||
| 
 | ||||
|             # enter stream loop | ||||
|             try: | ||||
|                 await stream_and_write( | ||||
|                     stream=stream, | ||||
|                     calc_price=calc_price, | ||||
|                     topic=topic, | ||||
|                     writer_already_exists=writer_already_exists, | ||||
|                     shm=shm, | ||||
|                     suffix=suffix, | ||||
|                     ctx=ctx, | ||||
|                 ) | ||||
|             finally: | ||||
|                 if not writer_already_exists: | ||||
|                     _local_buffer_writers[key] = False | ||||
|             # tell caller quotes are now coming in live | ||||
|             feed_is_live.set() | ||||
| 
 | ||||
|             async for ticker in stream: | ||||
| 
 | ||||
| async def stream_and_write( | ||||
|     stream, | ||||
|     calc_price: bool, | ||||
|     topic: str, | ||||
|     writer_already_exists: bool, | ||||
|     suffix: str, | ||||
|     ctx: tractor.Context, | ||||
|     shm: Optional['SharedArray'],  # noqa | ||||
| ) -> None: | ||||
|     """Core quote streaming and shm writing loop; optimize for speed! | ||||
| 
 | ||||
|     """ | ||||
|     # real-time stream | ||||
|     async for ticker in stream: | ||||
| 
 | ||||
|         # print(ticker.vwap) | ||||
|         quote = normalize( | ||||
|             ticker, | ||||
|             calc_price=calc_price | ||||
|         ) | ||||
|         quote['symbol'] = topic | ||||
|         # TODO: in theory you can send the IPC msg *before* | ||||
|         # writing to the sharedmem array to decrease latency, | ||||
|         # however, that will require `tractor.msg.pub` support | ||||
|         # here or at least some way to prevent task switching | ||||
|         # at the yield such that the array write isn't delayed | ||||
|         # while another consumer is serviced.. | ||||
| 
 | ||||
|         # if we are the lone tick writer start writing | ||||
|         # the buffer with appropriate trade data | ||||
|         if not writer_already_exists: | ||||
|             for tick in iterticks(quote, types=('trade', 'utrade',)): | ||||
|                 last = tick['price'] | ||||
| 
 | ||||
|                 # print(f"{quote['symbol']}: {tick}") | ||||
| 
 | ||||
|                 # update last entry | ||||
|                 # benchmarked in the 4-5 us range | ||||
|                 o, high, low, v = shm.array[-1][ | ||||
|                     ['open', 'high', 'low', 'volume'] | ||||
|                 ] | ||||
| 
 | ||||
|                 new_v = tick.get('size', 0) | ||||
| 
 | ||||
|                 if v == 0 and new_v: | ||||
|                     # no trades for this bar yet so the open | ||||
|                     # is also the close/last trade price | ||||
|                     o = last | ||||
| 
 | ||||
|                 shm.array[[ | ||||
|                     'open', | ||||
|                     'high', | ||||
|                     'low', | ||||
|                     'close', | ||||
|                     'volume', | ||||
|                 ]][-1] = ( | ||||
|                     o, | ||||
|                     max(high, last), | ||||
|                     min(low, last), | ||||
|                     last, | ||||
|                     v + new_v, | ||||
|                 # print(ticker.vwap) | ||||
|                 quote = normalize( | ||||
|                     ticker, | ||||
|                     calc_price=calc_price | ||||
|                 ) | ||||
| 
 | ||||
|         con = quote['contract'] | ||||
|         topic = '.'.join((con['symbol'], con[suffix])).lower() | ||||
|         quote['symbol'] = topic | ||||
|                 con = quote['contract'] | ||||
|                 topic = '.'.join((con['symbol'], con[suffix])).lower() | ||||
|                 quote['symbol'] = topic | ||||
| 
 | ||||
|         await ctx.send_yield({topic: quote}) | ||||
|                 await send_chan.send({topic: quote}) | ||||
| 
 | ||||
|         # ugh, clear ticks since we've consumed them | ||||
|         ticker.ticks = [] | ||||
|                 # ugh, clear ticks since we've consumed them | ||||
|                 ticker.ticks = [] | ||||
| 
 | ||||
| 
 | ||||
| def pack_position(pos: Position) -> Dict[str, Any]: | ||||
|     con = pos.contract | ||||
| 
 | ||||
|     if isinstance(con, Option): | ||||
|         # TODO: option symbol parsing and sane display: | ||||
|         symbol = con.localSymbol.replace(' ', '') | ||||
| 
 | ||||
|     else: | ||||
|         symbol = con.symbol | ||||
| 
 | ||||
|     return { | ||||
|         'broker': 'ib', | ||||
|         'account': pos.account, | ||||
|         'symbol': con.symbol, | ||||
|         'symbol': symbol, | ||||
|         'currency': con.currency, | ||||
|         'size': float(pos.position), | ||||
|         'avg_price': float(pos.avgCost) / float(con.multiplier or 1.0), | ||||
|  |  | |||
|  | @ -16,15 +16,17 @@ | |||
| 
 | ||||
| """ | ||||
| Kraken backend. | ||||
| 
 | ||||
| """ | ||||
| from contextlib import asynccontextmanager, AsyncExitStack | ||||
| from dataclasses import asdict, field | ||||
| from types import ModuleType | ||||
| from typing import List, Dict, Any, Tuple, Optional | ||||
| from typing import List, Dict, Any, Tuple | ||||
| import json | ||||
| import time | ||||
| 
 | ||||
| import trio_websocket | ||||
| from trio_typing import TaskStatus | ||||
| from trio_websocket._impl import ( | ||||
|     ConnectionClosed, | ||||
|     DisconnectionTimeout, | ||||
|  | @ -41,15 +43,11 @@ import tractor | |||
| from pydantic.dataclasses import dataclass | ||||
| from pydantic import BaseModel | ||||
| 
 | ||||
| 
 | ||||
| from .api import open_cached_client | ||||
| from ._util import resproc, SymbolNotFound, BrokerError | ||||
| from ..log import get_logger, get_console_log | ||||
| from ..data import ( | ||||
|     _buffer, | ||||
|     # iterticks, | ||||
|     attach_shm_array, | ||||
|     get_shm_token, | ||||
|     subscribe_ohlc_for_increment, | ||||
| ) | ||||
| from ..data import ShmArray | ||||
| 
 | ||||
| log = get_logger(__name__) | ||||
| 
 | ||||
|  | @ -315,6 +313,7 @@ def normalize( | |||
|     quote['brokerd_ts'] = time.time() | ||||
|     quote['symbol'] = quote['pair'] = quote['pair'].replace('/', '') | ||||
|     quote['last'] = quote['close'] | ||||
|     quote['bar_wap'] = ohlc.vwap | ||||
| 
 | ||||
|     # seriously eh? what's with this non-symmetry everywhere | ||||
|     # in subscription systems... | ||||
|  | @ -426,17 +425,37 @@ async def open_autorecon_ws(url): | |||
|             await stack.aclose() | ||||
| 
 | ||||
| 
 | ||||
| # @tractor.msg.pub | ||||
| async def backfill_bars( | ||||
|     sym: str, | ||||
|     shm: ShmArray,  # type: ignore # noqa | ||||
| 
 | ||||
|     count: int = 10,  # NOTE: any more and we'll overrun the underlying buffer | ||||
| 
 | ||||
|     task_status: TaskStatus[trio.CancelScope] = trio.TASK_STATUS_IGNORED, | ||||
| ) -> None: | ||||
|     """Fill historical bars into shared mem / storage afap. | ||||
|     """ | ||||
|     with trio.CancelScope() as cs: | ||||
|         async with open_cached_client('kraken') as client: | ||||
|             bars = await client.bars(symbol=sym) | ||||
|             shm.push(bars) | ||||
|             task_status.started(cs) | ||||
| 
 | ||||
| 
 | ||||
| async def stream_quotes( | ||||
|     # get_topics: Callable, | ||||
|     shm_token: Tuple[str, str, List[tuple]], | ||||
|     symbols: List[str] = ['XBTUSD', 'XMRUSD'], | ||||
|     # These are the symbols not expected by the ws api | ||||
|     # they are looked up inside this routine. | ||||
|     sub_type: str = 'ohlc', | ||||
| 
 | ||||
|     send_chan: trio.abc.SendChannel, | ||||
|     symbols: List[str], | ||||
|     shm: ShmArray, | ||||
|     feed_is_live: trio.Event, | ||||
|     loglevel: str = None, | ||||
|     # compat with eventual ``tractor.msg.pub`` | ||||
|     topics: Optional[List[str]] = None, | ||||
| 
 | ||||
|     # backend specific | ||||
|     sub_type: str = 'ohlc', | ||||
| 
 | ||||
|     # startup sync | ||||
|     task_status: TaskStatus[Tuple[Dict, Dict]] = trio.TASK_STATUS_IGNORED, | ||||
| 
 | ||||
| ) -> None: | ||||
|     """Subscribe for ohlc stream of quotes for ``pairs``. | ||||
| 
 | ||||
|  | @ -447,7 +466,8 @@ async def stream_quotes( | |||
| 
 | ||||
|     ws_pairs = {} | ||||
|     sym_infos = {} | ||||
|     async with get_client() as client: | ||||
| 
 | ||||
|     async with open_cached_client('kraken') as client, send_chan as send_chan: | ||||
| 
 | ||||
|         # keep client cached for real-time section | ||||
|         for sym in symbols: | ||||
|  | @ -458,40 +478,16 @@ async def stream_quotes( | |||
|             sym_infos[sym] = syminfo | ||||
|             ws_pairs[sym] = si.wsname | ||||
| 
 | ||||
|         # maybe load historical ohlcv in to shared mem | ||||
|         # check if shm has already been created by previous | ||||
|         # feed initialization | ||||
|         writer_exists = get_shm_token(shm_token['shm_name']) | ||||
| 
 | ||||
|         symbol = symbols[0] | ||||
| 
 | ||||
|         if not writer_exists: | ||||
|             shm = attach_shm_array( | ||||
|                 token=shm_token, | ||||
|                 # we are writer | ||||
|                 readonly=False, | ||||
|             ) | ||||
|             bars = await client.bars(symbol=symbol) | ||||
| 
 | ||||
|             shm.push(bars) | ||||
|             shm_token = shm.token | ||||
| 
 | ||||
|             times = shm.array['time'] | ||||
|             delay_s = times[-1] - times[times != times[-1]][-1] | ||||
|             subscribe_ohlc_for_increment(shm, delay_s) | ||||
| 
 | ||||
|         # yield shm_token, not writer_exists | ||||
|         init_msgs = { | ||||
|             # pass back token, and bool, signalling if we're the writer | ||||
|             # and that history has been written | ||||
|             symbol: { | ||||
|                 'is_shm_writer': not writer_exists, | ||||
|                 'shm_token': shm_token, | ||||
|                 'symbol_info': sym_infos[sym], | ||||
|             } | ||||
|             # for sym in symbols | ||||
|                 'shm_write_opts': {'sum_tick_vml': False}, | ||||
|             }, | ||||
|         } | ||||
|         yield init_msgs | ||||
| 
 | ||||
|         async with open_autorecon_ws('wss://ws.kraken.com/') as ws: | ||||
| 
 | ||||
|  | @ -521,15 +517,16 @@ async def stream_quotes( | |||
|             # pull a first quote and deliver | ||||
|             msg_gen = stream_messages(ws) | ||||
| 
 | ||||
|             # TODO: use ``anext()`` when it lands in 3.10! | ||||
|             typ, ohlc_last = await msg_gen.__anext__() | ||||
| 
 | ||||
|             topic, quote = normalize(ohlc_last) | ||||
| 
 | ||||
|             # packetize as {topic: quote} | ||||
|             yield {topic: quote} | ||||
|             first_quote = {topic: quote} | ||||
|             task_status.started((init_msgs,  first_quote)) | ||||
| 
 | ||||
|             # tell incrementer task it can start | ||||
|             _buffer.shm_incrementing(shm_token['shm_name']).set() | ||||
|             # lol, only "closes" when they're margin squeezing clients ;P | ||||
|             feed_is_live.set() | ||||
| 
 | ||||
|             # keep start of last interval for volume tracking | ||||
|             last_interval_start = ohlc_last.etime | ||||
|  | @ -546,15 +543,18 @@ async def stream_quotes( | |||
|                     # https://trade.kraken.com/charts/KRAKEN:BTC-USD?period=1m | ||||
|                     volume = ohlc.volume | ||||
| 
 | ||||
|                     # new interval | ||||
|                     # new OHLC sample interval | ||||
|                     if ohlc.etime > last_interval_start: | ||||
|                         last_interval_start = ohlc.etime | ||||
|                         tick_volume = volume | ||||
| 
 | ||||
|                     else: | ||||
|                         # this is the tick volume *within the interval* | ||||
|                         tick_volume = volume - ohlc_last.volume | ||||
| 
 | ||||
|                     ohlc_last = ohlc | ||||
|                     last = ohlc.close | ||||
| 
 | ||||
|                     if tick_volume: | ||||
|                         ohlc.ticks.append({ | ||||
|                             'type': 'trade', | ||||
|  | @ -564,43 +564,10 @@ async def stream_quotes( | |||
| 
 | ||||
|                     topic, quote = normalize(ohlc) | ||||
| 
 | ||||
|                     # if we are the lone tick writer start writing | ||||
|                     # the buffer with appropriate trade data | ||||
|                     if not writer_exists: | ||||
|                         # update last entry | ||||
|                         # benchmarked in the 4-5 us range | ||||
|                         o, high, low, v = shm.array[-1][ | ||||
|                             ['open', 'high', 'low', 'volume'] | ||||
|                         ] | ||||
|                         new_v = tick_volume | ||||
| 
 | ||||
|                         if v == 0 and new_v: | ||||
|                             # no trades for this bar yet so the open | ||||
|                             # is also the close/last trade price | ||||
|                             o = last | ||||
| 
 | ||||
|                         # write shm | ||||
|                         shm.array[ | ||||
|                             ['open', | ||||
|                              'high', | ||||
|                              'low', | ||||
|                              'close', | ||||
|                              'bar_wap',  # in this case vwap of bar | ||||
|                              'volume'] | ||||
|                         ][-1] = ( | ||||
|                             o, | ||||
|                             max(high, last), | ||||
|                             min(low, last), | ||||
|                             last, | ||||
|                             ohlc.vwap, | ||||
|                             volume, | ||||
|                         ) | ||||
|                     ohlc_last = ohlc | ||||
| 
 | ||||
|                 elif typ == 'l1': | ||||
|                     quote = ohlc | ||||
|                     topic = quote['symbol'] | ||||
| 
 | ||||
|                 # XXX: format required by ``tractor.msg.pub`` | ||||
|                 # requires a ``Dict[topic: str, quote: dict]`` | ||||
|                 yield {topic: quote} | ||||
|                 await send_chan.send({topic: quote}) | ||||
|  |  | |||
|  | @ -1180,6 +1180,11 @@ def normalize( | |||
|     return new | ||||
| 
 | ||||
| 
 | ||||
| # TODO: currently this backend uses entirely different | ||||
| # data feed machinery that was written earlier then the | ||||
| # existing stuff used in other backends. This needs to | ||||
| # be ported eventually and should *just work* despite | ||||
| # being a multi-symbol, poll-style feed system. | ||||
| @tractor.stream | ||||
| async def stream_quotes( | ||||
|     ctx: tractor.Context,  # marks this as a streaming func | ||||
|  | @ -1192,7 +1197,7 @@ async def stream_quotes( | |||
|     # XXX: required to propagate ``tractor`` loglevel to piker logging | ||||
|     get_console_log(loglevel) | ||||
| 
 | ||||
|     async with api.get_cached_client('questrade') as client: | ||||
|     async with api.open_cached_client('questrade') as client: | ||||
|         if feed_type == 'stock': | ||||
|             formatter = format_stock_quote | ||||
|             get_quotes = await stock_quoter(client, symbols) | ||||
|  |  | |||
|  | @ -181,6 +181,7 @@ async def maybe_open_emsd( | |||
| 
 | ||||
|     async with tractor.find_actor('pikerd') as portal: | ||||
|         assert portal | ||||
| 
 | ||||
|         name = await portal.run( | ||||
|             spawn_emsd, | ||||
|             brokername=brokername, | ||||
|  | @ -190,7 +191,6 @@ async def maybe_open_emsd( | |||
|             yield portal | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @asynccontextmanager | ||||
| async def open_ems( | ||||
|     broker: str, | ||||
|  | @ -247,4 +247,13 @@ async def open_ems( | |||
|         with trio.fail_after(10): | ||||
|             await book._ready_to_receive.wait() | ||||
| 
 | ||||
|         yield book, trades_stream | ||||
|         try: | ||||
|             yield book, trades_stream | ||||
| 
 | ||||
|         finally: | ||||
|             # TODO: we want to eventually keep this up (by having | ||||
|             # the exec loop keep running in the pikerd tree) but for | ||||
|             # now we have to kill the context to avoid backpressure | ||||
|             # build-up on the shm write loop. | ||||
|             with trio.CancelScope(shield=True): | ||||
|                 await trades_stream.aclose() | ||||
|  |  | |||
|  | @ -40,7 +40,11 @@ log = get_logger(__name__) | |||
| 
 | ||||
| 
 | ||||
| # TODO: numba all of this | ||||
| def mk_check(trigger_price, known_last) -> Callable[[float, float], bool]: | ||||
| def mk_check( | ||||
|     trigger_price: float, | ||||
|     known_last: float, | ||||
|     action: str, | ||||
| ) -> Callable[[float, float], bool]: | ||||
|     """Create a predicate for given ``exec_price`` based on last known | ||||
|     price, ``known_last``. | ||||
| 
 | ||||
|  | @ -68,7 +72,7 @@ def mk_check(trigger_price, known_last) -> Callable[[float, float], bool]: | |||
|         return check_lt | ||||
| 
 | ||||
|     else: | ||||
|         return None, None | ||||
|         return None | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
|  | @ -230,6 +234,7 @@ async def execute_triggers( | |||
| 
 | ||||
| async def exec_loop( | ||||
|     ctx: tractor.Context, | ||||
|     feed: 'Feed',  # noqa | ||||
|     broker: str, | ||||
|     symbol: str, | ||||
|     _exec_mode: str, | ||||
|  | @ -239,67 +244,63 @@ async def exec_loop( | |||
|     to brokers. | ||||
| 
 | ||||
|     """ | ||||
|     async with data.open_feed( | ||||
|         broker, | ||||
|         [symbol], | ||||
|         loglevel='info', | ||||
|     ) as feed: | ||||
| 
 | ||||
|         # TODO: get initial price quote from target broker | ||||
|         first_quote = await feed.receive() | ||||
|     # TODO: get initial price quote from target broker | ||||
|     first_quote = await feed.receive() | ||||
| 
 | ||||
|         book = get_dark_book(broker) | ||||
|         book.lasts[(broker, symbol)] = first_quote[symbol]['last'] | ||||
|     book = get_dark_book(broker) | ||||
|     book.lasts[(broker, symbol)] = first_quote[symbol]['last'] | ||||
| 
 | ||||
|         # TODO: wrap this in a more re-usable general api | ||||
|         client_factory = getattr(feed.mod, 'get_client_proxy', None) | ||||
|     # TODO: wrap this in a more re-usable general api | ||||
|     client_factory = getattr(feed.mod, 'get_client_proxy', None) | ||||
| 
 | ||||
|         if client_factory is not None and _exec_mode != 'paper': | ||||
|     if client_factory is not None and _exec_mode != 'paper': | ||||
| 
 | ||||
|             # we have an order API for this broker | ||||
|             client = client_factory(feed._brokerd_portal) | ||||
|         # we have an order API for this broker | ||||
|         client = client_factory(feed._brokerd_portal) | ||||
| 
 | ||||
|         else: | ||||
|             # force paper mode | ||||
|             log.warning(f'Entering paper trading mode for {broker}') | ||||
|     else: | ||||
|         # force paper mode | ||||
|         log.warning(f'Entering paper trading mode for {broker}') | ||||
| 
 | ||||
|             client = PaperBoi( | ||||
|                 broker, | ||||
|                 *trio.open_memory_channel(100), | ||||
|                 _buys={}, | ||||
|                 _sells={}, | ||||
|                 _reqids={}, | ||||
|             ) | ||||
|         client = PaperBoi( | ||||
|             broker, | ||||
|             *trio.open_memory_channel(100), | ||||
|             _buys={}, | ||||
|             _sells={}, | ||||
| 
 | ||||
|             # for paper mode we need to mock this trades response feed | ||||
|             # so we pass a duck-typed feed-looking mem chan which is fed | ||||
|             # fill and submission events from the exec loop | ||||
|             feed._trade_stream = client.trade_stream | ||||
|             _reqids={}, | ||||
|         ) | ||||
| 
 | ||||
|             # init the trades stream | ||||
|             client._to_trade_stream.send_nowait({'local_trades': 'start'}) | ||||
|         # for paper mode we need to mock this trades response feed | ||||
|         # so we pass a duck-typed feed-looking mem chan which is fed | ||||
|         # fill and submission events from the exec loop | ||||
|         feed._trade_stream = client.trade_stream | ||||
| 
 | ||||
|             _exec_mode = 'paper' | ||||
|         # init the trades stream | ||||
|         client._to_trade_stream.send_nowait({'local_trades': 'start'}) | ||||
| 
 | ||||
|         # return control to parent task | ||||
|         task_status.started((first_quote, feed, client)) | ||||
|         _exec_mode = 'paper' | ||||
| 
 | ||||
|         # shield this field so the remote brokerd does not get cancelled | ||||
|         stream = feed.stream | ||||
|         with stream.shield(): | ||||
|             async with trio.open_nursery() as n: | ||||
|                 n.start_soon( | ||||
|                     execute_triggers, | ||||
|                     broker, | ||||
|                     symbol, | ||||
|                     stream, | ||||
|                     ctx, | ||||
|                     client, | ||||
|                     book | ||||
|                 ) | ||||
|     # return control to parent task | ||||
|     task_status.started((first_quote, feed, client)) | ||||
| 
 | ||||
|                 if _exec_mode == 'paper': | ||||
|                     n.start_soon(simulate_fills, stream.clone(), client) | ||||
|     stream = feed.stream | ||||
|     async with trio.open_nursery() as n: | ||||
|         n.start_soon( | ||||
|             execute_triggers, | ||||
|             broker, | ||||
|             symbol, | ||||
|             stream, | ||||
|             ctx, | ||||
|             client, | ||||
|             book | ||||
|         ) | ||||
| 
 | ||||
|         if _exec_mode == 'paper': | ||||
|             # TODO: make this an actual broadcast channels as in: | ||||
|             # https://github.com/python-trio/trio/issues/987 | ||||
|             n.start_soon(simulate_fills, stream, client) | ||||
| 
 | ||||
| 
 | ||||
| # TODO: lots of cases still to handle | ||||
|  | @ -512,7 +513,6 @@ async def process_order_cmds( | |||
|             exec_mode = cmd['exec_mode'] | ||||
| 
 | ||||
|             broker = brokers[0] | ||||
|             last = dark_book.lasts[(broker, sym)] | ||||
| 
 | ||||
|             if exec_mode == 'live' and action in ('buy', 'sell',): | ||||
| 
 | ||||
|  | @ -557,9 +557,10 @@ async def process_order_cmds( | |||
|                 # price received from the feed, instead of being | ||||
|                 # like every other shitty tina platform that makes | ||||
|                 # the user choose the predicate operator. | ||||
|                 pred = mk_check(trigger_price, last) | ||||
|                 last = dark_book.lasts[(broker, sym)] | ||||
|                 pred = mk_check(trigger_price, last, action) | ||||
| 
 | ||||
|                 tick_slap: float = 5 | ||||
|                 spread_slap: float = 5 | ||||
|                 min_tick = feed.symbols[sym].tick_size | ||||
| 
 | ||||
|                 if action == 'buy': | ||||
|  | @ -569,12 +570,12 @@ async def process_order_cmds( | |||
|                     # TODO: we probably need to scale this based | ||||
|                     # on some near term historical spread | ||||
|                     # measure? | ||||
|                     abs_diff_away = tick_slap * min_tick | ||||
|                     abs_diff_away = spread_slap * min_tick | ||||
| 
 | ||||
|                 elif action == 'sell': | ||||
|                     tickfilter = ('bid', 'last', 'trade') | ||||
|                     percent_away = -0.005 | ||||
|                     abs_diff_away = -tick_slap * min_tick | ||||
|                     abs_diff_away = -spread_slap * min_tick | ||||
| 
 | ||||
|                 else:  # alert | ||||
|                     tickfilter = ('trade', 'utrade', 'last') | ||||
|  | @ -647,35 +648,41 @@ async def _emsd_main( | |||
|         async with trio.open_nursery() as n: | ||||
| 
 | ||||
|             # TODO: eventually support N-brokers | ||||
| 
 | ||||
|             # start the condition scan loop | ||||
|             quote, feed, client = await n.start( | ||||
|                 exec_loop, | ||||
|                 ctx, | ||||
|             async with data.open_feed( | ||||
|                 broker, | ||||
|                 symbol, | ||||
|                 _mode, | ||||
|             ) | ||||
|                 [symbol], | ||||
|                 loglevel='info', | ||||
|             ) as feed: | ||||
| 
 | ||||
|             await n.start( | ||||
|                 process_broker_trades, | ||||
|                 ctx, | ||||
|                 feed, | ||||
|                 dark_book, | ||||
|             ) | ||||
|                 # start the condition scan loop | ||||
|                 quote, feed, client = await n.start( | ||||
|                     exec_loop, | ||||
|                     ctx, | ||||
|                     feed, | ||||
|                     broker, | ||||
|                     symbol, | ||||
|                     _mode, | ||||
|                 ) | ||||
| 
 | ||||
|             # connect back to the calling actor (the one that is | ||||
|             # acting as an EMS client and will submit orders) to | ||||
|             # receive requests pushed over a tractor stream | ||||
|             # using (for now) an async generator. | ||||
|             order_stream = await portal.run(send_order_cmds) | ||||
|                 await n.start( | ||||
|                     process_broker_trades, | ||||
|                     ctx, | ||||
|                     feed, | ||||
|                     dark_book, | ||||
|                 ) | ||||
| 
 | ||||
|             # start inbound order request processing | ||||
|             await process_order_cmds( | ||||
|                 ctx, | ||||
|                 order_stream, | ||||
|                 symbol, | ||||
|                 feed, | ||||
|                 client, | ||||
|                 dark_book, | ||||
|             ) | ||||
|                 # connect back to the calling actor (the one that is | ||||
|                 # acting as an EMS client and will submit orders) to | ||||
|                 # receive requests pushed over a tractor stream | ||||
|                 # using (for now) an async generator. | ||||
|                 order_stream = await portal.run(send_order_cmds) | ||||
| 
 | ||||
|                 # start inbound order request processing | ||||
|                 await process_order_cmds( | ||||
|                     ctx, | ||||
|                     order_stream, | ||||
|                     symbol, | ||||
|                     feed, | ||||
|                     client, | ||||
|                     dark_book, | ||||
|                 ) | ||||
|  |  | |||
|  | @ -20,20 +20,29 @@ Data feed apis and infra. | |||
| We provide tsdb integrations for retrieving | ||||
| and storing data from your brokers as well as | ||||
| sharing your feeds with other fellow pikers. | ||||
| 
 | ||||
| """ | ||||
| from dataclasses import dataclass, field | ||||
| from contextlib import asynccontextmanager | ||||
| from functools import partial | ||||
| from importlib import import_module | ||||
| from types import ModuleType | ||||
| from typing import ( | ||||
|     Dict, Any, Sequence, AsyncIterator, Optional | ||||
|     Dict, Any, Sequence, | ||||
|     AsyncIterator, Optional, | ||||
|     List | ||||
| ) | ||||
| 
 | ||||
| import trio | ||||
| from trio_typing import TaskStatus | ||||
| import tractor | ||||
| from pydantic import BaseModel | ||||
| 
 | ||||
| from ..brokers import get_brokermod | ||||
| from ..log import get_logger, get_console_log | ||||
| from .._daemon import spawn_brokerd, maybe_open_pikerd | ||||
| from .._daemon import ( | ||||
|     maybe_spawn_brokerd, | ||||
| ) | ||||
| from ._normalize import iterticks | ||||
| from ._sharedmem import ( | ||||
|     maybe_open_shm_array, | ||||
|  | @ -43,9 +52,12 @@ from ._sharedmem import ( | |||
|     get_shm_token, | ||||
| ) | ||||
| from ._source import base_iohlc_dtype, Symbol | ||||
| from ._buffer import ( | ||||
| from ._sampling import ( | ||||
|     _shms, | ||||
|     _incrementers, | ||||
|     increment_ohlc_buffer, | ||||
|     subscribe_ohlc_for_increment | ||||
|     iter_ohlc_periods, | ||||
|     sample_and_broadcast, | ||||
| ) | ||||
| 
 | ||||
| __all__ = [ | ||||
|  | @ -54,7 +66,7 @@ __all__ = [ | |||
|     'attach_shm_array', | ||||
|     'open_shm_array', | ||||
|     'get_shm_token', | ||||
|     'subscribe_ohlc_for_increment', | ||||
|     # 'subscribe_ohlc_for_increment', | ||||
| ] | ||||
| 
 | ||||
| 
 | ||||
|  | @ -74,57 +86,229 @@ def get_ingestormod(name: str) -> ModuleType: | |||
|     return module | ||||
| 
 | ||||
| 
 | ||||
| @asynccontextmanager | ||||
| async def maybe_spawn_brokerd( | ||||
|     brokername: str, | ||||
|     loglevel: Optional[str] = None, | ||||
| class _FeedsBus(BaseModel): | ||||
|     """Data feeds broadcaster and persistence management. | ||||
| 
 | ||||
|     # XXX: you should pretty much never want debug mode | ||||
|     # for data daemons when running in production. | ||||
|     debug_mode: bool = True, | ||||
| ) -> tractor._portal.Portal: | ||||
|     """If no ``brokerd.{brokername}`` daemon-actor can be found, | ||||
|     spawn one in a local subactor and return a portal to it. | ||||
|     This is a brokerd side api used to manager persistent real-time | ||||
|     streams that can be allocated and left alive indefinitely. | ||||
| 
 | ||||
|     """ | ||||
|     if loglevel: | ||||
|         get_console_log(loglevel) | ||||
|     brokername: str | ||||
|     nursery: trio.Nursery | ||||
|     feeds: Dict[str, trio.CancelScope] = {} | ||||
|     subscribers: Dict[str, List[tractor.Context]] = {} | ||||
|     task_lock: trio.StrictFIFOLock = trio.StrictFIFOLock() | ||||
| 
 | ||||
|     dname = f'brokerd.{brokername}' | ||||
|     async with tractor.find_actor(dname) as portal: | ||||
|     class Config: | ||||
|         arbitrary_types_allowed = True | ||||
| 
 | ||||
|         # WTF: why doesn't this work? | ||||
|         if portal is not None: | ||||
|             yield portal | ||||
|     async def cancel_all(self) -> None: | ||||
|         for sym, (cs, msg, quote) in self.feeds.items(): | ||||
|             log.debug(f'Cancelling cached feed for {self.brokername}:{sym}') | ||||
|             cs.cancel() | ||||
| 
 | ||||
| 
 | ||||
| _bus: _FeedsBus = None | ||||
| 
 | ||||
| 
 | ||||
| def get_feed_bus( | ||||
|     brokername: str, | ||||
|     nursery: Optional[trio.Nursery] = None, | ||||
| ) -> _FeedsBus: | ||||
|     """ | ||||
|     Retreive broker-daemon-local data feeds bus from process global | ||||
|     scope. Serialize task access to lock. | ||||
| 
 | ||||
|     """ | ||||
| 
 | ||||
|     global _bus | ||||
| 
 | ||||
|     if nursery is not None: | ||||
|         assert _bus is None, "Feeds manager is already setup?" | ||||
| 
 | ||||
|         # this is initial setup by parent actor | ||||
|         _bus = _FeedsBus( | ||||
|             brokername=brokername, | ||||
|             nursery=nursery, | ||||
|         ) | ||||
|         assert not _bus.feeds | ||||
| 
 | ||||
|     assert _bus.brokername == brokername, "Uhhh wtf" | ||||
|     return _bus | ||||
| 
 | ||||
| 
 | ||||
| async def _setup_persistent_brokerd(brokername:  str) -> None: | ||||
|     """Allocate a actor-wide service nursery in ``brokerd`` | ||||
|     such that feeds can be run in the background persistently by | ||||
|     the broker backend as needed. | ||||
| 
 | ||||
|     """ | ||||
|     try: | ||||
|         async with trio.open_nursery() as service_nursery: | ||||
| 
 | ||||
|             # assign a nursery to the feeds bus for spawning | ||||
|             # background tasks from clients | ||||
|             bus = get_feed_bus(brokername, service_nursery) | ||||
| 
 | ||||
|             # we pin this task to keep the feeds manager active until the | ||||
|             # parent actor decides to tear it down | ||||
|             await trio.sleep_forever() | ||||
|     finally: | ||||
|         # TODO: this needs to be shielded? | ||||
|         await bus.cancel_all() | ||||
| 
 | ||||
| 
 | ||||
| async def allocate_persistent_feed( | ||||
|     ctx: tractor.Context, | ||||
|     bus: _FeedsBus, | ||||
|     brokername: str, | ||||
|     symbol: str, | ||||
|     loglevel: str, | ||||
|     task_status: TaskStatus[trio.CancelScope] = trio.TASK_STATUS_IGNORED, | ||||
| ) -> None: | ||||
| 
 | ||||
|     try: | ||||
|         mod = get_brokermod(brokername) | ||||
|     except ImportError: | ||||
|         mod = get_ingestormod(brokername) | ||||
| 
 | ||||
|     # allocate shm array for this broker/symbol | ||||
|     # XXX: we should get an error here if one already exists | ||||
| 
 | ||||
|     shm, opened = maybe_open_shm_array( | ||||
|         key=sym_to_shm_key(brokername, symbol), | ||||
| 
 | ||||
|         # use any broker defined ohlc dtype: | ||||
|         dtype=getattr(mod, '_ohlc_dtype', base_iohlc_dtype), | ||||
| 
 | ||||
|         # we expect the sub-actor to write | ||||
|         readonly=False, | ||||
|     ) | ||||
| 
 | ||||
|     # do history validation? | ||||
|     assert opened, f'Persistent shm for {symbol} was already open?!' | ||||
|     # if not opened: | ||||
|     #     raise RuntimeError("Persistent shm for sym was already open?!") | ||||
| 
 | ||||
|     send, quote_stream = trio.open_memory_channel(10) | ||||
|     feed_is_live = trio.Event() | ||||
| 
 | ||||
|     # establish broker backend quote stream | ||||
|     # ``stream_quotes()`` is a required backend func | ||||
|     init_msg, first_quote = await bus.nursery.start( | ||||
|         partial( | ||||
|             mod.stream_quotes, | ||||
|             send_chan=send, | ||||
|             feed_is_live=feed_is_live, | ||||
|             symbols=[symbol], | ||||
|             shm=shm, | ||||
|             loglevel=loglevel, | ||||
|         ) | ||||
|     ) | ||||
| 
 | ||||
|     init_msg[symbol]['shm_token'] = shm.token | ||||
|     cs = bus.nursery.cancel_scope | ||||
| 
 | ||||
|     # TODO: make this into a composed type which also | ||||
|     # contains the backfiller cs for individual super-based | ||||
|     # resspawns when needed. | ||||
|     bus.feeds[symbol] = (cs, init_msg, first_quote) | ||||
| 
 | ||||
|     if opened: | ||||
| 
 | ||||
|         # start history backfill task ``backfill_bars()`` is | ||||
|         # a required backend func this must block until shm is | ||||
|         # filled with first set of ohlc bars | ||||
|         await bus.nursery.start(mod.backfill_bars, symbol, shm) | ||||
| 
 | ||||
|     times = shm.array['time'] | ||||
|     delay_s = times[-1] - times[times != times[-1]][-1] | ||||
| 
 | ||||
|     # pass OHLC sample rate in seconds | ||||
|     init_msg[symbol]['sample_rate'] = delay_s | ||||
| 
 | ||||
|     # yield back control to starting nursery | ||||
|     task_status.started((init_msg,  first_quote)) | ||||
| 
 | ||||
|     await feed_is_live.wait() | ||||
| 
 | ||||
|     if opened: | ||||
|         _shms.setdefault(delay_s, []).append(shm) | ||||
| 
 | ||||
|         # start shm incrementing for OHLC sampling | ||||
|         if _incrementers.get(delay_s) is None: | ||||
|             cs = await bus.nursery.start(increment_ohlc_buffer, delay_s) | ||||
| 
 | ||||
|     sum_tick_vlm: bool = init_msg.get( | ||||
|         'shm_write_opts', {} | ||||
|     ).get('sum_tick_vlm', True) | ||||
| 
 | ||||
|     # start sample loop | ||||
|     await sample_and_broadcast(bus, shm, quote_stream, sum_tick_vlm) | ||||
| 
 | ||||
| 
 | ||||
| @tractor.stream | ||||
| async def attach_feed_bus( | ||||
|     ctx: tractor.Context, | ||||
|     brokername: str, | ||||
|     symbol: str, | ||||
|     loglevel: str, | ||||
| ): | ||||
| 
 | ||||
|     # try: | ||||
|     if loglevel is None: | ||||
|         loglevel = tractor.current_actor().loglevel | ||||
| 
 | ||||
|     # XXX: required to propagate ``tractor`` loglevel to piker logging | ||||
|     get_console_log(loglevel or tractor.current_actor().loglevel) | ||||
| 
 | ||||
|     # ensure we are who we think we are | ||||
|     assert 'brokerd' in tractor.current_actor().name | ||||
| 
 | ||||
|     bus = get_feed_bus(brokername) | ||||
| 
 | ||||
|     async with bus.task_lock: | ||||
|         task_cs = bus.feeds.get(symbol) | ||||
|         sub_only: bool = False | ||||
| 
 | ||||
|         # if no cached feed for this symbol has been created for this | ||||
|         # brokerd yet, start persistent stream and shm writer task in | ||||
|         # service nursery | ||||
|         if task_cs is None: | ||||
|             init_msg, first_quote = await bus.nursery.start( | ||||
|                 partial( | ||||
|                     allocate_persistent_feed, | ||||
|                     ctx=ctx, | ||||
|                     bus=bus, | ||||
|                     brokername=brokername, | ||||
|                     symbol=symbol, | ||||
|                     loglevel=loglevel, | ||||
|                 ) | ||||
|             ) | ||||
|             bus.subscribers.setdefault(symbol, []).append(ctx) | ||||
|         else: | ||||
|             # ask root ``pikerd`` daemon to spawn the daemon we need if | ||||
|             # pikerd is not live we now become the root of the | ||||
|             # process tree | ||||
|             async with maybe_open_pikerd( | ||||
|                 loglevel=loglevel | ||||
|             ) as pikerd_portal: | ||||
|             sub_only = True | ||||
| 
 | ||||
|                 if pikerd_portal is None: | ||||
|                     # we are root so spawn brokerd directly in our tree | ||||
|                     # the root nursery is accessed through process global state | ||||
|                     await spawn_brokerd(brokername, loglevel=loglevel) | ||||
|     # XXX: ``first_quote`` may be outdated here if this is secondary | ||||
|     # subscriber | ||||
|     cs, init_msg, first_quote = bus.feeds[symbol] | ||||
| 
 | ||||
|                 else: | ||||
|                     await pikerd_portal.run( | ||||
|                         spawn_brokerd, | ||||
|                         brokername=brokername, | ||||
|                         loglevel=loglevel, | ||||
|                         debug_mode=debug_mode, | ||||
|                     ) | ||||
|     # send this even to subscribers to existing feed? | ||||
|     await ctx.send_yield(init_msg) | ||||
|     await ctx.send_yield(first_quote) | ||||
| 
 | ||||
|                 async with tractor.wait_for_actor(dname) as portal: | ||||
|                     yield portal | ||||
|     if sub_only: | ||||
|         bus.subscribers[symbol].append(ctx) | ||||
| 
 | ||||
|     try: | ||||
|         await trio.sleep_forever() | ||||
|     finally: | ||||
|         bus.subscribers[symbol].remove(ctx) | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
| class Feed: | ||||
|     """A data feed for client-side interaction with far-process | ||||
|     """A data feed for client-side interaction with far-process# }}} | ||||
|     real-time data sources. | ||||
| 
 | ||||
|     This is an thin abstraction on top of ``tractor``'s portals for | ||||
|  | @ -135,10 +319,11 @@ class Feed: | |||
|     stream: AsyncIterator[Dict[str, Any]] | ||||
|     shm: ShmArray | ||||
|     mod: ModuleType | ||||
|     # ticks: ShmArray | ||||
| 
 | ||||
|     _brokerd_portal: tractor._portal.Portal | ||||
|     _index_stream: Optional[AsyncIterator[int]] = None | ||||
|     _trade_stream: Optional[AsyncIterator[Dict[str, Any]]] = None | ||||
|     _max_sample_rate: int = 0 | ||||
| 
 | ||||
|     # cache of symbol info messages received as first message when | ||||
|     # a stream startsc. | ||||
|  | @ -147,15 +332,19 @@ class Feed: | |||
|     async def receive(self) -> dict: | ||||
|         return await self.stream.__anext__() | ||||
| 
 | ||||
|     async def index_stream(self) -> AsyncIterator[int]: | ||||
|     async def index_stream( | ||||
|         self, | ||||
|         delay_s: Optional[int] = None | ||||
| 
 | ||||
|     ) -> AsyncIterator[int]: | ||||
| 
 | ||||
|         if not self._index_stream: | ||||
|             # XXX: this should be singleton on a host, | ||||
|             # a lone broker-daemon per provider should be | ||||
|             # created for all practical purposes | ||||
|             self._index_stream = await self._brokerd_portal.run( | ||||
|                 increment_ohlc_buffer, | ||||
|                 shm_token=self.shm.token, | ||||
|                 topics=['index'], | ||||
|                 iter_ohlc_periods, | ||||
|                 delay_s=delay_s or self._max_sample_rate, | ||||
|             ) | ||||
| 
 | ||||
|         return self._index_stream | ||||
|  | @ -214,40 +403,29 @@ async def open_feed( | |||
|     # TODO: do all! | ||||
|     sym = symbols[0] | ||||
| 
 | ||||
|     # Attempt to allocate (or attach to) shm array for this broker/symbol | ||||
|     shm, opened = maybe_open_shm_array( | ||||
|         key=sym_to_shm_key(brokername, sym), | ||||
| 
 | ||||
|         # use any broker defined ohlc dtype: | ||||
|         dtype=getattr(mod, '_ohlc_dtype', base_iohlc_dtype), | ||||
| 
 | ||||
|         # we expect the sub-actor to write | ||||
|         readonly=True, | ||||
|     ) | ||||
| 
 | ||||
|     async with maybe_spawn_brokerd( | ||||
| 
 | ||||
|         brokername, | ||||
|         loglevel=loglevel, | ||||
| 
 | ||||
|         # TODO: add a cli flag for this | ||||
|         # debug_mode=False, | ||||
| 
 | ||||
|     ) as portal: | ||||
| 
 | ||||
|         stream = await portal.run( | ||||
|             mod.stream_quotes, | ||||
| 
 | ||||
|             # TODO: actually handy multiple symbols... | ||||
|             symbols=symbols, | ||||
| 
 | ||||
|             shm_token=shm.token, | ||||
| 
 | ||||
|             # compat with eventual ``tractor.msg.pub`` | ||||
|             topics=symbols, | ||||
|             attach_feed_bus, | ||||
|             brokername=brokername, | ||||
|             symbol=sym, | ||||
|             loglevel=loglevel, | ||||
|         ) | ||||
| 
 | ||||
|         # TODO: can we make this work better with the proposed | ||||
|         # context based bidirectional streaming style api proposed in: | ||||
|         # https://github.com/goodboy/tractor/issues/53 | ||||
|         init_msg = await stream.receive() | ||||
| 
 | ||||
|         # we can only read from shm | ||||
|         shm = attach_shm_array( | ||||
|             token=init_msg[sym]['shm_token'], | ||||
|             readonly=True, | ||||
|         ) | ||||
| 
 | ||||
|         feed = Feed( | ||||
|             name=brokername, | ||||
|             stream=stream, | ||||
|  | @ -255,15 +433,12 @@ async def open_feed( | |||
|             mod=mod, | ||||
|             _brokerd_portal=portal, | ||||
|         ) | ||||
| 
 | ||||
|         # TODO: we can't do this **and** be compate with | ||||
|         # ``tractor.msg.pub``, should we maybe just drop this after | ||||
|         # tests are in? | ||||
|         init_msg = await stream.receive() | ||||
|         ohlc_sample_rates = [] | ||||
| 
 | ||||
|         for sym, data in init_msg.items(): | ||||
| 
 | ||||
|             si = data['symbol_info'] | ||||
|             ohlc_sample_rates.append(data['sample_rate']) | ||||
| 
 | ||||
|             symbol = Symbol( | ||||
|                 key=sym, | ||||
|  | @ -275,12 +450,17 @@ async def open_feed( | |||
| 
 | ||||
|             feed.symbols[sym] = symbol | ||||
| 
 | ||||
|             # cast shm dtype to list... can't member why we need this | ||||
|             shm_token = data['shm_token'] | ||||
|             if opened: | ||||
|                 assert data['is_shm_writer'] | ||||
|                 log.info("Started shared mem bar writer") | ||||
|             shm_token['dtype_descr'] = list(shm_token['dtype_descr']) | ||||
|             assert shm_token == shm.token  # sanity | ||||
| 
 | ||||
|         shm_token['dtype_descr'] = list(shm_token['dtype_descr']) | ||||
|         assert shm_token == shm.token  # sanity | ||||
|         feed._max_sample_rate = max(ohlc_sample_rates) | ||||
| 
 | ||||
|         yield feed | ||||
|         try: | ||||
|             yield feed | ||||
| 
 | ||||
|         finally: | ||||
|             # always cancel the far end producer task | ||||
|             with trio.CancelScope(shield=True): | ||||
|                 await stream.aclose() | ||||
|  |  | |||
|  | @ -1,115 +0,0 @@ | |||
| # piker: trading gear for hackers | ||||
| # Copyright (C) 2018-present  Tyler Goodlet (in stewardship of piker0) | ||||
| 
 | ||||
| # 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 <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| """ | ||||
| Data buffers for fast shared humpy. | ||||
| """ | ||||
| from typing import Tuple, Callable, Dict | ||||
| # import time | ||||
| 
 | ||||
| import tractor | ||||
| import trio | ||||
| 
 | ||||
| from ._sharedmem import ShmArray | ||||
| 
 | ||||
| 
 | ||||
| _shms: Dict[int, ShmArray] = {} | ||||
| _start_increment: Dict[str, trio.Event] = {} | ||||
| 
 | ||||
| 
 | ||||
| def shm_incrementing(shm_token_name: str) -> trio.Event: | ||||
|     global _start_increment | ||||
|     return _start_increment.setdefault(shm_token_name, trio.Event()) | ||||
| 
 | ||||
| 
 | ||||
| @tractor.msg.pub | ||||
| async def increment_ohlc_buffer( | ||||
|     shm_token: dict, | ||||
|     get_topics: Callable[..., Tuple[str]], | ||||
|     # delay_s: Optional[float] = None, | ||||
| ): | ||||
|     """Task which inserts new bars into the provide shared memory array | ||||
|     every ``delay_s`` seconds. | ||||
| 
 | ||||
|     This task fulfills 2 purposes: | ||||
|     - it takes the subscribed set of shm arrays and increments them | ||||
|       on a common time period | ||||
|     - broadcast of this increment "signal" message to other actor | ||||
|       subscribers | ||||
| 
 | ||||
|     Note that if **no** actor has initiated this task then **none** of | ||||
|     the underlying buffers will actually be incremented. | ||||
|     """ | ||||
| 
 | ||||
|     # wait for brokerd to signal we should start sampling | ||||
|     await shm_incrementing(shm_token['shm_name']).wait() | ||||
| 
 | ||||
|     # TODO: right now we'll spin printing bars if the last time stamp is | ||||
|     # before a large period of no market activity.  Likely the best way | ||||
|     # to solve this is to make this task aware of the instrument's | ||||
|     # tradable hours? | ||||
| 
 | ||||
|     # adjust delay to compensate for trio processing time | ||||
|     ad = min(_shms.keys()) - 0.001 | ||||
| 
 | ||||
|     total_s = 0  # total seconds counted | ||||
|     lowest = min(_shms.keys()) | ||||
|     ad = lowest - 0.001 | ||||
| 
 | ||||
|     while True: | ||||
|         # TODO: do we want to support dynamically | ||||
|         # adding a "lower" lowest increment period? | ||||
|         await trio.sleep(ad) | ||||
|         total_s += lowest | ||||
| 
 | ||||
|         # increment all subscribed shm arrays | ||||
|         # TODO: this in ``numba`` | ||||
|         for delay_s, shms in _shms.items(): | ||||
|             if total_s % delay_s != 0: | ||||
|                 continue | ||||
| 
 | ||||
|             # TODO: numa this! | ||||
|             for shm in shms: | ||||
|                 # TODO: in theory we could make this faster by copying the | ||||
|                 # "last" readable value into the underlying larger buffer's | ||||
|                 # next value and then incrementing the counter instead of | ||||
|                 # using ``.push()``? | ||||
| 
 | ||||
|                 # append new entry to buffer thus "incrementing" the bar | ||||
|                 array = shm.array | ||||
|                 last = array[-1:][shm._write_fields].copy() | ||||
|                 # (index, t, close) = last[0][['index', 'time', 'close']] | ||||
|                 (t, close) = last[0][['time', 'close']] | ||||
| 
 | ||||
|                 # this copies non-std fields (eg. vwap) from the last datum | ||||
|                 last[ | ||||
|                     ['time', 'volume', 'open', 'high', 'low', 'close'] | ||||
|                 ][0] = (t + delay_s, 0, close, close, close, close) | ||||
| 
 | ||||
|                 # write to the buffer | ||||
|                 shm.push(last) | ||||
| 
 | ||||
|         # broadcast the buffer index step | ||||
|         yield {'index': shm._last.value} | ||||
| 
 | ||||
| 
 | ||||
| def subscribe_ohlc_for_increment( | ||||
|     shm: ShmArray, | ||||
|     delay: int, | ||||
| ) -> None: | ||||
|     """Add an OHLC ``ShmArray`` to the increment set. | ||||
|     """ | ||||
|     _shms.setdefault(delay, []).append(shm) | ||||
|  | @ -0,0 +1,240 @@ | |||
| # piker: trading gear for hackers | ||||
| # Copyright (C) 2018-present  Tyler Goodlet (in stewardship of piker0) | ||||
| 
 | ||||
| # 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 <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| """ | ||||
| Data buffers for fast shared humpy. | ||||
| """ | ||||
| from typing import Dict, List | ||||
| 
 | ||||
| import tractor | ||||
| import trio | ||||
| from trio_typing import TaskStatus | ||||
| 
 | ||||
| from ._sharedmem import ShmArray | ||||
| from ..log import get_logger | ||||
| 
 | ||||
| 
 | ||||
| log = get_logger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| # TODO: we could stick these in a composed type to avoid | ||||
| # angering the "i hate module scoped variables crowd" (yawn). | ||||
| _shms: Dict[int, List[ShmArray]] = {} | ||||
| _start_increment: Dict[str, trio.Event] = {} | ||||
| _incrementers: Dict[int, trio.CancelScope] = {} | ||||
| _subscribers: Dict[str, tractor.Context] = {} | ||||
| 
 | ||||
| 
 | ||||
| def shm_incrementing(shm_token_name: str) -> trio.Event: | ||||
|     global _start_increment | ||||
|     return _start_increment.setdefault(shm_token_name, trio.Event()) | ||||
| 
 | ||||
| 
 | ||||
| async def increment_ohlc_buffer( | ||||
|     delay_s: int, | ||||
|     task_status: TaskStatus[trio.CancelScope] = trio.TASK_STATUS_IGNORED, | ||||
| ): | ||||
|     """Task which inserts new bars into the provide shared memory array | ||||
|     every ``delay_s`` seconds. | ||||
| 
 | ||||
|     This task fulfills 2 purposes: | ||||
|     - it takes the subscribed set of shm arrays and increments them | ||||
|       on a common time period | ||||
|     - broadcast of this increment "signal" message to other actor | ||||
|       subscribers | ||||
| 
 | ||||
|     Note that if **no** actor has initiated this task then **none** of | ||||
|     the underlying buffers will actually be incremented. | ||||
|     """ | ||||
| 
 | ||||
|     # # wait for brokerd to signal we should start sampling | ||||
|     # await shm_incrementing(shm_token['shm_name']).wait() | ||||
| 
 | ||||
|     # TODO: right now we'll spin printing bars if the last time stamp is | ||||
|     # before a large period of no market activity.  Likely the best way | ||||
|     # to solve this is to make this task aware of the instrument's | ||||
|     # tradable hours? | ||||
| 
 | ||||
|     global _incrementers | ||||
| 
 | ||||
|     # adjust delay to compensate for trio processing time | ||||
|     ad = min(_shms.keys()) - 0.001 | ||||
| 
 | ||||
|     total_s = 0  # total seconds counted | ||||
|     lowest = min(_shms.keys()) | ||||
|     ad = lowest - 0.001 | ||||
| 
 | ||||
|     with trio.CancelScope() as cs: | ||||
| 
 | ||||
|         # register this time period step as active | ||||
|         _incrementers[delay_s] = cs | ||||
|         task_status.started(cs) | ||||
| 
 | ||||
|         while True: | ||||
|             # TODO: do we want to support dynamically | ||||
|             # adding a "lower" lowest increment period? | ||||
|             await trio.sleep(ad) | ||||
|             total_s += lowest | ||||
| 
 | ||||
|             # increment all subscribed shm arrays | ||||
|             # TODO: this in ``numba`` | ||||
|             for delay_s, shms in _shms.items(): | ||||
|                 if total_s % delay_s != 0: | ||||
|                     continue | ||||
| 
 | ||||
|                 # TODO: ``numba`` this! | ||||
|                 for shm in shms: | ||||
|                     # TODO: in theory we could make this faster by copying the | ||||
|                     # "last" readable value into the underlying larger buffer's | ||||
|                     # next value and then incrementing the counter instead of | ||||
|                     # using ``.push()``? | ||||
| 
 | ||||
|                     # append new entry to buffer thus "incrementing" the bar | ||||
|                     array = shm.array | ||||
|                     last = array[-1:][shm._write_fields].copy() | ||||
|                     # (index, t, close) = last[0][['index', 'time', 'close']] | ||||
|                     (t, close) = last[0][['time', 'close']] | ||||
| 
 | ||||
|                     # this copies non-std fields (eg. vwap) from the last datum | ||||
|                     last[ | ||||
|                         ['time', 'volume', 'open', 'high', 'low', 'close'] | ||||
|                     ][0] = (t + delay_s, 0, close, close, close, close) | ||||
| 
 | ||||
|                     # write to the buffer | ||||
|                     shm.push(last) | ||||
| 
 | ||||
|                 # broadcast the buffer index step | ||||
|                 # yield {'index': shm._last.value} | ||||
|                 for ctx in _subscribers.get(delay_s, ()): | ||||
|                     try: | ||||
|                         await ctx.send_yield({'index': shm._last.value}) | ||||
|                     except ( | ||||
|                         trio.BrokenResourceError, | ||||
|                         trio.ClosedResourceError | ||||
|                     ): | ||||
|                         log.error(f'{ctx.chan.uid} dropped connection') | ||||
| 
 | ||||
| 
 | ||||
| @tractor.stream | ||||
| async def iter_ohlc_periods( | ||||
|     ctx: tractor.Context, | ||||
|     delay_s: int, | ||||
| ) -> None: | ||||
|     """ | ||||
|     Subscribe to OHLC sampling "step" events: when the time | ||||
|     aggregation period increments, this event stream emits an index | ||||
|     event. | ||||
| 
 | ||||
|     """ | ||||
|     # add our subscription | ||||
|     global _subscribers | ||||
|     subs = _subscribers.setdefault(delay_s, []) | ||||
|     subs.append(ctx) | ||||
| 
 | ||||
|     try: | ||||
|         # stream and block until cancelled | ||||
|         await trio.sleep_forever() | ||||
|     finally: | ||||
|         subs.remove(ctx) | ||||
| 
 | ||||
| 
 | ||||
| async def sample_and_broadcast( | ||||
|     bus: '_FeedBus',  # noqa | ||||
|     shm: ShmArray, | ||||
|     quote_stream: trio.abc.ReceiveChannel, | ||||
|     sum_tick_vlm: bool = True, | ||||
| ) -> None: | ||||
| 
 | ||||
|     log.info("Started shared mem bar writer") | ||||
| 
 | ||||
|     # iterate stream delivered by broker | ||||
|     async for quotes in quote_stream: | ||||
| 
 | ||||
|         # TODO: ``numba`` this! | ||||
|         for sym, quote in quotes.items(): | ||||
| 
 | ||||
|             # TODO: in theory you can send the IPC msg *before* | ||||
|             # writing to the sharedmem array to decrease latency, | ||||
|             # however, that will require `tractor.msg.pub` support | ||||
|             # here or at least some way to prevent task switching | ||||
|             # at the yield such that the array write isn't delayed | ||||
|             # while another consumer is serviced.. | ||||
| 
 | ||||
|             # start writing the shm buffer with appropriate | ||||
|             # trade data | ||||
|             for tick in quote['ticks']: | ||||
| 
 | ||||
|                 # if tick['type'] in ('utrade',): | ||||
|                 #     print(tick) | ||||
| 
 | ||||
|                 # write trade events to shm last OHLC sample | ||||
|                 if tick['type'] in ('trade', 'utrade'): | ||||
| 
 | ||||
|                     last = tick['price'] | ||||
| 
 | ||||
|                     # update last entry | ||||
|                     # benchmarked in the 4-5 us range | ||||
|                     o, high, low, v = shm.array[-1][ | ||||
|                         ['open', 'high', 'low', 'volume'] | ||||
|                     ] | ||||
| 
 | ||||
|                     new_v = tick.get('size', 0) | ||||
| 
 | ||||
|                     if v == 0 and new_v: | ||||
|                         # no trades for this bar yet so the open | ||||
|                         # is also the close/last trade price | ||||
|                         o = last | ||||
| 
 | ||||
|                     if sum_tick_vlm: | ||||
|                         volume = v + new_v | ||||
|                     else: | ||||
|                         # presume backend takes care of summing | ||||
|                         # it's own vlm | ||||
|                         volume = quote['volume'] | ||||
| 
 | ||||
|                     shm.array[[ | ||||
|                         'open', | ||||
|                         'high', | ||||
|                         'low', | ||||
|                         'close', | ||||
|                         'bar_wap',  # can be optionally provided | ||||
|                         'volume', | ||||
|                     ]][-1] = ( | ||||
|                         o, | ||||
|                         max(high, last), | ||||
|                         min(low, last), | ||||
|                         last, | ||||
|                         quote.get('bar_wap', 0), | ||||
|                         volume, | ||||
|                     ) | ||||
| 
 | ||||
|             # XXX: we need to be very cautious here that no | ||||
|             # context-channel is left lingering which doesn't have | ||||
|             # a far end receiver actor-task. In such a case you can | ||||
|             # end up triggering backpressure which which will | ||||
|             # eventually block this producer end of the feed and | ||||
|             # thus other consumers still attached. | ||||
|             subs = bus.subscribers[sym] | ||||
|             for ctx in subs: | ||||
|                 # print(f'sub is {ctx.chan.uid}') | ||||
|                 try: | ||||
|                     await ctx.send_yield({sym: quote}) | ||||
|                 except ( | ||||
|                     trio.BrokenResourceError, | ||||
|                     trio.ClosedResourceError | ||||
|                 ): | ||||
|                     subs.remove(ctx) | ||||
|                     log.error(f'{ctx.chan.uid} dropped connection') | ||||
|  | @ -1,5 +1,5 @@ | |||
| # piker: trading gear for hackers | ||||
| # Copyright (C) 2018-present  Tyler Goodlet (in stewardship of piker0) | ||||
| # Copyright (C) Tyler Goodlet (in stewardship for piker0) | ||||
| 
 | ||||
| # 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 | ||||
|  | @ -16,6 +16,7 @@ | |||
| 
 | ||||
| """ | ||||
| NumPy compatible shared memory buffers for real-time FSP. | ||||
| 
 | ||||
| """ | ||||
| from dataclasses import dataclass, asdict | ||||
| from sys import byteorder | ||||
|  | @ -370,7 +371,7 @@ def attach_shm_array( | |||
|     key = token.shm_name | ||||
| 
 | ||||
|     if key in _known_tokens: | ||||
|         assert _known_tokens[key] == token, "WTF" | ||||
|         assert _Token.from_msg(_known_tokens[key]) == token, "WTF" | ||||
| 
 | ||||
|     # attach to array buffer and view as per dtype | ||||
|     shm = SharedMemory(name=key) | ||||
|  | @ -426,7 +427,7 @@ def maybe_open_shm_array( | |||
|     **kwargs, | ||||
| ) -> Tuple[ShmArray, bool]: | ||||
|     """Attempt to attach to a shared memory block using a "key" lookup | ||||
|     to registered blocks in the users overall "system" registryt | ||||
|     to registered blocks in the users overall "system" registry | ||||
|     (presumes you don't have the block's explicit token). | ||||
| 
 | ||||
|     This function is meant to solve the problem of discovering whether | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ from ..log import get_logger | |||
| from .. import data | ||||
| from ._momo import _rsi, _wma | ||||
| from ._volume import _tina_vwap | ||||
| from ..data import attach_shm_array, Feed | ||||
| from ..data import attach_shm_array | ||||
| 
 | ||||
| log = get_logger(__name__) | ||||
| 
 | ||||
|  | @ -62,23 +62,6 @@ async def latency( | |||
|             yield value | ||||
| 
 | ||||
| 
 | ||||
| async def increment_signals( | ||||
|     feed: Feed, | ||||
|     dst_shm: 'SharedArray',  # noqa | ||||
| ) -> None: | ||||
|     """Increment the underlying shared memory buffer on every "increment" | ||||
|     msg received from the underlying data feed. | ||||
| 
 | ||||
|     """ | ||||
|     async for msg in await feed.index_stream(): | ||||
|         array = dst_shm.array | ||||
|         last = array[-1:].copy() | ||||
| 
 | ||||
|         # write new slot to the buffer | ||||
|         dst_shm.push(last) | ||||
|         len(dst_shm.array) | ||||
| 
 | ||||
| 
 | ||||
| @tractor.stream | ||||
| async def cascade( | ||||
|     ctx: tractor.Context, | ||||
|  | @ -150,7 +133,6 @@ async def cascade( | |||
|             ) | ||||
|             history[fsp_func_name] = history_output | ||||
| 
 | ||||
| 
 | ||||
|             # check for data length mis-allignment and fill missing values | ||||
|             diff = len(src.array) - len(history) | ||||
|             if diff >= 0: | ||||
|  | @ -182,8 +164,8 @@ async def cascade( | |||
| 
 | ||||
|             cs = await n.start(fsp_compute) | ||||
| 
 | ||||
|             # Increment the underlying shared memory buffer on every "increment" | ||||
|             # msg received from the underlying data feed. | ||||
|             # Increment the underlying shared memory buffer on every | ||||
|             # "increment" msg received from the underlying data feed. | ||||
| 
 | ||||
|             async for msg in await feed.index_stream(): | ||||
| 
 | ||||
|  | @ -198,10 +180,11 @@ async def cascade( | |||
|                     # TODO: adopt an incremental update engine/approach | ||||
|                     # where possible here eventually! | ||||
| 
 | ||||
|                 # read out last shm row | ||||
|                 array = dst.array | ||||
|                 last = array[-1:].copy() | ||||
| 
 | ||||
|                 # write new slot to the buffer | ||||
|                 # write new row to the shm buffer | ||||
|                 dst.push(last) | ||||
| 
 | ||||
|                 last_len = new_len | ||||
|  |  | |||
|  | @ -138,7 +138,7 @@ class ChartSpace(QtGui.QWidget): | |||
|         """ | ||||
|         # XXX: let's see if this causes mem problems | ||||
|         self.window.setWindowTitle( | ||||
|             f'piker chart {symbol.key}@{symbol.brokers} ' | ||||
|             f'{symbol.key}@{symbol.brokers} ' | ||||
|             f'tick:{symbol.tick_size}' | ||||
|         ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -22,6 +22,7 @@ All global Qt runtime settings are mostly defined here. | |||
| """ | ||||
| from typing import Tuple, Callable, Dict, Any | ||||
| import os | ||||
| import platform | ||||
| import signal | ||||
| import time | ||||
| import traceback | ||||
|  | @ -87,16 +88,19 @@ def current_screen() -> QtGui.QScreen: | |||
|     assert screen, "Wow Qt is dumb as shit and has no screen..." | ||||
|     return screen | ||||
| 
 | ||||
| # XXX: pretty sure none of this shit works | ||||
| 
 | ||||
| # XXX: pretty sure none of this shit works on linux as per: | ||||
| # https://bugreports.qt.io/browse/QTBUG-53022 | ||||
| # it seems to work on windows.. no idea wtf is up. | ||||
| if platform.system() == "Windows": | ||||
| 
 | ||||
| # Proper high DPI scaling is available in Qt >= 5.6.0. This attibute | ||||
| # must be set before creating the application | ||||
| # if hasattr(Qt, 'AA_EnableHighDpiScaling'): | ||||
| #     QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) | ||||
|     # Proper high DPI scaling is available in Qt >= 5.6.0. This attibute | ||||
|     # must be set before creating the application | ||||
|     if hasattr(Qt, 'AA_EnableHighDpiScaling'): | ||||
|         QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) | ||||
| 
 | ||||
| # if hasattr(Qt, 'AA_UseHighDpiPixmaps'): | ||||
| #     QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) | ||||
|     if hasattr(Qt, 'AA_UseHighDpiPixmaps'): | ||||
|         QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) | ||||
| 
 | ||||
| 
 | ||||
| class MainWindow(QtGui.QMainWindow): | ||||
|  | @ -196,7 +200,6 @@ def run_qtractor( | |||
|     async def main(): | ||||
| 
 | ||||
|         async with maybe_open_pikerd( | ||||
|             name='qtractor', | ||||
|             start_method='trio', | ||||
|             **tractor_kwargs, | ||||
|         ): | ||||
|  |  | |||
|  | @ -90,16 +90,11 @@ class LineDot(pg.CurvePoint): | |||
|         self, | ||||
|         ev: QtCore.QEvent, | ||||
|     ) -> None: | ||||
|         # print((ev, type(ev))) | ||||
|         if not isinstance( | ||||
|             ev, QtCore.QDynamicPropertyChangeEvent | ||||
|         ) or self.curve() is None: | ||||
|             return False | ||||
| 
 | ||||
|         # if ev.propertyName() == 'index': | ||||
|         #     print(ev) | ||||
|         #     # self.setProperty | ||||
| 
 | ||||
|         (x, y) = self.curve().getData() | ||||
|         index = self.property('index') | ||||
|         # first = self._plot._ohlc[0]['index'] | ||||
|  | @ -172,8 +167,6 @@ class ContentsLabel(pg.LabelItem): | |||
|         if inspect.isfunction(margins[1]): | ||||
|             margins = margins[0], ydim(anchor_font_size) | ||||
| 
 | ||||
|         print(f'margins: {margins}') | ||||
| 
 | ||||
|         self.anchor(itemPos=index, parentPos=index, offset=margins) | ||||
| 
 | ||||
|     def update_from_ohlc( | ||||
|  | @ -403,7 +396,6 @@ class Cursor(pg.GraphicsObject): | |||
| 
 | ||||
|             # update all trackers | ||||
|             for item in self._trackers: | ||||
|                 # print(f'setting {item} with {(ix, y)}') | ||||
|                 item.on_tracked_source(ix, iy) | ||||
| 
 | ||||
|         if ix != last_ix: | ||||
|  |  | |||
|  | @ -155,6 +155,8 @@ class LevelLabel(YAxisLabel): | |||
|             self._h_shift * (w + self._x_offset), | ||||
|             abs_pos.y() + self._v_shift * h | ||||
|         )) | ||||
|         # XXX: definitely need this! | ||||
|         self.update() | ||||
| 
 | ||||
|     def set_fmt_str( | ||||
|         self, | ||||
|  |  | |||
|  | @ -148,6 +148,7 @@ def chart(config, symbol, profile): | |||
|         tractor_kwargs={ | ||||
|             'debug_mode': True, | ||||
|             'loglevel': tractorloglevel, | ||||
|             'name': 'chart', | ||||
|             'enable_modules': [ | ||||
|                 'piker.clearing._client' | ||||
|             ], | ||||
|  |  | |||
|  | @ -321,97 +321,95 @@ async def start_order_mode( | |||
|     async with open_ems( | ||||
|         brokername, | ||||
|         symbol, | ||||
|     ) as (book, trades_stream): | ||||
|     ) as (book, trades_stream), open_order_mode( | ||||
|         symbol, | ||||
|         chart, | ||||
|         book, | ||||
|     ) as order_mode: | ||||
| 
 | ||||
|         async with open_order_mode( | ||||
|             symbol, | ||||
|             chart, | ||||
|             book, | ||||
|         ) as order_mode: | ||||
|         def get_index(time: float): | ||||
| 
 | ||||
|             def get_index(time: float): | ||||
|             # XXX: not sure why the time is so off here | ||||
|             # looks like we're gonna have to do some fixing.. | ||||
| 
 | ||||
|                 # XXX: not sure why the time is so off here | ||||
|                 # looks like we're gonna have to do some fixing.. | ||||
|             ohlc = chart._shm.array | ||||
|             indexes = ohlc['time'] >= time | ||||
| 
 | ||||
|                 ohlc = chart._shm.array | ||||
|                 indexes = ohlc['time'] >= time | ||||
|             if any(indexes): | ||||
|                 return ohlc['index'][indexes[-1]] | ||||
|             else: | ||||
|                 return ohlc['index'][-1] | ||||
| 
 | ||||
|                 if any(indexes): | ||||
|                     return ohlc['index'][indexes[-1]] | ||||
|                 else: | ||||
|                     return ohlc['index'][-1] | ||||
|         # Begin order-response streaming | ||||
| 
 | ||||
|             # Begin order-response streaming | ||||
|         # this is where we receive **back** messages | ||||
|         # about executions **from** the EMS actor | ||||
|         async for msg in trades_stream: | ||||
| 
 | ||||
|             # this is where we receive **back** messages | ||||
|             # about executions **from** the EMS actor | ||||
|             async for msg in trades_stream: | ||||
|             fmsg = pformat(msg) | ||||
|             log.info(f'Received order msg:\n{fmsg}') | ||||
| 
 | ||||
|                 fmsg = pformat(msg) | ||||
|                 log.info(f'Received order msg:\n{fmsg}') | ||||
|             resp = msg['resp'] | ||||
| 
 | ||||
|                 resp = msg['resp'] | ||||
|             if resp in ( | ||||
|                 'position', | ||||
|             ): | ||||
|                 # show line label once order is live | ||||
|                 order_mode.on_position_update(msg) | ||||
|                 continue | ||||
| 
 | ||||
|                 if resp in ( | ||||
|                     'position', | ||||
|                 ): | ||||
|                     # show line label once order is live | ||||
|                     order_mode.on_position_update(msg) | ||||
|                     continue | ||||
|             # delete the line from view | ||||
|             oid = msg['oid'] | ||||
| 
 | ||||
|                 # delete the line from view | ||||
|                 oid = msg['oid'] | ||||
|             # response to 'action' request (buy/sell) | ||||
|             if resp in ( | ||||
|                 'dark_submitted', | ||||
|                 'broker_submitted' | ||||
|             ): | ||||
| 
 | ||||
|                 # response to 'action' request (buy/sell) | ||||
|                 if resp in ( | ||||
|                     'dark_submitted', | ||||
|                     'broker_submitted' | ||||
|                 ): | ||||
|                 # show line label once order is live | ||||
|                 order_mode.on_submit(oid) | ||||
| 
 | ||||
|                     # show line label once order is live | ||||
|                     order_mode.on_submit(oid) | ||||
|             # resp to 'cancel' request or error condition | ||||
|             # for action request | ||||
|             elif resp in ( | ||||
|                 'broker_cancelled', | ||||
|                 'broker_inactive', | ||||
|                 'dark_cancelled' | ||||
|             ): | ||||
|                 # delete level line from view | ||||
|                 order_mode.on_cancel(oid) | ||||
| 
 | ||||
|                 # resp to 'cancel' request or error condition | ||||
|                 # for action request | ||||
|                 elif resp in ( | ||||
|                     'broker_cancelled', | ||||
|                     'broker_inactive', | ||||
|                     'dark_cancelled' | ||||
|                 ): | ||||
|                     # delete level line from view | ||||
|                     order_mode.on_cancel(oid) | ||||
|             elif resp in ( | ||||
|                 'dark_executed' | ||||
|             ): | ||||
|                 log.info(f'Dark order triggered for {fmsg}') | ||||
| 
 | ||||
|                 elif resp in ( | ||||
|                     'dark_executed' | ||||
|                 ): | ||||
|                     log.info(f'Dark order triggered for {fmsg}') | ||||
|                 # for alerts add a triangle and remove the | ||||
|                 # level line | ||||
|                 if msg['cmd']['action'] == 'alert': | ||||
| 
 | ||||
|                     # for alerts add a triangle and remove the | ||||
|                     # level line | ||||
|                     if msg['cmd']['action'] == 'alert': | ||||
| 
 | ||||
|                         # should only be one "fill" for an alert | ||||
|                         order_mode.on_fill( | ||||
|                             oid, | ||||
|                             price=msg['trigger_price'], | ||||
|                             arrow_index=get_index(time.time()) | ||||
|                         ) | ||||
|                         await order_mode.on_exec(oid, msg) | ||||
| 
 | ||||
|                 # response to completed 'action' request for buy/sell | ||||
|                 elif resp in ( | ||||
|                     'broker_executed', | ||||
|                 ): | ||||
|                     await order_mode.on_exec(oid, msg) | ||||
| 
 | ||||
|                 # each clearing tick is responded individually | ||||
|                 elif resp in ('broker_filled',): | ||||
|                     action = msg['action'] | ||||
|                     # TODO: some kinda progress system | ||||
|                     # should only be one "fill" for an alert | ||||
|                     order_mode.on_fill( | ||||
|                         oid, | ||||
|                         price=msg['price'], | ||||
|                         arrow_index=get_index(msg['broker_time']), | ||||
|                         pointing='up' if action == 'buy' else 'down', | ||||
|                         price=msg['trigger_price'], | ||||
|                         arrow_index=get_index(time.time()) | ||||
|                     ) | ||||
|                     await order_mode.on_exec(oid, msg) | ||||
| 
 | ||||
|             # response to completed 'action' request for buy/sell | ||||
|             elif resp in ( | ||||
|                 'broker_executed', | ||||
|             ): | ||||
|                 await order_mode.on_exec(oid, msg) | ||||
| 
 | ||||
|             # each clearing tick is responded individually | ||||
|             elif resp in ('broker_filled',): | ||||
|                 action = msg['action'] | ||||
|                 # TODO: some kinda progress system | ||||
|                 order_mode.on_fill( | ||||
|                     oid, | ||||
|                     price=msg['price'], | ||||
|                     arrow_index=get_index(msg['broker_time']), | ||||
|                     pointing='up' if action == 'buy' else 'down', | ||||
|                 ) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue