Add a normalizer routine which emits quote differentials/ticks
							parent
							
								
									75824f7afa
								
							
						
					
					
						commit
						5fe8e420b8
					
				| 
						 | 
				
			
			@ -9,6 +9,7 @@ from datetime import datetime
 | 
			
		|||
from functools import partial
 | 
			
		||||
import itertools
 | 
			
		||||
import configparser
 | 
			
		||||
from pprint import pformat
 | 
			
		||||
from typing import (
 | 
			
		||||
    List, Tuple, Dict, Any, Iterator, NamedTuple,
 | 
			
		||||
    AsyncGenerator,
 | 
			
		||||
| 
						 | 
				
			
			@ -838,7 +839,8 @@ _qt_stock_keys = {
 | 
			
		|||
    # 'low52w': 'low52w',  # put in info widget
 | 
			
		||||
    # 'high52w': 'high52w',
 | 
			
		||||
    # "lastTradePriceTrHrs": 7.99,
 | 
			
		||||
    'lastTradeTime': ('fill_time', datetime.fromisoformat),
 | 
			
		||||
    # 'lastTradeTime': ('fill_time', datetime.fromisoformat),
 | 
			
		||||
    'lastTradeTime': 'fill_time',
 | 
			
		||||
    "lastTradeTick": 'tick',  # ("Equal", "Up", "Down")
 | 
			
		||||
    # "symbolId": 3575753,
 | 
			
		||||
    # "tier": "",
 | 
			
		||||
| 
						 | 
				
			
			@ -914,6 +916,7 @@ def format_stock_quote(
 | 
			
		|||
        new[new_key] = value
 | 
			
		||||
        displayable[new_key] = display_value
 | 
			
		||||
 | 
			
		||||
    new['displayable'] = displayable
 | 
			
		||||
    return new, displayable
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -974,6 +977,7 @@ def format_option_quote(
 | 
			
		|||
    quote: dict,
 | 
			
		||||
    symbol_data: dict,
 | 
			
		||||
    keymap: dict = _qt_option_keys,
 | 
			
		||||
    include_displayables: bool = True,
 | 
			
		||||
) -> Tuple[dict, dict]:
 | 
			
		||||
    """Remap a list of quote dicts ``quotes`` using the mapping of old keys
 | 
			
		||||
    -> new keys ``keymap`` returning 2 dicts: one with raw data and the other
 | 
			
		||||
| 
						 | 
				
			
			@ -1061,7 +1065,10 @@ async def get_cached_client(
 | 
			
		|||
            await client._exit_stack.aclose()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def smoke_quote(get_quotes, tickers):  # , broker):
 | 
			
		||||
async def smoke_quote(
 | 
			
		||||
    get_quotes,
 | 
			
		||||
    tickers
 | 
			
		||||
):
 | 
			
		||||
    """Do an initial "smoke" request for symbols in ``tickers`` filtering
 | 
			
		||||
    out any symbols not supported by the broker queried in the call to
 | 
			
		||||
    ``get_quotes()``.
 | 
			
		||||
| 
						 | 
				
			
			@ -1100,6 +1107,7 @@ async def smoke_quote(get_quotes, tickers):  # , broker):
 | 
			
		|||
            log.error(
 | 
			
		||||
                f"{symbol} seems to be defunct")
 | 
			
		||||
 | 
			
		||||
        quote['symbol'] = symbol
 | 
			
		||||
        payload[symbol] = quote
 | 
			
		||||
 | 
			
		||||
    return payload
 | 
			
		||||
| 
						 | 
				
			
			@ -1108,20 +1116,90 @@ async def smoke_quote(get_quotes, tickers):  # , broker):
 | 
			
		|||
    ###########################################
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# unbounded, shared between streaming tasks
 | 
			
		||||
_symbol_info_cache = {}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# function to format packets delivered to subscribers
 | 
			
		||||
def packetizer(
 | 
			
		||||
    topic: str,
 | 
			
		||||
    quotes: Dict[str, Any],
 | 
			
		||||
    formatter: Callable,
 | 
			
		||||
    symbol_data: Dict[str, Any],
 | 
			
		||||
) -> Dict[str, Any]:
 | 
			
		||||
    """Normalize quotes by name into dicts using broker-specific
 | 
			
		||||
    processing.
 | 
			
		||||
    """
 | 
			
		||||
    new = {}
 | 
			
		||||
    for quote in quotes:
 | 
			
		||||
        new[quote['symbol']], _ = formatter(quote, symbol_data)
 | 
			
		||||
    # repack into symbol keyed dict
 | 
			
		||||
    return {q['symbol']: q for q in quotes}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def normalize(
 | 
			
		||||
    quotes: Dict[str, Any],
 | 
			
		||||
    _cache: Dict[str, Any],  # dict held in scope of the streaming loop
 | 
			
		||||
    formatter: Callable,
 | 
			
		||||
) -> Dict[str, Any]:
 | 
			
		||||
    """Deliver normalized quotes by name into dicts using
 | 
			
		||||
    broker-specific processing; only emit changes differeing from the
 | 
			
		||||
    last quote sample creating a psuedo-tick type datum.
 | 
			
		||||
    """
 | 
			
		||||
    new = {}
 | 
			
		||||
    # XXX: this is effectively emitting "sampled ticks"
 | 
			
		||||
    # useful for polling setups but obviously should be
 | 
			
		||||
    # disabled if you're already rx-ing per-tick data.
 | 
			
		||||
    for quote in quotes:
 | 
			
		||||
        symbol = quote['symbol']
 | 
			
		||||
 | 
			
		||||
        # look up last quote from cache
 | 
			
		||||
        last = _cache.setdefault(symbol, {})
 | 
			
		||||
        _cache[symbol] = quote
 | 
			
		||||
 | 
			
		||||
        # compute volume difference
 | 
			
		||||
        last_volume = last.get('volume', 0)
 | 
			
		||||
        current_volume = quote['volume']
 | 
			
		||||
        volume_diff = current_volume - last_volume
 | 
			
		||||
 | 
			
		||||
        # find all keys that have match to a new value compared
 | 
			
		||||
        # to the last quote received
 | 
			
		||||
        changed = set(quote.items()) - set(last.items())
 | 
			
		||||
        if changed:
 | 
			
		||||
            log.info(f"New quote {symbol}:\n{changed}")
 | 
			
		||||
 | 
			
		||||
            # TODO: can we reduce the # of iterations here and in
 | 
			
		||||
            # called funcs?
 | 
			
		||||
            payload = {k: quote[k] for k, v in changed}
 | 
			
		||||
            payload['symbol'] = symbol  # required by formatter
 | 
			
		||||
 | 
			
		||||
            # TODO: we should probaby do the "computed" fields
 | 
			
		||||
            # processing found inside this func in a downstream actor?
 | 
			
		||||
            fquote, _ = formatter(payload, _symbol_info_cache)
 | 
			
		||||
            fquote['key'] = fquote['symbol'] = symbol
 | 
			
		||||
 | 
			
		||||
            # if there was volume likely the last size of
 | 
			
		||||
            # shares traded is useful info and it's possible
 | 
			
		||||
            # that the set difference from above will disregard
 | 
			
		||||
            # a "size" value since the same # of shares were traded
 | 
			
		||||
            # volume = payload.get('volume')
 | 
			
		||||
            if volume_diff:
 | 
			
		||||
                if volume_diff < 0:
 | 
			
		||||
                    log.error(f"Uhhh {symbol} volume: {volume_diff} ?")
 | 
			
		||||
 | 
			
		||||
                fquote['volume_delta'] = volume_diff
 | 
			
		||||
 | 
			
		||||
                # TODO: We can emit 2 ticks here:
 | 
			
		||||
                # - one for the volume differential
 | 
			
		||||
                # - one for the last known trade size
 | 
			
		||||
                # The first in theory can be unwound and
 | 
			
		||||
                # interpolated assuming the broker passes an
 | 
			
		||||
                # accurate daily VWAP value.
 | 
			
		||||
                # To make this work we need a universal ``size``
 | 
			
		||||
                # field that is normalized before hitting this logic.
 | 
			
		||||
                fquote['size'] = quote.get('lastTradeSize', 0)
 | 
			
		||||
                if 'last' not in fquote:
 | 
			
		||||
                    fquote['last'] = quote.get('lastTradePrice', float('nan'))
 | 
			
		||||
 | 
			
		||||
            new[symbol] = fquote
 | 
			
		||||
 | 
			
		||||
    if new:
 | 
			
		||||
        log.info(f"New quotes:\n{pformat(new)}")
 | 
			
		||||
    return new
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1130,13 +1208,12 @@ async def stream_quotes(
 | 
			
		|||
    ctx: tractor.Context,  # marks this as a streaming func
 | 
			
		||||
    symbols: List[str],
 | 
			
		||||
    feed_type: str = 'stock',
 | 
			
		||||
    diff_cached: bool = True,
 | 
			
		||||
    rate: int = 3,
 | 
			
		||||
    loglevel: str = None,
 | 
			
		||||
    # feed_type: str = 'stock',
 | 
			
		||||
) -> AsyncGenerator[str, Dict[str, Any]]:
 | 
			
		||||
    # XXX: required to propagate ``tractor`` loglevel to piker logging
 | 
			
		||||
    get_console_log(tractor.current_actor().loglevel)
 | 
			
		||||
    get_console_log(loglevel)
 | 
			
		||||
 | 
			
		||||
    async with get_cached_client('questrade') as client:
 | 
			
		||||
        if feed_type == 'stock':
 | 
			
		||||
| 
						 | 
				
			
			@ -1145,17 +1222,25 @@ async def stream_quotes(
 | 
			
		|||
 | 
			
		||||
            # do a smoke quote (note this mutates the input list and filters
 | 
			
		||||
            # out bad symbols for now)
 | 
			
		||||
            payload = await smoke_quote(get_quotes, list(symbols))
 | 
			
		||||
            first_quotes = await smoke_quote(get_quotes, list(symbols))
 | 
			
		||||
        else:
 | 
			
		||||
            formatter = format_option_quote
 | 
			
		||||
            get_quotes = await option_quoter(client, symbols)
 | 
			
		||||
            # packetize
 | 
			
		||||
            payload = {
 | 
			
		||||
            first_quotes = {
 | 
			
		||||
                quote['symbol']: quote
 | 
			
		||||
                for quote in await get_quotes(symbols)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        # update global symbol data state
 | 
			
		||||
        sd = await client.symbol_info(symbols)
 | 
			
		||||
        _symbol_info_cache.update(sd)
 | 
			
		||||
 | 
			
		||||
        # pre-process first set of quotes
 | 
			
		||||
        payload = {}
 | 
			
		||||
        for sym, quote in first_quotes.items():
 | 
			
		||||
            fquote, _ = formatter(quote, sd)
 | 
			
		||||
            payload[sym] = fquote
 | 
			
		||||
 | 
			
		||||
        # push initial smoke quote response for client initialization
 | 
			
		||||
        await ctx.send_yield(payload)
 | 
			
		||||
| 
						 | 
				
			
			@ -1168,15 +1253,11 @@ async def stream_quotes(
 | 
			
		|||
            task_name=feed_type,
 | 
			
		||||
            ctx=ctx,
 | 
			
		||||
            topics=symbols,
 | 
			
		||||
            packetizer=partial(
 | 
			
		||||
                packetizer,
 | 
			
		||||
                formatter=formatter,
 | 
			
		||||
                symbol_data=sd,
 | 
			
		||||
            ),
 | 
			
		||||
            packetizer=packetizer,
 | 
			
		||||
 | 
			
		||||
            # actual target "streaming func" args
 | 
			
		||||
            get_quotes=get_quotes,
 | 
			
		||||
            diff_cached=diff_cached,
 | 
			
		||||
            normalizer=partial(normalize, formatter=formatter),
 | 
			
		||||
            rate=rate,
 | 
			
		||||
        )
 | 
			
		||||
        log.info("Terminating stream quoter task")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue