Merge pull request #249 from pikers/basic_vlm_display

Basic vlm display
misc_ib_updates
goodboy 2022-01-23 15:20:43 -05:00 committed by GitHub
commit 242d02b1cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 265 additions and 161 deletions

View File

@ -25,6 +25,9 @@ from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QFrame,
QWidget,
QHBoxLayout,
QVBoxLayout,
QSplitter,
# QSizePolicy,
)
import numpy as np
@ -53,6 +56,7 @@ from ._style import (
)
from ..data.feed import Feed
from ..data._source import Symbol
from ..data._sharedmem import ShmArray
from ..log import get_logger
from ._interaction import ChartView
from ._forms import FieldsForm
@ -64,11 +68,11 @@ log = get_logger(__name__)
class GodWidget(QWidget):
'''
"Our lord and savior, the holy child of window-shua, there is no
widget above thee." - 6|6
widget above thee." - 6||6
The highest level composed widget which contains layouts for
organizing lower level charts as well as other widgets used to
control or modify them.
organizing charts as well as other sub-widgets used to control or
modify them.
'''
def __init__(
@ -80,19 +84,19 @@ class GodWidget(QWidget):
super().__init__(parent)
self.hbox = QtWidgets.QHBoxLayout(self)
self.hbox = QHBoxLayout(self)
self.hbox.setContentsMargins(0, 0, 0, 0)
self.hbox.setSpacing(6)
self.hbox.setAlignment(Qt.AlignTop)
self.vbox = QtWidgets.QVBoxLayout()
self.vbox = QVBoxLayout()
self.vbox.setContentsMargins(0, 0, 0, 0)
self.vbox.setSpacing(2)
self.vbox.setAlignment(Qt.AlignTop)
self.hbox.addLayout(self.vbox)
# self.toolbar_layout = QtWidgets.QHBoxLayout()
# self.toolbar_layout = QHBoxLayout()
# self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
# self.vbox.addLayout(self.toolbar_layout)
@ -106,25 +110,8 @@ class GodWidget(QWidget):
# assigned in the startup func `_async_main()`
self._root_n: trio.Nursery = None
def set_chart_symbol(
self,
symbol_key: str, # of form <fqsn>.<providername>
linkedsplits: 'LinkedSplits', # type: ignore
) -> None:
# re-sort org cache symbol list in LIFO order
cache = self._chart_cache
cache.pop(symbol_key, None)
cache[symbol_key] = linkedsplits
def get_chart_symbol(
self,
symbol_key: str,
) -> 'LinkedSplits': # type: ignore
return self._chart_cache.get(symbol_key)
# def init_timeframes_ui(self):
# self.tf_layout = QtWidgets.QHBoxLayout()
# self.tf_layout = QHBoxLayout()
# self.tf_layout.setSpacing(0)
# self.tf_layout.setContentsMargins(0, 12, 0, 0)
# time_frames = ('1M', '5M', '15M', '30M', '1H', '1D', '1W', 'MN')
@ -145,6 +132,23 @@ class GodWidget(QWidget):
# self.strategy_box = StrategyBoxWidget(self)
# self.toolbar_layout.addWidget(self.strategy_box)
def set_chart_symbol(
self,
symbol_key: str, # of form <fqsn>.<providername>
linkedsplits: 'LinkedSplits', # type: ignore
) -> None:
# re-sort org cache symbol list in LIFO order
cache = self._chart_cache
cache.pop(symbol_key, None)
cache[symbol_key] = linkedsplits
def get_chart_symbol(
self,
symbol_key: str,
) -> 'LinkedSplits': # type: ignore
return self._chart_cache.get(symbol_key)
async def load_symbol(
self,
@ -255,7 +259,7 @@ class ChartnPane(QFrame):
'''
sidepane: FieldsForm
hbox: QtWidgets.QHBoxLayout
hbox: QHBoxLayout
chart: Optional['ChartPlotWidget'] = None
def __init__(
@ -271,7 +275,7 @@ class ChartnPane(QFrame):
self.sidepane = sidepane
self.chart = None
hbox = self.hbox = QtWidgets.QHBoxLayout(self)
hbox = self.hbox = QHBoxLayout(self)
hbox.setAlignment(Qt.AlignTop | Qt.AlignLeft)
hbox.setContentsMargins(0, 0, 0, 0)
hbox.setSpacing(3)
@ -281,21 +285,14 @@ class ChartnPane(QFrame):
class LinkedSplits(QWidget):
'''
Widget that holds a central chart plus derived
subcharts computed from the original data set apart
by splitters for resizing.
Composite that holds a central chart plus a set of (derived)
subcharts (usually computed from the original data) arranged in
a splitter for resizing.
A single internal references to the data is maintained
for each chart and can be updated externally.
'''
long_pen = pg.mkPen('#006000')
long_brush = pg.mkBrush('#00ff00')
short_pen = pg.mkPen('#600000')
short_brush = pg.mkBrush('#ff0000')
zoomIsDisabled = QtCore.pyqtSignal(bool)
def __init__(
self,
@ -325,11 +322,11 @@ class LinkedSplits(QWidget):
# self.xaxis_ind.setStyle(showValues=False)
# self.xaxis.hide()
self.splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
self.splitter.setMidLineWidth(1)
self.splitter.setHandleWidth(0)
self.splitter = QSplitter(QtCore.Qt.Vertical)
self.splitter.setMidLineWidth(0)
self.splitter.setHandleWidth(2)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(self.splitter)
@ -341,20 +338,28 @@ class LinkedSplits(QWidget):
def set_split_sizes(
self,
# prop: float = 0.375, # proportion allocated to consumer subcharts
prop: float = 5/8,
prop: Optional[float] = None,
) -> None:
'''Set the proportion of space allocated for linked subcharts.
'''
ln = len(self.subplots)
if not prop:
# proportion allocated to consumer subcharts
if ln < 2:
prop = 1/(.666 * 6)
elif ln >= 2:
prop = 3/8
major = 1 - prop
min_h_ind = int((self.height() * prop) / len(self.subplots))
min_h_ind = int((self.height() * prop) / ln)
sizes = [int(self.height() * major)]
sizes.extend([min_h_ind] * len(self.subplots))
sizes.extend([min_h_ind] * ln)
self.splitter.setSizes(sizes) # , int(self.height()*0.2)
self.splitter.setSizes(sizes)
def focus(self) -> None:
if self.chart is not None:
@ -495,8 +500,9 @@ class LinkedSplits(QWidget):
cpw.plotItem.vb.linkedsplits = self
cpw.setFrameStyle(
QtWidgets.QFrame.StyledPanel
# | QtWidgets.QFrame.Plain)
# | QtWidgets.QFrame.Plain
)
cpw.hideButtons()
# XXX: gives us outline on backside of y-axis
@ -515,7 +521,22 @@ class LinkedSplits(QWidget):
cpw.draw_ohlc(name, array, array_key=array_key)
elif style == 'line':
cpw.draw_curve(name, array, array_key=array_key)
cpw.draw_curve(
name,
array,
array_key=array_key,
color='default_light',
)
elif style == 'step':
cpw.draw_curve(
name,
array,
array_key=array_key,
step_mode=True,
color='davies',
fill_color='davies',
)
else:
raise ValueError(f"Chart style {style} is currently unsupported")
@ -523,14 +544,7 @@ class LinkedSplits(QWidget):
if not _is_main:
# track by name
self.subplots[name] = cpw
# if sidepane:
# # TODO: use a "panes" collection to manage this?
# qframe.setMaximumWidth(self.chart.sidepane.width())
# qframe.setMinimumWidth(self.chart.sidepane.width())
self.splitter.addWidget(qframe)
# scale split regions
self.set_split_sizes()
@ -600,7 +614,7 @@ class ChartPlotWidget(pg.PlotWidget):
# parent=None,
# plotItem=None,
# antialias=True,
useOpenGL=True,
# useOpenGL=True,
**kwargs
)
self.name = name
@ -619,7 +633,8 @@ class ChartPlotWidget(pg.PlotWidget):
'ohlc': array,
}
self._graphics = {} # registry of underlying graphics
self._overlays = set() # registry of overlay curve names
# registry of overlay curve names
self._overlays: dict[str, ShmArray] = {}
self._feeds: dict[Symbol, Feed] = {}
@ -732,6 +747,7 @@ class ChartPlotWidget(pg.PlotWidget):
self._vb.setXRange(
min=l + 1,
max=r + 1,
# TODO: holy shit, wtf dude... why tf would this not be 0 by
# default... speechless.
padding=0,
@ -772,7 +788,7 @@ class ChartPlotWidget(pg.PlotWidget):
update_func=ContentsLabel.update_from_ohlc,
)
self._add_sticky(name)
self._add_sticky(name, bg_color='davies')
return graphics
@ -784,7 +800,7 @@ class ChartPlotWidget(pg.PlotWidget):
array_key: Optional[str] = None,
overlay: bool = False,
color: str = 'default_light',
color: Optional[str] = None,
add_label: bool = True,
**pdi_kwargs,
@ -794,15 +810,18 @@ class ChartPlotWidget(pg.PlotWidget):
the input array ``data``.
"""
_pdi_defaults = {
'pen': pg.mkPen(hcolor(color)),
}
pdi_kwargs.update(_pdi_defaults)
color = color or self.pen_color or 'default_light'
pdi_kwargs.update({
'color': color
})
data_key = array_key or name
# pg internals for reference.
# curve = pg.PlotDataItem(
# curve = pg.PlotCurveItem(
# yah, we wrote our own B)
curve = FastAppendCurve(
y=data[data_key],
x=data['index'],
@ -840,14 +859,14 @@ class ChartPlotWidget(pg.PlotWidget):
if overlay:
anchor_at = ('bottom', 'left')
self._overlays.add(name)
self._overlays[name] = None
else:
anchor_at = ('top', 'left')
# TODO: something instead of stickies for overlays
# (we need something that avoids clutter on x-axis).
self._add_sticky(name, bg_color='default_light')
self._add_sticky(name, bg_color=color)
if self.linked.cursor:
self.linked.cursor.add_curve_cursor(self, curve)
@ -861,6 +880,7 @@ class ChartPlotWidget(pg.PlotWidget):
return curve
# TODO: make this a ctx mngr
def _add_sticky(
self,
@ -941,16 +961,19 @@ class ChartPlotWidget(pg.PlotWidget):
def _set_yrange(
self,
*,
yrange: Optional[tuple[float, float]] = None,
range_margin: float = 0.06,
bars_range: Optional[tuple[int, int, int, int]] = None
) -> None:
"""Set the viewable y-range based on embedded data.
'''Set the viewable y-range based on embedded data.
This adds auto-scaling like zoom on the scroll wheel such
that data always fits nicely inside the current view of the
data set.
"""
'''
set_range = True
if self._static_yrange == 'axis':
@ -966,7 +989,20 @@ class ChartPlotWidget(pg.PlotWidget):
# Determine max, min y values in viewable x-range from data.
# Make sure min bars/datums on screen is adhered.
l, lbar, rbar, r = self.bars_range()
l, lbar, rbar, r = bars_range or self.bars_range()
# TODO: we need a loop for auto-scaled subplots to all
# be triggered by one another
if self.name != 'volume':
vlm_chart = self.linked.subplots.get('volume')
if vlm_chart:
vlm_chart._set_yrange(bars_range=(l, lbar, rbar, r))
# curve = vlm_chart._graphics['volume']
# if rbar - lbar < 1500:
# # print('small range')
# curve._fill = True
# else:
# curve._fill = False
# figure out x-range in view such that user can scroll "off"
# the data set up to the point where ``_min_points_to_show``
@ -1003,15 +1039,17 @@ class ChartPlotWidget(pg.PlotWidget):
a = self._arrays['ohlc']
ifirst = a[0]['index']
bars = a[lbar - ifirst:rbar - ifirst + 1]
if not len(bars):
# likely no data loaded yet or extreme scrolling?
log.error(f"WTF bars_range = {lbar}:{rbar}")
return
if self.data_key != self.linked.symbol.key:
bars = a[self.data_key]
bars = bars[self.data_key]
ylow = np.nanmin(bars)
yhigh = np.nanmax((bars))
yhigh = np.nanmax(bars)
# print(f'{(ylow, yhigh)}')
else:
# just the std ohlc bars
ylow = np.nanmin(bars['low'])
@ -1072,7 +1110,6 @@ class ChartPlotWidget(pg.PlotWidget):
# TODO: this should go onto some sort of
# data-view strimg thinger..right?
ohlc = self._shm.array
# ohlc = chart._shm.array
# XXX: not sure why the time is so off here
# looks like we're gonna have to do some fixing..

View File

@ -19,12 +19,13 @@ Real-time display tasks for charting / graphics.
'''
from contextlib import asynccontextmanager
# from pprint import pformat
import time
from typing import Any
from types import ModuleType
from typing import Optional
import numpy as np
from pydantic import BaseModel
from pydantic import create_model
import tractor
import trio
@ -56,12 +57,14 @@ _clear_throttle_rate: int = 58 # Hz
_book_throttle_rate: int = 16 # Hz
async def chart_from_quotes(
async def update_chart_from_quotes(
chart: ChartPlotWidget,
stream: tractor.MsgStream,
ohlcv: np.ndarray,
wap_in_history: bool = False,
vlm_chart: Optional[ChartPlotWidget] = None,
) -> None:
'''The 'main' (price) chart real-time update loop.
@ -76,7 +79,8 @@ async def chart_from_quotes(
# - handle odd lot orders
# - update last open price correctly instead
# of copying it from last bar's close
# - 5 sec bar lookback-autocorrection like tws does?
# - 1-5 sec bar lookback-autocorrection like tws does?
# (would require a background history checker task)
# update last price sticky
last_price_sticky = chart._ysticks[chart.name]
@ -84,6 +88,9 @@ async def chart_from_quotes(
*ohlcv.array[-1][['index', 'close']]
)
if vlm_chart:
vlm_sticky = vlm_chart._ysticks['volume']
def maxmin():
# TODO: implement this
# https://arxiv.org/abs/cs/0610046
@ -94,7 +101,7 @@ async def chart_from_quotes(
last_bars_range = chart.bars_range()
l, lbar, rbar, r = last_bars_range
in_view = array[lbar - ifirst:rbar - ifirst]
in_view = array[lbar - ifirst:rbar - ifirst + 1]
assert in_view.size
@ -105,11 +112,20 @@ 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, max(mn, 0)
mx_vlm_in_view = 0
if vlm_chart:
mx_vlm_in_view = np.max(in_view['volume'])
return last_bars_range, mx, max(mn, 0), mx_vlm_in_view
chart.default_view()
last_bars_range, last_mx, last_mn = maxmin()
(
last_bars_range,
last_mx,
last_mn,
last_mx_vlm,
) = maxmin()
last, volume = ohlcv.array[-1][['close', 'volume']]
@ -129,10 +145,10 @@ async def chart_from_quotes(
# - if trade volume jumps above / below prior L1 price
# levels this might be dark volume we need to
# present differently?
# present differently -> likely dark vlm
tick_size = chart.linked.symbol.tick_size
tick_margin = 2 * tick_size
tick_margin = 3 * tick_size
last_ask = last_bid = last_clear = time.time()
chart.show()
@ -148,9 +164,40 @@ async def chart_from_quotes(
now = time.time()
# brange, mx_in_view, mn_in_view = maxmin()
(
brange,
mx_in_view,
mn_in_view,
mx_vlm_in_view,
) = maxmin()
l, lbar, rbar, r = brange
mx = mx_in_view + tick_margin
mn = mn_in_view - tick_margin
# NOTE: vlm may be written by the ``brokerd`` backend
# event though a tick sample is not emitted.
# TODO: show dark trades differently
# https://github.com/pikers/piker/issues/116
array = ohlcv.array
if vlm_chart:
# print(f"volume: {end['volume']}")
vlm_chart.update_curve_from_array('volume', array)
vlm_sticky.update_from_data(*array[-1][['index', 'volume']])
if (
mx_vlm_in_view != last_mx_vlm or
mx_vlm_in_view > last_mx_vlm
):
# print(f'mx vlm: {last_mx_vlm} -> {mx_vlm_in_view}')
vlm_chart._set_yrange(yrange=(0, mx_vlm_in_view * 1.375))
last_mx_vlm = mx_vlm_in_view
for tick in quote.get('ticks', ()):
# print(f"CHART: {quote['symbol']}: {tick}")
# log.info(
# f"quotes: {pformat(quote['symbol'])}: {pformat(tick)}")
ticktype = tick.get('type')
price = tick.get('price')
size = tick.get('size')
@ -172,16 +219,13 @@ async def chart_from_quotes(
# set time of last graphics update
last_clear = now
array = ohlcv.array
# update price sticky(s)
end = array[-1]
last_price_sticky.update_from_data(
*end[['index', 'close']]
)
# plot bars
# update price bar
# update ohlc sampled price bars
chart.update_ohlc_from_array(
chart.name,
array,
@ -214,11 +258,6 @@ async def chart_from_quotes(
# 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'):
@ -242,16 +281,14 @@ async def chart_from_quotes(
elif ticktype in ('bid', 'bsize'):
l1.bid_label.update_fields({'level': price, 'size': size})
# 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
# in view y-range checking for auto-scale
# update the max/min price in view to keep bid/ask on screen
mx = max(price + tick_margin, mx)
mn = min(price - tick_margin, mn)
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
@ -345,45 +382,51 @@ async def fan_out_spawn_fsp_daemons(
# blocks here until all fsp actors complete
class FspConfig(BaseModel):
class Config:
validate_assignment = True
name: str
period: int
@asynccontextmanager
async def open_sidepane(
linked: LinkedSplits,
display_name: str,
conf: dict[str, dict[str, str]],
) -> FspConfig:
) -> FieldsForm:
schema = {}
assert len(conf) == 1 # for now
# add (single) selection widget
for display_name, config in conf.items():
schema[display_name] = {
'label': '**fsp**:',
'type': 'select',
'default_value': [display_name],
}
# add parameters for selection "options"
defaults = config.get('params', {})
for name, default in defaults.items():
# add to ORM schema
schema.update({
name: {
'label': f'**{name}**:',
'type': 'edit',
'default_value': default,
},
})
sidepane: FieldsForm = mk_form(
parent=linked.godwidget,
fields_schema={
'name': {
'label': '**fsp**:',
'type': 'select',
'default_value': [
f'{display_name}'
],
},
fields_schema=schema,
)
# TODO: generate this from input map
'period': {
'label': '**period**:',
'type': 'edit',
'default_value': 14,
},
},
)
sidepane.model = FspConfig(
# https://pydantic-docs.helpmanual.io/usage/models/#dynamic-model-creation
FspConfig = create_model(
'FspConfig',
name=display_name,
period=14,
**defaults,
)
sidepane.model = FspConfig()
# just a logger for now until we get fsp configs up and running.
async def settings_change(key: str, value: str) -> bool:
@ -410,7 +453,7 @@ async def run_fsp(
src_shm: ShmArray,
fsp_func_name: str,
display_name: str,
conf: dict[str, Any],
conf: dict[str, dict],
group_status_key: str,
loglevel: str,
@ -444,7 +487,7 @@ async def run_fsp(
ctx.open_stream() as stream,
open_sidepane(
linkedsplits,
display_name,
{display_name: conf},
) as sidepane,
):
@ -453,9 +496,10 @@ async def run_fsp(
if conf.get('overlay'):
chart = linkedsplits.chart
chart.draw_curve(
name='vwap',
name=display_name,
data=shm.array,
overlay=True,
color='default_light',
)
last_val_sticky = None
@ -658,22 +702,25 @@ async def maybe_open_vlm_display(
) -> ChartPlotWidget:
# make sure that the instrument supports volume history
# (sometimes this is not the case for some commodities and
# derivatives)
# volm = ohlcv.array['volume']
# if (
# np.all(np.isin(volm, -1)) or
# np.all(np.isnan(volm))
# ):
if not has_vlm(ohlcv):
log.warning(f"{linked.symbol.key} does not seem to have volume info")
yield
return
else:
async with open_sidepane(linked, 'volume') as sidepane:
async with open_sidepane(
linked, {
'volume': {
'params': {
'price_func': 'ohl3'
}
}
},
) as sidepane:
# built-in $vlm
shm = ohlcv
chart = linked.add_plot(
name='vlm',
name='volume',
array=shm.array,
array_key='volume',
@ -682,9 +729,13 @@ async def maybe_open_vlm_display(
# curve by default
ohlc=False,
# vertical bars
# Draw vertical bars from zero.
# we do this internally ourselves since
# the curve item internals are pretty convoluted.
style='step',
# original pyqtgraph flag for reference
# stepMode=True,
# static_yrange=(0, 100),
)
# XXX: ONLY for sub-chart fsps, overlays have their
@ -703,9 +754,23 @@ async def maybe_open_vlm_display(
last_val_sticky.update_from_data(-1, value)
chart.update_curve_from_array(
'volume',
shm.array,
)
# size view to data once at outset
chart._set_yrange()
# size pain to parent chart
# TODO: this appears to nearly fix a bug where the vlm sidepane
# could be sized correctly nearly immediately (since the
# order pane is already sized), right now it doesn't seem to
# fully align until the VWAP fsp-actor comes up...
await trio.sleep(0)
chart.linked.resize_sidepanes()
await trio.sleep(0)
yield chart
@ -803,22 +868,13 @@ async def display_symbol_data(
# TODO: eventually we'll support some kind of n-compose syntax
fsp_conf = {
'rsi': {
'fsp_func_name': 'rsi',
'period': 14,
'chart_kwargs': {
'static_yrange': (0, 100),
},
},
# # test for duplicate fsps on same chart
# 'rsi2': {
# 'rsi': {
# 'fsp_func_name': 'rsi',
# 'period': 14,
# 'params': {'period': 14},
# 'chart_kwargs': {
# 'static_yrange': (0, 100),
# },
# },
}
if has_vlm(ohlcv):
@ -831,9 +887,16 @@ async def display_symbol_data(
},
})
async with (
# NOTE: we must immediately tell Qt to show the OHLC chart
# to avoid a race where the subplots get added/shown to
# the linked set *before* the main price chart!
linkedsplits.show()
linkedsplits.focus()
await trio.sleep(0)
async with (
trio.open_nursery() as ln,
maybe_open_vlm_display(linkedsplits, ohlcv) as vlm_chart,
):
# load initial fsp chain (otherwise known as "indicators")
ln.start_soon(
@ -849,11 +912,12 @@ async def display_symbol_data(
# start graphics update loop(s)after receiving first live quote
ln.start_soon(
chart_from_quotes,
update_chart_from_quotes,
chart,
feed.stream,
ohlcv,
wap_in_history,
vlm_chart,
)
ln.start_soon(
@ -864,10 +928,6 @@ async def display_symbol_data(
)
async with (
# XXX: this slipped in during a commits refacotr,
# it's actually landing proper in #231
# maybe_open_vlm_display(linkedsplits, ohlcv),
open_order_mode(
feed,
chart,

View File

@ -205,19 +205,26 @@ def hcolor(name: str) -> str:
'svags': '#0a0e14',
# fifty shades
'original': '#a9a9a9',
'gray': '#808080', # like the kick
'grayer': '#4c4c4c',
'grayest': '#3f3f3f',
'i3': '#494D4F',
'jet': '#343434',
'cadet': '#91A3B0',
'marengo': '#91A3B0',
'charcoal': '#36454F',
'gunmetal': '#91A3B0',
'battleship': '#848482',
'davies': '#555555',
# bluish
'charcoal': '#36454F',
# default bars
'bracket': '#666666', # like the logo
'original': '#a9a9a9',
# work well for filled polygons which want a 'bracket' feel
# going light to dark
'davies': '#555555',
'i3': '#494D4F',
'jet': '#343434',
# from ``qdarkstyle`` palette
'default_darkest': DarkPalette.COLOR_BACKGROUND_1,