Compare commits

..

10 Commits

Author SHA1 Message Date
Tyler Goodlet bb9f6475f4 Add disclaimer 2020-09-01 12:50:13 -04:00
Tyler Goodlet 54463b3595 Drop to 1k bars on init load 2020-09-01 12:46:30 -04:00
Tyler Goodlet aaf234cbaf Better bg color, tweak margins. 2020-08-31 17:18:35 -04:00
Tyler Goodlet 0f6589d9ff Add proper x-axis time-stamping 2020-08-31 17:18:02 -04:00
Tyler Goodlet 30d8e096c6 Use dashed crosshair, simplify x-axis alloc 2020-08-31 17:17:20 -04:00
Tyler Goodlet 19609178ce Even more colors 2020-08-31 17:16:44 -04:00
Tyler Goodlet 4c39407363 Use dashed lines for crosshair 2020-08-30 12:32:14 -04:00
Tyler Goodlet a345daa522 Try to find cad stocks 2020-08-30 12:31:32 -04:00
Tyler Goodlet ea75281cbc Add and update y-sticky labels on new price data 2020-08-30 12:29:29 -04:00
Tyler Goodlet 86a1f33abb Start color map 2020-08-30 12:28:38 -04:00
6 changed files with 200 additions and 81 deletions

View File

@ -1,5 +1,5 @@
# piker: trading gear for hackers. # piker: trading gear for hackers.
# Copyright 2018 Tyler Goodlet # Copyright 2018-forever Tyler Goodlet and p0
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by # it under the terms of the GNU Affero General Public License as published by
@ -16,6 +16,23 @@
""" """
piker: trading gear for hackers. piker: trading gear for hackers.
If your boss told you to checkout this code base, stop, right the fuck now.
Tell your boss this code base is absolute shit and you'll never be able to
make it "work" due to scaling and that Python is too slow. If you even for
a second suggest to them that it's useful you'll be hearing from us.
If you're a yabf (yet another backtesting framework) author/maintainer, also stop.
We don't slurp callback soup; your code can be called from any FSP routine and
``piker`` requires no adoption of your "framework". If you want to provide an
integration, cool, but we aren't going to adopt any oudated concurrency models.
This code is for **you** as a person to defend yourself from our messed
up finacial system and the charlatans and phonies who run it.
``piker`` will never be for sale nor influenced by suits of any type.
Join us, let's make the best software linux has ever seen.
""" """
import msgpack # noqa import msgpack # noqa
import msgpack_numpy import msgpack_numpy

View File

@ -142,7 +142,7 @@ class Client:
# durationStr='1 D', # durationStr='1 D',
# time length calcs # time length calcs
durationStr='{count} S'.format(count=2000 * 5), durationStr='{count} S'.format(count=1000 * 5),
barSizeSetting='5 secs', barSizeSetting='5 secs',
# always use extended hours # always use extended hours
@ -225,18 +225,22 @@ class Client:
# use heuristics to figure out contract "type" # use heuristics to figure out contract "type"
sym, exch = symbol.upper().split('.') sym, exch = symbol.upper().split('.')
# TODO: metadata system for all these exchange rules.. # futes
if exch in ('PURE',):
currency = 'CAD'
if exch in ('GLOBEX', 'NYMEX', 'CME', 'CMECRYPTO'): if exch in ('GLOBEX', 'NYMEX', 'CME', 'CMECRYPTO'):
con = await self.get_cont_fute(symbol=sym, exchange=exch) con = await self.get_cont_fute(symbol=sym, exchange=exch)
# commodities
elif exch == 'CMDTY': # eg. XAUUSD.CMDTY elif exch == 'CMDTY': # eg. XAUUSD.CMDTY
con_kwargs, bars_kwargs = _adhoc_cmdty_data_map[sym] con_kwargs, bars_kwargs = _adhoc_cmdty_data_map[sym]
con = ibis.Commodity(**con_kwargs) con = ibis.Commodity(**con_kwargs)
con.bars_kwargs = bars_kwargs con.bars_kwargs = bars_kwargs
# stonks
else: else:
# TODO: metadata system for all these exchange rules..
if exch in ('PURE', 'TSE'): # non-yankee
currency = 'CAD'
con = ibis.Stock(symbol=sym, exchange=exch, currency=currency) con = ibis.Stock(symbol=sym, exchange=exch, currency=currency)
try: try:
@ -454,6 +458,8 @@ def normalize(
return data return data
# TODO: figure out how to share quote feeds sanely despite
# the wacky ``ib_insync`` api.
# @tractor.msg.pub # @tractor.msg.pub
async def stream_quotes( async def stream_quotes(
symbols: List[str], symbols: List[str],

View File

@ -1,12 +1,17 @@
""" """
Chart axes graphics and behavior. Chart axes graphics and behavior.
""" """
import time
from functools import partial
from typing import List
# import numpy as np
import pandas as pd
import pyqtgraph as pg import pyqtgraph as pg
from PyQt5 import QtCore, QtGui from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import QPointF from PyQt5.QtCore import QPointF
# from .quantdom.base import Quotes
from .quantdom.utils import fromtimestamp from .quantdom.utils import fromtimestamp
from ._style import _font, hcolor from ._style import _font, hcolor
@ -15,7 +20,6 @@ class PriceAxis(pg.AxisItem):
def __init__( def __init__(
self, self,
# chart: 'ChartPlotWidget',
) -> None: ) -> None:
super().__init__(orientation='right') super().__init__(orientation='right')
self.setStyle(**{ self.setStyle(**{
@ -28,10 +32,7 @@ class PriceAxis(pg.AxisItem):
}) })
self.setLabel(**{'font-size': '10pt'}) self.setLabel(**{'font-size': '10pt'})
self.setTickFont(_font) self.setTickFont(_font)
self.setWidth(150) self.setWidth(125)
# self.chart = chart
# accesed normally via
# .getAxis('right')
# XXX: drop for now since it just eats up h space # XXX: drop for now since it just eats up h space
@ -43,9 +44,20 @@ class PriceAxis(pg.AxisItem):
class DynamicDateAxis(pg.AxisItem): class DynamicDateAxis(pg.AxisItem):
tick_tpl = {'D1': '%Y-%b-%d'} # time formats mapped by seconds between bars
tick_tpl = {
60*60*24: '%Y-%b-%d',
60: '%H:%M',
30: '%H:%M:%S',
5: '%H:%M:%S',
}
def __init__(self, linked_charts, *args, **kwargs): def __init__(
self,
linked_charts,
*args,
**kwargs
) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.linked_charts = linked_charts self.linked_charts = linked_charts
self.setTickFont(_font) self.setTickFont(_font)
@ -59,24 +71,25 @@ class DynamicDateAxis(pg.AxisItem):
) )
# self.setHeight(35) # self.setHeight(35)
def tickStrings(self, values, scale, spacing): def _indexes_to_timestrs(
# if len(values) > 1 or not values: self,
# values = Quotes.time indexes: List[int],
) -> List[str]:
# strings = super().tickStrings(values, scale, spacing)
s_period = 'D1'
strings = []
bars = self.linked_charts.chart._array bars = self.linked_charts.chart._array
quotes_count = len(bars) - 1 times = bars['time']
bars_len = len(bars)
delay = times[-1] - times[times != times[-1]][-1]
for ibar in values: epochs = times[list(
if ibar > quotes_count: map(int, filter(lambda i: i < bars_len, indexes))
return strings )]
dt_tick = fromtimestamp(bars[int(ibar)]['time']) # TODO: **don't** have this hard coded shift to EST
strings.append( dts = pd.to_datetime(epochs, unit='s') - 4*pd.offsets.Hour()
dt_tick.strftime(self.tick_tpl[s_period]) return dts.strftime(self.tick_tpl[delay])
)
return strings
def tickStrings(self, values: List[float], scale, spacing):
return self._indexes_to_timestrs(values)
class AxisLabel(pg.GraphicsObject): class AxisLabel(pg.GraphicsObject):
@ -88,7 +101,7 @@ class AxisLabel(pg.GraphicsObject):
def __init__( def __init__(
self, self,
parent=None, parent=None,
digits=1, digits=2,
color=None, color=None,
opacity=1, opacity=1,
**kwargs **kwargs
@ -128,6 +141,7 @@ class AxisLabel(pg.GraphicsObject):
p.drawText(option.rect, self.text_flags, self.label_str) p.drawText(option.rect, self.text_flags, self.label_str)
# uggggghhhh # uggggghhhh
def tick_to_string(self, tick_pos): def tick_to_string(self, tick_pos):
raise NotImplementedError() raise NotImplementedError()
@ -160,24 +174,20 @@ class XAxisLabel(AxisLabel):
) )
# text_flags = _common_text_flags # text_flags = _common_text_flags
def tick_to_string(self, tick_pos):
# TODO: change to actual period
tpl = self.parent.tick_tpl['D1']
bars = self.parent.linked_charts.chart._array
if tick_pos > len(bars):
return 'Unknown Time'
return fromtimestamp(bars[round(tick_pos)]['time']).strftime(tpl)
def boundingRect(self): # noqa def boundingRect(self): # noqa
return QtCore.QRectF(0, 0, 145, 40) # TODO: we need to get the parent axe's dimensions transformed
# to abs coords to be 100% correct here:
# self.parent.boundingRect()
return QtCore.QRectF(0, 0, 100, 31)
def update_label(self, abs_pos, data): def update_label(
# ibar = view_pos.x() self,
# if ibar > self.quotes_count: abs_pos: QPointF, # scene coords
# return data: float, # data for text
self.label_str = self.tick_to_string(data) offset: int = 0 # if have margins, k?
) -> None:
self.label_str = self.parent._indexes_to_timestrs([int(data)])[0]
width = self.boundingRect().width() width = self.boundingRect().width()
offset = 0 # if have margins
new_pos = QPointF(abs_pos.x() - width / 2 - offset, 0) new_pos = QPointF(abs_pos.x() - width / 2 - offset, 0)
self.setPos(new_pos) self.setPos(new_pos)
@ -202,10 +212,10 @@ class YAxisLabel(AxisLabel):
self, self,
abs_pos: QPointF, # scene coords abs_pos: QPointF, # scene coords
data: float, # data for text data: float, # data for text
offset: int = 0 # if have margins, k?
) -> None: ) -> None:
self.label_str = self.tick_to_string(data) self.label_str = self.tick_to_string(data)
height = self.boundingRect().height() height = self.boundingRect().height()
offset = 0 # if have margins
new_pos = QPointF(0, abs_pos.y() - height / 2 - offset) new_pos = QPointF(0, abs_pos.y() - height / 2 - offset)
self.setPos(new_pos) self.setPos(new_pos)

View File

@ -15,7 +15,8 @@ from ._axes import (
PriceAxis, PriceAxis,
) )
from ._graphics import CrossHair, BarItems from ._graphics import CrossHair, BarItems
from ._style import _xaxis_at, _min_points_to_show from ._axes import YSticky
from ._style import _xaxis_at, _min_points_to_show, hcolor
from ._source import Symbol from ._source import Symbol
from .. import brokers from .. import brokers
from .. import data from .. import data
@ -29,7 +30,7 @@ from .. import fsp
log = get_logger(__name__) log = get_logger(__name__)
# margins # margins
CHART_MARGINS = (0, 0, 10, 3) CHART_MARGINS = (0, 0, 5, 3)
class ChartSpace(QtGui.QWidget): class ChartSpace(QtGui.QWidget):
@ -42,7 +43,7 @@ class ChartSpace(QtGui.QWidget):
self.v_layout = QtGui.QVBoxLayout(self) self.v_layout = QtGui.QVBoxLayout(self)
self.v_layout.setContentsMargins(0, 0, 0, 0) self.v_layout.setContentsMargins(0, 0, 0, 0)
self.toolbar_layout = QtGui.QHBoxLayout() self.toolbar_layout = QtGui.QHBoxLayout()
self.toolbar_layout.setContentsMargins(10, 10, 15, 0) self.toolbar_layout.setContentsMargins(5, 5, 10, 0)
self.h_layout = QtGui.QHBoxLayout() self.h_layout = QtGui.QHBoxLayout()
# self.init_timeframes_ui() # self.init_timeframes_ui()
@ -225,7 +226,6 @@ class LinkedSplitCharts(QtGui.QWidget):
cpw.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain) cpw.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
cpw.getPlotItem().setContentsMargins(*CHART_MARGINS) cpw.getPlotItem().setContentsMargins(*CHART_MARGINS)
# self.splitter.addWidget(cpw)
# link chart x-axis to main quotes chart # link chart x-axis to main quotes chart
cpw.setXLink(self.chart) cpw.setXLink(self.chart)
@ -246,6 +246,9 @@ class LinkedSplitCharts(QtGui.QWidget):
# scale split regions # scale split regions
self.set_split_sizes() self.set_split_sizes()
# XXX: we need this right?
# self.splitter.addWidget(cpw)
return cpw return cpw
@ -272,16 +275,19 @@ class ChartPlotWidget(pg.PlotWidget):
# the data view we generate graphics from # the data view we generate graphics from
array: np.ndarray, array: np.ndarray,
**kwargs, **kwargs,
# parent=None,
# background='default',
# plotItem=None,
): ):
"""Configure chart display settings. """Configure chart display settings.
""" """
super().__init__(**kwargs) super().__init__(
background=hcolor('papas_special'),
# parent=None,
# plotItem=None,
**kwargs
)
self._array = array # readonly view of data self._array = array # readonly view of data
self._graphics = {} # registry of underlying graphics self._graphics = {} # registry of underlying graphics
self._labels = {} # registry of underlying graphics self._labels = {} # registry of underlying graphics
self._ysticks = {} # registry of underlying graphics
# show only right side axes # show only right side axes
self.hideAxis('left') self.hideAxis('left')
@ -350,6 +356,7 @@ class ChartPlotWidget(pg.PlotWidget):
self._graphics[name] = graphics self._graphics[name] = graphics
# XXX: How to stack labels vertically? # XXX: How to stack labels vertically?
# Ogi says: "
label = pg.LabelItem( label = pg.LabelItem(
justify='left', justify='left',
size='5pt', size='5pt',
@ -373,6 +380,8 @@ class ChartPlotWidget(pg.PlotWidget):
# show last 50 points on startup # show last 50 points on startup
self.plotItem.vb.setXRange(xlast - 50, xlast + 50) self.plotItem.vb.setXRange(xlast - 50, xlast + 50)
self._add_sticky(name)
return graphics return graphics
def draw_curve( def draw_curve(
@ -421,6 +430,21 @@ class ChartPlotWidget(pg.PlotWidget):
return curve return curve
def _add_sticky(
self,
name: str,
# retreive: Callable[None, np.ndarray],
) -> YSticky:
# add y-axis "last" value label
last = self._ysticks['last'] = YSticky(
chart=self,
parent=self.getAxis('right'),
# digits=0,
opacity=1,
color=pg.mkPen(hcolor('gray'))
)
return last
def update_from_array( def update_from_array(
self, self,
name: str, name: str,
@ -636,6 +660,10 @@ async def _async_main(
loglevel, loglevel,
) )
# update last price sticky
last = chart._ysticks['last']
last.update_from_data(*chart._array[-1][['index', 'close']])
# graphics update loop # graphics update loop
async with data.open_feed( async with data.open_feed(
@ -646,7 +674,7 @@ async def _async_main(
# wait for a first quote before we start any update tasks # wait for a first quote before we start any update tasks
quote = await stream.__anext__() quote = await stream.__anext__()
print(f'RECEIVED FIRST QUOTE {quote}') log.info(f'RECEIVED FIRST QUOTE {quote}')
# start graphics tasks after receiving first live quote # start graphics tasks after receiving first live quote
n.start_soon(add_new_bars, delay, linked_charts) n.start_soon(add_new_bars, delay, linked_charts)
@ -675,6 +703,10 @@ async def _async_main(
chart.name, chart.name,
chart._array, chart._array,
) )
# update sticky(s)
last = chart._ysticks['last']
last.update_from_data(
*chart._array[-1][['index', 'close']])
chart._set_yrange() chart._set_yrange()

View File

@ -10,7 +10,7 @@ from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import QLineF from PyQt5.QtCore import QLineF
from .quantdom.utils import timeit from .quantdom.utils import timeit
from ._style import _xaxis_at # , _tina_mode from ._style import _xaxis_at, hcolor
from ._axes import YAxisLabel, XAxisLabel from ._axes import YAxisLabel, XAxisLabel
@ -18,17 +18,28 @@ from ._axes import YAxisLabel, XAxisLabel
# - checkout pyqtgraph.PlotCurveItem.setCompositionMode # - checkout pyqtgraph.PlotCurveItem.setCompositionMode
_mouse_rate_limit = 30 _mouse_rate_limit = 30
_debounce_delay = 10
_ch_label_opac = 1
class CrossHair(pg.GraphicsObject): class CrossHair(pg.GraphicsObject):
def __init__( def __init__(
self, self,
linkedsplitcharts: 'LinkedSplitCharts', linkedsplitcharts: 'LinkedSplitCharts', # noqa
digits: int = 0 digits: int = 0
) -> None: ) -> None:
super().__init__() super().__init__()
self.pen = pg.mkPen('#a9a9a9') # gray? # XXX: not sure why these are instance variables?
# It's not like we can change them on the fly..?
self.pen = pg.mkPen(
color=hcolor('default'),
style=QtCore.Qt.DashLine,
)
self.lines_pen = pg.mkPen(
color='#a9a9a9', # gray?
style=QtCore.Qt.DashLine,
)
self.lsc = linkedsplitcharts self.lsc = linkedsplitcharts
self.graphics = {} self.graphics = {}
self.plots = [] self.plots = []
@ -42,12 +53,13 @@ class CrossHair(pg.GraphicsObject):
) -> None: ) -> None:
# add ``pg.graphicsItems.InfiniteLine``s # add ``pg.graphicsItems.InfiniteLine``s
# vertical and horizonal lines and a y-axis label # vertical and horizonal lines and a y-axis label
vl = plot.addLine(x=0, pen=self.pen, movable=False) vl = plot.addLine(x=0, pen=self.lines_pen, movable=False)
hl = plot.addLine(y=0, pen=self.pen, movable=False) hl = plot.addLine(y=0, pen=self.lines_pen, movable=False)
yl = YAxisLabel( yl = YAxisLabel(
parent=plot.getAxis('right'), parent=plot.getAxis('right'),
digits=digits or self.digits, digits=digits or self.digits,
opacity=1 opacity=_ch_label_opac,
color=self.pen,
) )
# TODO: checkout what ``.sigDelayed`` can be used for # TODO: checkout what ``.sigDelayed`` can be used for
@ -55,17 +67,20 @@ class CrossHair(pg.GraphicsObject):
px_moved = pg.SignalProxy( px_moved = pg.SignalProxy(
plot.scene().sigMouseMoved, plot.scene().sigMouseMoved,
rateLimit=_mouse_rate_limit, rateLimit=_mouse_rate_limit,
slot=self.mouseMoved slot=self.mouseMoved,
delay=_debounce_delay,
) )
px_enter = pg.SignalProxy( px_enter = pg.SignalProxy(
plot.sig_mouse_enter, plot.sig_mouse_enter,
rateLimit=_mouse_rate_limit, rateLimit=_mouse_rate_limit,
slot=lambda: self.mouseAction('Enter', plot), slot=lambda: self.mouseAction('Enter', plot),
delay=_debounce_delay,
) )
px_leave = pg.SignalProxy( px_leave = pg.SignalProxy(
plot.sig_mouse_leave, plot.sig_mouse_leave,
rateLimit=_mouse_rate_limit, rateLimit=_mouse_rate_limit,
slot=lambda: self.mouseAction('Leave', plot), slot=lambda: self.mouseAction('Leave', plot),
delay=_debounce_delay,
) )
self.graphics[plot] = { self.graphics[plot] = {
'vl': vl, 'vl': vl,
@ -75,18 +90,16 @@ class CrossHair(pg.GraphicsObject):
} }
self.plots.append(plot) self.plots.append(plot)
# determine where to place x-axis label # Determine where to place x-axis label.
if _xaxis_at == 'bottom': # Place below the last plot by default, ow
# place below the last plot
self.xaxis_label = XAxisLabel(
parent=self.plots[-1].getAxis('bottom'),
opacity=1
)
else:
# keep x-axis right below main chart # keep x-axis right below main chart
first = self.plots[0] plot_index = -1 if _xaxis_at == 'bottom' else 0
xaxis = first.getAxis('bottom')
self.xaxis_label = XAxisLabel(parent=xaxis, opacity=1) self.xaxis_label = XAxisLabel(
parent=self.plots[plot_index].getAxis('bottom'),
opacity=_ch_label_opac,
color=self.pen,
)
def mouseAction(self, action, plot): # noqa def mouseAction(self, action, plot): # noqa
if action == 'Enter': if action == 'Enter':
@ -100,7 +113,10 @@ class CrossHair(pg.GraphicsObject):
self.graphics[plot]['yl'].hide() self.graphics[plot]['yl'].hide()
self.active_plot = None self.active_plot = None
def mouseMoved(self, evt): # noqa def mouseMoved(
self,
evt: 'Tuple[QMouseEvent]', # noqa
) -> None: # noqa
"""Update horizonal and vertical lines when mouse moves inside """Update horizonal and vertical lines when mouse moves inside
either the main chart or any indicator subplot. either the main chart or any indicator subplot.
""" """
@ -108,6 +124,7 @@ class CrossHair(pg.GraphicsObject):
# find position inside active plot # find position inside active plot
try: try:
# map to view coordinate system
mouse_point = self.active_plot.mapToView(pos) mouse_point = self.active_plot.mapToView(pos)
except AttributeError: except AttributeError:
# mouse was not on active plot # mouse was not on active plot
@ -120,7 +137,7 @@ class CrossHair(pg.GraphicsObject):
self.graphics[plot]['hl'].setY(y) self.graphics[plot]['hl'].setY(y)
self.graphics[self.active_plot]['yl'].update_label( self.graphics[self.active_plot]['yl'].update_label(
evt_post=pos, point_view=mouse_point abs_pos=pos, data=y
) )
for plot, opts in self.graphics.items(): for plot, opts in self.graphics.items():
# move the vertical line to the current x # move the vertical line to the current x
@ -131,8 +148,8 @@ class CrossHair(pg.GraphicsObject):
# update the label on the bottom of the crosshair # update the label on the bottom of the crosshair
self.xaxis_label.update_label( self.xaxis_label.update_label(
evt_post=pos, abs_pos=pos,
point_view=mouse_point data=x
) )
def boundingRect(self): def boundingRect(self):
@ -226,7 +243,7 @@ class BarItems(pg.GraphicsObject):
# 0.5 is no overlap between arms, 1.0 is full overlap # 0.5 is no overlap between arms, 1.0 is full overlap
w: float = 0.43 w: float = 0.43
bull_pen = pg.mkPen('#808080') bull_pen = pg.mkPen(hcolor('gray'))
# XXX: tina mode, see below # XXX: tina mode, see below
# bull_brush = pg.mkPen('#00cc00') # bull_brush = pg.mkPen('#00cc00')
@ -244,6 +261,9 @@ class BarItems(pg.GraphicsObject):
# track the current length of drawable lines within the larger array # track the current length of drawable lines within the larger array
self.index: int = 0 self.index: int = 0
def last_value(self) -> QLineF:
return self.lines[self.index - 1]['rarm']
@timeit @timeit
def draw_from_data( def draw_from_data(
self, self,

View File

@ -3,6 +3,7 @@ Qt UI styling.
""" """
import pyqtgraph as pg import pyqtgraph as pg
from PyQt5 import QtGui from PyQt5 import QtGui
from qdarkstyle.palette import DarkPalette
# chart-wide font # chart-wide font
@ -27,3 +28,36 @@ def enable_tina_mode() -> None:
""" """
# white background (for tinas like our pal xb) # white background (for tinas like our pal xb)
pg.setConfigOption('background', 'w') pg.setConfigOption('background', 'w')
def hcolor(name: str) -> str:
"""Hex color codes by hipster speak.
"""
return {
# lives matter
'black': '#000000',
'erie_black': '#1B1B1B',
'licorice': '#1A1110',
'papas_special': '#06070c',
# fifty shades
'gray': '#808080', # like the kick
'jet': '#343434',
'charcoal': '#36454F',
# palette
'default': DarkPalette.COLOR_BACKGROUND_NORMAL,
'white': '#ffffff', # for tinas and sunbathers
# blue zone
'dad_blue': '#326693', # like his shirt
'vwap_blue': '#0582fb',
'dodger_blue': '#1e90ff', # like the team?
'panasonic_blue': '#0040be', # from japan
# traditional
'tina_green': '#00cc00',
'tina_red': '#fa0000',
}[name]