├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── pf.py ├── pf_shortcut.py ├── pypf ├── __init__.py ├── chart.py ├── instrument.py ├── terminal_format.py └── tests │ └── __init__.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | data/ 2 | __pycache__/ 3 | .DS_Store 4 | .tags* 5 | # Compiled python modules. 6 | *.pyc 7 | 8 | # Setuptools distribution folder. 9 | /dist/ 10 | 11 | # Python egg metadata, regenerated from source files by setuptools. 12 | /*.egg-info 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Peter J. Viglucci 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE.txt 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ==== 2 | pypf 3 | ==== 4 | 5 | Simple set of classes that can be used to generate point and figure charts. 6 | The package also includes a script (pf.py) that can be used to create charts 7 | at the command line that look like this:: 8 | 9 | BAC (2017-08-25 o: 23.89 h: 24.07 l: 23.75 c: 23.77) 10 | 1.00% box, 3 box reversal, HL method 11 | signal: sell status: bear correction 12 | 13 | 25.42| . |25.42 14 | 25.17| x . |25.17 15 | 24.92| d x x d . |24.92 16 | 24.68| d 7 d 8 d u . |24.68 17 | 24.43| d x d u d u d . |24.43 18 | 24.19| o x x x d u d u d |24.19 19 | 23.95| o x d u d x u x d u o d u |<< 23.77 20 | 23.71| o x u x d u d x d u d x o d u |23.71 21 | 23.48| o u 4 u d x d 5 d x x d u d u o u |23.48 22 | 23.25| o u u d u d x o d u d x d d u o |23.25 23 | 23.02| o u d u d d u d u d u o u |23.02 24 | 22.79| o u d u o u d u d u o |22.79 25 | 22.56| o d u o u d d u |22.56 26 | 22.34| d u o d 6 |22.34 27 | 22.12| d d |22.12 28 | 21.90| |21.90 29 | 30 | Installation 31 | ------------ 32 | 33 | Install using pip:: 34 | 35 | $ pip3 install --user pypf 36 | 37 | Usage 38 | ----- 39 | 40 | To use in a program, simply do:: 41 | 42 | from pypf.chart import PFChart 43 | from pypf.instrument import YahooSecurity 44 | i = YahooSecurity(symbol, force_download, force_cache, period, debug) 45 | c = PFChart(i, box_size, duration, interval, method, reversal, style, trend_lines, debug) 46 | c.create_chart() 47 | print(c.chart) 48 | 49 | To use at the command line:: 50 | 51 | $ pf.py -d pf --duration 1 --box-size .01 --reversal 3 AAPL 52 | 53 | pf.py supports the following arguments:: 54 | 55 | usage: pf.py [-h] [-d] [--force-cache] [--force-download] [--period PERIOD] 56 | [--provider PROVIDER] 57 | command ... 58 | 59 | positional arguments: 60 | command description 61 | pf create point and figure charts 62 | 63 | optional arguments: 64 | -h, --help show this help message and exit 65 | -d, --debug print debug messages to stderr 66 | --force-cache force use of cached data [default: False] 67 | --force-download force download of data [default: False] 68 | --period PERIOD set the years of data to download [default: 10] 69 | --provider PROVIDER specify the data provider (yahoo or google) [default: 70 | yahoo] 71 | 72 | The pf command supports the following arguments:: 73 | 74 | usage: pf.py pf [-h] [--box-size BOX_SIZE] [--dump-meta-data] 75 | [--duration DURATION] [--interval INTERVAL] [--method METHOD] 76 | [--reversal REVERSAL] [--style] [--suppress-chart] 77 | [--trend-lines] 78 | SYMBOL 79 | 80 | positional arguments: 81 | SYMBOL the symbol of the security to chart 82 | 83 | optional arguments: 84 | -h, --help show this help message and exit 85 | --box-size BOX_SIZE set the % box size [default: 0.01] 86 | --dump-meta-data print chart meta data to stdout [default: False] 87 | --duration DURATION set the duration in years for the chart [default: 1] 88 | --interval INTERVAL specify day (d), week (w), or month (m) interval 89 | [default: d] 90 | --method METHOD specify High/Low (hl) or Close (c) [default: hl] 91 | --reversal REVERSAL set the box reversal [default: 3] 92 | --indent INDENT set the indent of the chart [default: 3] 93 | --truncate TRUNCATE truncate the chart to fixed number of columns [default: 50] 94 | --style use color and style in terminal output [default: False] 95 | --suppress-chart do not print the chart to stdout [default: False] 96 | --trend-lines draw support and resistance lines [default: False] 97 | 98 | License 99 | ------- 100 | 101 | Copyright (c) 2021 Peter J. Viglucci 102 | 103 | Permission is hereby granted, free of charge, to any person obtaining a copy 104 | of this software and associated documentation files (the "Software"), to deal 105 | in the Software without restriction, including without limitation the rights 106 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 107 | copies of the Software, and to permit persons to whom the Software is 108 | furnished to do so, subject to the following conditions: 109 | 110 | The above copyright notice and this permission notice shall be included in all 111 | copies or substantial portions of the Software. 112 | 113 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 114 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 115 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 116 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 117 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 118 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 119 | SOFTWARE. 120 | -------------------------------------------------------------------------------- /pf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Script to create point and figure charts at the command line.""" 3 | from argparse import ArgumentParser 4 | from pypf.chart import PFChart 5 | from pypf.instrument import GoogleSecurity 6 | from pypf.instrument import YahooSecurity 7 | 8 | 9 | def main(): 10 | """Program entry.""" 11 | parser = __get_option_parser() 12 | options = parser.parse_args() 13 | __process_options(options) 14 | 15 | 16 | def __get_option_parser(): 17 | parser = ArgumentParser() 18 | parser.add_argument("-d", "--debug", 19 | action="store_true", dest="debug", 20 | help="print debug messages to stderr") 21 | parser.add_argument("--force-cache", 22 | action="store_true", dest="force_cache", 23 | help="force use of cached data [default: False]") 24 | parser.add_argument("--force-download", 25 | action="store_true", dest="force_download", 26 | help="force download of data [default: False]") 27 | parser.add_argument("--period", 28 | action="store", dest="period", 29 | type=int, default=10, 30 | metavar="PERIOD", 31 | help="set the years of data to download \ 32 | [default: %(default)s]") 33 | parser.add_argument("--provider", 34 | action="store", 35 | dest="provider", 36 | choices=['google', 'yahoo'], default='yahoo', 37 | metavar="PROVIDER", 38 | help="specify the data provider (yahoo or google) \ 39 | [default: %(default)s]") 40 | 41 | # Top level commands 42 | subparsers = parser.add_subparsers(help='description', 43 | metavar="command", 44 | dest='command') 45 | subparsers.required = True 46 | pf_parser = subparsers.add_parser('pf', 47 | help='create point and figure charts') 48 | pf_parser.add_argument("--box-size", 49 | action="store", dest="box_size", 50 | type=float, default=.01, 51 | metavar="BOX_SIZE", 52 | help="set the %% box size [default: %(default)s]") 53 | pf_parser.add_argument("--dump-meta-data", 54 | action="store_true", dest="dump_meta_data", 55 | help="print chart meta data to stdout \ 56 | [default: False]") 57 | pf_parser.add_argument("--duration", 58 | action="store", dest="duration", 59 | type=float, default=1, 60 | metavar="DURATION", 61 | help="set the duration in years for the chart \ 62 | [default: %(default)s]") 63 | pf_parser.add_argument("--interval", 64 | action="store", 65 | dest="interval", 66 | choices=['d', 'w', 'm'], default='d', 67 | metavar="INTERVAL", 68 | help="specify day (d), week (w), or month (m) \ 69 | interval [default: %(default)s]") 70 | pf_parser.add_argument("--method", 71 | action="store", 72 | dest="method", 73 | choices=['hl', 'c'], default='hl', 74 | metavar="METHOD", 75 | help="specify High/Low (hl) or Close (c) \ 76 | [default: %(default)s]") 77 | pf_parser.add_argument("--reversal", 78 | action="store", dest="reversal", 79 | type=int, default=3, 80 | metavar="REVERSAL", 81 | help="set the box reversal [default: %(default)s]") 82 | pf_parser.add_argument("--indent", 83 | action="store", dest="indent", 84 | type=int, default=3, 85 | metavar="INDENT", 86 | help="set the indent of the chart [default: %(default)s]") 87 | pf_parser.add_argument("--truncate", 88 | action="store", dest="truncate", 89 | type=int, default=50, 90 | metavar="TRUNCATE", 91 | help="truncate the chart to fixed number of columns [default: %(default)s]") 92 | pf_parser.add_argument("--style", 93 | action="store_true", dest="style", 94 | help="use color and style in terminal output \ 95 | [default: False]") 96 | pf_parser.add_argument("--suppress-chart", 97 | action="store_true", dest="suppress_chart", 98 | help="do not print the chart to stdout \ 99 | [default: False]") 100 | pf_parser.add_argument("--trend-lines", 101 | action="store_true", dest="trend_lines", 102 | help="draw support and resistance lines \ 103 | [default: False]") 104 | pf_parser.add_argument("symbol", metavar='SYMBOL', 105 | help='the symbol of the security to chart') 106 | 107 | return parser 108 | 109 | 110 | def __process_options(options): 111 | debug = options.debug 112 | interval = options.interval 113 | force_download = options.force_download 114 | force_cache = options.force_cache 115 | period = options.period 116 | 117 | box_size = options.box_size 118 | duration = options.duration 119 | method = options.method 120 | reversal = options.reversal 121 | style = options.style 122 | trend_lines = options.trend_lines 123 | symbol = options.symbol 124 | indent = options.indent 125 | truncate = options.truncate 126 | 127 | if options.provider == 'google': 128 | security = GoogleSecurity(symbol, force_download, force_cache, 129 | period, debug) 130 | else: 131 | security = YahooSecurity(symbol, force_download, force_cache, 132 | period, debug) 133 | chart = PFChart(security, box_size, duration, interval, method, 134 | reversal, style, trend_lines, debug, indent, truncate) 135 | chart.create_chart() 136 | if options.suppress_chart is False: 137 | print(chart.chart) 138 | if options.dump_meta_data is True: 139 | for day in chart.chart_meta_data: 140 | print(chart.chart_meta_data[day]) 141 | 142 | if __name__ == "__main__": 143 | main() 144 | -------------------------------------------------------------------------------- /pf_shortcut.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Script to create multiple point and figure charts. Good to look at the same charts frequently.""" 3 | from argparse import ArgumentParser 4 | from pypf.chart import PFChart 5 | from pypf.instrument import YahooSecurity 6 | 7 | 8 | def main(): 9 | """Program entry.""" 10 | debug = False 11 | interval = 'd' 12 | force_download = False 13 | force_cache = False 14 | period = 10 15 | 16 | box_size = .01 17 | method = 'hl' 18 | reversal = 3 19 | style = True 20 | trend_lines = True 21 | indent = 3 22 | 23 | truncate = 50 24 | duration = 4 25 | 26 | symbols = [['spy',duration], ['dia',duration], ['qqq',duration], ['bac',duration], ['bk',duration]] 27 | 28 | 29 | for pf in symbols: 30 | symbol = pf[0] 31 | duration = pf[1] 32 | security = YahooSecurity(symbol, force_download, force_cache, 33 | period, debug) 34 | chart = PFChart(security, box_size, duration, interval, method, 35 | reversal, style, trend_lines, debug, indent, truncate) 36 | chart.create_chart() 37 | print(chart.chart) 38 | 39 | 40 | if __name__ == "__main__": 41 | main() 42 | -------------------------------------------------------------------------------- /pypf/__init__.py: -------------------------------------------------------------------------------- 1 | """Module initialization.""" 2 | import logging 3 | 4 | logging.basicConfig(format=('%(levelname)-8s' 5 | + '%(name)14s:' 6 | + '%(lineno)4d ' 7 | + '%(funcName)22s() -- ' 8 | + '%(message)s'), 9 | level=logging.WARNING) 10 | -------------------------------------------------------------------------------- /pypf/chart.py: -------------------------------------------------------------------------------- 1 | """Classes to generate point and figure charts.""" 2 | from collections import OrderedDict 3 | from datetime import datetime 4 | from decimal import Decimal 5 | 6 | import logging 7 | import pypf.terminal_format 8 | 9 | 10 | class PFChart(object): 11 | """Base class for point and figure charts.""" 12 | 13 | TWOPLACES = Decimal('0.01') 14 | 15 | def __init__(self, instrument, box_size=.01, duration=1.0, 16 | interval='d', method='hl', reversal=3, style=False, 17 | trend_lines=False, debug=False, indent=0, truncate=0): 18 | """Initialize common functionality.""" 19 | self._log = logging.getLogger(self.__class__.__name__) 20 | if debug is True: 21 | self._log.setLevel(logging.DEBUG) 22 | self._log.debug(self) 23 | 24 | self.instrument = instrument 25 | self.interval = interval 26 | self.box_size = Decimal(box_size).quantize(PFChart.TWOPLACES) 27 | self.duration = Decimal(duration).quantize(PFChart.TWOPLACES) 28 | self.method = method 29 | self.reversal = int(reversal) 30 | self.style_output = style 31 | self.trend_lines = trend_lines 32 | self.indent = indent 33 | self.truncate = truncate 34 | 35 | @property 36 | def indent(self): 37 | """Get the box_size.""" 38 | return "".rjust(self._indent) 39 | 40 | @indent.setter 41 | def indent(self, value): 42 | self._indent = value 43 | self._log.debug('set self._indent to ' + str(value)) 44 | 45 | @property 46 | def truncate(self): 47 | """Get the box_size.""" 48 | return self._truncate 49 | 50 | @truncate.setter 51 | def truncate(self, value): 52 | self._truncate = value 53 | self._log.debug('set self._truncate to ' + str(self._truncate)) 54 | 55 | @property 56 | def box_size(self): 57 | """Get the box_size.""" 58 | return self._box_size 59 | 60 | @box_size.setter 61 | def box_size(self, value): 62 | self._box_size = value 63 | self._log.debug('set self._box_size to ' + str(self._box_size)) 64 | 65 | @property 66 | def chart(self): 67 | """Get the chart.""" 68 | return self._chart 69 | 70 | @property 71 | def chart_meta_data(self): 72 | """Get the chart meta data.""" 73 | return self._chart_meta_data 74 | 75 | @property 76 | def duration(self): 77 | """Get the duration.""" 78 | return self._duration 79 | 80 | @duration.setter 81 | def duration(self, value): 82 | self._duration = value 83 | self._log.debug('set self._duration to ' + str(self._duration)) 84 | 85 | @property 86 | def instrument(self): 87 | """Get the instrument.""" 88 | return self._instrument 89 | 90 | @instrument.setter 91 | def instrument(self, value): 92 | self._instrument = value 93 | self._log.debug('set self._instrument to ' + str(self._instrument)) 94 | 95 | @property 96 | def interval(self): 97 | """Specify day (d), week (w), or month (m) interval.""" 98 | return self._interval 99 | 100 | @interval.setter 101 | def interval(self, value): 102 | if value not in ["d", "w", "m"]: 103 | raise ValueError("incorrect interval: " 104 | "valid intervals are d, w, m") 105 | self._interval = value 106 | self._log.debug('set self._interval to ' 107 | + str(self._interval)) 108 | 109 | @property 110 | def method(self): 111 | """Get the method.""" 112 | return self._method 113 | 114 | @method.setter 115 | def method(self, value): 116 | if value not in ["hl", "c"]: 117 | raise ValueError("incorrect method: " 118 | "valid methods are hl, c") 119 | self._method = value 120 | self._log.debug('set self._method to ' + self._method) 121 | 122 | @property 123 | def reversal(self): 124 | """Get the reversal.""" 125 | return self._reversal 126 | 127 | @reversal.setter 128 | def reversal(self, value): 129 | self._reversal = value 130 | self._log.debug('set self._reversal to ' + str(self._reversal)) 131 | 132 | @property 133 | def style_output(self): 134 | """Get the style_output.""" 135 | return self._style_output 136 | 137 | @style_output.setter 138 | def style_output(self, value): 139 | self._style_output = value 140 | self._log.debug('set self._style_output to ' + str(self._style_output)) 141 | 142 | @property 143 | def trend_lines(self): 144 | """Get the trend_lines.""" 145 | return self._trend_lines 146 | 147 | @trend_lines.setter 148 | def trend_lines(self, value): 149 | self._trend_lines = value 150 | self._log.debug('set self._trend_lines to ' + str(self._trend_lines)) 151 | 152 | def create_chart(self): 153 | """Populate the data and create the chart.""" 154 | self._initialize() 155 | self._set_historical_data() 156 | self._set_price_fields() 157 | self._set_scale() 158 | self._set_chart_data() 159 | self._chart = self._get_chart() 160 | 161 | def _get_chart(self): 162 | self._set_current_state() 163 | chart = "" 164 | chart += "\n" 165 | chart += self._get_chart_title() 166 | 167 | index = len(self._chart_data[0]) - 1 168 | if self.truncate > 0: 169 | first_column = self._chart_data.pop(0) 170 | self._chart_data = self._chart_data[-self.truncate:] 171 | self._chart_data.insert(0,first_column) 172 | 173 | scale_right = None 174 | self._log.info(len(self._chart_data)) 175 | while index >= 0: 176 | found = False 177 | first = True 178 | for column in self._chart_data: 179 | if first: 180 | first = False 181 | continue 182 | if index in column: 183 | found = True 184 | break 185 | if found: 186 | first = True 187 | for column in self._chart_data: 188 | if index in column: 189 | if first: 190 | scale_value = column[index] 191 | if index == self._current_scale_index: 192 | scale_left = (self._style('red', 193 | self._style('bold', '{:7.2f}')) 194 | .format(scale_value)) 195 | scale_right = (self._style('red', 196 | self._style('bold', '<< ')) 197 | + self._style('red', 198 | self._style('bold', 199 | '{:.2f}')) 200 | .format(self._current_close)) 201 | else: 202 | scale_left = '{:7.2f}'.format(scale_value) 203 | scale_right = '{:.2f}'.format(scale_value) 204 | chart = chart + self.indent + scale_left + '| ' 205 | first = False 206 | else: 207 | chart = chart + ' ' + column[index][0] 208 | else: 209 | chart += ' ' 210 | 211 | chart += ' |' + scale_right 212 | chart += "\n" 213 | 214 | index -= 1 215 | return chart 216 | 217 | def _get_chart_title(self): 218 | self._set_current_prices() 219 | title = self.indent 220 | title = title + self._style('bold', 221 | self._style('underline', 222 | self.instrument.symbol)) 223 | title = (title + ' ' 224 | + "(" + self._style('bold', str(self.instrument.download_timestamp.strftime("%a %b %d, %Y %H:%M:%S"))) + ")\n") 225 | 226 | title = (title + self.indent 227 | + "o: {:.2f} h: {:.2f} l: {:.2f} c: {:.2f}" 228 | .format(self._current_open, 229 | self._current_high, self._current_low, 230 | self._current_close) 231 | + "\n") 232 | title = (title + self.indent 233 | + "box: " 234 | + str((self.box_size * 100).quantize(PFChart.TWOPLACES))) 235 | title = title + ", reversal: " + str(self.reversal) 236 | title = title + ", method: " + str(self.method) + "\n" 237 | title = (title + self.indent 238 | + "signal: " 239 | + self._style('bold', self._current_signal) 240 | + ", status: " + self._style('bold', self._current_status) 241 | + "\n\n") 242 | return title 243 | 244 | def _get_month(self, date_value): 245 | datetime_object = datetime.strptime(date_value, '%Y-%m-%d') 246 | month = str(datetime_object.month) 247 | if month == '10': 248 | month = 'A' 249 | elif month == '11': 250 | month = 'B' 251 | elif month == '12': 252 | month = 'C' 253 | return self._style('bold', self._style('red', month)) 254 | 255 | def _get_scale_index(self, value, direction): 256 | index = 0 257 | while index < len(self._scale): 258 | if self._scale[index] == value: 259 | return index 260 | elif self._scale[index] > value: 261 | if direction == 'x': 262 | return index - 1 263 | else: 264 | return index 265 | index += 1 266 | 267 | def _get_status(self, signal, direction): 268 | if signal == 'buy' and direction == 'x': 269 | status = 'bull confirmed' 270 | elif signal == 'buy' and direction == 'o': 271 | status = 'bull correction' 272 | elif signal == 'sell' and direction == 'o': 273 | status = 'bear confirmed' 274 | elif signal == 'sell' and direction == 'x': 275 | status = 'bear correction' 276 | else: 277 | status = 'none' 278 | return status 279 | 280 | def _set_chart_data(self): 281 | self._log.info('generating chart') 282 | self._chart_data = [] 283 | self._chart_meta_data = OrderedDict() 284 | self._support_lines = [] 285 | self._resistance_lines = [] 286 | self._chart_data.append(self._scale) 287 | 288 | column = OrderedDict() 289 | column_index = 1 290 | direction = 'x' 291 | index = None 292 | month = None 293 | signal = 'none' 294 | prior_high_index = len(self._scale) - 1 295 | prior_low_index = 0 296 | 297 | for row in self._historical_data: 298 | day = self._historical_data[row] 299 | action = 'none' 300 | move = 0 301 | current_month = self._get_month(day[self._date_field]) 302 | 303 | if index is None: 304 | # First day - set the starting index based 305 | # on the high and 'x' direction 306 | index = self._get_scale_index(day[self._high_field], 'x') 307 | column[index] = ['x', day[self._date_field]] 308 | month = current_month 309 | continue 310 | 311 | if direction == 'x': 312 | scale_index = self._get_scale_index(day[self._high_field], 'x') 313 | 314 | if scale_index > index: 315 | # new high 316 | action = 'x' 317 | move = scale_index - index 318 | 319 | if signal != 'buy' and scale_index > prior_high_index: 320 | signal = 'buy' 321 | 322 | first = True 323 | while index < scale_index: 324 | index += 1 325 | if first: 326 | if current_month != month: 327 | column[index] = [current_month, 328 | day[self._date_field]] 329 | else: 330 | column[index] = ['x', day[self._date_field]] 331 | first = False 332 | else: 333 | column[index] = ['x', day[self._date_field]] 334 | month = current_month 335 | else: 336 | # check for reversal 337 | x_scale_index = scale_index 338 | scale_index = self._get_scale_index(day[self._low_field], 339 | 'o') 340 | if index - scale_index >= self.reversal: 341 | # reversal 342 | action = 'reverse x->o' 343 | move = index - scale_index 344 | 345 | if signal != 'sell' and scale_index < prior_low_index: 346 | signal = 'sell' 347 | 348 | prior_high_index = index 349 | self._resistance_lines.append([column_index, 350 | prior_high_index + 1]) 351 | self._chart_data.append(column) 352 | column_index += 1 353 | column = OrderedDict() 354 | direction = 'o' 355 | first = True 356 | while index > scale_index: 357 | index -= 1 358 | if first: 359 | if current_month != month: 360 | column[index] = [current_month, 361 | day[self._date_field]] 362 | else: 363 | column[index] = ['d', 364 | day[self._date_field]] 365 | first = False 366 | else: 367 | column[index] = ['d', day[self._date_field]] 368 | month = current_month 369 | else: 370 | # no reversal - reset the scale_index 371 | scale_index = x_scale_index 372 | else: 373 | # in an 'o' column 374 | scale_index = self._get_scale_index(day[self._low_field], 'o') 375 | if scale_index < index: 376 | # new low 377 | action = 'o' 378 | move = index - scale_index 379 | 380 | if signal != 'sell' and scale_index < prior_low_index: 381 | signal = 'sell' 382 | 383 | first = True 384 | while index > scale_index: 385 | index -= 1 386 | if first: 387 | if current_month != month: 388 | column[index] = [current_month, 389 | day[self._date_field]] 390 | else: 391 | column[index] = ['o', day[self._date_field]] 392 | first = False 393 | else: 394 | column[index] = ['o', day[self._date_field]] 395 | month = current_month 396 | else: 397 | # check for reversal 398 | o_scale_index = scale_index 399 | scale_index = self._get_scale_index(day[self._high_field], 400 | 'x') 401 | if scale_index - index >= self.reversal: 402 | # reversal 403 | action = 'reverse o->x' 404 | move = scale_index - index 405 | 406 | if signal != 'buy' and scale_index > prior_high_index: 407 | signal = 'buy' 408 | 409 | prior_low_index = index 410 | self._support_lines.append([column_index, 411 | prior_low_index - 1]) 412 | self._chart_data.append(column) 413 | column_index += 1 414 | column = OrderedDict() 415 | direction = 'x' 416 | first = True 417 | while index < scale_index: 418 | index += 1 419 | if first: 420 | if current_month != month: 421 | column[index] = [current_month, 422 | day[self._date_field]] 423 | else: 424 | column[index] = ['u', 425 | day[self._date_field]] 426 | first = False 427 | else: 428 | column[index] = ['u', day[self._date_field]] 429 | month = current_month 430 | else: 431 | # no reversal - reset the scale_index 432 | scale_index = o_scale_index 433 | 434 | # Store the meta data for the day 435 | status = self._get_status(signal, direction) 436 | scale_value = (self._scale[scale_index] 437 | .quantize(PFChart.TWOPLACES)) 438 | prior_high = self._scale[prior_high_index] 439 | prior_low = self._scale[prior_low_index] 440 | self._store_base_metadata(day, signal, status, action, move, 441 | column_index, scale_index, scale_value, 442 | direction, prior_high, prior_low) 443 | 444 | self._chart_data.append(column) 445 | 446 | if len(self._chart_data[1]) < self.reversal: 447 | self._chart_data.pop(1) 448 | for line in self._support_lines: 449 | line[0] = line[0] - 1 450 | for line in self._resistance_lines: 451 | line[0] = line[0] - 1 452 | 453 | if self.trend_lines: 454 | self._set_trend_lines() 455 | 456 | return self._chart_data 457 | 458 | def _initialize(self): 459 | self._chart = None 460 | self._chart_data = [] 461 | self._chart_meta_data = OrderedDict() 462 | self._historical_data = [] 463 | self._scale = OrderedDict() 464 | 465 | self._current_date = None 466 | self._current_open = None 467 | self._current_high = None 468 | self._current_low = None 469 | self._current_close = None 470 | 471 | self._date_field = None 472 | self._open_field = None 473 | self._high_field = None 474 | self._low_field = None 475 | self._close_field = None 476 | self._volume_field = None 477 | 478 | self._current_signal = None 479 | self._current_status = None 480 | self._current_action = None 481 | self._current_move = None 482 | self._current_column_index = None 483 | self._current_scale_index = None 484 | self._current_scale_value = None 485 | self._current_direction = None 486 | self._support_lines = [] 487 | self._resistance_lines = [] 488 | 489 | def _is_complete_line(self, start_point, line_type='support'): 490 | c_index = start_point[0] 491 | s_index = start_point[1] 492 | while c_index < len(self._chart_data): 493 | if s_index in self._chart_data[c_index]: 494 | return False 495 | c_index += 1 496 | if line_type == 'support': 497 | s_index += 1 498 | else: 499 | s_index -= 1 500 | if c_index - start_point[0] > 2: 501 | return True 502 | else: 503 | return False 504 | 505 | def _set_trend_lines(self): 506 | for start_point in self._support_lines: 507 | c_index = start_point[0] 508 | s_index = start_point[1] 509 | if self._is_complete_line(start_point, 'support'): 510 | while c_index < len(self._chart_data): 511 | self._chart_data[c_index][s_index] = [self._style('bold', 512 | self._style('blue', 513 | '.')), 514 | ''] 515 | c_index += 1 516 | s_index += 1 517 | 518 | for start_point in self._resistance_lines: 519 | c_index = start_point[0] 520 | s_index = start_point[1] 521 | if self._is_complete_line(start_point, 'resistance'): 522 | while c_index < len(self._chart_data): 523 | self._chart_data[c_index][s_index] = [self._style('bold', 524 | self._style('blue', 525 | '.')), 526 | ''] 527 | c_index += 1 528 | s_index -= 1 529 | 530 | def _set_current_prices(self): 531 | day = next(reversed(self._historical_data)) 532 | current_day = self._historical_data[day] 533 | self._current_date = current_day[self._date_field] 534 | self._current_open = (current_day[self._open_field] 535 | .quantize(PFChart.TWOPLACES)) 536 | self._current_high = (current_day[self._high_field] 537 | .quantize(PFChart.TWOPLACES)) 538 | self._current_low = (current_day[self._low_field] 539 | .quantize(PFChart.TWOPLACES)) 540 | self._current_close = (current_day[self._close_field] 541 | .quantize(PFChart.TWOPLACES)) 542 | 543 | def _set_current_state(self): 544 | current_meta_index = next(reversed(self._chart_meta_data)) 545 | current_meta = self._chart_meta_data[current_meta_index] 546 | self._current_signal = current_meta['signal'] 547 | self._current_status = current_meta['status'] 548 | self._current_action = current_meta['action'] 549 | self._current_move = current_meta['move'] 550 | self._current_column_index = current_meta['column_index'] 551 | self._current_scale_index = current_meta['scale_index'] 552 | self._current_scale_value = current_meta['scale_value'] 553 | self._current_direction = current_meta['direction'] 554 | 555 | def _set_historical_data(self): 556 | self._log.info('setting historical data') 557 | if len(self.instrument.daily_historical_data) == 0: 558 | self.instrument.populate_data() 559 | 560 | if self.interval == 'd': 561 | days = int(self.duration * 252) 562 | self._historical_data = self.instrument.daily_historical_data 563 | elif self.interval == 'w': 564 | days = int(self.duration * 52) 565 | self._historical_data = self.instrument.weekly_historical_data 566 | elif self.interval == 'm': 567 | days = int(self.duration * 12) 568 | self._historical_data = self.instrument.monthly_historical_data 569 | 570 | if len(self._historical_data) > days: 571 | offset = len(self._historical_data) - days 572 | i = 0 573 | while i < offset: 574 | self._historical_data.popitem(False) 575 | i += 1 576 | 577 | def _set_price_fields(self): 578 | if self.method == 'hl': 579 | self._high_field = 'High' 580 | self._low_field = 'Low' 581 | else: 582 | self._high_field = 'Close' 583 | self._low_field = 'Close' 584 | self._open_field = 'Open' 585 | self._close_field = 'Close' 586 | self._volume_field = 'Volume' 587 | self._date_field = 'Date' 588 | 589 | def _set_scale(self): 590 | row = next(iter(self._historical_data)) 591 | day = self._historical_data[row] 592 | highest = day[self._high_field] 593 | lowest = day[self._low_field] 594 | 595 | for row in self._historical_data: 596 | day = self._historical_data[row] 597 | if day[self._high_field] > highest: 598 | highest = day[self._high_field] 599 | if day[self._low_field] < lowest: 600 | lowest = day[self._low_field] 601 | 602 | temp_scale = [] 603 | current = Decimal(.01) 604 | temp_scale.append(current) 605 | 606 | while current <= highest: 607 | value = current + (current * self.box_size) 608 | temp_scale.append(value) 609 | current = value 610 | 611 | slice_point = 0 612 | for index, scale_value in enumerate(temp_scale): 613 | if scale_value > lowest: 614 | slice_point = index - 1 615 | break 616 | temp_scale = temp_scale[slice_point:] 617 | 618 | self._scale = OrderedDict() 619 | for index, scale_value in enumerate(temp_scale): 620 | self._scale[index] = scale_value.quantize(PFChart.TWOPLACES) 621 | 622 | def _store_base_metadata(self, day, signal, status, action, move, 623 | column_index, scale_index, scale_value, 624 | direction, prior_high, prior_low): 625 | date_value = day['Date'] 626 | self._chart_meta_data[date_value] = {} 627 | self._chart_meta_data[date_value]['signal'] = signal 628 | self._chart_meta_data[date_value]['status'] = status 629 | self._chart_meta_data[date_value]['action'] = action 630 | self._chart_meta_data[date_value]['move'] = move 631 | self._chart_meta_data[date_value]['column_index'] = column_index 632 | self._chart_meta_data[date_value]['scale_index'] = scale_index 633 | self._chart_meta_data[date_value]['scale_value'] = scale_value 634 | self._chart_meta_data[date_value]['direction'] = direction 635 | self._chart_meta_data[date_value]['prior_high'] = (prior_high 636 | .quantize( 637 | PFChart 638 | .TWOPLACES)) 639 | self._chart_meta_data[date_value]['prior_low'] = (prior_low 640 | .quantize( 641 | PFChart 642 | .TWOPLACES)) 643 | self._chart_meta_data[date_value]['date'] = day['Date'] 644 | self._chart_meta_data[date_value]['open'] = (day['Open'] 645 | .quantize( 646 | PFChart 647 | .TWOPLACES)) 648 | self._chart_meta_data[date_value]['high'] = (day['High'] 649 | .quantize( 650 | PFChart 651 | .TWOPLACES)) 652 | self._chart_meta_data[date_value]['low'] = (day['Low'] 653 | .quantize( 654 | PFChart 655 | .TWOPLACES)) 656 | self._chart_meta_data[date_value]['close'] = (day['Close'] 657 | .quantize( 658 | PFChart 659 | .TWOPLACES)) 660 | self._chart_meta_data[date_value]['volume'] = day['Volume'] 661 | self._store_custom_metadata(day) 662 | 663 | def _store_custom_metadata(self, day): 664 | pass 665 | 666 | def _style(self, style, message): 667 | if self.style_output: 668 | method = getattr(pypf.terminal_format, style) 669 | return method(message) 670 | else: 671 | return message 672 | -------------------------------------------------------------------------------- /pypf/instrument.py: -------------------------------------------------------------------------------- 1 | """Classes to represent financial instruments.""" 2 | from collections import OrderedDict 3 | from decimal import Decimal 4 | from io import StringIO 5 | 6 | import csv 7 | import datetime 8 | import logging 9 | import os 10 | import re 11 | import requests 12 | import time 13 | 14 | import urllib.parse 15 | 16 | 17 | class Instrument(object): 18 | """Base class for all Instruments.""" 19 | 20 | TWOPLACES = Decimal('0.01') 21 | 22 | def __init__(self, symbol, force_download=False, force_cache=False, 23 | period=10, debug=False, data_directory='~/.pypf/data', 24 | data_file=''): 25 | """Initialize the common functionality for all Instruments.""" 26 | self._log = logging.getLogger(self.__class__.__name__) 27 | if debug is True: 28 | self._log.setLevel(logging.DEBUG) 29 | self._log.debug(self) 30 | 31 | self._data_directory = '' 32 | self._data_file = '' 33 | 34 | self.data_directory = data_directory 35 | self.data_file = data_file 36 | self.force_cache = force_cache 37 | self.force_download = force_download 38 | self.daily_historical_data = OrderedDict() 39 | self.weekly_historical_data = OrderedDict() 40 | self.monthly_historical_data = OrderedDict() 41 | self.period = int(period) 42 | self.symbol = symbol 43 | 44 | @property 45 | def data_directory(self): 46 | """Set the directory in which to store historical data.""" 47 | return self._data_directory 48 | 49 | @data_directory.setter 50 | def data_directory(self, value): 51 | value = os.path.expanduser(value) 52 | if os.path.isdir(value) is False: 53 | self._log.info('creating data directory ' + value) 54 | os.makedirs(value) 55 | self._data_directory = value 56 | self._data_path = os.path.join(value, self.data_file) 57 | self._log.debug('set self._data_directory to ' 58 | + str(self._data_directory)) 59 | self._log.debug('updating self._data_path to ' + str(self._data_path)) 60 | 61 | @property 62 | def data_file(self): 63 | """Set the file name that contains the historical data.""" 64 | return self._data_file 65 | 66 | @data_file.setter 67 | def data_file(self, value): 68 | self._data_file = value 69 | self._data_path = os.path.join(self.data_directory, value) 70 | self._log.debug('set self._data_file to ' 71 | + str(self._data_file)) 72 | self._log.debug('updating self._data_path to ' + str(self._data_path)) 73 | 74 | @property 75 | def data_path(self): 76 | """Get the full path of the data file.""" 77 | return self._data_path 78 | 79 | @property 80 | def download_timestamp(self): 81 | """Get the datetime the data was last downloaded""" 82 | modification_time = os.path.getmtime(self.data_path) 83 | return datetime.datetime.fromtimestamp(modification_time) 84 | 85 | @property 86 | def force_cache(self): 87 | """Force use of cached data.""" 88 | return self._force_cache 89 | 90 | @force_cache.setter 91 | def force_cache(self, value): 92 | self._force_cache = value 93 | self._log.debug('set self._force_cache to ' 94 | + str(self._force_cache)) 95 | 96 | @property 97 | def force_download(self): 98 | """Force download of data.""" 99 | return self._force_download 100 | 101 | @force_download.setter 102 | def force_download(self, value): 103 | self._force_download = value 104 | self._log.debug('set self._force_download to ' 105 | + str(self._force_download)) 106 | 107 | @property 108 | def period(self): 109 | """Set the years of data to download.""" 110 | return self._period 111 | 112 | @period.setter 113 | def period(self, value): 114 | if value <= 0: 115 | raise ValueError('period must be greater than 0.') 116 | self._period = value 117 | now = datetime.datetime.now() 118 | m = now.month 119 | d = now.day 120 | y = now.year - value 121 | self._start_date = int(time.mktime(datetime 122 | .datetime(y, m, d).timetuple())) 123 | self._end_date = int(time.time()) 124 | self._log.debug('set self._period to ' 125 | + str(self._period)) 126 | self._log.debug('updating self._start_date to ' 127 | + str(self._start_date)) 128 | self._log.debug('updating self._end_date to ' 129 | + str(self._end_date)) 130 | 131 | @property 132 | def symbol(self): 133 | """Set the symbol of the instrument.""" 134 | return self._symbol 135 | 136 | @symbol.setter 137 | def symbol(self, value): 138 | self._symbol = value.upper() 139 | self._log.debug('set self._symbol to ' 140 | + str(self._symbol)) 141 | 142 | def populate_data(self): 143 | """Populate the instrument with data. 144 | 145 | Data will only be downloaded if the data file doesn't exist or 146 | if the modification time of the file does not equal the current 147 | date. This behavior can be overridden with the --force-cache 148 | and --force-download options. 149 | """ 150 | self.daily_historical_data = OrderedDict() 151 | self.weekly_historical_data = OrderedDict() 152 | self.monthly_historical_data = OrderedDict() 153 | download_data = False 154 | 155 | if self.force_download: 156 | download_data = True 157 | else: 158 | if os.path.isfile(self.data_path): 159 | modification_time = os.path.getmtime(self.data_path) 160 | last_modified_date = (datetime.date 161 | .fromtimestamp(modification_time)) 162 | today = datetime.datetime.now().date() 163 | 164 | last_modified_time = datetime.datetime.fromtimestamp(modification_time) 165 | now = datetime.datetime.now() 166 | fourpm = now.replace(hour=16, minute=0, second=0, microsecond=0) 167 | 168 | if last_modified_date != today: 169 | download_data = True 170 | elif last_modified_date == today and last_modified_time < fourpm and now > fourpm: 171 | download_data = True 172 | else: 173 | download_data = True 174 | 175 | if self.force_cache: 176 | download_data = False 177 | 178 | if download_data: 179 | self._log.info('downloading data for ' + self.symbol) 180 | self._download_data() 181 | else: 182 | self._log.info('using cached data for ' + self.symbol) 183 | 184 | self._set_daily_data() 185 | self._set_weekly_data() 186 | self._set_monthly_data() 187 | 188 | def _set_daily_data(self): 189 | self._log.debug('setting daily historical data') 190 | csv_file = open(self.data_path, newline='') 191 | reader = csv.DictReader(csv_file) 192 | for row in reader: 193 | row['Open'] = Decimal(row['Open']).quantize(Instrument.TWOPLACES) 194 | row['High'] = Decimal(row['High']).quantize(Instrument.TWOPLACES) 195 | row['Low'] = Decimal(row['Low']).quantize(Instrument.TWOPLACES) 196 | row['Close'] = Decimal(row['Close']).quantize(Instrument.TWOPLACES) 197 | row['Volume'] = int(row['Volume']) 198 | self.daily_historical_data[row['Date']] = row 199 | 200 | def _set_weekly_data(self): 201 | self._log.debug('setting weekly historical data') 202 | 203 | def _set_monthly_data(self): 204 | self._log.debug('setting monthly historical data') 205 | 206 | def _download_data(self): 207 | """To be implemented in derived classes. 208 | 209 | Data must be stored in a csv file with the following heading: 210 | Date,Open,High,Low,Close,Volume 211 | Make any adjustments to the data before saving the data 212 | to the csv file. 213 | """ 214 | raise 215 | 216 | 217 | class YahooSecurity(Instrument): 218 | """Security instrument that uses Yahoo as the datasource.""" 219 | 220 | def __init__(self, symbol, force_download=False, force_cache=False, 221 | period=10, debug=False, data_directory='~/.pypf/data'): 222 | """Initialize the security.""" 223 | super().__init__(symbol, force_download, force_cache, 224 | period, debug, data_directory) 225 | self._log.info('formatting symbol for yahoo') 226 | self.symbol = self.symbol.replace('.', '-') 227 | self.data_file = (self.symbol 228 | + '_yahoo' 229 | + '.csv') 230 | 231 | def _get_cookie_crumb(self): 232 | """Return a tuple pair of cookie and crumb used in the request.""" 233 | self._log.info('getting cookie and crumb') 234 | url = 'https://finance.yahoo.com/quote/%s/history' % (self.symbol) 235 | self._log.debug(url) 236 | r = requests.get(url, headers={'User-agent': 'Mozilla/5.0'}) 237 | txt = r.content 238 | cookie = r.cookies['B'] 239 | pattern = re.compile('.*"CrumbStore":\{"crumb":"(?P[^"]+)"\}') 240 | for line in txt.splitlines(): 241 | m = pattern.match(line.decode("utf-8")) 242 | if m is not None: 243 | crumb = m.groupdict()['crumb'] 244 | crumb = crumb.replace(u'\\u002F', '/') 245 | return cookie, crumb 246 | 247 | def _download_data(self): 248 | cookie, crumb = self._get_cookie_crumb() 249 | self._log.debug('cookie is ' + str(cookie)) 250 | self._log.debug('crumb is ' + str(crumb)) 251 | api_url = ("https://query1.finance.yahoo.com/v7/finance/" 252 | "download/%s?period1=%s&period2=%s&interval=%s" 253 | "&events=history&crumb=%s") 254 | url = api_url % (self.symbol, self._start_date, self._end_date, 255 | '1d', crumb) 256 | self._log.info('fetching data') 257 | self._log.debug(url) 258 | data = requests.get(url, headers={'User-agent': 'Mozilla/5.0'}, cookies={'B': cookie}) 259 | content = StringIO(data.content.decode("utf-8")) 260 | self._log.info('saving data to ' + self.data_path) 261 | with open(self.data_path, 'w', newline='') as csvfile: 262 | first = True 263 | for row in content.readlines(): 264 | if first is True: 265 | csvfile.write("Date,Open,High,Low,Close,Volume\n") 266 | first = False 267 | continue 268 | # Yahoo provides an Adj Close - fields[5] 269 | # We use it to compute a factor to adjust the data 270 | fields = row.split(',') 271 | new_row = [] 272 | factor = Decimal(fields[5]) / Decimal(fields[4]) 273 | new_row.append(str(fields[0])) 274 | new_row.append(str((Decimal(fields[1]) * factor) 275 | .quantize(Instrument.TWOPLACES))) 276 | new_row.append(str((Decimal(fields[2]) * factor) 277 | .quantize(Instrument.TWOPLACES))) 278 | new_row.append(str((Decimal(fields[3]) * factor) 279 | .quantize(Instrument.TWOPLACES))) 280 | new_row.append(str((Decimal(fields[4]) * factor) 281 | .quantize(Instrument.TWOPLACES))) 282 | new_row.append(str(int(fields[6]))) 283 | write_row = ','.join(new_row) + "\n" 284 | csvfile.write(write_row) 285 | return True 286 | 287 | 288 | class GoogleSecurity(Instrument): 289 | """Security instrument that uses Yahoo as the datasource.""" 290 | 291 | def __init__(self, symbol, force_download=False, force_cache=False, 292 | period=10, debug=False, data_directory='~/.pypf/data'): 293 | """Initialize the security.""" 294 | super().__init__(symbol, force_download, force_cache, 295 | period, debug, data_directory) 296 | 297 | self.data_file = (self.symbol 298 | + '_google' 299 | + '.csv') 300 | 301 | def _download_data(self): 302 | # Download data from Google and transform into Yahoo format 303 | api_url = ('http://www.google.com/finance/historical?') 304 | params = { 305 | 'q': self.symbol, 306 | 'startdate': 307 | datetime.datetime 308 | .utcfromtimestamp(self._start_date).strftime('%b %d, %Y'), 309 | 'enddate': 310 | datetime.datetime 311 | .utcfromtimestamp(self._end_date).strftime('%b %d, %Y'), 312 | 'output': "csv" 313 | } 314 | url = api_url + urllib.parse.urlencode(params) 315 | 316 | self._log.debug(url) 317 | data = requests.get(url) 318 | content = StringIO(data.content.decode("utf-8")) 319 | lines = content.readlines() 320 | 321 | # Google data is in the opposite order of what we want 322 | # Once reversed we can pop off the header row 323 | lines.reverse() 324 | lines.pop() 325 | 326 | self._log.info('saving data to ' + self.data_path) 327 | with open(self.data_path, 'w', newline='') as csvfile: 328 | csvfile.write("Date,Open,High,Low,Close,Volume\n") 329 | for row in lines: 330 | fields = row.split(',') 331 | 332 | # Format the date 333 | fields[0] = (datetime.datetime 334 | .strptime(fields[0], '%d-%b-%y') 335 | .strftime('%Y-%m-%d')) 336 | 337 | # Check for missing data 338 | missing_data = False 339 | for field in fields: 340 | if field == '-': 341 | missing_data = True 342 | break 343 | 344 | if missing_data is True: 345 | self._log.warning('Missing data on ' + fields[0]) 346 | # Skip the day 347 | continue 348 | else: 349 | new_row = ','.join(fields) 350 | csvfile.write(new_row) 351 | return True 352 | -------------------------------------------------------------------------------- /pypf/terminal_format.py: -------------------------------------------------------------------------------- 1 | """Classes to simplify formatting terminal output.""" 2 | 3 | 4 | class Colors(object): 5 | """Define colors.""" 6 | 7 | BLACK = '\033[90m' 8 | RED = '\033[91m' 9 | GREEN = '\033[92m' 10 | YELLOW = '\033[93m' 11 | BLUE = '\033[94m' 12 | MAGENTA = '\033[95m' 13 | CYAN = '\033[96m' 14 | WHITE = '\033[97m' 15 | ENDC = '\033[0m' 16 | 17 | 18 | class Styles(object): 19 | """Define styles.""" 20 | 21 | BOLD = '\033[1m' 22 | UNDERLINE = '\033[4m' 23 | ENDC = '\033[0m' 24 | 25 | 26 | def black(message): 27 | """Set color to black.""" 28 | return Colors.BLACK + str(message) + Colors.ENDC 29 | 30 | 31 | def red(message): 32 | """Set color to red.""" 33 | return Colors.RED + str(message) + Colors.ENDC 34 | 35 | 36 | def green(message): 37 | """Set color to green.""" 38 | return Colors.GREEN + str(message) + Colors.ENDC 39 | 40 | 41 | def yellow(message): 42 | """Set color to yellow.""" 43 | return Colors.YELLOW + str(message) + Colors.ENDC 44 | 45 | 46 | def blue(message): 47 | """Set color to blue.""" 48 | return Colors.BLUE + str(message) + Colors.ENDC 49 | 50 | 51 | def magenta(message): 52 | """Set color to magenta.""" 53 | return Colors.MAGENTA + str(message) + Colors.ENDC 54 | 55 | 56 | def cyan(message): 57 | """Set color to cyan.""" 58 | return Colors.CYAN + str(message) + Colors.ENDC 59 | 60 | 61 | def white(message): 62 | """Set color to white.""" 63 | return Colors.WHITE + str(message) + Colors.ENDC 64 | 65 | 66 | def bold(message): 67 | """Set style to bold.""" 68 | return Styles.BOLD + str(message) + Styles.ENDC 69 | 70 | 71 | def underline(message): 72 | """Set style to underline.""" 73 | return Styles.UNDERLINE + str(message) + Styles.ENDC 74 | -------------------------------------------------------------------------------- /pypf/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Module initialization.""" 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup file to work with pypi and pip.""" 2 | from setuptools import setup 3 | 4 | 5 | with open('LICENSE.txt') as f: 6 | license = f.read() 7 | 8 | with open('README.rst') as f: 9 | long_description = f.read() 10 | 11 | 12 | setup(name='pypf', 13 | version='0.9.6', 14 | description='Create point and figure charts', 15 | long_description=long_description, 16 | classifiers=[ 17 | 'Development Status :: 4 - Beta', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Programming Language :: Python :: 3.6', 20 | 'Topic :: Office/Business :: Financial :: Investment', 21 | ], 22 | keywords='point figure stock chart', 23 | url='http://github.com/pviglucci/pypf', 24 | author='Peter Viglucci', 25 | author_email='pviglucci@gmail.com', 26 | license='MIT License', 27 | packages=['pypf'], 28 | install_requires=['requests', ], 29 | scripts=['pf.py'], 30 | include_package_data=True, 31 | zip_safe=False) 32 | --------------------------------------------------------------------------------