Start using `tick_size` throughout charting
The min tick size is the smallest step an instrument can move in value (think the number of decimals places of precision the value can have). We start leveraging this in a few places: - make our internal "symbol" type expose it as part of it's api so that it can be passed around by UI components - in y-axis view box scaling, use it to keep the bid/ask spread (L1 UI) always on screen even in the case where the spread has moved further out of view then the last clearing price - allows the EMS to determine dark order live order submission offsetsbasic_orders
parent
03541bd368
commit
a8c4829cb6
|
@ -114,7 +114,11 @@ def get_book(broker: str) -> _ExecBook:
|
|||
return _books.setdefault(broker, _ExecBook(broker))
|
||||
|
||||
|
||||
_DEFAULT_SIZE: float = 100.0
|
||||
# XXX: this is in place to prevent accidental positions that are too
|
||||
# big. Now obviously this won't make sense for crypto like BTC, but
|
||||
# for most traditional brokers it should be fine unless you start
|
||||
# slinging NQ futes or something.
|
||||
_DEFAULT_SIZE: float = 1.0
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -326,7 +330,7 @@ async def process_broker_trades(
|
|||
'fill' -> 'broker_filled'
|
||||
|
||||
Currently accepted status values from IB
|
||||
{'presubmitted', 'submitted', 'cancelled'}
|
||||
{'presubmitted', 'submitted', 'cancelled', 'inactive'}
|
||||
|
||||
"""
|
||||
broker = feed.mod.name
|
||||
|
@ -352,7 +356,9 @@ async def process_broker_trades(
|
|||
oid = book._broker2ems_ids.get(reqid)
|
||||
resp = {'oid': oid}
|
||||
|
||||
if name in ('error',):
|
||||
if name in (
|
||||
'error',
|
||||
):
|
||||
# TODO: figure out how this will interact with EMS clients
|
||||
# for ex. on an error do we react with a dark orders
|
||||
# management response, like cancelling all dark orders?
|
||||
|
@ -373,8 +379,9 @@ async def process_broker_trades(
|
|||
# another stupid ib error to handle
|
||||
# if 10147 in message: cancel
|
||||
|
||||
elif name in ('status',):
|
||||
|
||||
elif name in (
|
||||
'status',
|
||||
):
|
||||
# everyone doin camel case
|
||||
status = msg['status'].lower()
|
||||
|
||||
|
@ -397,7 +404,9 @@ async def process_broker_trades(
|
|||
|
||||
await ctx.send_yield(resp)
|
||||
|
||||
elif name in ('fill',):
|
||||
elif name in (
|
||||
'fill',
|
||||
):
|
||||
# proxy through the "fill" result(s)
|
||||
resp['resp'] = 'broker_filled'
|
||||
resp.update(msg)
|
||||
|
@ -534,7 +543,7 @@ async def _ems_main(
|
|||
# the user choose the predicate operator.
|
||||
pred = mk_check(trigger_price, last)
|
||||
|
||||
mt = feed.symbols[sym].min_tick
|
||||
mt = feed.symbols[sym].tick_size
|
||||
|
||||
if action == 'buy':
|
||||
tickfilter = ('ask', 'last', 'trade')
|
||||
|
|
|
@ -80,9 +80,11 @@ class Symbol:
|
|||
"""I guess this is some kinda container thing for dealing with
|
||||
all the different meta-data formats from brokers?
|
||||
|
||||
Yah, i guess dats what it izz.
|
||||
"""
|
||||
key: str = ''
|
||||
min_tick: float = 0.01
|
||||
tick_size: float = 0.01
|
||||
v_tick_size: float = 0.01
|
||||
broker_info: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
||||
deriv: str = ''
|
||||
|
||||
|
@ -91,17 +93,17 @@ class Symbol:
|
|||
return list(self.broker_info.keys())
|
||||
|
||||
def digits(self) -> int:
|
||||
"""Return the trailing number of digits specified by the
|
||||
min tick size for the instrument.
|
||||
"""Return the trailing number of digits specified by the min
|
||||
tick size for the instrument.
|
||||
|
||||
"""
|
||||
return float_digits(self.min_tick)
|
||||
return float_digits(self.tick_size)
|
||||
|
||||
def nearest_tick(self, value: float) -> float:
|
||||
"""Return the nearest tick value based on mininum increment.
|
||||
|
||||
"""
|
||||
mult = 1 / self.min_tick
|
||||
mult = 1 / self.tick_size
|
||||
return round(value * mult) / mult
|
||||
|
||||
|
||||
|
|
|
@ -140,7 +140,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'tick:{symbol.min_tick}'
|
||||
f'tick:{symbol.tick_size}'
|
||||
)
|
||||
|
||||
# TODO: symbol search
|
||||
|
@ -195,7 +195,6 @@ class LinkedSplitCharts(QtGui.QWidget):
|
|||
self._cursor: Cursor = None # crosshair graphics
|
||||
self.chart: ChartPlotWidget = None # main (ohlc) chart
|
||||
self.subplots: Dict[Tuple[str, ...], ChartPlotWidget] = {}
|
||||
self.digits: int = 2
|
||||
|
||||
self.xaxis = DynamicDateAxis(
|
||||
orientation='bottom',
|
||||
|
@ -245,13 +244,10 @@ class LinkedSplitCharts(QtGui.QWidget):
|
|||
|
||||
The data input struct array must include OHLC fields.
|
||||
"""
|
||||
self.min_tick = symbol.min_tick
|
||||
self.digits = symbol.digits()
|
||||
|
||||
# add crosshairs
|
||||
self._cursor = Cursor(
|
||||
linkedsplitcharts=self,
|
||||
digits=self.digits
|
||||
digits=symbol.digits(),
|
||||
)
|
||||
self.chart = self.add_plot(
|
||||
name=symbol.key,
|
||||
|
@ -428,7 +424,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
# Assign callback for rescaling y-axis automatically
|
||||
# based on data contents and ``ViewBox`` state.
|
||||
self.sigXRangeChanged.connect(self._set_yrange)
|
||||
# self.sigXRangeChanged.connect(self._set_yrange)
|
||||
|
||||
# for mouse wheel which doesn't seem to emit XRangeChanged
|
||||
self._vb.sigRangeChangedManually.connect(self._set_yrange)
|
||||
|
@ -506,6 +502,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
max=end,
|
||||
padding=0,
|
||||
)
|
||||
self._set_yrange()
|
||||
|
||||
def increment_view(
|
||||
self,
|
||||
|
@ -657,7 +654,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
chart=self,
|
||||
parent=self.getAxis('right'),
|
||||
# TODO: pass this from symbol data
|
||||
digits=self._lc._symbol.digits(),
|
||||
digits=self._lc.symbol.digits(),
|
||||
opacity=1,
|
||||
bg_color=bg_color,
|
||||
)
|
||||
|
@ -708,6 +705,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
self,
|
||||
*,
|
||||
yrange: Optional[Tuple[float, float]] = None,
|
||||
range_margin: float = 0.04,
|
||||
) -> None:
|
||||
"""Set the viewable y-range based on embedded data.
|
||||
|
||||
|
@ -767,7 +765,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
a = self._ohlc
|
||||
ifirst = a[0]['index']
|
||||
bars = a[lbar - ifirst:rbar - ifirst]
|
||||
bars = a[lbar - ifirst:rbar - ifirst + 1]
|
||||
|
||||
if not len(bars):
|
||||
# likely no data loaded yet or extreme scrolling?
|
||||
|
@ -788,8 +786,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
if set_range:
|
||||
# view margins: stay within a % of the "true range"
|
||||
diff = yhigh - ylow
|
||||
ylow = ylow - (diff * 0.04)
|
||||
yhigh = yhigh + (diff * 0.04)
|
||||
ylow = ylow - (diff * range_margin)
|
||||
yhigh = yhigh + (diff * range_margin)
|
||||
|
||||
self.setLimits(
|
||||
yMin=ylow,
|
||||
|
@ -992,20 +990,27 @@ async def _async_main(
|
|||
resp = msg['resp']
|
||||
|
||||
# response to 'action' request (buy/sell)
|
||||
if resp in ('dark_submitted', 'broker_submitted'):
|
||||
if resp in (
|
||||
'dark_submitted',
|
||||
'broker_submitted'
|
||||
):
|
||||
|
||||
# show line label once order is live
|
||||
line = order_mode.on_submit(oid)
|
||||
# await tractor.breakpoint()
|
||||
|
||||
# resp to 'cancel' request or error condition
|
||||
# for action request
|
||||
elif resp in ('broker_cancelled', 'dark_cancelled'):
|
||||
|
||||
elif resp in (
|
||||
'broker_cancelled',
|
||||
'broker_inactive',
|
||||
'dark_cancelled'
|
||||
):
|
||||
# delete level line from view
|
||||
order_mode.on_cancel(oid)
|
||||
|
||||
elif resp in ('dark_executed'):
|
||||
elif resp in (
|
||||
'dark_executed'
|
||||
):
|
||||
log.info(f'Dark order filled for {fmsg}')
|
||||
|
||||
# for alerts add a triangle and remove the
|
||||
|
@ -1021,7 +1026,9 @@ async def _async_main(
|
|||
line = await order_mode.on_exec(oid, msg)
|
||||
|
||||
# response to completed 'action' request for buy/sell
|
||||
elif resp in ('broker_executed',):
|
||||
elif resp in (
|
||||
'broker_executed',
|
||||
):
|
||||
await order_mode.on_exec(oid, msg)
|
||||
|
||||
# each clearing tick is responded individually
|
||||
|
@ -1081,7 +1088,7 @@ async def chart_from_quotes(
|
|||
# sym = chart.name
|
||||
# mx, mn = np.nanmax(in_view[sym]), np.nanmin(in_view[sym])
|
||||
|
||||
return last_bars_range, mx, mn
|
||||
return last_bars_range, mx, max(mn, 0)
|
||||
|
||||
chart.default_view()
|
||||
|
||||
|
@ -1104,6 +1111,9 @@ async def chart_from_quotes(
|
|||
# levels this might be dark volume we need to
|
||||
# present differently?
|
||||
|
||||
tick_size = chart._lc.symbol.tick_size
|
||||
tick_margin = 2 * tick_size
|
||||
|
||||
async for quotes in stream:
|
||||
for sym, quote in quotes.items():
|
||||
|
||||
|
@ -1114,20 +1124,18 @@ async def chart_from_quotes(
|
|||
price = tick.get('price')
|
||||
size = tick.get('size')
|
||||
|
||||
# compute max and min trade values to display in view
|
||||
# TODO: we need a streaming minmax algorithm here, see
|
||||
# def above.
|
||||
brange, mx_in_view, mn_in_view = maxmin()
|
||||
l, lbar, rbar, r = brange
|
||||
if ticktype == 'n/a' or price == -1:
|
||||
# okkk..
|
||||
continue
|
||||
|
||||
if ticktype in ('trade', 'utrade', 'last'):
|
||||
|
||||
array = ohlcv.array
|
||||
|
||||
# update price sticky(s)
|
||||
last = array[-1]
|
||||
end = array[-1]
|
||||
last_price_sticky.update_from_data(
|
||||
*last[['index', 'close']]
|
||||
*end[['index', 'close']]
|
||||
)
|
||||
|
||||
# plot bars
|
||||
|
@ -1139,7 +1147,17 @@ async def chart_from_quotes(
|
|||
|
||||
if wap_in_history:
|
||||
# update vwap overlay line
|
||||
chart.update_curve_from_array('bar_wap', ohlcv.array)
|
||||
chart.update_curve_from_array(
|
||||
'bar_wap', ohlcv.array)
|
||||
|
||||
# compute max and min trade values to display in view
|
||||
# TODO: we need a streaming minmax algorithm here, see
|
||||
# def above.
|
||||
brange, mx_in_view, mn_in_view = maxmin()
|
||||
l, lbar, rbar, r = brange
|
||||
|
||||
mx = mx_in_view + tick_margin
|
||||
mn = mn_in_view - tick_margin
|
||||
|
||||
# XXX: prettty sure this is correct?
|
||||
# if ticktype in ('trade', 'last'):
|
||||
|
@ -1162,25 +1180,33 @@ async def chart_from_quotes(
|
|||
l1.ask_label.size = size
|
||||
l1.ask_label.update_from_data(0, price)
|
||||
|
||||
# update max price in view to keep ask on screen
|
||||
mx_in_view = max(price, mx_in_view)
|
||||
|
||||
elif ticktype in ('bid', 'bsize'):
|
||||
l1.bid_label.size = size
|
||||
l1.bid_label.update_from_data(0, price)
|
||||
|
||||
# update min price in view to keep bid on screen
|
||||
mn_in_view = min(price, mn_in_view)
|
||||
# update min price in view to keep bid on screen
|
||||
mn = min(price - tick_margin, mn)
|
||||
# update max price in view to keep ask on screen
|
||||
mx = max(price + tick_margin, mx)
|
||||
|
||||
if mx_in_view > last_mx or mn_in_view < last_mn:
|
||||
chart._set_yrange(yrange=(mn_in_view, mx_in_view))
|
||||
last_mx, last_mn = mx_in_view, mn_in_view
|
||||
if (mx > last_mx) or (
|
||||
mn < last_mn
|
||||
):
|
||||
print(f'new y range: {(mn, mx)}')
|
||||
|
||||
chart._set_yrange(
|
||||
yrange=(mn, mx),
|
||||
# TODO: we should probably scale
|
||||
# the view margin based on the size
|
||||
# of the true range? This way you can
|
||||
# slap in orders outside the current
|
||||
# L1 (only) book range.
|
||||
# range_margin=0.1,
|
||||
)
|
||||
|
||||
last_mx, last_mn = mx, mn
|
||||
|
||||
if brange != last_bars_range:
|
||||
# we **must always** update the last values due to
|
||||
# the x-range change
|
||||
last_mx, last_mn = mx_in_view, mn_in_view
|
||||
last_bars_range = brange
|
||||
|
||||
|
||||
async def spawn_fsps(
|
||||
|
@ -1355,10 +1381,10 @@ async def update_signals(
|
|||
|
||||
# add moveable over-[sold/bought] lines
|
||||
# and labels only for the 70/30 lines
|
||||
l = level_line(chart, 20)
|
||||
level_line(chart, 20)
|
||||
level_line(chart, 30, orient_v='top')
|
||||
level_line(chart, 70, orient_v='bottom')
|
||||
l = level_line(chart, 80, orient_v='top')
|
||||
level_line(chart, 80, orient_v='top')
|
||||
|
||||
chart._set_yrange()
|
||||
|
||||
|
@ -1395,6 +1421,7 @@ async def update_signals(
|
|||
async def check_for_new_bars(feed, ohlcv, linked_charts):
|
||||
"""Task which updates from new bars in the shared ohlcv buffer every
|
||||
``delay_s`` seconds.
|
||||
|
||||
"""
|
||||
# TODO: right now we'll spin printing bars if the last time
|
||||
# stamp is before a large period of no market activity.
|
||||
|
@ -1422,12 +1449,6 @@ async def check_for_new_bars(feed, ohlcv, linked_charts):
|
|||
# current bar) and then either write the current bar manually
|
||||
# or place a cursor for visual cue of the current time step.
|
||||
|
||||
# price_chart.update_ohlc_from_array(
|
||||
# price_chart.name,
|
||||
# ohlcv.array,
|
||||
# just_history=True,
|
||||
# )
|
||||
|
||||
# XXX: this puts a flat bar on the current time step
|
||||
# TODO: if we eventually have an x-axis time-step "cursor"
|
||||
# we can get rid of this since it is extra overhead.
|
||||
|
@ -1437,9 +1458,6 @@ async def check_for_new_bars(feed, ohlcv, linked_charts):
|
|||
just_history=False,
|
||||
)
|
||||
|
||||
# resize view
|
||||
# price_chart._set_yrange()
|
||||
|
||||
for name in price_chart._overlays:
|
||||
|
||||
price_chart.update_curve_from_array(
|
||||
|
@ -1447,15 +1465,8 @@ async def check_for_new_bars(feed, ohlcv, linked_charts):
|
|||
price_chart._arrays[name]
|
||||
)
|
||||
|
||||
# # TODO: standard api for signal lookups per plot
|
||||
# if name in price_chart._ohlc.dtype.fields:
|
||||
|
||||
# # should have already been incremented above
|
||||
# price_chart.update_curve_from_array(name, price_chart._ohlc)
|
||||
|
||||
for name, chart in linked_charts.subplots.items():
|
||||
chart.update_curve_from_array(chart.name, chart._shm.array)
|
||||
# chart._set_yrange()
|
||||
|
||||
# shift the view if in follow mode
|
||||
price_chart.increment_view()
|
||||
|
@ -1467,6 +1478,7 @@ def _main(
|
|||
tractor_kwargs,
|
||||
) -> None:
|
||||
"""Sync entry point to start a chart app.
|
||||
|
||||
"""
|
||||
# Qt entry point
|
||||
run_qtractor(
|
||||
|
|
|
@ -228,7 +228,7 @@ class Cursor(pg.GraphicsObject):
|
|||
|
||||
# value used for rounding y-axis discreet tick steps
|
||||
# computing once, up front, here cuz why not
|
||||
self._y_incr_mult = 1 / self.lsc._symbol.min_tick
|
||||
self._y_incr_mult = 1 / self.lsc._symbol.tick_size
|
||||
|
||||
def add_hovered(
|
||||
self,
|
||||
|
|
Loading…
Reference in New Issue