diff --git a/assets/base.css b/assets/base.css deleted file mode 100644 index 931563c..0000000 --- a/assets/base.css +++ /dev/null @@ -1,2717 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dash-sample-apps/base.css at master · plotly/dash-sample-apps · GitHub - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Skip to content - - - - - - - - -
- -
- - - - - -
- - - -
- - - - - - - - - -
-
-
- - - - - - - - - - - - - - - -
- -
- -
-

- - - / - - dash-sample-apps - - -

- - -
- - - -
- - -
- - -
-
- - - - - - - Permalink - - - - - -
- -
-
- - - master - - - - -
- - - -
-
-
- -
- - - - Go to file - - -
- - -
- -
- - - -
- -
-
- - @ronniebugia - - -
- - Latest commit - 5facc76 - Jul 5, 2019 - - - - - - History - - -
-
- -
- -
-
- - - 1 - - contributor - - -
- -

- Users who have contributed to this file -

-
- - - - - - - - -
-
-
-
- - - - - - -
- -
-
- - 392 lines (390 sloc) - - 11.8 KB -
- -
- -
- Raw - Blame -
- - -
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/* Table of contents ––––––––––––––––––––––––––––––––––––––––––––––––––
Taken from https://codepen.io/chriddyp/pen/bWLwgP.css
- Grid
- Base Styles
- Typography
- Links
- Buttons
- Forms
- Lists
- Code
- Tables
- Spacing
- Utilities
- Clearing
- Media Queries
- Custom App CSS */
-
-
/* Grid –––––––––––––––––––––––––––––––––––––––––––––––––– */
.container {
position: relative;
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 0 20px;
box-sizing: border-box;
}
.column, .columns {
width: 100%;
float: left;
box-sizing: border-box;
}
/* For devices larger than 400px */
@media (min-width: 400px) {
.container {
width: 85%;
padding: 0;
}
}
/* For devices larger than 550px */
@media (min-width: 550px) {
.container {
width: 80%;
}
.column, .columns {
margin-left: 4%;
}
.column:first-child, .columns:first-child {
margin-left: 0;
}
.one.column, .one.columns {
width: 4.66666666667%;
}
.two.columns {
width: 13.3333333333%;
}
.three.columns {
width: 22%;
}
.four.columns {
width: 30.6666666667%;
}
.five.columns {
width: 39.3333333333%;
}
.six.columns {
width: 48%;
}
.seven.columns {
width: 56.6666666667%;
}
.eight.columns {
width: 65.3333333333%;
}
.nine.columns {
width: 74.0%;
}
.ten.columns {
width: 82.6666666667%;
}
.eleven.columns {
width: 91.3333333333%;
}
.twelve.columns {
width: 100%;
margin-left: 0;
}
.one-third.column {
width: 30.6666666667%;
}
.two-thirds.column {
width: 65.3333333333%;
}
.one-half.column {
width: 48%;
}
/* Offsets */
.offset-by-one.column, .offset-by-one.columns {
margin-left: 8.66666666667%;
}
.offset-by-two.column, .offset-by-two.columns {
margin-left: 17.3333333333%;
}
.offset-by-three.column, .offset-by-three.columns {
margin-left: 26%;
}
.offset-by-four.column, .offset-by-four.columns {
margin-left: 34.6666666667%;
}
.offset-by-five.column, .offset-by-five.columns {
margin-left: 43.3333333333%;
}
.offset-by-six.column, .offset-by-six.columns {
margin-left: 52%;
}
.offset-by-seven.column, .offset-by-seven.columns {
margin-left: 60.6666666667%;
}
.offset-by-eight.column, .offset-by-eight.columns {
margin-left: 69.3333333333%;
}
.offset-by-nine.column, .offset-by-nine.columns {
margin-left: 78.0%;
}
.offset-by-ten.column, .offset-by-ten.columns {
margin-left: 86.6666666667%;
}
.offset-by-eleven.column, .offset-by-eleven.columns {
margin-left: 95.3333333333%;
}
.offset-by-one-third.column, .offset-by-one-third.columns {
margin-left: 34.6666666667%;
}
.offset-by-two-thirds.column, .offset-by-two-thirds.columns {
margin-left: 69.3333333333%;
}
.offset-by-one-half.column, .offset-by-one-half.columns {
margin-left: 52%;
}
}
/* Base Styles –––––––––––––––––––––––––––––––––––––––––––––––––– */
/* NOTE html is set to 62.5% so that all the REM measurements throughout Skeleton are based on 10px sizing. So basically 1.5rem = 15px :) */
html {
font-size: 62.5%;
}
body {
font-size: 1.5em;
/* currently ems cause chrome bug misinterpreting rems on body element */
line-height: 1.6;
font-weight: 400;
font-family: "Open Sans", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: rgb(50, 50, 50);
}
/* Typography –––––––––––––––––––––––––––––––––––––––––––––––––– */
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 0;
font-weight: 300;
}
h1 {
font-size: 4.5rem;
line-height: 1.2;
letter-spacing: -.1rem;
margin-bottom: 2rem;
}
h2 {
font-size: 3.6rem;
line-height: 1.25;
letter-spacing: -.1rem;
margin-bottom: 1.8rem;
margin-top: 1.8rem;
}
h3 {
font-size: 3.0rem;
line-height: 1.3;
letter-spacing: -.1rem;
margin-bottom: 1.5rem;
margin-top: 1.5rem;
}
h4 {
font-size: 2.6rem;
line-height: 1.35;
letter-spacing: -.08rem;
margin-bottom: 1.2rem;
margin-top: 1.2rem;
}
h5 {
font-size: 2.2rem;
line-height: 1.5;
letter-spacing: -.05rem;
margin-bottom: 0.6rem;
margin-top: 0.6rem;
}
h6 {
font-size: 2.0rem;
line-height: 1.6;
letter-spacing: 0;
margin-bottom: 0.75rem;
margin-top: 0.75rem;
}
p {
margin-top: 0;
}
/* Blockquotes –––––––––––––––––––––––––––––––––––––––––––––––––– */
blockquote {
border-left: 4px lightgrey solid;
padding-left: 1rem;
margin-top: 2rem;
margin-bottom: 2rem;
margin-left: 0rem;
}
/* Links –––––––––––––––––––––––––––––––––––––––––––––––––– */
a {
color: #1EAEDB;
text-decoration: underline;
cursor: pointer;
}
a:hover {
color: #0FA0CE;
}
/* Buttons –––––––––––––––––––––––––––––––––––––––––––––––––– */
.button, button, input[type="submit"], input[type="reset"], input[type="button"] {
display: inline-block;
height: 38px;
padding: 0 30px;
color: #555;
text-align: center;
font-size: 11px;
font-weight: 600;
line-height: 38px;
letter-spacing: .1rem;
text-transform: uppercase;
text-decoration: none;
white-space: nowrap;
background-color: transparent;
border-radius: 4px;
border: 1px solid #bbb;
cursor: pointer;
box-sizing: border-box;
}
.button:hover, button:hover, input[type="submit"]:hover, input[type="reset"]:hover, input[type="button"]:hover, .button:focus, button:focus, input[type="submit"]:focus, input[type="reset"]:focus, input[type="button"]:focus {
color: #333;
border-color: #888;
outline: 0;
}
.button.button-primary, button.button-primary, input[type="submit"].button-primary, input[type="reset"].button-primary, input[type="button"].button-primary {
color: #FFF;
background-color: #33C3F0;
border-color: #33C3F0;
}
.button.button-primary:hover, button.button-primary:hover, input[type="submit"].button-primary:hover, input[type="reset"].button-primary:hover, input[type="button"].button-primary:hover, .button.button-primary:focus, button.button-primary:focus, input[type="submit"].button-primary:focus, input[type="reset"].button-primary:focus, input[type="button"].button-primary:focus {
color: #FFF;
background-color: #1EAEDB;
border-color: #1EAEDB;
}
/* Forms –––––––––––––––––––––––––––––––––––––––––––––––––– */
input[type="email"], input[type="number"], input[type="search"], input[type="text"], input[type="tel"], input[type="url"], input[type="password"], textarea, select {
height: 38px;
padding: 6px 10px;
/* The 6px vertically centers text on FF, ignored by Webkit */
background-color: #fff;
border: 1px solid #D1D1D1;
border-radius: 4px;
box-shadow: none;
box-sizing: border-box;
font-family: inherit;
font-size: inherit;
/*https://stackoverflow.com/questions/6080413/why-doesnt-input-inherit-the-font-from-body*/
}
/* Removes awkward default styles on some inputs for iOS */
input[type="email"], input[type="number"], input[type="search"], input[type="text"], input[type="tel"], input[type="url"], input[type="password"], textarea {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
textarea {
min-height: 65px;
padding-top: 6px;
padding-bottom: 6px;
}
input[type="email"]:focus, input[type="number"]:focus, input[type="search"]:focus, input[type="text"]:focus, input[type="tel"]:focus, input[type="url"]:focus, input[type="password"]:focus, textarea:focus, select:focus {
border: 1px solid #33C3F0;
outline: 0;
}
label, legend {
display: block;
margin-bottom: 0px;
}
fieldset {
padding: 0;
border-width: 0;
}
input[type="checkbox"], input[type="radio"] {
display: inline;
}
label > .label-body {
display: inline-block;
margin-left: .5rem;
font-weight: normal;
}
/* Lists –––––––––––––––––––––––––––––––––––––––––––––––––– */
ul {
list-style: circle inside;
}
ol {
list-style: decimal inside;
}
ol, ul {
padding-left: 0;
margin-top: 0;
}
ul ul, ul ol, ol ol, ol ul {
margin: 1.5rem 0 1.5rem 3rem;
font-size: 90%;
}
li {
margin-bottom: 1rem;
}
/* Tables –––––––––––––––––––––––––––––––––––––––––––––––––– */
table {
border-collapse: collapse;
}
th, td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #E1E1E1;
}
th:first-child, td:first-child {
padding-left: 0;
}
th:last-child, td:last-child {
padding-right: 0;
}
/* Spacing –––––––––––––––––––––––––––––––––––––––––––––––––– */
button, .button {
margin-bottom: 0rem;
}
input, textarea, select, fieldset {
margin-bottom: 0rem;
}
pre, dl, figure, table, form {
margin-bottom: 0rem;
}
p, ul, ol {
margin-bottom: 0.75rem;
}
/* Utilities –––––––––––––––––––––––––––––––––––––––––––––––––– */
.u-full-width {
width: 100%;
box-sizing: border-box;
}
.u-max-full-width {
max-width: 100%;
box-sizing: border-box;
}
.u-pull-right {
float: right;
}
.u-pull-left {
float: left;
}
/* Misc –––––––––––––––––––––––––––––––––––––––––––––––––– */
hr {
margin-top: 3rem;
margin-bottom: 3.5rem;
border-width: 0;
border-top: 1px solid #E1E1E1;
}
/* Clearing –––––––––––––––––––––––––––––––––––––––––––––––––– */
/* Self Clearing Goodness */
.container:after, .row:after, .u-cf {
content: "";
display: table;
clear: both;
}
/* Media Queries –––––––––––––––––––––––––––––––––––––––––––––––––– */
/* Note: The best way to structure the use of media queries is to create the queries near the relevant code. For example, if you wanted to change the styles for buttons on small devices, paste the mobile query code up in the buttons section and style it there. */
/* Larger than mobile */
@media (min-width: 400px) {
}
/* Larger than phablet (also point when grid becomes active) */
@media (min-width: 550px) {
}
/* Larger than tablet */
@media (min-width: 750px) {
}
/* Larger than desktop */
@media (min-width: 1000px) {
}
/* Larger than Desktop HD */
@media (min-width: 1200px) {
}
- - - -
- -
- - - - -
- - -
- - -
-
- - - - -
-
- -
-
- -
- - - - - - -
- - - You can’t perform that action at this time. -
- - - - - - - - - - - - - diff --git a/assets/dash-logo-new.png b/assets/dash-logo-new.png deleted file mode 100644 index 7919522..0000000 --- a/assets/dash-logo-new.png +++ /dev/null @@ -1,1118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dash-sample-apps/dash-logo-new.png at master · plotly/dash-sample-apps · GitHub - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Skip to content - - - - - - - - -
- -
- - - - - -
- - - -
- - - - - - - - - -
-
-
- - - - - - - - - - - - - - - -
- -
- -
-

- - - / - - dash-sample-apps - - -

- - -
- - - -
- - -
- - -
-
- - - - - - - Permalink - - - - - -
- -
-
- - - master - - - - -
- - - -
-
-
- -
- - - - Go to file - - -
- - -
- -
- - - -
- -
-
- - @ronniebugia - - -
- - Latest commit - 5facc76 - Jul 5, 2019 - - - - - - History - - -
-
- -
- -
-
- - - 1 - - contributor - - -
- -

- Users who have contributed to this file -

-
- - - - - - - - -
-
-
-
- - - - - - -
- -
-
- - 4.4 KB -
- -
- -
- Download -
- -
- - - - -
- -
-
-
- - - -
-
- dash-logo-new.png -
-
- -
- - - - -
- - -
- - -
-
- - - - -
-
- -
-
- -
- - - - - - -
- - - You can’t perform that action at this time. -
- - - - - - - - - - - - - diff --git a/assets/dash-logo.png b/assets/dash-logo.png new file mode 100644 index 0000000..a9bdbe0 Binary files /dev/null and b/assets/dash-logo.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a4af062 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,29 @@ +Brotli==1.0.9 +certifi==2020.12.5 +chardet==3.0.4 +click==7.1.2 +dash==1.18.1 +dash-core-components==1.14.1 +dash-html-components==1.1.1 +dash-renderer==1.8.3 +dash-table==4.11.1 +Flask==1.1.2 +Flask-Compress==1.8.0 +future==0.18.2 +idna==2.10 +itsdangerous==1.1.0 +Jinja2==2.11.2 +lxml==4.6.2 +MarkupSafe==1.1.1 +multitasking==0.0.9 +numpy==1.19.3 +pandas==1.1.5 +plotly==4.14.1 +python-dateutil==2.8.1 +pytz==2020.4 +requests==2.25.0 +retrying==1.3.3 +six==1.15.0 +urllib3==1.26.2 +Werkzeug==1.0.1 +yfinance==0.1.55 diff --git a/stockdash_loader.py b/stockdash_loader.py new file mode 100644 index 0000000..caf57ff --- /dev/null +++ b/stockdash_loader.py @@ -0,0 +1,247 @@ +import os, sys +import pandas as pd +import numpy as np +import sqlalchemy +import logging +import traceback +from timeit import default_timer as timer +from datetime import datetime, timedelta + +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import Column, Integer, String, DateTime +from sqlalchemy.orm import sessionmaker +from sqlalchemy.types import VARCHAR + +import yfinance as yf + +API_KEY = '44ced5e44c50543745b1d89fce8cd93a' +api_key = "?apikey=" + API_KEY +api_kpi_url = "https://financialmodelingprep.com/api/v3/key-metrics/" +api_batch_stock_price_url = "https://financialmodelingprep.com/api/v3/quote/" + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + handlers=[ + logging.FileHandler("debug.log"), + logging.StreamHandler() + ] +) + +Base = declarative_base() + +data_dir = 'data' +file_symbol = os.path.join(data_dir, 'symbols.json') +db_symbol = "base_symbol" +db_kpi = "base_kpi" +db_div = "base_dividend" +db_rec = "base_recommendation" +db_his = "base_price_history" + +sync_freq_kpi = 14 +sync_freq_rec = 7 +sync_freq_div = 21 +sync_freq_his = 1 + +sym_exclude = [] +engine = sqlalchemy.create_engine("mysql+pymysql://spcial:GOi1gA01@localhost:3306/app_stockdash?charset=utf8mb4") + + +def update_timestamp(symbol, schema): + Session = sessionmaker(bind=engine) + session = Session() + curr_symbol = session.query(Symbols).filter(Symbols.symbol == symbol) + if schema == db_kpi: + curr_symbol.update({Symbols.last_updated_kpi: datetime.now()}, synchronize_session=False) + elif schema == db_rec: + curr_symbol.update({Symbols.last_updated_rec: datetime.now()}, synchronize_session=False) + elif schema == db_div: + curr_symbol.update({Symbols.last_updated_div: datetime.now()}, synchronize_session=False) + elif schema == db_his: + curr_symbol.update({Symbols.last_updated_his: datetime.now()}, synchronize_session=False) + session.commit() + + +def update_loadable(symbol, loadable): + Session = sessionmaker(bind=engine) + session = Session() + curr_symbol = session.query(Symbols).filter(Symbols.symbol == symbol) + curr_symbol.update({Symbols.loadable: loadable}, synchronize_session=False) + session.commit() + + +def load_to_db(df, table_name): + start = timer() + + try: + df = df.replace([np.inf, -np.inf], np.nan) + df.to_sql(table_name, schema='app_stockdash', con=engine, if_exists='append', dtype={'symbol': VARCHAR(10)}) + except Exception as err: + logging.warning(" <%s> Error occured when loading data to DB. Error: \n%s" % (table_name, err)) + raise + + logging.info(" comleted in %f sec! " % (timer() - start)) + + +def load_from_db(table_name, where=None, limit=None, orderby=None): + start = timer() + + try: + sql = "SELECT * FROM %s " % table_name + if where is not None: + sql = sql + "WHERE %s " % where + if orderby is not None: + sql = sql + "ORDER BY %s " % orderby + if limit is not None: + sql = sql + "LIMIT %i " % limit + + df = pd.read_sql_query(sql, engine) + except sqlalchemy.exc.ProgrammingError as er: + logging.warning(" <%s> Error occured when quering data. Return None. Error: \n%s" % (table_name, er)) + return None + + logging.info(" comleted in %f sec! " % (timer() - start)) + return df + + +def load_symbols(): + logging.info("Loading symbols based on file %s" % file_symbol) + df_symbols = pd.read_json(file_symbol).drop(columns=['price']) + logging.info("Retrieved %i symbols from file" % len(df_symbols)) + + df_symbols = df_symbols[df_symbols['exchange'].isin(['Nasdaq Global Select', 'NASDAQ Global Market', 'NASDAQ Capital Market'])] + logging.info("Using %i symbols after filtering" % len(df_symbols)) + + df_symbols["initialized"] = datetime.now() + df_symbols["last_updated_kpi"] = pd.Timestamp.min + df_symbols["last_updated_div"] = pd.Timestamp.min + df_symbols["last_updated_rec"] = pd.Timestamp.min + df_symbols["last_updated_his"] = pd.Timestamp.min + df_symbols["loadable"] = True + + df_existing_symbols = load_from_db(db_symbol) + + if df_existing_symbols is not None: + logging.info("Retrieved %i symbols from DB" % len(df_existing_symbols)) + df_diff = pd.concat([df_symbols, df_existing_symbols]).drop_duplicates(subset=['symbol'], keep=False).set_index('symbol') + logging.info("Loading %i new symbols into DB..." % len(df_diff)) + if len(df_diff) > 0: + load_to_db(df_diff, db_symbol) + else: + logging.info("Could not retrieve any symbols from DB. Expecting table does not exist. Create table...") + load_to_db(df_symbols, db_symbol) + + return df_symbols + + +def synch_data(): + logging.info("Synching data. Loading available symbols from DB...") + df_symbols = load_from_db(db_symbol, where='loadable = 1') + num_symbols = len(df_symbols['symbol']) + logging.info("Loaded %i symbols from DB." % num_symbols) + final_kpi_columns = load_from_db(db_kpi, limit=1).columns + + i = 1 + for index, row in df_symbols.iterrows(): + try: + symbol = row['symbol'] + yticker = yf.Ticker(symbol) + + logging.info("%s/%s Querying data for ticker %s" % (i, num_symbols, symbol)) + + # KPI + if row['last_updated_kpi'] < datetime.today() - timedelta(days=sync_freq_kpi): + logging.info(" <%s> Last Updated above Threshold. Loading new KPI data for symbol into DB %s" % (db_kpi, symbol)) + ticker_dict = yticker.info + + for idx, val in ticker_dict.items(): + if type(val) != list: + ticker_dict[idx] = [val] + else: + ticker_dict[idx] = [",".join(val)] + + kpi = pd.DataFrame.from_dict(ticker_dict).set_index('symbol') + kpi["date"] = datetime.now() + + + kpi = kpi[kpi.columns.intersection(final_kpi_columns)] + + load_to_db(kpi, db_kpi) + update_timestamp(symbol, db_kpi) + else: + logging.info(" <%s> Data is up-to-date. Nothing to do." % db_kpi) + + # DIVIDENDS + if row['last_updated_div'] < datetime.today() - timedelta(days=sync_freq_div): + logging.info(" <%s> Last Updated above Threshold. Loading new DIVIDENDS data for symbol into DB %s" % (db_div, symbol)) + div = yticker.dividends.to_frame().reset_index() + div.insert(0, 'symbol', symbol) + div = div.set_index('symbol') + load_to_db(div, db_div) + update_timestamp(symbol, db_div) + else: + logging.info(" <%s> Data is up-to-date. Nothing to do." % db_div) + + # RECOMMENDATIONS + if row['last_updated_rec'] < datetime.today() - timedelta(days=sync_freq_rec): + logging.info(" <%s> Last Updated above Threshold. Loading new RECOMMENDATIONS data for symbol into DB %s" % (db_rec, symbol)) + rec = yticker.recommendations + if rec is not None: + rec = rec.reset_index() + rec.insert(0, 'symbol', symbol) + rec = rec.set_index('symbol').drop_duplicates(subset=['Date', 'Firm']) + load_to_db(rec, db_rec) + else: + logging.info(" <%s> No recommendation data found for %s" % (db_rec, symbol)) + update_timestamp(symbol, db_rec) + else: + logging.info(" <%s> Data is up-to-date. Nothing to do." % db_rec) + + # PRICE HISTORY + if row['last_updated_his'] < datetime.today() - timedelta(days=sync_freq_his): + if row['last_updated_his'].date() != pd.Timestamp.min.to_pydatetime().date(): + delta = (row['last_updated_his'] + timedelta(days=1)).strftime("%Y-%m-%d") + his = yticker.history(start=delta) + logging.info(" <%s> Last Updated above Threshold. Loading new PRICE data for symbol into DB %s since %s" % (db_his, symbol, delta)) + else: + his = yticker.history(period="max") + logging.info(" <%s> Never loaded price data. Loading all available price data for symbol into DB %s " % (db_his, symbol)) + + if his is not None: + his = his.reset_index() + his.insert(0, 'symbol', symbol) + his = his.set_index('symbol') + load_to_db(his, db_his) + else: + logging.info(" <%s> No price history data found for %s" % (db_rec, symbol)) + update_timestamp(symbol, db_his) + else: + logging.info(" <%s> Data is up-to-date. Nothing to do." % db_his) + + i += 1 + except Exception as er: + logging.warning("%s/%s Error occured - skipping this entry. Errormsg: \n%s" % ( + i, num_symbols, traceback.print_exception(*sys.exc_info()))) + update_loadable(symbol, 0) + i += 1 + continue + + +class Symbols(Base): + __tablename__ = db_symbol + + index = Column(Integer, primary_key=True) + symbol = Column(String) + name = Column(String) + exchange = Column(String) + initialized = Column(String) + last_updated_kpi = Column(DateTime) + last_updated_div = Column(DateTime) + last_updated_rec = Column(DateTime) + last_updated_his = Column(DateTime) + loadable = Column(Integer) + + +if __name__ == '__main__': + load_symbols() + synch_data() diff --git a/stockdash_main.py b/stockdash_main.py deleted file mode 100644 index 3ba1c5f..0000000 --- a/stockdash_main.py +++ /dev/null @@ -1,115 +0,0 @@ -import os -import pandas as pd -import json -from datetime import datetime -import time - -import stockdash_render -import yfinance as yf - -API_KEY = '44ced5e44c50543745b1d89fce8cd93a' -api_key = "?apikey=" + API_KEY -api_kpi_url = "https://financialmodelingprep.com/api/v3/key-metrics/" -api_batch_stock_price_url = "https://financialmodelingprep.com/api/v3/quote/" - -data_dir = 'data' -file_symbol = os.path.join(data_dir, 'symbols.json') -file_kpi = os.path.join(data_dir, 'comp_kpi.csv') -file_div = os.path.join(data_dir, 'comp_div.csv') -file_rec = os.path.join(data_dir, 'comp_rec.csv') -file_his = os.path.join(data_dir, 'comp_his.csv') - -sym_exclude = [] -used_columns = ['symbol', 'shortName', 'sector', 'industry', 'country', 'marketCap', 'enterpriseValue', 'dividendRate', - 'trailingPE', 'forwardPE', 'enterpriseToEbitda', 'shortRatio'] - - -def load_symbols(): - symbols = [] - with open(file_symbol) as json_file: - data = json.load(json_file) - for sym in data: - if 'exchange' in sym and sym['exchange'] == 'Nasdaq Global Select' and sym['symbol'] not in sym_exclude: - symbols.append(sym['symbol']) - return symbols - - -def get_data(symbols): - tickers = yf.Tickers(' '.join(symbols)) - - if os.path.exists(file_kpi) and os.path.exists(file_div) and os.path.exists(file_rec) and os.path.exists(file_his): - print("Found cached files. Loading cache...") - kpi_data = pd.read_csv(file_kpi) - print("%s KPIs loaded..." % len(kpi_data)) - div_data = pd.read_csv(file_div) - print("%s Dividends loaded..." % len(div_data)) - rec_data = pd.read_csv(file_rec) - print("%s Recommendations loaded..." % len(rec_data)) - his_data = pd.read_csv(file_his) - print("%s Price History loaded..." % len(his_data)) - else: - kpi_data, div_data, rec_data, his_data = pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), pd.DataFrame() - i = 1 - for ticker in tickers.tickers: - try: - info = ticker.info - print("%s/%s Querying data for ticker %s" % (i, len(symbols), info['symbol'])) - kpi_data = kpi_data.append(info, ignore_index=True) - - div = ticker.dividends.to_frame().reset_index() - div.insert(0, 'Symbol', info['symbol']) - div_data = div_data.append(div, ignore_index=True) - - rec = ticker.recommendations.reset_index() - rec.insert(0, 'Symbol', info['symbol']) - rec_data = rec_data.append(rec, ignore_index=True) - - his = ticker.history(period='5y').reset_index() - his.insert(0, 'Symbol', info['symbol']) - his_data = his_data.append(his, ignore_index=True) - i += 1 - except Exception: - print("Error occured when quering %s - skipping this entry") - continue - - kpi_data = kpi_data[['symbol'] + [col for col in kpi_data.columns if col != 'symbol']] - - rec_data['Date'] = pd.to_datetime(rec_data['Date']) - - his_data['priceMA50'] = his_data['Close'].rolling(window=50).mean() - his_data['priceMA200'] = his_data['Close'].rolling(window=200).mean() - his_data['diffMA50_200'] = his_data['priceMA50'] - his_data['priceMA200'] - - kpi_data.to_csv(file_kpi) - div_data.to_csv(file_div) - rec_data.to_csv(file_rec) - his_data.to_csv(file_his) - - return kpi_data, div_data, rec_data, his_data - - -if __name__ == '__main__': - start_time = time.time() - print("----- Starting STOCKDASH @ %s -----" % datetime.fromtimestamp(start_time)) - used_symbols = load_symbols() - - print("%s symbols loaded from file" % len(used_symbols)) - kpi_data, div_data, rec_data, his_data = get_data(used_symbols[:100]) - - # Modify Recommendation Data - rec_data_mod = pd.concat([rec_data, pd.get_dummies(rec_data['To Grade'], prefix='grade')], axis=1) - rec_data_mod.drop(['To Grade', 'From Grade', 'Action'], axis=1, inplace=True) - rec_data_mod['Date'] = pd.to_datetime(rec_data_mod['Date']) - df2 = rec_data_mod.groupby([pd.Grouper(key='Date', freq='Y'), pd.Grouper('Symbol')]).agg(['sum']) - - df2['Positive'] = df2['grade_Buy'] + df2['grade_Outperform'] + df2['grade_Market Outperform'] + df2[ - 'grade_Overweight'] + df2['grade_Positive'] + df2['grade_Strong Buy'] - df2['Neutral'] = df2['grade_Equal-Weight'] + df2['grade_Hold'] + df2['grade_Neutral'] - df2['Negative'] = df2['grade_Market Underperform'] + df2['grade_Reduce'] + df2['grade_Sell'] + df2[ - 'grade_Underweight'] - - columns = ['Positive', 'Neutral', 'Negative'] - rec_data_mod = df2[columns] - - print("Data loaded after %ss" % (time.time()-start_time)) - stockdash_render.load_dash(kpi_data[used_columns], rec_data_mod, div_data, his_data) diff --git a/stockdash_render.py b/stockdash_render.py index d18f792..fc04fa2 100644 --- a/stockdash_render.py +++ b/stockdash_render.py @@ -1,6 +1,7 @@ from datetime import datetime import time - +import flask +import pandas as pd import dash_table import plotly.graph_objects as go import dash_core_components as dcc @@ -8,167 +9,118 @@ import dash_html_components as html from dash import Dash from dash_table.Format import Format from dash.dependencies import Input, Output +from plotly.subplots import make_subplots +import stockdash_loader as sdl -colorway = ["#9a58cc", '#FF4F00', '#375CB1', '#FF7400', '#FFF400', '#FF0056'] +colorway = ["lightslategray", '#FF4F00', '#375CB1', '#FF7400', '#FFF400', '#FF0056'] +used_columns = ['symbol', 'shortName', 'sector', 'industry', 'country', 'marketCap', 'enterpriseValue', 'dividendRate', + 'trailingPE', 'forwardPE', 'enterpriseToEbitda', 'shortRatio'] PAGE_SIZE = 20 +app = Dash(__name__) +server = app.server -def load_dash(comp_kpi, rec_data_mod, div_data, his_data): - start_time = time.time() - print(" -- Rendering STOCKDASH @ %s -----" % datetime.fromtimestamp(start_time)) +start_time = time.time() +print("----- Starting STOCKDASH @ %s -----" % datetime.fromtimestamp(start_time)) +kpi_data = sdl.load_from_db(sdl.db_kpi, orderby="marketCap DESC") +print("Data loaded after %ss" % (time.time()-start_time)) +comp_kpi = kpi_data[used_columns] - app = Dash(__name__) +app.layout = html.Div(children=[ + html.Div(className='row', + children=[html.Div(className='three columns div-user-controls', + children=[ + html.H2('STOCKDASH'), + html.P('''Visualising data with Plotly - Dash'''), + html.P('''Pick one or more KPIs from the dropdown below.'''), + html.Div( + className='div-for-dropdown', + children=[ + dcc.Dropdown(id='stockselector', + options=[{'label': i, 'value': i} for i in + comp_kpi._get_numeric_data().columns], + multi=True, + value=[comp_kpi._get_numeric_data().columns[0]], + style={'backgroundColor': '#1E1E1E'}, + className='stockselector') + ], + style={'color': '#1E1E1E'}), + html.P(id="total-stocks"), + dcc.Markdown( + children=[ + "Source: [thiessen.io](https://www.thiessen.io)" + ]) + ]), + html.Div(className='nine columns div-for-charts bg-grey', + style={'padding': 0}, + children=[ + dash_table.DataTable( + id='company-kpi-data', + columns= + [{"name": i, "id": i, 'deletable': True, 'type': 'numeric', + 'format': Format(group=',')} if i in comp_kpi._get_numeric_data().columns + else {"name": i, "id": i, 'deletable': True} for i in comp_kpi.columns], + style_as_list_view=True, + style_data_conditional=[{ + 'if': {'column_editable': False}, + 'backgroundColor': 'rgba(50, 50, 50, 0.5)', + 'textAlign': 'left', + 'color': 'white', + 'padding': 7 + }], + style_filter_conditional=[{ + 'if': {'column_editable': False}, + 'backgroundColor': 'rgba(40, 40, 40,0.5)', + 'textAlign': 'left', + 'color': 'white' + }], + style_header_conditional=[{ + 'if': {'column_editable': False}, + 'backgroundColor': 'rgba(30, 30, 30,0.5)', + 'textAlign': 'left', + 'fontWeight': 'bold', + 'color': 'white' + }], + page_current=0, + page_size=PAGE_SIZE, + page_action='custom', - app.layout = html.Div(children=[ - html.Div(className='row', - children=[html.Div(className='three columns div-user-controls', - children=[ - html.H2('STOCKDASH'), - html.P('''Visualising data with Plotly - Dash'''), - html.P('''Pick one or more KPIs from the dropdown below.'''), - html.Div( - className='div-for-dropdown', - children=[ - dcc.Dropdown(id='stockselector', - options=[{'label': i, 'value': i} for i in - comp_kpi._get_numeric_data().columns], - multi=True, - value=[comp_kpi._get_numeric_data().columns[0]], - style={'backgroundColor': '#1E1E1E'}, - className='stockselector') - ], - style={'color': '#1E1E1E'}) - ]), - html.Div(className='nine columns div-for-charts bg-grey', - style={'padding': 0}, - children=[ - dash_table.DataTable( - id='company-kpi-data', - columns= - [{"name": i, "id": i, 'deletable': True, 'type': 'numeric', - 'format': Format(group=',')} if i in comp_kpi._get_numeric_data().columns - else {"name": i, "id": i, 'deletable': True} for i in comp_kpi.columns], - style_as_list_view=True, - style_data_conditional=[{ - 'if': {'column_editable': False}, - 'backgroundColor': 'rgba(50, 50, 50, 0.5)', - 'textAlign': 'left', - 'color': 'white', - 'padding': 7 - }], - style_filter_conditional=[{ - 'if': {'column_editable': False}, - 'backgroundColor': 'rgba(40, 40, 40,0.5)', - 'textAlign': 'left', - 'color': 'white' - }], - style_header_conditional=[{ - 'if': {'column_editable': False}, - 'backgroundColor': 'rgba(30, 30, 30,0.5)', - 'textAlign': 'left', - 'fontWeight': 'bold', - 'color': 'white' - }], - page_current=0, - page_size=PAGE_SIZE, - page_action='custom', + filter_action='custom', + filter_query='', - filter_action='custom', - filter_query='', + sort_action='custom', + sort_mode='multi', + sort_by=[] + ), + dcc.Graph( + id='bar-chart-marketcap', + className='bg-grey', + hoverData={'points': [{'x': 'AAPL'}]}, + animate=False), + dcc.Graph( + id='timeseries-chart-price', + className='bg-grey', + config={'displayModeBar': False}, + animate=False), + dcc.Graph( + id='recom-bar-chart', + className='bg-grey', + config={'displayModeBar': False}, + animate=False) + ]) + ]) +]) - sort_action='custom', - sort_mode='multi', - sort_by=[] - ), - dcc.Graph( - id='bar-chart-marketcap', - className='bg-grey', - hoverData={'points': [{'x': 'AAPL'}]}, - animate=True), - dcc.Graph( - id='timeseries-chart-price', - className='bg-grey', - config={'displayModeBar': False}, - animate=False), - dcc.Graph( - id='recom-bar-chart', - className='bg-grey', - config={'displayModeBar': False}, - animate=True) - ]) - ]) - ]) - @app.callback(Output('bar-chart-marketcap', 'figure'), - [Input('company-kpi-data', 'data'), - Input('stockselector', 'value')]) - def update_graph(data, selected_columns): - used_symbols = [x['symbol'] for x in data] +@app.callback(Output('bar-chart-marketcap', 'figure'), + [Input('company-kpi-data', 'data'), + Input('stockselector', 'value'), + Input('bar-chart-marketcap', 'clickData')]) +def update_graph(data, selected_columns, clickData): + used_symbols = [x['symbol'] for x in data] - figure = go.Figure( - layout=go.Layout( - colorway=colorway, - template='plotly_dark', - paper_bgcolor='rgba(0, 0, 0, 0)', - plot_bgcolor='rgba(0, 0, 0, 0)', - margin={'b': 15}, - hovermode='x', - autosize=True, - title={'text': 'Market Data', 'font': {'color': 'white'}, 'x': 0.5} - )) - - val = dict() - val["xaxis"] = dict(domain=[0.15, 0.85]) - for i, column in enumerate(selected_columns): - i += 1 - figure.add_trace(go.Bar(name=column, - x=used_symbols, - y=[x[column] for x in data], - yaxis='y' + str(i), offsetgroup=i)) - - val["yaxis%s" % i] = dict( - title=column, - titlefont=dict(color=colorway[i - 1]), - tickfont=dict(color=colorway[i - 1]), - ) - - if i == 2: - val["yaxis2"].update(dict( - anchor="x", - overlaying="y", - side="right" - )) - elif i == 3: - val["yaxis3"].update(dict( - anchor="free", - overlaying="y", - side="left", - position=0.05 - )) - elif i == 4: - val["yaxis4"].update(dict( - anchor="free", - overlaying="y", - side="right", - position=0.95 - )) - - figure.update_layout(val) - figure.update_yaxes( - showgrid=True, zeroline=True, zerolinewidth=1, zerolinecolor='White', - ) - - return figure - - @app.callback(Output('recom-bar-chart', 'figure'), - [Input('company-kpi-data', 'data')]) - def update_graph(data): - used_symbols = [x['symbol'] for x in data] - - df = rec_data_mod.loc['2020-12-31'].reset_index() - df_tmp = df.loc[df['Symbol'].isin(used_symbols)] - - figure = go.Figure(layout=go.Layout( + figure = go.Figure( + layout=go.Layout( colorway=colorway, template='plotly_dark', paper_bgcolor='rgba(0, 0, 0, 0)', @@ -176,113 +128,281 @@ def load_dash(comp_kpi, rec_data_mod, div_data, his_data): margin={'b': 15}, hovermode='x', autosize=True, - title={'text': 'Recommendation Data', 'font': {'color': 'white'}, 'x': 0.5}, - barmode='stack' + title={'text': 'Market Data', 'font': {'color': 'white'}, 'x': 0.5} )) - figure.add_trace(go.Bar(x=used_symbols, y=df_tmp['Positive'].tolist(), name='Positive Outlook', marker_color='#41B3A3')) - figure.add_trace(go.Bar(x=used_symbols, y=df_tmp['Neutral'].tolist(), name='Neutral Outlook', marker_color='#E8A87C')) - figure.add_trace(go.Bar(x=used_symbols, y=df_tmp['Negative'].tolist(), name='Negative Outlook', marker_color='#E27D60')) + val = dict() + val["xaxis"] = dict(domain=[0.15, 0.85]) + for i, column in enumerate(selected_columns): + i += 1 + figure.add_trace(go.Bar(name=column, + x=used_symbols, + y=[x[column] for x in data], + marker_color=['lightslategray',] * len(data), + yaxis='y' + str(i), offsetgroup=i)) - return figure + val["yaxis%s" % i] = dict( + title=column, + titlefont=dict(color=colorway[i - 1]), + tickfont=dict(color=colorway[i - 1]), + ) - @app.callback(Output('timeseries-chart-price', 'figure'), - [Input('bar-chart-marketcap', 'hoverData')]) - def update_graph(hoverData): - trace1 = [] - columns = ['Close', 'priceMA50', 'priceMA200'] - df_sub = his_data[his_data['Symbol'] == hoverData['points'][0]['x']] + if i == 2: + val["yaxis2"].update(dict( + anchor="x", + overlaying="y", + side="right" + )) + elif i == 3: + val["yaxis3"].update(dict( + anchor="free", + overlaying="y", + side="left", + position=0.10 + )) + elif i == 4: + val["yaxis4"].update(dict( + anchor="free", + overlaying="y", + side="right", + position=0.90 + )) - for column in columns: - trace1.append(go.Scatter(x=df_sub['Date'], - y=df_sub[column], - mode='lines', - opacity=0.7, - name=hoverData['points'][0]['x'] + "-" + column, - textposition='bottom center')) + figure.update_layout(val) + figure.update_yaxes( + showgrid=True, zeroline=True, zerolinewidth=1, zerolinecolor='White', + ) - traces = [trace1] - data = [val for sublist in traces for val in sublist] - figure = {'data': data, - 'layout': go.Layout( - colorway=colorway, - template='plotly_dark', - paper_bgcolor='rgba(0, 0, 0, 0)', - plot_bgcolor='rgba(0, 0, 0, 0)', - margin={'b': 15}, - hovermode='x', - autosize=True, + if clickData is not None: + i = 0 + for subFig in figure['data']: + color=[colorway[i],] * len(data) + color[clickData['points'][0]['pointNumber']] = 'crimson' - title={'text': 'Stock Prices', 'font': {'color': 'white'}, 'x': 0.5}, - xaxis={'range': [df_sub['Date'].min(), df_sub['Date'].max()]}, - yaxis={'range': [0, df_sub['Close'].max() + df_sub['Close'].max() / 10]} - ), - } + subFig['marker']['color'] = color + i = i +1 - return figure + return figure - def split_filter_part(filter_part): - operators = [['ge ', '>='], - ['le ', '<='], - ['lt ', '<'], - ['gt ', '>'], - ['ne ', '!='], - ['eq ', '='], - ['contains '], - ['datestartswith ']] - for operator_type in operators: - for operator in operator_type: - if operator in filter_part: - name_part, value_part = filter_part.split(operator, 1) - name = name_part[name_part.find('{') + 1: name_part.rfind('}')] +@app.callback(Output("total-stocks", "children"), + [Input('company-kpi-data', 'data')]) +def update_total_stocks(data): + stocks_picked = len(comp_kpi) + return "Total Number of Stocks loaded: %s" % stocks_picked - value_part = value_part.strip() - v0 = value_part[0] - if v0 == value_part[-1] and v0 in ("'", '"', '`'): - value = value_part[1: -1].replace('\\' + v0, v0) - else: - try: - value = float(value_part) - except ValueError: - value = value_part +@app.callback(Output('recom-bar-chart', 'figure'), + [Input('company-kpi-data', 'data')]) +def update_graph(data): + used_symbols = [x['symbol'] for x in data] - return name, operator_type[0].strip(), value - return [None] * 3 + # Modify Recommendation Data + where_clause = "symbol IN ('"+"','".join(used_symbols)+"')" + rec_data = sdl.load_from_db(sdl.db_rec, where=where_clause) + rec_data_mod = pd.concat([rec_data, pd.get_dummies(rec_data['To Grade'], prefix='grade')], axis=1) + rec_data_mod.drop(['To Grade', 'From Grade', 'Action'], axis=1, inplace=True) + rec_data_mod['Date'] = pd.to_datetime(rec_data_mod['Date']) + df2 = rec_data_mod.groupby([pd.Grouper(key='Date', freq='Y'), pd.Grouper('symbol')]).agg(['sum']) - @app.callback( - Output('company-kpi-data', "data"), - Input('company-kpi-data', "page_current"), - Input('company-kpi-data', "page_size"), - Input('company-kpi-data', "sort_by"), - Input('company-kpi-data', 'filter_query')) - def update_table(page_current, page_size, sort_by, filter): - filtering_expressions = filter.split(' && ') - dff = comp_kpi - for filter_part in filtering_expressions: - col_name, operator, filter_value = split_filter_part(filter_part) + df2['Positive'] = df2['grade_Buy'] + df2['grade_Outperform'] + df2['grade_Market Outperform'] + df2[ + 'grade_Overweight'] + df2['grade_Positive'] + df2['grade_Strong Buy'] + df2['Neutral'] = df2['grade_Equal-Weight'] + df2['grade_Hold'] + df2['grade_Neutral'] + df2['Negative'] = df2['grade_Market Underperform'] + df2['grade_Reduce'] + df2['grade_Sell'] + df2[ + 'grade_Underweight'] - if operator in ('eq', 'ne', 'lt', 'le', 'gt', 'ge'): - dff = dff.loc[getattr(dff[col_name], operator)(filter_value)] - elif operator == 'contains': - dff = dff.loc[dff[col_name].str.contains(filter_value)] - elif operator == 'datestartswith': - dff = dff.loc[dff[col_name].str.startswith(filter_value)] + columns = ['Positive', 'Neutral', 'Negative'] + rec_data_mod = df2[columns] - if len(sort_by): - dff = dff.sort_values( - [col['column_id'] for col in sort_by], - ascending=[ - col['direction'] == 'asc' - for col in sort_by - ], - inplace=False - ) + df = rec_data_mod.loc['2020-12-31'].reset_index() + df_tmp = df.loc[df['symbol'].isin(used_symbols)] - page = page_current - size = page_size - return dff.iloc[page * size: (page + 1) * size].to_dict('records') + figure = go.Figure(layout=go.Layout( + colorway=colorway, + template='plotly_dark', + paper_bgcolor='rgba(0, 0, 0, 0)', + plot_bgcolor='rgba(0, 0, 0, 0)', + margin={'b': 15}, + hovermode='x', + autosize=True, + title={'text': 'Recommendation Data', 'font': {'color': 'white'}, 'x': 0.5}, + barmode='stack' + )) - print("Rendering loaded after %ss" % (time.time()-start_time)) - app.run_server(debug=True) + figure.add_trace( + go.Bar(x=used_symbols, y=df_tmp['Positive'].tolist(), name='Positive Outlook', marker_color='#41B3A3')) + figure.add_trace( + go.Bar(x=used_symbols, y=df_tmp['Neutral'].tolist(), name='Neutral Outlook', marker_color='#E8A87C')) + figure.add_trace( + go.Bar(x=used_symbols, y=df_tmp['Negative'].tolist(), name='Negative Outlook', marker_color='#E27D60')) + + return figure + + +@app.callback(Output('timeseries-chart-price', 'figure'), + Input('bar-chart-marketcap', 'clickData'), + Input('company-kpi-data', 'data')) +def update_graph(clickData, kpi_data): + if clickData is None: + used_symbol = kpi_data[0]['symbol'] + else: + used_symbol = clickData['points'][0]['x'] + + where_clause = "symbol = '%s'" % used_symbol + his_data = sdl.load_from_db(sdl.db_his, where=where_clause, limit=1000, orderby="Date DESC") + where_clause = "symbol = '%s' and Date >= '%s'" % (used_symbol, his_data['Date'].min()) + div_data = sdl.load_from_db(sdl.db_div, where=where_clause, orderby="Date DESC") + + # Calculate rolling window + his_data['priceMA50'] = his_data['Close'].rolling(window=50, min_periods=1).mean() + std_dev = his_data['Close'].rolling(window=50, min_periods=1).std() + + his_data['priceMA50_lstd'] = his_data['priceMA50'] - 2*std_dev + his_data['priceMA50_hstd'] = his_data['priceMA50'] + 2*std_dev + his_data['priceMA200'] = his_data['Close'].rolling(window=200, min_periods=1).mean() + his_data['diffMA50_200'] = his_data['priceMA50'] - his_data['priceMA200'] + + fig = make_subplots(rows=3, cols=1, row_heights=[0.7, 0.2, 0.1], shared_xaxes=True, vertical_spacing=0.07) + + fig.add_trace(go.Candlestick(x=his_data['Date'], open=his_data['Open'], high=his_data['High'], low=his_data['Low'], + close=his_data['Close'], name=used_symbol), row=1, col=1) + + columns = ['priceMA50', 'priceMA200'] + for column in columns: + fig.add_trace(go.Scatter(x=his_data['Date'], y=his_data[column], mode='lines', opacity=0.7, + name=used_symbol + "-" + column, textposition='bottom center'), + row=1, col=1) + + fig.add_trace(go.Scatter(x=his_data['Date'], y=his_data['priceMA50_lstd'], mode='lines', opacity=0.7, line=dict(color='#ffdd70', width=1, dash='dash'), + name=used_symbol + "-" + 'Lower Band', textposition='bottom center'), + row=1, col=1) + + fig.add_trace(go.Scatter(x=his_data['Date'], y=his_data['priceMA50_hstd'], mode='lines', opacity=0.7, line=dict(color='#ffdd70', width=1, dash='dash'), + name=used_symbol + "-" + 'Higher Band', textposition='bottom center'), + row=1, col=1) + + fig.update_yaxes(showgrid=True, zeroline=False, + showspikes=True, spikemode='across', spikesnap='cursor', showline=False, spikedash='solid') + + fig.update_xaxes(showgrid=True, zeroline=False, rangeslider_visible=False, showticklabels=False, + showspikes=True, spikemode='across', spikesnap='cursor', showline=False, spikedash='solid', + rangebreaks=[ + dict(bounds=["sat", "mon"]), #hide weekends + #dict(values=["2015-12-25", "2016-01-01"]) # hide Christmas and New Year's + ]) + + fig.update_layout( + colorway=colorway, + template='plotly_dark', + paper_bgcolor='rgba(0, 0, 0, 0)', + plot_bgcolor='rgba(0, 0, 0, 0)', + autosize=True, + height=800, + hovermode='x unified', + hoverlabel=dict(font=dict(color='black')), + title={'text': 'Stock Prices', 'font': {'color': 'white'}, 'x': 0.5}, + xaxis={'range': [his_data['Date'].min(), his_data['Date'].max()], + 'showticklabels': True, + 'rangeselector_bgcolor':'rgba(0, 22, 0, 0)', + 'rangeselector': dict( + buttons=list([ + dict(count=1, + label="1m", + step="month", + stepmode="backward"), + dict(count=6, + label="6m", + step="month", + stepmode="backward"), + dict(count=1, + label="YTD", + step="year", + stepmode="todate"), + dict(count=1, + label="1y", + step="year", + stepmode="backward"), + dict(step="all") + ]))}, + yaxis1={'autorange': True, 'fixedrange': False}, + legend=dict(y=1, x=0), + dragmode='pan') + + fig.add_trace( + go.Bar(x=his_data['Date'], y=his_data['Volume'], marker_color='#3399ff', name='Volume'), + row=2, col=1) + + fig.add_trace( + go.Scatter(x=div_data['Date'], y=div_data['Dividends'], marker_color='#fae823', name='Dividends', line=dict( + shape='hv' + )), + row=3, col=1) + + return fig + + +def split_filter_part(filter_part): + operators = [['ge ', '>='], + ['le ', '<='], + ['lt ', '<'], + ['gt ', '>'], + ['ne ', '!='], + ['eq ', '='], + ['contains '], + ['datestartswith ']] + + for operator_type in operators: + for operator in operator_type: + if operator in filter_part: + name_part, value_part = filter_part.split(operator, 1) + name = name_part[name_part.find('{') + 1: name_part.rfind('}')] + + value_part = value_part.strip() + v0 = value_part[0] + + if v0 == value_part[-1] and v0 in ("'", '"', '`'): + value = value_part[1: -1].replace('\\' + v0, v0) + else: + try: + value = float(value_part) + except ValueError: + value = value_part + + return name, operator_type[0].strip(), value + return [None] * 3 + +@app.callback( + Output('company-kpi-data', "data"), + Input('company-kpi-data', "page_current"), + Input('company-kpi-data', "page_size"), + Input('company-kpi-data', "sort_by"), + Input('company-kpi-data', 'filter_query')) +def update_table(page_current, page_size, sort_by, filter): + filtering_expressions = filter.split(' && ') + dff = comp_kpi + for filter_part in filtering_expressions: + col_name, operator, filter_value = split_filter_part(filter_part) + + if operator in ('eq', 'ne', 'lt', 'le', 'gt', 'ge'): + dff = dff.loc[getattr(dff[col_name], operator)(filter_value)] + elif operator == 'contains': + dff = dff.loc[dff[col_name].str.contains(filter_value)] + elif operator == 'datestartswith': + dff = dff.loc[dff[col_name].str.startswith(filter_value)] + + if len(sort_by): + dff = dff.sort_values( + [col['column_id'] for col in sort_by], + ascending=[ + col['direction'] == 'asc' + for col in sort_by + ], + inplace=False + ) + + page = page_current + size = page_size + return dff.iloc[page * size: (page + 1) * size].to_dict('records') + + +app.run_server(debug=True, port=18051, threaded=True)