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