├── .gitignore ├── pytradelib ├── __init__.py ├── data.py ├── downloader.py ├── hash.py ├── logger.py ├── metrics.py ├── quandl │ ├── __init__.py │ ├── metadata.py │ └── wiki.py ├── settings.py ├── store.py ├── utils.py └── yahoo │ ├── __init__.py │ └── yql.py ├── setup.py ├── techanjs.html └── trade.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | *.sqlite3 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | bin/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # Installer logs 27 | pip-log.txt 28 | pip-delete-this-directory.txt 29 | 30 | # Unit test / coverage reports 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | .idea 45 | 46 | # Rope 47 | .ropeproject 48 | 49 | # Django stuff: 50 | *.log 51 | *.pot 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | .coverage 57 | htmlcov 58 | -------------------------------------------------------------------------------- /pytradelib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancappello/PyTradeLib/1fee936829885fa64fb9cfece8754dda7f260163/pytradelib/__init__.py -------------------------------------------------------------------------------- /pytradelib/data.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import pytz 4 | import pandas as pd 5 | from pandas.tseries.offsets import DateOffset 6 | from datetime import date, datetime 7 | from collections import defaultdict 8 | 9 | from pytradelib.store import CSVStore 10 | from pytradelib.quandl.wiki import QuandlDailyWikiProvider 11 | from pytradelib.quandl.metadata import get_symbols_list 12 | from pytradelib.yahoo.yql import get_symbols_info 13 | from pytradelib.settings import DATA_DIR 14 | from pytradelib.logger import logger 15 | 16 | 17 | class DataManager(object): 18 | def __init__(self, store=None, data_provider=None): 19 | self._store = store or CSVStore() 20 | self._provider = data_provider or QuandlDailyWikiProvider(batch_size=30) 21 | 22 | def initialize_store(self): 23 | symbols = get_symbols_list('WIKI') 24 | info = get_symbols_info(symbols) 25 | 26 | # HACK: find the most recent valid trade date by searching through 27 | # all of the last_trade_dates for the most frequent date 28 | dates = defaultdict(lambda: 0) 29 | for d in info: 30 | dates[d['last_trade_date']] += 1 31 | items = dates.items() 32 | items.sort(key=lambda x: x[1]) 33 | last_valid_trade_date = items[-1][0] 34 | logger.debug('last_valid_trade_date: %s' % last_valid_trade_date) 35 | 36 | # filter out all symbols which are no longer active 37 | symbols = [d['symbol'] for d in info 38 | if d['last_trade_date'] == last_valid_trade_date] 39 | logger.debug('expecting %d symbols' % len(symbols)) 40 | 41 | # download daily data 42 | self._store.set_dfs(self._provider.download(symbols)) 43 | 44 | def update_store(self): 45 | last_trading_day = pd.Timestamp(date.today(), tz=pytz.UTC) 46 | while last_trading_day.weekday() > 4: 47 | last_trading_day = last_trading_day - DateOffset(days=1) 48 | 49 | symbols = {} 50 | for symbol in self._store.symbols: 51 | latest_dt = self._store.get_end_date(symbol) 52 | if latest_dt != last_trading_day: 53 | symbols[symbol] = {'start': latest_dt, 'end': last_trading_day} 54 | 55 | if not symbols: 56 | return [] 57 | 58 | self._store.set_dfs(self._provider.download(symbols)) 59 | return symbols.keys() 60 | 61 | def analyze(self): 62 | results = self._store.analyze() 63 | filename = '%s-analysis.csv' % datetime.now().strftime('%Y-%m-%d') 64 | filepath = os.path.join(DATA_DIR, filename) 65 | results.to_csv(filepath) 66 | return results 67 | 68 | 69 | if __name__ == '__main__': 70 | data_manager = DataManager(CSVStore(), QuandlDailyWikiProvider()) 71 | # data_manager.update_store() 72 | data_manager.analyze() 73 | -------------------------------------------------------------------------------- /pytradelib/downloader.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from aiohttp import ClientSession, ClientResponse, ClientResponseError 4 | 5 | from .utils import chunk 6 | 7 | 8 | async def bulk_download(urls, handle_resp, batch_size=8): 9 | if not isinstance(urls, (list, tuple)): 10 | urls = [urls] 11 | 12 | async def dl(session, url): 13 | async with session.get(url) as r: 14 | data, error = await handle_resp(r) 15 | if error: 16 | return url, error 17 | return url, data 18 | 19 | async def dl_all(): 20 | results = [] 21 | async with ClientSession() as session: 22 | for batch in chunk(urls, batch_size): 23 | tasks = [dl(session, url) for url in batch] 24 | batch_results = await asyncio.gather(*tasks, return_exceptions=False) 25 | results.extend(batch_results) 26 | return results 27 | 28 | return await dl_all() 29 | -------------------------------------------------------------------------------- /pytradelib/hash.py: -------------------------------------------------------------------------------- 1 | 2 | class Hash(object): 3 | def __init__(self, **kwargs): 4 | self.__dict__.update(kwargs) 5 | 6 | def __str__(self): 7 | return str(self.__dict__) 8 | 9 | def __repr__(self): 10 | return self.__str__() 11 | 12 | def as_dict(self): 13 | return self.__dict__ 14 | 15 | def __contains__(self, item): 16 | return item in self.__dict__ 17 | -------------------------------------------------------------------------------- /pytradelib/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging.handlers import TimedRotatingFileHandler 3 | 4 | from pytradelib.settings import LOG_LEVEL, LOG_FILENAME 5 | 6 | LEVELS = { 7 | 'debug': logging.DEBUG, 8 | 'info': logging.INFO, 9 | 'warning': logging.WARNING, 10 | 'error': logging.ERROR, 11 | 'critical': logging.CRITICAL, 12 | } 13 | 14 | logger = logging.getLogger('PyTradeLib') 15 | logger.setLevel(LEVELS.get(LOG_LEVEL, logging.WARNING)) 16 | handler = TimedRotatingFileHandler(LOG_FILENAME, 'midnight') 17 | handler.setFormatter(logging.Formatter( 18 | '%(asctime)s %(levelname)s: pytradelib.%(module)s L%(lineno)s: %(message)s', 19 | '%Y-%m-%d %H:%M:%S' 20 | )) 21 | logger.addHandler(handler) 22 | 23 | 24 | __ALL__ = ('logger',) 25 | -------------------------------------------------------------------------------- /pytradelib/metrics.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import talib as ta 3 | 4 | from scipy import stats 5 | 6 | 7 | def calc_metrics(df, symbol): 8 | sma200 = ta.SMA(df.Close, timeperiod=200) 9 | slope200, *_ = stats.linregress(range(0, 10), sma200[-10:]) 10 | rsi14 = ta.RSI(df.Close, timeperiod=14) 11 | new_high_over_num_bars = new_high_over_num_prior_bars(df) 12 | expanding_bodies, expanding_volume = expanding_bodies_and_volume(df) 13 | close_crossed_200 = price_crossed_sma(df) 14 | return pd.DataFrame.from_records([dict( 15 | Symbol=symbol, 16 | SMA200=sma200[-1], 17 | Slope200=slope200, 18 | RSI=rsi14[-1], 19 | NewHighOverNumBars=new_high_over_num_bars, 20 | ExpandingBodies=expanding_bodies, 21 | ExpandingVolume=expanding_volume, 22 | CloseCrossed200=close_crossed_200, 23 | )], index='Symbol') 24 | 25 | 26 | """ 27 | metrics df: 28 | symbol 52w_high 52w_low 100sma(D/W/M) 200sma(D/W/M) 3month_vol rsi 29 | 30 | latest bar df: 31 | symbol open high low close volume 32 | """ 33 | 34 | 35 | def new_high_over_num_prior_bars(df: pd.DataFrame, col: str = "Close"): 36 | latest_val = df[col].iloc[-1] 37 | prev_val = df[col].iloc[-2] 38 | if latest_val < prev_val: 39 | return 0 40 | 41 | prior_high_timestamps = df.index[df[col] > latest_val] 42 | if prior_high_timestamps.empty: 43 | return len(df) 44 | 45 | latest_ts = df.index[-1] 46 | prior_high_ts = prior_high_timestamps[-1] 47 | num_bars = df.index.get_loc(latest_ts) - df.index.get_loc(prior_high_ts) 48 | return num_bars - 1 # subtract 1 to not count the latest/current bar 49 | 50 | 51 | def expanding_bodies_and_volume(df: pd.DataFrame, num_bars: int = 3, bullish: bool = True): 52 | df = df[-num_bars:] 53 | bodies = df.Close - df.Open 54 | bodies_expanding = (bodies.sort_values(ascending=bullish).index == bodies.index).all() 55 | volume_increasing = (df.sort_values('Volume').index == df.index).all() 56 | return bodies_expanding, volume_increasing 57 | 58 | 59 | def price_crossed_sma(df: pd.DataFrame, sma: int = 200, bullish: bool = True): 60 | """ 61 | Whether or not the latest bar has crossed the given SMA. 62 | """ 63 | if len(df) < sma: 64 | return False 65 | 66 | ma = ta.SMA(df.Close[-sma:], timeperiod=sma)[-1] 67 | open_ = df.Open.iloc[-1] 68 | close = df.Close.iloc[-1] 69 | prior_close = df.Close.iloc[-2] 70 | if bullish: 71 | intraday_cross = (open_ < ma) and (close > ma) 72 | gap_cross = (prior_close < ma) and (open_ > ma) 73 | else: 74 | intraday_cross = (open_ > ma) and (close < ma) 75 | gap_cross = (prior_close > ma) and (open_ < ma) 76 | return intraday_cross or gap_cross 77 | -------------------------------------------------------------------------------- /pytradelib/quandl/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancappello/PyTradeLib/1fee936829885fa64fb9cfece8754dda7f260163/pytradelib/quandl/__init__.py -------------------------------------------------------------------------------- /pytradelib/quandl/metadata.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import requests 3 | import pandas as pd 4 | from zipfile import ZipFile 5 | from io import StringIO 6 | 7 | 8 | URL = 'https://www.quandl.com/api/v3/databases/%(dataset)s/codes' 9 | 10 | 11 | def dataset_url(dataset): 12 | return URL % {'dataset': dataset} 13 | 14 | 15 | def download_file(url): 16 | r = requests.get(url) 17 | if r.status_code == 200: 18 | return StringIO(r.text) 19 | 20 | 21 | def unzip(file_): 22 | d = unzip_files(file_) 23 | return d[list(d.keys())[0]] 24 | 25 | 26 | def unzip_files(file_): 27 | d = {} 28 | with ZipFile(file_, 'r') as zipfile: 29 | for filename in zipfile.namelist(): 30 | d[filename] = str(zipfile.read(filename)) 31 | return d 32 | 33 | 34 | def csv_rows(str): 35 | for row in csv.reader(StringIO(str)): 36 | yield row 37 | 38 | 39 | def csv_dicts(str, fieldnames=None): 40 | for d in csv.DictReader(StringIO(str), fieldnames=fieldnames): 41 | yield d 42 | 43 | 44 | def get_symbols_list(dataset): 45 | csv_ = unzip(download_file(dataset_url(dataset))) 46 | return map(lambda x: x[0].replace(dataset + '/', ''), csv_rows(csv_)) 47 | 48 | 49 | def get_symbols_dict(dataset): 50 | csv_ = unzip(download_file(dataset_url(dataset))) 51 | return dict(csv_rows(csv_)) 52 | 53 | 54 | def get_symbols_df(dataset): 55 | csv_ = unzip(download_file(dataset_url(dataset))) 56 | df = pd.read_csv(StringIO(csv_), header=None, names=['symbol', 'company']) 57 | df.symbol = df.symbols.map(lambda x: x.replace(dataset + '/', '')) 58 | df.company = df.company.map(lambda x: x.replace('Prices, Dividends, Splits and Trading Volume', '')) 59 | return df 60 | -------------------------------------------------------------------------------- /pytradelib/quandl/wiki.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from pandas.io.common import urlencode as _encode_url 5 | 6 | from pytradelib.downloader import Downloader 7 | from pytradelib.utils import _sanitize_dates, csv_to_df 8 | 9 | 10 | class QuandlDailyWikiProvider(object): 11 | def __init__(self, api_key=None, batch_size=20, sleep=20): 12 | self._api_key = api_key 13 | self._downloader = Downloader(batch_size=batch_size, sleep=sleep) 14 | 15 | @property 16 | def api_key(self): 17 | return self._api_key 18 | 19 | @api_key.setter 20 | def api_key(self, api_key): 21 | self._api_key = api_key 22 | 23 | def download(self, symbols, start=None, end=None): 24 | if isinstance(symbols, str): 25 | url = self._construct_url(symbols, start, end) 26 | csv = self._downloader.download(url) 27 | return csv_to_df(csv) 28 | elif isinstance(symbols, (list, tuple)): 29 | urls = [self._construct_url(symbol, start, end) 30 | for symbol in symbols] 31 | elif isinstance(symbols, dict): 32 | urls = [self._construct_url(symbol, d['start'], d['end']) 33 | for symbol, d in symbols.items()] 34 | else: 35 | raise Exception('symbols must be a string, a list of strings, or a dict of string to start/end dates') 36 | 37 | results = {} 38 | for url, csv in self._downloader.download(urls): 39 | symbol, df = self._url_to_symbol(url), csv_to_df(csv) 40 | results[symbol] = df 41 | print('parsed results for ' + symbol) 42 | return results 43 | 44 | def _construct_url(self, symbol, start=None, end=None): 45 | """ 46 | Get historical data for the given name from quandl. 47 | Date format is datetime 48 | Returns a DataFrame. 49 | """ 50 | start, end = _sanitize_dates(start, end) 51 | 52 | # if no specific dataset was provided, default to free WIKI dataset 53 | if '/' not in symbol: 54 | symbol = 'WIKI/' + symbol 55 | 56 | url = 'https://www.quandl.com/api/v3/datasets/%s.csv?' % symbol 57 | 58 | query_params = {'start_date': start.strftime('%Y-%m-%d'), 59 | 'end_date': end.strftime('%Y-%m-%d'), 60 | 'collapse': 'daily'} 61 | 62 | if self._api_key or 'QUANDL_API_KEY' in os.environ: 63 | query_params['api_key'] = self._api_key or os.environ['QUANDL_API_KEY'] 64 | else: 65 | print('Please provide your API key in the constructor, or set the QUANDL_API_KEY environment variable') 66 | sys.exit(1) 67 | 68 | return url + _encode_url(query_params) 69 | 70 | def _url_to_symbol(self, url): 71 | return url[url.rfind('/')+1:url.rfind('.csv')] 72 | -------------------------------------------------------------------------------- /pytradelib/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # the commission cost per trade 4 | COMMISSION = 10 5 | 6 | # the default maximum dollar amount to allocate for a trade 7 | MAX_AMOUNT = 5000 8 | 9 | # the number of steps for scaling out of a position 10 | SCALE_OUT_LEVELS = 2 # sell half of quantity half way to the target price and the other half at the target price 11 | 12 | 13 | ################################################################### 14 | DATA_DIR = os.path.join(os.environ['HOME'], '.pytradelib') 15 | ZIPLINE_DIR = os.path.join(os.environ['HOME'], '.zipline') 16 | ZIPLINE_CACHE_DIR = os.path.join(ZIPLINE_DIR, 'cache') 17 | 18 | LOG_DIR = os.path.join(DATA_DIR, 'logs') 19 | LOG_FILENAME = os.path.join(LOG_DIR, 'pytradelib.log') 20 | LOG_LEVEL = 'info' # debug, info, warning, error or critical 21 | 22 | 23 | if not os.path.exists(LOG_DIR): 24 | os.makedirs(LOG_DIR) 25 | 26 | if not os.path.exists(ZIPLINE_CACHE_DIR): 27 | os.makedirs(ZIPLINE_CACHE_DIR) 28 | -------------------------------------------------------------------------------- /pytradelib/store.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytz 3 | import talib as ta 4 | import pandas as pd 5 | from pandas.tseries.offsets import DateOffset 6 | 7 | from pytradelib.settings import DATA_DIR, ZIPLINE_CACHE_DIR 8 | from pytradelib.utils import ( 9 | percent_change, 10 | within_percent_of_value, 11 | crossed, 12 | ) 13 | 14 | 15 | class CSVStore(object): 16 | _symbols = [] 17 | _csv_files = {} 18 | _start_dates = {} 19 | _end_dates = {} 20 | _df_cache = {} 21 | 22 | def __init__(self): 23 | self._store_contents = self._get_store_contents() 24 | for d in self._store_contents: 25 | symbol = d['symbol'] 26 | self._symbols.append(symbol) 27 | self._csv_files[symbol] = d['csv_path'] 28 | self._start_dates[symbol] = d['start'] 29 | self._end_dates[symbol] = d['end'] 30 | self._symbols.sort() 31 | 32 | @property 33 | def symbols(self): 34 | return self._symbols 35 | 36 | def get_df(self, symbol, start=None, end=None): 37 | symbol = symbol.upper() 38 | 39 | def to_timestamp(dt, default): 40 | if dt: 41 | return pd.Timestamp(dt, tz=pytz.UTC) 42 | return default 43 | start = to_timestamp(start, 0) 44 | end = to_timestamp(end, None) 45 | 46 | df = self._df_cache.get(symbol, None) 47 | if df is None: 48 | csv_path = self._csv_files[symbol] 49 | df = pd.DataFrame.from_csv(csv_path).tz_localize(pytz.UTC) 50 | self._df_cache[symbol] = df 51 | return df[start:end] 52 | 53 | def set_df(self, symbol, df): 54 | self._update_df(symbol, df) 55 | 56 | def _update_df(self, symbol, df): 57 | symbol = symbol.upper() 58 | if symbol in self.symbols: 59 | existing_df = self.get_df(symbol) 60 | df = existing_df.append(df[existing_df.index[-1] + DateOffset(days=1):]) 61 | os.remove(self.get_csv_path(symbol)) 62 | df = self._add_ta(df) 63 | csv_path = self._get_store_path(symbol, df.index[0], df.index[-1]) 64 | df.to_csv(csv_path) 65 | self._set_csv_path(symbol, csv_path) 66 | self._set_end_date(symbol, df.index[-1]) 67 | self._df_cache[symbol] = df 68 | 69 | def get_dfs(self, symbols=None, start=None, end=None): 70 | symbols = symbols or self.symbols 71 | if not isinstance(symbols, list): 72 | symbols = [symbols] 73 | return dict(zip( 74 | [symbol.upper() for symbol in symbols], 75 | [self.get_df(symbol, start, end) for symbol in symbols] 76 | )) 77 | 78 | def set_dfs(self, symbol_df_dict): 79 | for symbol, df in symbol_df_dict.items(): 80 | self.set_df(symbol, df) 81 | 82 | def analyze(self, symbols=None, use_adjusted=True): 83 | ''' 84 | :param symbols: list of symbols (defaults to all in the store) 85 | :param use_adjusted: whether or not to use adjusted prices 86 | :return: DataFrame 87 | ''' 88 | def key(price_key): 89 | return 'Adj ' + price_key if use_adjusted else price_key 90 | 91 | results = {} 92 | for symbol, df in self.get_dfs(symbols).items(): 93 | today = df.iloc[-1] 94 | yesterday = df.iloc[-2] 95 | most_recent_year = df[df.index[-1] - DateOffset(years=1):] 96 | year_low = most_recent_year[key('Low')][:-1].min() 97 | year_high = most_recent_year[key('High')][:-1].max() 98 | dollar_volume_desc = most_recent_year.dollar_volume.describe() 99 | 100 | results[symbol] = { 101 | # last-bar metrics 102 | 'previous_close': yesterday[key('Close')], 103 | 'close': today[key('Close')], 104 | 'percent_change': percent_change(yesterday[key('Close')], today[key('Close')]), 105 | 'dollar_volume': today.dollar_volume, 106 | 'percent_of_median_volume': (today.Volume * today[key('Close')]) / dollar_volume_desc['50%'], 107 | 'advancing': today[key('Close')] > yesterday[key('Close')], 108 | 'declining': today[key('Close')] < yesterday[key('Close')], 109 | 'new_low': today[key('Close')] < year_low, 110 | 'new_high': today[key('Close')] > year_high, 111 | 'percent_of_high': today[key('Close')] / year_high, 112 | 'near_sma100': within_percent_of_value(today[key('Close')], today['sma100'], 2), 113 | 'near_sma200': within_percent_of_value(today[key('Close')], today['sma200'], 2), 114 | 'crossed_sma100': crossed(today['sma100'], yesterday, today), 115 | 'crossed_sma200': crossed(today['sma200'], yesterday, today), 116 | 'crossed_5': crossed(5, yesterday, today), 117 | 'crossed_10': crossed(10, yesterday, today), 118 | 'crossed_50': crossed(50, yesterday, today), 119 | 'crossed_100': crossed(100, yesterday, today), 120 | 121 | # stock "health" metrics 122 | 'year_high': year_high, 123 | 'year_low': year_low, 124 | 'min_volume': int(dollar_volume_desc['min']), 125 | 'dollar_volume_25th_percentile': int(dollar_volume_desc['25%']), 126 | 'dollar_volume_75th_percentile': int(dollar_volume_desc['75%']), 127 | 'atr': most_recent_year.atr.describe()['50%'], 128 | } 129 | # transpose the DataFrame so we have rows of symbols, and metrics as columns 130 | return pd.DataFrame(results).T 131 | 132 | def get_start_date(self, symbol): 133 | return self._start_dates[symbol.upper()] 134 | 135 | def _set_start_date(self, symbol, start_date): 136 | self._start_dates[symbol] = start_date 137 | 138 | def get_end_date(self, symbol): 139 | return self._end_dates[symbol.upper()] 140 | 141 | def _set_end_date(self, symbol, end_date): 142 | self._end_dates[symbol] = end_date 143 | 144 | def get_csv_path(self, symbol): 145 | return self._csv_files[symbol] 146 | 147 | def _set_csv_path(self, symbol, csv_path): 148 | self._csv_files[symbol] = csv_path 149 | 150 | def _get_store_contents(self): 151 | csv_files = [os.path.join(ZIPLINE_CACHE_DIR, f)\ 152 | for f in os.listdir(ZIPLINE_CACHE_DIR)\ 153 | if f.endswith('.csv')] 154 | return [self._decode_store_path(path) for path in csv_files] 155 | 156 | def _decode_store_path(self, csv_path): 157 | filename = os.path.basename(csv_path).replace('--', os.path.sep) 158 | symbol = filename[:filename.find('-')] 159 | 160 | dates = filename[len(symbol)+1:].replace('.csv', '') 161 | start = dates[:len(dates)/2] 162 | end = dates[len(dates)/2:] 163 | 164 | def to_dt(dt_str): 165 | date, time = dt_str.split(' ') 166 | return pd.Timestamp(' '.join([date, time.replace('-', ':')])) 167 | 168 | return { 169 | 'symbol': symbol, 170 | 'csv_path': csv_path, 171 | 'start': to_dt(start), 172 | 'end': to_dt(end), 173 | } 174 | 175 | def _get_store_path(self, symbol, start, end): 176 | ''' 177 | :param symbol: string - the ticker 178 | :param start: pd.Timestamp - the earliest date 179 | :param end: pd.Timestamp - the latest date 180 | :return: string - path for the CSV 181 | ''' 182 | filename_format = '%(symbol)s-%(start)s-%(end)s.csv' 183 | return os.path.join(ZIPLINE_CACHE_DIR, filename_format % { 184 | 'symbol': symbol.upper().replace(os.path.sep, '--'), 185 | 'start': start, 186 | 'end': end, 187 | }).replace(':', '-') 188 | 189 | def _add_ta(self, df, use_adjusted=True): 190 | def key(price_key): 191 | return 'Adj ' + price_key if use_adjusted else price_key 192 | df['dollar_volume'] = df[key('Close')] * df.Volume 193 | df['atr'] = ta.NATR(df[key('High')].values, df[key('Low')].values, df[key('Close')].values) # timeperiod 14 194 | df['sma100'] = ta.SMA(df[key('Close')].values, timeperiod=100) 195 | df['sma200'] = ta.SMA(df[key('Close')].values, timeperiod=200) 196 | df['bbands_upper'], df['sma20'], df['bbands_lower'] = ta.BBANDS(df[key('Close')].values, timeperiod=20) 197 | df['macd_lead'], df['macd_lag'], df['macd_divergence'] = ta.MACD(df[key('Close')].values) # 12, 26, 9 198 | df['rsi'] = ta.RSI(df[key('Close')].values) # 14 199 | df['stoch_lead'], df['stoch_lag'] = ta.STOCH(df[key('High')].values, df[key('Low')].values, df[key('Close')].values, 200 | fastk_period=14, slowk_period=1, slowd_period=3) 201 | df['slope'] = ta.LINEARREG_SLOPE(df[key('Close')].values) * -1 # talib returns the inverse of what we want 202 | return df 203 | -------------------------------------------------------------------------------- /pytradelib/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | 3 | 4 | def utcnow(): 5 | return datetime.utcnow().replace(tzinfo=timezone.utc) 6 | 7 | 8 | def chunk(string, size): 9 | for i in range(0, len(string), size): 10 | yield string[i:i+size] 11 | 12 | 13 | def percent_change(from_val, to_val): 14 | # coerce to float to ensure non-integer division 15 | return (float(to_val - from_val) / from_val) * 100 16 | 17 | 18 | def within_percent_of_value(price, value, percent=1): 19 | diff = percent * 0.01 * 0.5 * value 20 | return (value - diff) < price < (value + diff) 21 | -------------------------------------------------------------------------------- /pytradelib/yahoo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancappello/PyTradeLib/1fee936829885fa64fb9cfece8754dda7f260163/pytradelib/yahoo/__init__.py -------------------------------------------------------------------------------- /pytradelib/yahoo/yql.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from pytradelib.utils import batch 4 | from pytradelib.downloader import Downloader 5 | 6 | from urllib import urlencode 7 | try: 8 | import simplejson as json 9 | except ImportError: 10 | import json 11 | 12 | 13 | def get_yql_url(yql): 14 | base_url = 'http://query.yahooapis.com/v1/public/yql?' 15 | url = base_url + urlencode({'q': yql, 16 | 'env': 'store://datatables.org/alltableswithkeys', 17 | 'format': 'json'}) 18 | if len(url) > (2 * 1024): 19 | raise Exception('URL must be shorter than 2048 characters') 20 | return url 21 | 22 | 23 | _EXCHANGES = { 24 | 'ASE': 'AMEX', 25 | 'NYQ': 'NYSE', 26 | 'NMS': 'NASDAQ', # 'NasdaqGS', 27 | 'NGM': 'NASDAQ', # 'NasdaqGM', 28 | 'NCM': 'NASDAQ', # 'NasdaqCM', 29 | } 30 | 31 | 32 | def _convert_result(result): 33 | """ 34 | Converts a YQL result dictionary into PyTradeLib format 35 | :param result: dict 36 | :return: dict with keys lowercased 37 | """ 38 | r = {} 39 | for k, v in result.items(): 40 | if k == 'StockExchange': 41 | k = 'exchange' 42 | v = _EXCHANGES.get(v, None) 43 | if k == 'ErrorIndicationreturnedforsymbolchangedinvalid': 44 | k = 'error' 45 | if k == 'LastTradeDate': 46 | k = 'last_trade_date' 47 | r[k.lower()] = v 48 | return r 49 | 50 | 51 | def get_symbols_info(symbols, keys=None): 52 | if not isinstance(symbols, (list, tuple)): 53 | symbols = list(symbols) 54 | keys = keys or ['Symbol', 'Name', 'StockExchange', 'LastTradeDate'] 55 | yql = 'select %(keys)s from yahoo.finance.quotes where symbol in (%(symbols)s)' 56 | 57 | urls = [] 58 | for batched_symbols in batch(symbols, 100): 59 | csv_symbols = ','.join(['"%s"' % s.upper() for s in batched_symbols]) 60 | urls.append(get_yql_url(yql % {'keys': ','.join(keys), 61 | 'symbols': csv_symbols})) 62 | downloader = Downloader() 63 | 64 | results = [] 65 | for url, text in downloader.download(urls): 66 | json_ = json.loads(text) 67 | for result in json_['query']['results']['quote']: 68 | results.append(_convert_result(result)) 69 | return results 70 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | setup( 5 | name='pytradelib', 6 | version='0.0.1', 7 | license='Apache-2.0', 8 | author='Brian Cappello', 9 | author_email='briancappello@gmail.com', 10 | install_requires=[ 11 | 'aiohttp', 12 | 'pandas', 13 | 'pytz', 14 | 'requests', 15 | 'scipy', 16 | 'ta-lib', 17 | ], 18 | packages=find_packages(exclude=['docs', 'test']), 19 | include_package_data=True, 20 | zip_safe=False, 21 | ) 22 | -------------------------------------------------------------------------------- /techanjs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 173 |
174 | 175 | 176 | 177 | 569 | -------------------------------------------------------------------------------- /trade.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | 4 | from colorclass import Color 5 | from terminaltables import AsciiTable 6 | 7 | from pytradelib.hash import Hash 8 | from pytradelib.settings import COMMISSION, MAX_AMOUNT 9 | 10 | 11 | def error(): 12 | print('Command should be in the format of:\n