highfestiva / finplot

Performant and effortless finance plotting for Python
MIT License
941 stars 187 forks source link

How to make the cursors independent of each other in embedded plots? #47

Closed xyberian-sg closed 4 years ago

xyberian-sg commented 4 years ago

Hi, I have modified the code from example-embed.py as the following:

#!/usr/bin/env python3

import finplot as fplt
from functools import lru_cache
from PyQt5.QtWidgets import QGraphicsView, QComboBox, QLabel
from PyQt5.QtGui import QApplication, QGridLayout
from threading import Thread
import yfinance as yf

app = QApplication([])
win = QGraphicsView()
win.setWindowTitle('TradingView wannabe #2')
layout = QGridLayout()
win.setLayout(layout)
win.resize(900, 600)

combo1 = QComboBox()
combo1.setEditable(True)
[combo1.addItem(i) for i in 'AMRK FB GFN REVG TSLA TWTR WMT CT=F GC=F ^FTSE ^N225 EURUSD=X ETH-USD'.split()]
layout.addWidget(combo1, 0, 0, 1, 1)
info1 = QLabel()
layout.addWidget(info1, 0, 1, 1, 1)

combo2 = QComboBox()
combo2.setEditable(True)
[combo2.addItem(i) for i in 'AMRK FB GFN REVG TSLA TWTR WMT CT=F GC=F ^FTSE ^N225 EURUSD=X ETH-USD'.split()]
layout.addWidget(combo2, 0, 2, 1, 1)
info2 = QLabel()
layout.addWidget(info2, 0, 3, 1, 1)

ax1, ax2 = fplt.create_plot_widget(win, init_zoom_periods=100, rows=2)
win.axs = [ax1, ax2] # finplot requres this property
layout.addWidget(ax1.ax_widget, 1, 0, 1, 2)
layout.addWidget(ax2.ax_widget, 1, 2, 1, 2)

@lru_cache(maxsize=15)
def download1(symbol):
    return yf.download(symbol, '2020-01-01')

@lru_cache(maxsize=15)
def download2(symbol):
    return yf.download(symbol, '2020-06-01')

@lru_cache(maxsize=100)
def get_name(symbol):
    return yf.Ticker(symbol).info['shortName']

plots1 = []
def update1(txt):
    df = download1(txt)
    if len(df) < 20: # symbol does not exist
        return
    info1.setText('Loading symbol name...')
    price = df['Open Close High Low'.split()]
    ma20 = df.Close.rolling(20).mean()
    ma50 = df.Close.rolling(50).mean()
    volume = df['Open Close Volume'.split()]
    if not plots1:
        plots1.append(fplt.candlestick_ochl(price, ax=ax1))
        plots1.append(fplt.plot(ma20, legend='MA-20', ax=ax1))
        plots1.append(fplt.plot(ma50, legend='MA-50', ax=ax1))
        plots1.append(fplt.volume_ocv(volume, ax=ax1.overlay()))
    else:
        plots1[0].update_data(price)
        plots1[1].update_data(ma20)
        plots1[2].update_data(ma50)
        plots1[3].update_data(volume)
    Thread(target=lambda: info1.setText(get_name(txt))).start() # slow, so use thread

plots2 = []
def update2(txt):
    df = download2(txt)
    if len(df) < 20: # symbol does not exist
        return
    info2.setText('Loading symbol name...')
    price = df['Open Close High Low'.split()]
    ma20 = df.Close.rolling(20).mean()
    ma50 = df.Close.rolling(50).mean()
    volume = df['Open Close Volume'.split()]
    if not plots2:
        plots2.append(fplt.candlestick_ochl(price, ax=ax2))
        plots2.append(fplt.plot(ma20, legend='MA-20', ax=ax2))
        plots2.append(fplt.plot(ma50, legend='MA-50', ax=ax2))
        plots2.append(fplt.volume_ocv(volume, ax=ax2.overlay()))
    else:
        plots2[0].update_data(price)
        plots2[1].update_data(ma20)
        plots2[2].update_data(ma50)
        plots2[3].update_data(volume)
    Thread(target=lambda: info2.setText(get_name(txt))).start() # slow, so use thread

combo1.currentTextChanged.connect(update1)
update1(combo1.currentText())

combo2.currentTextChanged.connect(update2)
update2(combo2.currentText())

fplt.show(qt_exec=False) # prepares plots when they're all setup
win.show()
app.exec_()

The idea is to see if possible to have 2 independent plots embedded in a single QApplication. It works mostly, but I've noticed a few things:

  1. The cursors are in sync across the 2 plots, and it would be great if only the "active" one moves while the "inactive" does not
  2. The cursors have the same date value (x-axis). It seems that is the common denominator driving the behavior
  3. The 2nd plot (right-hand side) has the volume missing, although the code snippets are exactly the same for both plots
  4. The 2nd plot has different background color, and it doesn't seem to change even if I specify a different color upfront
  5. Zooming is also in sync across the 2 plots. I purposely have different date ranges for the 2 plots to show the effect of the same scale being applied. Would be great if zooming can be made independent too.

Thanks in advance for your advice!

highfestiva commented 4 years ago

Hi! You can do ax2.decouple() to drop the zoom/pan link between the two axes. There is currently no support for not moving the crosshairs in all plots at once. What is the reason you do not want the other crosshair to move?

highfestiva commented 4 years ago

@xyberian-sg What is the reason you do not want the other crosshair to move?

xyberian-sg commented 4 years ago

Thanks for the suggestion, now I can zoom/pan the 2 plots independently. My reason for not wanting the other crosshair to move is that sometimes I need to have 2 plots showing different date ranges side by side, but it's ok now since I can zoom/pan them independently.

Regarding point #3, could you please take a look at the code below, and let me know if I made any mistakes? And how can I set the foreground and background colors for both plots? I tried but only the 1st plot would take on the colors specified.

highfestiva commented 4 years ago

See https://github.com/highfestiva/finplot/issues/25

Every other row uses odd row color to easily distinguish them.

highfestiva commented 4 years ago

Ah, missed what question you were asking about, sorry! Fixed in https://github.com/highfestiva/finplot/commit/b9a52cc096491a0f633e281f90981fd8498a3e2e -- the overlay was ending up behind the viewbox of the odd one (with odd color set).

highfestiva commented 4 years ago

In my last fix https://github.com/highfestiva/finplot/commit/5bb833277e70357349db3a61366a9ff306d1e55c I came up with a much better resolution for your overlay problem. I tried it with your above code, and that seems to work. Lemmeno if you are experiencing other issues with this.

xyberian-sg commented 4 years ago

Yes, it does work. Thanks a lot!