2020-05-26 18:34:22 +00:00
|
|
|
"""
|
2020-07-15 12:22:09 +00:00
|
|
|
Data feed apis and infra.
|
2020-05-26 18:34:22 +00:00
|
|
|
|
2020-08-02 00:08:05 +00:00
|
|
|
We provide tsdb integrations for retrieving
|
2020-07-15 12:22:09 +00:00
|
|
|
and storing data from your brokers as well as
|
|
|
|
sharing your feeds with other fellow pikers.
|
2020-05-26 18:34:22 +00:00
|
|
|
"""
|
2020-09-22 16:14:24 +00:00
|
|
|
from dataclasses import dataclass
|
2020-07-15 12:22:09 +00:00
|
|
|
from contextlib import asynccontextmanager
|
2020-07-28 17:47:18 +00:00
|
|
|
from importlib import import_module
|
|
|
|
from types import ModuleType
|
2020-07-15 12:22:09 +00:00
|
|
|
from typing import (
|
|
|
|
Dict, List, Any,
|
|
|
|
Sequence, AsyncIterator, Optional
|
|
|
|
)
|
|
|
|
|
|
|
|
import tractor
|
|
|
|
|
|
|
|
from ..brokers import get_brokermod
|
2020-07-28 17:47:18 +00:00
|
|
|
from ..log import get_logger, get_console_log
|
2020-09-17 13:12:05 +00:00
|
|
|
from ._normalize import iterticks
|
|
|
|
from ._sharedmem import (
|
2020-09-22 16:14:24 +00:00
|
|
|
maybe_open_shm_array,
|
|
|
|
attach_shm_array,
|
|
|
|
open_shm_array,
|
2020-09-25 19:16:58 +00:00
|
|
|
ShmArray,
|
2020-09-22 16:14:24 +00:00
|
|
|
get_shm_token,
|
2020-09-17 13:12:05 +00:00
|
|
|
)
|
2020-09-26 18:11:55 +00:00
|
|
|
from ._source import base_ohlc_dtype
|
2020-09-25 19:16:58 +00:00
|
|
|
from ._buffer import (
|
|
|
|
increment_ohlc_buffer,
|
|
|
|
subscribe_ohlc_for_increment
|
|
|
|
)
|
2020-09-17 13:12:05 +00:00
|
|
|
|
|
|
|
|
|
|
|
__all__ = [
|
2020-09-22 16:14:24 +00:00
|
|
|
'iterticks',
|
|
|
|
'maybe_open_shm_array',
|
|
|
|
'attach_shm_array',
|
|
|
|
'open_shm_array',
|
|
|
|
'get_shm_token',
|
2020-09-25 19:16:58 +00:00
|
|
|
'subscribe_ohlc_for_increment',
|
2020-09-17 13:12:05 +00:00
|
|
|
]
|
2020-07-15 12:22:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
log = get_logger(__name__)
|
|
|
|
|
2020-07-28 17:47:18 +00:00
|
|
|
__ingestors__ = [
|
|
|
|
'marketstore',
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2020-09-17 13:12:05 +00:00
|
|
|
def get_ingestormod(name: str) -> ModuleType:
|
2020-07-28 17:47:18 +00:00
|
|
|
"""Return the imported ingestor module by name.
|
|
|
|
"""
|
|
|
|
module = import_module('.' + name, 'piker.data')
|
|
|
|
# we only allow monkeying because it's for internal keying
|
|
|
|
module.name = module.__name__.split('.')[-1]
|
|
|
|
return module
|
|
|
|
|
2020-07-15 12:22:09 +00:00
|
|
|
|
|
|
|
_data_mods = [
|
|
|
|
'piker.brokers.core',
|
|
|
|
'piker.brokers.data',
|
2020-09-17 13:12:05 +00:00
|
|
|
'piker.data',
|
2020-07-15 12:22:09 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
async def maybe_spawn_brokerd(
|
|
|
|
brokername: str,
|
|
|
|
sleep: float = 0.5,
|
|
|
|
loglevel: Optional[str] = None,
|
|
|
|
expose_mods: List = [],
|
|
|
|
**tractor_kwargs,
|
|
|
|
) -> tractor._portal.Portal:
|
|
|
|
"""If no ``brokerd.{brokername}`` daemon-actor can be found,
|
|
|
|
spawn one in a local subactor and return a portal to it.
|
|
|
|
"""
|
2020-07-28 17:47:18 +00:00
|
|
|
if loglevel:
|
|
|
|
get_console_log(loglevel)
|
|
|
|
|
|
|
|
tractor_kwargs['loglevel'] = loglevel
|
|
|
|
|
2020-07-15 12:22:09 +00:00
|
|
|
brokermod = get_brokermod(brokername)
|
|
|
|
dname = f'brokerd.{brokername}'
|
|
|
|
async with tractor.find_actor(dname) as portal:
|
|
|
|
# WTF: why doesn't this work?
|
|
|
|
if portal is not None:
|
|
|
|
yield portal
|
|
|
|
else:
|
|
|
|
log.info(f"Spawning {brokername} broker daemon")
|
|
|
|
tractor_kwargs = getattr(brokermod, '_spawn_kwargs', {})
|
|
|
|
async with tractor.open_nursery() as nursery:
|
|
|
|
try:
|
|
|
|
# spawn new daemon
|
|
|
|
portal = await nursery.start_actor(
|
|
|
|
dname,
|
|
|
|
rpc_module_paths=_data_mods + [brokermod.__name__],
|
|
|
|
loglevel=loglevel,
|
|
|
|
**tractor_kwargs
|
|
|
|
)
|
|
|
|
async with tractor.wait_for_actor(dname) as portal:
|
|
|
|
yield portal
|
|
|
|
finally:
|
|
|
|
# client code may block indefinitely so cancel when
|
|
|
|
# teardown is invoked
|
|
|
|
await nursery.cancel()
|
|
|
|
|
|
|
|
|
2020-09-22 16:14:24 +00:00
|
|
|
@dataclass
|
|
|
|
class Feed:
|
|
|
|
"""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
|
|
|
|
interacting with IPC streams and conducting automatic
|
|
|
|
memory buffer orchestration.
|
|
|
|
"""
|
|
|
|
name: str
|
|
|
|
stream: AsyncIterator[Dict[str, Any]]
|
2020-09-25 19:16:58 +00:00
|
|
|
shm: ShmArray
|
2020-09-22 16:14:24 +00:00
|
|
|
_broker_portal: tractor._portal.Portal
|
|
|
|
_index_stream: Optional[AsyncIterator[Dict[str, Any]]] = None
|
|
|
|
|
|
|
|
async def receive(self) -> dict:
|
|
|
|
return await self.stream.__anext__()
|
|
|
|
|
|
|
|
async def index_stream(self) -> 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._broker_portal.run(
|
|
|
|
'piker.data',
|
2020-09-25 19:16:58 +00:00
|
|
|
'increment_ohlc_buffer',
|
2020-09-22 16:14:24 +00:00
|
|
|
shm_token=self.shm.token,
|
|
|
|
topics=['index'],
|
|
|
|
)
|
|
|
|
|
|
|
|
return self._index_stream
|
|
|
|
|
|
|
|
|
|
|
|
def sym_to_shm_key(
|
|
|
|
broker: str,
|
|
|
|
symbol: str,
|
|
|
|
) -> str:
|
|
|
|
return f'{broker}.{symbol}'
|
|
|
|
|
|
|
|
|
2020-07-15 12:22:09 +00:00
|
|
|
@asynccontextmanager
|
|
|
|
async def open_feed(
|
|
|
|
name: str,
|
|
|
|
symbols: Sequence[str],
|
2020-08-19 11:42:49 +00:00
|
|
|
loglevel: Optional[str] = None,
|
2020-07-15 12:22:09 +00:00
|
|
|
) -> AsyncIterator[Dict[str, Any]]:
|
2020-08-02 00:08:05 +00:00
|
|
|
"""Open a "data feed" which provides streamed real-time quotes.
|
|
|
|
"""
|
2020-07-15 12:22:09 +00:00
|
|
|
try:
|
|
|
|
mod = get_brokermod(name)
|
|
|
|
except ImportError:
|
2020-07-28 17:47:18 +00:00
|
|
|
mod = get_ingestormod(name)
|
2020-07-15 12:22:09 +00:00
|
|
|
|
2020-08-19 11:42:49 +00:00
|
|
|
if loglevel is None:
|
|
|
|
loglevel = tractor.current_actor().loglevel
|
|
|
|
|
2020-09-26 18:11:55 +00:00
|
|
|
# Attempt to allocate (or attach to) shm array for this broker/symbol
|
2020-09-22 16:14:24 +00:00
|
|
|
shm, opened = maybe_open_shm_array(
|
|
|
|
key=sym_to_shm_key(name, symbols[0]),
|
2020-09-26 18:11:55 +00:00
|
|
|
# use any broker defined ohlc dtype:
|
|
|
|
dtype=getattr(mod, '_ohlc_dtype', base_ohlc_dtype),
|
2020-09-22 16:14:24 +00:00
|
|
|
|
|
|
|
# we expect the sub-actor to write
|
|
|
|
readonly=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
async with maybe_spawn_brokerd(
|
|
|
|
mod.name,
|
|
|
|
loglevel=loglevel,
|
|
|
|
) as portal:
|
|
|
|
stream = await portal.run(
|
|
|
|
mod.__name__,
|
|
|
|
'stream_quotes',
|
|
|
|
symbols=symbols,
|
|
|
|
shm_token=shm.token,
|
|
|
|
|
|
|
|
# compat with eventual ``tractor.msg.pub``
|
|
|
|
topics=symbols,
|
|
|
|
)
|
2020-09-26 18:11:55 +00:00
|
|
|
|
|
|
|
# TODO: we can't do this **and** be compate with
|
|
|
|
# ``tractor.msg.pub``, should we maybe just drop this after
|
|
|
|
# tests are in?
|
2020-09-22 16:14:24 +00:00
|
|
|
shm_token, is_writer = await stream.receive()
|
2020-09-26 18:11:55 +00:00
|
|
|
|
2020-09-22 16:14:24 +00:00
|
|
|
shm_token['dtype_descr'] = list(shm_token['dtype_descr'])
|
|
|
|
assert shm_token == shm.token # sanity
|
|
|
|
|
|
|
|
if is_writer:
|
|
|
|
log.info("Started shared mem bar writer")
|
|
|
|
|
|
|
|
yield Feed(
|
|
|
|
name=name,
|
|
|
|
stream=stream,
|
|
|
|
shm=shm,
|
|
|
|
_broker_portal=portal,
|
|
|
|
)
|