How can I update and extend plot containing traces of different kinds (e.g. candlestics and lines)?

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…