Dear community! I have a question about Dash operation and prepared minimal working examples showing what works and what doesn’t (I hope someone will help me to fix this). So, there will be two topics connected one with another. Let’s start from simpler one. This is my code generating random candlestick plot which works (it’s funny that the code behavior differs when the Dash server is run from python IDLE and from cmd, I tested this only on Windows 7):
from dash import Dash, dcc, html, Input, Output, State
from dash.exceptions import PreventUpdate
import dash_bootstrap_components as dbc
import plotly.graph_objects as go
import numpy as np
import time
from threading import Thread, Timer, Lock, Condition
import plotly.graph_objs as go
sendPeriodMs = 200
cbPeriodMS = 75
np.random.seed( 2 )
class Candle():
def __init__( self, _o = .0, _c = .0, _h = .0, _l = .0, _v = 0, _x = 0 ):
self.o = _o
self.c = _c
self.h = _h
self.l = _l
self.v = _v
self.x = _x
class RandomCandle():
def __init__( self, _start, _stepLen, _maxTicksInCandle, _avgQtyPerTick ):
self.price = _start
self.stepLen = _stepLen
self.maxTicksInCandle = _maxTicksInCandle
self.avgQtyPerTick = _avgQtyPerTick
self.stepsDone = 0
self.maxTicksInThisCandle = 0
self.ticksInThisCandle = 0
self.ticks = []
self.qty = 0
self.x = -1
def generate( self ):
if self.ticksInThisCandle == 0:
self.ticks.clear()
self.maxTicksInThisCandle = np.random.choice(
range( self.maxTicksInCandle // 4,
self.maxTicksInCandle ) )
numSteps = 0
while numSteps == 0:
numSteps = np.random.choice( range( -7, 8 ) )
self.price += numSteps * self.stepLen
self.ticks = [ self.price ]
_q = 0
while _q == 0:
_q = np.random.choice( self.avgQtyPerTick * abs( numSteps ) )
self.qty = _q
self.ticksInThisCandle += 1
self.x += 1
else:
numSteps = 0
while numSteps == 0:
numSteps = np.random.choice( range( -7, 8 ) )
_q = 0
while _q == 0:
_q = np.random.choice( self.avgQtyPerTick * abs( numSteps ) )
self.qty += _q
nextPrice = self.price + numSteps * self.stepLen
self.ticks.append( nextPrice )
self.price = nextPrice
self.ticksInThisCandle += 1
_o = self.ticks[0]
_c = self.ticks[-1]
_h = max( self.ticks )
_l = min( self.ticks )
if self.ticksInThisCandle == self.maxTicksInThisCandle:
self.ticksInThisCandle = 0
return Candle( _o, _c, _h, _l, _q, self.x )
class CandleList():
def __init__( self ):
self.data = []
self.readPosition = 0
self.mutex = Lock()
def size( self ):
with self.mutex:
return len( self.data )
def getMutex( self ):
return self.mutex
def getList( self ):
return self.data
def append( self, _els ):
with self.mutex:
if type( _els ) is list:
for _el in _els:
self.data.append( _el )
else:
self.data.append( _els )
def getRecentElements( self, _count = 0 ):
assert( _count >= 0 )
res = []
_size = 0
with self.mutex:
_size = len( self.data )
if _count == 0:
_count = _size
_index = 0 if _size <= _count else _size - _count
while _index < _size:
res.append( self.data[ _index ] )
_index += 1
return res
def getReadPosition( self ):
with self.mutex:
return self.readPosition
def setReadPosition( self, _pos ):
with self.mutex:
self.readPosition = _pos
g_num_bars_curr = 100
qbuff = CandleList()
def get_q(): #is the function really needed when the object is accessed from Dash callbacks
return qbuff
app = Dash()
def _create_fig( _q ):
_cdsAll = _q.getList()
_lock = _q.getMutex()
xs = []
os = []
hs = []
ls = []
cs = []
with _lock:
_size = len( _cdsAll )
_startIndex = max( 0, _size - g_num_bars_curr )
print( '_candList.getRecentElements( 0 ) returned %d candles' % _size )
for i in range( _startIndex, len( _cdsAll ) ):
xs.append( _cdsAll[i].x )
os.append( _cdsAll[i].o )
hs.append( _cdsAll[i].h )
ls.append( _cdsAll[i].l )
cs.append( _cdsAll[i].c )
if _size > 0:
_q.readPosition = _size
__fig = go.Figure( data = [
go.Candlestick(
x = xs,
open = os,
high = hs,
low = ls,
close = cs
)
])
__fig.update_layout( xaxis_rangeslider_visible = False, height = 400 )
return __fig
def update_layout():
return html.Div([
dcc.Graph( id = 'candles',
#animate = True,
figure=_create_fig( qbuff ) ),
dcc.Interval( id = 'interval', interval = cbPeriodMS )
])
app.layout = update_layout
#fig = go.Figure()
@app.callback(
Output( "candles", "extendData" ),
[Input( "interval", "n_intervals" )],
[State( "candles", 'figure' )]
)
def update_figure( n_intervals, _fig ):
#print( 'Adding calnde #%d' % n_intervals )
_q = get_q()
_cdsAll = _q.getList()
_lock = _q.getMutex()
justAdded = False
_firstCandle = True
_xs = []
_os = []
_hs = []
_ls = []
_cs = []
with _lock:
assert( _q.readPosition >= 0 and _q.readPosition <= len( _cdsAll ) )
print( "(before callback) CandList.readPosition =", _q.readPosition )
numRead = 0
for i in range( _q.readPosition, len( _cdsAll ) ):
justAdded = False
_cd = _cdsAll[i]
printCandle( _cd )
if len(_fig["data"][0]["x"]) == 0:
justAdded = True
else:
#lastBarTime = _fig["data"][0]['x1'][-1]
#lastBarTimestamp = lastBarTime.timestamp()
print( '_fig[\"data\"][0][\'x\'][-1] =', _fig["data"][0]['x'][-1], '_cd[i].x =', _cd.x )
if _fig["data"][0]['x'][-1] != _cd.x:
justAdded = True
print( 'justAdded =', justAdded )
if justAdded:
_xs.append( _cd.x )
_os.append( _cd.o )
_hs.append( _cd.h )
_ls.append( _cd.l )
_cs.append( _cd.c )
else:
assert( _firstCandle )
_fig['data'][0]['x'][-1] = _cd.x
_fig['data'][0]['open'][-1] = _cd.o
_fig['data'][0]['high'][-1] = _cd.h
_fig['data'][0]['low'][-1] = _cd.l
_fig['data'][0]['close'][-1] = _cd.c
numRead += 1
if _firstCandle:
_firstCandle = False
if not justAdded:
break #need this to separate updating last bar and adding new bars ( new bars,
#if they are will be added at the next call
print( "(after callback) num bars read =", numRead )
if numRead == 0:
raise PreventUpdate
else:
_q.readPosition += numRead
if justAdded:
return (
{
'x': [_xs],
'open': [_os],
'high': [_hs],
'low': [_ls],
'close': [_cs]
},
[0], min( len(_fig["data"][0]["x"]) + len(_xs), g_num_bars_curr )
)
else:
return (
{
"x": [_fig['data'][0]['x']],
"open": [_fig['data'][0]['open']],
"high": [_fig['data'][0]['high']],
"low": [_fig['data'][0]['low']],
"close": [_fig['data'][0]['close']]
},
[0], len(_fig["data"][0]["x"])
)
def printCandle( _cd ):
print( 'xOCHLV: %d\t%f\t%f\t%f\t%f\t%d' % ( _cd.x, _cd.o, _cd.c, _cd.h, _cd.l, _cd.v ) )
def main():
__port = 55555
rc = RandomCandle( 1000, 1, 20, 5 )
senderObj = Thread( target = sender, args = ( qbuff, rc ) )
senderObj.start()
app.run_server( debug = False, port = __port ) #if debug = True, number of run threads are doubled on Windows
print( 'Dash server is down, joining threads...' )
try:
if senderObj.is_alive():
senderObj.join()
except RuntimeError as e:
print( '%s' % e.args[0] )
def sender( _q, _rc ):
time.sleep( 0.5 ) #maybe this can be deleted without any affection on the code stability
_cdsAll = _q.getList()
_lock = _q.getMutex()
justAdded = None
numCandles = 1000
i = 0
while i < numCandles:
with _lock:
cd = _rc.generate()
justAdded = True
if len( _cdsAll ) > 0:
_cdl = _cdsAll[-1]
if cd.x != _cdl.x:
_cdsAll.append( cd )
else:
_cdl.o = cd.o
_cdl.h = cd.h
_cdl.l = cd.l
_cdl.c = cd.c
_cdl.v = cd.v
if _q.readPosition == len( _cdsAll ):
_q.readPosition -= 1
justAdded = False
else:
_cdsAll.append( cd )
if justAdded:
print( 'Sent i = %d' % i )
i += 1
time.sleep( .001 * sendPeriodMs )
print( 'Sender thread returns' )
if __name__ == '__main__':
main()
Please point me out if something in the code could be improved easily. For example, I need to run server with debug = False
(Ln 80) because otherwise there two thread instances are launched at line 281 call. I didn’t find how it can be fixed.
Returning the differences between IDLE and cmd, the code above doesn’t work in IDLE but works in cmd. But when cbPeriodMS = 75
at line 12 changed to cbPeriodMS = 200
, it works in IDLE (on my machine) too.
And my main question is about update_figure callback. The situation is more obvious when sending period is small, say sendPeriodMs = 10
and callback polling period is much bigger, say cbPeriodMS = 500
, the code works fine at this parameters. But I have to break data into two pieces and process first candlestick updating the last one on the plot, do break
at the line 239, and then wait for a new callback to process rest candlesticks (and new added ones, if any). So, is it possible to do this in one shot?