Today I posted a question with a code showing live candlestick plot and it works fine.
But I want also add some lines (or scatters) which also must be live-updating. Here is the slightly updated code from my previous topic:
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 = 75
cbPeriodMS = 200
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
_red = _c < _o
self.par1 = .5 * ( _h + ( _o if _red else _c ) )
self.par2 = .5 * ( _l + ( _c if _red else _o ) )
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 = []
par1s = []
par2s = []
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 )
par1s.append( _cdsAll[i].par1 )
par2s.append( _cdsAll[i].par2 )
if _size > 0:
_q.readPosition = _size
__fig = go.Figure( data = [
go.Candlestick(
x = xs,
open = os,
high = hs,
low = ls,
close = cs
),
go.Scatter( x = xs,
y = par1s,
opacity=0.7,
line=dict(color='yellow')
),
go.Scatter( x = xs,
y = par2s,
opacity=0.7,
line=dict(color='purple')
)
])
__fig.update_layout( xaxis_rangeslider_visible = False,
height = 400, showlegend = False )
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()
lastCandleWasUpdated = False
_firstCandle = True
_xs = []
_os = []
_hs = []
_ls = []
_cs = []
_par1s = []
_par2s = []
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 ) ):
candleWasUpdated = True
_cd = _cdsAll[i]
printCandle( _cd )
print( '_fig[\"data\"][0][\'x\'][-1] =', _fig["data"][0]['x'][-1], '_cd[i].x =', _cd.x )
if _fig["data"][0]['x'][-1] != _cd.x:
candleWasUpdated = False
print( 'candleWasUpdated =', candleWasUpdated )
if candleWasUpdated:
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
_fig['data'][1]['x'][-1] = _cd.x
_fig['data'][1]['y'][-1] = _cd.par1
_fig['data'][2]['x'][-1] = _cd.x
_fig['data'][2]['y'][-1] = _cd.par2
lastCandleWasUpdated = True
else:
_xs.append( _cd.x )
_os.append( _cd.o )
_hs.append( _cd.h )
_ls.append( _cd.l )
_cs.append( _cd.c )
_par1s.append( _cd.par1 )
_par2s.append( _cd.par2 )
numRead += 1
if _firstCandle:
_firstCandle = False
print( "(after callback) num bars read =", numRead )
if numRead == 0:
raise PreventUpdate
else:
_q.readPosition += numRead
__xs = _fig['data'][0]['x']+_xs
if lastCandleWasUpdated:
return (
{
"x": [__xs,__xs,__xs],
"open": [_fig['data'][0]['open']+_os,[],[]],
"high": [_fig['data'][0]['high']+_hs,[],[]],
"low": [_fig['data'][0]['low']+_ls,[],[]],
"close": [_fig['data'][0]['close']+_cs,[],[]],
"y": [[],_fig['data'][1]['y']+_par1s,_fig['data'][2]['y']+_par2s]
},
[0,1,2], len(_fig["data"][0]["x"])+numRead-1
)
else:
return (
{
'x': [__xs,__xs,__xs],
'open': [_os,[],[]],
'high': [_hs,[],[]],
'low': [_ls,[],[]],
'close': [_cs,[],[]],
'y': [[],_par1s,_par2s]
},
[0,1,2], min( len(_fig["data"][0]["x"]) + len(_xs), g_num_bars_curr )
)
def printCandle( _cd ):
print( 'xOCHLV/p1/p2: %d\t%f\t%f\t%f\t%f\t%d\t%f\t%f' % ( _cd.x, _cd.o, _cd.c, _cd.h, _cd.l,
_cd.v, _cd.par1, _cd.par2 ) )
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 = 150
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
_cdl.par1 = cd.par1
_cdl.par2 = cd.par2
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()
When it run, there’s no any errors in console, but my changes have broken something in the update_figure callback. But when the dash web-page is refreshed, one can see that update_layout/_create_fig functions are working as expected:
Please, someone, help me to fix my code. The problem is like to add two live-updated custom indicators on the top of live candlestick chart. This should be doable and googlable, but I can’t manage with this yet…