├── LICENSE ├── Procfile ├── README.md ├── app.py ├── quantmod ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-36.pyc │ ├── auth.cpython-36.pyc │ ├── chart.cpython-36.pyc │ ├── core.cpython-36.pyc │ ├── factory.cpython-36.pyc │ ├── ta.cpython-36.pyc │ ├── tools.cpython-36.pyc │ ├── utils.cpython-36.pyc │ ├── valid.cpython-36.pyc │ └── version.cpython-36.pyc ├── auth.py ├── chart.py ├── core.py ├── datetools.py ├── factory.py ├── ta.py ├── tanolib.py ├── theming │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-36.pyc │ │ ├── palettes.cpython-36.pyc │ │ ├── skeleton.cpython-36.pyc │ │ └── themes.cpython-36.pyc │ ├── colors.py │ ├── palettes.py │ ├── skeleton.py │ └── themes.py ├── tools.py ├── utils.py ├── valid.py ├── vendors │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-36.pyc │ │ └── sources.cpython-36.pyc │ └── sources.py └── version.py ├── requirements.txt ├── runtime.txt └── screenshots └── Screenshot1.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Plotly 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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:server --timeout 300 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Dash Technical Charting App** 2 | 3 | This is a demo of the Dash interactive Python framework developed by [Plotly](https://plot.ly/). 4 | 5 | Dash abstracts away all of the technologies and protocols required to build an interactive web-based application and is a simple and effective way to bind a user interface around your Python code. 6 | 7 | This demo is very similar to the simple Stock Tickers App, but it dynamically binds py-Quantmod along with Ta-Lib to chart SP500 stocks and major ETFs 8 | with over 50 technical indicators, ranging from a simple moving average, to Stochastics, to more obscure indicators such as the Channel Commodity Index. 9 | It also makes use of caching to speed up app performance. 10 | 11 | To learn more check out our [documentation](https://plot.ly/dash). 12 | 13 | The following are screenshots for the app in this repo: 14 | 15 | ![Alt desc](https://raw.githubusercontent.com/plotly/dash-technical-charting/d3561c5426fba965fb7030410913ca3b5c89c31d/screenshots/Screenshot1.png?token=AK-nZOTHb7vTWF9xxIWLWSqqdG_lf25zks5ZW9QWwA%3D%3D) 16 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # In[]: 2 | # Import required libraries 3 | import os 4 | import datetime as dt 5 | 6 | import quantmod as qm 7 | import pandas_datareader.data as web 8 | 9 | import flask 10 | import dash 11 | from dash.dependencies import Input, Output 12 | import dash_core_components as dcc 13 | import dash_html_components as html 14 | from flask_caching import Cache 15 | 16 | 17 | # In[]: 18 | # Setup the app 19 | server = flask.Flask(__name__) 20 | app = dash.Dash(__name__) 21 | 22 | app.scripts.config.serve_locally = False 23 | dcc._js_dist[0]['external_url'] = 'https://cdn.plot.ly/plotly-finance-1.28.0.min.js' 24 | 25 | 26 | # In[]: 27 | # Put your Dash code here 28 | 29 | # Add caching 30 | cache = Cache(app.server, config={'CACHE_TYPE': 'simple'}) 31 | timeout = 60 * 60 # 1 hour 32 | 33 | # Controls 34 | sp500 = ['AAPL', 'ABT', 'ABBV', 'ACN', 'ACE', 'ADBE', 'ADT', 'AAP', 'AES', 35 | 'AET', 'AFL', 'AMG', 'A', 'GAS', 'ARE', 'APD', 'AKAM', 'AA', 'AGN', 36 | 'ALXN', 'ALLE', 'ADS', 'ALL', 'ALTR', 'MO', 'AMZN', 'AEE', 'AAL', 37 | 'AEP', 'AXP', 'AIG', 'AMT', 'AMP', 'ABC', 'AME', 'AMGN', 'APH', 'APC', 38 | 'ADI', 'AON', 'APA', 'AIV', 'AMAT', 'ADM', 'AIZ', 'T', 'ADSK', 'ADP', 39 | 'AN', 'AZO', 'AVGO', 'AVB', 'AVY', 'BHI', 'BLL', 'BAC', 'BK', 'BCR', 40 | 'BXLT', 'BAX', 'BBT', 'BDX', 'BBBY', 'BRK.B', 'BBY', 'BLX', 'HRB', 41 | 'BA', 'BWA', 'BXP', 'BSX', 'BMY', 'BRCM', 'BF.B', 'CHRW', 'CA', 42 | 'CVC', 'COG', 'CAM', 'CPB', 'COF', 'CAH', 'HSIC', 'KMX', 'CCL', 43 | 'CAT', 'CBG', 'CBS', 'CELG', 'CNP', 'CTL', 'CERN', 'CF', 'SCHW', 44 | 'CHK', 'CVX', 'CMG', 'CB', 'CI', 'XEC', 'CINF', 'CTAS', 'CSCO', 'C', 45 | 'CTXS', 'CLX', 'CME', 'CMS', 'COH', 'KO', 'CCE', 'CTSH', 'CL', 46 | 'CMCSA', 'CMA', 'CSC', 'CAG', 'COP', 'CNX', 'ED', 'STZ', 'GLW', 47 | 'COST', 'CCI', 'CSX', 'CMI', 'CVS', 'DHI', 'DHR', 'DRI', 'DVA', 48 | 'DE', 'DLPH', 'DAL', 'XRAY', 'DVN', 'DO', 'DTV', 'DFS', 'DISCA', 49 | 'DISCK', 'DG', 'DLTR', 'D', 'DOV', 'DOW', 'DPS', 'DTE', 'DD', 'DUK', 50 | 'DNB', 'ETFC', 'EMN', 'ETN', 'EBAY', 'ECL', 'EIX', 'EW', 'EA', 51 | 'EMC', 'EMR', 'ENDP', 'ESV', 'ETR', 'EOG', 'EQT', 'EFX', 'EQIX', 52 | 'EQR', 'ESS', 'EL', 'ES', 'EXC', 'EXPE', 'EXPD', 'ESRX', 'XOM', 53 | 'FFIV', 'FB', 'FAST', 'FDX', 'FIS', 'FITB', 'FSLR', 'FE', 'FISV', 54 | 'FLIR', 'FLS', 'FLR', 'FMC', 'FTI', 'F', 'FOSL', 'BEN', 'FCX', 55 | 'FTR', 'GME', 'GPS', 'GRMN', 'GD', 'GE', 'GGP', 'GIS', 'GM', 56 | 'GPC', 'GNW', 'GILD', 'GS', 'GT', 'GOOGL', 'GOOG', 'GWW', 'HAL', 57 | 'HBI', 'HOG', 'HAR', 'HRS', 'HIG', 'HAS', 'HCA', 'HCP', 'HCN', 58 | 'HP', 'HES', 'HPQ', 'HD', 'HON', 'HRL', 'HSP', 'HST', 'HCBK', 59 | 'HUM', 'HBAN', 'ITW', 'IR', 'INTC', 'ICE', 'IBM', 'IP', 'IPG', 60 | 'IFF', 'INTU', 'ISRG', 'IVZ', 'IRM', 'JEC', 'JBHT', 'JNJ', 61 | 'JCI', 'JOY', 'JPM', 'JNPR', 'KSU', 'K', 'KEY', 'GMCR', 'KMB', 62 | 'KIM', 'KMI', 'KLAC', 'KSS', 'KRFT', 'KR', 'LB', 'LLL', 'LH', 63 | 'LRCX', 'LM', 'LEG', 'LEN', 'LVLT', 'LUK', 'LLY', 'LNC', 'LLTC', 64 | 'LMT', 'L', 'LOW', 'LYB', 'MTB', 'MAC', 'M', 'MNK', 'MRO', 'MPC', 65 | 'MAR', 'MMC', 'MLM', 'MAS', 'MA', 'MAT', 'MKC', 'MCD', 'MCK', 66 | 'MJN', 'MMV', 'MDT', 'MRK', 'MET', 'KORS', 'MCHP', 'MU', 'MSFT', 67 | 'MHK', 'TAP', 'MDLZ', 'MON', 'MNST', 'MCO', 'MS', 'MOS', 'MSI', 68 | 'MUR', 'MYL', 'NDAQ', 'NOV', 'NAVI', 'NTAP', 'NFLX', 'NWL', 69 | 'NFX', 'NEM', 'NWSA', 'NEE', 'NLSN', 'NKE', 'NI', 'NE', 'NBL', 70 | 'JWN', 'NSC', 'NTRS', 'NOC', 'NRG', 'NUE', 'NVDA', 'ORLY', 71 | 'OXY', 'OMC', 'OKE', 'ORCL', 'OI', 'PCAR', 'PLL', 'PH', 'PDCO', 72 | 'PAYX', 'PNR', 'PBCT', 'POM', 'PEP', 'PKI', 'PRGO', 'PFE', 73 | 'PCG', 'PM', 'PSX', 'PNW', 'PXD', 'PBI', 'PCL', 'PNC', 'RL', 74 | 'PPG', 'PPL', 'PX', 'PCP', 'PCLN', 'PFG', 'PG', 'PGR', 'PLD', 75 | 'PRU', 'PEG', 'PSA', 'PHM', 'PVH', 'QRVO', 'PWR', 'QCOM', 76 | 'DGX', 'RRC', 'RTN', 'O', 'RHT', 'REGN', 'RF', 'RSG', 'RAI', 77 | 'RHI', 'ROK', 'COL', 'ROP', 'ROST', 'RLD', 'R', 'CRM', 'SNDK', 78 | 'SCG', 'SLB', 'SNI', 'STX', 'SEE', 'SRE', 'SHW', 'SPG', 'SWKS', 79 | 'SLG', 'SJM', 'SNA', 'SO', 'LUV', 'SWN', 'SE', 'STJ', 'SWK', 80 | 'SPLS', 'SBUX', 'HOT', 'STT', 'SRCL', 'SYK', 'STI', 'SYMC', 'SYY', 81 | 'TROW', 'TGT', 'TEL', 'TE', 'TGNA', 'THC', 'TDC', 'TSO', 'TXN', 82 | 'TXT', 'HSY', 'TRV', 'TMO', 'TIF', 'TWX', 'TWC', 'TJX', 'TMK', 83 | 'TSS', 'TSCO', 'RIG', 'TRIP', 'FOXA', 'TSN', 'TYC', 'UA', 84 | 'UNP', 'UNH', 'UPS', 'URI', 'UTX', 'UHS', 'UNM', 'URBN', 'VFC', 85 | 'VLO', 'VAR', 'VTR', 'VRSN', 'VZ', 'VRTX', 'VIAB', 'V', 'VNO', 86 | 'VMC', 'WMT', 'WBA', 'DIS', 'WM', 'WAT', 'ANTM', 'WFC', 'WDC', 87 | 'WU', 'WY', 'WHR', 'WFM', 'WMB', 'WEC', 'WYN', 'WYNN', 'XEL', 88 | 'XRX', 'XLNX', 'XL', 'XYL', 'YHOO', 'YUM', 'ZBH', 'ZION', 'ZTS'] 89 | 90 | etf = ['SPY', 'XLF', 'GDX', 'EEM', 'VXX', 'IWM', 'UVXY', 'UXO', 'GDXJ', 'QQQ'] 91 | 92 | tickers = sp500 + etf 93 | tickers = [dict(label=str(ticker), value=str(ticker)) 94 | for ticker in tickers] 95 | 96 | # Dynamic binding 97 | functions = dir(qm.ta)[9:-4] 98 | functions = [dict(label=str(function[4:]), value=str(function)) 99 | for function in functions] 100 | 101 | # Layout 102 | app.layout = html.Div( 103 | [ 104 | html.Div([ 105 | html.H2( 106 | 'Dash Finance', 107 | style={'padding-top': '20', 'text-align': 'center'} 108 | ), 109 | html.Div( 110 | [ 111 | html.Label('Select ticker:'), 112 | dcc.Dropdown( 113 | id='dropdown', 114 | options=tickers, 115 | value='SPY', 116 | ), 117 | ], 118 | style={ 119 | 'width': '510', 'display': 'inline-block', 120 | 'padding-left': '40', 'margin-bottom': '20'} 121 | ), 122 | html.Div( 123 | [ 124 | html.Label('Select technical indicators:'), 125 | dcc.Dropdown( 126 | id='multi', 127 | options=functions, 128 | multi=True, 129 | value=['add_BBANDS', 'add_RSI', 'add_MACD'], 130 | ), 131 | ], 132 | style={ 133 | 'width': '510', 'display': 'inline-block', 134 | 'padding-right': '40', 'margin-bottom': '20'} 135 | ), 136 | ]), 137 | html.Div( 138 | [ 139 | html.Label('Specify parameters of technical indicators:'), 140 | html.P('Use , to separate arguments and ; to separate indicators. () and spaces are ignored'), # noqa: E501 141 | dcc.Input( 142 | id='arglist', 143 | style={'height': '32', 'width': '1020'} 144 | ) 145 | ], 146 | id='arg-controls', 147 | style={'display': 'none'} 148 | ), 149 | dcc.Graph(id='output') 150 | ], 151 | style={ 152 | 'width': '1100', 153 | 'margin-left': 'auto', 154 | 'margin-right': 'auto', 155 | 'font-family': 'overpass', 156 | 'background-color': '#F3F3F3' 157 | } 158 | ) 159 | 160 | @app.callback(Output('arg-controls', 'style'), [Input('multi', 'value')]) 161 | def display_control(multi): 162 | if not multi: 163 | return {'display': 'none'} 164 | else: 165 | return {'margin-bottom': '20', 'padding-left': '40'} 166 | 167 | 168 | @cache.memoize(timeout=timeout) 169 | @app.callback(Output('output', 'figure'), [Input('dropdown', 'value'), 170 | Input('multi', 'value'), 171 | Input('arglist', 'value')]) 172 | def update_graph_from_dropdown(dropdown, multi, arglist): 173 | 174 | # Get Quantmod Chart 175 | try: 176 | df = web.DataReader(dropdown, 'google', dt.datetime(2016, 1, 1), dt.datetime.now()) 177 | print('Loading') 178 | ch = qm.Chart(df) 179 | except: 180 | pass 181 | 182 | # Get functions and arglist for technical indicators 183 | if arglist: 184 | arglist = arglist.replace('(', '').replace(')', '').split(';') 185 | arglist = [args.strip() for args in arglist] 186 | for function, args in zip(multi, arglist): 187 | if args: 188 | args = args.split(',') 189 | newargs = [] 190 | for arg in args: 191 | try: 192 | arg = int(arg) 193 | except: 194 | try: 195 | arg = float(arg) 196 | except: 197 | pass 198 | newargs.append(arg) 199 | print(newargs) 200 | # Dynamic calling 201 | getattr(qm, function)(ch, *newargs) 202 | else: 203 | getattr(qm, function)(ch) 204 | else: 205 | for function in multi: 206 | # Dynamic calling 207 | getattr(qm, function)(ch) 208 | 209 | # Return plot as figure 210 | fig = ch.to_figure(width=1100) 211 | return fig 212 | 213 | 214 | # In[]: 215 | # External css 216 | 217 | external_css = ["https://fonts.googleapis.com/css?family=Overpass:400,400i,700,700i", 218 | "https://cdn.rawgit.com/plotly/dash-app-stylesheets/c6a126a684eaaa94a708d41d6ceb32b28ac78583/dash-technical-charting.css"] 219 | 220 | for css in external_css: 221 | app.css.append_css({"external_url": css}) 222 | 223 | if 'DYNO' in os.environ: 224 | app.scripts.append_script({ 225 | 'external_url': 'https://cdn.rawgit.com/chriddyp/ca0d8f02a1659981a0ea7f013a378bbd/raw/e79f3f789517deec58f41251f7dbb6bee72c44ab/plotly_ga.js' 226 | }) 227 | 228 | 229 | # In[]: 230 | # Run the Dash app 231 | if __name__ == '__main__': 232 | app.server.run() 233 | -------------------------------------------------------------------------------- /quantmod/__init__.py: -------------------------------------------------------------------------------- 1 | """Quantmod 2 | 3 | A powerful financial charting library based on R's Quantmod. 4 | 5 | With a Plotly backend and Cufflinks simplicity, 6 | Quantmod provides beautiful charts and a variety of 7 | quantitiative and technical finance tools. 8 | 9 | Author 10 | ------ 11 | @jackwluo 12 | 13 | Credits 14 | ------- 15 | plotly.py : @chriddyp, @theengineear, et al. 16 | cufflinks : @jorgesantos 17 | 18 | """ 19 | # flake8: noqa 20 | 21 | from __future__ import absolute_import 22 | 23 | from .core import * 24 | from .chart import * 25 | from .tools import (go_offline, go_online, is_offline, 26 | get_config_file, set_config_file, reset_config_file, 27 | get_credentials_file, set_credentials_file, 28 | reset_credentials_file) 29 | from .version import __version__ 30 | 31 | 32 | __docformat__ = 'restructuredtext' 33 | 34 | 35 | # Offline mode from config initialization 36 | try: 37 | if get_config_file()['offline']: 38 | go_offline() 39 | else: 40 | go_online() 41 | except: 42 | pass 43 | -------------------------------------------------------------------------------- /quantmod/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-technical-charting/72af2c0c19bc2c56231061bff2fbd118b71a9819/quantmod/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /quantmod/__pycache__/auth.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-technical-charting/72af2c0c19bc2c56231061bff2fbd118b71a9819/quantmod/__pycache__/auth.cpython-36.pyc -------------------------------------------------------------------------------- /quantmod/__pycache__/chart.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-technical-charting/72af2c0c19bc2c56231061bff2fbd118b71a9819/quantmod/__pycache__/chart.cpython-36.pyc -------------------------------------------------------------------------------- /quantmod/__pycache__/core.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-technical-charting/72af2c0c19bc2c56231061bff2fbd118b71a9819/quantmod/__pycache__/core.cpython-36.pyc -------------------------------------------------------------------------------- /quantmod/__pycache__/factory.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-technical-charting/72af2c0c19bc2c56231061bff2fbd118b71a9819/quantmod/__pycache__/factory.cpython-36.pyc -------------------------------------------------------------------------------- /quantmod/__pycache__/ta.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-technical-charting/72af2c0c19bc2c56231061bff2fbd118b71a9819/quantmod/__pycache__/ta.cpython-36.pyc -------------------------------------------------------------------------------- /quantmod/__pycache__/tools.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-technical-charting/72af2c0c19bc2c56231061bff2fbd118b71a9819/quantmod/__pycache__/tools.cpython-36.pyc -------------------------------------------------------------------------------- /quantmod/__pycache__/utils.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-technical-charting/72af2c0c19bc2c56231061bff2fbd118b71a9819/quantmod/__pycache__/utils.cpython-36.pyc -------------------------------------------------------------------------------- /quantmod/__pycache__/valid.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-technical-charting/72af2c0c19bc2c56231061bff2fbd118b71a9819/quantmod/__pycache__/valid.cpython-36.pyc -------------------------------------------------------------------------------- /quantmod/__pycache__/version.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-technical-charting/72af2c0c19bc2c56231061bff2fbd118b71a9819/quantmod/__pycache__/version.cpython-36.pyc -------------------------------------------------------------------------------- /quantmod/auth.py: -------------------------------------------------------------------------------- 1 | """Functions that manage configuration writing 2 | 3 | Refactored from Plotly's 'auth.py'. 4 | 5 | """ 6 | from __future__ import absolute_import 7 | 8 | import os 9 | 10 | 11 | package = 'quantmod' 12 | 13 | AUTH_DIR = os.path.join(os.path.expanduser('~'), '.' + package) 14 | TEST_DIR = os.path.join(AUTH_DIR, 'test') 15 | TEST_FILE = os.path.join(AUTH_DIR, 'permission_test') 16 | CONFIG_FILE = os.path.join(AUTH_DIR, 'config.json') 17 | 18 | FILE_CONTENT = { 19 | CONFIG_FILE: { 20 | 'sharing': 'public', 21 | 'dimensions': None, 22 | 'theme': 'light', 23 | 'source': 'yahoo', 24 | 'offline': False, 25 | 'offline_url': '', 26 | 'offline_show_link': True, 27 | 'offline_link_text': 'Edit Chart', 28 | } 29 | } 30 | 31 | 32 | def _permissions(): 33 | """Check for write access.""" 34 | try: 35 | os.mkdir(TEST_DIR) 36 | os.rmdir(TEST_DIR) 37 | if not os.path.exists(AUTH_DIR): 38 | os.mkdir(AUTH_DIR) 39 | with open(TEST_FILE, 'w') as f: 40 | f.write('Testing\n') 41 | os.remove(TEST_FILE) 42 | return True 43 | except: 44 | return False 45 | 46 | 47 | _file_permissions = _permissions() 48 | 49 | 50 | def check_file_permissions(): 51 | """Return True if write permissions, else return False.""" 52 | return _file_permissions 53 | 54 | 55 | def get_path(): 56 | """Get path of AUTH_DIR.""" 57 | return AUTH_DIR 58 | -------------------------------------------------------------------------------- /quantmod/chart.py: -------------------------------------------------------------------------------- 1 | """Main Chart functionnality 2 | 3 | Chart is a wrapper on top of DataFrame that 4 | adds functionnality and allows for easy plotting. 5 | Features include time series adjustement, volume adjustement, and 6 | plotting of OHLCV data with over 100 technical indicators. 7 | 8 | """ 9 | # flake8: ignore=E251 10 | 11 | from __future__ import absolute_import 12 | 13 | import collections 14 | import copy 15 | import six 16 | import datetime as dt 17 | import pandas as pd 18 | import plotly.plotly as py 19 | import plotly.offline as pyo 20 | 21 | from . import tools 22 | from . import factory 23 | from .valid import VALID_FIGURE_KWARGS, VALID_TRACES, OHLC_TRACES 24 | from .ta import * # noqa : F405 25 | 26 | 27 | class Chart(object): 28 | """Quantmod Chart based on Pandas DataFrame. 29 | 30 | Chart is a wrapper on top of DataFrame that 31 | adds functionnality and allows for easy plotting. 32 | Features include time series adjustement, volume adjustement, and 33 | plotting of OHLCV data with over 100 technical indicators. 34 | 35 | """ 36 | def __init__(self, df, src=None, 37 | ticker=None, start=None, end=None): 38 | """Quantmod Chart based on Pandas DataFrame. 39 | 40 | Chart is a wrapper on top of DataFrame that 41 | adds functionnality and allows for easy plotting. 42 | Features include time series adjustement, volume adjustement, and 43 | plotting of OHLCV data with over 100 technical indicators. 44 | 45 | Parameters 46 | ---------- 47 | df : DataFrame 48 | Underlying DataFrame containing ticker data. 49 | src : string or dict 50 | If string, provenance of data (e.g. 'google', 'yahoo') to 51 | automatically map column names to OHLCV data. 52 | If dict, directly specifies how column names map to OHLCV data. 53 | ticker : string or False, default False 54 | Ticker associated with data. Used to plot titles. 55 | If False no ticker is specified. 56 | start : datetime, string or False, default df.index[0] 57 | Left boundary for date range, specified 58 | either as string or as a datetime object. 59 | If False no start is specified. Default set to first 60 | element of df.index. 61 | end : datetime, string or False, default df.index[-1] 62 | Right boundary for date range, specified 63 | either as string or as a datetime object. 64 | If False no start is specified. Default set to last 65 | element of df.index. 66 | 67 | """ 68 | self.df = df 69 | 70 | # Test if src is string or dict, or get default vendor otherwise 71 | if src is not None: 72 | if isinstance(src, six.string_types): 73 | src = factory.get_source(src) 74 | elif isinstance(src, dict): 75 | pass 76 | else: 77 | raise TypeError("Invalid src '{0}'. " 78 | "It should be string or dict." 79 | .format(src)) 80 | else: 81 | src = factory.get_source(tools.get_config_file()['source']) 82 | 83 | # Check if ticker is valid 84 | if ticker is not None: 85 | if ticker is False: 86 | pass 87 | elif isinstance(ticker, six.string_types): 88 | pass 89 | else: 90 | raise TypeError("Invalid ticker '{0}'. " 91 | "It should be string or dict." 92 | .format(ticker)) 93 | else: 94 | ticker = False 95 | 96 | # Check if start is valid 97 | if start is not None: 98 | if start is False: 99 | pass 100 | elif isinstance(start, six.string_types): 101 | pass 102 | elif isinstance(start, dt.datetime) or isinstance(start, dt.date): 103 | pass 104 | else: 105 | raise TypeError("Invalid start '{0}'. " 106 | "It should be string or datetime." 107 | .format(start)) 108 | else: 109 | start = self.df.index[0] 110 | 111 | # Check if end is valid 112 | if end is not None: 113 | if end is False: 114 | pass 115 | elif isinstance(end, six.string_types): 116 | pass 117 | elif isinstance(end, dt.datetime) or isinstance(end, dt.date): 118 | pass 119 | else: 120 | raise TypeError("Invalid end '{0}'. " 121 | "It should be string or datetime." 122 | .format(end)) 123 | else: 124 | end = self.df.index[-1] 125 | 126 | self.ticker = ticker 127 | self.start = start 128 | self.end = end 129 | 130 | self.op = src['op'] 131 | self.hi = src['hi'] 132 | self.lo = src['lo'] 133 | self.cl = src['cl'] 134 | self.aop = src['aop'] # Not used currently 135 | self.ahi = src['ahi'] # Not used currently 136 | self.alo = src['alo'] # Not used currently 137 | self.acl = src['acl'] 138 | self.vo = src['vo'] 139 | self.di = src['di'] # Not used currently 140 | 141 | self.ind = pd.DataFrame([], index=self.df.index) 142 | self.pri = collections.OrderedDict() 143 | self.sec = collections.OrderedDict() 144 | 145 | def __repr__(self): 146 | """Return representation of Chart object.""" 147 | return str(self.to_frame()) 148 | 149 | def __len__(self): 150 | """Return length of Chart object.""" 151 | return len(self.to_frame()) 152 | 153 | @property 154 | def shape(self): 155 | """Return shape of Chart object.""" 156 | return self.to_frame().shape 157 | 158 | @property 159 | def has_open(self): 160 | """Return True if Chart DataFrame has open, 161 | False otherwise.""" 162 | if self.op in self.df.columns: 163 | return True 164 | else: 165 | return False 166 | 167 | @property 168 | def has_high(self): 169 | """Return True if Chart DataFrame has high, 170 | False otherwise.""" 171 | if self.hi in self.df.columns: 172 | return True 173 | else: 174 | return False 175 | 176 | @property 177 | def has_low(self): 178 | """Return True if Chart DataFrame has low, 179 | False otherwise.""" 180 | if self.lo in self.df.columns: 181 | return True 182 | else: 183 | return False 184 | 185 | @property 186 | def has_close(self): 187 | """Return True if Chart DataFrame has close, 188 | False otherwise.""" 189 | if self.cl in self.df.columns: 190 | return True 191 | else: 192 | return False 193 | 194 | @property 195 | def has_adjusted_open(self): 196 | """Return True if Chart DataFrame has adjusted open, 197 | False otherwise.""" 198 | if self.aop in self.df.columns: 199 | return True 200 | else: 201 | return False 202 | 203 | @property 204 | def has_adjusted_high(self): 205 | """Return True if Chart DataFrame has adjusted high, 206 | False otherwise.""" 207 | if self.ahi in self.df.columns: 208 | return True 209 | else: 210 | return False 211 | 212 | @property 213 | def has_adjusted_low(self): 214 | """Return True if Chart DataFrame has adjusted low, 215 | False otherwise.""" 216 | if self.alo in self.df.columns: 217 | return True 218 | else: 219 | return False 220 | 221 | @property 222 | def has_adjusted_close(self): 223 | """Return True if Chart DataFrame has adjusted close, 224 | False otherwise.""" 225 | if self.acl in self.df.columns: 226 | return True 227 | else: 228 | return False 229 | 230 | @property 231 | def has_volume(self): 232 | """Return True if Chart DataFrame has volume, 233 | False otherwise.""" 234 | if self.vo in self.df.columns: 235 | return True 236 | else: 237 | return False 238 | 239 | @property 240 | def has_dividend(self): 241 | """Return True if Chart DataFrame has dividend, 242 | False otherwise.""" 243 | if self.di in self.df.columns: 244 | return True 245 | else: 246 | return False 247 | 248 | @property 249 | def has_OHLC(self): 250 | """Return True if Chart DataFrame has OHLC, False otherwise.""" 251 | cols = {self.op, self.hi, self.lo, self.cl} 252 | arr = self.df.columns.isin(cols) 253 | return sum(arr) >= len(cols) 254 | 255 | @property 256 | def has_OHLCV(self): 257 | """Return True if Chart DataFrame has OHLCV, False otherwise.""" 258 | cols = {self.op, self.hi, self.lo, self.cl, self.vo} 259 | arr = self.df.columns.isin(cols) 260 | return sum(arr) >= len(cols) 261 | 262 | def head(self, n=252): 263 | """Return first n elements of chart. 264 | 265 | Parameters 266 | ---------- 267 | n : int, default 252 268 | Number of elements to keep, starting from first element. 269 | 270 | """ 271 | chart = copy.deepcopy(self) 272 | chart.df = chart.df.head(n) 273 | chart.ind = chart.ind.head(n) 274 | return chart 275 | 276 | def tail(self, n=252): 277 | """Return last n elements of chart. 278 | 279 | Parameters 280 | ---------- 281 | n : int, default 252 282 | Number of elements to keep, starting from last element. 283 | 284 | """ 285 | chart = copy.deepcopy(self) 286 | chart.df = chart.df.tail(n) 287 | chart.ind = chart.ind.tail(n) 288 | return chart 289 | 290 | def adjust(self, inplace=False): 291 | """Adjust OHLC data for splits, dividends, etc. 292 | 293 | Requires an adjusted close column to adjust the rest of the OHLC bars. 294 | 295 | Parameters 296 | ---------- 297 | inplace : bool 298 | Modifies Chart inplace (returns None) if True, else 299 | returns modified Chart by default. 300 | 301 | """ 302 | if not self.has_OHLC and self.has_adjusted_close: 303 | raise Exception("Insufficient data to adjust OHLC data.") 304 | 305 | ratio = (self.df[self.cl] / self.df[self.acl]) 306 | 307 | if inplace: 308 | self.df[self.op] = self.df[self.op] / ratio 309 | self.df[self.hi] = self.df[self.hi] / ratio 310 | self.df[self.lo] = self.df[self.lo] / ratio 311 | self.df[self.cl] = self.df[self.cl] / ratio 312 | else: 313 | df2 = self.df.copy() 314 | df2[self.op] = self.df[self.op] / ratio 315 | df2[self.hi] = self.df[self.hi] / ratio 316 | df2[self.lo] = self.df[self.lo] / ratio 317 | df2[self.cl] = self.df[self.cl] / ratio 318 | return Chart(df2) 319 | 320 | def adjust_volume(self, inplace=False): 321 | """Adjust volume data for splits, dividends, etc. 322 | 323 | Requires a close and and adjusted close column to adjust volume. 324 | 325 | Parameters 326 | ---------- 327 | inplace : bool, default False 328 | Modifies Chart inplace (returns None) if True, else 329 | returns modified Chart by default. 330 | 331 | """ 332 | if not self.has_volume and self.has_close and self.has_adjusted_close: 333 | raise Exception("Insufficient data to adjust volume.") 334 | 335 | ratio = (self.df[self.cl] / self.df[self.acl]) 336 | 337 | if inplace: 338 | self.df[self.vo] = self.df[self.vo] / ratio 339 | else: 340 | df2 = self.df.copy() 341 | df2[self.vo] = self.df[self.vo] / ratio 342 | return Chart(df2) 343 | 344 | def to_frame(self): 345 | """Return DataFrame representation of Chart, including all added 346 | technical indicators.""" 347 | return self.df.join([self.ind]) 348 | 349 | def to_figure(self, type=None, volume=None, 350 | theme=None, layout=None, 351 | title=None, subtitle=None, log=None, hovermode=None, 352 | legend=None, annotations=None, shapes=None, 353 | dimensions=None, width=None, height=None, margin=None, 354 | **kwargs): 355 | """Return Plotly figure (dict) that is used to generate the stock chart. 356 | 357 | Parameters 358 | ---------- 359 | type : {'ohlc', 'candlestick', 360 | 'line', 'line_thin', 'line_thick', 'line_dashed', 361 | 'line_dashed_thin', 'line_dashed_thick', 362 | 'area', 'area_dashed', 363 | 'area_dashed_thin', 'area_dashed_thick', 'area_threshold', 364 | 'scatter'} 365 | Determine the chart type of the main trace. For candlestick 366 | and OHLC bars Chart needs to have OHLC enabled. 367 | volume : bool 368 | Toggle the diplay of a volume subplot in chart. Default True. 369 | theme : string 370 | Quantmod theme. 371 | layout : dict or Layout 372 | Plotly layout dict or graph_objs.Layout object. 373 | Will override all other arguments if conflicting as 374 | user-inputted layout is updated last. 375 | title : string 376 | Chart title. 377 | subtitle : bool, default True 378 | Toggle the display of last price and/or volume in chart. 379 | log : bool 380 | Toggle logarithmic y-axis. Default False. 381 | hovermode : {'x', 'y', 'closest', False} 382 | Toggle how a tooltip appears on cursor hover. 383 | legend : dict, Legend or bool, default True 384 | True/False or Plotly legend dict / graph_objs.Legend object. 385 | If legend is bool, Quantmod will only toggle legend visibility. 386 | annotations : list or Annotations 387 | Plotly annotations list / graph.objs.Annotations object. 388 | shapes : list or Shapes 389 | Plotly shapes list or graph_objs.Shapes object. 390 | dimensions : tuple 391 | Dimensions 2-tuple in order (width, height). 392 | Disables autosize=True in plotly. 393 | width : int 394 | Width of chart. Default 1080 pixels. 395 | If used with height, disables autosize=True (Equivalent to 396 | using dimensions). 397 | height : int 398 | Height of chart. Default 720 pixels. 399 | If used with width, disables autosize=True (Equivalent to 400 | using dimensions). 401 | margin : dict or tuple 402 | Plotly margin dict or 4-tuple in order (l, r, b, t) or 403 | 5-tuple in order (l, r, b, t, margin). Tuple input added for 404 | Cufflinks compatibility. 405 | 406 | Examples 407 | -------- 408 | ch = qm.Chart(df) 409 | ch.to_figure(type='ohlc', dimensions=(2560,1440)) 410 | 411 | ch = qm.Chart(df) 412 | ch.add_BBANDS() 413 | ch.add_RSI(14) 414 | ch.to_figure(type='candlestick', title='EQUITY') 415 | 416 | """ 417 | # Check for kwargs integrity 418 | for key in kwargs: 419 | if key not in VALID_FIGURE_KWARGS: 420 | raise Exception("Invalid keyword '{0}'.".format(key)) 421 | 422 | # Kwargs renaming 423 | if 'kind' in kwargs: 424 | type = kwargs['kind'] 425 | 426 | if 'showlegend' in kwargs: 427 | legend = kwargs['showlegend'] 428 | 429 | if 'figsize' in kwargs: # Matplotlib 430 | figsize = kwargs['figsize'] 431 | if isinstance(figsize, tuple): 432 | if len(figsize) == 2: 433 | dimensions = tuple(80 * i for i in figsize) # 80x size 434 | else: 435 | raise Exception("Invalid figsize '{0}'. " 436 | "It should be tuple of len 2." 437 | .format(figsize)) 438 | else: 439 | raise TypeError("Invalid figsize '{0}'. " 440 | "It should be tuple." 441 | .format(figsize)) 442 | 443 | # Default argument values 444 | if type is None: 445 | if self.has_OHLC: 446 | type = 'candlestick' 447 | elif self.has_close: 448 | type = 'line' 449 | else: 450 | raise Exception("Chart has neither OLHC nor close data.") 451 | 452 | if volume is None: 453 | if self.has_volume: 454 | volume = True 455 | else: 456 | volume = False 457 | 458 | if title is None: 459 | if self.ticker: 460 | title = self.ticker 461 | else: 462 | title = '' 463 | 464 | if subtitle is None: 465 | subtitle = True 466 | 467 | if log is None: 468 | log = False 469 | 470 | if legend is None: 471 | legend = True 472 | 473 | # Type checks for mandatorily used arguments 474 | if not isinstance(type, six.string_types): 475 | raise TypeError("Invalid type '{0}'. " 476 | "It should be string." 477 | .format(type)) 478 | if type not in VALID_TRACES: 479 | raise Exception("Invalid keyword '{0}'. " 480 | "It is not in VALID_TRACES." 481 | .format(type)) 482 | if type in OHLC_TRACES: 483 | if not self.has_OHLC: 484 | raise Exception("Insufficient data for '{}'. " 485 | "Chart does not have OHLC data." 486 | .format(type)) 487 | else: 488 | if not self.has_close: 489 | raise Exception("Insufficient data for '{}'. " 490 | "Chart does not have close data." 491 | .format(type)) 492 | 493 | if not isinstance(volume, bool): 494 | raise TypeError("Invalid volume'{0}'. " 495 | "It should be bool." 496 | .format(volume)) 497 | 498 | if not isinstance(subtitle, bool): 499 | raise TypeError("Invalid subtitle'{0}'. " 500 | "It should be bool." 501 | .format(subtitle)) 502 | 503 | if not isinstance(log, bool): 504 | raise TypeError("Invalid subtitle'{0}'. " 505 | "It should be bool." 506 | .format(subtitle)) 507 | 508 | # Get template and bind to colors, traces, additions and layotu 509 | template = factory.get_template(theme=theme, layout=layout, 510 | title=title, 511 | hovermode=hovermode, legend=legend, 512 | annotations=annotations, shapes=shapes, 513 | dimensions=dimensions, 514 | width=width, height=height, 515 | margin=margin) 516 | colors = template['colors'] 517 | traces = template['traces'] 518 | additions = template['additions'] 519 | layout = template['layout'] 520 | 521 | # Get data 522 | data = [] 523 | 524 | # Plot main chart 525 | if type in OHLC_TRACES: 526 | trace = copy.deepcopy(traces[type]) 527 | 528 | trace['x'] = self.df.index 529 | trace['open'] = self.df[self.op] 530 | trace['high'] = self.df[self.hi] 531 | trace['low'] = self.df[self.lo] 532 | trace['close'] = self.df[self.cl] 533 | trace['name'] = title 534 | trace['yaxis'] = 'y1' 535 | trace['showlegend'] = False 536 | 537 | # Colors 538 | if type == 'candlestick': 539 | trace['increasing']['fillcolor'] = colors['increasing'] 540 | trace['increasing']['line']['color'] = colors['border_increasing'] # noqa: E501 541 | trace['decreasing']['fillcolor'] = colors['decreasing'] 542 | trace['decreasing']['line']['color'] = colors['border_decreasing'] # noqa: E501 543 | 544 | if type == 'ohlc': 545 | trace['increasing']['line']['color'] = colors['increasing'] 546 | trace['decreasing']['line']['color'] = colors['decreasing'] 547 | 548 | data.append(trace) 549 | 550 | elif 'line' in type: 551 | trace = copy.deepcopy(traces[type]) 552 | 553 | trace['x'] = self.df.index 554 | trace['y'] = self.df[self.cl] 555 | trace['name'] = title 556 | trace['yaxis'] = 'y1' 557 | trace['showlegend'] = False 558 | 559 | # Colors 560 | trace['line']['color'] = colors['primary'] 561 | 562 | data.append(trace) 563 | 564 | elif 'area' in type: 565 | trace = copy.deepcopy(traces[type]) 566 | 567 | trace['x'] = self.df.index 568 | trace['y'] = self.df[self.cl] 569 | trace['name'] = title 570 | trace['yaxis'] = 'y1' 571 | trace['showlegend'] = False 572 | 573 | # Colors 574 | trace['line']['color'] = colors['primary'] 575 | 576 | data.append(trace) 577 | 578 | elif 'scatter' in type: 579 | trace = copy.deepcopy(traces[type]) 580 | 581 | trace['x'] = self.df.index 582 | trace['y'] = self.df[self.cl] 583 | trace['name'] = title 584 | trace['yaxis'] = 'y1' 585 | trace['showlegend'] = False 586 | 587 | # Colors 588 | trace['scatter']['color'] = colors['primary'] 589 | 590 | data.append(trace) 591 | 592 | else: 593 | raise Exception("Cannot plot this chart type '{0}'.".format(type)) 594 | 595 | # Plot primary indicators 596 | for name in self.pri: 597 | primary = self.pri[name] 598 | trace = copy.deepcopy(traces[primary['type']]) 599 | 600 | trace['x'] = self.ind.index 601 | trace['y'] = self.ind[name] 602 | trace['name'] = name 603 | 604 | # Colors 605 | if 'line' in primary['type']: 606 | trace['line']['color'] = colors[primary['color']] 607 | elif 'area' in primary['type']: 608 | trace['line']['color'] = colors[primary['color']] 609 | if 'fillcolor' in primary: 610 | trace['fillcolor'] = colors[primary['fillcolor']] 611 | elif 'scatter' in primary['type']: 612 | trace['marker']['color'] = colors[primary['color']] 613 | elif 'bar' or 'histogram' in primary['type']: 614 | trace['marker']['color'] = colors[primary['color']] 615 | else: 616 | raise Exception("Invalid chart type {0}." 617 | .format(primary['type'])) 618 | 619 | trace['yaxis'] = 'y1' 620 | 621 | data.append(trace) 622 | 623 | # Plot volume 624 | if volume: 625 | trace = copy.deepcopy(traces['bar']) 626 | 627 | trace['x'] = self.df.index 628 | trace['y'] = self.df[self.vo] 629 | trace['name'] = 'Volume' 630 | 631 | # Determine if volume should be in 2 colors or in 1 632 | if type in OHLC_TRACES and self.has_open and self.has_close: 633 | volume_color = [ 634 | colors['increasing'] 635 | if (value - self.df[self.op].values[i]) >= 0 636 | else colors['decreasing'] 637 | for i, value in enumerate(self.df[self.cl].values) 638 | ] 639 | border_color = [ 640 | colors['border_increasing'] 641 | if (value - self.df[self.op].values[i]) >= 0 642 | else colors['border_decreasing'] 643 | for i, value in enumerate(self.df[self.cl].values) 644 | ] 645 | else: 646 | volume_color = colors['primary'] 647 | 648 | if type == 'candlestick': 649 | trace['marker']['color'] = volume_color 650 | trace['marker']['line']['color'] = border_color 651 | else: 652 | trace['marker']['color'] = volume_color 653 | trace['marker']['line']['color'] = volume_color 654 | 655 | trace['yaxis'] = 'y2' 656 | trace['showlegend'] = False 657 | 658 | data.append(trace) 659 | 660 | # Subplot volume delta 661 | if volume: 662 | delta = 1 663 | else: 664 | delta = 0 665 | 666 | # Plot non-overlaid secondary indicators 667 | i = delta 668 | overlays = [] 669 | axes = {} # Axes mapping for overlays 670 | 671 | for name in self.sec: 672 | secondary = self.sec[name] 673 | 674 | if 'on' in secondary: 675 | if secondary['on'] is not None: 676 | overlays.append(name) 677 | else: 678 | trace = copy.deepcopy(traces[secondary['type']]) 679 | 680 | trace['x'] = self.ind.index 681 | trace['y'] = self.ind[name] 682 | trace['name'] = name 683 | 684 | # Colors 685 | if 'line' in secondary['type']: 686 | trace['line']['color'] = colors[secondary['color']] 687 | elif 'area' in secondary['type']: 688 | trace['line']['color'] = colors[secondary['color']] 689 | if 'fillcolor' in secondary: 690 | trace['fillcolor'] = colors[secondary['fillcolor']] 691 | elif 'scatter' in secondary['type']: 692 | trace['marker']['color'] = colors[secondary['color']] 693 | elif 'bar' or 'histogram' in secondary['type']: 694 | trace['marker']['color'] = colors[secondary['color']] 695 | else: 696 | raise Exception("Invalid chart type {0}." 697 | .format(secondary['type'])) 698 | 699 | axes[name] = 'y{0}'.format(i + 2) 700 | trace['yaxis'] = axes[name] 701 | data.append(trace) 702 | i += 1 703 | 704 | # Plot overlaid secondary indicators 705 | for name in overlays: 706 | secondary = self.sec[name] 707 | trace = copy.deepcopy(traces[secondary['type']]) 708 | 709 | trace['x'] = self.ind.index 710 | trace['y'] = self.ind[name] 711 | trace['name'] = name 712 | 713 | # Colors 714 | if 'line' in secondary['type']: 715 | trace['line']['color'] = colors[secondary['color']] 716 | elif 'area' in secondary['type']: 717 | trace['line']['color'] = colors[secondary['color']] 718 | if 'fillcolor' in secondary: 719 | trace['fillcolor'] = colors[secondary['fillcolor']] 720 | elif 'scatter' in secondary['type']: 721 | trace['marker']['color'] = colors[secondary['color']] 722 | elif 'bar' or 'histogram' in secondary['type']: 723 | trace['marker']['color'] = colors[secondary['color']] 724 | else: 725 | raise Exception("Invalid chart type {0}." 726 | .format(secondary['type'])) 727 | 728 | axes[name] = axes[secondary['on']] 729 | trace['yaxis'] = axes[name] 730 | data.append(trace) 731 | 732 | # Modify layout 733 | 734 | # Axis 735 | layout['xaxis'] = copy.deepcopy(additions['xaxis']) 736 | layout['yaxis'] = copy.deepcopy(additions['yaxis']) 737 | 738 | layout['yaxis']['side'] = 'right' 739 | if log: 740 | layout['yaxis']['type'] = 'log' 741 | 742 | # Subaxis 743 | 744 | # TO CHANGE 745 | if volume or (len(self.sec) - len(overlays)): 746 | 747 | n = len(self.sec) - len(overlays) + delta 748 | 749 | if n == 1: 750 | layout['yaxis']['domain'] = [0.30, 1.0] 751 | layout['yaxis2'] = copy.deepcopy(additions['yaxis']) 752 | layout['yaxis2']['domain'] = [0.0, 0.29] 753 | layout['xaxis']['anchor'] = 'y2' 754 | 755 | elif n == 2: 756 | layout['yaxis']['domain'] = [0.50, 1.0] 757 | layout['yaxis2'] = copy.deepcopy(additions['yaxis']) 758 | layout['yaxis2']['domain'] = [0.25, 0.49] 759 | layout['yaxis3'] = copy.deepcopy(additions['yaxis']) 760 | layout['yaxis3']['domain'] = [0.0, 0.24] 761 | layout['xaxis']['anchor'] = 'y3' 762 | 763 | elif n > 2: 764 | # One main plot, n gaps and sub plots 765 | main_height = 0.5 * layout['height'] 766 | sub_height = 0.24 * layout['height'] 767 | gap_height = 0.01 * layout['height'] 768 | new_height = main_height + n * (gap_height + sub_height) 769 | 770 | main = main_height/new_height 771 | sub = sub_height/new_height 772 | gap = gap_height/new_height 773 | 774 | # Main plot 775 | upper = 1.0 776 | lower = upper - main 777 | layout['yaxis']['domain'] = [lower, upper] 778 | 779 | # Subplots 780 | for i in range(n): 781 | upper = lower - gap 782 | lower = upper - sub 783 | yaxisn = 'yaxis{0}'.format(i + 2) 784 | layout[yaxisn] = copy.deepcopy(additions['yaxis']) 785 | layout[yaxisn]['domain'] = [lower, upper] 786 | 787 | layout['xaxis']['anchor'] = 'y{0}'.format(n + 1) 788 | layout['height'] = new_height 789 | 790 | else: 791 | raise Exception("Invalid subplot count {0}.".format(n)) 792 | 793 | # Margin 794 | if not layout['title']: 795 | layout['margin']['t'] = layout['margin']['b'] 796 | 797 | # Subtitle 798 | if layout['showlegend'] and subtitle: 799 | 800 | if 'annotations' not in layout: 801 | layout['annotations'] = [] 802 | 803 | if type in OHLC_TRACES: 804 | if (self.df[self.cl][-1] - self.df[self.op].values[-1]) >= 0: 805 | annotations_color = colors['increasing'] 806 | else: 807 | annotations_color = colors['decreasing'] 808 | else: 809 | annotations_color = colors['primary'] 810 | 811 | last_price = dict( 812 | x=layout['legend']['x'], 813 | xanchor=layout['legend']['xanchor'], 814 | xref='paper', 815 | y=layout['legend']['y'], 816 | yanchor=layout['legend']['yanchor'], 817 | yref='paper', 818 | showarrow=False, 819 | text='Last {0:,.02f}'.format(self.df[self.cl][-1]), 820 | font=dict(color=annotations_color), 821 | ) 822 | layout['annotations'].append(last_price) 823 | layout['legend']['y'] -= 0.03 824 | 825 | if volume: 826 | last_volume = dict( 827 | x=layout['legend']['x'], 828 | xanchor=layout['legend']['xanchor'], 829 | xref='paper', 830 | y=layout['yaxis2']['domain'][-1] - 0.01, 831 | yanchor=layout['legend']['yanchor'], 832 | yref='paper', 833 | showarrow=False, 834 | text='Volume {0:,}'.format(self.df[self.vo][-1]), 835 | font=dict(color=annotations_color), 836 | ) 837 | layout['annotations'].append(last_volume) 838 | 839 | figure = dict(data=data, layout=layout) 840 | return figure 841 | 842 | def plot(self, type=None, volume=None, 843 | theme=None, layout=None, 844 | title=None, subtitle=None, log=None, hovermode=None, 845 | legend=None, annotations=None, shapes=None, 846 | dimensions=None, width=None, height=None, margin=None, 847 | filename=None, online=None, **kwargs): 848 | """Generate a Plotly chart from Chart specifications. 849 | 850 | Parameters 851 | ---------- 852 | type : {'ohlc', 'candlestick', 853 | 'line', 'line_thin', 'line_thick', 'line_dashed', 854 | 'line_dashed_thin', 'line_dashed_thick', 855 | 'area', 'area_dashed', 856 | 'area_dashed_thin', 'area_dashed_thick', 'area_threshold', 857 | 'scatter'} 858 | Determine the chart type of the main trace. For candlestick 859 | and OHLC bars Chart needs to have OHLC enabled. 860 | volume : bool 861 | Toggle the diplay of a volume subplot in chart. Default True. 862 | log : bool 863 | Toggle logarithmic y-axis. Default False. 864 | theme : string 865 | Quantmod theme. 866 | layout : dict or Layout 867 | Plotly layout dict or graph_objs.Layout object. 868 | Will override all other arguments if conflicting as 869 | user-inputted layout is updated last. 870 | title : string 871 | Chart title. 872 | subtitle : bool, default True 873 | Toggle the display of last price and/or volume in chart. 874 | log : bool 875 | Toggle logarithmic y-axis. Default False. 876 | hovermode : {'x', 'y', 'closest', False} 877 | Toggle how a tooltip appears on cursor hover. 878 | legend : dict, Legend or bool, default True 879 | True/False or Plotly legend dict / graph_objs.Legend object. 880 | If legend is bool, Quantmod will only toggle legend visibility. 881 | annotations : list or Annotations 882 | Plotly annotations list / graph.objs.Annotations object. 883 | shapes : list or Shapes 884 | Plotly shapes list or graph_objs.Shapes object. 885 | dimensions : tuple 886 | Dimensions 2-tuple in order (width, height). 887 | Disables autosize=True. 888 | width : int 889 | Width of chart. Default 1080 pixels. 890 | If used with height, disables autosize=True (Equivalent to 891 | using dimensions). 892 | height : int 893 | Height of chart. Default 720 pixels. 894 | If used with width, disables autosize=True (Equivalent to 895 | using dimensions). 896 | margin : dict or tuple 897 | Plotly margin dict or 4-tuple in order (l, r, b, t) or 898 | 5-tuple in order (l, r, b, t, margin). Tuple input added for 899 | Cufflinks compatibility. 900 | filename : string, default datetime.now() 901 | Filename of chart that will appear on plot.ly. 902 | By default, filename is set according to current system time. 903 | online : bool, default False 904 | If True, forces chart to be drawn online even if 905 | qm.go_offline() has been called. 906 | 907 | Examples 908 | -------- 909 | ch = qm.Chart(df) 910 | ch.plot(type='ohlc', dimensions=(2560,1440)) 911 | 912 | ch = qm.Chart(df) 913 | ch.add_BBANDS() 914 | ch.add_RSI(14) 915 | ch.plot(type='candlestick', title='EQUITY') 916 | 917 | """ 918 | figure = self.to_figure(type=type, volume=volume, 919 | theme=theme, layout=layout, 920 | title=title, subtitle=subtitle, log=log, 921 | hovermode=hovermode, legend=legend, 922 | annotations=annotations, shapes=shapes, 923 | dimensions=dimensions, 924 | width=width, height=height, 925 | margin=margin, **kwargs) 926 | 927 | # Default argument values 928 | 929 | # To be fixed ASAP: race condition if 2 plots made within 1 sec 930 | # Link filename generation to streambed API to prevent overwriting 931 | if filename is None: 932 | timestamp = dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 933 | filename = 'Quantmod Chart {0}'.format(timestamp) 934 | 935 | if online is None: 936 | online = False 937 | 938 | # Type checks for mandatorily used arguments 939 | if not isinstance(filename, six.string_types): 940 | raise TypeError("Invalid filename '{0}'. " 941 | "It should be string." 942 | .format(filename)) 943 | 944 | if not isinstance(online, bool): 945 | raise TypeError("Invalid online '{0}'. " 946 | "It should be bool." 947 | .format(online)) 948 | 949 | if tools.is_offline() and not online: 950 | show_link = tools.get_config_file()['offline_show_link'] 951 | link_text = tools.get_config_file()['offline_link_text'] 952 | return pyo.plot(figure, filename=filename, 953 | show_link=show_link, 954 | link_text=link_text) 955 | else: 956 | return py.plot(figure, filename=filename) 957 | 958 | def iplot(self, type=None, volume=None, log=None, 959 | theme=None, layout=None, 960 | title=None, subtitle=None, hovermode=None, 961 | legend=None, annotations=None, shapes=None, 962 | dimensions=None, width=None, height=None, margin=None, 963 | filename=None, online=None, **kwargs): 964 | """Generate a Plotly chart from Chart specifications. 965 | 966 | The iplot function returns an embedded chart suitable for Jupyter 967 | notebooks, while the plot function simply opens it in the browser. 968 | 969 | Parameters 970 | ---------- 971 | type : {'ohlc', 'candlestick', 972 | 'line', 'line_thin', 'line_thick', 'line_dashed', 973 | 'line_dashed_thin', 'line_dashed_thick', 974 | 'area', 'area_dashed', 975 | 'area_dashed_thin', 'area_dashed_thick', 'area_threshold', 976 | 'scatter'} 977 | Determine the chart type of the main trace. For candlestick 978 | and OHLC bars Chart needs to have OHLC enabled. 979 | volume : bool 980 | Toggle the diplay of a volume subplot in chart. Default True. 981 | log : bool 982 | Toggle logarithmic y-axis. Default False. 983 | theme : string 984 | Quantmod theme. 985 | layout : dict or Layout 986 | Plotly layout dict or graph_objs.Layout object. 987 | Will override all other arguments if conflicting as 988 | user-inputted layout is updated last. 989 | title : string 990 | Chart title. 991 | subtitle : bool, default True 992 | Toggle the display of last price and/or volume in chart. 993 | log : bool 994 | Toggle logarithmic y-axis. Default False. 995 | hovermode : {'x', 'y', 'closest', False} 996 | Toggle how a tooltip appears on cursor hover. 997 | legend : dict, Legend or bool, default True 998 | True/False or Plotly legend dict / graph_objs.Legend object. 999 | If legend is bool, Quantmod will only toggle legend visibility. 1000 | annotations : list or Annotations 1001 | Plotly annotations list / graph.objs.Annotations object. 1002 | shapes : list or Shapes 1003 | Plotly shapes list or graph_objs.Shapes object. 1004 | dimensions : tuple 1005 | Dimensions 2-tuple in order (width, height). 1006 | Disables autosize=True. 1007 | width : int 1008 | Width of chart. Default 1080 pixels. 1009 | If used with height, disables autosize=True (Equivalent to 1010 | using dimensions). 1011 | height : int 1012 | Height of chart. Default 720 pixels. 1013 | If used with width, disables autosize=True (Equivalent to 1014 | using dimensions). 1015 | margin : dict or tuple 1016 | Plotly margin dict or 4-tuple in order (l, r, b, t) or 1017 | 5-tuple in order (l, r, b, t, margin). Tuple input added for 1018 | Cufflinks compatibility. 1019 | filename : string, default datetime.now() 1020 | Filename of chart that will appear on plot.ly. 1021 | By default, filename is set according to current system time. 1022 | online : bool, default False 1023 | If True, forces chart to be drawn online even if 1024 | qm.go_offline() has been called. 1025 | 1026 | Examples 1027 | -------- 1028 | ch = qm.Chart(df) 1029 | ch.iplot(type='ohlc', dimensions=(2560,1440)) 1030 | 1031 | ch = qm.Chart(df) 1032 | ch.add_BBANDS() 1033 | ch.add_RSI(14) 1034 | ch.iplot(type='candlestick', title='EQUITY') 1035 | 1036 | """ 1037 | figure = self.to_figure(type=type, volume=volume, 1038 | theme=theme, layout=layout, 1039 | title=title, subtitle=subtitle, log=log, 1040 | hovermode=hovermode, legend=legend, 1041 | annotations=annotations, shapes=shapes, 1042 | dimensions=dimensions, 1043 | width=width, height=height, 1044 | margin=margin, **kwargs) 1045 | 1046 | # Default argument values 1047 | if filename is None: 1048 | timestamp = dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 1049 | filename = 'Quantmod Chart {0}'.format(timestamp) 1050 | 1051 | if online is None: 1052 | online = False 1053 | 1054 | # Type checks for mandatorily used arguments 1055 | if not isinstance(filename, six.string_types): 1056 | raise TypeError("Invalid filename '{0}'. " 1057 | "It should be string." 1058 | .format(filename)) 1059 | 1060 | if not isinstance(online, bool): 1061 | raise TypeError("Invalid online '{0}'. " 1062 | "It should be bool." 1063 | .format(online)) 1064 | 1065 | if tools.is_offline() and not online: 1066 | show_link = tools.get_config_file()['offline_show_link'] 1067 | link_text = tools.get_config_file()['offline_link_text'] 1068 | return pyo.iplot(figure, filename=filename, 1069 | show_link=show_link, 1070 | link_text=link_text) 1071 | else: 1072 | return py.iplot(figure, filename=filename) 1073 | 1074 | 1075 | Chart.add_MA = add_MA # noqa : F405 1076 | Chart.add_SMA = add_SMA # noqa : F405 1077 | Chart.add_EMA = add_EMA # noqa : F405 1078 | Chart.add_WMA = add_WMA # noqa : F405 1079 | Chart.add_DEMA = add_DEMA # noqa : F405 1080 | Chart.add_TEMA = add_TEMA # noqa : F405 1081 | Chart.add_T3 = add_T3 # noqa : F405 1082 | Chart.add_KAMA = add_KAMA # noqa : F405 1083 | Chart.add_TRIMA = add_TRIMA # noqa : F405 1084 | Chart.add_MAMA = add_MAMA # noqa : F405 1085 | Chart.add_MAVP = add_MAVP # noqa : F405 1086 | 1087 | Chart.add_BBANDS = add_BBANDS # noqa : F405 1088 | Chart.add_HT_TRENDLINE = add_HT_TRENDLINE # noqa : F405, add_ht_trendline 1089 | Chart.add_MIDPOINT = add_MIDPOINT # noqa : F405, add_midpoint 1090 | Chart.add_SAR = add_SAR # noqa : F405 1091 | Chart.add_SAREXT = add_SAREXT # noqa : F405 1092 | 1093 | 1094 | Chart.add_APO = add_APO # noqa : F405, HL 0 (MACD no signal line) 1095 | Chart.add_AROON = add_AROON # noqa : F405, 1096 | Chart.add_AROONOSC = add_AROONOSC # noqa : F405, area (HL 0) 1097 | Chart.add_BOP = add_BOP # noqa : F405, smoothing, volume color (HL 0) 1098 | Chart.add_CCI = add_CCI # noqa : F405, band range color 1099 | Chart.add_CMO = add_CMO # noqa : F405, HL 0, HL +- 50 1100 | Chart.add_ADX = add_ADX # noqa : F405, ADX + +-DI, HL +20 1101 | Chart.add_ADXR = add_ADXR # noqa : F405, ADX + ADXR 1102 | Chart.add_DX = add_DX # noqa : F405, 1103 | Chart.add_MINUS_DI = add_MINUS_DI # noqa : F405 1104 | Chart.add_PLUS_DI = add_PLUS_DI # noqa : F405 1105 | Chart.add_MINUS_DM = add_MINUS_DM # noqa : F405 1106 | Chart.add_PLUS_DM = add_PLUS_DM # noqa : F405 1107 | Chart.add_MACD = add_MACD # noqa : F405 1108 | Chart.add_MACDEXT = add_MACDEXT # noqa : F405 1109 | # Chart.add_MACDFIX = add_MACDEFIX # noqa : F405, not present 1110 | Chart.add_MFI = add_MFI # noqa : F405, band range color, HL 20, HL 80 1111 | Chart.add_MOM = add_MOM # noqa : F405, (HL 0) 1112 | Chart.add_PPO = add_PPO # noqa : F405, PPO + Signal + Histogram 1113 | Chart.add_ROC = add_ROC # noqa : F405 1114 | Chart.add_ROCP = add_ROCP # noqa : F405 1115 | Chart.add_ROCR = add_ROCR # noqa : F405 1116 | Chart.add_ROCR100 = add_ROCR100 # noqa : F405 1117 | Chart.add_RSI = add_RSI # noqa : F405, band range color, HL 30, HL 70 1118 | Chart.add_STOCH = add_STOCH # noqa : F405, HL 20, HL 80 1119 | Chart.add_STOCHF = add_STOCHF # noqa : F405 1120 | Chart.add_STOCHRSI = add_STOCHRSI # noqa : F405 1121 | Chart.add_TRIX = add_TRIX # noqa : F405, + Signal, area (HL 0) 1122 | Chart.add_ULTOSC = add_ULTOSC # noqa : F405, band range color, HL 30, HL 70 1123 | Chart.add_WILLR = add_WILLR # noqa : F405, band range color, HL 20, HL 80 1124 | -------------------------------------------------------------------------------- /quantmod/core.py: -------------------------------------------------------------------------------- 1 | """Core Quantmod functions 2 | 3 | Contains some wrappers over 'chart.py' for those used to R's Quantmod style, 4 | along with other tools for financial data acquisition. 5 | 6 | """ 7 | from __future__ import absolute_import 8 | 9 | import six 10 | import datetime as dt 11 | import pandas_datareader.data as web 12 | 13 | from .chart import Chart 14 | 15 | 16 | def get_symbol(ticker, src='yahoo', start='01/01/2010', 17 | end=dt.datetime.today(), to_frame=False): 18 | """Get symbols 19 | 20 | Currently just a wrapper over pandas_datareader.data.DataReader. 21 | 22 | Parameters 23 | ---------- 24 | ticker : string or list 25 | Stock ticker or list of stock tickers to fetch from. 26 | src : string, default 'yahoo' 27 | String specifying the data source to fetch data from. 28 | start : string or datetime 29 | Left boundary for date range, specified either as string or as a 30 | datetime object. Defaults to 1/1/2010. 31 | end : string or datetime 32 | Right boundary for date range, specified either as string or as a 33 | datetime object. Defaults to datetime.today(). 34 | to_frame : bool, default False 35 | If True, returns obtained symbols as a DataFrame instead of a 36 | Quantmod Chart. 37 | 38 | Examples 39 | -------- 40 | # Get Apple stock 41 | df = get_symbols('AAPL') 42 | 43 | # Get Apple stock from Google and return DataFrame 44 | df = get_symbols('AAPL', 'google', to_frame=True) 45 | 46 | # Get the VIX from Fred 47 | df = get_symbols('VIX', 'fred') 48 | 49 | Returns 50 | ------- 51 | symbol : DataFrame 52 | 53 | """ 54 | # Type checks for mandatorily used arguments 55 | if isinstance(ticker, six.string_types): 56 | pass 57 | elif isinstance(ticker, list): 58 | pass 59 | else: 60 | raise TypeError("Invalid ticker '{0}'. " 61 | "It should be string or dict.".format(ticker)) 62 | 63 | if isinstance(src, six.string_types): 64 | pass 65 | elif isinstance(src, dict): 66 | pass 67 | else: 68 | raise TypeError("Invalid src '{0}'. " 69 | "It should be string or dict.".format(src)) 70 | 71 | if isinstance(start, six.string_types): 72 | pass 73 | elif isinstance(start, dt.datetime) or isinstance(start, dt.date): 74 | pass 75 | else: 76 | raise TypeError("Invalid start '{0}'. " 77 | "It should be string or datetime.".format(start)) 78 | 79 | if isinstance(end, six.string_types): 80 | pass 81 | elif isinstance(end, dt.datetime) or isinstance(end, dt.date): 82 | pass 83 | else: 84 | raise TypeError("Invalid end '{0}'. " 85 | "It should be string or datetime.".format(end)) 86 | 87 | if not isinstance(to_frame, bool): 88 | raise TypeError("Invalid to_frame '{0}'. " 89 | "It should be bool.".format(to_frame)) 90 | 91 | symbols = web.DataReader(ticker, data_source=src, start=start, end=end) 92 | 93 | if not to_frame: 94 | symbols = Chart(symbols, ticker=ticker, src=src, start=start, end=end) 95 | 96 | return symbols 97 | 98 | 99 | def chart_series(chart, iplot=False, **kwargs): 100 | """Wrapper over Chart.plot() and Chart.iplot(). 101 | 102 | Parameters 103 | ---------- 104 | chart : Quantmod Chart 105 | Quantmod Chart to plot with. 106 | iplot : bool, default False 107 | If True, plots chart for interactive display 108 | inside Jupyter notebooks instead of opening a browser HTML. 109 | 110 | """ 111 | if not isinstance(chart, Chart): 112 | raise TypeError("Invalid chart '{0}'. " 113 | "It should be Chart.".format(chart)) 114 | 115 | if not isinstance(iplot, bool): 116 | raise TypeError("Invalid to_frame '{0}'. " 117 | "It should be bool.".format(iplot)) 118 | 119 | if iplot: 120 | Chart.iplot(**kwargs) 121 | else: 122 | Chart.plot(**kwargs) 123 | -------------------------------------------------------------------------------- /quantmod/datetools.py: -------------------------------------------------------------------------------- 1 | """Date and time functions 2 | 3 | Refactored from Cufflinks' 'date_tools.py' module. 4 | Credits to @jorgesantos. 5 | 6 | """ 7 | import datetime as dt 8 | 9 | 10 | def get_date_from_today(delta, strfmt='%Y%m%d'): 11 | """ Returns a string that represents a date n numbers of days from today. 12 | 13 | Parameters 14 | ---------- 15 | delta : int 16 | number of days 17 | strfmt : string 18 | format in which the date will be represented 19 | 20 | """ 21 | return (dt.date.today() + dt.timedelta(delta)).strftime(strfmt) 22 | 23 | 24 | def string_to_date(string_date, strfmt='%Y%m%d'): 25 | """ Converts a string format date into datetime. 26 | 27 | Parameters 28 | ---------- 29 | string_date : string 30 | date in string format 31 | strfmt : string 32 | format in which the input date is represented 33 | 34 | """ 35 | return dt.datetime.strptime(string_date, strfmt).date() 36 | 37 | 38 | def int_to_date(int_date): 39 | """ Converts an int format date into datetime. 40 | 41 | Parameters 42 | ---------- 43 | int_date : int 44 | date in int format 45 | 46 | Example 47 | ------- 48 | int_date(20151023) 49 | 50 | """ 51 | return string_to_date(str(int_date)) 52 | 53 | 54 | def date_to_int(date, strfmt='%Y%m%d'): 55 | """ Converts a datetime date into int. 56 | 57 | Parameters 58 | ---------- 59 | date : datetime 60 | date in datetime format 61 | strfmt : string 62 | format in which the int date will be generated 63 | Example 64 | ------- 65 | date_to_int(dt.date(2015,10,23),'%Y') 66 | 67 | """ 68 | return int(date.strftime(strfmt)) 69 | -------------------------------------------------------------------------------- /quantmod/factory.py: -------------------------------------------------------------------------------- 1 | """High-level functions meant for user access 2 | 3 | This includes various plotting and theming helpers. 4 | Module also contains all argument validity checks. 5 | 6 | """ 7 | from __future__ import absolute_import 8 | 9 | import six 10 | import copy 11 | 12 | from . import utils 13 | from . import tools 14 | from .theming.skeleton import SKELETON 15 | from .theming.themes import THEMES 16 | from .vendors.sources import SOURCES 17 | from .valid import (VALID_COLORS, VALID_TRACES, 18 | VALID_LAYOUT, VALID_ADDITIONS, 19 | VALID_TEMPLATE_KWARGS, 20 | VALID_BASE_COMPONENTS, 21 | VALID_THEME_COMPONENTS) 22 | 23 | 24 | def get_theme(theme): 25 | """Return a Quantmod theme (as a dict). 26 | 27 | Parameters 28 | ---------- 29 | theme : string 30 | Quantmod theme. 31 | 32 | """ 33 | if theme in THEMES: 34 | return copy.deepcopy(THEMES[theme]) 35 | else: 36 | raise Exception("Theme not found '{0}'.".format(theme)) 37 | 38 | 39 | def get_themes(): 40 | """Return the list of available themes, or none if there is a problem.""" 41 | return list(THEMES) 42 | 43 | 44 | def get_skeleton(): 45 | """Return the base Quantmod skeleton.""" 46 | return copy.deepcopy(SKELETON) 47 | 48 | 49 | def get_source(source): 50 | """Return a Quantmod source (as a dict). 51 | 52 | Parameters 53 | ---------- 54 | source : string 55 | Quantmod source. 56 | 57 | """ 58 | if source in SOURCES: 59 | return copy.deepcopy(SOURCES[source]) 60 | else: 61 | raise Exception("Source not found '{0}'.".format(source)) 62 | 63 | 64 | def get_sources(): 65 | """Return the list of available sources, or none if there is a problem.""" 66 | return list(SOURCES) 67 | 68 | 69 | def make_colors(base_colors, colors): 70 | """Make trace configuration from theme/skeleton and theme/colors. 71 | 72 | Recursively update base_theme with theme using custom tool in utils. 73 | 74 | Parameters 75 | ---------- 76 | base_colors : dict 77 | Additions file containing primitives from 'skeleton.py'. 78 | colors : dict 79 | Additions configuration from specified theme. 80 | 81 | """ 82 | for key in colors: 83 | if key not in VALID_COLORS: 84 | raise Exception("Invalid keyword '{0}'".format(key)) 85 | 86 | def _expand(base_colors): 87 | pass 88 | 89 | _expand(base_colors) 90 | 91 | # Modifiers directly to base_colors 92 | utils.update(base_colors, colors) 93 | 94 | for key in base_colors: 95 | if key not in VALID_COLORS: 96 | raise Exception("Invalid keyword '{0}'".format(key)) 97 | 98 | return base_colors 99 | 100 | 101 | def make_traces(base_traces, traces): 102 | """Make trace configuration from theme/skeleton and theme/traces. 103 | 104 | Recursively update base_theme with theme using custom tool in utils. 105 | 106 | Parameters 107 | ---------- 108 | base_traces : dict 109 | Trace file containing primitives from 'skeleton.py'. 110 | traces : dict 111 | Trace configuration from specified theme. 112 | 113 | """ 114 | # Check for invalid entries 115 | for key in traces: 116 | if key not in VALID_TRACES: 117 | raise Exception("Invalid keyword '{0}'".format(key)) 118 | 119 | def _expand(base_traces): 120 | """Creates other traces from the three elementary ones.""" 121 | base_traces['candlestick'] 122 | 123 | base_traces['line'] 124 | base_traces['line_thin'] = copy.deepcopy(base_traces['line']) 125 | base_traces['line_thick'] = copy.deepcopy(base_traces['line']) 126 | base_traces['line_dashed'] = copy.deepcopy(base_traces['line']) 127 | base_traces['line_dashed_thin'] = copy.deepcopy(base_traces['line']) 128 | base_traces['line_dashed_thick'] = copy.deepcopy(base_traces['line']) 129 | 130 | base_traces['area'] = copy.deepcopy(base_traces['line']) 131 | base_traces['area']['fill'] = 'tonexty' 132 | base_traces['area_dashed'] = copy.deepcopy(base_traces['area']) 133 | base_traces['area_dashed_thin'] = copy.deepcopy(base_traces['area']) 134 | base_traces['area_dashed_thick'] = copy.deepcopy(base_traces['area']) 135 | base_traces['area_threshold'] = copy.deepcopy(base_traces['area']) 136 | 137 | base_traces['scatter'] = copy.deepcopy(base_traces['line']) 138 | base_traces['scatter']['mode'] = 'markers' 139 | base_traces['scatter']['opacity'] = 1.0 140 | 141 | base_traces['bar'] 142 | base_traces['histogram'] = copy.deepcopy(base_traces['bar']) 143 | 144 | _expand(base_traces) 145 | 146 | # Mdifiers currently to 'line' only 147 | # This may be subject to laterchange 148 | for key in traces: 149 | utils.update(base_traces[key]['line'], traces[key]) 150 | 151 | # Check after copying 152 | for key in base_traces: 153 | if key not in VALID_TRACES: 154 | raise Exception("Invalid keyword '{0}'".format(key)) 155 | 156 | return base_traces 157 | 158 | 159 | def make_additions(base_additions, additions): 160 | """Make trace configuration from theme/skeleton and theme/additions. 161 | 162 | Recursively update base_theme with theme using custom tool in utils. 163 | 164 | Parameters 165 | ---------- 166 | base_additions : dict 167 | Additions file containing primitives from 'skeleton.py'. 168 | additions : dict 169 | Additions configuration from specified theme. 170 | 171 | """ 172 | for key in additions: 173 | if key not in VALID_ADDITIONS: 174 | raise Exception("Invalid keyword '{0}'".format(key)) 175 | 176 | # No utility right now, planned in the future for additions 177 | def _expand(base_additions): 178 | pass 179 | 180 | _expand(base_additions) 181 | 182 | # Modifiers directly to base_additions 183 | utils.update(base_additions, additions) 184 | 185 | for key in base_additions: 186 | if key not in VALID_ADDITIONS: 187 | raise Exception("Invalid keyword '{0}'".format(key)) 188 | 189 | return base_additions 190 | 191 | 192 | def make_layout(base_layout, layout, custom_layout, 193 | title, hovermode, 194 | legend, annotations, shapes, 195 | dimensions, width, height, margin, **kwargs): 196 | """Make layout configuration from theme/skeleton and theme/traces. 197 | 198 | Recursively update base_theme with theme using custom tool in utils. 199 | 200 | Parameters 201 | ---------- 202 | base_traces : dict 203 | Layout file containing primitives from 'skeleton.py'. 204 | layout : dict 205 | Layout configuration from specified theme. 206 | custom_layout : dict 207 | Plotly layout dict or graph_objs.Layout object. 208 | Will override all other arguments if conflicting as 209 | user-inputted layout is updated last. 210 | title : string 211 | Chart title. 212 | hovermode : {'x', 'y', 'closest', False} 213 | Toggle how a tooltip appears on cursor hover. 214 | legend : dict, Legend or bool 215 | True/False or Plotly legend dict / graph_objs.Legend object. 216 | If legend is bool, Quantmod will only toggle legend visibility. 217 | annotations : list 218 | Plotly annotations list. 219 | shapes : list or 220 | Plotly shapes list. 221 | dimensions : tuple 222 | Dimensions 2-tuple in order (width, height). 223 | Disables autosize=True. 224 | width : int 225 | Width of chart. Default 1080 pixels. 226 | If used with height, disables autosize=True (Equivalent to 227 | using dimensions). 228 | height : int 229 | Height of chart. Default 720 pixels. 230 | If used with width, disables autosize=True (Equivalent to 231 | using dimensions). 232 | margin : dict or tuple 233 | Plotly margin dict or 4-tuple in order (l, r, b, t) or 234 | 5-tuple in order (l, r, b, t, margin). Tuple input added for 235 | Cufflinks compatibility. 236 | 237 | """ 238 | # Check for kwargs integrity 239 | for key in kwargs: 240 | if key not in VALID_TEMPLATE_KWARGS: 241 | raise Exception("Invalid keyword '{0}'.".format(key)) 242 | 243 | # Kwargs 244 | if 'showlegend' in kwargs: 245 | legend = kwargs['showlegend'] 246 | 247 | if 'figsize' in kwargs: # Matplotlib 248 | figsize = kwargs['figsize'] 249 | if isinstance(figsize, tuple): 250 | if len(figsize) == 2: 251 | dimensions = tuple(80 * i for i in figsize) # 80x size 252 | else: 253 | raise Exception("Invalid figsize '{0}'. " 254 | "It should be tuple of len 2." 255 | .format(figsize)) 256 | else: 257 | raise TypeError("Invalid figsize '{0}'. " 258 | "It should be tuple." 259 | .format(figsize)) 260 | 261 | # Check for invalid entries 262 | for key in layout: 263 | if key not in VALID_LAYOUT: 264 | raise Exception("Invalid keyword '{0}'".format(key)) 265 | 266 | # No utility right now, planned in the future for additions 267 | def _expand(base_layout): 268 | pass 269 | 270 | _expand(base_layout) 271 | 272 | # Modifiers directly to base_layout 273 | utils.update(base_layout, layout) 274 | 275 | if title is not None: 276 | base_layout['title'] = title 277 | 278 | if hovermode is not None: 279 | base_layout['hovermode'] = hovermode 280 | 281 | if legend is not None: 282 | if legend is True: 283 | base_layout['showlegend'] = True 284 | elif legend is False: 285 | base_layout['showlegend'] = False 286 | else: 287 | base_layout['showlegend'] = True 288 | base_layout['legend'] = legend 289 | 290 | if annotations is not None: 291 | base_layout['annotations'] = annotations 292 | 293 | if shapes is not None: 294 | base_layout['shapes'] = shapes 295 | 296 | if dimensions is not None: 297 | base_layout['width'] = dimensions[0] 298 | base_layout['height'] = dimensions[1] 299 | base_layout['autosize'] = False 300 | 301 | if width is not None: 302 | base_layout['width'] = width 303 | 304 | if height is not None: 305 | base_layout['height'] = height 306 | 307 | if width is not None and height is not None: 308 | base_layout['autosize'] = False 309 | 310 | if margin is not None: 311 | base_layout['margin'] = margin 312 | 313 | # Custom layout update 314 | if custom_layout is not None: 315 | utils.update(layout, custom_layout) 316 | 317 | for key in base_layout: 318 | if key not in VALID_LAYOUT: 319 | raise Exception("Invalid keyword '{0}'".format(key)) 320 | 321 | return base_layout 322 | 323 | 324 | def get_template(theme=None, layout=None, 325 | title=None, hovermode=None, 326 | legend=None, annotations=None, shapes=None, 327 | dimensions=None, width=None, height=None, margin=None, 328 | **kwargs): 329 | """Generate color, traces, additions and layout dicts. 330 | 331 | Parameters 332 | ---------- 333 | theme : string 334 | Quantmod theme. 335 | layout : dict or Layout 336 | Plotly layout dict or graph_objs.Layout object. 337 | Will override all other arguments if conflicting as 338 | user-inputted layout is updated last. 339 | title : string 340 | Chart title. 341 | hovermode : {'x', 'y', 'closest', False} 342 | Toggle how a tooltip appears on cursor hover. 343 | legend : dict, Legend or bool 344 | True/False or Plotly legend dict / graph_objs.Legend object. 345 | If legend is bool, Quantmod will only toggle legend visibility. 346 | annotations : list or Annotations 347 | Plotly annotations list / graph.objs.Annotations object. 348 | shapes : list or Shapes 349 | Plotly shapes list or graph_objs.Shapes object. 350 | dimensions : tuple 351 | Dimensions 2-tuple in order (width, height). 352 | Disables autosize=True. 353 | width : int 354 | Width of chart. Default 1080 pixels. 355 | If used with height, disables autosize=True (Equivalent to 356 | using dimensions). 357 | height : int 358 | Height of chart. Default 720 pixels. 359 | If used with width, disables autosize=True (Equivalent to 360 | using dimensions). 361 | margin : dict or tuple 362 | Plotly margin dict or 4-tuple in order (l, r, b, t) or 363 | 5-tuple in order (l, r, b, t, margin). Tuple input added for 364 | Cufflinks compatibility. 365 | 366 | """ 367 | # Check for kwargs integrity 368 | for key in kwargs: 369 | if key not in VALID_TEMPLATE_KWARGS: 370 | raise Exception("Invalid keyword '{0}'.".format(key)) 371 | 372 | # Kwargs renaming 373 | if 'showlegend' in kwargs: 374 | legend = kwargs['showlegend'] 375 | 376 | if 'figsize' in kwargs: # Matplotlib 377 | figsize = kwargs['figsize'] 378 | if isinstance(figsize, tuple): 379 | if len(figsize) == 2: 380 | dimensions = tuple(80 * i for i in figsize) # 80x size 381 | else: 382 | raise Exception("Invalid figsize '{0}'. " 383 | "It should be tuple of len 2." 384 | .format(figsize)) 385 | else: 386 | raise TypeError("Invalid figsize '{0}'. " 387 | "It should be tuple." 388 | .format(figsize)) 389 | 390 | # Get skeleton 391 | skeleton = get_skeleton() 392 | 393 | # Type checks for optionally used arguments 394 | 395 | # The if x not None: pattern is used instead of if x: 396 | # because it can catch other falsey values like False, 397 | # which may cause side effects to Plotly.py. 398 | 399 | # Test if theme is string or dict, get default theme from config otherwise 400 | if theme is not None: 401 | if isinstance(theme, six.string_types): 402 | theme = get_theme(theme) 403 | elif isinstance(theme, dict): 404 | pass 405 | else: 406 | raise TypeError("Invalid theme '{0}'. " 407 | "It should be string or dict." 408 | .format(theme)) 409 | else: 410 | theme = get_theme(tools.get_config_file()['theme']) 411 | 412 | # Test if layout is dict, else coerce Layout to regular dict 413 | # Rename to custom_layout (to distinguish from base_layout and layout) 414 | if layout is not None: 415 | custom_layout = layout 416 | if not isinstance(custom_layout, dict): 417 | try: 418 | custom_layout = dict(custom_layout.items()) 419 | except: 420 | raise TypeError("Invalid layout '{0}'. " 421 | "It should be dict or graph_objs.Layout." 422 | .format(custom_layout)) 423 | else: 424 | custom_layout = None 425 | 426 | # Test title if string, else raise exception 427 | if title is not None: 428 | if not isinstance(title, six.string_types): 429 | raise TypeError("Invalid title '{0}'. " 430 | "It should be string.".format(title)) 431 | 432 | # Test if hovermode is string or False, else raise exception 433 | if hovermode is not None: 434 | if hovermode is False: 435 | pass 436 | elif isinstance(hovermode, six.string_types): 437 | pass 438 | else: 439 | raise TypeError("Invalid hovermode '{0}'. " 440 | "It should be string or 'False'." 441 | .format(hovermode)) 442 | 443 | # Test if legend is True/False, else coerce Legend to regular dict 444 | # if legend is not regular dict 445 | if legend is not None: 446 | if isinstance(legend, bool): 447 | pass 448 | elif isinstance(legend, dict): 449 | pass 450 | else: 451 | try: 452 | layout = dict(layout.items()) 453 | except: 454 | raise TypeError("Invalid legend '{0}'. " 455 | "It should be bool, dict or graph_objs.Legend." 456 | .format(layout)) 457 | 458 | # Test if annotations is list, else coerce Annotations to regular list 459 | if annotations is not None: 460 | if not isinstance(annotations, list): 461 | try: 462 | annotations = list(annotations) 463 | except: 464 | raise TypeError("Invalid annotations '{0}'. " 465 | "It should be list or graph_objs.Annotations." 466 | .format(annotations)) 467 | 468 | # Test is shapes is list, else coerce Shapes into regular list 469 | if shapes is not None: 470 | if not isinstance(shapes, list): 471 | try: 472 | shapes = list(shapes) 473 | except: 474 | raise TypeError("Invalid shapes '{0}'. " 475 | "It should be list or graph_objs.Shapes." 476 | .format(shapes)) 477 | 478 | # Test if dimensions is tuple, else raise exception 479 | if dimensions is not None: # Cufflinks 480 | if not isinstance(dimensions, tuple): 481 | raise TypeError("Invalid dimensions '{0}'. " 482 | "It should be tuple." 483 | .format(dimensions)) 484 | if not len(dimensions) == 2: 485 | raise Exception("Invalid dimensions '{0}'. " 486 | "It should be tuple of len 2." 487 | .format(dimensions)) 488 | 489 | # Test below items if int, else raise exception 490 | if width is not None: 491 | if not isinstance(width, six.integer_types): 492 | raise TypeError("Invalid width '{0}'. " 493 | "It should be int." 494 | .format(width)) 495 | 496 | if height is not None: 497 | if not isinstance(height, six.integer_types): 498 | raise TypeError("Invalid height '{0}'. " 499 | "It should be int." 500 | .format(height)) 501 | 502 | # Test if margin is dict, else convert tuple to dict, else raise exception 503 | if margin is not None: 504 | if isinstance(margin, dict): 505 | pass 506 | elif isinstance(margin, tuple): # Cufflinks 507 | if len(margin) == 4: 508 | margin = dict(zip(('l', 'r', 'b', 't'), margin)) 509 | elif len(margin) == 5: 510 | margin = dict(zip(('l', 'r', 'b', 't', 'pad'), margin)) 511 | else: 512 | raise Exception("Invalid margin '{0}'. " 513 | "It should be tuple of len 4 or 5." 514 | .format(margin)) 515 | else: 516 | raise TypeError("Invalid margin '{0}'. " 517 | "It should be dict or tuple." 518 | .format(margin)) 519 | 520 | # Split theme and skeleton 521 | if all(key in skeleton for key in VALID_BASE_COMPONENTS): 522 | base_colors = skeleton['base_colors'] 523 | base_traces = skeleton['base_traces'] 524 | base_additions = skeleton['base_additions'] 525 | base_layout = skeleton['base_layout'] 526 | else: 527 | raise Exception("Improperly configured skeleton. " 528 | "Consider reinstalling Quantmod.") 529 | 530 | if all(key in theme for key in VALID_THEME_COMPONENTS): 531 | colors = theme['colors'] 532 | traces = theme['traces'] 533 | additions = theme['additions'] 534 | layout = theme['layout'] 535 | else: 536 | raise Exception("Improperly configured theme '{0}'.".format(theme)) 537 | 538 | # Generate final template 539 | final_colors = make_colors(base_colors, colors) 540 | final_traces = make_traces(base_traces, traces) 541 | final_additions = make_additions(base_additions, additions) 542 | final_layout = make_layout(base_layout, layout, custom_layout, 543 | title, hovermode, 544 | legend, annotations, shapes, 545 | dimensions, width, height, margin) 546 | 547 | # Convert to dict 548 | template = dict(colors=final_colors, traces=final_traces, 549 | additions=final_additions, layout=final_layout) 550 | 551 | return template 552 | 553 | 554 | def get_base_layout(figures): 555 | """Generate a layout with the union of multiple figures' layouts. 556 | 557 | Parameters 558 | ---------- 559 | figures : list 560 | List of Plotly figures to get base layout from. 561 | 562 | """ 563 | if not isinstance(figures, list): 564 | raise TypeError("Invalid figures '{0}'. " 565 | "It should be list." 566 | .format(figures)) 567 | 568 | layout = {} 569 | for figure in figures: 570 | if not figure['layout']: 571 | raise Exception("Figure does not have 'layout'.") 572 | 573 | for key, value in figure['layout'].items(): 574 | layout[key] = value 575 | 576 | return layout 577 | 578 | 579 | def strip_figure(figure): 580 | """Strip a Plotly figure into multiple figures with a trace on each of them. 581 | 582 | Parameters 583 | ---------- 584 | figure : dict or Figure 585 | Plotly figure to strip into multiple figures. 586 | 587 | """ 588 | if figure is not None: 589 | if isinstance(figure, dict): 590 | pass 591 | else: 592 | try: 593 | figure = dict(figure.items()) 594 | except: 595 | raise TypeError("Invalid figure '{0}'. " 596 | "It should be dict or graph_objs.Legend." 597 | .format(figure)) 598 | 599 | if not figure['layout']: 600 | raise Exception("Figure does not have 'data'.") 601 | 602 | figures = [] 603 | for trace in figure['data']: 604 | figures.append(dict(data=[trace], layout=figure['layout'])) 605 | 606 | return figures 607 | -------------------------------------------------------------------------------- /quantmod/ta.py: -------------------------------------------------------------------------------- 1 | """Wrappers around Ta-Lib technical indicators 2 | 3 | Python native indicators in 'tanolib.py' file. 4 | 5 | """ 6 | import numpy as np 7 | import pandas as pd 8 | import talib 9 | 10 | from . import utils 11 | from .valid import VALID_TA_KWARGS 12 | 13 | 14 | # Overlap studies 15 | 16 | 17 | def add_MA(self, timeperiod=20, matype=0, 18 | type='line', color='secondary', **kwargs): 19 | """Moving Average (customizable).""" 20 | 21 | if not self.has_close: 22 | raise Exception() 23 | 24 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 25 | if 'kind' in kwargs: 26 | type = kwargs['kind'] 27 | 28 | name = 'MA({})'.format(str(timeperiod)) 29 | self.pri[name] = dict(type=type, color=color) 30 | self.ind[name] = talib.MA(self.df[self.cl].values, 31 | timeperiod, matype) 32 | 33 | 34 | def add_SMA(self, timeperiod=20, 35 | type='line', color='secondary', **kwargs): 36 | """Simple Moving Average.""" 37 | 38 | if not self.has_close: 39 | raise Exception() 40 | 41 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 42 | if 'kind' in kwargs: 43 | type = kwargs['kind'] 44 | 45 | name = 'SMA({})'.format(str(timeperiod)) 46 | self.pri[name] = dict(type=type, color=color) 47 | self.ind[name] = talib.SMA(self.df[self.cl].values, 48 | timeperiod) 49 | 50 | 51 | def add_EMA(self, timeperiod=26, 52 | type='line', color='secondary', **kwargs): 53 | """Exponential Moving Average.""" 54 | 55 | if not self.has_close: 56 | raise Exception() 57 | 58 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 59 | if 'kind' in kwargs: 60 | type = kwargs['kind'] 61 | 62 | name = 'EMA({})'.format(str(timeperiod)) 63 | self.pri[name] = dict(type=type, color=color) 64 | self.ind[name] = talib.EMA(self.df[self.cl].values, 65 | timeperiod) 66 | 67 | 68 | def add_WMA(self, timeperiod=20, 69 | type='line', color='secondary', **kwargs): 70 | """Weighted Moving Average.""" 71 | 72 | if not self.has_close: 73 | raise Exception() 74 | 75 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 76 | if 'kind' in kwargs: 77 | type = kwargs['kind'] 78 | 79 | name = 'WMA({})'.format(str(timeperiod)) 80 | self.pri[name] = dict(type=type, color=color) 81 | self.ind[name] = talib.WMA(self.df[self.cl].values, 82 | timeperiod) 83 | 84 | 85 | def add_DEMA(self, timeperiod=26, 86 | type='line', color='secondary', **kwargs): 87 | """Double Exponential Moving Average.""" 88 | 89 | if not self.has_close: 90 | raise Exception() 91 | 92 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 93 | if 'kind' in kwargs: 94 | type = kwargs['kind'] 95 | 96 | name = 'DEMA({})'.format(str(timeperiod)) 97 | self.pri[name] = dict(type=type, color=color) 98 | self.ind[name] = talib.DEMA(self.df[self.cl].values, 99 | timeperiod) 100 | 101 | 102 | def add_TEMA(self, timeperiod=26, 103 | type='line', color='secondary', **kwargs): 104 | """Triple Moving Exponential Average.""" 105 | 106 | if not self.has_close: 107 | raise Exception() 108 | 109 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 110 | if 'kind' in kwargs: 111 | type = kwargs['kind'] 112 | 113 | name = 'TEMA({})'.format(str(timeperiod)) 114 | self.pri[name] = dict(type=type, color=color) 115 | self.ind[name] = talib.TEMA(self.df[self.cl].values, 116 | timeperiod) 117 | 118 | 119 | def add_T3(self, timeperiod=20, vfactor=0.7, 120 | type='line', color='secondary', **kwargs): 121 | """T3 Exponential Moving Average.""" 122 | 123 | if not self.has_close: 124 | raise Exception() 125 | 126 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 127 | if 'kind' in kwargs: 128 | type = kwargs['kind'] 129 | 130 | name = 'T3({}, {})'.format(str(timeperiod), str(vfactor)) 131 | self.pri[name] = dict(type=type, color=color) 132 | self.ind[name] = talib.T3(self.df[self.cl].values, 133 | timeperiod, vfactor) 134 | 135 | 136 | def add_KAMA(self, timeperiod=20, 137 | type='line', color='secondary', **kwargs): 138 | """Kaufmann Adaptive Moving Average.""" 139 | 140 | if not self.has_close: 141 | raise Exception() 142 | 143 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 144 | if 'kind' in kwargs: 145 | type = kwargs['kind'] 146 | 147 | name = 'KAMA({})'.format(str(timeperiod)) 148 | self.pri[name] = dict(type=type, color=color) 149 | self.ind[name] = talib.KAMA(self.df[self.cl].values, 150 | timeperiod) 151 | 152 | 153 | def add_TRIMA(self, timeperiod=20, 154 | type='line', color='secondary', **kwargs): 155 | """Triangular Moving Average.""" 156 | 157 | if not self.has_close: 158 | raise Exception() 159 | 160 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 161 | if 'kind' in kwargs: 162 | type = kwargs['kind'] 163 | 164 | name = 'TRIMA({})'.format(str(timeperiod)) 165 | self.pri[name] = dict(type=type, color=color) 166 | self.ind[name] = talib.TRIMA(self.df[self.cl].values, 167 | timeperiod) 168 | 169 | 170 | def add_MAMA(self, fastlimit=0.5, slowlimit=0.05, 171 | types=['line', 'line'], colors=['secondary', 'tertiary'], 172 | **kwargs): 173 | """MESA Adaptive Moving Average. 174 | 175 | Note that the first argument of types and colors refers to MAMA while the 176 | second argument refers to FAMA. 177 | 178 | """ 179 | if not self.has_close: 180 | raise Exception() 181 | 182 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 183 | if 'kind' in kwargs: 184 | kwargs['type'] = kwargs['kind'] 185 | if 'kinds' in kwargs: 186 | types = kwargs['type'] 187 | 188 | if 'type' in kwargs: 189 | types = [kwargs['type']] * 2 190 | if 'color' in kwargs: 191 | colors = [kwargs['color']] * 2 192 | 193 | mama = 'MAMA({},{})'.format(str(fastlimit), str(slowlimit)) 194 | fama = 'FAMA({},{})'.format(str(fastlimit), str(slowlimit)) 195 | self.pri[mama] = dict(type=types[0], color=colors[0]) 196 | self.pri[fama] = dict(type=types[1], color=colors[1]) 197 | self.ind[mama], self.ind[fama] = talib.MAMA(self.df[self.cl].values, 198 | fastlimit, slowlimit) 199 | 200 | 201 | def add_MAVP(self, periods, minperiod=2, maxperiod=30, matype=0, 202 | type='line', color='secondary', **kwargs): 203 | """Moving Average with Variable Period. 204 | 205 | Parameters 206 | ---------- 207 | 208 | periods : Series or array 209 | Moving Average period over timeframe to analyze, as a 1-dimensional 210 | shape of same length as chart. 211 | 212 | """ 213 | if not self.has_close: 214 | raise Exception() 215 | 216 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 217 | if 'kind' in kwargs: 218 | type = kwargs['kind'] 219 | 220 | if isinstance(periods, pd.Series): 221 | periods = periods.values 222 | elif isinstance(periods, np.ndarray): 223 | pass 224 | else: 225 | raise TypeError("Invalid periods {0}. " 226 | "It should be Series or array." 227 | .format(periods)) 228 | 229 | name = 'MAVP({},{})'.format(str(minperiod), str(maxperiod)) 230 | self.pri[name] = dict(type=type, color=color) 231 | self.ind[name] = talib.MAVP(self.df[self.cl].values, 232 | periods, minperiod, maxperiod, matype) 233 | 234 | 235 | def add_BBANDS(self, timeperiod=20, nbdevup=2, nbdevdn=2, matype=0, 236 | types=['line_dashed_thin', 'line_dashed_thin'], 237 | colors=['tertiary', 'grey_strong'], **kwargs): 238 | """Bollinger Bands. 239 | 240 | Note that the first argument of types and colors refers to upper and lower 241 | bands while second argument refers to middle band. (Upper and lower are 242 | symmetrical arguments, hence only 2 needed.) 243 | 244 | """ 245 | if not self.has_close: 246 | raise Exception() 247 | 248 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 249 | if 'kind' in kwargs: 250 | kwargs['type'] = kwargs['kind'] 251 | if 'kinds' in kwargs: 252 | types = kwargs['type'] 253 | 254 | if 'type' in kwargs: 255 | types = [kwargs['type']] * 2 256 | if 'color' in kwargs: 257 | colors = [kwargs['color']] * 2 258 | 259 | name = 'BBANDS({},{},{})'.format(str(timeperiod), 260 | str(nbdevup), 261 | str(nbdevdn)) 262 | ubb = name + '[Upper]' 263 | bb = name 264 | lbb = name + '[Lower]' 265 | self.pri[ubb] = dict(type='line_' + types[0][5:], 266 | color=colors[0]) 267 | self.pri[bb] = dict(type='area_' + types[1][5:], 268 | color=colors[1], fillcolor='fill') 269 | self.pri[lbb] = dict(type='area_' + types[0][5:], 270 | color=colors[0], fillcolor='fill') 271 | (self.ind[ubb], 272 | self.ind[bb], 273 | self.ind[lbb]) = talib.BBANDS(self.df[self.cl].values, 274 | timeperiod, nbdevup, nbdevdn, matype) 275 | 276 | 277 | def add_HT_TRENDLINE(self, timeperiod=20, 278 | type='line', color='secondary', **kwargs): 279 | """Hilert Transform Instantaneous Trendline.""" 280 | 281 | if not self.has_close: 282 | raise Exception() 283 | 284 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 285 | if 'kind' in kwargs: 286 | type = kwargs['kind'] 287 | 288 | name = 'HT_TRENDLINE' 289 | self.pri[name] = dict(type=type, color=color) 290 | self.ind[name] = talib.HT_TRENDLINE(self.df[self.cl].values) 291 | 292 | 293 | def add_MIDPOINT(self, timeperiod=14, 294 | type='line', color='secondary', **kwargs): 295 | """Midpoint Price over Period.""" 296 | 297 | if not self.has_close: 298 | raise Exception() 299 | 300 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 301 | if 'kind' in kwargs: 302 | type = kwargs['kind'] 303 | 304 | name = 'MIDPOINT({})'.format(str(timeperiod)) 305 | self.pri[name] = dict(type=type, color=color) 306 | self.ind[name] = talib.MIDPOINT(self.df[self.cl].values) 307 | 308 | 309 | def add_SAR(self, acceleration=0.02, maximum=0.20, 310 | type='scatter', color='tertiary', **kwargs): 311 | """Parabolic SAR.""" 312 | 313 | if not (self.has_high and self.has_low): 314 | raise Exception() 315 | 316 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 317 | if 'kind' in kwargs: 318 | type = kwargs['kind'] 319 | 320 | name = 'SAR({},{})'.format(str(acceleration), str(maximum)) 321 | self.pri[name] = dict(type=type, color=color) 322 | self.ind[name] = talib.SAR(self.df[self.hi].values, 323 | self.df[self.lo].values, 324 | acceleration, maximum) 325 | 326 | 327 | def add_SAREXT(self, startvalue=0, offsetonreverse=0, 328 | accelerationinitlong=0.02, accelerationlong=0.02, 329 | accelerationmaxlong=0.20, accelerationinitshort=0.02, 330 | accelerationshort=0.02, accelerationmaxshort=0.20, 331 | type='scatter', color='tertiary', **kwargs): 332 | """Parabolic SAR Extended.""" 333 | 334 | if not (self.has_high and self.has_low): 335 | raise Exception() 336 | 337 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 338 | if 'kind' in kwargs: 339 | type = kwargs['kind'] 340 | 341 | name = ('SAREXT({},{},{},{},' 342 | '{},{},{},{})'.format(str(startvalue), str(offsetonreverse), 343 | str(accelerationinitlong), 344 | str(accelerationlong), 345 | str(accelerationmaxlong), 346 | str(accelerationinitshort), 347 | str(accelerationshort), 348 | str(accelerationmaxshort))) 349 | self.pri[name] = dict(type=type, color=color) 350 | self.ind[name] = talib.SAREXT(self.df[self.hi].values, 351 | self.df[self.lo].values, 352 | startvalue, offsetonreverse, 353 | accelerationinitlong, 354 | accelerationlong, 355 | accelerationmaxlong, 356 | accelerationinitshort, 357 | accelerationshort, 358 | accelerationmaxshort) 359 | self.ind[name] = self.ind[name].abs() # Bug right now with negative value 360 | 361 | 362 | # Momentum indicators 363 | 364 | 365 | def add_APO(self, fastperiod=12, slowperiod=26, matype=0, 366 | type='line', color='secondary', **kwargs): 367 | """Absolute Price Oscillator.""" 368 | 369 | if not self.has_close: 370 | raise Exception() 371 | 372 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 373 | if 'kind' in kwargs: 374 | type = kwargs['kind'] 375 | 376 | name = 'APO({}, {})'.format(str(fastperiod), str(slowperiod)) 377 | self.sec[name] = dict(type=type, color=color) 378 | self.ind[name] = talib.APO(self.df[self.cl].values, 379 | fastperiod, slowperiod, matype) 380 | 381 | 382 | def add_AROON(self, timeperiod=14, 383 | types=['line', 'line'], 384 | colors=['increasing', 'decreasing'], 385 | **kwargs): 386 | """Aroon indicators. 387 | 388 | Note that the first argument of types and colors refers to Aroon up while 389 | the second argument refers to Aroon down. 390 | 391 | """ 392 | if not (self.has_high and self.has_low): 393 | raise Exception() 394 | 395 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 396 | if 'kind' in kwargs: 397 | kwargs['type'] = kwargs['kind'] 398 | if 'kinds' in kwargs: 399 | types = kwargs['type'] 400 | 401 | if 'type' in kwargs: 402 | types = [kwargs['type']] * 2 403 | if 'color' in kwargs: 404 | colors = [kwargs['color']] * 2 405 | 406 | name = 'AROON({})'.format(str(timeperiod)) 407 | uaroon = name + ' [Up]' 408 | daroon = name + ' [Dn]' 409 | self.sec[uaroon] = dict(type=types[0], color=colors[0]) 410 | self.sec[daroon] = dict(type=types[1], color=colors[1], on=uaroon) 411 | self.ind[uaroon], self.ind[daroon] = talib.AROON(self.df[self.hi].values, 412 | self.df[self.lo].values, 413 | timeperiod) 414 | 415 | 416 | def add_AROONOSC(self, timeperiod=14, 417 | type='area', color='secondary', **kwargs): 418 | """Aroon Oscillator.""" 419 | 420 | if not (self.has_high and self.has_low): 421 | raise Exception() 422 | 423 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 424 | if 'kind' in kwargs: 425 | type = kwargs['kind'] 426 | 427 | name = 'AROONOSC({})'.format(str(timeperiod)) 428 | self.sec[name] = dict(type=type, color=color) 429 | self.ind[name] = talib.AROONOSC(self.df[self.hi].values, 430 | self.df[self.lo].values, 431 | timeperiod) 432 | 433 | 434 | def add_BOP(self, 435 | type='histogram', color='tertiary', **kwargs): 436 | """Balance of Power.""" 437 | 438 | if not self.has_OHLC: 439 | raise Exception() 440 | 441 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 442 | if 'kind' in kwargs: 443 | type = kwargs['kind'] 444 | 445 | name = 'BOP' 446 | self.sec[name] = dict(type=type, color=color) 447 | self.ind[name] = talib.BOP(self.df[self.op].values, 448 | self.df[self.hi].values, 449 | self.df[self.lo].values, 450 | self.df[self.cl].values) 451 | 452 | 453 | def add_CCI(self, timeperiod=14, 454 | type='line', color='secondary', **kwargs): 455 | """Channel Commodity Index.""" 456 | 457 | if not (self.has_high and self.has_low and self.has_close): 458 | raise Exception() 459 | 460 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 461 | if 'kind' in kwargs: 462 | type = kwargs['kind'] 463 | 464 | name = 'CCI({})'.format(str(timeperiod)) 465 | self.sec[name] = dict(type=type, color=color) 466 | self.ind[name] = talib.CCI(self.df[self.hi].values, 467 | self.df[self.lo].values, 468 | self.df[self.cl].values, 469 | timeperiod) 470 | 471 | 472 | def add_CMO(self, timeperiod=14, 473 | type='line', color='secondary', **kwargs): 474 | """Chande Momentum Indicator.""" 475 | 476 | if not self.has_close: 477 | raise Exception() 478 | 479 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 480 | if 'kind' in kwargs: 481 | type = kwargs['kind'] 482 | 483 | name = 'CMO({})'.format(str(timeperiod)) 484 | self.sec[name] = dict(type=type, color=color) 485 | self.ind[name] = talib.CMO(self.df[self.cl].values, 486 | timeperiod) 487 | 488 | 489 | def add_ADX(self, timeperiod=14, 490 | type='line', color='secondary', **kwargs): 491 | """Average Directional Movement Index.""" 492 | 493 | if not (self.has_high and self.has_low and self.has_close): 494 | raise Exception() 495 | 496 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 497 | if 'kind' in kwargs: 498 | type = kwargs['kind'] 499 | 500 | name = 'ADX({})'.format(str(timeperiod)) 501 | self.sec[name] = dict(type=type, color=color) 502 | self.ind[name] = talib.ADX(self.df[self.hi].values, 503 | self.df[self.lo].values, 504 | self.df[self.cl].values, 505 | timeperiod) 506 | 507 | 508 | def add_ADXR(self, timeperiod=14, 509 | type='line', color='secondary', **kwargs): 510 | """Average Directional Movement Index Rating.""" 511 | 512 | if not (self.has_high and self.has_low and self.has_close): 513 | raise Exception() 514 | 515 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 516 | if 'kind' in kwargs: 517 | type = kwargs['kind'] 518 | 519 | name = 'ADXR({})'.format(str(timeperiod)) 520 | self.sec[name] = dict(type=type, color=color) 521 | self.ind[name] = talib.ADXR(self.df[self.hi].values, 522 | self.df[self.lo].values, 523 | self.df[self.cl].values, 524 | timeperiod) 525 | 526 | 527 | def add_DX(self, timeperiod=14, 528 | type='line', color='secondary', **kwargs): 529 | """Directional Movement Index.""" 530 | 531 | if not (self.has_high and self.has_low and self.has_close): 532 | raise Exception() 533 | 534 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 535 | if 'kind' in kwargs: 536 | type = kwargs['kind'] 537 | 538 | name = 'DX({})'.format(str(timeperiod)) 539 | self.sec[name] = dict(type=type, color=color) 540 | self.ind[name] = talib.DX(self.df[self.hi].values, 541 | self.df[self.lo].values, 542 | self.df[self.cl].values, 543 | timeperiod) 544 | 545 | 546 | def add_MINUS_DI(self, timeperiod=14, 547 | type='line', color='decreasing', **kwargs): 548 | """Minus Directional Indicator.""" 549 | 550 | if not (self.has_high and self.has_low and self.has_close): 551 | raise Exception() 552 | 553 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 554 | if 'kind' in kwargs: 555 | type = kwargs['kind'] 556 | 557 | name = 'MINUS_DI({})'.format(str(timeperiod)) 558 | self.sec[name] = dict(type=type, color=color) 559 | self.ind[name] = talib.MINUS_DI(self.df[self.hi].values, 560 | self.df[self.lo].values, 561 | self.df[self.cl].values, 562 | timeperiod) 563 | 564 | 565 | def add_PLUS_DI(self, timeperiod=14, 566 | type='line', color='increasing', **kwargs): 567 | """Plus Directional Indicator.""" 568 | 569 | if not (self.has_high and self.has_low and self.has_close): 570 | raise Exception() 571 | 572 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 573 | if 'kind' in kwargs: 574 | type = kwargs['kind'] 575 | 576 | name = 'PLUS_DI({})'.format(str(timeperiod)) 577 | self.sec[name] = dict(type=type, color=color) 578 | self.ind[name] = talib.PLUS_DI(self.df[self.hi].values, 579 | self.df[self.lo].values, 580 | self.df[self.cl].values, 581 | timeperiod) 582 | 583 | 584 | def add_MINUS_DM(self, timeperiod=14, 585 | type='line', color='decreasing', **kwargs): 586 | """Minus Directional Movement.""" 587 | 588 | if not (self.has_high and self.has_low): 589 | raise Exception() 590 | 591 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 592 | if 'kind' in kwargs: 593 | type = kwargs['kind'] 594 | 595 | name = 'MINUS_DM({})'.format(str(timeperiod)) 596 | self.sec[name] = dict(type=type, color=color) 597 | self.ind[name] = talib.MINUS_DM(self.df[self.hi].values, 598 | self.df[self.lo].values, 599 | timeperiod) 600 | 601 | 602 | def add_PLUS_DM(self, timeperiod=14, 603 | type='line', color='increasing', **kwargs): 604 | """Plus Directional Movement.""" 605 | 606 | if not (self.has_high and self.has_low): 607 | raise Exception() 608 | 609 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 610 | if 'kind' in kwargs: 611 | type = kwargs['kind'] 612 | 613 | name = 'PLUS_DM({})'.format(str(timeperiod)) 614 | self.sec[name] = dict(type=type, color=color) 615 | self.ind[name] = talib.PLUS_DM(self.df[self.hi].values, 616 | self.df[self.lo].values, 617 | timeperiod) 618 | 619 | 620 | def add_MACD(self, fastperiod=12, slowperiod=26, signalperiod=9, 621 | types=['line', 'line', 'histogram'], 622 | colors=['primary', 'tertiary', 'fill'], 623 | **kwargs): 624 | """Moving Average Convergence Divergence. 625 | 626 | Note that the first argument of types and colors refers to MACD, 627 | the second argument refers to MACD signal line and the third argument 628 | refers to MACD histogram. 629 | 630 | """ 631 | if not self.has_close: 632 | raise Exception() 633 | 634 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 635 | if 'kind' in kwargs: 636 | kwargs['type'] = kwargs['kind'] 637 | if 'kinds' in kwargs: 638 | types = kwargs['type'] 639 | 640 | if 'type' in kwargs: 641 | types = [kwargs['type']] * 3 642 | if 'color' in kwargs: 643 | colors = [kwargs['color']] * 3 644 | 645 | name = 'MACD({},{},{})'.format(str(fastperiod), 646 | str(slowperiod), 647 | str(signalperiod)) 648 | macd = name 649 | smacd = name + '[Sign]' 650 | hmacd = name + '[Hist]' 651 | self.sec[macd] = dict(type=types[0], color=colors[0]) 652 | self.sec[smacd] = dict(type=types[1], color=colors[1], on=macd) 653 | self.sec[hmacd] = dict(type=types[2], color=colors[2], on=macd) 654 | (self.ind[macd], 655 | self.ind[smacd], 656 | self.ind[hmacd]) = talib.MACD(self.df[self.cl].values, 657 | fastperiod, slowperiod, 658 | signalperiod) 659 | 660 | 661 | def add_MACDEXT(self, fastperiod=12, fastmatype=0, 662 | slowperiod=26, slowmatype=0, 663 | signalperiod=9, signalmatype=0, 664 | types=['line', 'line', 'histogram'], 665 | colors=['primary', 'tertiary', 'fill'], 666 | **kwargs): 667 | """Moving Average Convergence Divergence with Controllable MA Type. 668 | 669 | Note that the first argument of types and colors refers to MACD, 670 | the second argument refers to MACD signal line and the third argument 671 | refers to MACD histogram. 672 | 673 | """ 674 | if not self.has_close: 675 | raise Exception() 676 | 677 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 678 | if 'kind' in kwargs: 679 | kwargs['type'] = kwargs['kind'] 680 | if 'kinds' in kwargs: 681 | types = kwargs['type'] 682 | 683 | if 'type' in kwargs: 684 | types = [kwargs['type']] * 3 685 | if 'color' in kwargs: 686 | colors = [kwargs['color']] * 3 687 | 688 | name = 'MACDEXT({},{},{})'.format(str(fastperiod), 689 | str(slowperiod), 690 | str(signalperiod)) 691 | macd = name 692 | smacd = name + '[Sign]' 693 | hmacd = name + '[Hist]' 694 | self.sec[macd] = dict(type=types[0], color=colors[0]) 695 | self.sec[smacd] = dict(type=types[1], color=colors[1], on=macd) 696 | self.sec[hmacd] = dict(type=types[2], color=colors[2], on=macd) 697 | (self.ind[macd], 698 | self.ind[smacd], 699 | self.ind[hmacd]) = talib.MACDEXT(self.df[self.cl].values, 700 | fastperiod, fastmatype, 701 | slowperiod, slowmatype, 702 | signalperiod, signalmatype) 703 | 704 | 705 | def add_MFI(self, timeperiod=14, 706 | type='line', color='secondary', **kwargs): 707 | """Money Flow Index.""" 708 | 709 | if not (self.has_high and self.has_low and 710 | self.has_close and self.has_volume): 711 | raise Exception() 712 | 713 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 714 | if 'kind' in kwargs: 715 | type = kwargs['kind'] 716 | 717 | name = 'MFI({})'.format(str(timeperiod)) 718 | self.sec[name] = dict(type=type, color=color) 719 | self.ind[name] = talib.MFI(self.df[self.hi].values, 720 | self.df[self.lo].values, 721 | self.df[self.cl].values, 722 | self.df[self.vo].values, 723 | timeperiod) 724 | 725 | 726 | def add_MOM(self, timeperiod=10, 727 | type='line', color='secondary', **kwargs): 728 | """Momentum Indicator.""" 729 | 730 | if not self.has_close: 731 | raise Exception() 732 | 733 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 734 | if 'kind' in kwargs: 735 | type = kwargs['kind'] 736 | 737 | name = 'MOM({})'.format(str(timeperiod)) 738 | self.sec[name] = dict(type=type, color=color) 739 | self.ind[name] = talib.MOM(self.df[self.cl].values, 740 | timeperiod) 741 | 742 | 743 | def add_PPO(self, fastperiod=12, slowperiod=26, matype=0, 744 | type='line', color='secondary', 745 | **kwargs): 746 | """Percent Price Oscillator.""" 747 | 748 | if not self.has_close: 749 | raise Exception() 750 | 751 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 752 | if 'kind' in kwargs: 753 | kwargs['type'] = kwargs['kind'] 754 | 755 | name = 'PPO({},{})'.format(str(fastperiod), str(slowperiod)) 756 | self.ind[name] = talib.PPO(self.df[self.cl].values, 757 | fastperiod, slowperiod, 758 | matype) 759 | 760 | 761 | def add_ROC(self, timeperiod=10, 762 | type='line', color='tertiary', **kwargs): 763 | """Rate of Change.""" 764 | 765 | if not self.has_close: 766 | raise Exception() 767 | 768 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 769 | if 'kind' in kwargs: 770 | type = kwargs['kind'] 771 | 772 | name = 'ROC({})'.format(str(timeperiod)) 773 | self.sec[name] = dict(type=type, color=color) 774 | self.ind[name] = talib.ROC(self.df[self.cl].values, 775 | timeperiod) 776 | 777 | 778 | def add_ROCP(self, timeperiod=10, 779 | type='line', color='tertiary', **kwargs): 780 | """Rate of Change (Percentage).""" 781 | 782 | if not self.has_close: 783 | raise Exception() 784 | 785 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 786 | if 'kind' in kwargs: 787 | type = kwargs['kind'] 788 | 789 | name = 'ROCP({})'.format(str(timeperiod)) 790 | self.sec[name] = dict(type=type, color=color) 791 | self.ind[name] = talib.ROCP(self.df[self.cl].values, 792 | timeperiod) 793 | 794 | 795 | def add_ROCR(self, timeperiod=10, 796 | type='line', color='tertiary', **kwargs): 797 | """Rate of Change (Ratio).""" 798 | 799 | if not self.has_close: 800 | raise Exception() 801 | 802 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 803 | if 'kind' in kwargs: 804 | type = kwargs['kind'] 805 | 806 | name = 'ROCR({})'.format(str(timeperiod)) 807 | self.sec[name] = dict(type=type, color=color) 808 | self.ind[name] = talib.ROCR(self.df[self.cl].values, 809 | timeperiod) 810 | 811 | 812 | def add_ROCR100(self, timeperiod=10, 813 | type='line', color='tertiary', **kwargs): 814 | """Rate of Change (Ratio * 100).""" 815 | 816 | if not self.has_close: 817 | raise Exception() 818 | 819 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 820 | if 'kind' in kwargs: 821 | type = kwargs['kind'] 822 | 823 | name = 'ROCR100({})'.format(str(timeperiod)) 824 | self.sec[name] = dict(type=type, color=color) 825 | self.ind[name] = talib.ROCR100(self.df[self.cl].values, 826 | timeperiod) 827 | 828 | 829 | def add_RSI(self, timeperiod=14, 830 | type='line', color='secondary', **kwargs): 831 | """Relative Strength Index.""" 832 | 833 | if not self.has_close: 834 | raise Exception() 835 | 836 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 837 | if 'kind' in kwargs: 838 | type = kwargs['kind'] 839 | 840 | name = 'RSI({})'.format(str(timeperiod)) 841 | self.sec[name] = dict(type=type, color=color) 842 | self.ind[name] = talib.RSI(self.df[self.cl].values, 843 | timeperiod) 844 | 845 | 846 | def add_STOCH(self, fastk_period=5, slowk_period=3, 847 | slowk_matype=0, slowd_period=3, slowd_matype=0, 848 | types=['line', 'line'], 849 | colors=['primary', 'tertiary'], 850 | **kwargs): 851 | """Slow Stochastic Oscillator. 852 | 853 | Note that the first argument of types and colors refers to Slow Stoch %K, 854 | while second argument refers to Slow Stoch %D 855 | (signal line of %K obtained by MA). 856 | 857 | """ 858 | if not (self.has_high and self.has_low and self.has_close): 859 | raise Exception() 860 | 861 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 862 | if 'kind' in kwargs: 863 | kwargs['type'] = kwargs['kind'] 864 | if 'kinds' in kwargs: 865 | types = kwargs['type'] 866 | 867 | if 'type' in kwargs: 868 | types = [kwargs['type']] * 2 869 | if 'color' in kwargs: 870 | colors = [kwargs['color']] * 2 871 | 872 | name = 'STOCH({},{},{})'.format(str(fastk_period), 873 | str(slowk_period), 874 | str(slowd_period)) 875 | slowk = name + r'[%k]' 876 | slowd = name + r'[%d]' 877 | self.sec[slowk] = dict(type=types[0], color=colors[0]) 878 | self.sec[slowd] = dict(type=types[1], color=colors[1], on=slowk) 879 | self.ind[slowk], self.ind[slowd] = talib.STOCH(self.df[self.hi].values, 880 | self.df[self.lo].values, 881 | self.df[self.cl].values, 882 | fastk_period, slowk_period, 883 | slowk_matype, slowd_period, 884 | slowd_matype) 885 | 886 | 887 | def add_STOCHF(self, fastk_period=5, fastd_period=3, fastd_matype=0, 888 | types=['line', 'line'], 889 | colors=['primary', 'tertiary'], 890 | **kwargs): 891 | """Fast Stochastic Oscillator. 892 | 893 | Note that the first argument of types and colors refers to Fast Stoch %K, 894 | while second argument refers to Fast Stoch %D 895 | (signal line of %K obtained by MA). 896 | 897 | """ 898 | if not (self.has_high and self.has_low and self.has_close): 899 | raise Exception() 900 | 901 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 902 | if 'kind' in kwargs: 903 | kwargs['type'] = kwargs['kind'] 904 | if 'kinds' in kwargs: 905 | types = kwargs['type'] 906 | 907 | if 'type' in kwargs: 908 | types = [kwargs['type']] * 2 909 | if 'color' in kwargs: 910 | colors = [kwargs['color']] * 2 911 | 912 | name = 'STOCHF({},{})'.format(str(fastk_period), 913 | str(fastd_period)) 914 | fastk = name + r'[%k]' 915 | fastd = name + r'[%d]' 916 | self.sec[fastk] = dict(type=types[0], color=colors[0]) 917 | self.sec[fastd] = dict(type=types[1], color=colors[1], on=fastk) 918 | self.ind[fastk], self.ind[fastd] = talib.STOCHF(self.df[self.hi].values, 919 | self.df[self.lo].values, 920 | self.df[self.cl].values, 921 | fastk_period, fastd_period, 922 | fastd_matype) 923 | 924 | 925 | def add_STOCHRSI(self, timeperiod=14, 926 | fastk_period=5, fastd_period=3, fastd_matype=0, 927 | types=['line', 'line'], 928 | colors=['primary', 'tertiary'], 929 | **kwargs): 930 | """Stochastic Relative Strength Index. 931 | 932 | Note that the first argument of types and colors refers to StochRSI %K 933 | while second argument refers to StochRSI %D 934 | (signal line of %K obtained by MA). 935 | 936 | """ 937 | if not self.has_close: 938 | raise Exception() 939 | 940 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 941 | if 'kind' in kwargs: 942 | kwargs['type'] = kwargs['kind'] 943 | if 'kinds' in kwargs: 944 | types = kwargs['type'] 945 | 946 | if 'type' in kwargs: 947 | types = [kwargs['type']] * 2 948 | if 'color' in kwargs: 949 | colors = [kwargs['color']] * 2 950 | 951 | name = 'STOCHRSI({},{},{})'.format(str(timeperiod), 952 | str(fastk_period), 953 | str(fastd_period)) 954 | fastk = name + r'[%k]' 955 | fastd = name + r'[%d]' 956 | self.sec[fastk] = dict(type=types[0], color=colors[0]) 957 | self.sec[fastd] = dict(type=types[1], color=colors[1], on=fastk) 958 | self.ind[fastk], self.ind[fastd] = talib.STOCHRSI(self.df[self.cl].values, 959 | timeperiod, 960 | fastk_period, 961 | fastd_period, 962 | fastd_matype) 963 | 964 | 965 | def add_TRIX(self, timeperiod=15, 966 | type='area', color='secondary', **kwargs): 967 | """1-day Rate of Change of Triple Smooth EMA.""" 968 | 969 | if not self.has_close: 970 | raise Exception() 971 | 972 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 973 | if 'kind' in kwargs: 974 | type = kwargs['kind'] 975 | 976 | name = 'TRIX({})'.format(str(timeperiod)) 977 | self.sec[name] = dict(type=type, color=color) 978 | self.ind[name] = talib.TRIX(self.df[self.cl].values, 979 | timeperiod) 980 | 981 | 982 | def add_ULTOSC(self, timeperiod=14, timeperiod2=14, timeperiod3=28, 983 | type='line', color='secondary', **kwargs): 984 | """Ultimate Oscillator.""" 985 | 986 | if not (self.has_high and self.has_low and self.has_close): 987 | raise Exception() 988 | 989 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 990 | if 'kind' in kwargs: 991 | type = kwargs['kind'] 992 | 993 | name = 'ULTOSC({})'.format(str(timeperiod), 994 | str(timeperiod2), 995 | str(timeperiod3)) 996 | self.sec[name] = dict(type=type, color=color) 997 | self.ind[name] = talib.ULTOSC(self.df[self.hi].values, 998 | self.df[self.lo].values, 999 | self.df[self.cl].values, 1000 | timeperiod, 1001 | timeperiod2, 1002 | timeperiod3) 1003 | 1004 | 1005 | def add_WILLR(self, timeperiod=14, 1006 | type='line', color='secondary', **kwargs): 1007 | """Williams %R.""" 1008 | 1009 | if not (self.has_high and self.has_low and self.has_close): 1010 | raise Exception() 1011 | 1012 | utils.kwargs_check(kwargs, VALID_TA_KWARGS) 1013 | if 'kind' in kwargs: 1014 | type = kwargs['kind'] 1015 | 1016 | name = 'WILLR({})'.format(str(timeperiod)) 1017 | self.sec[name] = dict(type=type, color=color) 1018 | self.ind[name] = talib.WILLR(self.df[self.hi].values, 1019 | self.df[self.lo].values, 1020 | self.df[self.cl].values, 1021 | timeperiod) 1022 | -------------------------------------------------------------------------------- /quantmod/tanolib.py: -------------------------------------------------------------------------------- 1 | """Python native technical indicators 2 | 3 | Ta-Lib indicators in 'ta.py' file. 4 | 5 | """ 6 | -------------------------------------------------------------------------------- /quantmod/theming/__init__.py: -------------------------------------------------------------------------------- 1 | """File added for Python 2 support""" 2 | 3 | from __future__ import absolute_import 4 | -------------------------------------------------------------------------------- /quantmod/theming/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-technical-charting/72af2c0c19bc2c56231061bff2fbd118b71a9819/quantmod/theming/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /quantmod/theming/__pycache__/palettes.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-technical-charting/72af2c0c19bc2c56231061bff2fbd118b71a9819/quantmod/theming/__pycache__/palettes.cpython-36.pyc -------------------------------------------------------------------------------- /quantmod/theming/__pycache__/skeleton.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-technical-charting/72af2c0c19bc2c56231061bff2fbd118b71a9819/quantmod/theming/__pycache__/skeleton.cpython-36.pyc -------------------------------------------------------------------------------- /quantmod/theming/__pycache__/themes.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-technical-charting/72af2c0c19bc2c56231061bff2fbd118b71a9819/quantmod/theming/__pycache__/themes.cpython-36.pyc -------------------------------------------------------------------------------- /quantmod/theming/colors.py: -------------------------------------------------------------------------------- 1 | """Plotly colors 2 | 3 | Refer common colors by name instead of by RGB value. 4 | 5 | """ 6 | # flake8: noqa 7 | 8 | colors = dict( 9 | aliceblue = "rgb(240, 248, 255)", 10 | antiquewhite = "rgb(250, 235, 215)", 11 | aqua = "rgb(0, 255, 255)", 12 | aquamarine = "rgb(127, 255, 212)", 13 | azure = "rgb(240, 255, 255)", 14 | beige = "rgb(245, 245, 220)", 15 | bisque = "rgb(255, 228, 196)", 16 | black = "rgb(0, 0, 0)", 17 | blanchedalmond = "rgb(255, 235, 205)", 18 | blue = "rgb(0, 0, 255)", 19 | blueviolet = "rgb(138, 43, 226)", 20 | brown = "rgb(165, 42, 42)", 21 | burlywood = "rgb(222, 184, 135)", 22 | cadetblue = "rgb(95, 158, 160)", 23 | chartreuse = "rgb(127, 255, 0)", 24 | chocolate = "rgb(210, 105, 30)", 25 | coral = "rgb(255, 127, 80)", 26 | cornflowerblue = "rgb(100, 149, 237)", 27 | cornsilk = "rgb(255, 248, 220)", 28 | crimson = "rgb(220, 20, 60)", 29 | cyan = "rgb(0, 255, 255)", 30 | darkblue = "rgb(0, 0, 139)", 31 | darkcyan = "rgb(0, 139, 139)", 32 | darkgoldenrod = "rgb(184, 134, 11)", 33 | darkgray = "rgb(169, 169, 169)", 34 | darkgreen = "rgb(0, 100, 0)", 35 | darkgrey = "rgb(169, 169, 169)", 36 | darkkhaki = "rgb(189, 183, 107)", 37 | darkmagenta = "rgb(139, 0, 139)", 38 | darkolivegreen = "rgb(85, 107, 47)", 39 | darkorange = "rgb(255, 140, 0)", 40 | darkorchid = "rgb(153, 50, 204)", 41 | darkred = "rgb(139, 0, 0)", 42 | darksalmon = "rgb(233, 150, 122)", 43 | darkseagreen = "rgb(143, 188, 143)", 44 | darkslateblue = "rgb(72, 61, 139)", 45 | darkslategray = "rgb(47, 79, 79)", 46 | darkslategrey = "rgb(47, 79, 79)", 47 | darkturquoise = "rgb(0, 206, 209)", 48 | darkviolet = "rgb(148, 0, 211)", 49 | deeppink = "rgb(255, 20, 147)", 50 | deepskyblue = "rgb(0, 191, 255)", 51 | dimgray = "rgb(105, 105, 105)", 52 | dimgrey = "rgb(105, 105, 105)", 53 | dodgerblue = "rgb(30, 144, 255)", 54 | firebrick = "rgb(178, 34, 34)", 55 | floralwhite = "rgb(255, 250, 240)", 56 | forestgreen = "rgb(34, 139, 34)", 57 | fuchsia = "rgb(255, 0, 255)", 58 | gainsboro = "rgb(220, 220, 220)", 59 | ghostwhite = "rgb(248, 248, 255)", 60 | gold = "rgb(255, 215, 0)", 61 | goldenrod = "rgb(218, 165, 32)", 62 | gray = "rgb(128, 128, 128)", 63 | green = "rgb(0, 128, 0)", 64 | greenyellow = "rgb(173, 255, 47)", 65 | grey = "rgb(128, 128, 128)", 66 | honeydew = "rgb(240, 255, 240)", 67 | hotpink = "rgb(255, 105, 180)", 68 | indianred = "rgb(205, 92, 92)", 69 | indigo = "rgb(75, 0, 130)", 70 | ivory = "rgb(255, 255, 240)", 71 | khaki = "rgb(240, 230, 140)", 72 | lavender = "rgb(230, 230, 250)", 73 | lavenderblush = "rgb(255, 240, 245)", 74 | lawngreen = "rgb(124, 252, 0)", 75 | lemonchiffon = "rgb(255, 250, 205)", 76 | lightblue = "rgb(173, 216, 230)", 77 | lightcoral = "rgb(240, 128, 128)", 78 | lightcyan = "rgb(224, 255, 255)", 79 | lightgoldenrodyellow = "rgb(250, 250, 210)", 80 | lightgray = "rgb(211, 211, 211)", 81 | lightgreen = "rgb(144, 238, 144)", 82 | lightgrey = "rgb(211, 211, 211)", 83 | lightpink = "rgb(255, 182, 193)", 84 | lightsalmon = "rgb(255, 160, 122)", 85 | lightseagreen = "rgb(32, 178, 170)", 86 | lightskyblue = "rgb(135, 206, 250)", 87 | lightslategray = "rgb(119, 136, 153)", 88 | lightslategrey = "rgb(119, 136, 153)", 89 | lightsteelblue = "rgb(176, 196, 222)", 90 | lightyellow = "rgb(255, 255, 224)", 91 | lime = "rgb(0, 255, 0)", 92 | limegreen = "rgb(50, 205, 50)", 93 | linen = "rgb(250, 240, 230)", 94 | magenta = "rgb(255, 0, 255)", 95 | maroon = "rgb(128, 0, 0)", 96 | mediumaquamarine = "rgb(102, 205, 170)", 97 | mediumblue = "rgb(0, 0, 205)", 98 | mediumorchid = "rgb(186, 85, 211)", 99 | mediumpurple = "rgb(147, 112, 219)", 100 | mediumseagreen = "rgb(60, 179, 113)", 101 | mediumslateblue = "rgb(123, 104, 238)", 102 | mediumspringgreen = "rgb(0, 250, 154)", 103 | mediumturquoise = "rgb(72, 209, 204)", 104 | mediumvioletred = "rgb(199, 21, 133)", 105 | midnightblue = "rgb(25, 25, 112)", 106 | mintcream = "rgb(245, 255, 250)", 107 | mistyrose = "rgb(255, 228, 225)", 108 | moccasin = "rgb(255, 228, 181)", 109 | navajowhite = "rgb(255, 222, 173)", 110 | navy = "rgb(0, 0, 128)", 111 | oldlace = "rgb(253, 245, 230)", 112 | olive = "rgb(128, 128, 0)", 113 | olivedrab = "rgb(107, 142, 35)", 114 | orange = "rgb(255, 165, 0)", 115 | orangered = "rgb(255, 69, 0)", 116 | orchid = "rgb(218, 112, 214)", 117 | palegoldenrod = "rgb(238, 232, 170)", 118 | palegreen = "rgb(152, 251, 152)", 119 | paleturquoise = "rgb(175, 238, 238)", 120 | palevioletred = "rgb(219, 112, 147)", 121 | papayawhip = "rgb(255, 239, 213)", 122 | peachpuff = "rgb(255, 218, 185)", 123 | peru = "rgb(205, 133, 63)", 124 | pink = "rgb(255, 192, 203)", 125 | plum = "rgb(221, 160, 221)", 126 | powderblue = "rgb(176, 224, 230)", 127 | purple = "rgb(128, 0, 128)", 128 | rebeccapurple = "rgb(102, 51, 153)", 129 | red = "rgb(255, 0, 0)", 130 | rosybrown = "rgb(188, 143, 143)", 131 | royalblue = "rgb(65, 105, 225)", 132 | saddlebrown = "rgb(139, 69, 19)", 133 | salmon = "rgb(250, 128, 114)", 134 | sandybrown = "rgb(244, 164, 96)", 135 | seagreen = "rgb(46, 139, 87)", 136 | seashell = "rgb(255, 245, 238)", 137 | sienna = "rgb(160, 82, 45)", 138 | silver = "rgb(192, 192, 192)", 139 | skyblue = "rgb(135, 206, 235)", 140 | slateblue = "rgb(106, 90, 205)", 141 | slategray = "rgb(112, 128, 144)", 142 | slategrey = "rgb(112, 128, 144)", 143 | snow = "rgb(255, 250, 250)", 144 | springgreen = "rgb(0, 255, 127)", 145 | steelblue = "rgb(70, 130, 180)", 146 | tan = "rgb(210, 180, 140)", 147 | teal = "rgb(0, 128, 128)", 148 | thistle = "rgb(216, 191, 216)", 149 | tomato = "rgb(255, 99, 71)", 150 | turquoise = "rgb(64, 224, 208)", 151 | violet = "rgb(238, 130, 238)", 152 | wheat = "rgb(245, 222, 179)", 153 | white = "rgb(255, 255, 255)", 154 | whitesmoke = "rgb(245, 245, 245)", 155 | yellow = "rgb(255, 255, 0)", 156 | yellowgreen = "rgb(154, 205, 50)" 157 | } 158 | -------------------------------------------------------------------------------- /quantmod/theming/palettes.py: -------------------------------------------------------------------------------- 1 | """RGBA color palettes 2 | 3 | Used to dynamically link to theme colors rather than rely on hardcoding 4 | of greyscale traces. 5 | 6 | For readability, files under theming do not follow PEP8 guideline of 7 | no space between assignment of named arguments. 8 | 9 | """ 10 | # flake8: noqa 11 | 12 | # Palette for lighter themes 13 | LIGHT_PALETTE = dict( 14 | white = '#FFFFFF', 15 | black = '#000000', 16 | transparent = 'rgba(0, 0, 0, 0.00)', 17 | grey02 = 'rgba(0, 0, 0, 0.02)', 18 | grey05 = 'rgba(0, 0, 0, 0.05)', 19 | grey10 = 'rgba(0, 0, 0, 0.10)', 20 | grey15 = 'rgba(0, 0, 0, 0.15)', 21 | grey20 = 'rgba(0, 0, 0, 0.20)', 22 | grey25 = 'rgba(0, 0, 0, 0.25)', 23 | grey30 = 'rgba(0, 0, 0, 0.30)', 24 | grey35 = 'rgba(0, 0, 0, 0.35)', 25 | grey40 = 'rgba(0, 0, 0, 0.40)', 26 | grey45 = 'rgba(0, 0, 0, 0.45)', 27 | grey50 = 'rgba(0, 0, 0, 0.50)', 28 | grey55 = 'rgba(0, 0, 0, 0.55)', 29 | grey60 = 'rgba(0, 0, 0, 0.60)', 30 | grey65 = 'rgba(0, 0, 0, 0.65)', 31 | grey70 = 'rgba(0, 0, 0, 0.70)', 32 | grey75 = 'rgba(0, 0, 0, 0.75)', 33 | grey80 = 'rgba(0, 0, 0, 0.80)', 34 | grey85 = 'rgba(0, 0, 0, 0.85)', 35 | grey90 = 'rgba(0, 0, 0, 0.90)', 36 | grey95 = 'rgba(0, 0, 0, 0.95)', 37 | grey98 = 'rgba(0, 0, 0, 0.98)', 38 | ) 39 | 40 | 41 | # Palette for darker themes 42 | DARK_PALETTE = dict( 43 | white = '#FFFFFF', 44 | black = '#000000', 45 | transparent = 'rgba(255, 255, 255, 0.00)', 46 | grey02 = 'rgba(255, 255, 255, 0.98)', 47 | grey05 = 'rgba(255, 255, 255, 0.95)', 48 | grey10 = 'rgba(255, 255, 255, 0.90)', 49 | grey15 = 'rgba(255, 255, 255, 0.85)', 50 | grey20 = 'rgba(255, 255, 255, 0.80)', 51 | grey25 = 'rgba(255, 255, 255, 0.75)', 52 | grey30 = 'rgba(255, 255, 255, 0.70)', 53 | grey35 = 'rgba(255, 255, 255, 0.65)', 54 | grey40 = 'rgba(255, 255, 255, 0.60)', 55 | grey45 = 'rgba(255, 255, 255, 0.55)', 56 | grey50 = 'rgba(255, 255, 255, 0.50)', 57 | grey55 = 'rgba(255, 255, 255, 0.45)', 58 | grey60 = 'rgba(255, 255, 255, 0.40)', 59 | grey65 = 'rgba(255, 255, 255, 0.35)', 60 | grey70 = 'rgba(255, 255, 255, 0.30)', 61 | grey75 = 'rgba(255, 255, 255, 0.25)', 62 | grey80 = 'rgba(255, 255, 255, 0.20)', 63 | grey85 = 'rgba(255, 255, 255, 0.15)', 64 | grey90 = 'rgba(255, 255, 255, 0.10)', 65 | grey95 = 'rgba(255, 255, 255, 0.05)', 66 | grey98 = 'rgba(255, 255, 255, 0.02)', 67 | ) 68 | -------------------------------------------------------------------------------- /quantmod/theming/skeleton.py: -------------------------------------------------------------------------------- 1 | """Quantmod skeleton module 2 | 3 | Edit your own modules by copying one of the themes. 4 | Make sure that colors, traces, additions and layout are all under one dict. 5 | 6 | For readability, files under theming do not follow PEP8 guideline of 7 | no space between assignment of named arguments. 8 | 9 | """ 10 | # flake8: noqa 11 | 12 | _PLACEHOLDER = False 13 | 14 | # Color primitives 15 | BASE_COLORS = dict() 16 | 17 | # Trace primitives 18 | BASE_TRACES = dict( 19 | 20 | candlestick = dict( 21 | type = 'candlestick', 22 | hoverinfo = 'x+y+text+name', 23 | whiskerwidth = 0, 24 | # Increasing 25 | increasing = dict( 26 | line = dict( 27 | color = _PLACEHOLDER, 28 | width = 1, 29 | ), 30 | fillcolor = _PLACEHOLDER, 31 | ), 32 | # Decreasing 33 | decreasing = dict( 34 | line = dict( 35 | color = _PLACEHOLDER, 36 | width = 1, 37 | ), 38 | fillcolor = _PLACEHOLDER, 39 | ), 40 | ), 41 | 42 | ohlc = dict( 43 | type = 'ohlc', 44 | hoverinfo = 'x+y+text+name', 45 | # Increasing 46 | increasing = dict( 47 | line = dict( 48 | color = _PLACEHOLDER, 49 | width = 1.5, 50 | ), 51 | ), 52 | # Decreasing 53 | decreasing = dict( 54 | line = dict( 55 | color = _PLACEHOLDER, 56 | width = 1.5, 57 | ), 58 | ), 59 | ), 60 | 61 | line = dict( 62 | type = 'scatter', 63 | hoverinfo = 'x+y+text+name', 64 | mode = 'lines', 65 | #fill = 'tonexty', 66 | opacity = 0.6, 67 | # Marker 68 | marker = dict( 69 | color = _PLACEHOLDER, 70 | size = 3, 71 | opacity = 1.0, 72 | symbol = 'square', 73 | ), 74 | # Line 75 | line = dict( 76 | color = _PLACEHOLDER, 77 | width = 2, 78 | #dash = 4, 79 | #shape = 'spline', 80 | #smoothing = '2', 81 | ), 82 | # Area 83 | fillcolor = _PLACEHOLDER, 84 | ), 85 | 86 | bar = dict( 87 | type = 'bar', 88 | hoverinfo = 'x+y+text+name', 89 | #opacity = 0.6, 90 | # Marker 91 | marker = dict( 92 | color = _PLACEHOLDER, 93 | line = dict( 94 | color = _PLACEHOLDER, 95 | width = 1, 96 | ), 97 | ), 98 | ), 99 | 100 | ) 101 | 102 | # Layout modifiers 103 | BASE_ADDITIONS = dict( 104 | 105 | xaxis = dict( 106 | # Range 107 | #nticks = , #OR 108 | #tick0 = , #AND 109 | #dtick = , 110 | # Ticks 111 | #tickfont = dict(size = 10), 112 | #showticklabels = False, 113 | # Range slider 114 | rangeslider = dict( 115 | visible = False, 116 | bordercolor = _PLACEHOLDER, 117 | bgcolor = _PLACEHOLDER, 118 | thickness = 0.1, 119 | ), 120 | # Range selector 121 | rangeselector = dict( 122 | visible = True, 123 | bordercolor = _PLACEHOLDER, 124 | bgcolor = _PLACEHOLDER, 125 | activecolor = _PLACEHOLDER, 126 | buttons = [ 127 | dict(count = 1, step = 'day', stepmode = 'backward', label = '1D'), 128 | dict(count = 5, step = 'day', stepmode = 'backward', label = '5D'), 129 | dict(count = 1, step = 'month', stepmode = 'backward', label = '1M'), 130 | dict(count = 3, step = 'month', stepmode = 'backward', label = '3M'), 131 | dict(count = 6, step = 'month', stepmode = 'backward', label = '6M'), 132 | dict(count = 1, step = 'year', stepmode = 'backward', label = '1Y'), 133 | dict(count = 2, step = 'year', stepmode = 'backward', label = '2Y'), 134 | dict(count = 5, step = 'year', stepmode = 'backward', label = '5Y'), 135 | dict(count = 1, step = 'all', stepmode = 'backward', label = 'MAX'), 136 | dict(count = 1, step = 'year', stepmode = 'todate', label = 'YTD'), 137 | ], 138 | ), 139 | # Other 140 | #type = 'datetime' 141 | anchor = 'y', 142 | side = 'bottom', 143 | #showline = False, 144 | #showgrid = False, 145 | #zeroline = False, 146 | #titlefont = dict(size = 10), 147 | ), 148 | 149 | yaxis = dict( 150 | # Range 151 | #rangemode = 'tozero', 152 | #range = , 153 | #nticks = , #OR 154 | #tick0 = , #AND 155 | #dtick = , 156 | # Ticks 157 | #tickfont = dict(size = 10), 158 | #showticklabels = False, 159 | # Other 160 | type = 'linear', 161 | domain = [0.0, 1], 162 | side = 'right', 163 | #showline = False, 164 | #showgrid = False, 165 | #zeroline = False, 166 | #titlefont = dict(size = 10), 167 | ), 168 | 169 | ) 170 | 171 | # Layout primitives 172 | BASE_LAYOUT = dict( 173 | # General 174 | title = '', 175 | width = 1080, 176 | height = 720, 177 | autosize = True, 178 | font = dict( 179 | family = _PLACEHOLDER, 180 | size = _PLACEHOLDER, 181 | color = _PLACEHOLDER, 182 | ), 183 | margin = dict( 184 | t = 60, 185 | l = 40, 186 | b = 40, 187 | r = 40, 188 | pad = 0, 189 | ), 190 | hovermode = 'x', 191 | barmode = "group", 192 | # Color theme 193 | plot_bgcolor = _PLACEHOLDER, 194 | paper_bgcolor = _PLACEHOLDER, 195 | # Gaps 196 | bargap = 0.3, 197 | bargroupgap = 0.0, 198 | boxgap = 0.3, 199 | boxgroupgap = 0.0, 200 | # Legend 201 | showlegend = False, 202 | legend = dict( 203 | bgcolor = _PLACEHOLDER, 204 | x = 0.01, 205 | y = 0.99, 206 | xanchor = 'left', 207 | yanchor = 'top', 208 | tracegroupgap = 10, 209 | #font = dict( 210 | # size = 10, 211 | # color = _OPTIONAL, 212 | #), 213 | ), 214 | ) 215 | 216 | 217 | SKELETON = {'base_colors': BASE_COLORS, 'base_traces': BASE_TRACES, 218 | 'base_additions': BASE_ADDITIONS, 'base_layout': BASE_LAYOUT} 219 | -------------------------------------------------------------------------------- /quantmod/theming/themes.py: -------------------------------------------------------------------------------- 1 | """Quandmod themes module 2 | 3 | Create your own modules by copying one of the themes and editing it after. 4 | Make sure that colors, traces, additions and layout are all under one 5 | main dict, and add that dict to '_VALID_THEMES' at the bottom of the file. 6 | 7 | For readability, files under theming do not follow PEP8 guideline of 8 | no space between assignment of named arguments. 9 | 10 | """ 11 | # flake8: noqa 12 | 13 | from __future__ import absolute_import 14 | 15 | from .palettes import LIGHT_PALETTE, DARK_PALETTE 16 | 17 | 18 | # Light Quantmod theme 19 | LIGHT_QM = dict( 20 | 21 | colors = dict( 22 | increasing = '#00CC00', 23 | decreasing = '#FF7700', 24 | border_increasing = LIGHT_PALETTE['grey25'], 25 | border_decreasing = LIGHT_PALETTE['grey25'], 26 | primary = '#252585', 27 | secondary = '#0044FF', 28 | tertiary = '#FF0000', 29 | quaternary = '#00CC00', 30 | grey = LIGHT_PALETTE['grey25'], 31 | grey_light = LIGHT_PALETTE['grey15'], 32 | grey_strong = LIGHT_PALETTE['grey40'], 33 | fill = LIGHT_PALETTE['grey05'], 34 | fill_light = LIGHT_PALETTE['grey02'], 35 | fill_strong = LIGHT_PALETTE['grey10'], 36 | ), 37 | 38 | traces = dict( 39 | line_thin = dict(width = 1,), 40 | line_thick = dict(width = 4,), 41 | line_dashed = dict(dash = 5,), 42 | line_dashed_thin = dict(dash = 5, width = 1,), 43 | line_dashed_thick = dict(dash = 5, width = 4,), 44 | area_dashed = dict(dash = 5,), 45 | area_dashed_thin = dict(dash = 5, width = 1,), 46 | area_dashed_thick = dict(dash = 5, width = 4,), 47 | ), 48 | 49 | additions = dict( 50 | xaxis = dict( 51 | color = '#444444', 52 | tickfont = dict(color = '#222222',), 53 | rangeslider = dict( 54 | bordercolor = '#CCCCCC', 55 | bgcolor = '#CCCCCC', 56 | thickness = 0.1, 57 | ), 58 | rangeselector = dict( 59 | bordercolor = '#C9C9C9', 60 | bgcolor = '#C9C9C9', 61 | activecolor = '#888888', 62 | ), 63 | ), 64 | yaxis = dict( 65 | color = '#444444', 66 | tickfont = dict(color = '#222222',), 67 | side = 'left', 68 | ), 69 | ), 70 | 71 | layout = dict( 72 | font = dict( 73 | family = 'droid sans mono', 74 | size = 12, 75 | color = '#222222', 76 | ), 77 | plot_bgcolor = '#FFFFFF', 78 | paper_bgcolor = '#F3F3F3', 79 | legend = dict( 80 | bgcolor = LIGHT_PALETTE['transparent'], 81 | ), 82 | ), 83 | 84 | ) 85 | 86 | # Dark Quantmod theme 87 | DARK_QM = dict( 88 | 89 | colors = dict( 90 | increasing = '#00FF00', 91 | decreasing = '#FF9900', 92 | border_increasing = DARK_PALETTE['grey95'], 93 | border_decreasing = DARK_PALETTE['grey95'], 94 | primary = '#11AAEE', 95 | secondary = '#0084FF', 96 | tertiary = '#FC0D1B', 97 | quaternary = '#00FF00', 98 | grey = DARK_PALETTE['grey75'], 99 | grey_light = DARK_PALETTE['grey85'], 100 | grey_strong = DARK_PALETTE['grey60'], 101 | fill = DARK_PALETTE['grey90'], 102 | fill_light = DARK_PALETTE['grey95'], 103 | fill_strong = DARK_PALETTE['grey85'], 104 | ), 105 | 106 | traces = dict( 107 | line_thin = dict(width = 1,), 108 | line_thick = dict(width = 4,), 109 | line_dashed = dict(dash = 5,), 110 | line_dashed_thin = dict(dash = 5, width = 1,), 111 | line_dashed_thick = dict(dash = 5, width = 4,), 112 | area_dashed = dict(dash = 5,), 113 | area_dashed_thin = dict(dash = 5, width = 1,), 114 | area_dashed_thick = dict(dash = 5, width = 4,), 115 | ), 116 | 117 | additions = dict( 118 | xaxis = dict( 119 | color = '#999999', 120 | tickfont = dict(color = '#CCCCCC',), 121 | rangeslider = dict( 122 | bordercolor = '#444444', 123 | bgcolor = '#444444', 124 | thickness = 0.1, 125 | ), 126 | rangeselector = dict( 127 | bordercolor = '#444444', 128 | bgcolor = '#444444', 129 | activecolor = '#666666', 130 | ), 131 | ), 132 | yaxis = dict( 133 | color = '#999999', 134 | tickfont = dict(color = '#CCCCCC',), 135 | side = 'left', 136 | ), 137 | ), 138 | 139 | layout = dict( 140 | font = dict( 141 | family = 'droid sans mono', 142 | size = 12, 143 | color = '#CCCCCC', 144 | ), 145 | plot_bgcolor = '#252525', 146 | paper_bgcolor = '#202020', 147 | legend = dict( 148 | bgcolor = DARK_PALETTE['transparent'], 149 | ), 150 | ), 151 | 152 | ) 153 | 154 | 155 | THEMES = {'light': LIGHT_QM, 'dark': DARK_QM} # light-qm': LIGHT_QM, 'dark-qm': DARK_QM} 156 | -------------------------------------------------------------------------------- /quantmod/tools.py: -------------------------------------------------------------------------------- 1 | """Functions meant for user access 2 | 3 | All non-Chart related functions are in this module. 4 | For Chart-related functions go in 'factory.py'. 5 | 6 | """ 7 | from __future__ import absolute_import 8 | 9 | import os 10 | import six 11 | import warnings 12 | import plotly 13 | 14 | from . import auth 15 | from . import utils 16 | from .auth import AUTH_DIR, FILE_CONTENT, CONFIG_FILE 17 | 18 | 19 | pyo = plotly.offline 20 | 21 | 22 | def go_offline(connected=False): 23 | """Take plotting offline. 24 | 25 | __PLOTLY_OFFLINE_INITIALIZED is a secret variable 26 | in plotly/offline/offline.py. 27 | 28 | Parameters 29 | --------- 30 | connected : bool 31 | Determines if init_notebook_mode should be set to 'connected'. 32 | 99% of time will not need to touch this. 33 | 34 | """ 35 | try: 36 | pyo.init_notebook_mode(connected) 37 | except TypeError: 38 | pyo.init_notebook_mode() 39 | 40 | pyo.__PLOTLY_OFFLINE_INITIALIZED = True 41 | 42 | 43 | def go_online(): 44 | """Take plotting offline.""" 45 | pyo.__PLOTLY_OFFLINE_INITIALIZED = False 46 | 47 | 48 | def is_offline(): 49 | """Check online/offline status.""" 50 | return pyo.__PLOTLY_OFFLINE_INITIALIZED 51 | 52 | 53 | def check_url(url=None): 54 | """Check URL integrity. 55 | 56 | Parameters 57 | ---------- 58 | url : string 59 | URL to be checked. 60 | 61 | """ 62 | if url is None: 63 | if 'http' not in get_config_file()['offline_url']: 64 | raise Exception("No default offline URL set. " 65 | "Please run " 66 | "quantmod.set_config_file(offline_url=YOUR_URL) " 67 | "to set the default offline URL.") 68 | else: 69 | url = get_config_file()['offline_url'] 70 | 71 | if url is not None: 72 | if not isinstance(url, six.string_types): 73 | raise TypeError("Invalid url '{0}'. " 74 | "It should be string." 75 | .format(url)) 76 | 77 | pyo.download_plotlyjs(url) 78 | 79 | 80 | def ensure_local_files(): 81 | """Ensure that filesystem is setup/filled out in a valid way.""" 82 | if auth.check_file_permissions(): 83 | 84 | if not os.path.isdir(AUTH_DIR): 85 | os.mkdir(AUTH_DIR) 86 | 87 | for fn in [CONFIG_FILE]: 88 | contents = utils.load_json_dict(fn) 89 | 90 | for key, value in list(FILE_CONTENT[fn].items()): 91 | if key not in contents: 92 | contents[key] = value 93 | contents_keys = list(contents.keys()) 94 | 95 | for key in contents_keys: 96 | if key not in FILE_CONTENT[fn]: 97 | del contents[key] 98 | utils.save_json_dict(fn, contents) 99 | 100 | else: 101 | warnings.warn("Looks like you don't have 'read-write' permission to " 102 | "your specified home ('~') directory.") 103 | 104 | 105 | def set_config_file(sharing=None, theme=None, dimensions=None, 106 | offline=None, offline_url=None, 107 | offline_show_link=None, offline_link_text=None): 108 | """Set the keyword-value pairs in `~/config`. 109 | 110 | Parameters 111 | ---------- 112 | sharing : string or bool 113 | Sets the sharing level permission. 114 | True / 'public' - anyone can see this chart 115 | False / 'private' - only you can see this chart 116 | 'secret' - only people with the link can see the chart 117 | theme : string 118 | Sets the default theme. 119 | See factory.get_themes() for available themes. 120 | dimensions : tuple 121 | Sets the default (width, height) of the chart. 122 | offline : bool 123 | If true then the charts are rendered 124 | locally. 125 | offline_show_link : bool 126 | If true then the chart will show a link to 127 | plot.ly at the bottom right of the chart. 128 | offline_link_text : string 129 | Text to display as link at the bottom 130 | right of the chart. 131 | 132 | """ 133 | if not auth.check_file_permissions(): 134 | raise Exception("You don't have proper file permissions " 135 | "to run this function.") 136 | 137 | config = get_config_file() 138 | 139 | # Type checks for optionally used arguments 140 | if sharing is not None: 141 | if isinstance(sharing, bool): 142 | pass 143 | elif isinstance(sharing, six.string_types): 144 | pass 145 | else: 146 | raise TypeError("Invalid sharing '{0}'. " 147 | "It should be string or bool." 148 | .format(sharing)) 149 | 150 | if theme is not None: 151 | if not isinstance(theme, six.string_types): 152 | raise TypeError("Invalid theme '{0}'. " 153 | "It should be string." 154 | .format(theme)) 155 | 156 | if dimensions is not None: # Cufflinks 157 | if not isinstance(dimensions, tuple): 158 | raise TypeError("Invalid dimensions '{0}'. " 159 | "It should be tuple." 160 | .format(dimensions)) 161 | if not len(dimensions) == 2: 162 | raise Exception("Invalid dimensions '{0}'. " 163 | "It should be tuple of len 2." 164 | .format(dimensions)) 165 | 166 | if offline is not None: 167 | if not isinstance(offline, bool): 168 | raise TypeError("Invalid offline '{0}'. " 169 | "It should be bool." 170 | .format(offline)) 171 | 172 | if offline_url is not None: 173 | if not isinstance(offline_url, six.string_types): 174 | raise TypeError("Invalid offline_url '{0}'. " 175 | "It should be string." 176 | .format(offline_url)) 177 | 178 | if offline_show_link is not None: 179 | if not isinstance(offline_show_link, six.string_types): 180 | raise TypeError("Invalid offline_show_link '{0}'. " 181 | "It should be string." 182 | .format(offline_show_link)) 183 | 184 | if offline_link_text is not None: 185 | if not isinstance(offline_link_text, six.string_types): 186 | raise TypeError("Invalid offline_link_text '{0}'. " 187 | "It should be string." 188 | .format(offline_link_text)) 189 | 190 | # Argument parsing 191 | if sharing is not None: 192 | if sharing is True: 193 | config['sharing'] = 'public' 194 | elif sharing is False: 195 | config['sharing'] = 'private' 196 | else: 197 | config['sharing'] = sharing 198 | 199 | if theme is not None: 200 | config['theme'] = theme 201 | 202 | if dimensions is not None: 203 | config['dimensions'] = dimensions 204 | 205 | if offline is not None: 206 | config['offline'] = offline 207 | if offline: 208 | go_offline() 209 | 210 | if offline_url is not None: 211 | config['offline_url'] = offline_url 212 | 213 | if offline_show_link is not None: 214 | config['offline_show_link'] = offline_show_link 215 | 216 | if offline_link_text is not None: 217 | config['offline_link_text'] = offline_link_text 218 | 219 | utils.save_json_dict(CONFIG_FILE, config) 220 | ensure_local_files() 221 | 222 | 223 | def get_config_file(*args): 224 | """ 225 | Return specified args from `~/config`. as dict. 226 | Return all if no arguments are specified. 227 | 228 | Example 229 | ------- 230 | get_config_file('sharing') 231 | 232 | """ 233 | if auth.check_file_permissions(): 234 | ensure_local_files() 235 | return utils.load_json_dict(CONFIG_FILE, *args) 236 | else: 237 | return FILE_CONTENT[CONFIG_FILE] 238 | 239 | 240 | def reset_config_file(): 241 | """Reset config file to package defaults.""" 242 | ensure_local_files() # Make sure what's there is OK 243 | f = open(CONFIG_FILE, 'w') 244 | f.close() 245 | ensure_local_files() 246 | 247 | 248 | set_credentials_file = plotly.tools.set_credentials_file 249 | get_credentials_file = plotly.tools.get_credentials_file 250 | reset_credentials_file = plotly.tools.reset_credentials_file 251 | -------------------------------------------------------------------------------- /quantmod/utils.py: -------------------------------------------------------------------------------- 1 | """Low-level functions not meant for user access 2 | 3 | Functions used to maintain consistency for certain Python tasks, 4 | e.g. type checking of function arguments. Users should not expect any function 5 | inside this module to keep a consistent API, as they are only used internally. 6 | 7 | """ 8 | from __future__ import absolute_import 9 | 10 | import collections 11 | import json 12 | import os 13 | 14 | 15 | def update(dict1, dict2): 16 | """Recursivel update dict-like objects and returns it. 17 | 18 | Need return to work properly even though dict1 is updated inplace. 19 | 20 | Parameters 21 | ---------- 22 | dict1 : dict 23 | Dictionary that contains the values to update. 24 | dict2 : dict 25 | Dictionary to be updated. 26 | 27 | """ 28 | for key, value in dict2.items(): 29 | if isinstance(value, collections.Mapping): 30 | temp = update(dict1.get(key, {}), value) 31 | dict1[key] = temp 32 | elif isinstance(dict1, collections.Mapping): 33 | dict1[key] = dict2[key] 34 | else: 35 | dict1 = {key: dict2[key]} 36 | 37 | return dict1 38 | 39 | 40 | def deep_update(dict1, dict2): 41 | """Update the values (deep form) of a given dictionary and returns it. 42 | 43 | Need return to work properly even though dict1 is updated inplace. 44 | 45 | Parameters 46 | ---------- 47 | dict1 : dict 48 | Dictionary that contains the values to update. 49 | dict2 : dict 50 | Dictionary to be updated. 51 | 52 | """ 53 | for key, value in dict2.items(): 54 | if isinstance(value, collections.Mapping): 55 | if key in dict1: 56 | deep_update(dict1[key], value) 57 | else: 58 | dict1[key] = value 59 | else: 60 | dict1[key] = value 61 | 62 | return dict1 63 | 64 | 65 | def type_check(arg, arg_types, arg_name): 66 | """Check if argument is of one or multiple allowed types. 67 | 68 | Pass if argument is within an allowed type, and raise exception 69 | if argument is not within these types, thus allowing for 70 | strong typing of Python arguments. 71 | 72 | Parameters 73 | ---------- 74 | arg : [any type] 75 | Argument that can be of any type. 76 | arg_types : list or [any type] 77 | Type or list of allowed argument types. 78 | arg_name : string 79 | Name of argument to be printed in exception. 80 | 81 | Example 82 | ------- 83 | layout = dict(title='Test', showlegend=False) 84 | typecheck(layout, dict, 'layout') # pass 85 | 86 | """ 87 | if not isinstance(arg_types, list): 88 | arg_types = [arg_types] 89 | 90 | if any(isinstance(arg, arg_type) for arg_type in arg_types): 91 | pass 92 | else: 93 | raise Exception("Invalid {0} '{1}'.".format(arg_name, arg)) 94 | 95 | 96 | def kwargs_check(kwargs, validator): 97 | """Check kwargs for validity 98 | 99 | Parameters 100 | ---------- 101 | kwargs : dict 102 | Keyword arguments to check for validity. 103 | validator : iterable 104 | Iterable of valid arguments to check from. 105 | 106 | """ 107 | for key in kwargs: 108 | if key not in validator: 109 | raise Exception("Invalid keyword '{0}'.".format(key)) 110 | 111 | 112 | def parse(kwargs, dict): 113 | """Parse kwargs into input dict. 114 | 115 | Parameters 116 | ---------- 117 | kwargs : dict 118 | Keyword arguments to update dict with 119 | dict : dict 120 | Dict to update with keyword arguments 121 | 122 | """ 123 | for key in kwargs: 124 | dict[key] = kwargs[key] 125 | 126 | 127 | def kwargs_from_keyword(keyword, from_kwargs, to_kwargs=None, inplace=False): 128 | """Look for keys of the format keyword_value. 129 | 130 | Return a dictionary with {keyword: value} format. 131 | 132 | Parameters 133 | ---------- 134 | keyword : string 135 | Keyword to look for in the orginal dictionary. 136 | from_kwargs : dict 137 | Original dictionary. 138 | to_kwargs : dict 139 | Dictionary where the items will be appended. 140 | inplace : bool 141 | If True then the key, value pairs from the original 142 | dictionary are modified. 143 | 144 | """ 145 | if not inplace: 146 | if to_kwargs is None: 147 | to_kwargs = {} 148 | 149 | keys = set(from_kwargs.keys()) 150 | 151 | for key in keys: 152 | if '{0}_'.format(keyword) in key: 153 | updated_key = key.replace('{0}_'.format(keyword), '') 154 | if inplace: 155 | from_kwargs[updated_key] = from_kwargs[key] 156 | del from_kwargs[key] 157 | else: 158 | to_kwargs[updated_key] = from_kwargs[key] 159 | 160 | if not inplace: 161 | return to_kwargs 162 | 163 | 164 | def load_json_dict(filename, *args): 165 | """Check if file exists. Return {} if something fails. 166 | 167 | Parameters 168 | ---------- 169 | filename : string 170 | Filename of file to check. 171 | 172 | """ 173 | data = {} 174 | if os.path.exists(filename): 175 | with open(filename, "r") as f: 176 | try: 177 | data = json.load(f) 178 | if not isinstance(data, dict): 179 | data = {} 180 | except: 181 | pass 182 | if args: 183 | return {key: data[key] for key in args if key in data} 184 | return data 185 | 186 | 187 | def save_json_dict(filename, json_dict): 188 | """Will error if filename is not appropriate, but it's checked elsewhere. 189 | 190 | Parameters 191 | ---------- 192 | filename : string 193 | Filename of json_dict to save. 194 | json_dict : dict 195 | Dict that will be saved as json. 196 | 197 | """ 198 | if isinstance(json_dict, dict): 199 | with open(filename, 'w') as f: 200 | f.write(json.dumps(json_dict, indent=4)) 201 | else: 202 | raise TypeError("Couldn't save because 'json_dict' " 203 | "was not a dictionary.") 204 | -------------------------------------------------------------------------------- /quantmod/valid.py: -------------------------------------------------------------------------------- 1 | """Function validity module not meant for user access 2 | 3 | Quantmod functions have checks against these sets below to guard 4 | against bad input. 5 | 6 | """ 7 | # flake8: noqa 8 | 9 | # Mandatory dict names for skeleton structure 10 | VALID_BASE_COMPONENTS = {'base_colors', 'base_traces', 11 | 'base_additions', 'base_layout',} 12 | 13 | # Mandatory dict names for theme structure 14 | VALID_THEME_COMPONENTS = {'colors', 'traces', 'additions', 'layout',} 15 | 16 | # Valid colors for base_colors or colors 17 | VALID_COLORS = {'increasing', 'decreasing', 18 | 'border_increasing', 'border_decreasing', 19 | 'primary', 'secondary', 'tertiary', 'quaternary', 20 | 'grey', 'grey_light', 'grey_strong', 21 | 'fill', 'fill_light', 'fill_strong', 22 | 'fillcolor',} 23 | 24 | # Valid trace types for base_traces or traces 25 | VALID_TRACES = {'ohlc', 'candlestick', 26 | 'line', 'line_thin', 'line_thick', 'line_dashed', 27 | 'line_dashed_thin', 'line_dashed_thick', 28 | 'area', 'area_dashed', 29 | 'area_dashed_thin', 'area_dashed_thick', 'area_threshold', 30 | 'scatter', 'bar', 'histogram',} 31 | 32 | # Subcategories of VALID_TRACES 33 | OHLC_TRACES = {'ohlc', 'candlestick'} 34 | NONLINEAR_TRACES = {'bar', 'histogram'} 35 | LINEAR_TRACES = VALID_TRACES - (OHLC_TRACES | NONLINEAR_TRACES) 36 | 37 | # Valid addition types for baes_additions or additions 38 | VALID_ADDITIONS = {'xaxis', 'yaxis',} 39 | 40 | # Valid layout arguements for base_layout or layout 41 | VALID_LAYOUT = {'title', 'width', 'height', 'autosize', 42 | 'font', 'margin', 'hovermode', 'barmode', 43 | 'bargap', 'bargroupgap', 'boxgap', 'boxgroupgap', 44 | 'plot_bgcolor', 'paper_bgcolor', 45 | 'showlegend', 'legend',} 46 | 47 | # Valid columns for Chart 48 | VALID_COLUMNS = {'op', 'hi', 'lo', 'cl', 49 | 'aop', 'ahi', 'alo', 'acl', 50 | 'vo', 'di',} 51 | 52 | # Alternative syntax for get_template and make_layout 53 | VALID_TEMPLATE_KWARGS = {'showlegend', 'figsize',} 54 | 55 | # Alternative syntax for to_frame 56 | VALID_FIGURE_KWARGS = {'kind', 'showlegend', 'figsize',} 57 | 58 | # Alternative syntax for TA_indicators 59 | VALID_TA_KWARGS = {'kind', 'kinds', 'type', 'color', 'fillcolor'} 60 | -------------------------------------------------------------------------------- /quantmod/vendors/__init__.py: -------------------------------------------------------------------------------- 1 | """File added for Python 2 support""" 2 | 3 | from __future__ import absolute_import 4 | -------------------------------------------------------------------------------- /quantmod/vendors/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-technical-charting/72af2c0c19bc2c56231061bff2fbd118b71a9819/quantmod/vendors/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /quantmod/vendors/__pycache__/sources.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-technical-charting/72af2c0c19bc2c56231061bff2fbd118b71a9819/quantmod/vendors/__pycache__/sources.cpython-36.pyc -------------------------------------------------------------------------------- /quantmod/vendors/sources.py: -------------------------------------------------------------------------------- 1 | """Quandmod sources module 2 | 3 | Sources are dicts that map OHLC column names to specific data vendors, e.g. 4 | Bloomberg or Yahoo. 5 | 6 | """ 7 | # flake8: noqa 8 | 9 | # Yahoo 10 | YAHOO = dict( 11 | index = 'Date', 12 | op = 'Open', 13 | hi = 'High', 14 | lo = 'Low', 15 | cl = 'Close', 16 | aop = None, 17 | ahi = None, 18 | alo = None, 19 | acl = 'Adj Close', 20 | vo = 'Volume', 21 | di = None, 22 | ) 23 | 24 | # Google 25 | GOOGLE = dict( 26 | index = 'Date', 27 | op = 'Open', 28 | hi = 'High', 29 | lo = 'Low', 30 | cl = 'Close', 31 | aop = None, 32 | ahi = None, 33 | alo = None, 34 | acl = None, 35 | vo = 'Volume', 36 | di = None, 37 | ) 38 | 39 | SOURCES = {'yahoo': YAHOO, 'google': GOOGLE} 40 | -------------------------------------------------------------------------------- /quantmod/version.py: -------------------------------------------------------------------------------- 1 | """Quantmod version 2 | 3 | Past versions 4 | ------------- 5 | 0.1.2 6 | 0.1.1 7 | 0.1.0 8 | 9 | """ 10 | __version__ = "0.1.3" 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | gunicorn>=19.7.1 2 | plotly>=2.0.9 3 | dash==0.21.0 4 | dash-renderer==0.11.3 5 | dash-html-components==0.9.0 6 | dash-core-components==0.18.1 7 | pandas_datareader==0.3.0.post0 8 | pandas==0.19.2 9 | Flask-Caching==1.2.0 10 | Flask==0.12 11 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.1 2 | -------------------------------------------------------------------------------- /screenshots/Screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-technical-charting/72af2c0c19bc2c56231061bff2fbd118b71a9819/screenshots/Screenshot1.png --------------------------------------------------------------------------------