├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── example.py ├── img ├── extrema.png ├── extrema.svg ├── hough.png ├── hough.svg ├── linregrs.png ├── linregrs.svg ├── reimann.png ├── reimann.svg ├── slopeint.png ├── slopeint.svg ├── suppres.png ├── suppres.svg ├── suppreserr.png └── suppreserr.svg ├── meta.yaml ├── requirements.txt ├── setup.cfg ├── setup.py └── trendln └── __init__.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 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | sudo: true 4 | fast_finish: true 5 | 6 | matrix: 7 | include: 8 | - python: 2.7 9 | - python: 3.5 10 | - python: 3.6 11 | - python: 3.7 12 | dist: xenial 13 | sudo: true 14 | 15 | install: 16 | - pip install Cython 17 | - pip install -r requirements.txt 18 | - pip install . 19 | 20 | script: 21 | - nosetests 22 | 23 | after_success: 24 | - coveralls 25 | 26 | branches: 27 | only: 28 | - master 29 | 30 | notifications: 31 | webhooks: 32 | on_success: change # options: [always|never|change] default: always 33 | on_failure: always # options: [always|never|change] default: always 34 | on_start: never # options: [always|never|change] default: always 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | =========== 3 | 4 | 0.1.8 5 | ------- 6 | - Initial release 7 | 8 | 0.1.10 9 | ------- 10 | - Fixed error in ybar for linear regression equation 11 | - Added parameter error checking to avoid crashing with not easily understood messages (thanks ravi2007147) 12 | - Added ability to pass tuple with Low and/or High values for separate minima and/or maxima trend lines 13 | - Added library function for getting local minima/maxima as a separate operation 14 | - Fixed bug in plotting when empty range occurred (thanks 5xcor) 15 | - Refactored the pandas minima/maxima code for performance 16 | - Refactored several other places to eliminate duplicate code and improve performance 17 | - Added accuracy parameter for numerical differentiation method 18 | - Test coverage for important part of the new functionality 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Gregory Morse 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include the license file 2 | include LICENSE 3 | 4 | # Include the data files 5 | recursive-include * 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # trendln 2 | 3 | Support and Resistance Trend lines Calculator for Financial Analysis 4 | ==================================================================== 5 | 6 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/trendln)](https://pypi.python.org/pypi/trendln) 7 | [![PyPI - Version](https://img.shields.io/pypi/v/trendln.svg?maxAge=60)](https://pypi.python.org/pypi/trendln) 8 | [![PyPI - Status](https://img.shields.io/pypi/status/trendln.svg?maxAge=60)](https://pypi.python.org/pypi/trendln) 9 | [![PyPI - Downloads](https://img.shields.io/pypi/dm/trendln.svg?maxAge=2592000&label=installs&color=%2327B1FF)](https://pypi.python.org/pypi/trendln) 10 | [![PyPI - License](https://img.shields.io/pypi/l/trendln)](https://pypi.python.org/pypi/trendln) 11 | [![PyPI - Implementation](https://img.shields.io/pypi/implementation/trendln)](https://pypi.python.org/pypi/trendln) 12 | [![Latest push build on default branch](https://travis-ci.com/GregoryMorse/trendln.svg?branch=master)](https://travis-ci.com/GregoryMorse/trendln) 13 | [![GitHub stars](https://img.shields.io/github/stars/GregoryMorse/trendln?style=social)](https://github.com/GregoryMorse/trendln) 14 | 15 | Note 16 | ---- 17 | 18 | This library can calculate and plot trend lines for any time series, not only for its primary intended purpose of financial analysis. 19 | 20 | [Changelog »](./CHANGELOG.md) 21 | 22 | --- 23 | 24 | ==> Check out this article on [Programmatic Identification of Support/Resistance Trend lines with Python](https://towardsdatascience.com/programmatic-identification-of-support-resistance-trend-lines-with-python-d797a4a90530) or [alternatively here](https://medium.com/@gregory.morse1/programmatic-identification-of-support-resistance-trend-lines-with-python-d797a4a90530) 25 | for details on how the library and its features are implemented and work. 26 | 27 | --- 28 | 29 | Quick Start 30 | =========== 31 | 32 | Calculation Only 33 | ---------------- 34 | 35 | The **calc_support_resistance** function will calculate all support and 36 | resistance information including local extrema, average and their 37 | trend lines using several different methods: 38 | 39 | import trendln 40 | # this will serve as an example for security or index closing prices, or low and high prices 41 | import yfinance as yf # requires yfinance - pip install yfinance 42 | tick = yf.Ticker('^GSPC') # S&P500 43 | hist = tick.history(period="max", rounding=True) 44 | h = hist[-1000:].Close 45 | mins, maxs = trendln.calc_support_resistance(h) 46 | minimaIdxs, pmin, mintrend, minwindows = trendln.calc_support_resistance((hist[-1000:].Low, None)) #support only 47 | mins, maxs = trendln.calc_support_resistance((hist[-1000:].Low, hist[-1000:].High)) 48 | (minimaIdxs, pmin, mintrend, minwindows), (maximaIdxs, pmax, maxtrend, maxwindows) = mins, maxs 49 | 50 | Documentation for usage: 51 | 52 | (minimaIdxs, pmin, mintrend, minwindows), (maximaIdxs, pmax, maxtrend, maxwindows) = \ 53 | trendln.calc_support_resistance( 54 | # list/numpy ndarray/pandas Series of data as bool/int/float and if not a list also unsigned 55 | # or 2-tuple (support, resistance) where support and resistance are 1-dimensional array-like or one or the other is None 56 | # can calculate only support, only resistance, both for different data, or both for identical data 57 | h, 58 | 59 | # METHOD_NAIVE - any local minima or maxima only for a single interval (currently requires pandas) 60 | # METHOD_NAIVECONSEC - any local minima or maxima including those for consecutive constant intervals (currently requires pandas) 61 | # METHOD_NUMDIFF (default) - numerical differentiation determined local minima or maxima (requires findiff) 62 | extmethod = METHOD_NUMDIFF, 63 | 64 | # METHOD_NCUBED - simple exhuastive 3 point search (slowest) 65 | # METHOD_NSQUREDLOGN (default) - 2 point sorted slope search (fast) 66 | # METHOD_HOUGHPOINTS - Hough line transform optimized for points 67 | # METHOD_HOUGHLINES - image-based Hough line transform (requires scikit-image) 68 | # METHOD_PROBHOUGH - image-based Probabilistic Hough line transform (requires scikit-image) 69 | method=METHOD_NSQUREDLOGN, 70 | 71 | # window size when searching for trend lines prior to merging together 72 | window=125, 73 | 74 | # maximum percentage slope standard error 75 | errpct = 0.005, 76 | 77 | # for all METHOD_*HOUGH*, the smallest unit increment for discretization e.g. cents/pennies 0.01 78 | hough_scale=0.01, 79 | 80 | # only for METHOD_PROBHOUGH, number of iterations to run 81 | hough_prob_iter=10, 82 | 83 | # sort by area under wrong side of curve, otherwise sort by slope standard error 84 | sortError=False, 85 | 86 | # accuracy if using METHOD_NUMDIFF for example 5-point stencil is accuracy=3 87 | accuracy=1) 88 | # if h is a 2-tuple with one value as None, then a 2-tuple is not returned, but the appropriate tuple instead 89 | # minimaIdxs - sorted list of indexes to the local minima 90 | # pmin - [slope, intercept] of average best fit line through all local minima points 91 | # mintrend - sorted list containing (points, result) for local minima trend lines 92 | # points - list of indexes to points in trend line 93 | # result - (slope, intercept, SSR, slopeErr, interceptErr, areaAvg) 94 | # slope - slope of best fit trend line 95 | # intercept - y-intercept of best fit trend line 96 | # SSR - sum of squares due to regression 97 | # slopeErr - standard error of slope 98 | # interceptErr - standard error of intercept 99 | # areaAvg - Reimann sum area of difference between best fit trend line 100 | # and actual data points averaged per time unit 101 | # minwindows - list of windows each containing mintrend for that window 102 | 103 | # maximaIdxs - sorted list of indexes to the local maxima 104 | # pmax - [slope, intercept] of average best fit line through all local maxima points 105 | # maxtrend - sorted list containing (points, result) for local maxima trend lines 106 | #see for mintrend above 107 | # maxwindows - list of windows each containing maxtrend for that window 108 | 109 | The **get_extrema** function will calculate all of the local minima and local maxima 110 | without performing the full trend line calculation. 111 | 112 | minimaIdxs, maximaIdxs = trendln.get_extrema(hist[-1000:].Close) 113 | maximaIdxs = trendln.get_extrema((None, hist[-1000:].High)) #maxima only 114 | minimaIdxs, maximaIdxs = trendln.get_extrema((hist[-1000:].Low, hist[-1000:].High)) 115 | 116 | Documentation for usage: 117 | 118 | minimaIdxs, maximaIdxs = trendln.get_extrema( 119 | h, 120 | extmethod=METHOD_NUMDIFF, 121 | accuracy=1) 122 | # parameters and results are as per defined for calc_support_resistance 123 | 124 | Plotting Calculations 125 | --------------------- 126 | The **plot_support_resistance** function will calculate and plot the average 127 | and top 2 support and resistance lines, along with marking extrema used with 128 | a maximum history length, and otherwise identical arguments to the 129 | calculation function. 130 | 131 | fig = trendln.plot_support_resistance(hist[-1000:].Close) # requires matplotlib - pip install matplotlib 132 | plt.savefig('suppres.svg', format='svg') 133 | plt.show() 134 | plt.clf() #clear figure 135 | 136 | Documentation for usage: 137 | 138 | fig = trendln.plot_support_resistance( 139 | hist, #as per h for calc_support_resistance 140 | xformatter = None, #x-axis data formatter turning numeric indexes to display output 141 | # e.g. ticker.FuncFormatter(func) otherwise just display numeric indexes 142 | numbest = 2, #number of best support and best resistance lines to display 143 | fromwindows = True, #draw numbest best from each window, otherwise draw numbest across whole range 144 | pctbound = 0.1, # bound trend line based on this maximum percentage of the data range above the high or below the low 145 | extmethod = METHOD_NUMDIFF, 146 | method=METHOD_NSQUREDLOGN, 147 | window=125, 148 | errpct = 0.005, 149 | hough_prob_iter=10, 150 | sortError=False, 151 | accuracy=1) 152 | # other parameters as per calc_support_resistance 153 | # fig - returns matplotlib.pyplot.gcf() or the current figure 154 | 155 | The **plot_sup_res_date** function will do the same as **plot_support_resistance** with 156 | help for nice formatting of dates based on a pandas date index. 157 | 158 | idx = hist[-1000:].index 159 | fig = trendln.plot_sup_res_date((hist[-1000:].Low, hist[-1000:].High), idx) #requires pandas 160 | plt.savefig('suppres.svg', format='svg') 161 | plt.show() 162 | plt.clf() #clear figure 163 | 164 | Documentation for usage: 165 | 166 | fig = trendln.plot_sup_res_date( #automatic date formatter based on US trading calendar 167 | hist, #as per h for calc_support_resistance 168 | idx, #date index from pandas 169 | numbest = 2, 170 | fromwindows = True, 171 | pctbound = 0.1, 172 | extmethod = METHOD_NUMDIFF, 173 | method=METHOD_NSQUREDLOGN, 174 | window=125, 175 | errpct = 0.005, 176 | hough_scale=0.01, 177 | hough_prob_iter=10, 178 | sortError=False, 179 | accuracy=1) 180 | # other parameters as per plot_support_resistance 181 | 182 | Finally, for the above mentioned article, some figures were generated for reference material, 183 | while others use the library to demonstrate how it works. These can be generated as well: 184 | 185 | trendln.plot_sup_res_learn('.', hist) 186 | 187 | Documentation for usage: 188 | 189 | trendln.plot_sup_res_learn( #draw learning figures, included for reference material only 190 | curdir, #base output directory for png and svg images, will be saved in 'data' subfolder 191 | hist) #pandas DataFrame containing Close and date index 192 | 193 | ![Example output of plotting support resistance](https://github.com/GregoryMorse/trendln/blob/master/img/suppres.svg) 194 | 195 | Installation 196 | ------------ 197 | 198 | Install ``trendln`` using ``pip``: 199 | 200 | $ pip install trendln --upgrade --no-cache-dir 201 | 202 | 203 | Install ``trendln`` using ``conda``: 204 | 205 | $ conda install -c GregoryMorse trendln 206 | 207 | Installation sanity check: 208 | 209 | import trendln 210 | #requires yfinance library install, not a package requirement, but used to assist with sanity check 211 | #pip install yfinance 212 | directory = '.' # a 'data' folder will be created here if not existing to store images 213 | trendln.test_sup_res(directory) #simple tests that all methods are executing correct, assertion or other error indicates problem 214 | 215 | Requirements 216 | ------------ 217 | 218 | * [Python](https://www.python.org) >= 2.7, 3.4+ 219 | * [numpy](http://www.numpy.org) >= 1.15 220 | * [findiff](https://github.com/maroba/findiff) >= 0.7.0 (if using default numerical differentiation method) 221 | * [scikit-image](https://scikit-image.org) >= 0.14.0 (if using image-based Hough line transform or its probabilistic variant) 222 | * [pandas](https://github.com/pydata/pandas) >= 0.23.1 (if using date plotting function, or using naive minima/maxima methods) 223 | * [matplotlib](https://matplotlib.org) >= 2.2.4 (if using any plotting function) 224 | 225 | 226 | License 227 | ------- 228 | 229 | **trendln** is distributed under the **MIT License**. See the [LICENSE](./LICENSE) file in the release for details. 230 | 231 | Support 232 | ------- 233 | 234 | Any questions, issues or ideas can kindly be submitted for review. 235 | 236 | **Gregory Morse** 237 | 238 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import trendln 2 | import matplotlib.pyplot as plt 3 | # this will serve as an example for security or index closing prices, or low and high prices 4 | import yfinance as yf # requires yfinance - pip install yfinance 5 | tick = yf.Ticker('^GSPC') # S&P500 6 | hist = tick.history(period="max", rounding=True) 7 | mins, maxs = trendln.calc_support_resistance(hist[-1000:].Close) 8 | minimaIdxs, pmin, mintrend, minwindows = trendln.calc_support_resistance((hist[-1000:].Low, None)) #support only 9 | mins, maxs = trendln.calc_support_resistance((hist[-1000:].Low, hist[-1000:].High)) 10 | (minimaIdxs, pmin, mintrend, minwindows), (maximaIdxs, pmax, maxtrend, maxwindows) = mins, maxs 11 | minimaIdxs, maximaIdxs = trendln.get_extrema(hist[-1000:].Close) 12 | maximaIdxs = trendln.get_extrema((None, hist[-1000:].High)) #maxima only 13 | minimaIdxs, maximaIdxs = trendln.get_extrema((hist[-1000:].Low, hist[-1000:].High)) 14 | fig = trendln.plot_support_resistance(hist[-1000:].Close) # requires matplotlib - pip install matplotlib 15 | plt.savefig('suppres.svg', format='svg') 16 | plt.show() 17 | plt.clf() #clear figure 18 | fig = trendln.plot_sup_res_date((hist[-1000:].Low, hist[-1000:].High), hist[-1000:].index) #requires pandas 19 | plt.savefig('suppres.svg', format='svg') 20 | plt.show() 21 | plt.clf() #clear figure 22 | curdir = '.' 23 | trendln.plot_sup_res_learn(curdir, hist) 24 | -------------------------------------------------------------------------------- /img/extrema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GregoryMorse/trendln/3f5ff2e8c067684cec99e29d5e0d51b323c0fb8d/img/extrema.png -------------------------------------------------------------------------------- /img/extrema.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 28 | 29 | 30 | 41 | 42 | 43 | 54 | 55 | 56 | 67 | 68 | 69 | 80 | 81 | 82 | 93 | 94 | 95 | 106 | 107 | 108 | 119 | 120 | 121 | 132 | 133 | 134 | 145 | 146 | 147 | 158 | 159 | 160 | 161 | 162 | 163 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 198 | 219 | 232 | 262 | 268 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 474 | 506 | 526 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 740 | 756 | 767 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 847 | 848 | 849 | 852 | 853 | 854 | 857 | 858 | 859 | 862 | 863 | 864 | 867 | 868 | 869 | 870 | 871 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 903 | 909 | 910 | 911 | 912 | 913 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | 950 | 951 | 952 | 953 | 954 | 955 | 956 | 957 | 958 | 959 | 962 | 968 | 969 | 970 | 971 | 972 | 973 | 974 | 975 | 976 | 977 | 978 | 979 | 980 | 981 | 982 | 983 | 984 | 985 | 986 | 987 | 988 | 989 | 990 | 991 | 992 | 993 | 996 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | 1013 | 1014 | 1015 | 1016 | 1017 | 1018 | 1027 | 1028 | 1029 | 1030 | 1031 | 1032 | 1033 | 1034 | 1035 | 1036 | 1037 | 1040 | 1046 | 1047 | 1048 | 1049 | 1050 | 1051 | 1052 | 1053 | 1054 | 1055 | 1056 | 1057 | 1058 | 1059 | 1060 | 1061 | 1062 | 1063 | 1064 | 1065 | 1066 | 1067 | 1068 | 1069 | 1070 | 1071 | 1072 | 1075 | 1081 | 1082 | 1083 | 1084 | 1085 | 1086 | 1087 | 1088 | 1089 | 1090 | 1091 | 1092 | 1093 | 1094 | 1095 | 1096 | 1097 | 1098 | 1099 | 1100 | 1101 | 1102 | 1103 | 1104 | 1105 | 1106 | 1107 | 1110 | 1116 | 1117 | 1118 | 1119 | 1120 | 1121 | 1122 | 1123 | 1124 | 1125 | 1126 | 1127 | 1128 | 1129 | 1130 | 1131 | 1132 | 1133 | 1134 | 1135 | 1136 | 1137 | 1138 | 1139 | 1140 | 1141 | 1144 | 1150 | 1151 | 1152 | 1153 | 1154 | 1155 | 1156 | 1157 | 1158 | 1159 | 1160 | 1161 | 1162 | 1163 | 1164 | 1165 | 1166 | 1167 | 1168 | 1169 | 1170 | 1171 | 1172 | 1173 | 1176 | 1182 | 1183 | 1184 | 1185 | 1186 | 1187 | 1188 | 1189 | 1190 | 1191 | 1192 | 1193 | 1194 | 1195 | 1196 | 1197 | 1198 | 1199 | 1200 | 1201 | 1202 | 1203 | 1204 | 1205 | 1206 | 1209 | 1215 | 1216 | 1217 | 1218 | 1219 | 1220 | 1221 | 1222 | 1223 | 1224 | 1225 | 1226 | 1227 | 1228 | 1229 | 1230 | 1231 | 1232 | 1233 | 1234 | 1235 | 1236 | 1237 | 1238 | 1239 | 1240 | 1241 | 1242 | 1245 | 1251 | 1252 | 1253 | 1254 | 1255 | 1276 | 1282 | 1303 | 1334 | 1353 | 1386 | 1387 | 1402 | 1421 | 1430 | 1438 | 1453 | 1483 | 1504 | 1518 | 1519 | 1520 | 1521 | 1522 | 1523 | 1524 | 1525 | 1526 | 1527 | 1528 | 1529 | 1530 | 1531 | 1532 | 1533 | 1534 | 1535 | 1536 | 1537 | 1538 | 1539 | 1540 | 1541 | 1542 | 1543 | 1544 | 1545 | 1546 | 1547 | 1548 | 1549 | 1550 | 1551 | 1552 | 1553 | 1554 | 1555 | 1556 | 1557 | 1558 | 1559 | 1560 | 1561 | 1562 | 1563 | 1564 | 1565 | 1566 | 1567 | 1568 | 1569 | 1570 | 1571 | 1572 | 1573 | 1574 | 1575 | 1576 | 1577 | 1578 | 1579 | 1590 | 1591 | 1592 | 1593 | 1594 | 1595 | 1596 | 1597 | 1598 | 1599 | 1600 | 1601 | 1602 | 1603 | 1604 | 1605 | 1606 | 1607 | 1608 | 1609 | 1610 | 1611 | 1612 | 1613 | 1614 | 1615 | 1616 | 1617 | 1631 | 1632 | 1633 | 1634 | 1635 | 1636 | 1637 | 1638 | 1639 | 1640 | 1641 | 1642 | 1645 | 1646 | 1647 | 1648 | 1649 | 1650 | 1651 | 1652 | 1653 | 1654 | 1655 | 1656 | 1657 | 1658 | 1659 | 1660 | 1661 | 1662 | 1663 | 1664 | 1670 | 1671 | 1672 | 1673 | 1674 | 1683 | 1699 | 1700 | 1701 | 1702 | 1703 | 1704 | 1705 | 1706 | 1707 | 1708 | 1709 | 1710 | 1711 | 1712 | 1718 | 1719 | 1720 | 1721 | 1722 | 1723 | 1724 | 1725 | 1726 | 1727 | 1728 | 1729 | 1730 | 1731 | 1732 | 1733 | 1734 | 1735 | 1736 | 1737 | 1738 | 1739 | 1740 | 1741 | 1742 | 1743 | 1744 | 1745 | -------------------------------------------------------------------------------- /img/hough.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GregoryMorse/trendln/3f5ff2e8c067684cec99e29d5e0d51b323c0fb8d/img/hough.png -------------------------------------------------------------------------------- /img/linregrs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GregoryMorse/trendln/3f5ff2e8c067684cec99e29d5e0d51b323c0fb8d/img/linregrs.png -------------------------------------------------------------------------------- /img/reimann.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GregoryMorse/trendln/3f5ff2e8c067684cec99e29d5e0d51b323c0fb8d/img/reimann.png -------------------------------------------------------------------------------- /img/slopeint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GregoryMorse/trendln/3f5ff2e8c067684cec99e29d5e0d51b323c0fb8d/img/slopeint.png -------------------------------------------------------------------------------- /img/suppres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GregoryMorse/trendln/3f5ff2e8c067684cec99e29d5e0d51b323c0fb8d/img/suppres.png -------------------------------------------------------------------------------- /img/suppreserr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GregoryMorse/trendln/3f5ff2e8c067684cec99e29d5e0d51b323c0fb8d/img/suppreserr.png -------------------------------------------------------------------------------- /img/suppreserr.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 68 | 89 | 102 | 141 | 147 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 255 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 423 | 455 | 475 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 635 | 651 | 662 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 813 | 814 | 815 | 818 | 819 | 820 | 1068 | 1069 | 1070 | 1073 | 1074 | 1075 | 1078 | 1079 | 1080 | 1083 | 1084 | 1085 | 1088 | 1089 | 1090 | 1093 | 1094 | 1095 | 1098 | 1099 | 1100 | 1103 | 1104 | 1105 | 1108 | 1109 | 1110 | 1111 | 1112 | 1133 | 1139 | 1160 | 1191 | 1210 | 1243 | 1244 | 1259 | 1278 | 1309 | 1330 | 1356 | 1362 | 1389 | 1399 | 1425 | 1433 | 1434 | 1435 | 1436 | 1437 | 1438 | 1439 | 1440 | 1441 | 1442 | 1443 | 1444 | 1445 | 1446 | 1447 | 1448 | 1449 | 1450 | 1451 | 1452 | 1453 | 1454 | 1455 | 1456 | 1457 | 1458 | 1459 | 1460 | 1461 | 1462 | 1463 | 1464 | 1465 | 1466 | 1467 | 1468 | 1469 | 1470 | 1471 | 1472 | 1473 | 1474 | 1475 | 1476 | 1477 | 1478 | 1479 | 1480 | 1481 | 1482 | 1483 | 1484 | 1485 | 1486 | 1487 | 1488 | 1499 | 1500 | 1501 | 1504 | 1505 | 1506 | 1507 | 1508 | 1509 | 1523 | 1532 | 1538 | 1539 | 1540 | 1541 | 1542 | 1543 | 1544 | 1545 | 1546 | 1547 | 1548 | 1549 | 1550 | 1551 | 1552 | 1553 | 1554 | 1555 | 1558 | 1559 | 1560 | 1561 | 1562 | 1563 | 1564 | 1565 | 1566 | 1567 | 1568 | 1569 | 1570 | 1571 | 1572 | 1573 | 1574 | 1575 | 1576 | 1577 | 1578 | 1579 | 1580 | 1581 | 1584 | 1585 | 1586 | 1587 | 1588 | 1589 | 1590 | 1591 | 1592 | 1593 | 1594 | 1595 | 1596 | 1597 | 1598 | 1599 | 1600 | 1601 | 1602 | 1603 | 1606 | 1607 | 1608 | 1609 | 1610 | 1611 | 1612 | 1613 | 1614 | 1615 | 1616 | 1617 | 1618 | 1619 | 1620 | 1621 | 1624 | 1625 | 1626 | 1627 | 1628 | 1629 | 1630 | 1631 | 1632 | 1633 | 1634 | 1635 | 1636 | 1637 | 1638 | 1639 | 1640 | 1641 | 1642 | 1643 | 1644 | 1645 | 1646 | 1647 | 1648 | 1649 | 1650 | -------------------------------------------------------------------------------- /meta.yaml: -------------------------------------------------------------------------------- 1 | {% set name = "trendln" %} 2 | {% set version = "0.1.10" %} 3 | 4 | package: 5 | name: "{{ name|lower }}" 6 | version: "{{ version }}" 7 | 8 | source: 9 | url: "https://pypi.io/packages/source/{{ name[0] }}/{{ name }}/{{ name }}-{{ version }}.tar.gz" 10 | sha256: "e2131c5e724258597f828e3210084d41f8eeedc38b462c8a33c9fbb772935f06" 11 | 12 | build: 13 | noarch: python 14 | number: 0 15 | script: "{{ PYTHON }} -m pip install . --no-deps --ignore-installed -vv " 16 | 17 | requirements: 18 | host: 19 | - numpy >=1.15 20 | - findiff >=0.7.0 21 | - scikit-image >=0.14.0 22 | - pandas >=0.23.1 23 | - matplotlib >=2.2.4 24 | - pip 25 | - python 26 | run: 27 | - numpy >=1.15 28 | - findiff >=0.7.0 29 | - scikit-image >=0.14.0 30 | - pandas >=0.23.1 31 | - matplotlib >=2.2.4 32 | - python 33 | 34 | test: 35 | imports: 36 | - trendln 37 | 38 | about: 39 | home: "https://github.com/GregoryMorse/trendln" 40 | license: "MIT" 41 | license_family: "MIT" 42 | license_file: "LICENSE" 43 | summary: "Support and Resistance Trend lines Calculator for Financial Analysis" 44 | description: "See the package README.md for more information." 45 | doc_url: "https://github.com/GregoryMorse/trendln" 46 | dev_url: "https://pypi.python.org/pypi/trendln" 47 | doc_source_url: https://github.com/GregoryMorse/trendln/blob/master/README.md 48 | 49 | 50 | extra: 51 | recipe-maintainers: 52 | - GregoryMorse 53 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.15 2 | findiff>=0.7.0 3 | scikit-image>=0.14.0 4 | pandas>=0.23.1 5 | matplotlib>=2.2.4 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | universal=1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | # 4 | # Support and Resistance Trendlines Calculator for Financial Analysis 5 | # https://pypi.org/project/trendln 6 | # https://github.com/GregoryMorse/trendln 7 | 8 | """Support and Resistance Trendlines Calculator for Financial Analysis""" 9 | 10 | #pip install twine 11 | 12 | #cd /D D:\OneDrive\Documents\Projects\trader\trendln 13 | #del dist\*.tar.gz 14 | #"%ProgramFiles%\Python37\python.exe" setup.py sdist 15 | #"%ProgramFiles%\Python37\scripts\twine.exe" upload dist/* --verbose 16 | #"%ProgramFiles%\Python37\scripts\pip.exe" install trendln --upgrade 17 | #"%ProgramData%\Anaconda3\scripts\pip.exe" install trendln --upgrade 18 | #import importlib 19 | #importlib.reload(trendln) 20 | 21 | from setuptools import setup, find_packages 22 | import io 23 | from os import path 24 | 25 | here = path.abspath(path.dirname(__file__)) 26 | 27 | # Get the long description from the README file 28 | with io.open(path.join(here, 'README.md'), encoding='utf-8') as f: 29 | long_description = f.read() 30 | 31 | setup( 32 | name='trendln', 33 | version="0.1.10", 34 | description='Support and Resistance Trend lines Calculator for Financial Analysis', 35 | long_description=long_description, 36 | long_description_content_type='text/markdown', 37 | url='https://github.com/GregoryMorse/trendln', 38 | author='Gregory Morse', 39 | author_email='gregory.morse@live.com', 40 | license='MIT', 41 | classifiers=[ 42 | 'License :: OSI Approved :: MIT License', 43 | # 'Development Status :: 3 - Alpha', 44 | # 'Development Status :: 4 - Beta', 45 | 'Development Status :: 5 - Production/Stable', 46 | 47 | 48 | 'Operating System :: OS Independent', 49 | 'Intended Audience :: Developers', 50 | 'Topic :: Office/Business :: Financial', 51 | 'Topic :: Office/Business :: Financial :: Investment', 52 | 'Topic :: Software Development :: Libraries', 53 | 'Topic :: Software Development :: Libraries :: Python Modules', 54 | 55 | 'Programming Language :: Python :: 2.7', 56 | 'Programming Language :: Python :: 3.4', 57 | 'Programming Language :: Python :: 3.5', 58 | 'Programming Language :: Python :: 3.6', 59 | 'Programming Language :: Python :: 3.7', 60 | ], 61 | platforms = ['any'], 62 | keywords='trendlines, trend lines, trend, support, resistance, trends, technical, indicators, financial, analysis', 63 | packages=find_packages(exclude=['contrib', 'docs', 'tests', 'examples']), 64 | install_requires=['numpy>=1.15', 'findiff>=0.7.0', 'scikit-image>=0.14.0', 'pandas>=0.23.1', 'matplotlib>=2.2.4'], 65 | ) 66 | -------------------------------------------------------------------------------- /trendln/__init__.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | #exec(open(r'D:\OneDrive\documents\Projects\trader\trendln\trendln\__init__.py').read()) 3 | 4 | def datefmt(xdate, cal=None): 5 | from pandas.tseries.holiday import AbstractHolidayCalendar, Holiday, nearest_workday, \ 6 | USMartinLutherKingJr, USPresidentsDay, GoodFriday, USMemorialDay, \ 7 | USLaborDay, USThanksgivingDay 8 | from pandas.tseries.offsets import CustomBusinessDay 9 | class USTradingCalendar(AbstractHolidayCalendar): 10 | rules = [ 11 | Holiday('NewYearsDay', month=1, day=1, observance=nearest_workday), 12 | USMartinLutherKingJr, 13 | USPresidentsDay, 14 | GoodFriday, 15 | USMemorialDay, 16 | Holiday('USIndependenceDay', month=7, day=4, observance=nearest_workday), 17 | USLaborDay, 18 | USThanksgivingDay, 19 | Holiday('Christmas', month=12, day=25, observance=nearest_workday) 20 | ] 21 | if cal == None: cal = USTradingCalendar() 22 | def mydate(x,pos): 23 | #print((x,pos)) 24 | val = int(x + 0.5) 25 | if val < 0: return (xdate[0].to_pydatetime() - CustomBusinessDay(-val, calendar=cal)).strftime('%Y-%m-%d') 26 | elif val >= len(xdate): return (xdate[-1].to_pydatetime() + CustomBusinessDay(val - len(xdate) + 1, calendar=cal)).strftime('%Y-%m-%d') 27 | else: return xdate[val].strftime('%Y-%m-%d') 28 | return mydate 29 | 30 | def plot_sup_res_learn(curdir, hist): 31 | import os 32 | if not os.path.isdir(os.path.join(curdir, 'data')): os.mkdir(os.path.join(curdir, 'data')) #image folder 33 | import pandas as pd 34 | import matplotlib.pyplot as plt 35 | import matplotlib.ticker as ticker 36 | #for x in plt.get_fignums(): plt.close(plt.figure(x)) #clean up when crashes occur and figures left open 37 | #plt.get_backend(): 'TkAgg' is default 38 | hist = hist[:'2019-10-07'] 39 | def fig_slopeint(): 40 | plt.clf() 41 | plt.rcParams.update({'font.size': 14}) 42 | plt.gcf().set_size_inches(1000/plt.gcf().dpi, 1000/plt.gcf().dpi) #plt.gcf().dpi=100 43 | spec = gridspec.GridSpec(ncols=1, nrows=2, figure=plt.gcf(), height_ratios=[3, 1]) 44 | plt.subplot(spec[1, 0]) 45 | plt.axis('off') 46 | plt.gca().get_xaxis().set_visible(False) 47 | plt.gca().get_yaxis().set_visible(False) 48 | plt.annotate(r'Standard slope-intercept line equation: $f(x)=y=mx+b$' + '\n' 49 | r'For 2 points $(x_0, y_0), (x_1, y_1)$:' + '\n' + 50 | r'Slope derived from two points: $m=\frac{\Delta y}{\Delta x}=\frac{y_0-y_1}{x_0-x_1}$' + '\n' + 51 | r'Intercept derived from slope and point: $b=y_0-mx_0=y_1-mx_1$' + '\n' + 52 | r'Y-axis Distance to point from line: $d=\left|mx_2+b-y_2\right|$' + '\n' + 53 | r'''Pythagorean's Theorem for Right Triangles: $c^2=a^2+b^2\equiv$ $d^2=\Delta x^2+\Delta y^2$''' + '\n' + 54 | r'Distance between Points: d=$\sqrt{(x_1-x_0)^2+(y_1-y_0)^2}$', (0, 0)) 55 | plt.subplot(spec[0, 0]) 56 | m = (hist.Close.iloc[-3] - hist.Close.iloc[-1]) / -2 57 | b1, b2 = hist.Close.iloc[-1] - m * 2, hist.Close.iloc[-3] - m * 0 58 | d = abs(m * 1 + b1 - hist.Close.iloc[-2]) 59 | dist = np.sqrt(np.square(hist.Close.iloc[-3] - hist.Close.iloc[-1]) + np.square(-2)) 60 | height = hist.Close.iloc[-3:].max() - hist.Close.iloc[-3:].min() 61 | plt.plot(range(len(hist.Close)-3, len(hist.Close)), hist.Close.iloc[-3:]) 62 | plt.yticks(hist.Close.iloc[-3:]) 63 | plt.plot([len(hist.Close)-3, len(hist.Close)-1], [hist.Close.iloc[-3], hist.Close.iloc[-1]], 'g--') 64 | #perpendicular slope: 1/-m, intercept to midpoint b=y-mx: 65 | #intcpt = (hist.Close.iloc[-3] + hist.Close.iloc[-1]) / 2 - (-1/m) 66 | 67 | ax = plt.gca() 68 | plt.ylim(ax.get_ylim()[0] - height * 0.1, ax.get_ylim()[1]) 69 | #drawdim = plt.gcf().get_size_inches()*plt.gcf().dpi 70 | bbox = ax.get_window_extent()#.transformed(plt.gcf().dpi_scale_trans.inverted()) #convert pixels to points 71 | drawdim = [bbox.width, bbox.height] 72 | xaxwdt, yaxhgt = ax.get_xlim()[1] - ax.get_xlim()[0], ax.get_ylim()[1] - ax.get_ylim()[0] 73 | mvisual = (hist.Close.iloc[-3] - hist.Close.iloc[-1]) * drawdim[1] / yaxhgt / (-2 * drawdim[0] / xaxwdt) #scale is 2:yaxhgt, could use this in computations, but must do dynamically with event handler since draw scale changes 74 | #intcpt = (hist.Close.iloc[-3] - ax.get_ylim()[0]) * drawdim[1] / yaxhgt - mvisual * (0.1 * drawdim[0] / xaxwdt) 75 | #print((mvisual, intcpt, xaxwdt, yaxhgt, drawdim, ax.get_ylim())) 76 | #(len(hist.Close)-3, hist.Close.iloc[-3]) 77 | #a = plt.annotate('', (0.1 * drawdim[0] / xaxwdt, mvisual * (0.1 * drawdim[0] / xaxwdt) + intcpt), (2.1 * drawdim[0] / xaxwdt, mvisual * (2.1 * drawdim[0] / xaxwdt) + intcpt), xycoords='axes pixels', textcoords='axes pixels', arrowprops={'arrowstyle':'-['}) 78 | intcpt = ((hist.Close.iloc[-3] + hist.Close.iloc[-1]) / 2 - ax.get_ylim()[0]) * drawdim[1] / yaxhgt - (-(drawdim[0] / 2) / mvisual) 79 | ann = plt.annotate(r'$d=\sqrt{{({}-{})^2+({}-{})^2}}={}$'.format(hist.Close.iloc[-3], hist.Close.iloc[-1], 0, 2, round(dist, 2)), (len(hist.Close)-2, (hist.Close.iloc[-3] + hist.Close.iloc[-1]) / 2), ax.transData.inverted().transform(((drawdim[0] * 0.54)+bbox.x0, (-(drawdim[0] * 0.54)/mvisual + intcpt)+bbox.y0)), textcoords='data', color='green', ha='center', va='center', arrowprops={'arrowstyle':'-[', 'color':'green'}) 80 | #print(drawdim, ann.xyann, mvisual, intcpt, ax.get_xlim()) 81 | plt.annotate(r'$b={}-{}*{}={}-{}*{}={}$'.format(hist.Close.iloc[-1], round(m, 2), 2, hist.Close.iloc[-3], round(m, 2), 0, b1), (len(hist.Close)-3, hist.Close.iloc[-3]), (len(hist.Close)-3, hist.Close.iloc[-3] - height*0.1), arrowprops={'arrowstyle':'->'}) 82 | plt.plot([len(hist.Close)-2, len(hist.Close)-2], [m * 1 + b1, hist.Close.iloc[-2]], 'r--') 83 | plt.annotate((r'$d=$' + '\n' + r'$\left|{}*{}+{}-{}\right|$' + '\n' + r'$={}$').format(round(m, 2), 1, b1, hist.Close.iloc[-2], round(d, 2)), (len(hist.Close)-2, (m * 1 + b1 + hist.Close.iloc[-2]) / 2), (len(hist.Close)-2+0.1, (m * 1 + b1 + hist.Close.iloc[-2]) / 2), va='center', color='red', arrowprops={'arrowstyle':'-[', 'color':'red'}) 84 | plt.annotate(r'$m=\frac{{{}}}{{{}}}={}$'.format(round(hist.Close.iloc[-3] - hist.Close.iloc[-1], 2), 0 - 2, round(m, 2)), (len(hist.Close)-2, (hist.Close.iloc[-3] + hist.Close.iloc[-1]) / 2), (len(hist.Close)-2+0.2, (hist.Close.iloc[-3] + hist.Close.iloc[-1]) / 2 - height * 0.1), color='black', arrowprops={'arrowstyle':'->'}) 85 | plt.plot([len(hist.Close)-3, len(hist.Close)-1], [hist.Close.iloc[-3], hist.Close.iloc[-3]], 'c--') 86 | plt.annotate(r'$\Delta x={}-{}={}$'.format(0, 2, 0 - 2), (len(hist.Close)-2, hist.Close.iloc[-3]), (len(hist.Close)-2, hist.Close.iloc[-3] + height * 0.10), color='cyan', ha='center', va='center', arrowprops={'arrowstyle':'-[', 'color':'cyan'}) 87 | plt.plot([len(hist.Close)-1, len(hist.Close)-1], [hist.Close.iloc[-3], hist.Close.iloc[-1]], 'c--') 88 | plt.annotate(r'$\Delta y={}-{}={}$'.format(hist.Close.iloc[-3], hist.Close.iloc[-1], round(hist.Close.iloc[-3] - hist.Close.iloc[-1], 2)), (len(hist.Close)-1, (hist.Close.iloc[-3] + hist.Close.iloc[-1]) / 2), (len(hist.Close)-2+0.5, (hist.Close.iloc[-3] + hist.Close.iloc[-1]) / 2), color='cyan', ha='center', va='center', arrowprops={'arrowstyle':'-[', 'color':'cyan'}) 89 | 90 | plt.title('Closing Price Points Demonstrating Line Calculations') 91 | plt.xlabel('Date') 92 | plt.ylabel('Price') 93 | ax.xaxis.set_major_locator(ticker.IndexLocator(1, 0)) 94 | #ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) 95 | ax.xaxis.set_major_formatter(ticker.FuncFormatter(datefmt(hist.index))) 96 | plt.setp(ax.get_xticklabels(), rotation=30, ha='right') 97 | #plt.axis('equal') 98 | def redraw(event): 99 | #cxaxwdt, cyaxhgt = ax.get_xlim()[1] - ax.get_xlim()[0], ax.get_ylim()[1] - ax.get_ylim()[0] 100 | bbox = ax.get_window_extent()#.transformed(plt.gcf().dpi_scale_trans.inverted()) 101 | drawdim = [bbox.width, bbox.height] 102 | mvisual = (hist.Close.iloc[-3] - hist.Close.iloc[-1]) * drawdim[1] / yaxhgt / (-2 * drawdim[0] / xaxwdt) 103 | intcpt = ((hist.Close.iloc[-3] + hist.Close.iloc[-1]) / 2 - ax.get_ylim()[0]) * drawdim[1] / yaxhgt - (-(drawdim[0] / 2) / mvisual) 104 | #print(drawdim, ann.xyann, mvisual, intcpt, ax.get_xlim()) 105 | ann.xyann = ax.transData.inverted().transform(((drawdim[0] * 0.54)+bbox.x0, (-(drawdim[0] * 0.54)/mvisual + intcpt)+bbox.y0)) 106 | plt.gcf().canvas.draw_idle() 107 | #idx = ax.callbacks.connect('xlim_changed', redraw) 108 | #idy = ax.callbacks.connect('ylim_changed', redraw) 109 | cid = plt.gcf().canvas.mpl_connect('resize_event', redraw) 110 | plt.tight_layout() 111 | #plt.gcf().canvas.draw() 112 | #redraw(None) 113 | #plt.gcf().canvas.draw() 114 | #extent = plt.gcf().get_window_extent(renderer=plt.gcf().canvas.get_renderer()).transformed(plt.gcf().dpi_scale_trans.inverted()) 115 | plt.savefig(os.path.join(curdir, 'data', 'slopeint.svg'), format='svg')#, bbox_inches = extent, pad_inches = 0) 116 | plt.savefig(os.path.join(curdir, 'data', 'slopeint.png'), format='png')#, bbox_inches = extent, pad_inches = 0) 117 | #plt.show() 118 | plt.gcf().canvas.mpl_disconnect(cid) 119 | #ax.callbacks.disconnect(idx) 120 | #ax.callbacks.disconnect(idy) 121 | 122 | def fig_linregrs(): 123 | plt.clf() 124 | plt.rcParams.update({'font.size': 14}) 125 | plt.gcf().set_size_inches(1024/plt.gcf().dpi, 768/plt.gcf().dpi) #plt.gcf().dpi=100 126 | spec = gridspec.GridSpec(ncols=1, nrows=2, figure=plt.gcf(), height_ratios=[3, 1]) 127 | plt.subplot(spec[1, 0]) 128 | plt.axis('off') 129 | plt.gca().get_xaxis().set_visible(False) 130 | plt.gca().get_yaxis().set_visible(False) 131 | plt.annotate(r'Mean of n-Points along x and y-axes: $\bar{x}=\frac{\sum_{i=1}^n{x_i}}{n}, \bar{y}=\frac{\sum_{i=1}^n{y_i}}{n}$' + '\n' + 132 | r'Regression slope: $m=\frac{\sum_{i=1}^n(x_i-\bar{x})(y_i-\bar{y})}{\sum_{i=1}^n(x_i-\bar{x})^2}$' + ' ' + 133 | r'Regression intercept: $b=\bar{y}-m\bar{x}$' + '\n' + 134 | r'Sum of Squared Residuals for expected $y_i$ $(\hat{y}_i)$: $SSR=\sum_{i=1}^n{(y_i-\hat{y}_i)^2}$' + '\n' + 135 | r'Standard Error of Slope: $\sigma_m=\sqrt{\frac{SSR}{(n-2)\sum_{i=1}^n{(x_i-\bar{x})^2}}}$' + ' ' + 136 | r'Standard Error of Intercept: $\sigma_b=\sigma_m\sqrt{\frac{\sum_{i=1}^nx_i^2}{n}}$', (0, 0)) 137 | plt.subplot(spec[0, 0]) 138 | plt.plot(range(len(hist.Close)-3, len(hist.Close)), hist.Close.iloc[-3:], 'bo') 139 | xbar, ybar = (0 + 1 + 2) / 3, (hist.Close.iloc[-3] + hist.Close.iloc[-2] + hist.Close.iloc[-1]) / 3 140 | height = hist.Close.iloc[-3:].max() - hist.Close.iloc[-3:].min() 141 | plt.hlines(ybar, len(hist.Close)-3, len(hist.Close)-1, colors='r', linestyles='--') 142 | plt.annotate(r'$\bar{{x}}=\frac{{{}+{}+{}}}{{{}}}={}$'.format(0, 1, 2, 3, 1), (xbar + len(hist.Close)-3, (hist.Close.iloc[-3:].min() + hist.Close.iloc[-3:].max()) / 2), (xbar + len(hist.Close)-3, hist.Close.iloc[-3:].min()), color='red', va='center', arrowprops={'arrowstyle':'->', 'color':'red'}) 143 | plt.vlines(xbar + len(hist.Close)-3, hist.Close.iloc[-3:].min(), hist.Close.iloc[-3:].max(), colors='r', linestyles='--') 144 | plt.annotate(r'$\bar{{y}}=\frac{{{}+{}+{}}}{{{}}}={}$'.format(hist.Close.iloc[-3], hist.Close.iloc[-2], hist.Close.iloc[-1], 3, round(ybar, 2)), (len(hist.Close)-2, ybar), (len(hist.Close)-1, ybar - height * 0.1), color='red', va='top', ha='right', arrowprops={'arrowstyle':'->', 'color':'red'}) 145 | m = ((0 - xbar) * (hist.Close.iloc[-3] - ybar) + (1 - xbar) * (hist.Close.iloc[-2] - ybar) + (2 - xbar) * (hist.Close.iloc[-1] - ybar)) / (np.square(0-xbar)+np.square(1-xbar)+np.square(2-xbar)) 146 | b = ybar - m * xbar 147 | SSR = np.square(hist.Close.iloc[-3] - (m * 0 + b)) + np.square(hist.Close.iloc[-2] - (m * 1 + b)) + np.square(hist.Close.iloc[-1] - (m * 2 + b)) 148 | err1 = np.sqrt(SSR / ((3 - 2) * (np.square(0-xbar)+np.square(1-xbar)+np.square(2-xbar)))) 149 | err2 = err1*np.sqrt((np.square(0)+np.square(1)+np.square(2))/3) 150 | plt.annotate(r'$\hat{{y}}_0={}*{}+{}={}$'.format(round(m, 2), 0, round(b, 2), round(m*0+b, 2)), (len(hist.Close) - 3, m*0+b), (len(hist.Close) - 3 + 0.1, m*0+b), va='top', arrowprops={'arrowstyle':'->'}) 151 | plt.annotate(r'$\hat{{y}}_1={}*{}+{}={}$'.format(round(m, 2), 1, round(b, 2), round(m*1+b, 2)), (len(hist.Close) - 2, m*1+b), (len(hist.Close) - 2 + 0.15, m*1+b+height*0.01), arrowprops={'arrowstyle':'->'}) 152 | plt.annotate(r'$\hat{{y}}_2={}*{}+{}={}$'.format(round(m, 2), 2, round(b, 2), round(m*2+b, 2)), (len(hist.Close) - 1, m*2+b), (len(hist.Close) - 1 - 0.1, m*2+b+height*0.1), ha='right', arrowprops={'arrowstyle':'->'}) 153 | plt.plot([len(hist.Close) - 3, len(hist.Close) - 3], [hist.Close.iloc[-3], ybar], color='green') 154 | plt.plot([len(hist.Close) - 2, len(hist.Close) - 2], [hist.Close.iloc[-2], ybar], color='green') 155 | plt.plot([len(hist.Close) - 1, len(hist.Close) - 1], [hist.Close.iloc[-1], ybar], color='green') 156 | plt.annotate(r'$y_0-\bar{{y}}={}$'.format(round(hist.Close.iloc[-3] - ybar, 2)), (len(hist.Close) - 3, (hist.Close.iloc[-3] + ybar) / 2 + height * 0.1), (len(hist.Close) - 3 + 0.1, (hist.Close.iloc[-3] + ybar) / 2 + height * 0.1), color='green', va='center', arrowprops={'arrowstyle':'-[', 'color':'green'}) 157 | plt.annotate(r'$y_1-\bar{{y}}={}$'.format(round(hist.Close.iloc[-2] - ybar, 2)), (len(hist.Close) - 2, (hist.Close.iloc[-2] + ybar) / 2 + height * 0.1), (len(hist.Close) - 2 + 0.1, (hist.Close.iloc[-2] + ybar) / 2 + height * 0.1), color='green', va='center', arrowprops={'arrowstyle':'-[', 'color':'green'}) 158 | plt.annotate(r'$y_2-\bar{{y}}={}$'.format(round(hist.Close.iloc[-1] - ybar, 2)), (len(hist.Close) - 1, (hist.Close.iloc[-1] + ybar) / 2), (len(hist.Close) - 1 - 0.1, (hist.Close.iloc[-1] + ybar) / 2), color='green', va='center', ha='right', arrowprops={'arrowstyle':'-[', 'color':'green'}) 159 | plt.plot([len(hist.Close) - 3, len(hist.Close) - 3], [hist.Close.iloc[-3], m*0+b], color='cyan') 160 | plt.plot([len(hist.Close) - 2, len(hist.Close) - 2], [hist.Close.iloc[-2], m*1+b], color='cyan') 161 | plt.plot([len(hist.Close) - 1, len(hist.Close) - 1], [hist.Close.iloc[-1], m*2+b], color='cyan') 162 | plt.annotate(r'$y_0-\hat{{y}}={}$'.format(round(hist.Close.iloc[-3] - (m*0+b), 2)), (len(hist.Close) - 3, (hist.Close.iloc[-3] + m*0+b) / 2), (len(hist.Close) - 3 + 0.1, (hist.Close.iloc[-3] + m*0+b) / 2), color='cyan', va='center', arrowprops={'arrowstyle':'-[', 'color':'cyan'}) 163 | plt.annotate(r'$y_1-\hat{{y}}={}$'.format(round(hist.Close.iloc[-2] - (m*1+b), 2)), (len(hist.Close) - 2, (hist.Close.iloc[-2] + m*1+b) / 2), (len(hist.Close) - 2 + 0.1, (hist.Close.iloc[-2] + m*1+b) / 2), color='cyan', va='center', arrowprops={'arrowstyle':'-[', 'color':'cyan'}) 164 | plt.annotate(r'$y_2-\hat{{y}}={}$'.format(round(hist.Close.iloc[-1] - (m*2+b), 2)), (len(hist.Close) - 1, (hist.Close.iloc[-1] + m*2+b) / 2 - height * 0.05), (len(hist.Close) - 1 - 0.1, (hist.Close.iloc[-1] + m*2+b) / 2 - height * 0.05), color='cyan', va='center', ha='right', arrowprops={'arrowstyle':'-[', 'color':'cyan'}) 165 | plt.annotate((r'$m=\frac{{({}-{})*{}+({}-{})*{}+({}-{})*{}}}{{({}-{})^2+({}-{})^2+({}-{})^2}}$' + '\n' + '=${}$' + '\n' + 166 | r'$SSR={}^2+{}^2+{}^2={}$' + '\n' + 167 | r'$\sigma_m=\sqrt{{\frac{{{}}}{{({}-2)(({}-{})^2+({}-{})^2+({}-{})^2)}}}}={}$' + '\n' + 168 | r'$\sigma_b={}\sqrt{{\frac{{{}^2+{}^2+{}^2}}{{{}}}}}={}$' 169 | ).format(0, round(xbar, 2), round(hist.Close.iloc[-3] - ybar, 2), 1, round(xbar, 2), round(hist.Close.iloc[-2] - ybar, 2), 2, round(xbar, 2), round(hist.Close.iloc[-1] - ybar, 2), 0, round(xbar, 2), 1, round(xbar, 2), 2, round(xbar, 2), round(m, 2), 170 | round(hist.Close.iloc[-3] - (m*0+b), 2), round(hist.Close.iloc[-2] - (m*1+b), 2), round(hist.Close.iloc[-1] - (m*2+b), 2), round(SSR, 2), 171 | round(SSR, 2), 3, 0, round(xbar, 2), 1, round(xbar, 2), 2, round(xbar, 2), round(err1, 2), 172 | round(err1, 2), 0, 1, 2, 3, round(err2, 2)), 173 | (len(hist.Close)-2, m * 1 + b), (len(hist.Close)-1, hist.Close.iloc[-3:].min()), color='blue', va='bottom', ha='right', arrowprops={'arrowstyle':'->', 'color':'blue'}) 174 | plt.annotate(r'$b={}-{}*{}={}$'.format(round(ybar, 2), round(m, 2), xbar, round(b, 2)), (len(hist.Close)-3, b), (len(hist.Close)-3+0.1, b), color='blue', ha='left', arrowprops={'arrowstyle':'->', 'color':'blue'}) 175 | plt.plot([len(hist.Close)-3, len(hist.Close)-1], [b, 2 * m + b]) 176 | ax = plt.gca() 177 | plt.yticks(hist.Close.iloc[-3:]) 178 | plt.title('Closing Price Points Demonstrating Linear Regression') 179 | plt.xlabel('Date') 180 | plt.ylabel('Price') 181 | ax.xaxis.set_major_locator(ticker.IndexLocator(1, 0)) 182 | #ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) 183 | ax.xaxis.set_major_formatter(ticker.FuncFormatter(datefmt(hist.index))) 184 | plt.setp(ax.get_xticklabels(), rotation=30, ha='right') 185 | plt.tight_layout() 186 | #extent = plt.gcf().get_window_extent(renderer=plt.gcf().canvas.get_renderer()).transformed(plt.gcf().dpi_scale_trans.inverted()) 187 | plt.savefig(os.path.join(curdir, 'data', 'linregrs.svg'), format='svg')#, bbox_inches = extent, pad_inches = 0) 188 | plt.savefig(os.path.join(curdir, 'data', 'linregrs.png'), format='png')#, bbox_inches = extent, pad_inches = 0) 189 | #plt.show() 190 | 191 | def fig_hough(): 192 | plt.clf() 193 | plt.rcParams.update({'font.size': 14}) 194 | plt.gcf().set_size_inches(1280/plt.gcf().dpi, 1024/plt.gcf().dpi) #plt.gcf().dpi=100 195 | spec = gridspec.GridSpec(ncols=1, nrows=2, figure=plt.gcf(), height_ratios=[3, 1]) 196 | plt.subplot(spec[1, 0]) 197 | plt.axis('off') 198 | plt.gca().get_xaxis().set_visible(False) 199 | plt.gca().get_yaxis().set_visible(False) 200 | plt.annotate(r'Slope of Perpendicular Line: $m_p=-\frac{1}{m}, mm_p=-1$' + '\n' + 201 | r'Perpencicular Line passing through Point: $y=\frac{x_0-x}{m}+y_0$' + '\n' + 202 | r'Point $(x\prime, y\prime)$ of Intersection of Lines: $mx+b=\frac{x_0-x}{m}+y_0\equiv x\prime=\frac{x_0+my_0-mb}{m^2+1}, y\prime=mx\prime+b$' + '\n' + 203 | r'Distance of Point to Line after simplification: $d=\frac{\left|b+mx_0-y_0\right|}{\sqrt{1 + m^2}}$' + '\n' + 204 | r'$\rho=x \cos \theta+y \sin \theta$ where $\sin \theta=\frac{opposite}{hypotenuse}, \cos \theta=\frac{adjacent}{hypotenuse}$ and $y=\frac{\sin \theta}{\cos \theta}x$ while its perpendicular line is $y=-\frac{\cos \theta}{\sin \theta}x+\frac{\rho}{\sin \theta}$', (0, 0)) 205 | plt.subplot(spec[0, 0]) 206 | plt.plot([len(hist.Close)-10, len(hist.Close)-1], [hist.Close.iloc[-10], hist.Close.iloc[-1]], 'ro') 207 | plt.plot([len(hist.Close)-10, len(hist.Close)-1], [hist.Close.iloc[-10], hist.Close.iloc[-1]], 'k-') 208 | mn, mx = min(hist.Close.iloc[-10], hist.Close.iloc[-1]), max(hist.Close.iloc[-10], hist.Close.iloc[-1]) 209 | plt.plot([len(hist.Close)-10, len(hist.Close)-1], [mn, mx], 'b--') 210 | plt.annotate(r'Diagonal length=$\sqrt{{{}^2+{}^2}}={}$'.format(9, round(mx-mn, 2), round(np.sqrt(np.square(9)+np.square(mx-mn)), 2)), (len(hist.Close)-1, mx), (len(hist.Close)-1-1, mx), ha='right', va='top', color='blue', arrowprops={'arrowstyle':'->', 'color':'blue'}) 211 | #plt.xlim(0, 30) 212 | #plt.ylim(0, 30) 213 | #plt.gca().add_line(plt.Line2D([0, 30], [30, 0])) 214 | #height = hist.Close.iloc[-10:].max() - hist.Close.iloc[-10:].min() 215 | ax = plt.gca() 216 | #plt.ylim(ax.get_ylim()[0] - height * 0.2, ax.get_ylim()[1]) 217 | #plt.xlim(ax.get_xlim()[0] - 4, ax.get_xlim()[1]) 218 | m = (hist.Close.iloc[-10] - hist.Close.iloc[-1]) / (0 - 9) 219 | b = hist.Close.iloc[-10] - m * 0 - mn #+ height * 0.2 220 | plt.annotate(r'$y={}x+{}$'.format(round(m, 2), round(b, 2)), (len(hist.Close)-5.5, (mn+mx)/2), (len(hist.Close)-5.5, mn+(mx-mn)*0.7), arrowprops={'arrowstyle':'->'}) 221 | #axes origin is (len(hist.Close)-10, hist.Close.iloc[-10:].min()-height*0.2) 222 | bperp = 0 #hist.Close.iloc[-10:].min() - (-1/m * 0) 223 | #y0=mx0+b, y0=-x0/m+bperp, mx0+b=-x0/m+bperp, m^2x0+m(b-bperp)=-x0, x0(m^2+1)=m(bperp-b), x0=m(bperp-b)/(m^2+1) =(bperp-b)/(m-(-1/m))=(bperb-b)/((m^2+1)/m) 224 | x0 = (m * (bperp - b)) / (m*m+1) 225 | angle = np.arctan((-x0/m+bperp) / (x0)) 226 | #print((angle * 180 / np.pi, height, m, b, -1/m, bperp, x0, -x0/m+bperp, x0*m+b, np.abs(b)/np.sqrt(1+m*m))) 227 | plt.annotate('', (len(hist.Close)-10, mn), (len(hist.Close)-10 + x0, mn + -x0/m + bperp), arrowprops=dict(arrowstyle="<|-", color='red')) 228 | plt.gca().add_patch(mpatches.Wedge((len(hist.Close)-10 + x0, mn + -x0/m + bperp), 1, angle * 180 / np.pi - 180, angle * 180 / np.pi - 90, fill=False)) 229 | plt.gca().add_patch(mpatches.Wedge((len(hist.Close)-10 + x0, mn + -x0/m + bperp), 0.5, angle * 180 / np.pi - 270, angle * 180 / np.pi - 180, fill=False)) 230 | plt.annotate(r'$90\circ$', (len(hist.Close)-10 + x0 - 1, mn + -x0/m + bperp - 2)) 231 | plt.annotate(r'$90\circ$', (len(hist.Close)-10 + x0 - 1, mn + -x0/m + bperp + 1)) 232 | plt.gca().add_patch(mpatches.Wedge((len(hist.Close)-10, mn), 3, 0, angle * 180 / np.pi, fill=False)) 233 | plt.annotate(r'$\theta={}^\circ$'.format(round(angle * 180/np.pi, 2)), (len(hist.Close) - 6.75, mn+0.1)) 234 | plt.gca().add_patch(mpatches.Wedge((len(hist.Close)-10, mx), 3, 270, 270 + angle * 180 / np.pi, fill=False)) 235 | plt.annotate(r'$\theta$', (len(hist.Close)-9.5, mx-5)) 236 | plt.annotate((r'$\rho=\frac{{\left|{}+{}*{}-{}\right|}}{{\sqrt{{1 + {}^2}}}}$' + '\n' + '$={}\cos {}+{}\sin {}$' + '\n' + '$={}\cos {}+{}\sin {}$' + '\n' + '$={}$').format( 237 | round(b, 2), round(m, 2), 0, 0, round(m, 2), 238 | 0, round(angle*180/np.pi, 2), round(hist.Close.iloc[-10]-mn, 2), round(angle*180/np.pi, 2), 9, round(angle*180/np.pi, 2), hist.Close.iloc[-1]-mn, round(angle*180/np.pi, 2), round(0 * np.cos(angle) + (hist.Close.iloc[-10]-mn) * np.sin(angle), 2)), 239 | (len(hist.Close)-10+x0/2, mn + (-x0/m + bperp) / 2), (len(hist.Close)-10+x0/2, mn + (-x0/m + bperp) / 2+0.9), ha='center', color='red', arrowprops=dict(arrowstyle="->", color='red')) 240 | plt.plot([len(hist.Close)-10, len(hist.Close)-1], [mn, mn], 'k-') 241 | plt.plot([len(hist.Close)-10, len(hist.Close)-10], [mn, mx], 'k-') 242 | plt.annotate('{}'.format(9), (len(hist.Close)-5.5, mn), (len(hist.Close)-5.5, mn+0.5), ha='center', arrowprops=dict(arrowstyle="->")) 243 | plt.annotate('{}'.format(round(mx-mn, 2)), (len(hist.Close)-10, (mn+mx)/2), (len(hist.Close)-10+0.5, (mn+mx)/2), ha='left', arrowprops=dict(arrowstyle="->")) 244 | plt.yticks([hist.Close.iloc[-10], hist.Close.iloc[-1]]) 245 | plt.title('Closing Price Points Demonstrating Hough transform accumulation of rho-theta for 2 point line') 246 | plt.xlabel('Date') 247 | plt.ylabel('Price') 248 | ax.xaxis.set_major_locator(ticker.IndexLocator(1, 0)) 249 | #ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) 250 | ax.xaxis.set_major_formatter(ticker.FuncFormatter(datefmt(hist.index))) 251 | plt.setp(ax.get_xticklabels(), rotation=30, ha='right') 252 | plt.tight_layout() 253 | 254 | #extent = ann.get_window_extent(renderer=plt.gcf().canvas.get_renderer()).transformed(plt.gcf().dpi_scale_trans.inverted()) 255 | plt.savefig(os.path.join(curdir, 'data', 'hough.svg'), format='svg')#, bbox_inches = extent, pad_inches = 0) 256 | plt.savefig(os.path.join(curdir, 'data', 'hough.png'), format='png')#, bbox_inches = extent, pad_inches = 0) 257 | 258 | #plt.show() 259 | def fig_minima(): 260 | h = hist[-10:] 261 | mins, maxs = calc_support_resistance(h.Close) 262 | minimaIdxs, pmin, mintrend, minwindows = mins 263 | maximaIdxs, pmax, maxtrend, maxwindows = maxs 264 | plt.clf() 265 | plt.gcf().set_size_inches(1024/plt.gcf().dpi, 768/plt.gcf().dpi) #plt.gcf().dpi=100 266 | plt.subplot(111) 267 | plt.plot(minimaIdxs, [h.Close.iloc[x] for x in minimaIdxs], 'yo', label='Minima') 268 | plt.plot(maximaIdxs, [h.Close.iloc[x] for x in maximaIdxs], 'bo', label='Maxima') 269 | from findiff import FinDiff 270 | dx = 1 #grid scale could be amplified with pennies 0.01 271 | d_dx = FinDiff(0, dx, 1) #acc=3 #for 5-point stencil, currenly uses +/-1 day only 272 | d2_dx2 = FinDiff(0, dx, 2) #acc=3 #for 5-point stencil, currenly uses +/-1 day only 273 | clarr = np.asarray(h.Close) 274 | mom = d_dx(clarr) 275 | momacc = d2_dx2(clarr) 276 | for x in range(len(h)): 277 | ann = plt.gca().annotate('{}\n'.format(round(mom[x], 2), round(momacc[x], 2)), (x, h.Close.iloc[x]), (x, h.Close.iloc[x] - (h.Close.max() - h.Close.min()) / 3), ha='center') #bbox=dict(boxstyle='round', fc='gray', alpha=0.3) 278 | #plt.rcParams['font.size'] 279 | ext = ann.get_window_extent(renderer = plt.gcf().canvas.get_renderer()).transformed(plt.gca().transData.inverted()) 280 | ann = plt.gca().annotate('{}'.format(round(momacc[x], 2)), (x, ext.y0), ha='center', va='bottom', color='r') 281 | next = ann.get_window_extent(renderer = plt.gcf().canvas.get_renderer()).transformed(plt.gca().transData.inverted()) 282 | patch = plt.gca().add_artist(mpatches.FancyBboxPatch((min(ext.x0, next.x0)+0.1, ext.y0), width=max(ext.width, next.width)-0.2, height=ext.height+3, boxstyle='round', fc='gray', alpha=0.3)) 283 | plt.gca().annotate(''.format(round(mom[x], 2), round(momacc[x], 2)), (x, h.Close.iloc[x]), (x, ext.y1+3), arrowprops={'arrowstyle':'-|>'}) 284 | plt.ylim(h.Close.min() - (h.Close.max() - h.Close.min()) / 2.8, h.Close.max() + (h.Close.max() - h.Close.min()) / 10) 285 | p1 = mpatches.Patch(color='black', label='Velocity') 286 | p2 = mpatches.Patch(color='red', label='Acceleration') 287 | plt.plot(range(len(h.index)), h.Close, 'g--', label='Close Price') 288 | plt.xlim(plt.gca().get_xlim()[0] - 0.5, plt.gca().get_xlim()[1] + 0.5) 289 | plt.title('Closing Price with Pivot Points, Momentum, Acceleration') 290 | plt.xlabel('Date') 291 | plt.ylabel('Price') 292 | plt.legend(handles=plt.gca().get_legend_handles_labels()[0] + [p1, p2]) 293 | plt.gca().xaxis.set_major_locator(ticker.MaxNLocator(6)) 294 | #plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) 295 | plt.gca().xaxis.set_major_formatter(ticker.FuncFormatter(datefmt(h.index))) 296 | plt.setp(plt.gca().get_xticklabels(), rotation=30, ha='right') 297 | plt.savefig(os.path.join(curdir, 'data', 'extrema.svg'), format='svg', bbox_inches = 'tight') 298 | plt.savefig(os.path.join(curdir, 'data', 'extrema.png'), format='png', bbox_inches = 'tight') 299 | #plt.show() 300 | def fig_reimann(): 301 | mins, maxs = calc_support_resistance(hist[-250:].Close, sortError = True) 302 | minimaIdxs, pmin, mintrend, minwindows = mins 303 | maximaIdxs, pmax, maxtrend, maxwindows = maxs 304 | plt.clf() 305 | plt.gcf().set_size_inches(800/plt.gcf().dpi, 720/plt.gcf().dpi) #plt.gcf().dpi=100 306 | plt.subplot(211) 307 | plt.title('Closing Price with Resistance and Area') 308 | plt.xlabel('Date') 309 | plt.ylabel('Price') 310 | trendline = maxtrend[0] 311 | base = trendline[0][0] 312 | m, b, ser = trendline[1][0], trendline[1][1], hist[-250:][base:trendline[0][-1]+1].Close 313 | plt.plot(range(base, trendline[0][-1]+1), hist[-250:][base:trendline[0][-1]+1].Close, 'b-', label='Price') 314 | plt.plot((base, trendline[0][-1]+1), (m * base + b, m * (trendline[0][-1]+1) + b), 'r-', label='Resistance') 315 | plt.gca().xaxis.set_major_locator(ticker.MaxNLocator(6)) 316 | #plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) 317 | plt.gca().xaxis.set_major_formatter(ticker.FuncFormatter(datefmt(hist[-250:].index))) 318 | plt.setp(plt.gca().get_xticklabels(), rotation=30, ha='right') 319 | plt.legend() 320 | plt.subplot(212) 321 | plt.ylabel('Price Difference from Trend') 322 | #plt.plot(hist.Close.iloc[-250:]) 323 | isMin = False 324 | S = sum([max(0, (m * (x+base) + b) - y if isMin else y - (m * (x+base) + b)) for x, y in enumerate(ser)]) 325 | area = S / len(ser) 326 | for x, y in enumerate(ser): 327 | plt.bar(x, (m * (x+base) + b) - y if isMin else y - (m * (x+base) + b), color='r' if (y < (m * (x+base) + b) if isMin else y > (m * (x+base) + b)) else 'gray') 328 | 329 | plt.annotate(r'S={}, $\frac{{{}}}{{{}}}$={}$\frac{{\$}}{{day}}$'.format(round(S, 2), round(S, 2), len(range(base, trendline[0][-1]+1)), round(area, 2)) + '\n' + r'Reimann Sum where $\Delta x=x_i-x_{i-1}$,' + '\n' + '$x_i^* \in [x_{i-1}, x_i]$: $S=\sum_{i=1}^n{f(x_i^*)\Delta x_i}$', (0, plt.gca().get_ylim()[0]+5), va='bottom') 330 | plt.gca().xaxis.set_major_locator(ticker.MaxNLocator(6)) 331 | #plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) 332 | plt.gca().xaxis.set_major_formatter(ticker.FuncFormatter(datefmt(hist[-250:].index))) 333 | plt.setp(plt.gca().get_xticklabels(), rotation=30, ha='right') 334 | plt.savefig(os.path.join(curdir, 'data', 'reimann.svg'), format='svg', bbox_inches = 'tight') 335 | plt.savefig(os.path.join(curdir, 'data', 'reimann.png'), format='png', bbox_inches = 'tight') 336 | #plt.show() 337 | def fig_suppres(): 338 | plot_sup_res_date(hist[-250:].Close, hist[-250:].index, fromwindows=False, sortError = True) 339 | plt.savefig(os.path.join(curdir, 'data', 'suppreserr.svg'), format='svg', bbox_inches = 'tight') 340 | plt.savefig(os.path.join(curdir, 'data', 'suppreserr.png'), format='png', bbox_inches = 'tight') 341 | plot_sup_res_date(hist[-250:].Close, hist[-250:].index, fromwindows=False) 342 | plt.savefig(os.path.join(curdir, 'data', 'suppres.svg'), format='svg', bbox_inches = 'tight') 343 | plt.savefig(os.path.join(curdir, 'data', 'suppres.png'), format='png', bbox_inches = 'tight') 344 | 345 | import matplotlib.gridspec as gridspec 346 | import matplotlib.patches as mpatches 347 | sz = plt.gcf().get_size_inches() 348 | fig_slopeint() 349 | fig_linregrs() 350 | fig_hough() 351 | fig_minima() 352 | fig_reimann() 353 | plt.gcf().set_size_inches(sz) 354 | fig_suppres() 355 | def test_sup_res(curdir): 356 | data = [0, 1, 2, 3, 2, 1, 0, 1, 2, 3, 2, 1, 0, 1, 2, 3, 2, 1, 0, 1, 2, 3, 2, 1, 0] 357 | data = [float(x) for x in data] 358 | result = (([6, 12, 18], [0.0, 0.0], [([6, 12, 18], (0.0, 0.0, 0.0, 0.0, 0.0, 0.0))], [[([6, 12, 18], (0.0, 0.0, 0.0, 0.0, 0.0, 0.0))]]), ([3, 9, 15, 21], [0.0, 3.0], [([3, 9, 15, 21], (0.0, 3.0, 0.0, 0.0, 0.0, 0.0))], [[([3, 9, 15, 21], (0.0, 3.0, 0.0, 0.0, 0.0, 0.0))]])) 359 | assert result == calc_support_resistance(data, extmethod=METHOD_NAIVE) 360 | assert result == calc_support_resistance(data, extmethod=METHOD_NAIVECONSEC) 361 | assert result == calc_support_resistance(data) 362 | assert result == calc_support_resistance(np.array(data)) 363 | import pandas as pd 364 | assert result == calc_support_resistance(pd.Series(data)) 365 | assert result == calc_support_resistance(data, method=METHOD_NCUBED) 366 | assert result == calc_support_resistance(data, method=METHOD_HOUGHPOINTS) 367 | assert result == calc_support_resistance(data, method=METHOD_HOUGHLINES) 368 | assert result == calc_support_resistance(data, method=METHOD_PROBHOUGH) 369 | data = [0, 1, 2, 3, 2, 1, 1, 1, 2, 4, 3, 2, 2, 2, 3, 5, 4, 3, 3, 3, 4, 6, 5, 4, 4] 370 | data = [float(x) for x in data] 371 | result = (([], [np.nan, np.nan], [], [[]]), ([3, 9, 15, 21], [0.16666666666666677, 2.499999999999998], [([3, 9, 15, 21], (0.16666666666666666, 2.5, 0.0, 0.0, 0.0, 0.0))], [[([3, 9, 15, 21], (0.16666666666666666, 2.5, 0.0, 0.0, 0.0, 0.0))]])) 372 | assert result == calc_support_resistance(data, extmethod=METHOD_NAIVE) 373 | result = (([7, 13, 19], [0.1666666666666666, -0.1666666666666652], [([7, 13, 19], (0.16666666666666666, -0.16666666666666652, 0.0, 0.0, 0.0, 0.0))], [[([7, 13, 19], (0.16666666666666666, -0.16666666666666652, 0.0, 0.0, 0.0, 0.0))]]), ([3, 9, 15, 21], [0.16666666666666677, 2.499999999999998], [([3, 9, 15, 21], (0.16666666666666666, 2.5, 0.0, 0.0, 0.0, 0.0))], [[([3, 9, 15, 21], (0.16666666666666666, 2.5, 0.0, 0.0, 0.0, 0.0))]])) 374 | assert result == calc_support_resistance(data, extmethod=METHOD_NAIVECONSEC) 375 | result = (([23], [np.nan, np.nan], [], [[]]), ([3, 9, 15, 21], [0.16666666666666677, 2.499999999999998], [([3, 9, 15, 21], (0.16666666666666666, 2.5, 0.0, 0.0, 0.0, 0.0))], [[([3, 9, 15, 21], (0.16666666666666666, 2.5, 0.0, 0.0, 0.0, 0.0))]])) 376 | assert result == calc_support_resistance(data) 377 | assert result == calc_support_resistance(data, method=METHOD_NCUBED) 378 | assert result == calc_support_resistance(data, method=METHOD_HOUGHPOINTS) 379 | assert result == calc_support_resistance(data, method=METHOD_HOUGHLINES) 380 | assert result == calc_support_resistance(data, method=METHOD_PROBHOUGH) 381 | import yfinance as yf #pip install yfinance 382 | tick = yf.Ticker('^GSPC') 383 | hist = tick.history(period="max", rounding=True) 384 | plot_sup_res_learn(curdir, hist) 385 | import matplotlib.pyplot as plt 386 | plot_sup_res_date(hist[-250:].Close, hist[-250:].index) 387 | plt.show() 388 | plot_sup_res_date((hist[-250:].Close, None), hist[-250:].index) 389 | plt.show() 390 | plot_sup_res_date((None, hist[-250:].Close), hist[-250:].index) 391 | plt.show() 392 | plot_sup_res_date((hist[-250:].Low, hist[-250:].High), hist[-250:].index) 393 | plt.show() 394 | plt.clf() 395 | return None 396 | METHOD_NAIVE, METHOD_NAIVECONSEC, METHOD_NUMDIFF = 0, 1, 2 397 | METHOD_NCUBED, METHOD_NSQUREDLOGN, METHOD_HOUGHPOINTS, METHOD_HOUGHLINES, METHOD_PROBHOUGH = 0, 1, 2, 3, 4 398 | def check_num_alike(h): 399 | if type(h) is list and all([isinstance(x, (bool, int, float)) for x in h]): return True 400 | elif type(h) is np.ndarray and h.ndim==1 and h.dtype.kind in 'biuf': return True 401 | else: 402 | import pandas as pd 403 | if type(h) is pd.Series and h.dtype.kind in 'biuf': return True 404 | else: return False 405 | def get_extrema(h, extmethod=METHOD_NUMDIFF, accuracy=1): 406 | #h must be single dimensional array-like object e.g. List, np.ndarray, pd.Series 407 | if type(h) is tuple and len(h) == 2 and (h[0] is None or check_num_alike(h[0])) and (h[1] is None or check_num_alike(h[1])) and (not h[0] is None or not h[1] is None): 408 | hmin, hmax = h[0], h[1] 409 | if not h[0] is None and not h[1] is None and len(hmin) != len(hmax): #not strict requirement, but contextually ideal 410 | raise ValueError('h does not have a equal length minima and maxima data') 411 | elif check_num_alike(h): hmin, hmax = None, None 412 | else: raise ValueError('h is not list, numpy ndarray or pandas Series of numeric values or a 2-tuple thereof') 413 | if extmethod == METHOD_NAIVE: 414 | #naive method 415 | import pandas as pd 416 | def get_minmax(h): 417 | rollwin = pd.Series(h).rolling(window=3, min_periods=1, center=True) 418 | minFunc = lambda x: len(x) == 3 and x.iloc[0] > x.iloc[1] and x.iloc[2] > x.iloc[1] 419 | maxFunc = lambda x: len(x) == 3 and x.iloc[0] < x.iloc[1] and x.iloc[2] < x.iloc[1] 420 | numdiff_extrema = lambda func: np.flatnonzero(rollwin.aggregate(func)).tolist() 421 | return minFunc, maxFunc, numdiff_extrema 422 | elif extmethod == METHOD_NAIVECONSEC: 423 | #naive method collapsing duplicate consecutive values 424 | import pandas as pd 425 | def get_minmax(h): 426 | hist = pd.Series(h) 427 | rollwin = hist.loc[hist.shift(-1) != hist].rolling(window=3, center=True) 428 | minFunc = lambda x: x.iloc[0] > x.iloc[1] and x.iloc[2] > x.iloc[1] 429 | maxFunc = lambda x: x.iloc[0] < x.iloc[1] and x.iloc[2] < x.iloc[1] 430 | def numdiff_extrema(func): 431 | x = rollwin.aggregate(func) 432 | return x[x == 1].index.tolist() 433 | return minFunc, maxFunc, numdiff_extrema 434 | elif extmethod == METHOD_NUMDIFF: 435 | #pip install findiff 436 | from findiff import FinDiff 437 | dx = 1 #1 day interval 438 | d_dx = FinDiff(0, dx, 1, acc=accuracy) #acc=3 #for 5-point stencil, currenly uses +/-1 day only 439 | d2_dx2 = FinDiff(0, dx, 2, acc=accuracy) #acc=3 #for 5-point stencil, currenly uses +/-1 day only 440 | def get_minmax(h): 441 | clarr = np.asarray(h, dtype=np.float64) 442 | mom, momacc = d_dx(clarr), d2_dx2(clarr) 443 | #print(mom[-10:], momacc[-10:]) 444 | #numerical derivative will yield prominent extrema points only 445 | def numdiff_extrema(func): 446 | return [x for x in range(len(mom)) 447 | if func(x) and 448 | (mom[x] == 0 or #either slope is 0, or it crosses from positive to negative with the closer to 0 of the two chosen or prior if a tie 449 | (x != len(mom) - 1 and (mom[x] > 0 and mom[x+1] < 0 and h[x] >= h[x+1] or #mom[x] >= -mom[x+1] 450 | mom[x] < 0 and mom[x+1] > 0 and h[x] <= h[x+1]) or #-mom[x] >= mom[x+1]) or 451 | x != 0 and (mom[x-1] > 0 and mom[x] < 0 and h[x-1] < h[x] or #mom[x-1] < -mom[x] or 452 | mom[x-1] < 0 and mom[x] > 0 and h[x-1] > h[x])))] #-mom[x-1] < mom[x])))] 453 | return lambda x: momacc[x] > 0, lambda x: momacc[x] < 0, numdiff_extrema 454 | else: raise ValueError('extmethod must be METHOD_NAIVE, METHOD_NAIVECONSEC, METHOD_NUMDIFF') 455 | if hmin is None and hmax is None: 456 | minFunc, maxFunc, numdiff_extrema = get_minmax(h) 457 | return numdiff_extrema(minFunc), numdiff_extrema(maxFunc) 458 | if not hmin is None: 459 | minf = get_minmax(hmin) 460 | if hmax is None: return minf[2](minf[0]) 461 | if not hmax is None: 462 | maxf = get_minmax(hmax) 463 | if hmin is None: return maxf[2](maxf[1]) 464 | return minf[2](minf[0]), maxf[2](maxf[1]) 465 | 466 | #returns (list of minima indexes, list of maxima indexes, [support slope coefficient, intersect], [resistance slope coefficient, intersect], [[support point indexes], (slope, intercept, residual, slope error, intercept error, area on wrong side of trend line per time unit)] 467 | def calc_support_resistance(h, extmethod = METHOD_NUMDIFF, method=METHOD_NSQUREDLOGN, 468 | window=125, errpct=0.005, hough_scale=0.01, hough_prob_iter=10, 469 | sortError=False, accuracy=1): 470 | if not type(window) is int: 471 | raise ValueError('window must be of type int') 472 | if not type(errpct) is float: 473 | raise ValueError('errpct must be of type float') 474 | if not type(hough_scale) is float: 475 | raise ValueError('house_scale must be of type float') 476 | if not type(hough_prob_iter) is int: 477 | raise ValueError('house_prob_iter must be of type int') 478 | if not type(sortError) is bool: 479 | raise ValueError('sortError must be True of False') 480 | #h = hist.Close.tolist() 481 | if type(h) is tuple and len(h) == 2 and (h[0] is None or check_num_alike(h[0])) and (h[1] is None or check_num_alike(h[1])) and (not h[0] is None or not h[1] is None): 482 | if not h[0] is None and not h[1] is None and len(h[0]) != len(h[1]): #not strict requirement, but contextually ideal 483 | raise ValueError('h does not have a equal length minima and maxima data') 484 | hmin, hmax, len_h = h[0], h[1], len(h[1 if h[0] is None else 0]) 485 | elif check_num_alike(h): hmin, hmax, len_h = None, None, len(h) 486 | else: raise ValueError('h is not list, numpy ndarray or pandas Series of numeric values or a 2-tuple thereof') 487 | #https://stackoverflow.com/questions/8587047/support-resistance-algorithm-technical-analysis/8590007#8590007 488 | #print((minimaIdxs[-10:], maximaIdxs[-10:])) 489 | #https://en.wikipedia.org/wiki/Trend_line_(technical_analysis) 490 | #It is formed when a diagonal line can be drawn between a minimum of three or more price pivot points. A line can be drawn between any two points, but it does not qualify as a trend line until tested. Hence the need for the third point, the test. 491 | #Given N points on the plane, what is an efficient algorithm to find all the sets of 3 or more collinear points? 492 | #also principle of optimality - for each point, calculate angle formed by every other group of 2 points, check for 180 degree angled points also O(n^3) 493 | #sort the points in some order, and find slope for selected origin point to all other points, sort and check if identical slopes, move on O(n^2*log(n)) 494 | #Hough transform solves with linear complexity 495 | #https://en.wikipedia.org/wiki/Regression_analysis#Linear_regression 496 | #line of best fit using least squared method: 497 | #Xbar=sum of all x over number of x, Ybar=sum of all y over number of y 498 | #m=sum((x-Xbar) * (y-Ybar) for all (x, y)) / sum((x-Xbar)^2 for all x), b=Ybar-m*Xbar 499 | #standard error of regression of slope: sqrt(sum((y-yexpected)^2 for all y) / (n-2)) / sqrt(sum((x-Xbar)^2 for all x)) 500 | #standard error of regression of intercept: (standard error of regression of slope) * sqrt(sum(x^2 for all x)/n) 501 | def get_bestfit3(x0, y0, x1, y1, x2, y2): 502 | xbar, ybar = (x0 + x1 + x2) / 3, (y0 + y1 + y2) / 3 503 | xb0, yb0, xb1, yb1, xb2, yb2 = x0-xbar, y0-ybar, x1-xbar, y1-ybar, x2-xbar, y2-ybar 504 | xs = xb0*xb0+xb1*xb1+xb2*xb2 505 | m = (xb0*yb0+xb1*yb1+xb2*yb2) / xs 506 | b = ybar - m * xbar 507 | ys0, ys1, ys2 = (y0 - (m * x0 + b)),(y1 - (m * x1 + b)),(y2 - (m * x2 + b)) 508 | ys = ys0*ys0+ys1*ys1+ys2*ys2 509 | ser = np.sqrt(ys / xs) 510 | return m, b, ys, ser, ser * np.sqrt((x0*x0+x1*x1+x2*x2)/3) 511 | def get_bestfit(pts): 512 | xbar, ybar = [sum(x) / len(x) for x in zip(*pts)] 513 | def subcalc(x, y): 514 | tx, ty = x - xbar, y - ybar 515 | return tx * ty, tx * tx, x * x 516 | (xy, xs, xx) = [sum(q) for q in zip(*[subcalc(x, y) for x, y in pts])] 517 | m = xy / xs 518 | b = ybar - m * xbar 519 | ys = sum([np.square(y - (m * x + b)) for x, y in pts]) 520 | ser = np.sqrt(ys / ((len(pts) - 2) * xs)) 521 | return m, b, ys, ser, ser * np.sqrt(xx / len(pts)) 522 | def get_trend(Idxs, h, fltpct, min_h, max_h): 523 | trend = [] 524 | for x in range(len(Idxs)): #unfortunately an O(n(n-1)(n-2))=O((n^2-n)(n-2))=O(n^3-3n^2-2n)~=O(n^3) algorithm but meets the strict definition of a trendline 525 | for y in range(x+1, len(Idxs)): 526 | #slope = (h[Idxs[x]] - h[Idxs[y]]) / (Idxs[x] - Idxs[y]) #m=dy/dx #if slope 0 then intercept does not exist constant y where y=b 527 | #intercept = h[Idxs[x]] - slope * Idxs[x] #y=mx+b, b=y-mx 528 | for z in range(y+1, len(Idxs)): 529 | #distance = abs(slope * Idxs[z] + intercept - h[Idxs[z]]) #distance to y value based on x with slope-intercept 530 | trend.append(([Idxs[x], Idxs[y], Idxs[z]], get_bestfit3(Idxs[x], h[Idxs[x]], Idxs[y], h[Idxs[y]], Idxs[z], h[Idxs[z]]))) 531 | return list(filter(lambda val: val[1][3] <= fltpct, trend)) 532 | def get_trend_opt(Idxs, h, fltpct, min_h, max_h): 533 | slopes, trend = [], [] 534 | for x in range(len(Idxs)): #O(n^2*log n) algorithm 535 | slopes.append([]) 536 | for y in range(x+1, len(Idxs)): 537 | slope = (h[Idxs[x]] - h[Idxs[y]]) / (Idxs[x] - Idxs[y]) #m=dy/dx #if slope 0 then intercept does not exist constant y where y=b 538 | #intercept = h[Idxs[x]] - slope * Idxs[x] #y=mx+b, b=y-mx 539 | slopes[x].append((slope, y)) 540 | for x in range(len(Idxs)): 541 | slopes[x].sort() #key=lambda val: val[0]) 542 | CurIdxs = [Idxs[x]] 543 | for y in range(0, len(slopes[x])): 544 | #distance = abs(slopes[x][y][2] * slopes[x][y+1][1] + slopes[x][y][3] - h[slopes[x][y+1][1]]) 545 | CurIdxs.append(Idxs[slopes[x][y][1]]) 546 | if len(CurIdxs) < 3: continue 547 | res = get_bestfit([(p, h[p]) for p in CurIdxs]) 548 | if res[3] <= fltpct: 549 | CurIdxs.sort() 550 | if len(CurIdxs) == 3: 551 | trend.append((CurIdxs, res)) 552 | CurIdxs = list(CurIdxs) 553 | else: CurIdxs, trend[-1] = list(CurIdxs), (CurIdxs, res) 554 | #if len(CurIdxs) >= MaxPts: CurIdxs = [CurIdxs[0], CurIdxs[-1]] 555 | else: CurIdxs = [CurIdxs[0], CurIdxs[-1]] #restart search from this point 556 | return trend 557 | def make_image(Idxs, h, min_h, max_h): 558 | #np.arctan(2/len_h), np.arctan(2/int((hist.Close.max() - m + 1) * (1/hough_scale))) #minimal angles to find all points 559 | max_size = int(np.ceil(2/np.tan(np.pi / (360 * 5)))) #~1146 560 | m, tested_angles = min_h, np.linspace(-np.pi / 2, np.pi / 2, 360*5) #degree of precision from 90 to 270 degrees with 360*5 increments 561 | height = int((max_h - m + 0.01) * (1/hough_scale)) 562 | mx = min(max_size, height) 563 | scl = (1/hough_scale) * mx / height 564 | image = np.zeros((mx, len_h)) #in rows, columns or y, x image format 565 | for x in Idxs: 566 | image[int((h[x] - m) * scl), x] = 255 567 | return image, tested_angles, scl, m 568 | def find_line_pts(Idxs, x0, y0, x1, y1, h, fltpct): 569 | s = (y0 - y1) / (x0 - x1) 570 | i, dnm = y0 - s * x0, np.sqrt(1 + s*s) 571 | dist = [(np.abs(i+s*x-h[x])/dnm, x) for x in Idxs] 572 | dist.sort() #(key=lambda val: val[0]) 573 | pts, res = [], None 574 | for x in range(len(dist)): 575 | pts.append((dist[x][1], h[dist[x][1]])) 576 | if len(pts) < 3: continue 577 | r = get_bestfit(pts) 578 | if r[3] > fltpct: 579 | pts = pts[:-1] 580 | break 581 | res = r 582 | pts = [x for x, _ in pts] 583 | pts.sort() 584 | return pts, res 585 | def hough_points(pts, width, height, thetas): 586 | diag_len = int(np.ceil(np.sqrt(width * width + height * height))) 587 | rhos = np.linspace(-diag_len, diag_len, diag_len * 2.0) 588 | # Cache some resuable values 589 | cos_t = np.cos(thetas) 590 | sin_t = np.sin(thetas) 591 | num_thetas = len(thetas) 592 | # Hough accumulator array of theta vs rho 593 | accumulator = np.zeros((2 * diag_len, num_thetas), dtype=np.uint64) 594 | # Vote in the hough accumulator 595 | for i in range(len(pts)): 596 | x, y = pts[i] 597 | for t_idx in range(num_thetas): 598 | # Calculate rho. diag_len is added for a positive index 599 | rho = int(round(x * cos_t[t_idx] + y * sin_t[t_idx])) + diag_len 600 | accumulator[rho, t_idx] += 1 601 | return accumulator, thetas, rhos 602 | def houghpt(Idxs, h, fltpct, min_h, max_h): 603 | max_size = int(np.ceil(2/np.tan(np.pi / (360 * 5)))) #~1146 604 | m, tested_angles = min_h, np.linspace(-np.pi / 2, np.pi / 2, 360*5) #degree of precision from 90 to 270 degrees with 360*5 increments 605 | height = int((max_h - m + 1) * (1/hough_scale)) 606 | mx = min(max_size, height) 607 | scl = (1/hough_scale) * mx / height 608 | acc, theta, d = hough_points([(x, int((h[x] - m) * scl)) for x in Idxs], mx, len_h, np.linspace(-np.pi / 2, np.pi / 2, 360*5)) 609 | origin, lines = np.array((0, len_h)), [] 610 | for x, y in np.argwhere(acc >= 3): 611 | dist, angle = d[x], theta[y] 612 | y0, y1 = (dist - origin * np.cos(angle)) / np.sin(angle) 613 | y0, y1 = y0 / scl + m, y1 / scl + m 614 | pts, res = find_line_pts(Idxs, 0, y0, len_h, y1, h, fltpct) 615 | if len(pts) >= 3: lines.append((pts, res)) 616 | return lines 617 | def hough(Idxs, h, fltpct, min_h, max_h): 618 | image, tested_angles, scl, m = make_image(Idxs, h, min_h, max_h) 619 | from skimage.transform import hough_line, hough_line_peaks 620 | hl, theta, d = hough_line(image, theta=tested_angles) 621 | origin, lines = np.array((0, image.shape[1])), [] 622 | for pts, angle, dist in zip(*hough_line_peaks(hl, theta, d, threshold=2)): #> threshold 623 | y0, y1 = (dist - origin * np.cos(angle)) / np.sin(angle) 624 | y0, y1 = y0 / scl + m, y1 / scl + m 625 | pts, res = find_line_pts(Idxs, 0, y0, image.shape[1], y1, h, fltpct) 626 | if len(pts) >= 3: lines.append((pts, res)) 627 | return lines 628 | def prob_hough(Idxs, h, fltpct, min_h, max_h): 629 | image, tested_angles, scl, m = make_image(Idxs, h, min_h, max_h) 630 | from skimage.transform import probabilistic_hough_line 631 | lines = [] 632 | for x in range(hough_prob_iter): 633 | lines.extend(probabilistic_hough_line(image, threshold=2, theta=tested_angles, line_length=0, 634 | line_gap=int(np.ceil(np.sqrt(np.square(image.shape[0]) + np.square(image.shape[1])))))) 635 | l = [] 636 | for (x0, y0), (x1, y1) in lines: 637 | if x0 == x1: continue 638 | if x1 < x0: (x0, y0), (x1, y1) = (x1, y1), (x0, y0) 639 | y0, y1 = y0 / scl + m, y1 / scl + m 640 | pts, res = find_line_pts(Idxs, x0, y0, x1, y1, h, fltpct) 641 | if len(pts) >= 3: l.append((pts, res)) 642 | return l 643 | def merge_lines(Idxs, trend, h, fltpct): 644 | for x in Idxs: 645 | l = [] 646 | for i, (p, r) in enumerate(trend): 647 | if x in p: l.append((r[0], i)) 648 | l.sort() #key=lambda val: val[0]) 649 | if len(l) > 1: CurIdxs = list(trend[l[0][1]][0]) 650 | for (s, i) in l[1:]: 651 | CurIdxs += trend[i][0] 652 | CurIdxs = list(dict.fromkeys(CurIdxs)) 653 | CurIdxs.sort() 654 | res = get_bestfit([(p, h[p]) for p in CurIdxs]) 655 | if res[3] <= fltpct: trend[i-1], trend[i], CurIdxs = ([], None), (CurIdxs, res), list(CurIdxs) 656 | else: CurIdxs = list(trend[i][0]) #restart search from here 657 | return list(filter(lambda val: val[0] != [], trend)) 658 | def measure_area(trendline, isMin, h): # Reimann sum of line to discrete time series data 659 | #first determine the time range, and subtract the line values to obtain a single function 660 | #support subtracts the line minus the series and eliminates the negative values 661 | #resistances subtracts the series minus the line and eliminate the negatives 662 | base = trendline[0][0] 663 | m, b, ser = trendline[1][0], trendline[1][1], h[base:trendline[0][-1]+1] 664 | return sum([max(0, (m * (x+base) + b) - y if isMin else y - (m * (x+base) + b)) for x, y in enumerate(ser)]) / len(ser) 665 | def window_results(trends, isMin, h): 666 | windows = [[] for x in range(len(divide)-1)] 667 | for x in trends: 668 | fstwin, lastwin = int(x[0][0] / window), int(x[0][-1] / window) 669 | wins = [[] for _ in range(fstwin, lastwin+1)] 670 | for y in x[0]: wins[int(y / window) - fstwin].append(y) 671 | for y in range(0, lastwin-fstwin): 672 | if len(wins[y+1]) == 0 and len(wins[y]) >= 3: windows[fstwin+y].append(wins[y]) 673 | if len(wins[y]) + len(wins[y + 1]) >= 3: 674 | windows[fstwin+y+1].append(wins[y] + wins[y+1]) 675 | if lastwin-fstwin==0 and len(wins[0]) >= 3: windows[fstwin].append(wins[0]) 676 | def fitarea(x): 677 | fit = get_bestfit([(y, h[y]) for y in x]) 678 | return (x, fit + (measure_area((x, fit), isMin, h),)) 679 | def dosort(x): 680 | x.sort(key = lambda val: val[1][skey]) 681 | return x 682 | return [dosort(list(fitarea(pts) for pts in x)) for x in windows] 683 | #print((mintrend[:5], maxtrend[:5])) 684 | 685 | #find all places where derivative is 0 - in finite case when it crosses positive to negative and choose the closer to 0 value 686 | #second derivative being positive or negative decides if they are minima or maxima 687 | #now for all pairs of 3 points construct the average line, rate it based on # of additional points, # of points on the wrong side of the line, and the margin of error for the line passing through all of them 688 | #finally select the best based on this rating 689 | 690 | #first find the peaks and troughs 691 | #https://github.com/dysonance/Trendy/blob/master/trendy.py #not proper trendlines only takes extremal points, and next best extrema, not 3 points 692 | #https://github.com/harttraveller/roughsr2/blob/master/roughsr2.py 693 | #https://www.candlestick.ninja/2019/02/support-and-resistance.html 694 | #https://www.candlestick.ninja/2019/02/identifying-support-and-resistance-part2.html 695 | #zmin, zmne, _, _, _ = np.polyfit(minimaIdxs, ymin, 1, full=True) #y=zmin[0]*x+zmin[1] 696 | #pmin = np.poly1d(zmin).c 697 | #zmax, zmxe, _, _, _ = np.polyfit(maximaIdxs, ymax, 1, full=True) #y=zmax[0]*x+zmax[1] 698 | #pmax = np.poly1d(zmax).c 699 | def overall_line(idxs, vals): 700 | if len(idxs) <= 1: pm, zme = [np.nan, np.nan], [np.nan] 701 | else: 702 | p, r = np.polynomial.polynomial.Polynomial.fit(idxs, vals, 1, full=True) #more numerically stable 703 | pm, zme = list(reversed(p.convert().coef)), r[0] 704 | if len(pm) == 1: pm.insert(0, 0.0) 705 | return pm 706 | def calc_all(idxs, h, isMin): 707 | min_h, max_h = min(h), max(h) 708 | scale = (max_h - min_h) / len_h 709 | fltpct = scale * errpct 710 | midxs = [[] for _ in range(len(divide)-1)] 711 | for x in idxs: midxs[int((x + rem) / window)].append(x) 712 | mtrend = [] 713 | for x in range(len(divide)-1-1): 714 | m = midxs[x] + midxs[x+1] 715 | mtrend.extend(trendmethod(m, h, fltpct, min_h, max_h)) 716 | if len(divide) == 2: 717 | mtrend.extend(trendmethod(midxs[0], h, fltpct, min_h, max_h)) 718 | mtrend = merge_lines(idxs, mtrend, h, fltpct) 719 | mtrend = [(pts, (res[0], res[1], res[2], res[3], res[4], measure_area((pts, res), isMin, h))) for pts, res in mtrend] 720 | mtrend.sort(key=lambda val: val[1][skey]) 721 | mwindows = window_results(mtrend, isMin, h) 722 | pm = overall_line(idxs, [h[x] for x in idxs]) 723 | #print((pmin, pmax, zmne, zmxe)) 724 | return pm, mtrend, mwindows 725 | if method == METHOD_NCUBED: 726 | trendmethod = get_trend 727 | elif method == METHOD_NSQUREDLOGN: 728 | trendmethod = get_trend_opt 729 | elif method == METHOD_HOUGHPOINTS: 730 | trendmethod = houghpt 731 | #pip install scikit-image 732 | elif method == METHOD_HOUGHLINES: 733 | trendmethod = hough 734 | elif method == METHOD_PROBHOUGH: 735 | trendmethod = prob_hough 736 | else: raise ValueError('method must be one of METHOD_NCUBED, METHOD_NSQUREDLOGN, METHOD_HOUGHPOINTS, METHOD_HOUGHLINES, METHOD_PROBHOUGH') 737 | extremaIdxs = get_extrema(h, extmethod, accuracy) 738 | divide = list(reversed(range(len_h, -window, -window))) 739 | rem, divide[0] = window - len_h % window, 0 740 | if rem == window: rem = 0 741 | skey = 3 if sortError else 5 742 | if hmin is None and hmax is None: 743 | pmin, mintrend, minwindows = calc_all(extremaIdxs[0], h, True) 744 | pmax, maxtrend, maxwindows = calc_all(extremaIdxs[1], h, False) 745 | else: 746 | if not hmin is None: 747 | pmin, mintrend, minwindows = calc_all(extremaIdxs if hmax is None else extremaIdxs[0], hmin, True) 748 | if hmax is None: return (extremaIdxs, pmin, mintrend, minwindows) 749 | if not hmax is None: 750 | pmax, maxtrend, maxwindows = calc_all(extremaIdxs if hmin is None else extremaIdxs[1], hmax, False) 751 | if hmin is None: return (extremaIdxs, pmax, maxtrend, maxwindows) 752 | return (extremaIdxs[0], pmin, mintrend, minwindows), (extremaIdxs[1], pmax, maxtrend, maxwindows) 753 | 754 | def plot_sup_res_date(hist, idx, numbest = 2, fromwindows = True, pctbound=0.1, 755 | extmethod = METHOD_NUMDIFF, method=METHOD_NSQUREDLOGN, window=125, 756 | errpct = 0.005, hough_scale=0.01, hough_prob_iter=10, sortError=False, accuracy=1): 757 | import matplotlib.ticker as ticker 758 | return plot_support_resistance(hist, ticker.FuncFormatter(datefmt(idx)), numbest, fromwindows, 759 | pctbound, extmethod, method, window, errpct, hough_scale, hough_prob_iter, sortError, accuracy) 760 | def plot_support_resistance(hist, xformatter = None, numbest = 2, fromwindows = True, 761 | pctbound=0.1, extmethod = METHOD_NUMDIFF, method=METHOD_NSQUREDLOGN, 762 | window=125, errpct = 0.005, hough_scale=0.01, hough_prob_iter=10, sortError=False, accuracy=1): 763 | import matplotlib.pyplot as plt 764 | import matplotlib.ticker as ticker 765 | ret = calc_support_resistance(hist, extmethod, method, window, errpct, hough_scale, hough_prob_iter, sortError, accuracy) 766 | plt.clf() 767 | plt.subplot(111) 768 | if len(ret) == 2: 769 | minimaIdxs, pmin, mintrend, minwindows = ret[0] 770 | maximaIdxs, pmax, maxtrend, maxwindows = ret[1] 771 | if type(hist) is tuple and len(hist) == 2 and check_num_alike(hist[0]) and check_num_alike(hist[1]): 772 | len_h = len(hist[0]) 773 | min_h, max_h = min(min(hist[0]), min(hist[1])), max(max(hist[0]), max(hist[1])) 774 | disp = [(hist[0], minimaIdxs, pmin, 'yo', 'Avg. Support', 'y--'), (hist[1], maximaIdxs, pmax, 'bo', 'Avg. Resistance', 'b--')] 775 | dispwin = [(hist[0], minwindows, 'Support', 'g--'), (hist[1], maxwindows, 'Resistance', 'r--')] 776 | disptrend = [(hist[0], mintrend, 'Support', 'g--'), (hist[1], maxtrend, 'Resistance', 'r--')] 777 | plt.plot(range(len_h), hist[0], 'k--', label='Low Price') 778 | plt.plot(range(len_h), hist[1], 'm--', label='High Price') 779 | else: 780 | len_h = len(hist) 781 | min_h, max_h = min(hist), max(hist) 782 | disp = [(hist, minimaIdxs, pmin, 'yo', 'Avg. Support', 'y--'), (hist, maximaIdxs, pmax, 'bo', 'Avg. Resistance', 'b--')] 783 | dispwin = [(hist, minwindows, 'Support', 'g--'), (hist, maxwindows, 'Resistance', 'r--')] 784 | disptrend = [(hist, mintrend, 'Support', 'g--'), (hist, maxtrend, 'Resistance', 'r--')] 785 | plt.plot(range(len_h), hist, 'k--', label='Close Price') 786 | else: 787 | minimaIdxs, pmin, mintrend, minwindows = ([], [], [], []) if hist[0] is None else ret 788 | maximaIdxs, pmax, maxtrend, maxwindows = ([], [], [], []) if hist[1] is None else ret 789 | len_h = len(hist[1 if hist[0] is None else 0]) 790 | min_h, max_h = min(hist[1 if hist[0] is None else 0]), max(hist[1 if hist[0] is None else 0]) 791 | disp = [(hist[1], maximaIdxs, pmax, 'bo', 'Avg. Resistance', 'b--') if hist[0] is None else (hist[0], minimaIdxs, pmin, 'yo', 'Avg. Support', 'y--')] 792 | dispwin = [(hist[1], maxwindows, 'Resistance', 'r--') if hist[0] is None else (hist[0], minwindows, 'Support', 'g--')] 793 | disptrend = [(hist[1], maxtrend, 'Resistance', 'r--') if hist[0] is None else (hist[0], mintrend, 'Support', 'g--')] 794 | plt.plot(range(len_h), hist[1 if hist[0] is None else 0], 'k--', label= ('High' if hist[0] is None else 'Low') + ' Price') 795 | for h, idxs, pm, clrp, lbl, clrl in disp: 796 | plt.plot(idxs, [h[x] for x in idxs], clrp) 797 | plt.plot([0, len_h-1],[pm[1],pm[0] * (len_h-1) + pm[1]],clrl, label=lbl) 798 | def add_trend(h, trend, lbl, clr, bFirst): 799 | for ln in trend[:numbest]: 800 | maxx = ln[0][-1]+1 801 | while maxx < len_h: 802 | ypred = ln[1][0] * maxx + ln[1][1] 803 | if (h[maxx] > ypred and h[maxx-1] < ypred or h[maxx] < ypred and h[maxx-1] > ypred or 804 | ypred > max_h + (max_h-min_h)*pctbound or ypred < min_h - (max_h-min_h)*pctbound): break 805 | maxx += 1 806 | x_vals = np.array((ln[0][0], maxx)) # plt.gca().get_xlim()) 807 | y_vals = ln[1][0] * x_vals + ln[1][1] 808 | if bFirst: 809 | plt.plot([ln[0][0], maxx], y_vals, clr, label=lbl) 810 | bFirst = False 811 | else: plt.plot([ln[0][0], maxx], y_vals, clr) 812 | return bFirst 813 | if fromwindows: 814 | for h, windows, lbl, clr in dispwin: 815 | bFirst = True 816 | for trend in windows: 817 | bFirst = add_trend(h, trend, lbl, clr, bFirst) 818 | else: 819 | for h, trend, lbl, clr in disptrend: 820 | add_trend(h, trend, lbl, clr, True) 821 | plt.title('Prices with Support/Resistance Trend Lines') 822 | plt.xlabel('Date') 823 | plt.ylabel('Price') 824 | plt.legend() 825 | plt.gca().xaxis.set_major_locator(ticker.MaxNLocator(6)) 826 | #plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) 827 | if not xformatter is None: plt.gca().xaxis.set_major_formatter(xformatter) 828 | plt.setp(plt.gca().get_xticklabels(), rotation=30, ha='right') 829 | #plt.gca().set_position([0, 0, 1, 1]) 830 | #plt.savefig(os.path.join(curdir, 'data', 'suppres.svg'), format='svg', bbox_inches = 'tight') 831 | #plt.show() 832 | return plt.gcf() 833 | --------------------------------------------------------------------------------