├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── _bootswatch.scss ├── _variables.scss └── bootstrap.css ├── dash_server.py ├── data └── README.md ├── dockerFiles ├── Dockerfile.scrapper └── Dockerfile.server ├── images ├── code-fork-solid.svg ├── sample.png ├── sample2.png └── sample3.png ├── requirements.txt ├── scripts ├── scrapper.sh └── server.sh ├── src ├── config.json ├── fetch_data.py └── utils.py └── stock_scrapper.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | data/*.csv 132 | *.csv 133 | *.json -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Python image with a specific version 2 | FROM python:3.9-slim 3 | 4 | # Set the working directory 5 | WORKDIR /app 6 | 7 | # Copy the requirements file and source code 8 | COPY requirements.txt ./ 9 | COPY . . 10 | 11 | # Install dependencies 12 | RUN pip install --no-cache-dir -r requirements.txt 13 | 14 | # Expose the port that Dash will run on 15 | EXPOSE 5000 16 | 17 | # Install supervisor 18 | RUN apt-get update && apt-get install -y supervisor && rm -rf /var/lib/apt/lists/* 19 | 20 | # Create supervisor configuration file 21 | RUN echo "[supervisord]\n" \ 22 | "nodaemon=true\n\n" \ 23 | "[program:app]\n" \ 24 | "command=python3 dash_server.py\n" \ 25 | "autostart=true\n" \ 26 | "autorestart=true\n" \ 27 | "stdout_logfile=/var/log/app.log\n" \ 28 | "stderr_logfile=/var/log/app.err\n\n" \ 29 | "[program:main]\n" \ 30 | "command=python3 stock_scrapper.py\n" \ 31 | "autostart=true\n" \ 32 | "autorestart=true\n" \ 33 | "stdout_logfile=/var/log/main.log\n" \ 34 | "stderr_logfile=/var/log/main.err\n" \ 35 | > /etc/supervisor/conf.d/supervisord.conf 36 | 37 | # Run supervisor to start both scripts 38 | CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Deepak Raj 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stock Dashboard 2 | 3 | Dashboard to track the stock market and send notifications when the stock price is above or below a certain threshold. 4 | 5 | Data for stocks is scrapped from [Google Finance](https://www.google.com/finance/). 6 | 7 | ## Demo 8 | 9 | A Demo can be check here:- [Demo](http://13.235.246.34:5000/) : not available right now 10 | 11 | ## How to use 12 | 13 | ### for sending stock alert in discord channel 14 | 15 | ```bash 16 | python -m venv venv 17 | source venv/bin/activate 18 | pip install -r requirements.txt 19 | python stock_scrapper.py 20 | ``` 21 | 22 | ### for running dashboard 23 | 24 | ```bash 25 | python dash_server.py 26 | ``` 27 | 28 | ## Dashboard Table 29 | 30 | ![Dashboard table](/images/sample3.png) 31 | 32 | 33 | # Dashboard Graph 34 | 35 | ![Dashboard Graph](/images/sample2.png) 36 | 37 | 38 | - Update the webhook url in the utils.py, webhook url can be found in the discord channel. 39 | - Update the config.json with your favorite stocks and thresholds 40 | 41 | ## Upcoming features 42 | 43 | 44 | 45 | ## Why this project? 46 | 47 | to track your favorite stocks in one click and send notifications when the stock price is above or below a certain threshold and to keep update yourself with the stock market without any hassle. 48 | 49 | ## Author 50 | 51 | - [Deepak Raj](https://github.com/codeperfectplus) 52 | 53 | 54 | sudo docker build -t stock-alert-dashboard . 55 | sudo docker run -p 5000:5000 stock-alert-dashboard 56 | -------------------------------------------------------------------------------- /assets/_bootswatch.scss: -------------------------------------------------------------------------------- 1 | // Superhero 5.1.3 2 | // Bootswatch 3 | 4 | 5 | // Variables 6 | 7 | $web-font-path: "https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap" !default; 8 | @if $web-font-path { 9 | @import url($web-font-path); 10 | } 11 | 12 | // Buttons 13 | 14 | .btn { 15 | @each $color, $value in $theme-colors { 16 | &-#{$color} { 17 | @if $enable-gradients { 18 | background: $value linear-gradient(180deg, mix($white, $value, 15%), $value) repeat-x; 19 | } @else { 20 | background-color: $value; 21 | } 22 | } 23 | } 24 | } 25 | 26 | // Typography 27 | 28 | .dropdown-menu { 29 | font-size: $font-size-sm; 30 | } 31 | 32 | .dropdown-header { 33 | font-size: $font-size-sm; 34 | } 35 | 36 | .blockquote-footer { 37 | color: $body-color; 38 | } 39 | 40 | // Tables 41 | 42 | .table { 43 | font-size: $font-size-sm; 44 | 45 | .thead-dark th { 46 | color: $white; 47 | } 48 | 49 | a:not(.btn) { 50 | color: $white; 51 | text-decoration: underline; 52 | } 53 | 54 | .dropdown-menu a { 55 | text-decoration: none; 56 | } 57 | 58 | .text-muted { 59 | color: $text-muted; 60 | } 61 | } 62 | 63 | // Forms 64 | 65 | label, 66 | .radio label, 67 | .checkbox label, 68 | .help-block { 69 | font-size: $font-size-sm; 70 | } 71 | 72 | .form-floating { 73 | label { 74 | color: $input-placeholder-color; 75 | } 76 | } 77 | 78 | // Navs 79 | 80 | .nav-tabs, 81 | .nav-pills { 82 | .nav-link, 83 | .nav-link:hover { 84 | color: $body-color; 85 | } 86 | 87 | .nav-link.disabled { 88 | color: $nav-link-disabled-color; 89 | } 90 | } 91 | 92 | .page-link:hover, 93 | .page-link:focus { 94 | color: $white; 95 | text-decoration: none; 96 | } 97 | 98 | // Indicators 99 | 100 | .alert { 101 | color: $white; 102 | border: none; 103 | 104 | a, 105 | .alert-link { 106 | color: $white; 107 | text-decoration: underline; 108 | } 109 | 110 | @each $color, $value in $theme-colors { 111 | &-#{$color} { 112 | @if $enable-gradients { 113 | background: $value linear-gradient(180deg, mix($white, $value, 15%), $value) repeat-x; 114 | } @else { 115 | background-color: $value; 116 | } 117 | } 118 | } 119 | } 120 | 121 | .badge { 122 | &-warning, 123 | &-info { 124 | color: $white; 125 | } 126 | } 127 | 128 | // Popovers 129 | 130 | .popover-header { 131 | border-top-left-radius: 0; 132 | border-top-right-radius: 0; 133 | } 134 | 135 | // Containers 136 | 137 | .modal { 138 | &-header, 139 | &-footer { 140 | background-color: $table-hover-bg; 141 | } 142 | } -------------------------------------------------------------------------------- /assets/_variables.scss: -------------------------------------------------------------------------------- 1 | // Superhero 5.1.3 2 | // Bootswatch 3 | 4 | $theme: "superhero" !default; 5 | 6 | // 7 | // Color system 8 | // 9 | 10 | $white: #fff !default; 11 | $gray-100: #ebebeb !default; 12 | $gray-200: #4e5d6c !default; 13 | $gray-300: #dee2e6 !default; 14 | $gray-400: #ced4da !default; 15 | $gray-500: #adb5bd !default; 16 | $gray-600: #868e96 !default; 17 | $gray-700: #495057 !default; 18 | $gray-800: #343a40 !default; 19 | $gray-900: #212529 !default; 20 | $black: #000 !default; 21 | 22 | $blue: #4c9be8 !default; 23 | $indigo: #6610f2 !default; 24 | $purple: #6f42c1 !default; 25 | $pink: #e83e8c !default; 26 | $red: #d9534f !default; 27 | $orange: #f0ad4e !default; 28 | $yellow: #ffc107 !default; 29 | $green: #5cb85c !default; 30 | $teal: #20c997 !default; 31 | $cyan: #5bc0de !default; 32 | 33 | $primary: $blue !default; 34 | $secondary: $gray-200 !default; 35 | $success: $green !default; 36 | $info: $cyan !default; 37 | $warning: $yellow !default; 38 | $danger: $red !default; 39 | $light: lighten($gray-200, 35%) !default; 40 | $dark: #20374c !default; 41 | 42 | $min-contrast-ratio: 1.6 !default; 43 | 44 | // Body 45 | 46 | $body-bg: #0f2537 !default; 47 | $body-color: $gray-100 !default; 48 | 49 | // Components 50 | 51 | $border-radius: 0 !default; 52 | $border-radius-lg: 0 !default; 53 | $border-radius-sm: 0 !default; 54 | 55 | // Fonts 56 | 57 | // stylelint-disable-next-line value-keyword-case 58 | $font-family-sans-serif: Lato, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default; 59 | 60 | $text-muted: rgba(255, 255, 255, .4) !default; 61 | 62 | // Tables 63 | 64 | $table-accent-bg: rgba($white, .05) !default; 65 | $table-hover-bg: rgba($white, .075) !default; 66 | $table-border-color: rgba($black, .15) !default; 67 | $table-head-bg: $light !default; 68 | $table-dark-bg: $light !default; 69 | $table-dark-border-color: $gray-200 !default; 70 | $table-dark-color: $body-bg !default; 71 | 72 | $table-bg-scale: 0 !default; 73 | 74 | // Forms 75 | 76 | $input-bg: $white !default; 77 | $input-disabled-bg: $gray-100 !default; 78 | 79 | $input-color: $gray-900 !default; 80 | $input-border-color: transparent !default; 81 | $input-border-width: 0 !default; 82 | 83 | $input-placeholder-color: $gray-600 !default; 84 | 85 | $input-group-addon-color: $body-color !default; 86 | 87 | $form-check-input-bg: $white !default; 88 | $form-check-input-border: none !default; 89 | 90 | $form-file-button-color: $body-color !default; 91 | 92 | $form-floating-label-opacity: 1 !default; 93 | 94 | // Dropdowns 95 | 96 | $dropdown-bg: $gray-200 !default; 97 | $dropdown-divider-bg: rgba($black, .15) !default; 98 | $dropdown-link-color: $body-color !default; 99 | $dropdown-link-hover-color: $dropdown-link-color !default; 100 | $dropdown-link-hover-bg: $table-hover-bg !default; 101 | 102 | // Navs 103 | 104 | $nav-link-disabled-color: rgba(255, 255, 255, .4) !default; 105 | $nav-tabs-border-color: $gray-200 !default; 106 | $nav-tabs-link-active-color: $body-color !default; 107 | $nav-tabs-link-active-border-color: $gray-200 !default; 108 | 109 | // Navbar 110 | 111 | $navbar-dark-color: rgba($white, .75) !default; 112 | $navbar-dark-hover-color: $white !default; 113 | 114 | // Pagination 115 | 116 | $pagination-color: $white !default; 117 | $pagination-bg: $gray-200 !default; 118 | $pagination-border-color: transparent !default; 119 | $pagination-hover-color: $white !default; 120 | $pagination-hover-bg: $nav-link-disabled-color !default; 121 | $pagination-hover-border-color: $pagination-border-color !default; 122 | $pagination-disabled-color: $nav-link-disabled-color !default; 123 | $pagination-disabled-bg: $pagination-bg !default; 124 | $pagination-disabled-border-color: $pagination-border-color !default; 125 | 126 | // Cards 127 | 128 | $card-cap-bg: $table-hover-bg !default; 129 | $card-bg: $gray-200 !default; 130 | $card-inner-border-radius: 0 !default; 131 | 132 | // Popovers 133 | 134 | $popover-bg: $gray-200 !default; 135 | $popover-header-bg: $table-hover-bg !default; 136 | 137 | // Toasts 138 | 139 | $toast-background-color: $gray-200 !default; 140 | $toast-border-color: rgba(0, 0, 0, .2) !default; 141 | $toast-header-color: $body-color !default; 142 | $toast-header-background-color: $toast-background-color !default; 143 | $toast-header-border-color: $toast-border-color !default; 144 | 145 | // Modals 146 | 147 | $modal-content-bg: $gray-200 !default; 148 | $modal-header-border-color: rgba(0, 0, 0, .2) !default; 149 | 150 | // List group 151 | 152 | $list-group-color: $white !default; 153 | $list-group-bg: $gray-200 !default; 154 | $list-group-border-color: transparent !default; 155 | $list-group-hover-bg: $nav-link-disabled-color !default; 156 | $list-group-disabled-color: $nav-link-disabled-color !default; 157 | $list-group-action-color: $white !default; 158 | $list-group-action-hover-color: $white !default; 159 | 160 | // Breadcrumbs 161 | 162 | $breadcrumb-padding-y: .375rem !default; 163 | $breadcrumb-padding-x: .75rem !default; 164 | $breadcrumb-bg: $gray-200 !default; 165 | $breadcrumb-divider-color: $body-color !default; 166 | $breadcrumb-active-color: $body-color !default; 167 | 168 | // Close 169 | 170 | $btn-close-color: $white !default; 171 | $btn-close-opacity: .5 !default; 172 | $btn-close-hover-opacity: 1 !default; 173 | 174 | // Code 175 | 176 | $pre-color: inherit !default; -------------------------------------------------------------------------------- /dash_server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytz 3 | import datetime 4 | import pandas as pd 5 | import dash_bootstrap_components as dbc 6 | from dash import Dash, html, dcc, dash_table, Input, Output 7 | 8 | from src.utils import root_dir 9 | 10 | import warnings 11 | warnings.filterwarnings("ignore") 12 | 13 | IST = pytz.timezone('Asia/Kolkata') 14 | today_date = datetime.datetime.now(IST).strftime('%d-%m-%Y') 15 | 16 | app = Dash(__name__) 17 | app.title = 'Stock Alert Dashboard(v1.1)' 18 | 19 | def get_filtered_data(): 20 | df = pd.read_csv(os.path.join(root_dir, 'data/stock.csv'), header=None) 21 | df.rename(columns={0: 'Stock Name', 22 | 1: 'Previous Close', 23 | 2: 'Current Price', 24 | 3: 'Minimum(Day)', 25 | 4: 'Maximum(Day)', 26 | 5: 'Minimum(Year)', 27 | 6: 'Maximum(Year)', 28 | 7: 'Minimum(Threshold)', 29 | 8: 'Maximum(Threshold)', 30 | 9: 'Last Update', 31 | 10: 'difference', 32 | 11: 'difference(%)', 33 | 12: 'buy', 34 | 13: 'market', 35 | 14: 'currency'}, inplace=True) 36 | 37 | 38 | columns = ['Stock Name', 'Previous Close', 'Current Price', 'difference', 'difference(%)', 39 | 'Minimum(Day)', 'Maximum(Day)', 'Minimum(Year)', 'Maximum(Year)', 'Minimum(Threshold)', 'Maximum(Threshold)'] 40 | df = df.round(2) 41 | df['Last Update'] = pd.to_datetime(df['Last Update']) 42 | lastest_date = df.groupby(['Stock Name'])[ 43 | 'Last Update'].max().reset_index()['Last Update'] 44 | 45 | filtered_df = df[df['Last Update'].isin(lastest_date)] 46 | filtered_df.sort_values(by=['difference(%)'], inplace=True, ascending=False) 47 | filtered_df['difference(%)'] = filtered_df['difference(%)'].apply(lambda x: '{} %'.format(x)) 48 | filtered_df["Last Update"] = filtered_df["Last Update"].apply(lambda x: x.strftime('%H:%M:%S')) 49 | df["Last Update"] = df["Last Update"].apply(lambda x: x.strftime('%H:%M:%S')) 50 | 51 | indian_stocks = filtered_df[filtered_df['market'] == 'IN'] 52 | 53 | buy_table = indian_stocks[indian_stocks['buy'] == True] 54 | watch_table = indian_stocks[indian_stocks['buy'] == False] 55 | 56 | buy_table = buy_table[columns] 57 | watch_table = watch_table[columns] 58 | 59 | us_stocks = filtered_df[filtered_df['market'] == 'US'] 60 | us_stocks = us_stocks[columns] 61 | 62 | return df, buy_table, watch_table, us_stocks 63 | 64 | 65 | def overall_market_data(): 66 | market_data = pd.read_csv(os.path.join(root_dir, 'data/market.csv'), header=None) 67 | columns = ['Stock Name', 'Previous Close', 'Current Price', 'difference', 'difference(%)', 68 | 'Minimum(Day)', 'Maximum(Day)', 'Minimum(Year)', 'Maximum(Year)'] 69 | market_data.rename(columns={0: 'Stock Name', 70 | 1: 'Previous Close', 71 | 2: 'Current Price', 72 | 3: 'Minimum(Day)', 73 | 4: 'Maximum(Day)', 74 | 5: 'Minimum(Year)', 75 | 6: 'Maximum(Year)', 76 | 7: 'Last Update', 77 | 8: 'difference', 78 | 9: 'difference(%)'}, inplace=True) 79 | market_data = market_data.round(2) 80 | market_data['Last Update'] = pd.to_datetime(market_data['Last Update']) 81 | lastest_date = market_data.groupby(['Stock Name'])['Last Update'].max().reset_index()['Last Update'] 82 | filtered_df = market_data[market_data['Last Update'].isin(lastest_date)] 83 | filtered_df = filtered_df[columns] 84 | 85 | return filtered_df, lastest_date[0] 86 | 87 | 88 | df, buy_table, watch_table, us_stocks = get_filtered_data() 89 | market_data, latest_date = overall_market_data() 90 | 91 | min_time = '09:00:00' 92 | max_time = '16:00:00' 93 | 94 | min_time = datetime.datetime.strptime(min_time, '%H:%M:%S') 95 | max_time = datetime.datetime.strptime(max_time, '%H:%M:%S') 96 | 97 | def get_dash_table(table_id, df): 98 | return dash_table.DataTable( 99 | id=table_id, 100 | columns=[{"name": i, "id": i} for i in df.columns], 101 | data=df.to_dict('records'), 102 | style_cell={'textAlign': 'center'}, 103 | style_data={ 104 | 'border': '1px solid black', 105 | }, 106 | style_header={ 107 | 'border': '1px solid black', 108 | 'backgroundColor': 'white', 109 | 'color': 'black', 110 | 'fontWeight': 'bold', 111 | 'textAlign': 'center', 112 | 'font-family': 'Courier New', 113 | }, 114 | style_data_conditional=[ 115 | { 116 | 'if': { 117 | 'filter_query': '{difference} >= 0'}, 118 | 'backgroundColor': 'rgb(0, 102, 51)', 119 | 'color': 'white', 120 | 'fontWeight': 'bold' 121 | }, 122 | { 123 | 'if': { 124 | 'filter_query': '{difference} < 0'}, 125 | 'backgroundColor': 'rgb(102, 0, 0)', 126 | 'color': 'white', 127 | 'fontWeight': 'bold' 128 | } 129 | ], 130 | style_table={ 131 | 'width': '100%', 132 | 'height': '100%', 133 | 'overflowY': 'scroll', 134 | 'overflowX': 'scroll', 135 | 'textAlign': 'center', 136 | }, 137 | ) 138 | 139 | 140 | app.layout = html.Div([ 141 | 142 | html.H1('Stock Alert Dashboard(v1.1)', 143 | style={'textAlign': 'center', 'color': '#0099ff', 'font-family': 'Courier New', 144 | 'font-size': '30px', 'font-weight': 'bold', 'margin-top': '20px'}), 145 | # update dbc badge 146 | html.H3(id='last-update-badge', style={'textAlign': 'center', 'color': '#0099ff', 147 | 'font-family': 'Courier New', 'font-size': '20px', 'font-weight': 'bold'}), 148 | 149 | # overall market table 150 | html.H2('OverAll Market Status', style={ 151 | 'textAlign': 'center', 'color': '#0099ff', 'font-family': 'Courier New', 'font-size': '20px', 'font-weight': 'bold'}), 152 | get_dash_table('overall-table', market_data), 153 | 154 | dcc.Interval( 155 | id='interval-component', 156 | interval=5*1000, 157 | n_intervals=0), 158 | 159 | # buy table and watch table 160 | dbc.Row([ 161 | html.H2('Buy Stocks(INR)', style={'textAlign': 'center', 'color': '#0099ff', 162 | 'font-family': 'Courier New', 'font-size': '20px', 'font-weight': 'bold', 'margin-top': '20px'}), 163 | get_dash_table('buy-table', buy_table), 164 | 165 | html.H2('Watch Stocks(INR)', style={'textAlign': 'center', 'color': '#0099ff', 166 | 'font-family': 'Courier New', 'font-size': '20px', 'font-weight': 'bold', 'margin-top': '20px'}), 167 | get_dash_table('watch-table', watch_table), 168 | 169 | html.H2('US Stocks($)', style={'textAlign': 'center', 'color': '#0099ff', 170 | 'font-family': 'Courier New', 'font-size': '20px', 'font-weight': 'bold', 'margin-top': '20px'}), 171 | 172 | get_dash_table('us-table', us_stocks), 173 | ]), 174 | 175 | 176 | # graph by stock name select box 177 | html.Div([ 178 | html.H3('Graph for Stock Data', style={'textAlign': 'center', 'color': '#0099ff', 'font-family': 'Courier New', 179 | 'font-size': '30px', 'font-weight': 'bold', 'margin-top': '20px'}), 180 | dcc.Dropdown( 181 | id='stock-name-select', 182 | options=[{'label': i, 'value': i} for i in df['Stock Name'].unique()], 183 | value='Steel Authority of India Limited', 184 | style={'width': '100%', 'margin-top': '20px', 'color': '#0099ff', 185 | 'font-family': 'Courier New', 'font-size': '20px'} 186 | ) 187 | ]), 188 | html.Div([ 189 | dcc.Graph(id='stock-graph'), 190 | ]), 191 | ]) 192 | 193 | # callback for interval component 194 | 195 | 196 | @app.callback( 197 | Output('buy-table', 'data'), 198 | Input('interval-component', 'n_intervals')) 199 | def update_buy_table(n): 200 | _, buy_table, _, _ = get_filtered_data() 201 | 202 | return buy_table.to_dict('records') 203 | 204 | 205 | @app.callback( 206 | Output('watch-table', 'data'), 207 | Input('interval-component', 'n_intervals')) 208 | def update_watch_table(n): 209 | _, _, watch_table, _ = get_filtered_data() 210 | 211 | return watch_table.to_dict('records') 212 | 213 | @app.callback( 214 | Output('us-table', 'data'), 215 | Input('interval-component', 'n_intervals')) 216 | def update_watch_table(n): 217 | _, _, _, us_stocks = get_filtered_data() 218 | 219 | return us_stocks.to_dict('records') 220 | 221 | 222 | @app.callback( 223 | Output('overall-table', 'data'), 224 | Input('interval-component', 'n_intervals')) 225 | def update_index_table(n): 226 | market_data, _ = overall_market_data() 227 | return market_data.to_dict('records') 228 | 229 | 230 | @app.callback( 231 | Output('last-update-badge', 'children'), 232 | Input('interval-component', 'n_intervals')) 233 | def update_badge(n): 234 | 235 | _, latest_date = overall_market_data() 236 | 237 | return "Dashboard last updated at {}".format(latest_date.strftime('%d-%m-%Y %H:%M:%S')) 238 | 239 | 240 | @app.callback( 241 | Output('stock-graph', 'figure'), 242 | Input('stock-name-select', 'value'), 243 | Input('interval-component', 'n_intervals')) 244 | def update_graph(stock_name, n): 245 | df, _, _, _= get_filtered_data() 246 | filtered_df = df[df['Stock Name'] == stock_name] 247 | 248 | return { 249 | 'data': [{ 250 | 'x': filtered_df['Last Update'], 251 | 'y': filtered_df['Current Price'], 252 | 'name': 'Current Price', 253 | 'mode': 'lines', 254 | 'line': {'width': 1} 255 | }, 256 | { 257 | 'x': filtered_df['Last Update'], 258 | 'y': filtered_df['Minimum(Threshold)'], 259 | 'name': 'Min Threshold', 260 | 'mode': 'lines', 261 | 'line': {'width': 1, 'dash': 'dash', 'color': 'red'}, 262 | }, 263 | { 264 | 'x': filtered_df['Last Update'], 265 | 'y': filtered_df['Maximum(Threshold)'], 266 | 'name': 'Max Threshold', 267 | 'mode': 'lines', 268 | 'line': {'width': 1, 'dash': 'dash', 'color': 'green'}, 269 | }], 270 | 271 | 'layout': { 272 | 'title': '{}- ({}) @ {}'.format(stock_name, filtered_df['market'].iloc[-1], filtered_df['Current Price'].iloc[-1]), 273 | 'xaxis': {'title': 'Date', 274 | 'autorange': True, 275 | 'showgrid': True, 276 | 'zeroline': True, 277 | 'showline': True, 278 | 'mirror': True, 279 | 'ticks': '', 280 | 'showticklabels': True, 281 | 'tickangle': 90, 282 | 'tickfont': {'size': 10}, 283 | 'range': [min_time, max_time]}, 284 | 'yaxis': {'title': 'Price(Per Stock)-{}'.format(filtered_df['currency'].iloc[-1]), 285 | 'range': [min(filtered_df['Minimum(Threshold)']) - 10, max(filtered_df['Maximum(Threshold)']) + 10]}, 286 | 'height': 600, 287 | 'margin': {'l': 60, 'r': 10}, 288 | 'hovermode': 'closest', 289 | 'showlegend': True, 290 | 'legend': {'x': 0.8, 'y': 1.1, 'orientation': 'h'}, 291 | } 292 | } 293 | 294 | 295 | if __name__ == '__main__': 296 | app.run_server(host='0.0.0.0', port=5000, debug=True) 297 | -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeperfectplus/Stock-Dashboard/aad3032933b95a3f5c8c98ee4b7579eb2dd981f8/data/README.md -------------------------------------------------------------------------------- /dockerFiles/Dockerfile.scrapper: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | USER root 3 | 4 | # Create a working directory 5 | RUN mkdir /app 6 | WORKDIR /app 7 | RUN chmod 777 /app 8 | 9 | # Install python packages from requirements file 10 | RUN python -m pip install --upgrade pip 11 | ADD requirements.txt /app/ 12 | RUN pip install -r /app/requirements.txt 13 | 14 | ADD . /app/ 15 | RUN which python \ 16 | && export PYTHONPATH=. 17 | 18 | WORKDIR /app 19 | RUN chmod 755 /app/scripts/scrapper.sh -------------------------------------------------------------------------------- /dockerFiles/Dockerfile.server: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | USER root 3 | 4 | # Create a working directory 5 | RUN mkdir /app 6 | WORKDIR /app 7 | RUN chmod 777 /app 8 | 9 | # Install python packages from requirements file 10 | RUN python -m pip install --upgrade pip 11 | ADD requirements.txt /app/ 12 | RUN pip install -r /app/requirements.txt 13 | 14 | ADD . /app/ 15 | RUN which python \ 16 | && export PYTHONPATH=. 17 | 18 | WORKDIR /app 19 | RUN chmod 755 /app/scripts/server.sh -------------------------------------------------------------------------------- /images/code-fork-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeperfectplus/Stock-Dashboard/aad3032933b95a3f5c8c98ee4b7579eb2dd981f8/images/sample.png -------------------------------------------------------------------------------- /images/sample2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeperfectplus/Stock-Dashboard/aad3032933b95a3f5c8c98ee4b7579eb2dd981f8/images/sample2.png -------------------------------------------------------------------------------- /images/sample3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeperfectplus/Stock-Dashboard/aad3032933b95a3f5c8c98ee4b7579eb2dd981f8/images/sample3.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | numpy 3 | pandas 4 | dash 5 | discord-webhook 6 | beautifulsoup4 7 | dash_bootstrap_components -------------------------------------------------------------------------------- /scripts/scrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | touch /app/data/stock.csv 4 | touch /app/data/market.csv 5 | 6 | cd /app 7 | 8 | nohup python3 stock_scrapper.py & 9 | tail -100f nohup.out 10 | -------------------------------------------------------------------------------- /scripts/server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd /app 3 | 4 | nohup python3 dash_server.py & 5 | tail -100f nohup.out 6 | -------------------------------------------------------------------------------- /src/config.json: -------------------------------------------------------------------------------- 1 | [{"stock_name": "Vodafone Idea Ltd", "symbol": "IDEA:NSE", "min_price": 8.5, "max_price": 15.88, "buy": true, "market": "IN", "currency": "INR"}, {"stock_name": "Infosys Ltd", "symbol": "INFY:NSE", "min_price": 1459, "max_price": 2000, "buy": false, "market": "IN", "currency": "INR"}, {"stock_name": "Oil & Natural Gas Corporation Limited", "symbol": "ONGC:NSE", "min_price": 127.65, "max_price": 330.0, "buy": true, "market": "IN", "currency": "INR"}, {"stock_name": "Adani Ports and Special Economic Zone Ltd", "symbol": "ADANIPORTS:NSE", "min_price": 1053, "max_price": 1490.0, "buy": false, "market": "IN", "currency": "INR"}, {"stock_name": "NVIDIA Corporation", "symbol": "NVDA:NASDAQ", "min_price": 112.27, "max_price": 1064.69, "buy": true, "market": "US", "currency": "Dollar"}] -------------------------------------------------------------------------------- /src/fetch_data.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pytz 3 | import requests 4 | from bs4 import BeautifulSoup 5 | from datetime import datetime 6 | 7 | base_url = "https://www.google.com/finance/quote/" 8 | 9 | IST = pytz.timezone('Asia/Kolkata') 10 | 11 | def fetch_data(stock): 12 | final_url = base_url + stock 13 | data = {} 14 | html_data = requests.get(final_url).text.replace(',', '') 15 | soup = BeautifulSoup(html_data, "html.parser") 16 | data["title"] = soup.title.string 17 | 18 | current_price = soup.find('div', class_='YMlKec fxKbKc').text 19 | current_price = re.findall('\d+\.\d+', current_price) 20 | data['current_price'] = float(current_price[0]) 21 | 22 | sidebar_data = soup.find('div', 'eYanAe').text 23 | sidebar_data = re.findall('\d+\.\d+', sidebar_data) 24 | sidebar_data = [float(data) for data in sidebar_data] 25 | 26 | data['previous_close'] = sidebar_data[0] 27 | data['day_min'] = sidebar_data[1] 28 | data['day_max'] = sidebar_data[2] 29 | data['year_min'] = sidebar_data[3] 30 | data['year_max'] = sidebar_data[4] 31 | data['date_time'] = datetime.now(IST).strftime("%Y-%m-%d %H:%M:%S") 32 | try: 33 | data['market_cap'] = sidebar_data[5] 34 | 35 | except Exception as e: 36 | print(e) 37 | return data -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from discord_webhook import DiscordWebhook, DiscordEmbed 4 | 5 | discord_webhook_url = "https://discord.com/api/webhooks/832610199613210646/WuPRjWtvLe3NFAqyUqXtkC_l5irBQcor-XKBLv5IvZ1-p2F8R0q7Y3MepckZNbVy4AO9" 6 | 7 | root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 8 | config_path = os.path.join(root_dir, 'src/config.json') 9 | 10 | def update_config(data): 11 | with open(config_path, 'w') as f: 12 | json.dump(data, f) 13 | 14 | 15 | def read_config(): 16 | with open(config_path, 'r') as f: 17 | configs = json.load(f) 18 | 19 | return configs 20 | 21 | 22 | def send_message_to_discord(message): 23 | """ Send message to discord """ 24 | webhook = DiscordWebhook(url=discord_webhook_url, username="Stock Bot") 25 | embed = DiscordEmbed(title='Stock Dashboard Alert', description=message, color='03b2f8') 26 | embed.set_author(name='Stock Bot', url='https://github.com/Py-Contributors/Stock-Dashboard') 27 | webhook.add_embed(embed) 28 | webhook.execute() 29 | 30 | -------------------------------------------------------------------------------- /stock_scrapper.py: -------------------------------------------------------------------------------- 1 | import os 2 | from logging import root, shutdown 3 | import time 4 | import shutil 5 | import pytz 6 | import datetime 7 | from src.fetch_data import fetch_data 8 | from src.utils import read_config, update_config, root_dir 9 | 10 | IST = pytz.timezone('Asia/Kolkata') 11 | 12 | 13 | def check_alert(config): 14 | stock_data = fetch_data(config['symbol']) 15 | print('Checking alert for {}'.format(config['symbol'])) 16 | # update csv 17 | difference = stock_data['current_price'] - stock_data['previous_close'] 18 | difference_percentage = (difference / stock_data['previous_close']) * 100 19 | with open(os.path.join(root_dir, 'data/stock.csv'), 'a') as f: 20 | f.write('{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}\n'.format( 21 | config['stock_name'], 22 | stock_data['previous_close'], 23 | stock_data['current_price'], 24 | stock_data['day_min'], 25 | stock_data['day_max'], 26 | stock_data['year_min'], 27 | stock_data['year_max'], 28 | config['min_price'], 29 | config['max_price'], 30 | stock_data['date_time'], 31 | difference, 32 | difference_percentage, 33 | config['buy'], 34 | config['market'], 35 | config['currency'] 36 | )) 37 | 38 | if stock_data['current_price'] < config['min_price']: 39 | difference = config['min_price'] - stock_data['current_price'] 40 | difference = round(difference, 2) 41 | message = ':small_red_triangle_down: __**{}**__ \nCurrent Value: {}\nMin threshold: {}\ndifference: {}'.format( 42 | config['stock_name'], 43 | stock_data['current_price'], 44 | config['min_price'], 45 | difference) 46 | config['min_price'] = stock_data['current_price'] 47 | #send_message_to_discord(message) 48 | print("Min Price updated to {}".format(config['min_price'])) 49 | 50 | if stock_data['current_price'] > config['max_price']: 51 | difference = stock_data['current_price'] - config['max_price'] 52 | difference = round(difference, 2) 53 | message = ':arrow_up_small: __**{}**__ \nCurrent Value: {}\nMax threshold: {}\ndifference: {}'.format( 54 | config['stock_name'], 55 | stock_data['current_price'], 56 | config['max_price'], 57 | difference) 58 | 59 | config['max_price'] = stock_data['current_price'] 60 | #send_message_to_discord(message) 61 | print("Max Price updated to {}".format(config['max_price'])) 62 | 63 | def check_overall(): 64 | symbols = {'BSE SENSEX': 'SENSEX:INDEXBOM', 'NIFTY 50': 'NIFTY_50:INDEXNSE'} 65 | for stock_name, symbol in symbols.items(): 66 | stock_data = fetch_data(symbol) 67 | print('Checking alert for {}'.format(symbol)) 68 | differnce = stock_data['current_price'] - stock_data['previous_close'] 69 | differnce_percentage = (differnce / stock_data['previous_close']) * 100 70 | with open(os.path.join(root_dir, 'data/market.csv'), 'a') as f: 71 | f.write('{},{},{},{},{},{},{},{},{},{}\n'.format( 72 | stock_name, 73 | stock_data['previous_close'], 74 | stock_data['current_price'], 75 | stock_data['day_min'], 76 | stock_data['day_max'], 77 | stock_data['year_min'], 78 | stock_data['year_max'], 79 | stock_data['date_time'], 80 | differnce, 81 | differnce_percentage 82 | )) 83 | 84 | def main(): 85 | configs = read_config() 86 | for config in configs: 87 | check_alert(config) 88 | update_config(configs) 89 | 90 | 91 | def print_time_left(minutes_left, print_time_delay=1): 92 | seconds_left = minutes_left * 60 93 | while seconds_left > 0: 94 | print("Remaining time: {} mins".format(round(seconds_left / 60, 2)), end='\r') 95 | time.sleep(print_time_delay) 96 | seconds_left -= print_time_delay 97 | minutes_left -= print_time_delay / 60 98 | 99 | import datetime 100 | import time 101 | 102 | # Function to print the time left in a countdown 103 | def print_time_left(minutes_left): 104 | for minute in range(int(minutes_left), 0, -1): 105 | print(f'Market will open in {minute} minutes') 106 | time.sleep(60) 107 | 108 | # Main function 109 | def market_monitor(): 110 | IST = datetime.timezone(datetime.timedelta(hours=5, minutes=30)) # Define the IST timezone 111 | market_open_time = datetime.time(9, 0, 0) # Market opens at 09:00:00 112 | market_close_time = datetime.time(15, 30, 0) # Market closes at 15:30:00 113 | 114 | while True: 115 | current_time = datetime.datetime.now(IST).time() 116 | current_day = datetime.datetime.now(IST).strftime('%A') 117 | 118 | if current_day in ['Saturday', 'Sunday']: 119 | check_overall() 120 | main() 121 | print('Market is closed') 122 | print('Market will open on Monday at 09:00 AM') 123 | time.sleep(1200) 124 | continue 125 | 126 | if current_time < market_open_time: 127 | time_until_open = (datetime.datetime.combine(datetime.date.today(), market_open_time) - 128 | datetime.datetime.now(IST)).total_seconds() / 60 129 | print('Market is closed') 130 | print(f'Market will open in {int(time_until_open)} minutes') 131 | print_time_left(time_until_open) 132 | continue 133 | 134 | elif current_time > market_close_time: 135 | print('Market is closed') 136 | print('Market will open tomorrow at 09:00 AM') 137 | time_until_open = (datetime.datetime.combine(datetime.date.today() + datetime.timedelta(days=1), market_open_time) - 138 | datetime.datetime.now(IST)).total_seconds() / 60 139 | print_time_left(time_until_open) 140 | continue 141 | 142 | else: 143 | print('Market is open') 144 | check_overall() 145 | main() 146 | time.sleep(20) # Sleep for 20 seconds before checking again 147 | 148 | if __name__ == '__main__': 149 | market_monitor() 150 | --------------------------------------------------------------------------------