diff --git a/README.md b/README.md index b93f063..2175ffe 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ This aplication is a exercice working with Qt, PyQt arround the finance. It is largely inspired by `TradingView` which is a wonderfull tool for finance analysis. +![Preview](./preview_home.png) + ![Preview](./preview.jpg) ## Installation @@ -44,4 +46,9 @@ It is largely inspired by `TradingView` which is a wonderfull tool for finance a - [ ] Save users settings - [ ] Embeded Python Console ? - [ ] Draw over the graph -- [ ] Use Machine learning to determine patterns \ No newline at end of file +- [ ] Use Machine learning to determine patterns +- [ ] Draw Fibonnaci on Chart => 50% done +- [ ] Make Virtual Portefolio +- [ ] Avaible Trading +- [ ] SplashScreen Loading +- [ ] Find Compagnies with differents stratgies (Buffet, Peter Lynch, Carl Icahn) \ No newline at end of file diff --git a/app/add_ons/indicators/fibonnaci.py b/app/add_ons/indicators/fibonnaci.py new file mode 100644 index 0000000..9b95950 --- /dev/null +++ b/app/add_ons/indicators/fibonnaci.py @@ -0,0 +1,177 @@ +import numpy as np +import pyqtgraph as pg + +from utils.indicators_utils import Indicator, InputField, ChoiceField + +RATIO = [78.6, 61.8, 50.0, 38.2, 23.6] +COLORS = { + 78.6: (255, 56, 56), + 61.8: (218, 255, 56), + 50.0: (56, 255, 82), + 38.2: (148, 56, 255), + 23.6: (56, 95, 255), + } + +class Fibonnaci(Indicator): + def __init__(self): + super(Fibonnaci, self).__init__() + + self.name = "Fibonnaci" + self.description = "Fibonnaci" + + # Define and register all customisable settings + # field_input = ChoiceField( + # "Input", choices=["Open", "Close", "High", "Low"], default="Close" + # ) + # self.register_field(field_input) + # line1 = InputField( + # "Fibonnaci", color=(51, 153, 255), value=3, width=2 + # ) + # self.register_fields(line1) + + def create_indicator(self, graph_view, *args, **kwargs): + super(Fibonnaci, self).create_indicator(self, graph_view) + + # Get values + values = graph_view.values + self.quotation_plot = graph_view.g_quotation + + self.roi = pg.RectROI([347, 113], [50, 60], + invertible=True, + pen=pg.mkPen(color=(255, 255, 255), + width=1,), + hoverPen=None, + ) + + self.roi.addTranslateHandle((0,0)) + self.quotation_plot.addItem(self.roi) + + self.set_fibonnaci_levels() + self.roi.sigRegionChanged.connect(self.move_items) + + def move_items(self): + """This method is call everytime the ROI is move. + """ + self.set_fibonnaci_levels() + + def set_fibonnaci_levels(self): + try: + self.quotation_plot.removeItem(self.line_78) + self.quotation_plot.removeItem(self.label_78) + self.quotation_plot.removeItem(self.line_61) + self.quotation_plot.removeItem(self.label_61) + self.quotation_plot.removeItem(self.line_50) + self.quotation_plot.removeItem(self.label_50) + self.quotation_plot.removeItem(self.line_38) + self.quotation_plot.removeItem(self.label_38) + self.quotation_plot.removeItem(self.line_23) + self.quotation_plot.removeItem(self.label_23) + self.quotation_plot.removeItem(self.label_1) + self.quotation_plot.removeItem(self.label_100) + except: + pass + + self.line_78 = self._set_fibonnaci_level(prc=78.6) + self.label_78 = self._set_label_level(prc=78.6) + + self.line_61 = self._set_fibonnaci_level(prc=61.8) + self.label_61 = self._set_label_level(prc=61.8) + + self.line_50 = self._set_fibonnaci_level(prc=50.0) + self.label_50 = self._set_label_level(prc=50.0) + + self.line_38 = self._set_fibonnaci_level(prc=38.2) + self.label_38 = self._set_label_level(prc=38.2) + + self.line_23 = self._set_fibonnaci_level(prc=23.6) + self.label_23 = self._set_label_level(prc=23.6) + + self.label_100 = self._set_label_level(prc=100) + self.label_1 = self._set_label_level(prc=1) + + self.quotation_plot.addItem(self.line_78) + self.quotation_plot.addItem(self.label_78) + self.quotation_plot.addItem(self.line_61) + self.quotation_plot.addItem(self.label_61) + self.quotation_plot.addItem(self.line_50) + self.quotation_plot.addItem(self.label_50) + self.quotation_plot.addItem(self.line_38) + self.quotation_plot.addItem(self.label_38) + self.quotation_plot.addItem(self.line_23) + self.quotation_plot.addItem(self.label_23) + self.quotation_plot.addItem(self.label_100) + self.quotation_plot.addItem(self.label_1) + + def _set_fibonnaci_level(self, prc=50.0): + """This method plot line for each retracement + :param widget: ROIWidget + :type widget: PQQt.GraphWidget + """ + color = COLORS[prc] + xpos, ypos = self.position_line(prc) + + line = pg.LineSegmentROI(positions=(xpos, ypos), + pen=pg.mkPen(color, width=2), + movable=False, + rotatable=False, + resizable=False, + ) + line.setSelected(False) + return line + + + def _set_label_level(self, prc=50.0): + """This method plot label for each retracement + :param widget: TextItem + :type widget: PQQt.GraphWidget + """ + xpos, ypos = self.position_line(prc) + + percentg_lb = "0.{}".format(int(prc)) + label = pg.TextItem(text=' {} ({})'.format(percentg_lb, round(ypos[1], 2)), + anchor=(0, 0.5), + ) + + # Lock Label to the Right of ROI + if xpos[0] < ypos[0]: + position = ypos[0] + else: + position = xpos[0] + + label.setPos(position, ypos[1]) + return label + + def position_line(self, prc=50.0): + """This method return the graph position for the levels + """ + rtc = self._get_fibonnaci_level(prc)[0] + x_pos = [self.roi.pos()[0], rtc] + y_pos = [self.roi.pos()[0] + self.roi.size()[0], rtc] + return x_pos, y_pos + + + def _get_fibonnaci_level(self, prc): + """This method calcul the retracement level calculating the difference + between the min/max of the ROI. + :param prc: Percentage to calculate + :type float: + """ + # position is lower-left ROI + # use x_min and y_min with width and height to find the position + # on the graph. + pos = self.roi.getState()['pos'] + size = self.roi.getState()['size'] + + y_min = pos[1] + y_max = pos[1] + size[1] + variation = y_max - y_min + + retracement = [] + retrc = variation * (prc / 100) + value = y_min + retrc + retracement.append(value) + + return retracement + + def remove_indicator(self, graph_view, *args, **kwargs): + super(Fibonnaci, self).remove_indicator(graph_view) diff --git a/app/libs/analysies/analyse_financials.py b/app/libs/analysies/analyse_financials.py index 83553b4..04240cc 100644 --- a/app/libs/analysies/analyse_financials.py +++ b/app/libs/analysies/analyse_financials.py @@ -18,7 +18,7 @@ from pprint import pprint from utils import utils as utl from libs.yahoo_fin import stock_info as sf -from .analyse import AnalyseData +from libs.analysies.analyse import AnalyseData class AnalyseFondamental(object): @@ -34,12 +34,6 @@ def __init__(self, ticker): self.year_atual = self.resultat_datas.keys()[0] self.year_before = self.resultat_datas.keys()[1] - # pprint(self.resultat_datas) - # pprint(self.balance_datas) - # pprint(self.cash_flow_datas) - # pprint(self.per_datas) - # pprint(self.statistic_datas) - self.datas = {} self.data_analyse = {} @@ -50,6 +44,7 @@ def __init__(self, ticker): self.analyse = AnalyseData(self.data_analyse) self.extend_dict_data() + self.tes() def set_var(self): self.actions = None @@ -202,7 +197,7 @@ def datas_dict(self): self.datas["Capitaux Propre"] = utl.format_data( self.total_capitaux_propre.values.tolist() ) - self.datas["Score"] = [self.total_score()] + self.datas["Score"] = ["", "", "", "", self.total_score()] self.bna_years() self.per_years() @@ -213,6 +208,12 @@ def datas_dict(self): self.roe_roa_ratio(roa=False) self.roe_roa_ratio(roa=True) + + def tes(self): + for key, value in self.datas.items(): + if len(value) <= 4: + self.datas[key].append("") + def data_for_analyse(self): self.data_analyse["Actifs Total"] = self.datas["Actifs Total"] self.data_analyse["BNA"] = self.datas["BNA"] @@ -222,7 +223,6 @@ def data_for_analyse(self): "Capitaux Propre" ] = self.total_capitaux_propre.values.tolist() self.data_analyse["Chiffre d'affaires"] = self.chiffre_affaire.tolist() - self.data_analyse["Dividendes"] = self.datas["Dividendes"] self.data_analyse["EBITDA"] = self.ebitda.values.tolist() self.data_analyse["PER"] = self.datas["PER"] self.data_analyse["ROA"] = self.datas["ROA"] @@ -294,7 +294,10 @@ def dividendes_ratio(self): calcul_rend = round(calcul_rend, 2) rendement.append("{}€".format(calcul_rend)) - self.datas["Dividendes"] = dividendes + self.data_analyse["Dividendes"] = dividendes + self.datas["Dividendes"] = [] + for i in dividendes: + self.datas["Dividendes"].append(" ".join(["{}€".format(str(x)) for x in i])) self.datas["Dividendes Rendement"] = [ "{}%".format(i) for i in rendement ] @@ -339,6 +342,6 @@ def extend_dict_data(self): if __name__ == "__main__": test = AnalyseFondamental("AAPL") - pprint(test.data_analyse) - pprint(test.analyse.__dict__) + pprint(test.datas) + # pprint(test.analyse.__dict__) # testq = AnalyseFondamental("BN.PA") diff --git a/app/libs/articles/get_articles.py b/app/libs/articles/boursorama_articles.py similarity index 92% rename from app/libs/articles/get_articles.py rename to app/libs/articles/boursorama_articles.py index 88b27b9..5e32c0b 100644 --- a/app/libs/articles/get_articles.py +++ b/app/libs/articles/boursorama_articles.py @@ -32,14 +32,15 @@ def __init__(self, ticker=None): self.get_url(ticker=ticker) def get_url(self, ticker): - if "{}.PA".format(ticker) in sf.tickers_cac().keys(): + if "{}".format(ticker) in sf.tickers_cac().keys(): url = "https://www.boursorama.com/cours/actualites/1rP{}/".format( - ticker + ticker.split('.')[0] ) else: url = "https://www.boursorama.com/cours/actualites/{}/".format( ticker ) + print(url) self.response = requests.get(url, headers=self.headers) self.soup() @@ -61,8 +62,8 @@ def soup(self): self.articles.append( { "title": title, - "date": time, - "descritpion": descritpion, + "published": time, + "summary": descritpion, "link": "https://www.boursorama.com{}".format(link), } ) diff --git a/app/libs/articles/yahoo_articles.py b/app/libs/articles/yahoo_articles.py new file mode 100644 index 0000000..b9e5bdd --- /dev/null +++ b/app/libs/articles/yahoo_articles.py @@ -0,0 +1,86 @@ +# +# This Class get all articles from selected ticker +# from the Yahoo website. + +import requests +from bs4 import BeautifulSoup +from utils import utils +from pprint import pprint +from libs.yahoo_fin import news + + +class ArticlesYahoo(object): + def __init__(self): + super(ArticlesYahoo, self).__init__() + + self.articles = None + + def get_articles_from_compagny(self, ticker): + self.articles = news.get_yf_rss(ticker) + compagny = utils.get_compagny_name_from_tick(ticker=ticker) + + if not self.articles: + return + + for article in self.articles: + article['compagny'] = compagny + self.get_thumbnail_link(article) + article['summary'] = self.cup_long_text(article['summary']) + + return self.articles + + def get_home_articles(self): + self.articles = news.get_yf_home_rss() + + if not self.articles: + return + + for article in self.articles: + article['published'] = article['published'].replace('T', ' ') + article['summary'] = "" + try: + article['img'] = article['media_content'][0]['url'] + except: + article['img'] = "" + + + return self.articles + + def get_thumbnail_link(self, article): + """ + This method scrap the img url of the article from the website. + """ + header = { + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36", + } + link = article['link'] + request = requests.get(link, headers=header) + soup = BeautifulSoup(request.text, "html.parser") + + try: + img = soup.findAll('img', {"class": "caas-img"}) + img = img[-1]['src'] + except: + img = None + article['img'] = img + + return article + + + def cup_long_text(self, value): + """ + This method cut long description. + return: list. + """ + len_chart = 180 + if len(value) > len_chart: + value = "{}...".format(value[0:len_chart]) + return value + + +if __name__ == '__main__': + tick = "MSFT" + x = ArticlesYahoo() + # articles = x.get_articles_from_compagny(tick) + articles = x.get_home_articles() + pprint(articles) diff --git a/app/libs/articles_widget.py b/app/libs/articles_widget.py index 5d047a0..2886a55 100644 --- a/app/libs/articles_widget.py +++ b/app/libs/articles_widget.py @@ -1,104 +1,38 @@ -import webbrowser -from PySide2 import QtWidgets, QtGui -from libs.thread_pool import ThreadPool -from libs.articles.get_articles import Articles +# +# This class set all the articles from a selected compagny. +# +from pprint import pprint +from PySide2 import QtWidgets, QtCore +from libs.articles.yahoo_articles import ArticlesYahoo +from libs.widgets.article_itemwidget import ArticlesWidgetItem -class ArticlesWidget(QtWidgets.QWidget): + +class ArticlesWidget(QtWidgets.QListWidget): def __init__(self, parent=None, ticker=None): super(ArticlesWidget, self).__init__(parent) + # self._on_get_articles(ticker) + + @QtCore.Slot(str) + def _on_get_articles(self, ticker): + articles = self._get_articles_dict(ticker=ticker) + self.clear() + + for index, i in enumerate(articles): + article = ArticlesWidgetItem(parent=self) + article.set_title(i["title"], i["link"]) + article.set_compagny(i["compagny"]) + article.set_date(i["published"]) + article.set_description(i["summary"]) + article.set_thumbnail(i["img"]) + item = QtWidgets.QListWidgetItem() + item.setSizeHint(article.sizeHint()) + self.addItem(item) + self.setItemWidget(item, article) - self.thread_pool = ThreadPool() - self.get_articles(ticker) - - def get_articles(self, ticker): - if ticker: - articles = self._get_articles_dict(ticker=ticker).articles - - else: - # Remplacer par 2 3 ticker Random - articles = self._get_articles_dict(ticker="GLE").articles - - scroll = QtWidgets.QScrollArea(self) - widget = QtWidgets.QWidget() - container = QtWidgets.QVBoxLayout(widget) - - for i in articles: - title = i["title"] - date = i["date"] - description = i["descritpion"] - link = i["link"] - article = ArticlesWidgetItem( - parent=self, - title=title, - date=date, - description=description, - link=link, - ) - container.addWidget(article) - - widget.setLayout(container) - scroll.setWidget(widget) - scroll.setWidgetResizable(True) - - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(scroll) def _get_articles_dict(self, ticker): - return Articles(ticker=ticker) - - -class ArticlesWidgetItem(QtWidgets.QWidget): - def __init__( - self, parent=None, title=None, date=None, description=None, link=None - ): - super(ArticlesWidgetItem, self).__init__(parent) - - self.layout = QtWidgets.QVBoxLayout(self) - self.layout.setObjectName(u"layout") - self.title = LabelTitle(title, link) - self.title.setObjectName(u"title") - self.layout.addWidget(self.title) - - self.date = QtWidgets.QLabel(self) - self.date.setObjectName(u"date") - self.date.setText(date) - self.date.setFont(QtGui.QFont("Times", 10)) - self.layout.addWidget(self.date) - - self.description = Description(description) - self.description.setObjectName(u"description") - self.description.setEnabled(True) - self.layout.addWidget(self.description) - - self.spliter = QHLine() - self.layout.addWidget(self.spliter) - - -class QHLine(QtWidgets.QFrame): - def __init__(self): - super(QHLine, self).__init__() - self.setFrameShape(QtWidgets.QFrame.HLine) - self.setFrameShadow(QtWidgets.QFrame.Sunken) - - -class LabelTitle(QtWidgets.QLabel): - def __init__(self, text, link=None): - super(LabelTitle, self).__init__() - self.link = link - - if text: - self.setText(text) - self.setFont(QtGui.QFont("Times", 20)) - - def mousePressEvent(self, event) -> None: - webbrowser.open(self.link) + article = ArticlesYahoo() + return article.get_articles_from_compagny(ticker=ticker) -class Description(QtWidgets.QTextBrowser): - def __init__(self, text): - super(Description, self).__init__() - self.setStyleSheet("background-color: rgba(0, 0, 0, 0);") - if text: - self.setText(text) - self.setFont(QtGui.QFont("Times", 10)) diff --git a/app/libs/events_handler.py b/app/libs/events_handler.py index 2021684..167c240 100644 --- a/app/libs/events_handler.py +++ b/app/libs/events_handler.py @@ -22,7 +22,7 @@ class EventHandler(QtCore.QObject): sig_indicator_settings_canceled = QtCore.Signal(object) sig_indicator_settings_reseted = QtCore.Signal(object) - sig_action_triggered = QtCore.Signal(str) + sig_action_triggered = QtCore.Signal(str, dict) sig_favorite_created = QtCore.Signal(str) sig_favorite_loaded = QtCore.Signal(list) @@ -32,3 +32,8 @@ class EventHandler(QtCore.QObject): sig_favorite_clicked = QtCore.Signal(str) sig_articles = QtCore.Signal(dict) + + sig_graph_clicked = QtCore.Signal(list, object) + sig_graph_mouse_moved = QtCore.Signal(object) + sig_graph_mouse_pressed = QtCore.Signal(object) + sig_graph_mouse_released = QtCore.Signal(object) diff --git a/app/libs/financial_widget.py b/app/libs/financial_widget.py new file mode 100644 index 0000000..2f43d7a --- /dev/null +++ b/app/libs/financial_widget.py @@ -0,0 +1,82 @@ +from pprint import pprint +from PySide2 import QtWidgets, QtCore, QtGui +from libs.analysies.analyse_financials import AnalyseFondamental + +class QTableWidgetFinance(QtWidgets.QTableWidget): + def __init__(self, parent=None): + super(QTableWidgetFinance, self).__init__(parent=parent) + + +class TableFinance(QTableWidgetFinance): + def __init__(self, parent=None): + super(TableFinance, self).__init__(parent=parent) + + @QtCore.Slot(str) + def on_set_financials_table(self, ticker): + """ + This method get the fondamental from the compagny + and fill the table. + """ + analyses = AnalyseFondamental(ticker) + self.data = analyses.datas + + # Remove all Cells and Items + self.clear() + self.setRowCount(0) + self.setColumnCount(0) + + self.header = self.data['YEAR'] + self.header[-1] = "Bilan" + self.header.insert(0, 'Valorisation') + score = self.data["Score"] + + del self.data["YEAR"] + del self.data["Score"] + + self.setColumnCount(len(self.header)) + self.setHorizontalHeaderLabels(self.header) + self.horizontalHeader().resizeSection(0, 150) + self.setColumnWidth(len(self.header) - 1, 250) + self.setWordWrap(True) + + # Rows + for row, (title, donnee) in enumerate(sorted(self.data.items())): + self.insertRow(row) + cell_val = QtWidgets.QTableWidgetItem() + cell_val.setData(QtCore.Qt.DisplayRole, title) + self.setItem(row, 0, cell_val) + for column, data in enumerate(donnee): + cell = QtWidgets.QTableWidgetItem() + cell.setData(QtCore.Qt.DisplayRole, str(data)) + cell.setTextAlignment(QtCore.Qt.AlignCenter) + self.setItem(row, column + 1, cell) + + # Add Score to the last Row + last_row = len(self.data) + self.insertRow(last_row) + cell_score_title = QtWidgets.QTableWidgetItem() + cell_score_title.setData(QtCore.Qt.DisplayRole, "Score") + self.setItem(last_row, 0, cell_score_title) + for column, data in enumerate(score): + cell_score = QtWidgets.QTableWidgetItem() + cell_score.setData(QtCore.Qt.DisplayRole, str(data)) + cell_score.setTextAlignment(QtCore.Qt.AlignCenter) + self.setItem(last_row, column + 1, cell_score) + + for i in range(self.rowCount()): + self.setRowHeight(i, 50) + + self.verticalHeader().setVisible(False) + self.setShowGrid(False) + self.setMouseTracking(True) + self.cellEntered.connect(self.cellHover) + + def cellHover(self, row, column): + """ + This method get position (row,column) of cursor. + """ + self.clearSelection() + self.current_hover = [0, 0] + for i in range(len(self.header)): + item = self.item(row, i) + item.setSelected(True) diff --git a/app/libs/graph/graphwidget.py b/app/libs/graph/graphwidget.py index 7e58082..b1692d6 100644 --- a/app/libs/graph/graphwidget.py +++ b/app/libs/graph/graphwidget.py @@ -2,6 +2,7 @@ from PySide2 import QtCore, QtGui, QtWidgets from libs.graph.candlestick import CandlestickItem +from libs.events_handler import EventHandler from utils import utils # TODO import from palette or Qss @@ -19,11 +20,16 @@ def __init__(self, parent=None): self.v_line = None self.h_line = None + self.signals = EventHandler() + self.g_quotation = self.addPlot(row=0, col=0, name="Quotation") self.g_quotation.showGrid(x=True, y=True, alpha=0.3) self.g_vb = self.g_quotation.vb self.set_cross_hair() + self.g_quotation.scene().sigMouseClicked.connect( + self._on_mouse_clicked + ) def plot_quotation(self, data, clear=True): """Plot the quotation @@ -51,7 +57,25 @@ def plot_quotation(self, data, clear=True): item = CandlestickItem(ls_data) self.g_quotation.addItem(item) self.g_quotation.enableAutoRange() + + color_line = (38, 166, 154) + if data['Close'].iloc[-1] < data['Open'].iloc[-1]: + color_line = (239, 83, 80) + + self.h_price = pg.InfiniteLine( + pos=data['Close'].iloc[-1], + angle=0, + movable=False, + pen=pg.mkPen( + color=color_line, + width=1, + style=QtCore.Qt.DotLine, + ), + ) + self.g_quotation.addItem(self.h_price, ignoreBounds=True) + self.set_time_x_axis(widget=self.g_quotation) + self._set_y_axis_(widget=self.g_quotation, data_close=data['Close']) self.set_cross_hair() def set_cross_hair(self): @@ -79,6 +103,7 @@ def set_cross_hair(self): def set_time_x_axis(self, widget): widget.setAxisItems({"bottom": pg.DateAxisItem(orientation="bottom")}) + @QtCore.Slot(object) def _on_mouse_moved(self, event): """Signal on mouse moved @@ -91,6 +116,42 @@ def _on_mouse_moved(self, event): self.v_line.setPos(mousePoint.x()) self.h_line.setPos(mousePoint.y()) + def _set_y_axis_(self, widget, data_close): + """Set Y Axis in Left and add Price. + + :param widget: GraphWidget + :type widget: PQQt.GraphWidget + :param data_close: Data Price 'Close' + :type data_close: DataFrame + """ + widget.showAxis('right') + axis = widget.getAxis('right') + axis.setTicks([[(data_close[-1], str(round(data_close[-1], 2)))]]) + + @QtCore.Slot(object) + def _on_mouse_clicked(self, event): + """Called on mouse clicked + + :param event: Mouse clicked + :type event: event + """ + items = self.g_quotation.scene().items(event.scenePos()) + plot_items = [x for x in items if isinstance(x, pg.PlotItem)] + self.signals.sig_graph_clicked.emit(plot_items, event) + + def mousePressEvent(self, event): + self.signals.sig_graph_mouse_pressed.emit(event) + super(GraphView, self).mousePressEvent(event) + print(event.pos()) + + def mouseReleaseEvent(self, event): + self.signals.sig_graph_mouse_released.emit(event) + super(GraphView, self).mouseReleaseEvent(event) + + def mouseMoveEvent(self, event): + self.signals.sig_graph_mouse_moved.emit(event) + super(GraphView, self).mouseMoveEvent(event) + class GraphWidget(QtWidgets.QWidget): """Widget wrapper for the graph""" @@ -103,6 +164,22 @@ def __init__(self, parent=None): layout.addWidget(self._graph) self.setLayout(layout) + self.signals = EventHandler() + + # Relay Signals + self._graph.signals.sig_graph_clicked.connect( + self.signals.sig_graph_clicked.emit + ) + self._graph.signals.sig_graph_mouse_pressed.connect( + self.signals.sig_graph_mouse_pressed.emit + ) + self._graph.signals.sig_graph_mouse_released.connect( + self.signals.sig_graph_mouse_released.emit + ) + self._graph.signals.sig_graph_mouse_moved.connect( + self.signals.sig_graph_mouse_moved.emit + ) + @property def graph(self): """Return the graph diff --git a/app/libs/markets_widget.py b/app/libs/markets_widget.py new file mode 100644 index 0000000..04a5c54 --- /dev/null +++ b/app/libs/markets_widget.py @@ -0,0 +1,73 @@ + +import numpy as np +from ui import markets_widget +from PySide2 import QtWidgets +from libs.yahoo_fin import stock_info +from libs.widgets.stackedwidget import StackedWidget + +TICKERS = { + "NASDAQ": "%5EIXIC", + "S&P 500": "%5EGSPC", + "Dow Jones": "%5EDJI", + "Oil": "CL%3DF", + "BTC": "BTC-USD", + "ETH": "ETH-USD", + "EUR/USD": "EURUSD%3DX", + "GBP/USD": "GBPUSD%3DX", + "Gold": "GC%3DF", +} + +class MarketsWidget(StackedWidget): + """ + Markets are the informations in the Welcome Page showing price and + variation from day before. + """ + def __init__(self, parent=None): + super(MarketsWidget, self).__init__(parent) + + page = QtWidgets.QWidget() + layout = QtWidgets.QHBoxLayout(page) + + for count, (name, tick) in enumerate(TICKERS.items()): + if count % 5 == 0: + page = QtWidgets.QWidget() + layout = QtWidgets.QHBoxLayout(page) + item = MarketsWidgetItem(self, ticker=tick, compagny=name) + layout.addWidget(item) + + self.addWidget(page) + + +class MarketsWidgetItem(QtWidgets.QWidget, markets_widget.Ui_markets): + def __init__(self, parent=None, ticker=None, compagny=None): + super(MarketsWidgetItem, self).__init__(parent) + + self.setupUi(self) + try: + x = stock_info.get_data(ticker) + except: + return + + day = float(x['adjclose'][-1]) + prev_day = float(x['adjclose'][-2]) + + if np.isnan(prev_day): + if not np.isnan(x['adjclose'][-3]): + prev_day = float(x['adjclose'][-3]) + else: + prev_day = float(x['adjclose'][-4]) + + variation = ((day - prev_day) / prev_day) * 100 + variation = round(variation, 2) + + self.title.setText(compagny) + self.price.setText("{} €".format(str(round(day, 2)))) + self.pourcentage.setText("{}%".format(str(variation))) + + if variation <= 0: + self.pourcentage.setStyleSheet("color:rgb(239, 83, 80);") + else: + self.pourcentage.setStyleSheet("color:rgb(38, 166, 154);") + + + diff --git a/app/libs/predictions/predictions_1.py b/app/libs/predictions/predictions_1.py new file mode 100644 index 0000000..99663cd --- /dev/null +++ b/app/libs/predictions/predictions_1.py @@ -0,0 +1,84 @@ +import pandas as pd +import numpy as np +import yfinance as yf +from pprint import pprint +import matplotlib.pyplot as plt +from sklearn.preprocessing import MinMaxScaler +from tensorflow.keras.models import Sequential +from tensorflow.keras.layers import Dense, Dropout, LSTM + +# https://www.youtube.com/watch?v=PuZY9q-aKLw&ab_channel=NeuralNine +class MakePrediction(object): + def __init__(self, data): + + # pprint(data) # 4105 days + + prediction_days = 60 + _end = 3009 + + # Prepare Data + test_data = data['Close'].iloc[:_end].values + scaler = MinMaxScaler(feature_range=(0, 1)) + scaled_data = scaler.fit_transform(test_data.reshape(-1, 1)) + + x_train = [] + y_train = [] + + for x in range(prediction_days, len(scaled_data)): + x_train.append(scaled_data[x - prediction_days:x, 0]) + y_train.append(scaled_data[x, 0]) + + x_train, y_train = np.array(x_train), np.array(y_train) + x_train = np.reshape(x_train, (x_train.shape[0], x_train.shape[1], 1)) + + # Build the Model + model = Sequential() + + model.add(LSTM(units=50, return_sequences=True, input_shape=(x_train.shape[1], 1))) + model.add(Dropout(0.2)) + model.add(LSTM(units=50, return_sequences=True)) + model.add(Dropout(0.2)) + model.add(LSTM(units=50)) + model.add(Dropout(0.2)) + model.add(Dense(units=1)) # Prediction of the next price Closing + + model.compile(optimizer="adam", loss='mean_squared_error') + model.fit(x_train, y_train, epochs=25, batch_size=32) + + """ Test Model Accurency on Existing Data """ + + # Load Test Data + test_data = data['Close'].iloc[_end:] + + total_dataset = pd.concat((test_data, test_data), axis=0) + + model_inputs = total_dataset[len(total_dataset) - len(test_data) - prediction_days:].values + model_inputs = model_inputs.reshape(-1, 1) + model_inputs = scaler.transform(model_inputs) + + # Make Predictions on Test Data + + x_test = [] + + for x in range(prediction_days, len(model_inputs)): + x_test.append(model_inputs[x-prediction_days:x, 0]) + + x_test = np.array(x_test) + x_test = np.reshape(x_test, (x_test.shape[0], x_test.shape[1], 1)) + + predicted_prices = model.predict(x_test) + predicted_prices = scaler.inverse_transform(predicted_prices) + + # Plot Predictions + plt.plot(data['Close'].values, color='black', label='Price') + plt.plot(predicted_prices, color='green', label="Predict") + plt.xlabel('Time') + plt.ylabel('Price') + plt.legend() + plt.show() + +if __name__ == '__main__': + tick = "GLE.PA" + ticker = yf.Ticker(tick) + data = ticker.history(period="1y", interval="1d", start="2008-01-01") + pre = MakePrediction(data=data) diff --git a/app/libs/predictions/predictions_2.py b/app/libs/predictions/predictions_2.py new file mode 100644 index 0000000..605f233 --- /dev/null +++ b/app/libs/predictions/predictions_2.py @@ -0,0 +1,90 @@ +import pandas as pd +import numpy as np +import yfinance as yf +from pprint import pprint +import matplotlib.pyplot as plt +from sklearn.preprocessing import MinMaxScaler +from tensorflow.keras.models import Sequential +from tensorflow.keras.layers import Dense, Dropout, LSTM +from keras.callbacks import ModelCheckpoint + +# https://www.youtube.com/watch?v=PuZy9q-aKLw&ab_channel=NeuralNine +class MakePrediction(object): + def __init__(self, data): + + # pprint(data) # 4105 days + prices = data['Close'].values + + print(data.shape) + + prediction_days = 60 + + # Prepare Data + + scaler = MinMaxScaler(feature_range=(0, 1)) + scaled_data = scaler.fit_transform(prices.reshape(-1, 1)) + + # Split 80-20 + train_size = int(len(scaled_data) * 0.80) + test_size = int(len(scaled_data) - train_size) + train = prices[0:train_size] + test = scaled_data[train_size:len(scaled_data)] + + # Method for create features from the time series data + def create_features(data, window_size): + x, y = [], [] + for i in range(len(data) - window_size - 1): + window = data[i:(i + window_size), 0] + x.append(window) + y.append(data[i + window_size, 0]) + return np.array(x), np.array(y) + + # Roughly one month of trading assuming 5 trading days per week + window_size = 20 + x_train, y_train = create_features(train, window_size) + + x_test, y_test = create_features(test, window_size) + + # Reshape to the format of [samples, time steps, features] + x_train = np.reshape(x_train, (x_train.shape[0], 1, x_train.shape[1])) + + x_test = np.reshape(x_test, (x_test.shape[0], 1, x_test.shape[1])) + + scaled_data_shape = scaled_data.shape + train_shape = train.shape + test_shape = test.shape + + # Make sure that the number of rows in the dataset = train rows + test rows + def is_leak(T_shape, train_shape, test_shape): + return not (T_shape[0] == (train_shape[0] + test_shape[0])) + + print(is_leak(scaled_data_shape, train_shape, test_shape)) + + # Building model + model = Sequential() + + model.add(LSTM(units=50, activation='relu', # return_sequences = True, + input_shape=(x_train.shape[1], window_size))) + model.add(Dropout(0.2)) + + # Output layer + model.add(Dense(1)) + model.compile(loss='mean_squared_error', optimizer='adam') + model.fit(x_train, y_train, epochs=25, batch_size=32) + + predicted_prices = model.predict(x_test) + predicted_prices = scaler.inverse_transform(predicted_prices) + + # Plot Predictions + plt.plot(data['Close'].values, color='black', label='Price') + plt.plot(predicted_prices, color='green', label="Predict") + plt.xlabel('Time') + plt.ylabel('Price') + plt.legend() + plt.show() + +if __name__ == '__main__': + tick = "GLE.PA" + ticker = yf.Ticker(tick) + data = ticker.history(period="1y", interval="1d", start="2008-01-01") + pre = MakePrediction(data=data) diff --git a/app/libs/roi_manager.py b/app/libs/roi_manager.py new file mode 100644 index 0000000..f4456ad --- /dev/null +++ b/app/libs/roi_manager.py @@ -0,0 +1,102 @@ +import time +import pyqtgraph as pg +from PySide2 import QtCore, QtWidgets + + +class ROIManager(QtCore.QObject): + def __init__(self, parent=None): + super(ROIManager, self).__init__(parent) + + # Constants + self.current_tool = None + self.current_handle = None + self.current_graph = None + self._graph = self.parent().wgt_graph.graph + + # Signals + self.parent().wgt_graph.signals.sig_graph_clicked.connect( + self._on_roi_add_requested + ) + self.parent().wgt_graph.signals.sig_graph_mouse_moved.connect(self._on_mouse_moved) + # self.parent().wgt_graph.signals.sig_graph_mouse_pressed.connect(self._on_mouse_pressed) + # self.parent().wgt_graph.signals.sig_graph_mouse_released.connect(self._on_mouse_released) + + def drawer(self, graph, event): + """Drawer over the graph + + :param graph: The graph on whch to draw + :type graph: pg.PlotItem + :param event: The event + :type event: object + """ + self.current_graph = graph + vb = graph.vb + mouse_point = vb.mapSceneToView(event.pos()) + if self.current_tool: + self.current_tool(initial_pos=mouse_point) # Exec the current tool + + def bounded_line_drawer(self, initial_pos, **kwargs): + """Draw a bounded line + """ + print('la') + roi = pg.LineSegmentROI((initial_pos, initial_pos), removable=True) + self.current_handle = roi.getHandles()[-1] + self.current_graph.addItem(roi) + roi.sigRemoveRequested.connect(self._on_roi_remove_requested) + + def set_tool(self, **kwargs): + """Set the current tool. kwargs may have a tool which corresponds to + the method to call inside this module. + + :return: The current tool + :rtype: method if found, None instead + """ + self.current_tool = getattr(self, kwargs.get("tool", None), None) + return self.current_tool + + def unset_tool(self): + """Unset the current tool""" + self.current_tool = None + self.current_handle = None + self.current_graph = None + + def remove_roi(self, roi): + """Remove the given roi + + :param roi: The roi to remove + :type roi: pg.ROI + """ + print(roi) + + @QtCore.Slot(object) + def _on_mouse_moved(self, event): + if self.current_handle: + vb = self.current_graph.vb + mousePoint = vb.mapSceneToView(event.pos()) + self.current_handle.setPos(mousePoint) + + @QtCore.Slot(object) + def _on_mouse_released(self, event): + print("Released", event) + + @QtCore.Slot(object) + def _on_mouse_pressed(self, event): + print("Pressed", event) + + @QtCore.Slot(list, object) + def _on_roi_add_requested(self, objects, event): + """Called on a draw requested""" + if not self.current_handle: + if objects: + self.drawer(graph=objects[0], event=event) + else: + self.unset_tool() + + @QtCore.Slot(object) + def _on_roi_remove_requested(self, roi): + """Called on a request for ROI deletion + + :param roi: The roi to remove + :type roi: pg.ROI + """ + self.remove_roi(roi=roi) diff --git a/app/libs/splashcreen.py b/app/libs/splashcreen.py new file mode 100644 index 0000000..2e01c15 --- /dev/null +++ b/app/libs/splashcreen.py @@ -0,0 +1,50 @@ +import os + +from PySide2 import QtCore, QtGui, QtWidgets +from ui.splashscreen import Ui_splashscreen + + +class SplashScreen(QtWidgets.QSplashScreen, Ui_splashscreen): + """This class is a subclass of a QtWidgets.QSplashScreen + + :param QtWidgets: QtWidgets.QSplashScreen object + :type QtWidgets: obj + """ + def __init__(self, path, title): + + pixmap = QtGui.QPixmap(path).scaled(900, 600, + QtCore.Qt.KeepAspectRatio) + + super(SplashScreen, self).__init__(pixmap, QtCore.Qt.WindowStaysOnTopHint) + + self.setupUi(self) + + self.lab_app_name.setText(title) + self.splash_color = QtGui.QColor(100, 100, 100) + self.setWindowFlags( + QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.FramelessWindowHint + ) + self.center() + + blur = QtWidgets.QGraphicsDropShadowEffect(self) + blur.setBlurRadius(10) + blur.setColor(QtGui.QColor(0, 0, 0)) + self.setGraphicsEffect(blur) + + def show_message(self, message=None): + """Thi function shows the message in the splashscreen + + :param message: The message to show, defaults to None + :type message: str, optional + """ + self.showMessage( + message, + QtCore.Qt.AlignBottom | QtCore.Qt.AlignHCenter, + self.splash_color, + ) + + def center(self): + qRect = self.frameGeometry() + centerPoint = QtWidgets.QDesktopWidget().availableGeometry().center() + qRect.moveCenter(centerPoint) + self.move(qRect.topLeft()) \ No newline at end of file diff --git a/app/libs/tradingview_ta/__init__.py b/app/libs/tradingview_ta/__init__.py new file mode 100644 index 0000000..f90cfeb --- /dev/null +++ b/app/libs/tradingview_ta/__init__.py @@ -0,0 +1,2 @@ +from .main import TA_Handler, TradingView, Analysis, Interval, Exchange, __version__ +from .technicals import Recommendation, Compute diff --git a/app/libs/tradingview_ta/main.py b/app/libs/tradingview_ta/main.py new file mode 100644 index 0000000..fec22dd --- /dev/null +++ b/app/libs/tradingview_ta/main.py @@ -0,0 +1,338 @@ +# Tradingview Technical Analysis (tradingview-ta) +# Author: deathlyface (https://github.com/deathlyface) +# License: MIT + +import re +import requests, json, datetime, warnings +from pprint import pprint +from .technicals import Compute + +__version__ = "3.2.3" + + +class Analysis(object): + exchange = "" + symbol = "" + screener = "" + time = "" + interval = "" + summary = {} + oscillators = {} + moving_averages = {} + indicators = {} + + +class Interval: + INTERVAL_1_MINUTE = "1m" + INTERVAL_5_MINUTES = "5m" + INTERVAL_15_MINUTES = "15m" + INTERVAL_1_HOUR = "1h" + INTERVAL_4_HOURS = "4h" + INTERVAL_1_DAY = "1d" + INTERVAL_1_WEEK = "1W" + INTERVAL_1_MONTH = "1M" + + +class Exchange: + FOREX = "FX_IDC" + CFD = "TVC" + + +class TradingView: + scan_url = "https://scanner.tradingview.com/" + indicators = ["Recommend.Other{}", "Recommend.All{}", "Recommend.MA{}", "RSI{}", "RSI[1]{}", "Stoch.K{}", + "Stoch.D{}", "Stoch.K[1]{}", "Stoch.D[1]{}", "CCI20{}", "CCI20[1]{}", "ADX{}", "ADX+DI{}", "ADX-DI{}", + "ADX+DI[1]{}", "ADX-DI[1]{}", "AO{}", "AO[1]{}", "Mom{}", "Mom[1]{}", "MACD.macd{}", "MACD.signal{}", + "Rec.Stoch.RSI{}", "Stoch.RSI.K{}", "Rec.WR{}", "W.R{}", "Rec.BBPower{}", "BBPower{}", "Rec.UO{}", + "UO{}", "close{}", "EMA5{}", "SMA5{}", "EMA10{}", "SMA10{}", "EMA20{}", "SMA20{}", "EMA30{}", + "SMA30{}", "EMA50{}", "SMA50{}", "EMA100{}", "SMA100{}", "EMA200{}", "SMA200{}", "Rec.Ichimoku{}", + "Ichimoku.BLine{}", "Rec.VWMA{}", "VWMA{}", "Rec.HullMA9{}", "HullMA9{}", "Pivot.M.Classic.S3{}", + "Pivot.M.Classic.S2{}", "Pivot.M.Classic.S1{}", "Pivot.M.Classic.Middle{}", "Pivot.M.Classic.R1{}", + "Pivot.M.Classic.R2{}", "Pivot.M.Classic.R3{}", "Pivot.M.Fibonacci.S3{}", "Pivot.M.Fibonacci.S2{}", + "Pivot.M.Fibonacci.S1{}", "Pivot.M.Fibonacci.Middle{}", "Pivot.M.Fibonacci.R1{}", + "Pivot.M.Fibonacci.R2{}", "Pivot.M.Fibonacci.R3{}", "Pivot.M.Camarilla.S3{}", + "Pivot.M.Camarilla.S2{}", "Pivot.M.Camarilla.S1{}", "Pivot.M.Camarilla.Middle{}", + "Pivot.M.Camarilla.R1{}", "Pivot.M.Camarilla.R2{}", "Pivot.M.Camarilla.R3{}", "Pivot.M.Woodie.S3{}", + "Pivot.M.Woodie.S2{}", "Pivot.M.Woodie.S1{}", "Pivot.M.Woodie.Middle{}", "Pivot.M.Woodie.R1{}", + "Pivot.M.Woodie.R2{}", "Pivot.M.Woodie.R3{}", "Pivot.M.Demark.S1{}", "Pivot.M.Demark.Middle{}", + "Pivot.M.Demark.R1{}", "open{}", "P.SAR{}", "BB.lower{}", "BB.upper{}"] + + def data(symbol, interval): + """Format TradingView's Scanner Post Data + + Args: + symbol (string): EXCHANGE:SYMBOL (ex: NASDAQ:AAPL or BINANCE:BTCUSDT) + interval (string): Time Interval (ex: 1m, 5m, 15m, 1h, 4h, 1d, 1W, 1M) + + Returns: + string: JSON object as a string. + """ + if interval == "1m": + # 1 Minute + data_interval = "|1" + elif interval == "5m": + # 5 Minutes + data_interval = "|5" + elif interval == "15m": + # 15 Minutes + data_interval = "|15" + elif interval == "1h": + # 1 Hour + data_interval = "|60" + elif interval == "4h": + # 4 Hour + data_interval = "|240" + elif interval == "1W": + # 1 Week + data_interval = "|1W" + elif interval == "1M": + # 1 Month + data_interval = "|1M" + else: + if interval != '1d': + warnings.warn("Interval is empty or not valid, defaulting to 1 day.") + # Default, 1 Day + data_interval = "" + + data_json = {"symbols": {"tickers": [symbol.upper()], "query": {"types": []}}, + "columns": [x.format(data_interval) for x in TradingView.indicators]} + + return data_json + + +class TA_Handler(object): + screener = "" + exchange = "" + symbol = "" + interval = "" + timeout = None + + def __init__(self, screener="", exchange="", symbol="", interval="", timeout=None): + """Create an instance of TA_Handler class + + Args: + screener (str, required): Screener (see documentation and tradingview's site). + exchange (str, required): Exchange (see documentation and tradingview's site). + symbol (str, required): Abbreviation of a stock or currency (see documentation and tradingview's site). + interval (str, optional): See the interval class and the documentation. Defaults to 1 day. + timeout (float, optional): Timeout for requests (in seconds). Defaults to None. + """ + self.screener = screener + self.exchange = exchange + self.symbol = symbol + self.interval = interval + self.timeout = timeout + + def get_screener_from_symabol(self, symbol): + countries = ['france', 'america'] + for country in countries: + try: + scan_url = TradingView.scan_url + country.lower() + "/scan" + headers = {"User-Agent": "tradingview_ta/{}".format(__version__)} + response = requests.post(scan_url, headers=headers) + result = json.loads(response.text)["data"] + for ticks in result: + ticker = ticks['s'] + + if ticker.split(':')[1].startswith(symbol): + if ticker.split(':')[1].endswith(symbol): + return country, ticker.split(':')[0] + except: + raise Exception("Exchange not found for this symbol.") + + # Set functions + def set_screener_as_stock(self, country): + """Set the screener as a country (for stocks). + + Args: + country (string): Stock's country (ex: If NFLX or AAPL, then "america" is the screener) + """ + self.screener = country + + def set_screener_as_crypto(self): + """Set the screener as crypto (for cryptocurrencies). + """ + self.screener = "crypto" + + def set_screener_as_cfd(self): + """Set the screener as cfd (contract for differences). + """ + self.screener = "cfd" + + def set_screener_as_forex(self): + """Set the screener as forex. + """ + self.screener = "forex" + + def set_exchange_as_crypto_or_stock(self, exchange): + """Set the exchange + + Args: + exchange (string): Stock/Crypto's exchange (NASDAQ, NYSE, BINANCE, BITTREX, etc). + """ + self.exchange = exchange + + def set_exchange_as_forex(self): + """Set the exchange as FX_IDC for forex. + """ + self.exchange = "FX_IDC" + + def set_exchange_as_cfd(self): + """Set the exchange as TVC for cfd. + """ + self.exchange = "TVC" + + def set_interval_as(self, intvl): + """Set the interval. + + Refer to: https://python-tradingview-ta.readthedocs.io/en/latest/usage.html#setting-the-interval + + Args: + intvl (string): interval. You can use values from the Interval class. + + """ + self.interval = intvl + + def set_symbol_as(self, symbol): + """Set the symbol. + + Refer to: https://python-tradingview-ta.readthedocs.io/en/latest/usage.html#setting-the-symbol + + Args: + symbol (string): abbreviation of a stock or currency (ex: NFLX, AAPL, BTCUSD). + """ + self.symbol = symbol + + # Get analysis + def get_analysis(self): + """Get analysis from TradingView and compute it. + + Returns: + Analysis: Contains information about the analysis. + """ + if self.screener == "" or type(self.screener) != str: + raise Exception("Screener is empty or not valid.") + elif self.exchange == "" or type(self.exchange) != str: + raise Exception("Exchange is empty or not valid.") + elif self.symbol == "" or type(self.symbol) != str: + raise Exception("Symbol is empty or not valid.") + + exch_smbl = self.exchange.upper() + ":" + self.symbol.upper() + data = TradingView.data(exch_smbl, self.interval) + scan_url = TradingView.scan_url + self.screener.lower() + "/scan" + headers = {"User-Agent": "tradingview_ta/{}".format(__version__)} + # https://scanner.tradingview.com/france/scan + response = requests.post(scan_url, json=data, headers=headers, timeout=self.timeout) + + # Return False if can't get data + if response.status_code != 200: + raise Exception("Can't access TradingView's API. HTTP status code: {}.".format(response.status_code)) + + result = json.loads(response.text)["data"] + if result != []: + indicator_values = result[0]["d"] + else: + raise Exception("Exchange or symbol not found.") + + oscillators_counter, ma_counter = {"BUY": 0, "SELL": 0, "NEUTRAL": 0}, {"BUY": 0, "SELL": 0, "NEUTRAL": 0} + computed_oscillators, computed_ma = {}, {} + + # RECOMMENDATIONS + recommend_oscillators = Compute.Recommend(indicator_values[0]) + recommend_summary = Compute.Recommend(indicator_values[1]) + recommend_moving_averages = Compute.Recommend(indicator_values[2]) + + # OSCILLATORS + # RSI (14) + if None not in indicator_values[3:5]: + computed_oscillators["RSI"] = Compute.RSI(indicator_values[3], indicator_values[4]) + oscillators_counter[computed_oscillators["RSI"]] += 1 + # Stoch %K + if None not in indicator_values[5:9]: + computed_oscillators["STOCH.K"] = Compute.Stoch(indicator_values[5], indicator_values[6], + indicator_values[7], indicator_values[8]) + oscillators_counter[computed_oscillators["STOCH.K"]] += 1 + # CCI (20) + if None not in indicator_values[9:11]: + computed_oscillators["CCI"] = Compute.CCI20(indicator_values[9], indicator_values[10]) + oscillators_counter[computed_oscillators["CCI"]] += 1 + # ADX (14) + if None not in indicator_values[11:16]: + computed_oscillators["ADX"] = Compute.ADX(indicator_values[11], indicator_values[12], indicator_values[13], + indicator_values[14], indicator_values[15]) + oscillators_counter[computed_oscillators["ADX"]] += 1 + # AO + if None not in indicator_values[16:18]: + computed_oscillators["AO"] = Compute.AO(indicator_values[16], indicator_values[17]) + oscillators_counter[computed_oscillators["AO"]] += 1 + # Mom (10) + if None not in indicator_values[18:20]: + computed_oscillators["Mom"] = Compute.Mom(indicator_values[18], indicator_values[19]) + oscillators_counter[computed_oscillators["Mom"]] += 1 + # MACD + if None not in indicator_values[20:22]: + computed_oscillators["MACD"] = Compute.MACD(indicator_values[20], indicator_values[21]) + oscillators_counter[computed_oscillators["MACD"]] += 1 + # Stoch RSI + if indicator_values[22] != None: + computed_oscillators["Stoch.RSI"] = Compute.Simple(indicator_values[22]) + oscillators_counter[computed_oscillators["Stoch.RSI"]] += 1 + # W%R + if indicator_values[24] != None: + computed_oscillators["W%R"] = Compute.Simple(indicator_values[24]) + oscillators_counter[computed_oscillators["W%R"]] += 1 + # BBP + if indicator_values[26] != None: + computed_oscillators["BBP"] = Compute.Simple(indicator_values[26]) + oscillators_counter[computed_oscillators["BBP"]] += 1 + # UO + if indicator_values[28] != None: + computed_oscillators["UO"] = Compute.Simple(indicator_values[28]) + oscillators_counter[computed_oscillators["UO"]] += 1 + + # MOVING AVERAGES + ma_list = ["EMA10", "SMA10", "EMA20", "SMA20", "EMA30", "SMA30", "EMA50", "SMA50", "EMA100", "SMA100", "EMA200", + "SMA200"] + close = indicator_values[30] + ma_list_counter = 0 + for index in range(33, 45): + if indicator_values[index] != None: + computed_ma[ma_list[ma_list_counter]] = Compute.MA(indicator_values[index], close) + ma_counter[computed_ma[ma_list[ma_list_counter]]] += 1 + ma_list_counter += 1 + + # MOVING AVERAGES, pt 2 + # ICHIMOKU + if indicator_values[45] != None: + computed_ma["Ichimoku"] = Compute.Simple(indicator_values[45]) + ma_counter[computed_ma["Ichimoku"]] += 1 + # VWMA + if indicator_values[47] != None: + computed_ma["VWMA"] = Compute.Simple(indicator_values[47]) + ma_counter[computed_ma["VWMA"]] += 1 + # HullMA (9) + if indicator_values[49] != None: + computed_ma["HullMA"] = Compute.Simple(indicator_values[49]) + ma_counter[computed_ma["HullMA"]] += 1 + + analysis = Analysis() + analysis.screener = self.screener + analysis.exchange = self.exchange + analysis.symbol = self.symbol + analysis.interval = self.interval + analysis.time = datetime.datetime.now() + + for x in range(len(indicator_values)): + analysis.indicators[TradingView.indicators[x].format("")] = indicator_values[x] + + analysis.oscillators = {"RECOMMENDATION": recommend_oscillators, "BUY": oscillators_counter["BUY"], + "SELL": oscillators_counter["SELL"], "NEUTRAL": oscillators_counter["NEUTRAL"], + "COMPUTE": computed_oscillators} + analysis.moving_averages = {"RECOMMENDATION": recommend_moving_averages, "BUY": ma_counter["BUY"], + "SELL": ma_counter["SELL"], "NEUTRAL": ma_counter["NEUTRAL"], + "COMPUTE": computed_ma} + analysis.summary = {"RECOMMENDATION": recommend_summary, "BUY": oscillators_counter["BUY"] + ma_counter["BUY"], + "SELL": oscillators_counter["SELL"] + ma_counter["SELL"], + "NEUTRAL": oscillators_counter["NEUTRAL"] + ma_counter["NEUTRAL"]} + + return analysis diff --git a/app/libs/tradingview_ta/technicals.py b/app/libs/tradingview_ta/technicals.py new file mode 100644 index 0000000..e43bb42 --- /dev/null +++ b/app/libs/tradingview_ta/technicals.py @@ -0,0 +1,239 @@ +# Tradingview Technical Analysis (tradingview-ta) +# Author: deathlyface (https://github.com/deathlyface) +# Rewritten from https://www.tradingview.com/static/bundles/technicals.f2e6e6a51aebb6cd46f8.js +# License: MIT + +class Recommendation: + buy = "BUY" + strong_buy = "STRONG_BUY" + sell = "SELL" + strong_sell = "STRONG_SELL" + neutral = "NEUTRAL" + error = "ERROR" + +class Compute: + def MA(ma, close): + """Compute Moving Average + + Args: + ma (float): MA value + close (float): Close value + + Returns: + string: "BUY", "SELL", or "NEUTRAL" + """ + if (ma < close): + return Recommendation.buy + elif (ma > close): + return Recommendation.sell + else: + return Recommendation.neutral + + def RSI(rsi, rsi1): + """Compute Relative Strength Index + + Args: + rsi (float): RSI value + rsi1 (float): RSI[1] value + + Returns: + string: "BUY", "SELL", or "NEUTRAL" + """ + if (rsi < 30 and rsi1 > rsi): + return Recommendation.buy + elif (rsi > 70 and rsi1 < rsi): + return Recommendation.sell + else: + return Recommendation.neutral + + def Stoch(k, d, k1, d1): + """Compute Stochastic + + Args: + k (float): Stoch.K value + d (float): Stoch.D value + k1 (float): Stoch.K[1] value + d1 (float): Stoch.D[1] value + + Returns: + string: "BUY", "SELL", or "NEUTRAL" + """ + if (k < 20 and d < 20 and k > d and k1 < d1): + return Recommendation.buy + elif (k > 80 and d > 80 and k < d and k1 > d1): + return Recommendation.sell + else: + return Recommendation.neutral + + def CCI20(cci20, cci201): + """Compute Commodity Channel Index 20 + + Args: + cci20 (float): CCI20 value + cci201 ([type]): CCI20[1] value + + Returns: + string: "BUY", "SELL", or "NEUTRAL" + """ + if (cci20 < -100 and cci20 > cci201): + return Recommendation.buy + elif (cci20 > 100 and cci20 < cci201): + return Recommendation.sell + else: + return Recommendation.neutral + + def ADX(adx, adxpdi, adxndi, adxpdi1, adxndi1): + """Compute Average Directional Index + + Args: + adx (float): ADX value + adxpdi (float): ADX+DI value + adxndi (float): ADX-DI value + adxpdi1 (float): ADX+DI[1] value + adxndi1 (float): ADX-DI[1] value + + Returns: + string: "BUY", "SELL", or "NEUTRAL" + """ + if (adx > 20 and adxpdi1 < adxndi1 and adxpdi > adxndi): + return Recommendation.buy + elif (adx > 20 and adxpdi1 > adxndi1 and adxpdi < adxndi): + return Recommendation.sell + else: + return Recommendation.neutral + + def AO(ao, ao1): + """Compute Awesome Oscillator + + Args: + ao (float): AO value + ao1 (float): AO[1] value + + Returns: + string: "BUY", "SELL", or "NEUTRAL" + """ + if (ao > 0 and ao1 < 0 or ao > 0 and ao1 > 0 and ao > ao1): + return Recommendation.buy + elif (ao < 0 and ao1 > 0 or ao < 0 and ao1 < 0 and ao < ao1): + return Recommendation.sell + else: + return Recommendation.neutral + + def Mom(mom, mom1): + """Compute Momentum + + Args: + mom (float): Mom value + mom1 (float): Mom[1] value + + Returns: + string: "BUY", "SELL", or "NEUTRAL" + """ + if (mom < mom1): + return Recommendation.sell + elif (mom > mom1): + return Recommendation.buy + else: + return Recommendation.neutral + + def MACD(macd, signal): + """Compute Moving Average Convergence/Divergence + + Args: + macd (float): MACD.macd value + signal (float): MACD.signal value + + Returns: + string: "BUY", "SELL", or "NEUTRAL" + """ + if (macd > signal): + return Recommendation.buy + elif (macd < signal): + return Recommendation.sell + else: + return Recommendation.neutral + + def BBBuy(close, bblower): + """Compute Bull Bear Buy + + Args: + close (float): close value + bblower (float): BB.lower value + + Returns: + string: "BUY", "SELL", or "NEUTRAL" + """ + if (close < bblower): + return Recommendation.buy + else: + return Recommendation.neutral + + def BBSell(close, bbupper): + """Compute Bull Bear Sell + + Args: + close (float): close value + bbupper (float): BB.upper value + + Returns: + string: "BUY", "SELL", or "NEUTRAL" + """ + if (close > bbupper): + return Recommendation.sell + else: + return Recommendation.neutral + + def PSAR(psar, open): + """Compute Parabolic Stop-And-Reverse + + Args: + psar (float): P.SAR value + open (float): open value + + Returns: + string: "BUY", "SELL", or "NEUTRAL" + """ + if (psar < open): + return Recommendation.buy + elif (psar > open): + return Recommendation.sell + else: + return Recommendation.neutral + + def Recommend(value): + """Compute Recommend + + Args: + value (float): recommend value + + Returns: + string: "STRONG_BUY", "BUY", "NEUTRAL", "SELL", "STRONG_SELL", or "ERROR" + """ + if (value >= -1 and value < -.5): + return Recommendation.strong_sell + elif (value >= -.5 and value < 0): + return Recommendation.sell + elif (value == 0): + return Recommendation.neutral + elif (value > 0 and value <= .5): + return Recommendation.buy + elif (value > .5 and value <= 1): + return Recommendation.strong_buy + else: + return Recommendation.error + + def Simple(value): + """Compute Simple + + Args: + value (float): Rec.X value + + Returns: + string: "BUY", "SELL", or "NEUTRAL" + """ + if (value == -1): + return Recommendation.sell + elif (value == 1): + return Recommendation.buy + else: + return Recommendation.neutral diff --git a/app/libs/welcome_widget.py b/app/libs/welcome_widget.py new file mode 100644 index 0000000..18bfb9f --- /dev/null +++ b/app/libs/welcome_widget.py @@ -0,0 +1,36 @@ +# +# This class set the default Welcome Page. +# +from pprint import pprint +from PySide2 import QtWidgets +from libs.articles.yahoo_articles import ArticlesYahoo +from libs.widgets.article_itemwidget import ArticlesWidgetItem + + +class WelcomeWidget(QtWidgets.QListWidget): + """ + This is loading all articles from the main Yahoo Page. + https://finance.yahoo.com/ + """ + def __init__(self, parent=None): + super(WelcomeWidget, self).__init__(parent) + + # articles = self._get_articles_dict()[:5] + # + # for index, i in enumerate(articles): + # article = ArticlesWidgetItem(parent=self) + # article.set_title(i["title"], i["link"]) + # article.set_date(i["published"]) + # article.set_description(i["summary"]) + # article.set_thumbnail(i["img"]) + # item = QtWidgets.QListWidgetItem() + # item.setSizeHint(article.sizeHint()) + # self.addItem(item) + # self.setItemWidget(item, article) + + def _get_articles_dict(self): + article = ArticlesYahoo() + return article.get_home_articles() + + + diff --git a/app/libs/widgets/article_itemwidget.py b/app/libs/widgets/article_itemwidget.py new file mode 100644 index 0000000..f706f70 --- /dev/null +++ b/app/libs/widgets/article_itemwidget.py @@ -0,0 +1,71 @@ + +import os +from utils import utils +from ui.article import Ui_Article +from libs.thread_pool import ThreadPool +from PySide2 import QtWidgets, QtCore, QtGui + +class ArticlesWidgetItem(QtWidgets.QWidget, Ui_Article): + def __init__(self, parent=None): + super(ArticlesWidgetItem, self).__init__(parent) + self.setupUi(self) + self.thread_pool = ThreadPool() + self.thread_pool.signals.sig_thread_result.connect( + self._on_thumbnail_available + ) + + def set_compagny(self, text): + """ + Set Compagny name on the article. + """ + self.lb_compagny.setText(text) + self.lb_compagny.setFont(QtGui.QFont("Times", 12)) + + def set_title(self, text, link): + """ + Set Title of the article and link clickable. + """ + self.lb_title.setText(text) + self.lb_title.set_link(link) + self.lb_title.set_font_size(size=14) + + def set_date(self, text): + """ + Set date published article. + """ + self.lb_date.setText(text) + + def set_description(self, text): + """ + Set description of the article. + """ + self.desc.setText(text) + self.desc.setFont(QtGui.QFont("Times", 10)) + + def set_thumbnail(self, link): + if link: + self.thread_pool.execution( + function=utils.get_image_from_url, url=link + ) + else: + path = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + image = os.path.join(path, 'resources', 'img', "no_file.png") + image = QtGui.QPixmap(image) + # image = QtGui.QPixmap(":/img/no_file.png") + self._on_thumbnail_available(image) + + + @QtCore.Slot(object) + def _on_thumbnail_available(self, image): + """Called when a thumbnail is available for the company + + :param image: The thumbnail + :type image: QPixmap + """ + image = image.scaled(self.size(), + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + ) + self.thumbnail.setPixmap(image) + + diff --git a/app/libs/widgets/label.py b/app/libs/widgets/label.py new file mode 100644 index 0000000..5f7e97d --- /dev/null +++ b/app/libs/widgets/label.py @@ -0,0 +1,18 @@ +import webbrowser +from PySide2 import QtWidgets, QtGui + +class LabelTitle(QtWidgets.QLabel): + def __init__(self, link=None, size=20): + super(LabelTitle, self).__init__() + + self.font = "Times" + self.set_font_size(size) + + def set_font_size(self, size): + self.setFont(QtGui.QFont(self.font, size)) + + def set_link(self, link): + self.link = link + + def mousePressEvent(self, event) -> None: + webbrowser.open(self.link) diff --git a/app/libs/widgets/sentimentals_widget.py b/app/libs/widgets/sentimentals_widget.py new file mode 100644 index 0000000..0c8c6d7 --- /dev/null +++ b/app/libs/widgets/sentimentals_widget.py @@ -0,0 +1,84 @@ +from utils import utils +from ui.sentimentals import Ui_Sentiment_Form +from PySide2 import QtCore, QtGui, QtWidgets +from libs.tradingview_ta import TA_Handler, Interval, Exchange + +TICKERS = ['TSLA', 'AAPL', 'MSFT', 'GLE.PA', 'FP.PA'] + +LVLS = { + "STRONG_SELL": 10, + "SELL": 25, + "NEUTRAL": 50, + "BUY": 75, + "STRONG_BUY": 90, +} + + +class Sentimental_Widget(QtWidgets.QWidget): + def __init__(self, parent=None, ticker=None): + super(Sentimental_Widget, self).__init__(parent=parent) + + layout = QtWidgets.QVBoxLayout(self) + + for ticker in TICKERS: + item = self.get_widget_items(ticker) + layout.addWidget(item) + + def get_widget_items(self, tick): + widget = Sentimental_Widget_Item() + widget.set_sentimental_ui(ticker=tick) + return widget + + +class Sentimental_Widget_Item(QtWidgets.QWidget, Ui_Sentiment_Form): + def __init__(self, parent=None): + super(Sentimental_Widget_Item, self).__init__(parent=parent) + self.setupUi(self) + + def set_sentimental_ui(self, ticker): + sentiment = self.get_sentiments(ticker) + prc = LVLS[sentiment['RECOMMENDATION']] + compagny_name = utils.get_compagny_name_from_tick(ticker) + + self.horizontalSlider.setValue(prc) + self.horizontalSlider.setEnabled(False) + self.label.setText(compagny_name) + self.label.setFont(QtGui.QFont("Times", 12)) + + def get_sentiments(self, tick): + """This method get the Sentiment from the Sentimentals Class + """ + sentiment = Sentimentals(tick) + sentiment = sentiment.get_senti() + return sentiment + + +class Sentimentals(object): + """ + This is returning the sentiment for a compagny. + Going on TradingView, look for indicator if it s a buying, selling or neutral. + + :return: {'RECOMMENDATION': 'BUY', 'BUY: 15, 'SELL': 6, 'NEUTRAL': 4} + """ + + def __init__(self, ticker): + self.handler = TA_Handler() + + # IF + ticker = utils.check_french_ticker(ticker) + + country, screener = self.handler.get_screener_from_symabol(ticker) + + self.handler.set_symbol_as(ticker) + self.handler.set_screener_as_stock(country) + self.handler.set_exchange_as_crypto_or_stock(screener) + self.handler.set_interval_as(Interval.INTERVAL_1_DAY) + + def get_senti(self): + return self.handler.get_analysis().summary + + +if __name__ == '__main__': + ticker = "FP.PA" + x = Sentimentals(ticker=ticker) + print(x.get_senti()) diff --git a/app/libs/widgets/textbrowser.py b/app/libs/widgets/textbrowser.py new file mode 100644 index 0000000..009ef23 --- /dev/null +++ b/app/libs/widgets/textbrowser.py @@ -0,0 +1,24 @@ +from PySide2 import QtWidgets, QtGui, QtCore + + +class Description(QtWidgets.QTextEdit): + def __init__(self, text): + super(Description, self).__init__() + self.setStyleSheet("background-color: rgba(0, 0, 0, 0);") + + size_policy = self.sizePolicy() + document = self.document() + margins = self.contentsMargins() + # document_width = size_policy - margins.left() - margins.right() + # print(QtCore.QSize(size_policy)) + # print(document.setTextWidth(document_width)) + + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + # self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed) + + self.setFixedHeight(self.document().size().height() + self.contentsMargins().top() * 2) + + if text: + self.setText("text") + self.setFont(QtGui.QFont("Times", 10)) diff --git a/app/libs/widgets/toolbar.py b/app/libs/widgets/toolbar.py index bdb1b34..97b4638 100644 --- a/app/libs/widgets/toolbar.py +++ b/app/libs/widgets/toolbar.py @@ -28,7 +28,7 @@ def __init__(self, parent=None): def _trigger_action(self): """Called on action triggered""" sender = self.sender() - self.signals.sig_action_triggered.emit(sender.action) + self.signals.sig_action_triggered.emit(sender.action, sender.args) def init_toolbar(self): """This method inits the toolbar of the app""" @@ -136,6 +136,7 @@ def __init__(self, parent=None, text=None, action=None, **kwargs): # Properties self.action = action + self.args = kwargs.get("args", {}) icon = kwargs.get("icon", None) if icon: diff --git a/app/libs/yahoo_fin/news.py b/app/libs/yahoo_fin/news.py index 3a791cc..0abe32c 100644 --- a/app/libs/yahoo_fin/news.py +++ b/app/libs/yahoo_fin/news.py @@ -1,10 +1,18 @@ import feedparser -yf_rss_url = "https://feeds.finance.yahoo.com/rss/2.0/headline?s=%s®ion=US&lang=en-US" +yf_rss_url = "https://feeds.finance.yahoo.com/rss/2.0/headline?s=%s®ion=FR&lang=fr-FR" +home_page = "https://finance.yahoo.com/news/rssr/2.0/" def get_yf_rss(ticker): - feed = feedparser.parse(yf_rss_url % ticker) + feeds = feedparser.parse(yf_rss_url % ticker) - return feed.entries + return feeds.entries + + +def get_yf_home_rss(): + + feeds = feedparser.parse(home_page) + + return feeds.entries \ No newline at end of file diff --git a/app/libs/yahoo_fin/stock_info.py b/app/libs/yahoo_fin/stock_info.py index 506c973..d532964 100644 --- a/app/libs/yahoo_fin/stock_info.py +++ b/app/libs/yahoo_fin/stock_info.py @@ -5,6 +5,7 @@ import re import json import datetime +from bs4 import BeautifulSoup try: from requests_html import HTMLSession @@ -227,14 +228,14 @@ def tickers_other(include_company_data=False): return tickers -def tickers_dow(include_company_data=False): +def _tickers_dow_(include_company_data=False): """Downloads list of currently traded tickers on the Dow""" site = "https://finance.yahoo.com/quote/%5EDJI/components?p=%5EDJI" table = pd.read_html(site)[0] - + print(table) if include_company_data: return table @@ -245,6 +246,23 @@ def tickers_dow(include_company_data=False): return dow_tickers +def tickers_dow(): + + site = "https://en.wikipedia.org/wiki/Dow_Jones_Industrial_Average" + head = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36" + + request = requests.get(site, headers={"user-agent": head}) + soup = BeautifulSoup(request.text, 'html.parser') + + ul = soup.find("table", {"class": "wikitable sortable"}) + title = ul.findAll("th", {"scope": "row"}) + tickers = ul.findAll("a", {"class": "external text"}) + + dow_tickers = { + key: value + for key, value in zip([i.text for i in tickers], [i.text.strip() for i in title]) + } + return dow_tickers def tickers_ibovespa(include_company_data=False): diff --git a/app/resources/img/no_file.png b/app/resources/img/no_file.png new file mode 100644 index 0000000..bd0ad99 Binary files /dev/null and b/app/resources/img/no_file.png differ diff --git a/app/resources/img/splashscreen.jpg b/app/resources/img/splashscreen.jpg new file mode 100644 index 0000000..ed53ac8 Binary files /dev/null and b/app/resources/img/splashscreen.jpg differ diff --git a/app/resources/img/splashscreen_2.jpg b/app/resources/img/splashscreen_2.jpg new file mode 100644 index 0000000..be99274 Binary files /dev/null and b/app/resources/img/splashscreen_2.jpg differ diff --git a/app/resources/resources.qrc b/app/resources/resources.qrc index d05714d..f5c1a54 100644 --- a/app/resources/resources.qrc +++ b/app/resources/resources.qrc @@ -27,8 +27,9 @@ svg/line.svg svg/dot-line.svg svg/dash-line.svg - style/style.qss toolbar/toolbar.json + img/no_file.png + img/splashscreen.jpg diff --git a/app/resources/resources_rc.py b/app/resources/resources_rc.py new file mode 100644 index 0000000..e69de29 diff --git a/app/resources/style/style.qss b/app/resources/style/style.qss index b637ede..34535a0 100644 --- a/app/resources/style/style.qss +++ b/app/resources/style/style.qss @@ -44,6 +44,20 @@ QTabBar::tab:selected:disabled { border-bottom: 2px solid palette(light); } + +/* + * QTabWidget + */ + +QTabWidget#tab_settings { + background: transparent; + border: none; +} + +QWidget#wgt_settings_input,#wgt_settings_styles { + background: palette(base); +} + /* * QScrollBar */ @@ -132,7 +146,7 @@ QSlider::handle:horizontal { } QSlider::add-page:horizontal { - background: palette(base); + background: palette(base); } QSlider::sub-page:horizontal { @@ -143,6 +157,25 @@ QSlider::sub-page:horizontal:disabled { background-color: palette(light); } +QSlider::sub-page#horizontalSlider:horizontal:disabled{ + border-radius: 5px; + background:transparent; +} + +QSlider::sub-page#horizontalSlider:horizontal:disabled{ + background-color: rgb(42, 218, 130); + background-color: qlineargradient(spread:pad, + x1:0, y1:0, x2:1, y2:0, + stop:0 rgb(61, 217, 245), + stop:1 rgb(42, 130, 218)); +} + +QSlider::handle#horizontalSlider:horizontal:disabled{ + +} + +/* QTableView */ + QTableView { background-color: palette(link-visited); alternate-background-color: palette(midlight); @@ -175,3 +208,52 @@ QDockWidget { titlebar-close-icon: url(:/svg/times.svg); titlebar-normal-icon: url(:/svg/window.svg); } + +/* TableWidget */ +QTableWidget#wid_table_financ,#tab_settings { + background:transparent; + border-style: none; +} + +/* QListWidget */ + +QListWidget#wdg_article_welcome,#wgt_articles,#lst_inputs { + background:transparent; + border-style: none; +} + +/* QPushButton */ +QPushButton#pub_cancel,#pub_ok,#pub_reset{ + background: palette(midlight); + height:20px; + border-radius:3px; +} + +/* QComboBox */ +QComboBox#cob_value_list,#cob_line_style{ + background: palette(midlight); + border-radius:3px; + width: 50px; + height:20px; +} + +/* QDoubleSpinBox */ +QDoubleSpinBox{ + background: palette(midlight); + border-radius:3px; + width: 40px; + height:20px; +} + + +/* QSpinBox */ +QSpinBox{ + background: palette(midlight); + border-radius:3px; + width: 40px; + height:20px; +} + +SetSysColors{ + border: 1px solid black; +} \ No newline at end of file diff --git a/app/resources/toolbar/toolbar.json b/app/resources/toolbar/toolbar.json index f3f4d7b..dd5c0bd 100644 --- a/app/resources/toolbar/toolbar.json +++ b/app/resources/toolbar/toolbar.json @@ -16,7 +16,10 @@ "type": "resources", "value": ":/svg/trend-line.svg" }, - "action": "method_to_call", + "action": "roi_manager.set_tool", + "args": { + "tool": "bounded_line_drawer" + }, "tooltip": "Trend Line", "text": "Trend Line", "shortcut": "Alt+T", diff --git a/app/ui/__convert.py b/app/ui/__convert.py index 9e6c83e..78e4af8 100644 --- a/app/ui/__convert.py +++ b/app/ui/__convert.py @@ -1,6 +1,6 @@ import subprocess -cmd = "C:\\Users\\vince\\AppData\\Roaming\\Python\\Python39\\Scripts\\pyside2-uic.exe main_window.ui > main_window.py" +cmd = "C:\\Users\\vince\\AppData\\Roaming\\Python\\Python39\\Scripts\\pyside2-uic.exe splashscreen.ui > splashscreen.py" # cmd = "C:\\Users\\vince\\AppData\\Roaming\\Python\\Python39\\Scripts\\pyside2-uic.exe search.ui > search.py" - +# main_window markets_widget article indicator_settings_dialog sentimentals splashscreen subprocess.run(cmd, shell=True) diff --git a/app/ui/article.py b/app/ui/article.py new file mode 100644 index 0000000..61573c3 --- /dev/null +++ b/app/ui/article.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'article.ui' +## +## Created by: Qt User Interface Compiler version 5.15.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide2.QtCore import * +from PySide2.QtGui import * +from PySide2.QtWidgets import * + +from libs.widgets.label import LabelTitle + + +class Ui_Article(object): + def setupUi(self, Article): + if not Article.objectName(): + Article.setObjectName(u"Article") + Article.resize(638, 219) + self.verticalLayout_2 = QVBoxLayout(Article) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.thumbnail = QLabel(Article) + self.thumbnail.setObjectName(u"thumbnail") + self.thumbnail.setMinimumSize(QSize(250, 150)) + self.thumbnail.setMaximumSize(QSize(250, 150)) + + self.horizontalLayout.addWidget(self.thumbnail) + + self.horizontalSpacer = QSpacerItem(5, 20, QSizePolicy.Fixed, QSizePolicy.Minimum) + + self.horizontalLayout.addItem(self.horizontalSpacer) + + self.verticalLayout = QVBoxLayout() + self.verticalLayout.setObjectName(u"verticalLayout") + self.verticalLayout.setContentsMargins(-1, -1, 0, -1) + self.lb_title = LabelTitle(Article) + self.lb_title.setObjectName(u"lb_title") + + self.verticalLayout.addWidget(self.lb_title) + + self.horizontalLayout_2 = QHBoxLayout() + self.horizontalLayout_2.setSpacing(0) + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.lb_date = QLabel(Article) + self.lb_date.setObjectName(u"lb_date") + self.lb_date.setMinimumSize(QSize(100, 25)) + self.lb_date.setMaximumSize(QSize(100, 16777215)) + + self.horizontalLayout_2.addWidget(self.lb_date) + + self.lb_compagny = QLabel(Article) + self.lb_compagny.setObjectName(u"lb_compagny") + self.lb_compagny.setMinimumSize(QSize(0, 25)) + self.lb_compagny.setMaximumSize(QSize(16777215, 16777215)) + + self.horizontalLayout_2.addWidget(self.lb_compagny) + + + self.verticalLayout.addLayout(self.horizontalLayout_2) + + self.desc = QLabel(Article) + self.desc.setObjectName(u"desc") + self.desc.setMinimumSize(QSize(175, 0)) + + self.verticalLayout.addWidget(self.desc) + + + self.horizontalLayout.addLayout(self.verticalLayout) + + + self.verticalLayout_2.addLayout(self.horizontalLayout) + + self.line = QFrame(Article) + self.line.setObjectName(u"line") + self.line.setFrameShape(QFrame.HLine) + self.line.setFrameShadow(QFrame.Sunken) + + self.verticalLayout_2.addWidget(self.line) + + + self.retranslateUi(Article) + + QMetaObject.connectSlotsByName(Article) + # setupUi + + def retranslateUi(self, Article): + Article.setWindowTitle(QCoreApplication.translate("Article", u"Form", None)) + self.thumbnail.setText("") + self.lb_title.setText(QCoreApplication.translate("Article", u"Title", None)) + self.lb_date.setText(QCoreApplication.translate("Article", u"date", None)) + self.lb_compagny.setText("") + self.desc.setText("") + # retranslateUi + diff --git a/app/ui/article.ui b/app/ui/article.ui new file mode 100644 index 0000000..2dc2890 --- /dev/null +++ b/app/ui/article.ui @@ -0,0 +1,146 @@ + + + Article + + + + 0 + 0 + 638 + 219 + + + + Form + + + + + + + + + 250 + 150 + + + + + 250 + 150 + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + 0 + + + + + Title + + + + + + + 0 + + + + + + 100 + 25 + + + + + 100 + 16777215 + + + + date + + + + + + + + 0 + 25 + + + + + 16777215 + 16777215 + + + + + + + + + + + + + + 175 + 0 + + + + + + + + + + + + + + + Qt::Horizontal + + + + + + + + LabelTitle + QLabel +
libs.widgets.label.h
+
+
+ + +
diff --git a/app/ui/indicator_settings_dialog.py b/app/ui/indicator_settings_dialog.py index 167e7cc..0a003c9 100644 --- a/app/ui/indicator_settings_dialog.py +++ b/app/ui/indicator_settings_dialog.py @@ -16,10 +16,8 @@ class Ui_IndicatorSettingsDialogWindow(object): def setupUi(self, IndicatorSettingsDialogWindow): if not IndicatorSettingsDialogWindow.objectName(): - IndicatorSettingsDialogWindow.setObjectName( - u"IndicatorSettingsDialogWindow" - ) - IndicatorSettingsDialogWindow.resize(400, 300) + IndicatorSettingsDialogWindow.setObjectName(u"IndicatorSettingsDialogWindow") + IndicatorSettingsDialogWindow.resize(400, 301) self.verticalLayout = QVBoxLayout(IndicatorSettingsDialogWindow) self.verticalLayout.setObjectName(u"verticalLayout") self.tab_settings = QTabWidget(IndicatorSettingsDialogWindow) @@ -71,51 +69,25 @@ def setupUi(self, IndicatorSettingsDialogWindow): self.horizontalLayout.addWidget(self.pub_ok) + self.verticalLayout.addLayout(self.horizontalLayout) + self.retranslateUi(IndicatorSettingsDialogWindow) self.tab_settings.setCurrentIndex(0) self.pub_ok.setDefault(True) - QMetaObject.connectSlotsByName(IndicatorSettingsDialogWindow) + QMetaObject.connectSlotsByName(IndicatorSettingsDialogWindow) # setupUi def retranslateUi(self, IndicatorSettingsDialogWindow): - IndicatorSettingsDialogWindow.setWindowTitle( - QCoreApplication.translate( - "IndicatorSettingsDialogWindow", - u"IndicatorSettingsDialogWindow", - None, - ) - ) - self.tab_settings.setTabText( - self.tab_settings.indexOf(self.wgt_settings_input), - QCoreApplication.translate( - "IndicatorSettingsDialogWindow", u"Input", None - ), - ) - self.tab_settings.setTabText( - self.tab_settings.indexOf(self.wgt_settings_styles), - QCoreApplication.translate( - "IndicatorSettingsDialogWindow", u"Style", None - ), - ) - self.pub_reset.setText( - QCoreApplication.translate( - "IndicatorSettingsDialogWindow", u"Reset values", None - ) - ) - self.pub_cancel.setText( - QCoreApplication.translate( - "IndicatorSettingsDialogWindow", u"Cancel", None - ) - ) - self.pub_ok.setText( - QCoreApplication.translate( - "IndicatorSettingsDialogWindow", u"Ok", None - ) - ) - + IndicatorSettingsDialogWindow.setWindowTitle(QCoreApplication.translate("IndicatorSettingsDialogWindow", u"Settings Indicators", None)) + self.tab_settings.setTabText(self.tab_settings.indexOf(self.wgt_settings_input), QCoreApplication.translate("IndicatorSettingsDialogWindow", u"Input", None)) + self.tab_settings.setTabText(self.tab_settings.indexOf(self.wgt_settings_styles), QCoreApplication.translate("IndicatorSettingsDialogWindow", u"Style", None)) + self.pub_reset.setText(QCoreApplication.translate("IndicatorSettingsDialogWindow", u"Reset values", None)) + self.pub_cancel.setText(QCoreApplication.translate("IndicatorSettingsDialogWindow", u"Cancel", None)) + self.pub_ok.setText(QCoreApplication.translate("IndicatorSettingsDialogWindow", u"Ok", None)) # retranslateUi + diff --git a/app/ui/indicator_settings_dialog.ui b/app/ui/indicator_settings_dialog.ui index 4aca884..aae48b8 100644 --- a/app/ui/indicator_settings_dialog.ui +++ b/app/ui/indicator_settings_dialog.ui @@ -7,11 +7,11 @@ 0 0 400 - 300 + 301 - IndicatorSettingsDialogWindow + Settings Indicators diff --git a/app/ui/main_window.py b/app/ui/main_window.py index 1d8a1a4..2ab8841 100644 --- a/app/ui/main_window.py +++ b/app/ui/main_window.py @@ -12,6 +12,7 @@ from PySide2.QtGui import * from PySide2.QtWidgets import * +from libs.widgets.label import LabelTitle from libs.graph.graphwidget import GraphWidget from libs.indicators_widget import IndicatorsWidget from libs.widgets.toolbar import ToolBar @@ -19,19 +20,20 @@ from libs.company_widget import CompanyWidget from libs.favorites_widget import FavoritesWidget from libs.articles_widget import ArticlesWidget +from libs.financial_widget import TableFinance +from libs.welcome_widget import WelcomeWidget +from libs.markets_widget import MarketsWidget +from libs.widgets.sentimentals_widget import Sentimental_Widget import resources_rc - class Ui_MainWindow(object): def setupUi(self, MainWindow): if not MainWindow.objectName(): MainWindow.setObjectName(u"MainWindow") - MainWindow.resize(900, 700) + MainWindow.resize(998, 754) self.action_reload_indicators = QAction(MainWindow) - self.action_reload_indicators.setObjectName( - u"action_reload_indicators" - ) + self.action_reload_indicators.setObjectName(u"action_reload_indicators") self.centralwidget = QWidget(MainWindow) self.centralwidget.setObjectName(u"centralwidget") self.verticalLayout = QVBoxLayout(self.centralwidget) @@ -44,9 +46,7 @@ def setupUi(self, MainWindow): self.pub_go_welcome.setMaximumSize(QSize(25, 25)) self.pub_go_welcome.setCursor(QCursor(Qt.PointingHandCursor)) icon = QIcon() - icon.addFile( - u":/svg/keyboard-arrow-left.svg", QSize(), QIcon.Normal, QIcon.Off - ) + icon.addFile(u":/svg/keyboard-arrow-left.svg", QSize(), QIcon.Normal, QIcon.Off) self.pub_go_welcome.setIcon(icon) self.pub_go_welcome.setIconSize(QSize(28, 28)) self.pub_go_welcome.setFlat(True) @@ -66,36 +66,164 @@ def setupUi(self, MainWindow): self.pub_go_graph.setMaximumSize(QSize(25, 25)) self.pub_go_graph.setCursor(QCursor(Qt.PointingHandCursor)) icon1 = QIcon() - icon1.addFile( - u":/svg/keyboard-arrow-right.svg", QSize(), QIcon.Normal, QIcon.Off - ) + icon1.addFile(u":/svg/keyboard-arrow-right.svg", QSize(), QIcon.Normal, QIcon.Off) self.pub_go_graph.setIcon(icon1) self.pub_go_graph.setIconSize(QSize(28, 28)) self.pub_go_graph.setFlat(True) self.horizontalLayout.addWidget(self.pub_go_graph) + self.verticalLayout.addLayout(self.horizontalLayout) self.stw_main = StackedWidget(self.centralwidget) self.stw_main.setObjectName(u"stw_main") self.wgt_welcome = QWidget() self.wgt_welcome.setObjectName(u"wgt_welcome") + self.verticalLayout_3 = QVBoxLayout(self.wgt_welcome) + self.verticalLayout_3.setObjectName(u"verticalLayout_3") + self.label = LabelTitle(self.wgt_welcome) + self.label.setObjectName(u"label") + self.label.setAlignment(Qt.AlignCenter) + + self.verticalLayout_3.addWidget(self.label) + + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Minimum) + + self.verticalLayout_3.addItem(self.verticalSpacer) + + self.horizontalLayout_3 = QHBoxLayout() + self.horizontalLayout_3.setSpacing(0) + self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") + self.horizontalLayout_3.setSizeConstraint(QLayout.SetMinimumSize) + self.pub_go_market_prev = QPushButton(self.wgt_welcome) + self.pub_go_market_prev.setObjectName(u"pub_go_market_prev") + self.pub_go_market_prev.setMinimumSize(QSize(25, 25)) + self.pub_go_market_prev.setMaximumSize(QSize(25, 25)) + self.pub_go_market_prev.setCursor(QCursor(Qt.PointingHandCursor)) + self.pub_go_market_prev.setIcon(icon) + self.pub_go_market_prev.setIconSize(QSize(28, 28)) + self.pub_go_market_prev.setFlat(True) + + self.horizontalLayout_3.addWidget(self.pub_go_market_prev) + + self.wgt_markets_2 = MarketsWidget(self.wgt_welcome) + self.wgt_markets_2.setObjectName(u"wgt_markets_2") + self.wgt_markets_2.setMaximumSize(QSize(16777215, 80)) + + self.horizontalLayout_3.addWidget(self.wgt_markets_2) + + self.pub_go_market_next = QPushButton(self.wgt_welcome) + self.pub_go_market_next.setObjectName(u"pub_go_market_next") + self.pub_go_market_next.setMinimumSize(QSize(25, 25)) + self.pub_go_market_next.setMaximumSize(QSize(25, 25)) + self.pub_go_market_next.setCursor(QCursor(Qt.PointingHandCursor)) + self.pub_go_market_next.setIcon(icon1) + self.pub_go_market_next.setIconSize(QSize(28, 28)) + self.pub_go_market_next.setFlat(True) + + self.horizontalLayout_3.addWidget(self.pub_go_market_next) + + + self.verticalLayout_3.addLayout(self.horizontalLayout_3) + + self.line = QFrame(self.wgt_welcome) + self.line.setObjectName(u"line") + self.line.setFrameShape(QFrame.HLine) + self.line.setFrameShadow(QFrame.Sunken) + + self.verticalLayout_3.addWidget(self.line) + + self.horizontalLayout_5 = QHBoxLayout() + self.horizontalLayout_5.setObjectName(u"horizontalLayout_5") + self.wdg_article_welcome = WelcomeWidget(self.wgt_welcome) + self.wdg_article_welcome.setObjectName(u"wdg_article_welcome") + + self.horizontalLayout_5.addWidget(self.wdg_article_welcome) + + self.verticalWidget = Sentimental_Widget(self.wgt_welcome) + self.verticalWidget.setObjectName(u"verticalWidget") + self.verticalWidget.setMinimumSize(QSize(0, 0)) + self.verticalWidget.setMaximumSize(QSize(300, 16777215)) + self.verticalLayout_6 = QVBoxLayout(self.verticalWidget) + self.verticalLayout_6.setObjectName(u"verticalLayout_6") + + self.horizontalLayout_5.addWidget(self.verticalWidget) + + + self.verticalLayout_3.addLayout(self.horizontalLayout_5) + self.stw_main.addWidget(self.wgt_welcome) - self.wgt_articles = ArticlesWidget() + self.wgt_article = QWidget() + self.wgt_article.setObjectName(u"wgt_article") + self.verticalLayout_2 = QVBoxLayout(self.wgt_article) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.label_2 = LabelTitle(self.wgt_article) + self.label_2.setObjectName(u"label_2") + self.label_2.setAlignment(Qt.AlignCenter) + + self.verticalLayout_2.addWidget(self.label_2) + + self.line_2 = QFrame(self.wgt_article) + self.line_2.setObjectName(u"line_2") + self.line_2.setFrameShape(QFrame.HLine) + self.line_2.setFrameShadow(QFrame.Sunken) + + self.verticalLayout_2.addWidget(self.line_2) + + self.wgt_articles = ArticlesWidget(self.wgt_article) self.wgt_articles.setObjectName(u"wgt_articles") - self.stw_main.addWidget(self.wgt_articles) + + self.verticalLayout_2.addWidget(self.wgt_articles) + + self.stw_main.addWidget(self.wgt_article) self.wgt_graph = GraphWidget() self.wgt_graph.setObjectName(u"wgt_graph") self.wgt_graph.setCursor(QCursor(Qt.CrossCursor)) self.stw_main.addWidget(self.wgt_graph) + self.wgt_financ = QWidget() + self.wgt_financ.setObjectName(u"wgt_financ") + self.verticalLayout_4 = QVBoxLayout(self.wgt_financ) + self.verticalLayout_4.setObjectName(u"verticalLayout_4") + self.financial_label = LabelTitle(self.wgt_financ) + self.financial_label.setObjectName(u"financial_label") + self.financial_label.setAlignment(Qt.AlignCenter) + + self.verticalLayout_4.addWidget(self.financial_label) + + self.financials_layout = QHBoxLayout() + self.financials_layout.setObjectName(u"financials_layout") + + self.verticalLayout_4.addLayout(self.financials_layout) + + self.horizontalLayout_2 = QHBoxLayout() + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.horizontalSpacer = QSpacerItem(240, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + + self.horizontalLayout_2.addItem(self.horizontalSpacer) + + self.wid_table_financ = TableFinance(self.wgt_financ) + self.wid_table_financ.setObjectName(u"wid_table_financ") + self.wid_table_financ.setMinimumSize(QSize(850, 0)) + self.wid_table_financ.setMaximumSize(QSize(850, 16777215)) + + self.horizontalLayout_2.addWidget(self.wid_table_financ) + + self.horizontalSpacer_2 = QSpacerItem(240, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + + self.horizontalLayout_2.addItem(self.horizontalSpacer_2) + + + self.verticalLayout_4.addLayout(self.horizontalLayout_2) + + self.stw_main.addWidget(self.wgt_financ) self.verticalLayout.addWidget(self.stw_main) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QMenuBar(MainWindow) self.menubar.setObjectName(u"menubar") - self.menubar.setGeometry(QRect(0, 0, 900, 21)) + self.menubar.setGeometry(QRect(0, 0, 998, 21)) self.menuOptions = QMenu(self.menubar) self.menuOptions.setObjectName(u"menuOptions") MainWindow.setMenuBar(self.menubar) @@ -107,9 +235,7 @@ def setupUi(self, MainWindow): sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth( - self.dock_wgt_company.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.dock_wgt_company.sizePolicy().hasHeightForWidth()) self.dock_wgt_company.setSizePolicy(sizePolicy) font = QFont() font.setBold(True) @@ -125,9 +251,7 @@ def setupUi(self, MainWindow): self.wgt_indicators = IndicatorsWidget() self.wgt_indicators.setObjectName(u"wgt_indicators") self.dock_wgt_indicators.setWidget(self.wgt_indicators) - MainWindow.addDockWidget( - Qt.RightDockWidgetArea, self.dock_wgt_indicators - ) + MainWindow.addDockWidget(Qt.RightDockWidgetArea, self.dock_wgt_indicators) self.tool_bar = ToolBar(MainWindow) self.tool_bar.setObjectName(u"tool_bar") MainWindow.addToolBar(Qt.LeftToolBarArea, self.tool_bar) @@ -137,46 +261,33 @@ def setupUi(self, MainWindow): self.wgt_favorites = FavoritesWidget() self.wgt_favorites.setObjectName(u"wgt_favorites") self.dock_wgt_favorites.setWidget(self.wgt_favorites) - MainWindow.addDockWidget( - Qt.RightDockWidgetArea, self.dock_wgt_favorites - ) + MainWindow.addDockWidget(Qt.RightDockWidgetArea, self.dock_wgt_favorites) self.menubar.addAction(self.menuOptions.menuAction()) self.menuOptions.addAction(self.action_reload_indicators) self.retranslateUi(MainWindow) - self.stw_main.setCurrentIndex(1) + self.stw_main.setCurrentIndex(0) - QMetaObject.connectSlotsByName(MainWindow) + QMetaObject.connectSlotsByName(MainWindow) # setupUi def retranslateUi(self, MainWindow): - MainWindow.setWindowTitle( - QCoreApplication.translate("MainWindow", u"Trade Helper", None) - ) - self.action_reload_indicators.setText( - QCoreApplication.translate( - "MainWindow", u"Reload Indicators", None - ) - ) + MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"Trade Helper", None)) + self.action_reload_indicators.setText(QCoreApplication.translate("MainWindow", u"Reload Indicators", None)) self.pub_go_welcome.setText("") self.pub_go_graph.setText("") - self.menuOptions.setTitle( - QCoreApplication.translate("MainWindow", u"Options", None) - ) - self.dock_wgt_company.setWindowTitle( - QCoreApplication.translate("MainWindow", u"Company", None) - ) - self.dock_wgt_indicators.setWindowTitle( - QCoreApplication.translate("MainWindow", u"Indicators", None) - ) - self.tool_bar.setWindowTitle( - QCoreApplication.translate("MainWindow", u"toolBar", None) - ) - self.dock_wgt_favorites.setWindowTitle( - QCoreApplication.translate("MainWindow", u"Favorites", None) - ) - + self.label.setText(QCoreApplication.translate("MainWindow", u"Trading Visualisation", None)) + self.pub_go_market_prev.setText("") + self.pub_go_market_next.setText("") + self.label_2.setText(QCoreApplication.translate("MainWindow", u"Related News", None)) + self.financial_label.setText(QCoreApplication.translate("MainWindow", u"Financials", None)) + self.menuOptions.setTitle(QCoreApplication.translate("MainWindow", u"Options", None)) + self.dock_wgt_company.setWindowTitle(QCoreApplication.translate("MainWindow", u"Company", None)) + self.dock_wgt_indicators.setWindowTitle(QCoreApplication.translate("MainWindow", u"Indicators", None)) + self.tool_bar.setWindowTitle(QCoreApplication.translate("MainWindow", u"toolBar", None)) + self.dock_wgt_favorites.setWindowTitle(QCoreApplication.translate("MainWindow", u"Favorites", None)) # retranslateUi + diff --git a/app/ui/main_window.ui b/app/ui/main_window.ui index 85ba138..5df0186 100644 --- a/app/ui/main_window.ui +++ b/app/ui/main_window.ui @@ -6,8 +6,8 @@ 0 0 - 900 - 700 + 998 + 754 @@ -102,15 +102,257 @@ - 1 + 0 - - + + + + + + Trading Visualisation + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 40 + + + + + + + + 0 + + + QLayout::SetMinimumSize + + + + + + 25 + 25 + + + + + 25 + 25 + + + + PointingHandCursor + + + + + + + :/svg/keyboard-arrow-left.svg:/svg/keyboard-arrow-left.svg + + + + 28 + 28 + + + + true + + + + + + + + 16777215 + 80 + + + + + + + + + 25 + 25 + + + + + 25 + 25 + + + + PointingHandCursor + + + + + + + :/svg/keyboard-arrow-right.svg:/svg/keyboard-arrow-right.svg + + + + 28 + 28 + + + + true + + + + + + + + + Qt::Horizontal + + + + + + + + + + + + + 0 + 0 + + + + + 300 + 16777215 + + + + + + + + + + + + + + + Related News + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + + + + + + CrossCursor + + + + + + Financials + + + Qt::AlignCenter + + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 240 + 20 + + + + + + + + + 850 + 0 + + + + + 850 + 16777215 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 240 + 20 + + + + + + + + @@ -120,7 +362,7 @@ 0 0 - 900 + 998 21 @@ -202,6 +444,11 @@ + + LabelTitle + QLabel +
libs.widgets.label.h
+
GraphWidget QWidget @@ -243,6 +490,28 @@
libs.articles_widget.h
1
+ + TableFinance + QTableWidget +
libs.financial_widget.h
+
+ + WelcomeWidget + QListWidget +
libs.welcome_widget.h
+
+ + MarketsWidget + QStackedWidget +
libs.markets_widget.h
+ 1 +
+ + Sentimental_Widget + QWidget +
libs.widgets.sentimentals_widget.h
+ 1 +
diff --git a/app/ui/markets_widget.py b/app/ui/markets_widget.py new file mode 100644 index 0000000..4071473 --- /dev/null +++ b/app/ui/markets_widget.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'markets_widget.ui' +## +## Created by: Qt User Interface Compiler version 5.15.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide2.QtCore import * +from PySide2.QtGui import * +from PySide2.QtWidgets import * + + +class Ui_markets(object): + def setupUi(self, markets): + if not markets.objectName(): + markets.setObjectName(u"markets") + markets.resize(125, 80) + markets.setMinimumSize(QSize(125, 80)) + markets.setMaximumSize(QSize(125, 80)) + self.horizontalLayout = QHBoxLayout(markets) + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.verticalLayout = QVBoxLayout() + self.verticalLayout.setObjectName(u"verticalLayout") + self.title = QLabel(markets) + self.title.setObjectName(u"title") + font = QFont() + font.setPointSize(12) + self.title.setFont(font) + + self.verticalLayout.addWidget(self.title) + + self.price = QLabel(markets) + self.price.setObjectName(u"price") + font1 = QFont() + font1.setPointSize(10) + self.price.setFont(font1) + + self.verticalLayout.addWidget(self.price) + + self.pourcentage = QLabel(markets) + self.pourcentage.setObjectName(u"pourcentage") + self.pourcentage.setFont(font1) + + self.verticalLayout.addWidget(self.pourcentage) + + + self.horizontalLayout.addLayout(self.verticalLayout) + + self.line = QFrame(markets) + self.line.setObjectName(u"line") + self.line.setFrameShape(QFrame.VLine) + self.line.setFrameShadow(QFrame.Sunken) + + self.horizontalLayout.addWidget(self.line) + + + self.retranslateUi(markets) + + QMetaObject.connectSlotsByName(markets) + # setupUi + + def retranslateUi(self, markets): + markets.setWindowTitle(QCoreApplication.translate("markets", u"Form", None)) + self.title.setText(QCoreApplication.translate("markets", u"Markets", None)) + self.price.setText(QCoreApplication.translate("markets", u"Price", None)) + self.pourcentage.setText(QCoreApplication.translate("markets", u"%", None)) + # retranslateUi + diff --git a/app/ui/markets_widget.ui b/app/ui/markets_widget.ui new file mode 100644 index 0000000..601b552 --- /dev/null +++ b/app/ui/markets_widget.ui @@ -0,0 +1,80 @@ + + + markets + + + + 0 + 0 + 125 + 80 + + + + + 125 + 80 + + + + + 125 + 80 + + + + Form + + + + + + + + + 12 + + + + Markets + + + + + + + + 10 + + + + Price + + + + + + + + 10 + + + + % + + + + + + + + + Qt::Vertical + + + + + + + + diff --git a/app/ui/sentimentals.py b/app/ui/sentimentals.py new file mode 100644 index 0000000..9f46817 --- /dev/null +++ b/app/ui/sentimentals.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'sentimentals.ui' +## +## Created by: Qt User Interface Compiler version 5.15.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide2.QtCore import * +from PySide2.QtGui import * +from PySide2.QtWidgets import * + +import resources_rc + +class Ui_Sentiment_Form(object): + def setupUi(self, Sentiment_Form): + if not Sentiment_Form.objectName(): + Sentiment_Form.setObjectName(u"Sentiment_Form") + Sentiment_Form.resize(300, 90) + Sentiment_Form.setMaximumSize(QSize(300, 90)) + self.verticalLayout = QVBoxLayout(Sentiment_Form) + self.verticalLayout.setObjectName(u"verticalLayout") + self.label = QLabel(Sentiment_Form) + self.label.setObjectName(u"label") + self.label.setMinimumSize(QSize(0, 15)) + self.label.setMaximumSize(QSize(16777215, 80)) + self.label.setAlignment(Qt.AlignCenter) + + self.verticalLayout.addWidget(self.label) + + self.horizontalLayout_2 = QHBoxLayout() + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.label_6 = QLabel(Sentiment_Form) + self.label_6.setObjectName(u"label_6") + self.label_6.setAlignment(Qt.AlignCenter) + + self.horizontalLayout_2.addWidget(self.label_6) + + self.label_7 = QLabel(Sentiment_Form) + self.label_7.setObjectName(u"label_7") + self.label_7.setAlignment(Qt.AlignCenter) + + self.horizontalLayout_2.addWidget(self.label_7) + + self.label_8 = QLabel(Sentiment_Form) + self.label_8.setObjectName(u"label_8") + self.label_8.setAlignment(Qt.AlignCenter) + + self.horizontalLayout_2.addWidget(self.label_8) + + self.label_9 = QLabel(Sentiment_Form) + self.label_9.setObjectName(u"label_9") + self.label_9.setAlignment(Qt.AlignCenter) + + self.horizontalLayout_2.addWidget(self.label_9) + + self.label_10 = QLabel(Sentiment_Form) + self.label_10.setObjectName(u"label_10") + self.label_10.setAlignment(Qt.AlignCenter) + + self.horizontalLayout_2.addWidget(self.label_10) + + + self.verticalLayout.addLayout(self.horizontalLayout_2) + + self.horizontalSlider = QSlider(Sentiment_Form) + self.horizontalSlider.setObjectName(u"horizontalSlider") + self.horizontalSlider.setTracking(True) + self.horizontalSlider.setOrientation(Qt.Horizontal) + + self.verticalLayout.addWidget(self.horizontalSlider) + + + self.retranslateUi(Sentiment_Form) + + QMetaObject.connectSlotsByName(Sentiment_Form) + # setupUi + + def retranslateUi(self, Sentiment_Form): + Sentiment_Form.setWindowTitle(QCoreApplication.translate("Sentiment_Form", u"Form", None)) + self.label.setText(QCoreApplication.translate("Sentiment_Form", u"Compagny", None)) + self.label_6.setText(QCoreApplication.translate("Sentiment_Form", u"Strong Sell", None)) + self.label_7.setText(QCoreApplication.translate("Sentiment_Form", u"Sell", None)) + self.label_8.setText(QCoreApplication.translate("Sentiment_Form", u"Neutral", None)) + self.label_9.setText(QCoreApplication.translate("Sentiment_Form", u"Buy", None)) + self.label_10.setText(QCoreApplication.translate("Sentiment_Form", u"Strong Buy", None)) + # retranslateUi + diff --git a/app/ui/sentimentals.ui b/app/ui/sentimentals.ui new file mode 100644 index 0000000..e48bbb4 --- /dev/null +++ b/app/ui/sentimentals.ui @@ -0,0 +1,115 @@ + + + Sentiment_Form + + + + 0 + 0 + 300 + 90 + + + + + 300 + 90 + + + + Form + + + + + + + 0 + 15 + + + + + 16777215 + 80 + + + + Compagny + + + Qt::AlignCenter + + + + + + + + + Strong Sell + + + Qt::AlignCenter + + + + + + + Sell + + + Qt::AlignCenter + + + + + + + Neutral + + + Qt::AlignCenter + + + + + + + Buy + + + Qt::AlignCenter + + + + + + + Strong Buy + + + Qt::AlignCenter + + + + + + + + + true + + + Qt::Horizontal + + + + + + + + + + diff --git a/app/ui/splashscreen.py b/app/ui/splashscreen.py new file mode 100644 index 0000000..df6e56f --- /dev/null +++ b/app/ui/splashscreen.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'splashscreen.ui' +## +## Created by: Qt User Interface Compiler version 5.15.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide2.QtCore import * +from PySide2.QtGui import * +from PySide2.QtWidgets import * + + +class Ui_splashscreen(object): + def setupUi(self, splashscreen): + if not splashscreen.objectName(): + splashscreen.setObjectName(u"splashscreen") + splashscreen.resize(900, 500) + self.verticalLayout = QVBoxLayout(splashscreen) + self.verticalLayout.setObjectName(u"verticalLayout") + self.lab_app_name = QLabel(splashscreen) + self.lab_app_name.setObjectName(u"lab_app_name") + self.lab_app_name.setMinimumSize(QSize(0, 230)) + font = QFont() + font.setPointSize(18) + self.lab_app_name.setFont(font) + self.lab_app_name.setStyleSheet(u"color:#FFFFFF; ") + self.lab_app_name.setAlignment(Qt.AlignCenter) + + self.verticalLayout.addWidget(self.lab_app_name) + + + self.retranslateUi(splashscreen) + + QMetaObject.connectSlotsByName(splashscreen) + # setupUi + + def retranslateUi(self, splashscreen): + splashscreen.setWindowTitle(QCoreApplication.translate("splashscreen", u"Form", None)) + self.lab_app_name.setText(QCoreApplication.translate("splashscreen", u"app_name", None)) + # retranslateUi + diff --git a/app/ui/splashscreen.ui b/app/ui/splashscreen.ui new file mode 100644 index 0000000..2a390aa --- /dev/null +++ b/app/ui/splashscreen.ui @@ -0,0 +1,45 @@ + + + splashscreen + + + + 0 + 0 + 900 + 500 + + + + Form + + + + + + + 0 + 230 + + + + + 18 + + + + color:#FFFFFF; + + + app_name + + + Qt::AlignCenter + + + + + + + + diff --git a/app/utils/utils.py b/app/utils/utils.py index 1708220..3d91841 100644 --- a/app/utils/utils.py +++ b/app/utils/utils.py @@ -1,6 +1,10 @@ +import os +import json +import random import datetime import urllib.request from statistics import mean +from libs.yahoo_fin import stock_info as sf from sklearn import preprocessing import numpy as np @@ -36,16 +40,20 @@ def savgol_filter(values, window_length, polyorder=3): mode="interp", ) - -def remove_nan(values): - """Remove NaN from array - - :param values: array of data - :type values: np.array - :return: The array without NaN - :rtype: np.array +def remove_nan(data): + """ + This method replace Nan value by 0. + Sinon return float. + :param data: + :return: List """ - return values[~np.isnan(values)] + data_format = [] + for i in data: + if str(i) == "nan": + i = 0 + data_format.append(float(i)) + return data_format + def _peaks_detection(values, rounded=3, direction="up"): @@ -236,3 +244,89 @@ def get_main_window_instance(name: str = "MainWindow"): continue return widget return None + + + +def get_all_tickers(): + """ + This method return a dict of all the compagny for each markets. + """ + try: + dow = sf.tickers_dow() + cac = sf.tickers_cac() + sp500 = sf.tickers_sp500() + nasdaq = sf.tickers_nasdaq() + + data = {} + for i in [cac, dow, nasdaq, sp500]: + data.update(i) + + except: + SCRIPT_PATH = os.path.dirname(os.path.dirname(__file__)) + with open(os.path.join(SCRIPT_PATH, "data", "dataset.json"), "r") as f: + data = json.load(f) + + return data + + +def get_compagny_name_from_tick(ticker): + """ + This method return the Compagny name for his ticker. + """ + + data = get_all_tickers() + + for tick, company in data.items(): + if ticker.startswith(tick): + return company + +def get_last_value(data): + if data[0] != 0: + index = 0 + value = data[index] + else: + index = 1 + value = data[index] + return value, index + +def format_data(data): + """ + This method format number with ','. + exemple: 2,120,350 + :param data: + :return: List of string + """ + data_format = [] + for i in remove_nan(data): + i = f"{int(i):,}" + data_format.append(i) + return data_format + + +def get_rdm_tickers(qty=5): + """ + This method get a quantity of randoms tickers. + return: list + """ + all_tickers = get_all_tickers().keys() + tickers = random.sample(all_tickers, qty) + return tickers + +def check_french_ticker(ticker): + """This method split the ticker if endswith '.PA' + :return: str + """ + try: + ticker = ticker.split('.')[0] + except: + pass + + return ticker + +def clear_layout(layout): + """ + This method remove all widgets inside a layout. + :param: QtLayout + """ + for i in reversed(range(layout.count())): + layout.itemAt(i).widget().setParent(None) \ No newline at end of file diff --git a/app/view.py b/app/view.py index ee274fe..04b7724 100644 --- a/app/view.py +++ b/app/view.py @@ -13,6 +13,9 @@ from libs.thread_pool import ThreadPool from libs.graph.candlestick import CandlestickItem from libs.io.favorite_settings import FavoritesManager +from libs.widgets.sentimentals_widget import Sentimental_Widget_Item +from libs.splashcreen import SplashScreen +from libs.roi_manager import ROIManager from ui import main_window @@ -20,12 +23,20 @@ from utils import decorators SCRIPT_PATH = os.path.dirname(__file__) - +TITLE = "TRADING VISUALISATION" class MainWindow(QtWidgets.QMainWindow, main_window.Ui_MainWindow): def __init__(self, parent=None, data=None): super(MainWindow, self).__init__(parent=parent) + img = "resources\img\splashscreen.jpg" + path = os.path.join(SCRIPT_PATH, img) + + self.splash = SplashScreen(path, TITLE) + # self.splash.show() + self.splash.show_message("Loading UI...\n\n") + QtWidgets.QApplication.processEvents() + self.setupUi(self) self.setWindowState(QtCore.Qt.WindowMaximized) @@ -67,9 +78,23 @@ def __init__(self, parent=None, data=None): self.signals.sig_ticker_infos_fetched.connect( self.wgt_company._on_ticker_infos ) - ## - self.signals.sig_ticker_articles_fetched.connect( - self.wgt_articles.get_articles + self.tickers_dialog.signal.sig_ticker_choosen.connect( + self.wgt_articles._on_get_articles + ) + self.wgt_favorites.signals.sig_favorite_clicked.connect( + self.wgt_articles._on_get_articles + ) + self.tickers_dialog.signal.sig_ticker_choosen.connect( + self.wid_table_financ.on_set_financials_table + ) + self.tickers_dialog.signal.sig_ticker_choosen.connect( + self.set_sentiment_compagny + ) + self.wgt_favorites.signals.sig_favorite_clicked.connect( + self.wid_table_financ.on_set_financials_table + ) + self.wgt_favorites.signals.sig_favorite_clicked.connect( + self.set_sentiment_compagny ) self.thread_pool.signals.sig_thread_pre.connect( self.busy_indicator.show @@ -95,10 +120,15 @@ def __init__(self, parent=None, data=None): self.pub_go_welcome.clicked.connect(self.stw_main.slide_in_prev) self.pub_go_graph.clicked.connect(self.stw_main.slide_in_next) + self.pub_go_market_prev.clicked.connect(self.wgt_markets_2.slide_in_prev) + self.pub_go_market_next.clicked.connect(self.wgt_markets_2.slide_in_next) # Action which needs to be loaded after all signals + self.splash.show_message("Loading Favorites...\n\n") self.favorites_manager.load_favorite() + self.splash.hide() + def _init_app_home(self): """Init the APP_HOME of the application""" base_path = os.path.expanduser("~") @@ -157,16 +187,28 @@ def _on_indicator_switched(self, indicator: object, state: bool): else: indicator.remove_indicator(graph_view=self.wgt_graph.graph) - @QtCore.Slot(str) - def _on_action_triggered(self, action: str): + @QtCore.Slot(str, dict) + def _on_action_triggered(self, action: str, args: dict): """Callback on action triggered from the toolbar :param action: The action to find and call :type action: str + :param args: Args for the action + :type args: dict """ action_obj = utils.find_method(module=action, obj=self) if action_obj: - action_obj() + action_obj(**args) + + @QtCore.Slot(str) + def set_sentiment_compagny(self, ticker): + """This method set the sentimental widget for + the select tick. + """ + utils.clear_layout(self.financials_layout) + widget = Sentimental_Widget_Item() + widget.set_sentimental_ui(ticker=ticker) + self.financials_layout.addWidget(widget) def resizeEvent(self, event): if self.tickers_dialog: diff --git a/preview_home.png b/preview_home.png new file mode 100644 index 0000000..248c638 Binary files /dev/null and b/preview_home.png differ