2020-11-06 17:23:14 +00:00
|
|
|
# piker: trading gear for hackers
|
2021-01-19 00:55:50 +00:00
|
|
|
# Copyright (C) Tyler Goodlet (in stewardship for piker0)
|
2020-11-06 17:23:14 +00:00
|
|
|
|
|
|
|
# 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/>.
|
|
|
|
|
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
|
|
|
"""
|
2021-01-23 03:56:22 +00:00
|
|
|
from dataclasses import dataclass, field
|
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 (
|
2021-01-19 00:55:50 +00:00
|
|
|
Dict, Any, Sequence, AsyncIterator, Optional
|
2020-07-15 12:22:09 +00:00
|
|
|
)
|
|
|
|
|
2021-01-19 00:55:50 +00:00
|
|
|
import trio
|
2020-07-15 12:22:09 +00:00
|
|
|
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
|
|
|
)
|
2021-01-23 03:56:22 +00:00
|
|
|
from ._source import base_iohlc_dtype, Symbol
|
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
|
|
|
|
2021-01-04 19:45:34 +00:00
|
|
|
# capable rpc modules
|
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',
|
2021-01-04 19:45:34 +00:00
|
|
|
'piker.data._buffer',
|
2020-07-15 12:22:09 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
async def maybe_spawn_brokerd(
|
|
|
|
brokername: str,
|
|
|
|
sleep: float = 0.5,
|
|
|
|
loglevel: Optional[str] = None,
|
|
|
|
**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)
|
|
|
|
|
2020-11-06 17:23:14 +00:00
|
|
|
# disable debugger in brokerd?
|
|
|
|
# tractor._state._runtime_vars['_debug_mode'] = False
|
|
|
|
|
2020-07-28 17:47:18 +00:00
|
|
|
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:
|
2021-01-04 19:45:34 +00:00
|
|
|
|
2020-07-15 12:22:09 +00:00
|
|
|
# WTF: why doesn't this work?
|
|
|
|
if portal is not None:
|
|
|
|
yield portal
|
2021-01-04 19:45:34 +00:00
|
|
|
|
|
|
|
else: # no daemon has been spawned yet
|
|
|
|
|
2020-07-15 12:22:09 +00:00
|
|
|
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,
|
2021-01-04 19:45:34 +00:00
|
|
|
enable_modules=_data_mods + [brokermod.__name__],
|
2020-07-15 12:22:09 +00:00
|
|
|
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
|
2021-01-09 15:54:45 +00:00
|
|
|
mod: ModuleType
|
2020-12-12 22:14:03 +00:00
|
|
|
# ticks: ShmArray
|
2021-01-04 19:45:34 +00:00
|
|
|
_brokerd_portal: tractor._portal.Portal
|
2021-01-09 15:54:45 +00:00
|
|
|
_index_stream: Optional[AsyncIterator[int]] = None
|
|
|
|
_trade_stream: Optional[AsyncIterator[Dict[str, Any]]] = None
|
2020-09-22 16:14:24 +00:00
|
|
|
|
2021-01-23 03:56:22 +00:00
|
|
|
# cache of symbol info messages received as first message when
|
|
|
|
# a stream startsc.
|
|
|
|
symbols: Dict[str, Symbol] = field(default_factory=dict)
|
|
|
|
|
2020-09-22 16:14:24 +00:00
|
|
|
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
|
2021-01-04 19:45:34 +00:00
|
|
|
self._index_stream = await self._brokerd_portal.run(
|
|
|
|
increment_ohlc_buffer,
|
2020-09-22 16:14:24 +00:00
|
|
|
shm_token=self.shm.token,
|
|
|
|
topics=['index'],
|
|
|
|
)
|
|
|
|
|
|
|
|
return self._index_stream
|
|
|
|
|
2021-01-09 15:54:45 +00:00
|
|
|
async def recv_trades_data(self) -> AsyncIterator[dict]:
|
|
|
|
|
|
|
|
if not getattr(self.mod, 'stream_trades', False):
|
2021-01-19 00:55:50 +00:00
|
|
|
log.warning(
|
|
|
|
f"{self.mod.name} doesn't have trade data support yet :(")
|
2021-01-09 15:54:45 +00:00
|
|
|
|
2021-01-19 00:55:50 +00:00
|
|
|
if not self._trade_stream:
|
|
|
|
raise RuntimeError(
|
|
|
|
f'Can not stream trade data from {self.mod.name}')
|
2021-01-09 15:54:45 +00:00
|
|
|
|
2021-01-19 00:55:50 +00:00
|
|
|
# NOTE: this can be faked by setting a rx chan
|
|
|
|
# using the ``_.set_fake_trades_stream()`` method
|
2021-02-21 16:42:19 +00:00
|
|
|
if self._trade_stream is None:
|
2021-01-19 00:55:50 +00:00
|
|
|
|
2021-01-09 15:54:45 +00:00
|
|
|
self._trade_stream = await self._brokerd_portal.run(
|
2021-01-14 17:59:00 +00:00
|
|
|
|
2021-01-09 15:54:45 +00:00
|
|
|
self.mod.stream_trades,
|
2021-01-14 17:59:00 +00:00
|
|
|
|
|
|
|
# do we need this? -> yes
|
|
|
|
# the broker side must declare this key
|
|
|
|
# in messages, though we could probably use
|
|
|
|
# more then one?
|
2021-01-16 00:41:03 +00:00
|
|
|
topics=['local_trades'],
|
2021-01-09 15:54:45 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
return self._trade_stream
|
|
|
|
|
2020-09-22 16:14:24 +00:00
|
|
|
|
|
|
|
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(
|
2021-01-23 03:56:22 +00:00
|
|
|
brokername: str,
|
2020-07-15 12:22:09 +00:00
|
|
|
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:
|
2021-01-23 03:56:22 +00:00
|
|
|
mod = get_brokermod(brokername)
|
2020-07-15 12:22:09 +00:00
|
|
|
except ImportError:
|
2021-01-23 03:56:22 +00:00
|
|
|
mod = get_ingestormod(brokername)
|
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
|
|
|
|
|
2021-01-23 03:56:22 +00:00
|
|
|
# TODO: do all!
|
|
|
|
sym = symbols[0]
|
|
|
|
|
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(
|
2021-01-23 03:56:22 +00:00
|
|
|
key=sym_to_shm_key(brokername, sym),
|
2020-10-15 19:02:42 +00:00
|
|
|
|
2020-09-26 18:11:55 +00:00
|
|
|
# use any broker defined ohlc dtype:
|
2020-12-12 22:14:03 +00:00
|
|
|
dtype=getattr(mod, '_ohlc_dtype', base_iohlc_dtype),
|
2020-09-22 16:14:24 +00:00
|
|
|
|
|
|
|
# we expect the sub-actor to write
|
|
|
|
readonly=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
async with maybe_spawn_brokerd(
|
2021-01-23 03:56:22 +00:00
|
|
|
brokername,
|
2020-09-22 16:14:24 +00:00
|
|
|
loglevel=loglevel,
|
|
|
|
) as portal:
|
|
|
|
stream = await portal.run(
|
2021-01-04 19:45:34 +00:00
|
|
|
mod.stream_quotes,
|
2021-01-23 03:56:22 +00:00
|
|
|
|
|
|
|
# TODO: actually handy multiple symbols...
|
2020-09-22 16:14:24 +00:00
|
|
|
symbols=symbols,
|
2021-01-23 03:56:22 +00:00
|
|
|
|
2020-09-22 16:14:24 +00:00
|
|
|
shm_token=shm.token,
|
|
|
|
|
|
|
|
# compat with eventual ``tractor.msg.pub``
|
|
|
|
topics=symbols,
|
2021-02-21 16:42:19 +00:00
|
|
|
loglevel=loglevel,
|
2020-09-22 16:14:24 +00:00
|
|
|
)
|
2020-09-26 18:11:55 +00:00
|
|
|
|
2021-01-23 03:56:22 +00:00
|
|
|
feed = Feed(
|
|
|
|
name=brokername,
|
|
|
|
stream=stream,
|
|
|
|
shm=shm,
|
|
|
|
mod=mod,
|
|
|
|
_brokerd_portal=portal,
|
|
|
|
)
|
|
|
|
|
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?
|
2021-01-23 03:56:22 +00:00
|
|
|
init_msg = await stream.receive()
|
|
|
|
|
|
|
|
for sym, data in init_msg.items():
|
|
|
|
|
|
|
|
si = data['symbol_info']
|
2021-02-06 19:38:00 +00:00
|
|
|
|
2021-01-23 03:56:22 +00:00
|
|
|
symbol = Symbol(
|
2021-02-06 19:38:00 +00:00
|
|
|
key=sym,
|
2021-02-21 16:42:19 +00:00
|
|
|
type_key=si.get('asset_type', 'forex'),
|
2021-02-06 19:38:00 +00:00
|
|
|
tick_size=si.get('price_tick_size', 0.01),
|
2021-02-08 11:42:59 +00:00
|
|
|
lot_tick_size=si.get('lot_tick_size', 0.0),
|
2021-01-23 03:56:22 +00:00
|
|
|
)
|
|
|
|
symbol.broker_info[brokername] = si
|
|
|
|
|
|
|
|
feed.symbols[sym] = symbol
|
2020-09-26 18:11:55 +00:00
|
|
|
|
2021-01-23 03:56:22 +00:00
|
|
|
shm_token = data['shm_token']
|
|
|
|
if opened:
|
|
|
|
assert data['is_shm_writer']
|
|
|
|
log.info("Started shared mem bar writer")
|
2020-10-15 19:02:42 +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
|
|
|
|
|
2021-01-23 03:56:22 +00:00
|
|
|
yield feed
|