├── Procfile ├── runtime.txt ├── .gitignore ├── README.md ├── requirements.txt ├── assets ├── base.css ├── styles.css └── base2.css └── app.py /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:server -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.8.12 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | *.pyc 3 | .DS_Store 4 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stock-vcp-dash 2 | 3 | This web-based Dash visualization displays the daily US stock market analysis report compiled by my [stock-vcpscreener](https://github.com/jeffreyrdcs/stock-vcpscreener) repo. Currently, the dash app is hosted on https://stock-vcp-dash.herokuapp.com/ and https://jeffreyrdcs.pythonanywhere.com. A scheduled job is run on every weekday at ___ to update the app with the latest report. 4 | 5 | 6 | ## Usage 7 | 8 | To run locally: 9 | ``` 10 | python app.py 11 | ``` 12 | The dash app is then available at http://127.0.0.1:8050 13 | 14 | 15 | ## Visualization 16 | 17 | The Dash app can be accessed at the link below: 18 | 19 | https://stock-vcp-dash.herokuapp.com/ 20 | 21 | https://jeffreyrdcs.pythonanywhere.com 22 | 23 | 24 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Brotli==1.0.9 2 | certifi==2021.5.30 3 | charset-normalizer==2.0.4 4 | click==8.0.1 5 | dash==2.0.0 6 | dash-core-components==2.0.0 7 | dash-html-components==2.0.0 8 | dash-table==5.0.0 9 | dataclasses 10 | DateTime==4.3 11 | Flask==2.0.1 12 | Flask-Compress==1.10.1 13 | future==0.18.2 14 | gunicorn==20.1.0 15 | idna==3.2 16 | importlib-metadata==4.8.1 17 | itsdangerous==2.0.1 18 | Jinja2==3.0.1 19 | lxml==4.6.3 20 | MarkupSafe==2.0.1 21 | multitasking==0.0.9 22 | numpy==1.19.5 23 | pandas==1.3.4 24 | pandas-datareader==0.10.0 25 | plotly==5.3.1 26 | python-dateutil==2.8.2 27 | pytz==2021.1 28 | requests==2.26.0 29 | six==1.16.0 30 | tenacity==8.0.1 31 | typing-extensions==3.10.0.2 32 | urllib3==1.26.6 33 | Werkzeug==2.0.1 34 | yfinance==0.1.63 35 | zipp==3.5.0 36 | zope.interface==5.4.0 37 | -------------------------------------------------------------------------------- /assets/base.css: -------------------------------------------------------------------------------- 1 | /* original style sheet */ 2 | @font-face { 3 | font-family: "FontAwesome"; 4 | src: url("../fonts/fontawesome-webfont.eot?v=4.7.0"); 5 | src: url("../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0") 6 | format("embedded-opentype"), 7 | url("../fonts/fontawesome-webfont.woff2?v=4.7.0") format("woff2"), 8 | url("../fonts/fontawesome-webfont.woff?v=4.7.0") format("woff"), 9 | url("../fonts/fontawesome-webfont.ttf?v=4.7.0") format("truetype"), 10 | url("../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular") 11 | format("svg"); 12 | font-weight: normal; 13 | font-style: normal; 14 | } 15 | 16 | /*.subpage { 17 | padding: 1cm; 18 | border: 5px #ccc solid; 19 | height: 257mm; 20 | outline: 2cm whitesmoke solid; 21 | }*/ 22 | 23 | .gs-header { 24 | color: white !important; 25 | -webkit-print-color-adjust: exact; 26 | } 27 | 28 | .gs-text-header { 29 | background: #c41230 !important; 30 | -webkit-print-color-adjust: exact; 31 | } 32 | 33 | .gs-table-header { 34 | background: #c41230 !important; 35 | -webkit-print-color-adjust: exact; 36 | } 37 | 38 | .gs-accent-header { 39 | background: #65201f !important; 40 | -webkit-print-color-adjust: exact; 41 | } 42 | 43 | div.padded { 44 | padding: 10px; 45 | } 46 | 47 | h6.padded, 48 | p.padded { 49 | padding: 2px 5px; 50 | margin-top: 10px; 51 | margin-bottom: 2px; 52 | } 53 | 54 | table.reversed tr:nth-child(odd) { 55 | background-color: #d7dbe1; 56 | -webkit-print-color-adjust: exact; 57 | } 58 | table.reversed tr:nth-child(even) { 59 | background-color: white; 60 | } 61 | 62 | table { 63 | font-size: 0.95rem; 64 | border-spacing: 0; 65 | border-collapse: collapse; 66 | } 67 | 68 | table.tiny-header tr:first-child { 69 | font-size: 8px; 70 | } 71 | 72 | /* .columns{ margin-left: 0 !important; } */ 73 | 74 | /* .row > .columns:not(:first-child){ 75 | padding-left: 20px; 76 | } */ 77 | 78 | h6 { 79 | font-size: 1.425rem; 80 | } 81 | 82 | h6.tiny-header { 83 | font-size: 10px; 84 | } 85 | 86 | li { 87 | padding-left: 10px; 88 | } 89 | 90 | a { 91 | color: black; 92 | cursor: pointer; 93 | } 94 | 95 | .page-view { 96 | text-align: left; 97 | padding-top: 40px; 98 | } 99 | 100 | .middle-aligned { 101 | padding: 50px 20px 0 0; 102 | text-align: left; 103 | vertical-align: middle; 104 | } 105 | 106 | .right-aligned { 107 | text-align: right; 108 | } 109 | 110 | .no-page { 111 | text-align: center; 112 | margin-top: 100px; 113 | font-size: 22px; 114 | font-weight: bold; 115 | } 116 | 117 | .tab { 118 | border-style: solid; 119 | border-color: rgba(0, 0, 0, 0.2); 120 | border-bottom-style: none; 121 | border-top-style: none; 122 | border-right-style: none; 123 | color: black; 124 | padding: 10px 14px; 125 | text-align: center; 126 | text-decoration: none; 127 | display: inline-block; 128 | } 129 | 130 | .tab.first { 131 | border-left-style: none; 132 | } 133 | 134 | /* .tab:focus { 135 | border-style: solid; 136 | border-color: rgba(0,0,0,1); 137 | border-bottom-style: none; 138 | border-top-style: none; 139 | color: black; 140 | padding: 10px 14px; 141 | text-align: center; 142 | text-decoration: none; 143 | display: inline-block; 144 | } */ 145 | 146 | #page-content > div:nth-child(n + 2) > a { 147 | visibility: hidden; 148 | } 149 | -------------------------------------------------------------------------------- /assets/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | background-color: #fdfdfd; 7 | font-size: 1.1rem; 8 | line-height: 1.25; 9 | } 10 | * { 11 | box-sizing: border-box; 12 | -moz-box-sizing: border-box; 13 | } 14 | .page { 15 | position: relative; 16 | width: 270mm; 17 | min-height: 300mm; 18 | padding: 5mm; 19 | margin: 10mm auto; 20 | border-radius: 5px; 21 | background: white; 22 | box-shadow: 0 0 10px lightgrey; 23 | } 24 | 25 | .pagetwo { 26 | position: relative; 27 | width: 210mm; 28 | min-height: 240mm; 29 | padding: 0mm; 30 | margin: 10mm auto; 31 | border: 1px #d3d3d3 solid; 32 | border-radius: 5px; 33 | background: white; 34 | } 35 | 36 | .sub_page { 37 | padding: 1cm; 38 | min-height: 300mm; 39 | } 40 | 41 | @page { 42 | size: A4; 43 | margin: 0; 44 | } 45 | @media print { 46 | html, 47 | body { 48 | width: 210mm; 49 | height: 297mm; 50 | } 51 | .page { 52 | margin: 0; 53 | border: initial; 54 | border-radius: initial; 55 | width: initial; 56 | min-height: initial; 57 | box-shadow: initial; 58 | background: initial; 59 | page-break-after: always; 60 | } 61 | .no-print, 62 | .no-print * { 63 | display: none !important; 64 | } 65 | } 66 | 67 | p { 68 | margin-bottom: 1rem; 69 | } 70 | 71 | .iframe { 72 | border: none; 73 | } 74 | 75 | h6.padded, 76 | p.padded { 77 | padding: 2px 5px; 78 | margin-top: 10px; 79 | margin-bottom: 10px; 80 | } 81 | 82 | .blue-text { 83 | color: #8496b8; 84 | } 85 | 86 | .table-wrapper { 87 | position:relative; 88 | 89 | } 90 | 91 | .table-scroll { 92 | width: 970px; 93 | height: 283px; 94 | overflow: auto; 95 | margin-top: 10px; 96 | } 97 | 98 | table { 99 | width: 100%; 100 | } 101 | 102 | /*td { 103 | padding: 20px 1px; 104 | }*/ 105 | 106 | tr:nth-child(even) { 107 | background-color: #f0f0f0; 108 | } 109 | tr:nth-child(odd) { 110 | background-color: #fafafa; 111 | } 112 | 113 | table.reversed tr:nth-child(odd) { 114 | background-color: #f0f0f0; 115 | } 116 | table.reversed tr:nth-child(even) { 117 | background-color: #f0f0f0; 118 | } 119 | 120 | table { 121 | font-size: 1.1rem; 122 | border-spacing: 0; 123 | border-collapse: collapse; 124 | } 125 | td, 126 | th { 127 | border: 0px solid #ddd; 128 | padding: 6px 3px; 129 | } 130 | 131 | .trfixsize { 132 | width: 945px; 133 | /* width: 100%;*/ 134 | 135 | } 136 | 137 | .tdfixsize { 138 | width: 80px; 139 | } 140 | 141 | .tdcol2 { 142 | width: 30%; 143 | } 144 | 145 | .tdchangepos { 146 | color: #008500; 147 | width: 70px; 148 | } 149 | 150 | .tdchangeneg { 151 | color: #DD390A; 152 | width: 70px; 153 | } 154 | 155 | table.tiny-header tr:first-child { 156 | font-size: 8px; 157 | } 158 | 159 | .columns { 160 | margin-left: 0 !important; 161 | } 162 | 163 | .row > .columns:not(:first-child) { 164 | padding-left: 0px; 165 | } 166 | 167 | h1, 168 | h5, 169 | h6 { 170 | margin: 0; 171 | } 172 | h5 { 173 | font-size: 2.6rem; 174 | } 175 | h6 { 176 | font-size: 1.5rem; 177 | } 178 | 179 | .risk-reward { 180 | height: 150px; 181 | width: auto; 182 | } 183 | 184 | li { 185 | color: #7a7a7a; 186 | list-style: square; 187 | padding-left: 0; 188 | } 189 | 190 | .s-summary { 191 | position: relative; 192 | height: 15%; 193 | padding: 20px; 194 | border-radius: 1px; 195 | background: #008500; 196 | color: #ffffff; 197 | } 198 | 199 | .all-tabs { 200 | text-align: center; 201 | font-size: 13px; 202 | margin-top: 10px; 203 | } 204 | .main-title { 205 | position: relative; 206 | color: #3a3a3a; 207 | font-size: 40px !important; 208 | width: 80%; 209 | border-left: 20px solid #008500; 210 | background: #ffffff; 211 | padding-left: 30px; 212 | } 213 | 214 | .headtitle { 215 | position: relative; 216 | color: #3a3a3a; 217 | left: 0; 218 | font-size: 18px; 219 | height: 20%; 220 | width: 50%; 221 | margin: 0mm; 222 | border-left: 15px groove #009900; 223 | background: #ffffff; 224 | padding-top: 5px; 225 | padding-bottom: 5px; 226 | /* padding-left: 50px; 227 | padding-right: 10px;*/ 228 | } 229 | 230 | .subtitle { 231 | position: relative; 232 | color: #3a3a3a; 233 | left: 0; 234 | height: 20%; 235 | width: 80%; 236 | margin: 0mm; 237 | border-left: 15px groove #009900; 238 | background: #ffffff; 239 | padding-top: 1px; 240 | padding-bottom: 1px; 241 | padding-left: 50px; 242 | padding-right: 10px; 243 | } 244 | 245 | .logo { 246 | height: 30px; 247 | width: auto; 248 | margin: 25px 25px; 249 | } 250 | 251 | a:link { 252 | color: DarkSlateBlue; 253 | text-decoration: none; 254 | } 255 | 256 | /* visited link */ 257 | a:visited { 258 | color: #696969; 259 | text-decoration: none; 260 | } 261 | 262 | /* mouse over link */ 263 | a:hover { 264 | opacity: 0.6; 265 | } 266 | 267 | /* selected link */ 268 | a:active { 269 | color: lightgrey; 270 | text-decoration: underline; 271 | } 272 | 273 | .greyline { 274 | width: 90%; 275 | border-bottom: 1px solid lightgrey; 276 | } 277 | 278 | .tab { 279 | border-style: solid; 280 | border-color: rgb(0, 0, 0, 0.2); 281 | border-bottom-style: none; 282 | border-top-style: none; 283 | border-right-style: none; 284 | padding: 5px 10px; 285 | } 286 | 287 | .rowrow { 288 | margin: auto; 289 | text-align: center; 290 | width: 97%; 291 | } 292 | 293 | .rowrow2 { 294 | margin: auto; 295 | width: 97%; 296 | } 297 | 298 | .tablast { 299 | border-style: solid; 300 | border-color: rgb(0, 0, 0, 0.2); 301 | border-bottom-style: none; 302 | border-top-style: none; 303 | color: black; 304 | padding: 6px 20px; 305 | text-align: center; 306 | text-decoration: none; 307 | display: inline-block; 308 | } 309 | 310 | /* for screens smaller than 768px */ 311 | @media only screen and (max-width: 550px) { 312 | .tab { 313 | display: block; 314 | } 315 | .tablast { 316 | display: block; 317 | border-right-style: none; 318 | } 319 | .page { 320 | width: auto; 321 | } 322 | .sub_page { 323 | width: auto; 324 | } 325 | .risk-reward { 326 | height: 120px; 327 | width: auto; 328 | } 329 | .logo { 330 | height: 20px; 331 | width: auto; 332 | } 333 | h5 { 334 | font-size: 1.5rem; 335 | } 336 | .main-title { 337 | width: 55%; 338 | padding-left: 15px; 339 | border-left: 15px solid #98151b; 340 | } 341 | .five.columns { 342 | width: 45%; 343 | } 344 | .six.columns { 345 | width: 90%; 346 | } 347 | .fees { 348 | width: auto; 349 | } 350 | .full-view-link { 351 | font-size: small; 352 | padding: 10px 10px; 353 | width: 40%; 354 | margin-left: 60%; 355 | } 356 | .row > .columns:not(:first-child) { 357 | padding: 0 0; 358 | } 359 | .svg-container { 360 | width: 255px !important; 361 | } 362 | .main-svg { 363 | width: 255px !important; 364 | } 365 | .three.columns { 366 | text-align: left; 367 | padding: 0 10px; 368 | margin-bottom: 5px; 369 | } 370 | .nine.columns { 371 | padding: 0 20px !important; 372 | } 373 | } 374 | @media only screen and (max-width: 400px) { 375 | .full-view-link { 376 | width: 45%; 377 | margin-left: 55%; 378 | } 379 | } 380 | 381 | @media only screen and (max-width: 350px) { 382 | .full-view-link { 383 | width: 50%; 384 | margin-left: 50%; 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /assets/base2.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V2.0.4 3 | * Copyright 2014, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 12/29/2014 8 | */ 9 | 10 | /* Table of contents 11 | - Grid 12 | - Base Styles 13 | - Typography 14 | - Links 15 | - Buttons 16 | - Forms 17 | - Lists 18 | - Code 19 | - Tables 20 | - Spacing 21 | - Utilities 22 | - Clearing 23 | - Media Queries 24 | */ 25 | 26 | /* Grid */ 27 | .container { 28 | position: relative; 29 | width: 100%; 30 | max-width: 960px; 31 | margin: 0 auto; 32 | padding: 0 20px; 33 | box-sizing: border-box; 34 | } 35 | .column, 36 | .columns { 37 | width: 100%; 38 | float: left; 39 | box-sizing: border-box; 40 | } 41 | 42 | /* For devices larger than 400px */ 43 | @media (min-width: 400px) { 44 | .container { 45 | width: 85%; 46 | padding: 0; 47 | } 48 | } 49 | 50 | /* For devices larger than 550px */ 51 | @media (min-width: 550px) { 52 | .container { 53 | width: 80%; 54 | } 55 | .column, 56 | .columns { 57 | margin-left: 4%; 58 | } 59 | .column:first-child, 60 | .columns:first-child { 61 | margin-left: 0; 62 | } 63 | 64 | .one.column, 65 | .one.columns { 66 | width: 4.66666666667%; 67 | } 68 | .two.columns { 69 | width: 13.3333333333%; 70 | } 71 | .three.columns { 72 | width: 22%; 73 | } 74 | .four.columns { 75 | width: 30.6666666667%; 76 | } 77 | .five.columns { 78 | width: 39.3333333333%; 79 | } 80 | .six.columns { 81 | width: 48%; 82 | } 83 | .seven.columns { 84 | width: 56.6666666667%; 85 | } 86 | .eight.columns { 87 | width: 65.3333333333%; 88 | } 89 | .nine.columns { 90 | width: 74%; 91 | } 92 | .ten.columns { 93 | width: 82.6666666667%; 94 | } 95 | .eleven.columns { 96 | width: 91.3333333333%; 97 | } 98 | .twelve.columns { 99 | width: 100%; 100 | margin-left: 0; 101 | } 102 | 103 | .one-third.column { 104 | width: 30.6666666667%; 105 | } 106 | .two-thirds.column { 107 | width: 65.3333333333%; 108 | } 109 | 110 | .one-half.column { 111 | width: 48%; 112 | } 113 | 114 | /* Offsets */ 115 | .offset-by-one.column, 116 | .offset-by-one.columns { 117 | margin-left: 8.66666666667%; 118 | } 119 | .offset-by-two.column, 120 | .offset-by-two.columns { 121 | margin-left: 17.3333333333%; 122 | } 123 | .offset-by-three.column, 124 | .offset-by-three.columns { 125 | margin-left: 26%; 126 | } 127 | .offset-by-four.column, 128 | .offset-by-four.columns { 129 | margin-left: 34.6666666667%; 130 | } 131 | .offset-by-five.column, 132 | .offset-by-five.columns { 133 | margin-left: 43.3333333333%; 134 | } 135 | .offset-by-six.column, 136 | .offset-by-six.columns { 137 | margin-left: 52%; 138 | } 139 | .offset-by-seven.column, 140 | .offset-by-seven.columns { 141 | margin-left: 60.6666666667%; 142 | } 143 | .offset-by-eight.column, 144 | .offset-by-eight.columns { 145 | margin-left: 69.3333333333%; 146 | } 147 | .offset-by-nine.column, 148 | .offset-by-nine.columns { 149 | margin-left: 78%; 150 | } 151 | .offset-by-ten.column, 152 | .offset-by-ten.columns { 153 | margin-left: 86.6666666667%; 154 | } 155 | .offset-by-eleven.column, 156 | .offset-by-eleven.columns { 157 | margin-left: 95.3333333333%; 158 | } 159 | 160 | .offset-by-one-third.column, 161 | .offset-by-one-third.columns { 162 | margin-left: 34.6666666667%; 163 | } 164 | .offset-by-two-thirds.column, 165 | .offset-by-two-thirds.columns { 166 | margin-left: 69.3333333333%; 167 | } 168 | 169 | .offset-by-one-half.column, 170 | .offset-by-one-half.columns { 171 | margin-left: 52%; 172 | } 173 | } 174 | 175 | /* NOTE 176 | html is set to 62.5% so that all the REM measurements throughout Skeleton 177 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 178 | html { 179 | font-size: 62.5%; 180 | } 181 | body { 182 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 183 | line-height: 1.6; 184 | font-weight: 400; 185 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, 186 | sans-serif; 187 | color: #222; 188 | } 189 | 190 | /* Typography */ 191 | h1, 192 | h2, 193 | h3, 194 | h4, 195 | h5, 196 | h6 { 197 | margin-top: 0; 198 | margin-bottom: 2rem; 199 | font-weight: 300; 200 | } 201 | h1 { 202 | font-size: 4rem; 203 | line-height: 1.2; 204 | letter-spacing: -0.1rem; 205 | } 206 | h2 { 207 | font-size: 3.6rem; 208 | line-height: 1.25; 209 | letter-spacing: -0.1rem; 210 | } 211 | h3 { 212 | font-size: 3rem; 213 | line-height: 1.3; 214 | letter-spacing: -0.1rem; 215 | } 216 | h4 { 217 | font-size: 2.4rem; 218 | line-height: 1.35; 219 | letter-spacing: -0.08rem; 220 | } 221 | h5 { 222 | font-size: 1.8rem; 223 | line-height: 1.5; 224 | letter-spacing: -0.05rem; 225 | } 226 | h6 { 227 | font-size: 1.5rem; 228 | line-height: 1.6; 229 | letter-spacing: 0; 230 | } 231 | 232 | /* Larger than phablet */ 233 | @media (min-width: 550px) { 234 | h1 { 235 | font-size: 5rem; 236 | } 237 | h2 { 238 | font-size: 4.2rem; 239 | } 240 | h3 { 241 | font-size: 3.6rem; 242 | } 243 | h4 { 244 | font-size: 3rem; 245 | } 246 | h5 { 247 | font-size: 2.4rem; 248 | } 249 | h6 { 250 | font-size: 1.5rem; 251 | } 252 | } 253 | 254 | p { 255 | margin-top: 0; 256 | } 257 | 258 | /* Links */ 259 | a { 260 | color: #1eaedb; 261 | } 262 | a:hover { 263 | color: #0fa0ce; 264 | } 265 | 266 | /* Buttons */ 267 | .button, 268 | button, 269 | input[type="submit"], 270 | input[type="reset"], 271 | input[type="button"] { 272 | display: inline-block; 273 | height: 38px; 274 | padding: 0 30px; 275 | color: #555; 276 | text-align: center; 277 | font-size: 11px; 278 | font-weight: 600; 279 | line-height: 38px; 280 | letter-spacing: 0.1rem; 281 | text-transform: uppercase; 282 | text-decoration: none; 283 | white-space: nowrap; 284 | background-color: transparent; 285 | border-radius: 4px; 286 | border: 1px solid #bbb; 287 | cursor: pointer; 288 | box-sizing: border-box; 289 | } 290 | .button:hover, 291 | button:hover, 292 | input[type="submit"]:hover, 293 | input[type="reset"]:hover, 294 | input[type="button"]:hover, 295 | .button:focus, 296 | button:focus, 297 | input[type="submit"]:focus, 298 | input[type="reset"]:focus, 299 | input[type="button"]:focus { 300 | color: #333; 301 | border-color: #888; 302 | outline: 0; 303 | } 304 | .button.button-primary, 305 | button.button-primary, 306 | input[type="submit"].button-primary, 307 | input[type="reset"].button-primary, 308 | input[type="button"].button-primary { 309 | color: #fff; 310 | background-color: #33c3f0; 311 | border-color: #33c3f0; 312 | } 313 | .button.button-primary:hover, 314 | button.button-primary:hover, 315 | input[type="submit"].button-primary:hover, 316 | input[type="reset"].button-primary:hover, 317 | input[type="button"].button-primary:hover, 318 | .button.button-primary:focus, 319 | button.button-primary:focus, 320 | input[type="submit"].button-primary:focus, 321 | input[type="reset"].button-primary:focus, 322 | input[type="button"].button-primary:focus { 323 | color: #fff; 324 | background-color: #1eaedb; 325 | border-color: #1eaedb; 326 | } 327 | 328 | /* Forms */ 329 | input[type="email"], 330 | input[type="number"], 331 | input[type="search"], 332 | input[type="text"], 333 | input[type="tel"], 334 | input[type="url"], 335 | input[type="password"], 336 | textarea, 337 | select { 338 | height: 38px; 339 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 340 | background-color: #fff; 341 | border: 1px solid #d1d1d1; 342 | border-radius: 4px; 343 | box-shadow: none; 344 | box-sizing: border-box; 345 | } 346 | /* Removes awkward default styles on some inputs for iOS */ 347 | input[type="email"], 348 | input[type="number"], 349 | input[type="search"], 350 | input[type="text"], 351 | input[type="tel"], 352 | input[type="url"], 353 | input[type="password"], 354 | textarea { 355 | -webkit-appearance: none; 356 | -moz-appearance: none; 357 | appearance: none; 358 | } 359 | textarea { 360 | min-height: 65px; 361 | padding-top: 6px; 362 | padding-bottom: 6px; 363 | } 364 | input[type="email"]:focus, 365 | input[type="number"]:focus, 366 | input[type="search"]:focus, 367 | input[type="text"]:focus, 368 | input[type="tel"]:focus, 369 | input[type="url"]:focus, 370 | input[type="password"]:focus, 371 | textarea:focus, 372 | select:focus { 373 | border: 1px solid #33c3f0; 374 | outline: 0; 375 | } 376 | label, 377 | legend { 378 | display: block; 379 | margin-bottom: 0.5rem; 380 | font-weight: 600; 381 | } 382 | fieldset { 383 | padding: 0; 384 | border-width: 0; 385 | } 386 | input[type="checkbox"], 387 | input[type="radio"] { 388 | display: inline; 389 | } 390 | label > .label-body { 391 | display: inline-block; 392 | margin-left: 0.5rem; 393 | font-weight: normal; 394 | } 395 | 396 | /* Lists */ 397 | 398 | ul { 399 | list-style: circle inside; 400 | } 401 | ol { 402 | list-style: decimal inside; 403 | } 404 | ol, 405 | ul { 406 | padding-left: 0; 407 | margin-top: 0; 408 | } 409 | ul ul, 410 | ul ol, 411 | ol ol, 412 | ol ul { 413 | margin: 1.5rem 0 1.5rem 3rem; 414 | font-size: 90%; 415 | } 416 | li { 417 | margin-bottom: 1rem; 418 | } 419 | 420 | /* Code */ 421 | 422 | code { 423 | padding: 0.2rem 0.5rem; 424 | margin: 0 0.2rem; 425 | font-size: 90%; 426 | white-space: nowrap; 427 | background: #f1f1f1; 428 | border: 1px solid #e1e1e1; 429 | border-radius: 4px; 430 | } 431 | pre > code { 432 | display: block; 433 | padding: 1rem 1.5rem; 434 | white-space: pre; 435 | } 436 | 437 | /* Tables */ 438 | /* 439 | th, 440 | td { 441 | padding: 12px 15px; 442 | text-align: left; 443 | border-bottom: 1px solid #e1e1e1; 444 | } 445 | th:first-child, 446 | td:first-child { 447 | padding-left: 0; 448 | } 449 | th:last-child, 450 | td:last-child { 451 | padding-right: 0; 452 | }*/ 453 | 454 | /* Spacing */ 455 | 456 | button, 457 | .button { 458 | margin-bottom: 1rem; 459 | } 460 | input, 461 | textarea, 462 | select, 463 | fieldset { 464 | margin-bottom: 1.5rem; 465 | } 466 | pre, 467 | blockquote, 468 | dl, 469 | figure, 470 | table, 471 | p, 472 | ul, 473 | ol, 474 | form { 475 | margin-bottom: 2.5rem; 476 | } 477 | 478 | /* Utilities */ 479 | 480 | .u-full-width { 481 | width: 100%; 482 | box-sizing: border-box; 483 | } 484 | .u-max-full-width { 485 | max-width: 100%; 486 | box-sizing: border-box; 487 | } 488 | .u-pull-right { 489 | float: right; 490 | } 491 | .u-pull-left { 492 | float: left; 493 | } 494 | hr { 495 | margin-top: 3rem; 496 | margin-bottom: 3.5rem; 497 | border-width: 0; 498 | border-top: 1px solid #e1e1e1; 499 | } 500 | 501 | /* Self Clearing Goodness */ 502 | .container:after, 503 | .row:after, 504 | .u-cf { 505 | content: ""; 506 | display: table; 507 | clear: both; 508 | } 509 | 510 | /* 511 | Note: The best way to structure the use of media queries is to create the queries 512 | near the relevant code. For example, if you wanted to change the styles for buttons 513 | on small devices, paste the mobile query code up in the buttons section and style it 514 | there. 515 | */ 516 | 517 | /* Larger than mobile */ 518 | @media (min-width: 400px) { 519 | } 520 | 521 | /* Larger than phablet (also point when grid becomes active) */ 522 | @media (min-width: 550px) { 523 | } 524 | 525 | /* Larger than tablet */ 526 | @media (min-width: 750px) { 527 | } 528 | 529 | /* Larger than desktop */ 530 | @media (min-width: 1000px) { 531 | } 532 | 533 | /* Larger than Desktop HD */ 534 | @media (min-width: 1200px) { 535 | } 536 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | from datetime import datetime, timedelta 4 | 5 | import pandas as pd 6 | import numpy as np 7 | import plotly.express as px 8 | import plotly.graph_objects as go 9 | from pandas_datareader import data as pdr 10 | 11 | import dash 12 | from dash import dcc 13 | from dash import html 14 | from dash.dependencies import Input, Output 15 | 16 | import yfinance as yf 17 | 18 | _DEFAULT_URL_PATH_NAME = "https://uk.finance.yahoo.com/quote/" 19 | _STOCK_INFO_URL_PATH_NAME = 'https://raw.githubusercontent.com/jeffreyrdcs/stock-vcpscreener/main/' 20 | _MAX_NUM_OF_STOCK_TO_DISPLAY = 50 21 | _NUM_OF_STOCK_CHART_TO_DISPLAY = 4 22 | 23 | 24 | app = dash.Dash(__name__, meta_tags=[{"name": "viewport", "content": "width=device-width"}]) 25 | app.title = "Stock Analysis Report" 26 | 27 | server = app.server 28 | 29 | 30 | def make_performance_table(df): 31 | """ 32 | Return a dash definition of an HTML table for a dataframe. For daily performance. 33 | # [html.Tr([html.Td(4),html.Td(2)]), html.Tr([html.Td(5),html.Td(1),html.Td(1)]) ] 34 | """ 35 | table = [] 36 | 37 | for col in df.columns: 38 | if "Tickers" in col: 39 | continue 40 | elif col[0:5] == "Gauge": 41 | table.append(html.Tr([html.Td(col), html.Td(f"{df[col].values[0]:.2f}", className="tdcol2")])) 42 | elif col[0:5] == "Stock": 43 | table.append(html.Tr([html.Td(col), html.Td(f"{df[col].values[0]:.2f}")])) 44 | elif col == "AD Percent" or col[0:5] == "Perce": 45 | table.append(html.Tr([html.Td(col), html.Td(f"{df[col].values[0]:.2f}%")])) 46 | else: 47 | table.append(html.Tr([html.Td(col), html.Td(f"{df[col].values[0]}")])) 48 | 49 | return html.Table(table) 50 | 51 | 52 | def make_stock_info_table(df): 53 | """Return a dash definition of an HTML table for a dataframe. For selected stock info. Only return the top 50.""" 54 | selected_stock_df = df.loc[0:_MAX_NUM_OF_STOCK_TO_DISPLAY - 1] 55 | 56 | table = [] 57 | 58 | header = [html.Td("Rank")] 59 | for col in selected_stock_df.columns: 60 | header.append(html.Td(col)) 61 | table.append(html.Tr(header)) 62 | 63 | for ind, col in selected_stock_df.iterrows(): 64 | tmprow = [] 65 | 66 | for key, item in zip(col.keys(), col): 67 | if key == "Ticker": 68 | tmprow.append(html.Td(html.A(f'{item}', target="_blank", href=_DEFAULT_URL_PATH_NAME + item))) 69 | elif key == "Volume": 70 | tmprow.append(html.Td(f'{item:.0f}')) 71 | elif key == "RS Rank": 72 | tmprow.append(html.Td(f'{item:.4f}')) 73 | elif key[0:2] == "52": 74 | tmprow.append(html.Td(f'{item:.2f}', className="tdfixsize")) 75 | elif key[0:6] == "Change" and item >= 0: 76 | tmprow.append(html.Td(f'+{item:.2f}', className="tdchangepos")) 77 | elif key[0:6] == "Change" and item < 0: 78 | tmprow.append(html.Td(f'{item:.2f}', className="tdchangeneg")) 79 | else: 80 | tmprow.append(html.Td(f'{item:.2f}')) 81 | 82 | rank = str(ind + 1) 83 | tmprow.insert(0, html.Td(rank)) 84 | table.append(html.Tr(tmprow)) 85 | 86 | return html.Table(table, className="trfixsize") 87 | 88 | 89 | def _convert_str_list_column_to_float(in_str): 90 | """Convert the string column back into an array of float. Could use np.fromstring(mmmm.strip("[]"), sep=',').""" 91 | in_str = re.sub("[\[\]']", '', in_str) 92 | 93 | if not in_str: 94 | return np.nan 95 | 96 | return np.array(in_str.split(',')).astype(float) 97 | 98 | 99 | def _convert_str_list_column_to_str(in_str): 100 | """Convert the string column back into an array of str. Could use literal_eval""" 101 | in_str = re.sub("[\[\]']", '', in_str) 102 | return np.array(in_str.split(',')) 103 | 104 | 105 | def _get_dropdown_list_from_date_index(in_df): 106 | """Convert the date index of the input dataframe into a dict for the dropdown list.""" 107 | df_index = in_df.index.to_numpy() 108 | return [{'label': str(date_index), 'value': str(date_index)} for date_index in df_index] 109 | 110 | 111 | def _get_dropdown_list_from_ticker(in_df): 112 | """Convert the ticker column of the input dataframe into a dict for the dropdown list.""" 113 | df_ticker = in_df['Ticker'].to_numpy() 114 | return [{'label': str(ind + 1) + '. ' + str(ticker), 'value': str(ticker)} for ind, ticker in enumerate(df_ticker)] 115 | 116 | 117 | def get_ohlc_data(in_ticker): 118 | """Fetch OHLC data from csv, format the DataFrame and compute SMA.""" 119 | db_dir_add = '../stock_vcpscreener/db_yfinance/' 120 | db_filename = in_ticker.strip().ljust(5, '_') + '.csv' 121 | ticker_data_df = pd.read_csv(db_dir_add + db_filename) 122 | ticker_data_df["Date"] = pd.to_datetime(ticker_data_df["Date"]) 123 | ticker_data_df = ticker_data_df.set_index("Date") 124 | 125 | ticker_data_df["SMA_20"] = ticker_data_df["Adj Close"].rolling(window=20).mean() 126 | ticker_data_df["SMA_50"] = ticker_data_df["Adj Close"].rolling(window=50).mean() 127 | ticker_data_df["SMA_200"] = ticker_data_df["Adj Close"].rolling(window=200).mean() 128 | 129 | return ticker_data_df 130 | 131 | 132 | def get_ohlc_data_web(in_ticker): 133 | """Fetch OHLC data from yahoo finance, format the DataFrame and compute SMA.""" 134 | yf.pdr_override() 135 | curr_day = datetime.utcnow() - timedelta(hours=5) # UTC -5, i.e. set to US NY timezone 136 | 137 | # Fetch a year's worth of data 138 | ticker_data_df = pdr.get_data_yahoo(in_ticker.strip(), 139 | start=curr_day.date() - timedelta(days=365), 140 | end=curr_day.date(), 141 | threads=False) 142 | 143 | ticker_data_df["SMA_20"] = ticker_data_df["Adj Close"].rolling(window=20).mean() 144 | ticker_data_df["SMA_50"] = ticker_data_df["Adj Close"].rolling(window=50).mean() 145 | ticker_data_df["SMA_200"] = ticker_data_df["Adj Close"].rolling(window=200).mean() 146 | 147 | return ticker_data_df 148 | 149 | 150 | def serve_layout(): 151 | # Read the daily stock data 152 | daily_stock_file = f"{_STOCK_INFO_URL_PATH_NAME}daily_selected_stock_info.csv" 153 | global df 154 | df = pd.read_csv(daily_stock_file) 155 | df = df.set_index('Date') 156 | 157 | # Convert the string column into an object column 158 | df['Breadth Percentage'] = df['Breadth Percentage'].apply(_convert_str_list_column_to_float) 159 | df['Tickers that fit the conditions'] = df['Tickers that fit the conditions'].apply(_convert_str_list_column_to_str) 160 | df['RS rating of Tickers'] = df['RS rating of Tickers'].apply(_convert_str_list_column_to_float) 161 | df['RS rank of Tickers'] = df['RS rank of Tickers'].apply(_convert_str_list_column_to_float) 162 | 163 | # Read the corresponding info dataset of the most recent day 164 | selected_info_file = f'{_STOCK_INFO_URL_PATH_NAME}output/selected_stock_{df.index[-1]}.csv' 165 | df_info = pd.read_csv(selected_info_file) 166 | df_info = df_info.drop(df_info.columns[0], axis=1) 167 | 168 | # Reorder the columns, move the location of the change columns 169 | try: 170 | df_info = df_info[ 171 | ['Ticker', 'Open', 'High', 'Low', 'Close', 'Adj Close', 'Change', 'Change (%)', 'Volume', '52 Week Min', 172 | '52 Week Max', 'RS Rating', 'RS Rank'] 173 | ] 174 | except KeyError as e: 175 | print(f"Some columns in the {selected_info_file} file are not as expected. Please check the file. KeyError: {e}") 176 | 177 | # Get the list for drop list 178 | out_date_list = _get_dropdown_list_from_date_index(df) 179 | 180 | # Get the stock list of the most recent day for drop list 181 | out_stock_list = _get_dropdown_list_from_ticker(df_info) 182 | 183 | # Make a display copy 184 | global df_dis 185 | df_dis = pd.DataFrame([], index=df.index) 186 | df_dis['Number of stocks monitored'] = df['Number of stock'] 187 | df_dis['Advanced / Declined stock'] = df['Advanced (Day)'].astype(str) + " / " + df['Declined (Day)'].astype(str) 188 | df_dis['AD Percent'] = (df['Advanced (Day)'] - df['Declined (Day)']) / ( 189 | df['Advanced (Day)'] + df['Declined (Day)']) * 100 190 | df_dis['New 52W high / New 52W low'] = df['New High'].astype(str) + " / " + df['New Low'].astype(str) 191 | df_dis['Gauge (Billion $)'] = df['Gauge'] / 1e9 192 | df_dis['Percentage of stocks above its 20 Day SMA (SMA 20)'] = df['Stock above 20-DMA'] 193 | df_dis['Percentage of stocks above its 50 Day SMA (SMA 50)'] = df['Stock above 50-DMA'] 194 | df_dis['Percentage of stocks with SMA 20 > SMA 50'] = df['Stock with 20-DMA > 50-DMA'] 195 | # df_dis['Percentage of stocks with 50 Day SMA > 200 Day SMA'] = df['Stock with 50-DMA > 200-DMA'] 196 | df_dis['Percentage of stocks with SMA 50 > SMA 150 > SMA 200'] = df['Stock with 50 > 150 > 200-DMA'] 197 | df_dis['Percentage of stocks trending 200 Day SMA'] = df['Stock with 200-DMA is rising'] 198 | df_dis['Number of stocks that fit the criteria'] = df['Number of Stock that fit condition'] 199 | df_dis['Percentage of stocks that fit the criteria'] = df['Number of Stock that fit condition(%)'] 200 | 201 | up_layout = html.Div([ 202 | # html.H4("Dashboard with Dash", style={'text-align': 'center'}, className="padded"), 203 | dcc.Dropdown(id="check_date", 204 | options=out_date_list, 205 | multi=False, 206 | value=out_date_list[-1]["value"], 207 | style={"width": "50%", "float": "right"} 208 | ), 209 | html.Div(id="output_container", children=[], style={"margin-bottom": "20px"}, className="headtitle padded"), 210 | html.Div( 211 | [ 212 | html.H6("What are we showing here?"), 213 | html.Br([]), 214 | dcc.Markdown(''' 215 | The top stocks in the US market are selected based on multiple criteria applied to \ 216 | the simple moving averages and price performance over the last year. \ 217 | This report is generated based on the output of a custom US stock screener package 'stock_vcpscreener'. \ 218 | The source code of the stock screener package and this dashboard can be found \ 219 | [here](https://github.com/jeffreyrdcs/stock-vcpscreener) at my github. \ 220 | The screener calculates various market breadth indicators and selects stocks on a daily basis based on \ 221 | the criteria. To rank the selected stocks, a rating score is computed using past performances, similar \ 222 | to the IBD RS rating. The rating and rank of the stock can be found in the summary table.''', 223 | style={"color": "#ffffff"}, 224 | className="row", 225 | ), 226 | ], 227 | className="s-summary", 228 | style={"margin-bottom": "15px"}, 229 | ), 230 | 231 | # Row 1 232 | html.Div( 233 | [ 234 | html.Div( 235 | [ 236 | html.H6("Stock Ratings (Top 50 that fit the criteria)", className="subtitle padded"), 237 | dcc.Graph(id="stock_bar", 238 | figure={}, 239 | config={"displayModeBar": False, "responsive": True}) 240 | ], 241 | className="twelve columns", 242 | ), 243 | ], 244 | className="row", 245 | style={"margin-bottom": "5px"}, 246 | ), 247 | 248 | # Row 2 249 | html.Div( 250 | [ 251 | html.Div( 252 | [ 253 | html.H6( 254 | "Daily Market Breadth", 255 | className="subtitle padded" 256 | ), 257 | dcc.Graph(id='breadth_hist', figure={}, config={"displayModeBar": False}) 258 | ], 259 | className="six columns", 260 | ), 261 | html.Div( 262 | [ 263 | html.H6( 264 | "Daily Market Performance", 265 | className="subtitle padded", 266 | ), 267 | html.Div( 268 | make_performance_table(df_dis[df.index == df.index[-1]]), 269 | id="daily_report", 270 | ), 271 | ], 272 | className="six columns", 273 | ), 274 | ], 275 | className="row", 276 | style={"margin-bottom": "5px"}, 277 | ), 278 | 279 | # Row 3 280 | html.Div( 281 | [ 282 | html.Div( 283 | [ 284 | html.H6( 285 | "Stock Summary (Top 50 that fit the criteria)", 286 | className="subtitle padded" 287 | ), 288 | html.Div( 289 | [ 290 | html.Div( 291 | [ 292 | html.Div( 293 | make_stock_info_table(df_info), 294 | id='stock_report' 295 | ), 296 | ], className="table-scroll" 297 | ), 298 | ], className="table-wrapper"), 299 | ], 300 | className="twelve columns", 301 | ), 302 | ], 303 | className="row", 304 | style={"margin-bottom": "15px"}, 305 | ), 306 | 307 | # Row 4 308 | html.Div( 309 | _get_chart_divs(out_stock_list), 310 | className="row", 311 | style={"margin-bottom": "15px"}, 312 | ), 313 | ], className="page") 314 | 315 | return up_layout 316 | 317 | 318 | def _get_chart_divs(stock_list): 319 | """Return a list of Divs depend on the number of items in the stock list.""" 320 | num_of_selected_stocks = len(stock_list) 321 | 322 | chart_divs = [ 323 | html.Div( 324 | [ 325 | html.H6( 326 | "Charts", 327 | className="subtitle padded" 328 | ), 329 | ], 330 | className="twelve columns", 331 | ) 332 | ] 333 | 334 | for i in range(1, _NUM_OF_STOCK_CHART_TO_DISPLAY + 1): 335 | if i > num_of_selected_stocks - 1: 336 | chart_divs.append( 337 | html.Div( 338 | _get_dropdown_list_and_chart(f"check_stock{i}", stock_list, stock_list[0]["value"], 339 | f"stock_chart{i}"), 340 | className="six columns", 341 | ), 342 | ) 343 | else: 344 | chart_divs.append( 345 | html.Div( 346 | _get_dropdown_list_and_chart(f"check_stock{i}", stock_list, stock_list[i]["value"], 347 | f"stock_chart{i}"), 348 | className="six columns", 349 | ), 350 | ) 351 | 352 | return chart_divs 353 | 354 | 355 | def _get_dropdown_list_and_chart(dropdown_id, dropdown_options, dropdown_value, graph_id): 356 | return [ 357 | dcc.Dropdown( 358 | id=dropdown_id, 359 | options=dropdown_options, 360 | multi=False, 361 | value=dropdown_value, 362 | style={"width": "50%"}, 363 | ), 364 | dcc.Graph( 365 | id=graph_id, 366 | figure={}, 367 | config={"displayModeBar": False}), 368 | ] 369 | 370 | 371 | # ------------------------------------------------------------------------------ 372 | # Page layout 373 | app.layout = serve_layout 374 | 375 | # ------------------------------------------------------------------------------ 376 | # Call back functions 377 | @app.callback( 378 | [Output(component_id='output_container', component_property='children'), 379 | Output(component_id='breadth_hist', component_property='figure'), 380 | Output(component_id='daily_report', component_property='children'), 381 | Output(component_id='stock_bar', component_property='figure'), 382 | Output(component_id='stock_report', component_property='children'), 383 | Output(component_id='check_stock1', component_property='options'), 384 | Output(component_id='check_stock2', component_property='options'), 385 | Output(component_id='check_stock3', component_property='options'), 386 | Output(component_id='check_stock4', component_property='options'), 387 | Output(component_id='check_stock1', component_property='value'), 388 | Output(component_id='check_stock2', component_property='value'), 389 | Output(component_id='check_stock3', component_property='value'), 390 | Output(component_id='check_stock4', component_property='value') 391 | ], 392 | [Input(component_id='check_date', component_property='value')] 393 | ) 394 | def display_page(in_check_date): 395 | print(f'Viewing report for {in_check_date}') 396 | 397 | df_match_date = df[df.index == in_check_date].copy() 398 | df_dis_match_date = df_dis[df_dis.index == in_check_date].copy() 399 | 400 | # For the daily breadth histogram 401 | df_match_date_histodata = df_match_date['Breadth Percentage'].iloc[0] 402 | histogram_range_to_plot = (df_match_date_histodata > -20) & (df_match_date_histodata < 20) 403 | 404 | # Read the corresponding info dataset of the selected date 405 | selected_info_file = f'{_STOCK_INFO_URL_PATH_NAME}output/selected_stock_{in_check_date}.csv' 406 | df_match_date_info = pd.read_csv(selected_info_file) 407 | df_match_date_info = df_match_date_info.drop(df_match_date_info.columns[0], axis=1) 408 | 409 | # Reorder the columns, move the location of the change columns 410 | df_match_date_info = df_match_date_info[ 411 | ['Ticker', 'Open', 'High', 'Low', 'Close', 'Adj Close', 'Change', 'Change (%)', 'Volume', '52 Week Min', 412 | '52 Week Max', 'RS Rating', 'RS Rank'] 413 | ] 414 | 415 | if df_match_date_info.shape[0] >= _MAX_NUM_OF_STOCK_TO_DISPLAY: 416 | num_of_stocks_to_display = _MAX_NUM_OF_STOCK_TO_DISPLAY 417 | else: 418 | num_of_stocks_to_display = df_match_date_info.shape[0] 419 | 420 | # Update text in the status container 421 | container = f"US Stock Market Analysis Report for {in_check_date}" 422 | 423 | # Update stock rating plot 424 | fig = px.bar( 425 | x=df_match_date['Tickers that fit the conditions'].iloc[0][0:num_of_stocks_to_display], 426 | y=df_match_date['RS rating of Tickers'].iloc[0][0:num_of_stocks_to_display], 427 | color_continuous_scale=px.colors.sequential.Greens_r[1:7], 428 | color=np.linspace(0, 255, num_of_stocks_to_display), 429 | orientation='v' 430 | ) 431 | fig.update_coloraxes(showscale=False) 432 | fig.update_yaxes(categoryorder="total ascending") 433 | fig.update_layout(showlegend=False, autosize=False, hovermode="x", 434 | height=200, width=945, 435 | font_family="Arial", 436 | margin={ 437 | "r": 0, 438 | "t": 2, 439 | "b": 20, 440 | "l": 10, 441 | }, 442 | plot_bgcolor='rgba(250,250,250,1)', 443 | yaxis_title='RS Rating', 444 | xaxis_title="", title_x=0.5, title_y=1.0) 445 | 446 | # Update daily breadth plot 447 | fig2 = px.histogram(x=df_match_date_histodata[histogram_range_to_plot], range_x=[-20, 20], nbins=100, 448 | labels={"value": "Percentage Change (%)"}, 449 | color_discrete_sequence=['#009900'], title='') 450 | fig2.add_annotation(xref="x domain", 451 | yref="y domain", 452 | x=0.025, 453 | y=0.975, 454 | showarrow=False, 455 | text=f"Net Breadth (AD Percent) = {df_dis_match_date['AD Percent'].values[0]:.2f}%" 456 | ) 457 | fig2.add_vline(x=0, line_width=2, line_dash="dash", 458 | line_color='rgba(227,227,227,0.75)') 459 | # fig2.add_shape(type='line', xref='x', yref='y', 460 | # x0=0, y0=0, x1=0, y1=1000, line=dict(dash='dash', color='rgba(227,227,227,0.75)', width=1.5) 461 | # ) 462 | fig2.update_layout(showlegend=False, autosize=False, hovermode="x", 463 | height=275, width=450, 464 | font_family="Arial", 465 | margin={ 466 | "r": 0, 467 | "t": 2, 468 | "b": 20, 469 | "l": 10, 470 | }, 471 | plot_bgcolor='rgba(250,250,250,1)', 472 | xaxis_title='Percentage Change (%)', 473 | yaxis_title="Count", title_x=0.5, title_y=1.0) 474 | 475 | # Update daily performance table and stock info table 476 | table_daily = make_performance_table(df_dis_match_date) 477 | table_info = make_stock_info_table(df_match_date_info) 478 | 479 | # Update the check_stock1 and 2 dropdown list 480 | out_stock_list = _get_dropdown_list_from_ticker(df_match_date_info) 481 | 482 | # Get the default values for the stock charts 483 | out_stock_value1, out_stock_value2, out_stock_value3, out_stock_value4 = _get_default_stock_chart_values( 484 | out_stock_list, df_match_date_info 485 | ) 486 | 487 | return container, fig2, table_daily, fig, table_info, \ 488 | out_stock_list, out_stock_list, out_stock_list, out_stock_list, \ 489 | out_stock_value1, out_stock_value2, out_stock_value3, out_stock_value4 490 | 491 | 492 | def _get_default_stock_chart_values(stock_list, df): 493 | num_of_stocks = df.shape[0] 494 | 495 | output_list = [] 496 | if num_of_stocks < _NUM_OF_STOCK_CHART_TO_DISPLAY: 497 | for stock in stock_list: 498 | output_list.append(stock["value"]) 499 | 500 | while len(output_list) < _NUM_OF_STOCK_CHART_TO_DISPLAY: 501 | output_list.append(stock_list[0]["value"]) 502 | 503 | return output_list 504 | else: 505 | return [stock_list[i]["value"] for i in range(_NUM_OF_STOCK_CHART_TO_DISPLAY)] 506 | 507 | 508 | # Callbacks to update the stock OHLC charts. Made them individual function so that we can change the input for them. 509 | @app.callback( 510 | Output(component_id='stock_chart1', component_property='figure'), 511 | [Input(component_id='check_stock1', component_property='value'), 512 | Input(component_id='check_date', component_property='value')] 513 | ) 514 | def display_stock_graph1(in_ticker, in_date): 515 | """Currently OHLC data is fetched online.""" 516 | stock_df = get_ohlc_data_web(in_ticker) 517 | # stock_df = get_ohlc_data(in_ticker) 518 | 519 | return _get_stock_graph(stock_df, in_ticker, in_date) 520 | 521 | 522 | @app.callback( 523 | Output(component_id='stock_chart2', component_property='figure'), 524 | [Input(component_id='check_stock2', component_property='value'), 525 | Input(component_id='check_date', component_property='value')] 526 | ) 527 | def display_stock_graph2(in_ticker, in_date): 528 | stock_df = get_ohlc_data_web(in_ticker) 529 | 530 | return _get_stock_graph(stock_df, in_ticker, in_date) 531 | 532 | 533 | @app.callback( 534 | Output(component_id='stock_chart3', component_property='figure'), 535 | [Input(component_id='check_stock3', component_property='value'), 536 | Input(component_id='check_date', component_property='value')] 537 | ) 538 | def display_stock_graph3(in_ticker, in_date): 539 | stock_df = get_ohlc_data_web(in_ticker) 540 | 541 | return _get_stock_graph(stock_df, in_ticker, in_date) 542 | 543 | 544 | @app.callback( 545 | Output(component_id='stock_chart4', component_property='figure'), 546 | [Input(component_id='check_stock4', component_property='value'), 547 | Input(component_id='check_date', component_property='value')] 548 | ) 549 | def display_stock_graph4(in_ticker, in_date): 550 | stock_df = get_ohlc_data_web(in_ticker) 551 | 552 | return _get_stock_graph(stock_df, in_ticker, in_date) 553 | 554 | 555 | def _get_stock_graph(stock_df, in_ticker, in_date): 556 | fig = go.Figure( 557 | data=[ 558 | go.Ohlc( 559 | x=stock_df.index, 560 | open=stock_df["Open"], 561 | high=stock_df["High"], 562 | low=stock_df["Low"], 563 | close=stock_df["Close"], 564 | name=in_ticker, 565 | ), 566 | go.Scatter(x=stock_df.index, y=stock_df["SMA_20"], line=dict(color="orange", width=1), name="SMA 20"), 567 | go.Scatter(x=stock_df.index, y=stock_df["SMA_50"], line=dict(color="green", width=1), name="SMA 50"), 568 | go.Scatter(x=stock_df.index, y=stock_df["SMA_200"], line=dict(color="darkblue", width=1), name="SMA 200"), 569 | ] 570 | ) 571 | 572 | fig.update_layout( 573 | showlegend=False, autosize=False, hovermode="x", 574 | height=370, width=465, 575 | font_family="Arial", 576 | margin={ 577 | "r": 0, 578 | "t": 10, 579 | "b": 20, 580 | "l": 0, 581 | }, 582 | plot_bgcolor="rgba(250,250,250,1)", 583 | xaxis_title="", yaxis_title="", 584 | title_x=0.5, title_y=1.0, 585 | ) 586 | fig.add_vline(x=in_date, line_width=2, line_dash="dash", line_color="rgba(227,227,227,0.75)") 587 | 588 | return fig 589 | 590 | # ------------------------------------------------------------------------------ 591 | if __name__ == '__main__': 592 | app.run_server(debug=True) # debug=True 593 | --------------------------------------------------------------------------------