Support async-batched ohlc queries in all backends
Expect each backend to deliver a `config: dict[str, Any]` which provides concurrency controls to `trimeter`'s batch task scheduler such that backends can define their own concurrency limits. The dirty deats in this patch include handling history "gaps" where a query returns a history-frame-result which spans more then the typical frame size (in seconds). In such cases we reset the target frame index (datetime index sequence implemented with a `pendulum.Period`) using a generator protocol `.send()` such that the sequence can be dynamically re-indexed starting at the new (possibly) pre-gap datetime. The new gap logic also allows us to detect out of order frames easier and thus wait for the next-in-order to arrive before making more requests.incr_update_backup
							parent
							
								
									303a5cc66c
								
							
						
					
					
						commit
						1967bc7973
					
				| 
						 | 
					@ -402,7 +402,7 @@ async def open_history_client(
 | 
				
			||||||
            end_dt = pendulum.from_timestamp(array[-1]['time'])
 | 
					            end_dt = pendulum.from_timestamp(array[-1]['time'])
 | 
				
			||||||
            return array, start_dt, end_dt
 | 
					            return array, start_dt, end_dt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        yield get_ohlc
 | 
					        yield get_ohlc, {'erlangs': 4, 'rate': 4}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async def backfill_bars(
 | 
					async def backfill_bars(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,6 +57,8 @@ from ib_insync.wrapper import Wrapper
 | 
				
			||||||
from ib_insync.client import Client as ib_Client
 | 
					from ib_insync.client import Client as ib_Client
 | 
				
			||||||
from fuzzywuzzy import process as fuzzy
 | 
					from fuzzywuzzy import process as fuzzy
 | 
				
			||||||
import numpy as np
 | 
					import numpy as np
 | 
				
			||||||
 | 
					import pendulum
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .. import config
 | 
					from .. import config
 | 
				
			||||||
from ..log import get_logger, get_console_log
 | 
					from ..log import get_logger, get_console_log
 | 
				
			||||||
| 
						 | 
					@ -1442,8 +1444,6 @@ async def get_bars(
 | 
				
			||||||
    a ``MethoProxy``.
 | 
					    a ``MethoProxy``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    import pendulum
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fails = 0
 | 
					    fails = 0
 | 
				
			||||||
    bars: Optional[list] = None
 | 
					    bars: Optional[list] = None
 | 
				
			||||||
    first_dt: datetime = None
 | 
					    first_dt: datetime = None
 | 
				
			||||||
| 
						 | 
					@ -1471,7 +1471,9 @@ async def get_bars(
 | 
				
			||||||
            time = bars_array['time']
 | 
					            time = bars_array['time']
 | 
				
			||||||
            assert time[-1] == last_dt.timestamp()
 | 
					            assert time[-1] == last_dt.timestamp()
 | 
				
			||||||
            assert time[0] == first_dt.timestamp()
 | 
					            assert time[0] == first_dt.timestamp()
 | 
				
			||||||
            log.info(f'bars retreived for dts {first_dt}:{last_dt}')
 | 
					            log.info(
 | 
				
			||||||
 | 
					                f'{len(bars)} bars retreived for {first_dt} -> {last_dt}'
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return (bars, bars_array, first_dt, last_dt), fails
 | 
					            return (bars, bars_array, first_dt, last_dt), fails
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1485,20 +1487,27 @@ async def get_bars(
 | 
				
			||||||
                raise NoData(
 | 
					                raise NoData(
 | 
				
			||||||
                    f'Symbol: {fqsn}',
 | 
					                    f'Symbol: {fqsn}',
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                break
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            elif (
 | 
					            elif (
 | 
				
			||||||
                err.code == 162
 | 
					                err.code == 162
 | 
				
			||||||
                and 'HMDS query returned no data' in err.message
 | 
					                and 'HMDS query returned no data' in err.message
 | 
				
			||||||
            ):
 | 
					            ):
 | 
				
			||||||
                # try to decrement start point and look further back
 | 
					                # XXX: this is now done in the storage mgmt layer
 | 
				
			||||||
                end_dt = last_dt = last_dt.subtract(seconds=2000)
 | 
					                # and we shouldn't implicitly decrement the frame dt
 | 
				
			||||||
 | 
					                # index since the upper layer may be doing so
 | 
				
			||||||
 | 
					                # concurrently and we don't want to be delivering frames
 | 
				
			||||||
 | 
					                # that weren't asked for.
 | 
				
			||||||
                log.warning(
 | 
					                log.warning(
 | 
				
			||||||
                    f'No data found ending @ {end_dt}\n'
 | 
					                    f'NO DATA found ending @ {end_dt}\n'
 | 
				
			||||||
                    f'Starting another request for {end_dt}'
 | 
					 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                continue
 | 
					                # try to decrement start point and look further back
 | 
				
			||||||
 | 
					                # end_dt = last_dt = last_dt.subtract(seconds=2000)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                raise NoData(
 | 
				
			||||||
 | 
					                    f'Symbol: {fqsn}',
 | 
				
			||||||
 | 
					                    frame_size=2000,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            elif _pacing in msg:
 | 
					            elif _pacing in msg:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1578,7 +1587,12 @@ async def open_history_client(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return bars_array, first_dt, last_dt
 | 
					            return bars_array, first_dt, last_dt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        yield get_hist
 | 
					        # TODO: it seems like we can do async queries for ohlc
 | 
				
			||||||
 | 
					        # but getting the order right still isn't working and I'm not
 | 
				
			||||||
 | 
					        # quite sure why.. needs some tinkering and probably
 | 
				
			||||||
 | 
					        # a lookthrough of the ``ib_insync`` machinery, for eg. maybe
 | 
				
			||||||
 | 
					        # we have to do the batch queries on the `asyncio` side?
 | 
				
			||||||
 | 
					        yield get_hist, {'erlangs': 1, 'rate': 6}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async def backfill_bars(
 | 
					async def backfill_bars(
 | 
				
			||||||
| 
						 | 
					@ -1840,6 +1854,7 @@ async def stream_quotes(
 | 
				
			||||||
        symbol=sym,
 | 
					        symbol=sym,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    first_quote = normalize(first_ticker)
 | 
					    first_quote = normalize(first_ticker)
 | 
				
			||||||
 | 
					    # print(f'first quote: {first_quote}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def mk_init_msgs() -> dict[str, dict]:
 | 
					    def mk_init_msgs() -> dict[str, dict]:
 | 
				
			||||||
        # pass back some symbol info like min_tick, trading_hours, etc.
 | 
					        # pass back some symbol info like min_tick, trading_hours, etc.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1066,7 +1066,7 @@ async def open_history_client(
 | 
				
			||||||
            end_dt = pendulum.from_timestamp(array[-1]['time'])
 | 
					            end_dt = pendulum.from_timestamp(array[-1]['time'])
 | 
				
			||||||
            return array, start_dt, end_dt
 | 
					            return array, start_dt, end_dt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        yield get_ohlc
 | 
					        yield get_ohlc, {'erlangs': 1, 'rate': 1}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async def backfill_bars(
 | 
					async def backfill_bars(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,6 +29,7 @@ from types import ModuleType
 | 
				
			||||||
from typing import (
 | 
					from typing import (
 | 
				
			||||||
    Any,
 | 
					    Any,
 | 
				
			||||||
    AsyncIterator, Optional,
 | 
					    AsyncIterator, Optional,
 | 
				
			||||||
 | 
					    Generator,
 | 
				
			||||||
    Awaitable,
 | 
					    Awaitable,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -241,7 +242,7 @@ async def start_backfill(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
) -> int:
 | 
					) -> int:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async with mod.open_history_client(bfqsn) as hist:
 | 
					    async with mod.open_history_client(bfqsn) as (hist, config):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # get latest query's worth of history all the way
 | 
					        # get latest query's worth of history all the way
 | 
				
			||||||
        # back to what is recorded in the tsdb
 | 
					        # back to what is recorded in the tsdb
 | 
				
			||||||
| 
						 | 
					@ -260,7 +261,9 @@ async def start_backfill(
 | 
				
			||||||
        for delay_s in sampler.subscribers:
 | 
					        for delay_s in sampler.subscribers:
 | 
				
			||||||
            await broadcast(delay_s)
 | 
					            await broadcast(delay_s)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # signal that backfilling to tsdb's end datum is complete
 | 
				
			||||||
        bf_done = trio.Event()
 | 
					        bf_done = trio.Event()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # let caller unblock and deliver latest history frame
 | 
					        # let caller unblock and deliver latest history frame
 | 
				
			||||||
        task_status.started((shm, start_dt, end_dt, bf_done))
 | 
					        task_status.started((shm, start_dt, end_dt, bf_done))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -269,7 +272,7 @@ async def start_backfill(
 | 
				
			||||||
            pendulum.from_timestamp(times[-1]) -
 | 
					            pendulum.from_timestamp(times[-1]) -
 | 
				
			||||||
            pendulum.from_timestamp(times[-2])
 | 
					            pendulum.from_timestamp(times[-2])
 | 
				
			||||||
        ).seconds
 | 
					        ).seconds
 | 
				
			||||||
        frame_step_s = (end_dt - start_dt).seconds
 | 
					        frame_size_s = len(to_push) * step_size_s
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if last_tsdb_dt is None:
 | 
					        if last_tsdb_dt is None:
 | 
				
			||||||
            # maybe a better default (they don't seem to define epoch?!)
 | 
					            # maybe a better default (they don't seem to define epoch?!)
 | 
				
			||||||
| 
						 | 
					@ -277,7 +280,7 @@ async def start_backfill(
 | 
				
			||||||
            # based on the sample step size load a certain amount
 | 
					            # based on the sample step size load a certain amount
 | 
				
			||||||
            # history
 | 
					            # history
 | 
				
			||||||
            if step_size_s == 1:
 | 
					            if step_size_s == 1:
 | 
				
			||||||
                last_tsdb_dt = pendulum.now().subtract(weeks=2)
 | 
					                last_tsdb_dt = pendulum.now().subtract(days=6)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            elif step_size_s == 60:
 | 
					            elif step_size_s == 60:
 | 
				
			||||||
                last_tsdb_dt = pendulum.now().subtract(years=2)
 | 
					                last_tsdb_dt = pendulum.now().subtract(years=2)
 | 
				
			||||||
| 
						 | 
					@ -290,69 +293,159 @@ async def start_backfill(
 | 
				
			||||||
                    'do dat bruh.'
 | 
					                    'do dat bruh.'
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        hist_period = pendulum.period(
 | 
					        # configure async query throttling
 | 
				
			||||||
            start_dt.subtract(seconds=step_size_s),
 | 
					        erlangs = config.get('erlangs', 1)
 | 
				
			||||||
            last_tsdb_dt,
 | 
					        rate = config.get('rate', 1)
 | 
				
			||||||
        )
 | 
					        frames = {}
 | 
				
			||||||
        end_dts = list(hist_period.range('seconds', frame_step_s))
 | 
					
 | 
				
			||||||
 | 
					        def iter_dts(start: datetime):
 | 
				
			||||||
 | 
					            while True:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                hist_period = pendulum.period(
 | 
				
			||||||
 | 
					                    start.subtract(seconds=step_size_s),
 | 
				
			||||||
 | 
					                    last_tsdb_dt,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                dtrange = hist_period.range('seconds', frame_size_s)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                for end_dt in dtrange:
 | 
				
			||||||
 | 
					                    log.warning(f'Yielding next frame start {end_dt}')
 | 
				
			||||||
 | 
					                    start = yield end_dt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    # if caller sends a new start date, reset to that
 | 
				
			||||||
 | 
					                    if start is not None:
 | 
				
			||||||
 | 
					                        log.warning(f'Resetting date range: {start}')
 | 
				
			||||||
 | 
					                        # import pdbpp
 | 
				
			||||||
 | 
					                        # pdbpp.set_trace()
 | 
				
			||||||
 | 
					                        break
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    # from while
 | 
				
			||||||
 | 
					                    return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # pull new history frames until we hit latest
 | 
					        # pull new history frames until we hit latest
 | 
				
			||||||
        # already in the tsdb or a max count.
 | 
					        # already in the tsdb or a max count.
 | 
				
			||||||
        count = 0
 | 
					        count = 0
 | 
				
			||||||
        frames = {}
 | 
					
 | 
				
			||||||
 | 
					        # NOTE: when gaps are detected in the retreived history (by
 | 
				
			||||||
 | 
					        # comparisor of the end - start versus the expected "frame size"
 | 
				
			||||||
 | 
					        # in seconds) we need a way to alert the async request code not
 | 
				
			||||||
 | 
					        # to continue to query for data "within the gap". This var is
 | 
				
			||||||
 | 
					        # set in such cases such that further requests in that period
 | 
				
			||||||
 | 
					        # are discarded and further we reset the "datetimem query frame
 | 
				
			||||||
 | 
					        # index" in such cases to avoid needless noop requests.
 | 
				
			||||||
 | 
					        earliest_end_dt: Optional[datetime] = start_dt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        async def get_ohlc_frame(
 | 
					        async def get_ohlc_frame(
 | 
				
			||||||
            input_end_dt: datetime,
 | 
					            input_end_dt: datetime,
 | 
				
			||||||
 | 
					            iter_dts_gen: Generator[datetime],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ) -> np.ndarray:
 | 
					        ) -> np.ndarray:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            nonlocal count
 | 
					            nonlocal count, frames, earliest_end_dt, frame_size_s
 | 
				
			||||||
            count += 1
 | 
					            count += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if input_end_dt > earliest_end_dt:
 | 
				
			||||||
 | 
					                # if a request comes in for an inter-gap frame we
 | 
				
			||||||
 | 
					                # discard it since likely this request is still
 | 
				
			||||||
 | 
					                # lingering from before the reset of ``iter_dts()`` via
 | 
				
			||||||
 | 
					                # ``.send()`` below.
 | 
				
			||||||
 | 
					                log.info(f'Discarding request history ending @ {input_end_dt}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # signals to ``trimeter`` loop to discard and
 | 
				
			||||||
 | 
					                # ``continue`` in it's schedule loop.
 | 
				
			||||||
 | 
					                return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
 | 
					                log.info(
 | 
				
			||||||
 | 
					                    f'Requesting {step_size_s}s frame ending in {input_end_dt}'
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
                array, start_dt, end_dt = await hist(end_dt=input_end_dt)
 | 
					                array, start_dt, end_dt = await hist(end_dt=input_end_dt)
 | 
				
			||||||
                # if input_end_dt.timestamp() == end_dts[0].timestamp():
 | 
					                assert array['time'][0] == start_dt.timestamp()
 | 
				
			||||||
                #     await tractor.breakpoint()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            except NoData:
 | 
					            except NoData:
 | 
				
			||||||
                # decrement by the diff in time last delivered.
 | 
					                log.warning(
 | 
				
			||||||
                end_dt = start_dt.subtract(seconds=(end_dt - start_dt).seconds)
 | 
					                    f'NO DATA for {frame_size_s}s frame @ {end_dt} ?!?'
 | 
				
			||||||
                log.warning('no data for range {(end_dt - start_dt)} ?!?')
 | 
					                )
 | 
				
			||||||
                # continue
 | 
					                return None  # discard signal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            except DataUnavailable:
 | 
					            except DataUnavailable as duerr:
 | 
				
			||||||
                # broker is being a bish and we can't pull
 | 
					                # broker is being a bish and we can't pull
 | 
				
			||||||
                # any more..
 | 
					                # any more..
 | 
				
			||||||
                log.warning('backend halted on data deliver !?!?')
 | 
					                log.warning('backend halted on data deliver !?!?')
 | 
				
			||||||
                return input_end_dt, None
 | 
					
 | 
				
			||||||
 | 
					                # ugh, what's a better way?
 | 
				
			||||||
 | 
					                # TODO: fwiw, we probably want a way to signal a throttle
 | 
				
			||||||
 | 
					                # condition (eg. with ib) so that we can halt the
 | 
				
			||||||
 | 
					                # request loop until the condition is resolved?
 | 
				
			||||||
 | 
					                return duerr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            diff = end_dt - start_dt
 | 
				
			||||||
 | 
					            frame_time_diff_s = diff.seconds
 | 
				
			||||||
 | 
					            expected_frame_size_s = frame_size_s + step_size_s
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if frame_time_diff_s > expected_frame_size_s:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # XXX: query result includes a start point prior to our
 | 
				
			||||||
 | 
					                # expected "frame size" and thus is likely some kind of
 | 
				
			||||||
 | 
					                # history gap (eg. market closed period, outage, etc.)
 | 
				
			||||||
 | 
					                # so indicate to the request loop that this gap is
 | 
				
			||||||
 | 
					                # expected by both,
 | 
				
			||||||
 | 
					                # - resetting the ``iter_dts()`` generator to start at
 | 
				
			||||||
 | 
					                #   the new start point delivered in this result
 | 
				
			||||||
 | 
					                # - setting the non-locally scoped ``earliest_end_dt``
 | 
				
			||||||
 | 
					                #   to this new value so that the request loop doesn't
 | 
				
			||||||
 | 
					                #   get tripped up thinking there's an out of order
 | 
				
			||||||
 | 
					                #   request-result condition.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                log.warning(
 | 
				
			||||||
 | 
					                    f'History frame ending @ {end_dt} appears to have a gap:\n'
 | 
				
			||||||
 | 
					                    f'{diff} ~= {frame_time_diff_s} seconds'
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # reset dtrange gen to new start point
 | 
				
			||||||
 | 
					                next_end = iter_dts_gen.send(start_dt)
 | 
				
			||||||
 | 
					                log.info(
 | 
				
			||||||
 | 
					                    f'Reset frame index to start at {start_dt}\n'
 | 
				
			||||||
 | 
					                    f'Was at {next_end}'
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # TODO: can we avoid this?
 | 
				
			||||||
 | 
					                earliest_end_dt = start_dt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            to_push = diff_history(
 | 
					            to_push = diff_history(
 | 
				
			||||||
                array,
 | 
					                array,
 | 
				
			||||||
                start_dt,
 | 
					                start_dt,
 | 
				
			||||||
                end_dt,
 | 
					                end_dt,
 | 
				
			||||||
 | 
					 | 
				
			||||||
                last_tsdb_dt=last_tsdb_dt,
 | 
					                last_tsdb_dt=last_tsdb_dt,
 | 
				
			||||||
                # XXX: hacky, just run indefinitely
 | 
					 | 
				
			||||||
                # last_tsdb_dt=None,
 | 
					 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            print(f"PULLING {count}")
 | 
					            ln = len(to_push)
 | 
				
			||||||
            log.info(f'Pushing {to_push.size} to shm!')
 | 
					            if ln:
 | 
				
			||||||
 | 
					                log.info(f'{ln} bars for {start_dt} -> {end_dt}')
 | 
				
			||||||
 | 
					                frames[input_end_dt.timestamp()] = (to_push, start_dt, end_dt)
 | 
				
			||||||
 | 
					                return to_push, start_dt, end_dt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            frames[input_end_dt.timestamp()] = (to_push, start_dt, end_dt)
 | 
					            else:
 | 
				
			||||||
 | 
					                log.warning(
 | 
				
			||||||
 | 
					                    f'{ln} BARS TO PUSH after diff?!: {start_dt} -> {end_dt}'
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return to_push, start_dt, end_dt
 | 
					        # initial dt index starts at the start of the first query result
 | 
				
			||||||
 | 
					        idts = iter_dts(start_dt)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # if to_push.size < 1:
 | 
					 | 
				
			||||||
            #     print('UHHH SIZE <1 BREAKING!?')
 | 
					 | 
				
			||||||
            #     break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        rate = erlangs = 5
 | 
					 | 
				
			||||||
        async with trimeter.amap(
 | 
					        async with trimeter.amap(
 | 
				
			||||||
 | 
					            partial(
 | 
				
			||||||
            get_ohlc_frame,
 | 
					                get_ohlc_frame,
 | 
				
			||||||
            end_dts,
 | 
					                # we close in the ``iter_dt()`` gen in so we can send
 | 
				
			||||||
 | 
					                # reset signals as needed for gap dection in the
 | 
				
			||||||
 | 
					                # history.
 | 
				
			||||||
 | 
					                iter_dts_gen=idts,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            idts,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            capture_outcome=True,
 | 
					            capture_outcome=True,
 | 
				
			||||||
            include_value=True,
 | 
					            include_value=True,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # better technical names bruv...
 | 
				
			||||||
            max_at_once=erlangs,
 | 
					            max_at_once=erlangs,
 | 
				
			||||||
            max_per_second=rate,
 | 
					            max_per_second=rate,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -362,59 +455,101 @@ async def start_backfill(
 | 
				
			||||||
            # (i.e., not necessarily in the original order)
 | 
					            # (i.e., not necessarily in the original order)
 | 
				
			||||||
            async for input_end_dt, outcome in outcomes:
 | 
					            async for input_end_dt, outcome in outcomes:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # no data available case..
 | 
					 | 
				
			||||||
                if outcome is None:
 | 
					 | 
				
			||||||
                    break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                try:
 | 
					                try:
 | 
				
			||||||
                    out = outcome.unwrap()
 | 
					                    out = outcome.unwrap()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if out is None:
 | 
				
			||||||
 | 
					                        # skip signal
 | 
				
			||||||
 | 
					                        continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    elif isinstance(out, DataUnavailable):
 | 
				
			||||||
 | 
					                        # no data available case signal.. so just kill
 | 
				
			||||||
 | 
					                        # further requests and basically just stop
 | 
				
			||||||
 | 
					                        # trying...
 | 
				
			||||||
 | 
					                        break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                except Exception:
 | 
					                except Exception:
 | 
				
			||||||
                    log.exception('uhh trimeter bail')
 | 
					                    log.exception('uhh trimeter bail')
 | 
				
			||||||
                    raise
 | 
					                    raise
 | 
				
			||||||
                else:
 | 
					                else:
 | 
				
			||||||
                    to_push, start_dt, end_dt = out
 | 
					                    to_push, start_dt, end_dt = out
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if not len(to_push):
 | 
				
			||||||
 | 
					                    # diff returned no new data (i.e. we probablyl hit
 | 
				
			||||||
 | 
					                    # the ``last_tsdb_dt`` point).
 | 
				
			||||||
 | 
					                    # TODO: raise instead?
 | 
				
			||||||
 | 
					                    log.warning(f'No history for range {start_dt} -> {end_dt}')
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # pipeline-style pull frames until we need to wait for
 | 
					                # pipeline-style pull frames until we need to wait for
 | 
				
			||||||
                # the next in order to arrive.
 | 
					                # the next in order to arrive.
 | 
				
			||||||
                i = end_dts.index(input_end_dt)
 | 
					                # i = end_dts.index(input_end_dt)
 | 
				
			||||||
                print(f'latest end_dt {end_dt} found at index {i}')
 | 
					                # print(f'latest end_dt {end_dt} found at index {i}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                for epoch in reversed(sorted(frames)):
 | 
					                epochs = list(reversed(sorted(frames)))
 | 
				
			||||||
 | 
					                for epoch in epochs:
 | 
				
			||||||
                    start = shm.array['time'][0]
 | 
					                    start = shm.array['time'][0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    # we don't yet have the next frame to push
 | 
					 | 
				
			||||||
                    # so break back to the async request loop.
 | 
					 | 
				
			||||||
                    diff = epoch - start
 | 
					                    diff = epoch - start
 | 
				
			||||||
                    if abs(diff) > step_size_s:
 | 
					                    if abs(diff) > step_size_s:
 | 
				
			||||||
                        if len(frames) > 20:
 | 
					
 | 
				
			||||||
 | 
					                        if earliest_end_dt < end_dt:
 | 
				
			||||||
 | 
					                            # XXX: an expected gap was encountered (see
 | 
				
			||||||
 | 
					                            # logic in ``get_ohlc_frame()``, so allow
 | 
				
			||||||
 | 
					                            # this frame through to the storage layer.
 | 
				
			||||||
                            log.warning(
 | 
					                            log.warning(
 | 
				
			||||||
                                f'there appears to be a history gap of {diff}?'
 | 
					                                f'there is an expected history gap of {diff}s:'
 | 
				
			||||||
 | 
					                            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        elif (
 | 
				
			||||||
 | 
					                            erlangs > 1
 | 
				
			||||||
 | 
					                            and len(epochs) < erlangs
 | 
				
			||||||
 | 
					                        ):
 | 
				
			||||||
 | 
					                            # we don't yet have the next frame to push
 | 
				
			||||||
 | 
					                            # so break back to the async request loop
 | 
				
			||||||
 | 
					                            # while we wait for more async frame-results
 | 
				
			||||||
 | 
					                            # to arrive.
 | 
				
			||||||
 | 
					                            expect_end = pendulum.from_timestamp(start)
 | 
				
			||||||
 | 
					                            expect_start = expect_end.subtract(
 | 
				
			||||||
 | 
					                                seconds=frame_size_s)
 | 
				
			||||||
 | 
					                            log.warning(
 | 
				
			||||||
 | 
					                                'waiting on out-of-order history frame:\n'
 | 
				
			||||||
 | 
					                                f'{expect_end - expect_start}'
 | 
				
			||||||
                            )
 | 
					                            )
 | 
				
			||||||
                            # from pprint import pprint
 | 
					 | 
				
			||||||
                            # await tractor.breakpoint()
 | 
					 | 
				
			||||||
                        else:
 | 
					 | 
				
			||||||
                            break
 | 
					                            break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    to_push, start_dt, end_dt = frames.pop(epoch)
 | 
					                    to_push, start_dt, end_dt = frames.pop(epoch)
 | 
				
			||||||
                    print(f'pushing frame ending at {end_dt}')
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if not len(to_push):
 | 
					                    # bail gracefully on shm allocation overrun/full condition
 | 
				
			||||||
                        break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    # bail on shm allocation overrun
 | 
					 | 
				
			||||||
                    try:
 | 
					                    try:
 | 
				
			||||||
                        shm.push(to_push, prepend=True)
 | 
					                        shm.push(to_push, prepend=True)
 | 
				
			||||||
                    except ValueError:
 | 
					                    except ValueError:
 | 
				
			||||||
                        await tractor.breakpoint()
 | 
					                        log.info(
 | 
				
			||||||
 | 
					                            f'Shm buffer overrun on: {start_dt} -> {end_dt}?'
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        # await tractor.breakpoint()
 | 
				
			||||||
                        break
 | 
					                        break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    for delay_s in sampler.subscribers:
 | 
					                    log.info(
 | 
				
			||||||
                        await broadcast(delay_s)
 | 
					                        f'Shm pushed {len(to_push)} frame:\n'
 | 
				
			||||||
 | 
					                        f'{start_dt} -> {end_dt}'
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    # keep track of most recent "prepended" ``start_dt``
 | 
				
			||||||
 | 
					                    # both for detecting gaps and ensuring async
 | 
				
			||||||
 | 
					                    # frame-result order.
 | 
				
			||||||
 | 
					                    earliest_end_dt = start_dt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # TODO: can we only trigger this if the respective
 | 
				
			||||||
 | 
					                # history in "in view"?!?
 | 
				
			||||||
 | 
					                # XXX: extremely important, there can be no checkpoints
 | 
				
			||||||
 | 
					                # in the block above to avoid entering new ``frames``
 | 
				
			||||||
 | 
					                # values while we're pipelining the current ones to
 | 
				
			||||||
 | 
					                # memory...
 | 
				
			||||||
 | 
					                for delay_s in sampler.subscribers:
 | 
				
			||||||
 | 
					                    await broadcast(delay_s)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        bf_done.set()
 | 
					        bf_done.set()
 | 
				
			||||||
        # update start index to include all tsdb history
 | 
					 | 
				
			||||||
        # that was pushed in the caller parent task.
 | 
					 | 
				
			||||||
        # shm._first.value = 0
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async def manage_history(
 | 
					async def manage_history(
 | 
				
			||||||
| 
						 | 
					@ -490,6 +625,17 @@ async def manage_history(
 | 
				
			||||||
                    last_tsdb_dt=last_dt,
 | 
					                    last_tsdb_dt=last_dt,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # if len(shm.array) < 2:
 | 
				
			||||||
 | 
					            # TODO: there's an edge case here to solve where if the last
 | 
				
			||||||
 | 
					            # frame before market close (at least on ib) was pushed and
 | 
				
			||||||
 | 
					            # there was only "1 new" row pushed from the first backfill
 | 
				
			||||||
 | 
					            # query-iteration, then the sample step sizing calcs will
 | 
				
			||||||
 | 
					            # break upstream from here since you can't diff on at least
 | 
				
			||||||
 | 
					            # 2 steps... probably should also add logic to compute from
 | 
				
			||||||
 | 
					            # the tsdb series and stash that somewhere as meta data on
 | 
				
			||||||
 | 
					            # the shm buffer?.. no se.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            task_status.started(shm)
 | 
					            task_status.started(shm)
 | 
				
			||||||
            some_data_ready.set()
 | 
					            some_data_ready.set()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -524,14 +670,7 @@ async def manage_history(
 | 
				
			||||||
                    prepend=True,
 | 
					                    prepend=True,
 | 
				
			||||||
                    # update_first=False,
 | 
					                    # update_first=False,
 | 
				
			||||||
                    # start=prepend_start,
 | 
					                    # start=prepend_start,
 | 
				
			||||||
                    field_map={
 | 
					                    field_map=marketstore.ohlc_key_map,
 | 
				
			||||||
                        'Epoch': 'time',
 | 
					 | 
				
			||||||
                        'Open': 'open',
 | 
					 | 
				
			||||||
                        'High': 'high',
 | 
					 | 
				
			||||||
                        'Low': 'low',
 | 
					 | 
				
			||||||
                        'Close': 'close',
 | 
					 | 
				
			||||||
                        'Volume': 'volume',
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # load as much from storage into shm as spacec will
 | 
					                # load as much from storage into shm as spacec will
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue