From e0fc69a16a7e60dcc615d296bdba9a2b154856cd Mon Sep 17 00:00:00 2001 From: Alex McFarlane Date: Tue, 5 Sep 2017 18:35:22 +0100 Subject: [PATCH 1/8] added generic input support for Pandas DataFrames and List Structures --- pyalgotrade/barfeed/customfeed.py | 241 ++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 pyalgotrade/barfeed/customfeed.py diff --git a/pyalgotrade/barfeed/customfeed.py b/pyalgotrade/barfeed/customfeed.py new file mode 100644 index 000000000..d2e74fe22 --- /dev/null +++ b/pyalgotrade/barfeed/customfeed.py @@ -0,0 +1,241 @@ +# PyAlgoTrade +# +# Copyright 2011-2015 Gabriel Martin Becedillas Ruiz +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +.. moduleauthor:: Alex McFarlane +""" + +from pyalgotrade.utils import dt +from pyalgotrade.barfeed import membf +from pyalgotrade.barfeed import csvfeed +from pyalgotrade import bar + +import datetime +import pytz + + +# Interface for csv row parsers. +class RowParser(object): + def parseBar(self, csvRowDict): + raise NotImplementedError() + + def getFieldNames(self): + raise NotImplementedError() + + def getDelimiter(self): + raise NotImplementedError() + + +# Interface for bar filters. +class BarFilter(object): + def includeBar(self, bar_): + raise NotImplementedError() + + +class BarFeed(membf.BarFeed): + """Base class for CSV file based :class:`pyalgotrade.barfeed.BarFeed`. + + .. note:: + This is a base class and should not be used directly. + """ + + def __init__(self, frequency, maxLen=None): + super(BarFeed, self).__init__(frequency, maxLen) + + self.__barFilter = None + self.__dailyTime = datetime.time(0, 0, 0) + + def getDailyBarTime(self): + return self.__dailyTime + + def setDailyBarTime(self, time): + self.__dailyTime = time + + def getBarFilter(self): + return self.__barFilter + + def setBarFilter(self, barFilter): + self.__barFilter = barFilter + + def _addBarsFromListofDicts(self, instrument, iterable, rowParser): + loadedBars = map(rowParser.parseBar, iterable) + loadedBars = filter( + lambda bar_: (bar_ is not None) and + (self.__barFilter is None or self.__barFilter.includeBar(bar_)), + loadedBars + ) + self.addBarsFromSequence(instrument, loadedBars) + + def _addBarsFromDataFrame(self, instrument, df, rowParser): + # Load the DataFrame + # replicate FastDictReader & reduce to required columns + list_of_dicts = df.fillna('').astype(str).to_dict('records') + self._addBarsFromListofDicts(instrument, list_of_dicts, rowParser) + + +class Feed(BarFeed): + """A BarFeed that loads bars from a custom feed that has the following columns: + :: + + Date Time Open Close High Low Volume Adj Close + 2015-08-14 09:06:00 0.00690 0.00690 0.00690 0.00690 1.346117 9567 + + + :param frequency: The frequency of the bars. Check :class:`pyalgotrade.bar.Frequency`. + :param timezone: The default timezone to use to localize bars. Check :mod:`pyalgotrade.marketsession`. + :type timezone: A pytz timezone. + :param maxLen: The maximum number of values that the :class:`pyalgotrade.dataseries.bards.BarDataSeries` will hold. + Once a bounded length is full, when new items are added, a corresponding number of items are discarded from the + opposite end. If None then dataseries.DEFAULT_MAX_LEN is used. + :type maxLen: int. + + .. note:: + * The data should be sampled across regular time points, you can + regularlise (e.g. for 5min intervals) as:: + + df = df.set_index('Date Time').resample('s').interpolate().resample('5T').asfreq() + df = df.dropna().reset_index() + which is described in a SO [post](https://stackoverflow.com/a/39730730/4013571) + * It is ok if the **Adj Close** column is empty. + * When working with multiple instruments: + + * If all the instruments loaded are in the same timezone, then the timezone parameter may not be specified. + * If any of the instruments loaded are in different timezones, then the timezone parameter should be set. + """ + + def __init__(self, frequency, timezone=None, maxLen=None): + super(Feed, self).__init__(frequency, maxLen) + + self.__timezone = timezone + # Assume bars don't have adjusted close. This will be set to True after + # loading the first file if the adj_close column is there. + self.__haveAdjClose = False + + self.__barClass = bar.BasicBar + + self.__dateTimeFormat = "%Y-%m-%d %H:%M:%S" + self.__columnNames = { + "datetime": "Date Time", + "open": "Open", + "high": "High", + "low": "Low", + "close": "Close", + "volume": "Volume", + "adj_close": "Adj Close", + } + # self.__dateTimeFormat expects time to be set so there is no need to + # fix time. + self.setDailyBarTime(None) + + def barsHaveAdjClose(self): + return self.__haveAdjClose + + def setNoAdjClose(self): + self.__columnNames["adj_close"] = None + self.__haveAdjClose = False + + def setColumnName(self, col, name): + self.__columnNames[col] = name + + def setDateTimeFormat(self, dateTimeFormat): + self.__dateTimeFormat = dateTimeFormat + + def setBarClass(self, barClass): + self.__barClass = barClass + + def addBarsFromDataFrame(self, instrument, df, timezone=None): + """Loads bars for a given instrument from a Pandas DataFrame. + The instrument gets registered in the bar feed. + + :param instrument: Instrument identifier. + :type instrument: string. + :param df: The pandas DataFrame + :type df: pd.DataFrame + :param timezone: The timezone to use to localize bars. Check :mod:`pyalgotrade.marketsession`. + :type timezone: A pytz timezone. + """ + + if timezone is None: + timezone = self.__timezone + + rowParser = csvfeed.GenericRowParser( + self.__columnNames, + self.__dateTimeFormat, + self.getDailyBarTime(), + self.getFrequency(), + timezone, + self.__barClass + ) + + missing_columns = [ + col for col in self.__columnNames.values() + if col not in df.columns + ] + if missing_columns: + raise ValueError('Missing required columns: {}'.format(repr(missing_columns))) + + df = df[self.__columnNames.values()] + super(Feed, self)._addBarsFromDataFrame(instrument, df, rowParser) + + if rowParser.barsHaveAdjClose(): + self.__haveAdjClose = True + elif self.__haveAdjClose: + raise Exception("Previous bars had adjusted close and these ones don't have.") + + def addBarsFromListofDicts(self, instrument, list_of_dicts, timezone=None): + """Loads bars for a given instrument from a list of dictionaries. + The instrument gets registered in the bar feed. + + :param instrument: Instrument identifier. + :type instrument: string. + :param list_of_dicts: A list of dicts. First item should contain + columns. + :type list_of_dicts: list + :param timezone: The timezone to use to localize bars. Check :mod:`pyalgotrade.marketsession`. + :type timezone: A pytz timezone. + """ + + if timezone is None: + timezone = self.__timezones + + if not isinstance(list_of_dicts, (list, tuple)): + raise ValueError('This function only supports types: {list, tuple}') + if not isinstance(list_of_dicts[0], dict): + raise ValueError('List should only contain dicts') + + rowParser = csvfeed.GenericRowParser( + self.__columnNames, + self.__dateTimeFormat, + self.getDailyBarTime(), + self.getFrequency(), + timezone, + self.__barClass + ) + + missing_columns = [ + col for col in self.__columnNames.values() + if col not in list_of_dicts[0].keys() + ] + if missing_columns: + raise ValueError('Missing required columns: {}'.format(repr(missing_columns))) + + super(Feed, self)._addBarsFromListofDicts( + instrument, list_of_dicts, rowParser) + + if rowParser.barsHaveAdjClose(): + self.__haveAdjClose = True + elif self.__haveAdjClose: + raise Exception("Previous bars had adjusted close and these ones don't have.") \ No newline at end of file From cf3f5ad586df0dab7b029d78a4ba058393b6bd54 Mon Sep 17 00:00:00 2001 From: ttymck Date: Tue, 24 Dec 2019 11:09:46 -0500 Subject: [PATCH 2/8] feat(barfeed): separate customfeed into listfeed and pandasfeed - pandas feed no longer circuitously convert to string -> csv parser --- .../barfeed/{customfeed.py => listfeed.py} | 70 +------ pyalgotrade/barfeed/pandasfeed.py | 173 ++++++++++++++++++ 2 files changed, 176 insertions(+), 67 deletions(-) rename pyalgotrade/barfeed/{customfeed.py => listfeed.py} (74%) create mode 100644 pyalgotrade/barfeed/pandasfeed.py diff --git a/pyalgotrade/barfeed/customfeed.py b/pyalgotrade/barfeed/listfeed.py similarity index 74% rename from pyalgotrade/barfeed/customfeed.py rename to pyalgotrade/barfeed/listfeed.py index d2e74fe22..322cd4f84 100644 --- a/pyalgotrade/barfeed/customfeed.py +++ b/pyalgotrade/barfeed/listfeed.py @@ -15,7 +15,7 @@ # limitations under the License. """ -.. moduleauthor:: Alex McFarlane +.. moduleauthor:: Alex McFarlane , Tyler Kontra """ from pyalgotrade.utils import dt @@ -24,29 +24,10 @@ from pyalgotrade import bar import datetime -import pytz - - -# Interface for csv row parsers. -class RowParser(object): - def parseBar(self, csvRowDict): - raise NotImplementedError() - - def getFieldNames(self): - raise NotImplementedError() - - def getDelimiter(self): - raise NotImplementedError() - - -# Interface for bar filters. -class BarFilter(object): - def includeBar(self, bar_): - raise NotImplementedError() class BarFeed(membf.BarFeed): - """Base class for CSV file based :class:`pyalgotrade.barfeed.BarFeed`. + """Base class for Iterable[Dict] based :class:`pyalgotrade.barfeed.BarFeed`. .. note:: This is a base class and should not be used directly. @@ -79,12 +60,6 @@ def _addBarsFromListofDicts(self, instrument, iterable, rowParser): ) self.addBarsFromSequence(instrument, loadedBars) - def _addBarsFromDataFrame(self, instrument, df, rowParser): - # Load the DataFrame - # replicate FastDictReader & reduce to required columns - list_of_dicts = df.fillna('').astype(str).to_dict('records') - self._addBarsFromListofDicts(instrument, list_of_dicts, rowParser) - class Feed(BarFeed): """A BarFeed that loads bars from a custom feed that has the following columns: @@ -155,46 +130,7 @@ def setDateTimeFormat(self, dateTimeFormat): def setBarClass(self, barClass): self.__barClass = barClass - - def addBarsFromDataFrame(self, instrument, df, timezone=None): - """Loads bars for a given instrument from a Pandas DataFrame. - The instrument gets registered in the bar feed. - - :param instrument: Instrument identifier. - :type instrument: string. - :param df: The pandas DataFrame - :type df: pd.DataFrame - :param timezone: The timezone to use to localize bars. Check :mod:`pyalgotrade.marketsession`. - :type timezone: A pytz timezone. - """ - - if timezone is None: - timezone = self.__timezone - - rowParser = csvfeed.GenericRowParser( - self.__columnNames, - self.__dateTimeFormat, - self.getDailyBarTime(), - self.getFrequency(), - timezone, - self.__barClass - ) - - missing_columns = [ - col for col in self.__columnNames.values() - if col not in df.columns - ] - if missing_columns: - raise ValueError('Missing required columns: {}'.format(repr(missing_columns))) - - df = df[self.__columnNames.values()] - super(Feed, self)._addBarsFromDataFrame(instrument, df, rowParser) - - if rowParser.barsHaveAdjClose(): - self.__haveAdjClose = True - elif self.__haveAdjClose: - raise Exception("Previous bars had adjusted close and these ones don't have.") - + def addBarsFromListofDicts(self, instrument, list_of_dicts, timezone=None): """Loads bars for a given instrument from a list of dictionaries. The instrument gets registered in the bar feed. diff --git a/pyalgotrade/barfeed/pandasfeed.py b/pyalgotrade/barfeed/pandasfeed.py new file mode 100644 index 000000000..5246a38f6 --- /dev/null +++ b/pyalgotrade/barfeed/pandasfeed.py @@ -0,0 +1,173 @@ +# PyAlgoTrade +# +# Copyright 2011-2015 Gabriel Martin Becedillas Ruiz +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +.. moduleauthor:: Alex McFarlane , Tyler Kontra +""" + +from pyalgotrade.utils import dt +from pyalgotrade.barfeed import membf +from pyalgotrade.barfeed import csvfeed +from pyalgotrade import bar + +import datetime + + +class BarFeed(membf.BarFeed): + """Base class for Pandas DataFrame based :class:`pyalgotrade.barfeed.BarFeed`. + + .. note:: + This is a base class and should not be used directly. + """ + + def __init__(self, frequency, maxLen=None): + super(BarFeed, self).__init__(frequency, maxLen) + + self.__barFilter = None + self.__dailyTime = datetime.time(0, 0, 0) + + def getDailyBarTime(self): + return self.__dailyTime + + def setDailyBarTime(self, time): + self.__dailyTime = time + + def getBarFilter(self): + return self.__barFilter + + def setBarFilter(self, barFilter): + self.__barFilter = barFilter + + +class Feed(BarFeed): + """A BarFeed that loads bars from a custom feed that has the following columns: + :: + + Date Time Open Close High Low Volume Adj Close + 2015-08-14 09:06:00 0.00690 0.00690 0.00690 0.00690 1.346117 9567 + + + :param frequency: The frequency of the bars. Check :class:`pyalgotrade.bar.Frequency`. + :param timezone: The default timezone to use to localize bars. Check :mod:`pyalgotrade.marketsession`. + :type timezone: A pytz timezone. + :param maxLen: The maximum number of values that the :class:`pyalgotrade.dataseries.bards.BarDataSeries` will hold. + Once a bounded length is full, when new items are added, a corresponding number of items are discarded from the + opposite end. If None then dataseries.DEFAULT_MAX_LEN is used. + :type maxLen: int. + + .. note:: + * The data should be sampled across regular time points, you can + regularlise (e.g. for 5min intervals) as:: + + df = df.set_index('Date Time').resample('s').interpolate().resample('5T').asfreq() + df = df.dropna().reset_index() + which is described in a SO [post](https://stackoverflow.com/a/39730730/4013571) + * It is ok if the **Adj Close** column is empty. + * When working with multiple instruments: + + * If all the instruments loaded are in the same timezone, then the timezone parameter may not be specified. + * If any of the instruments loaded are in different timezones, then the timezone parameter should be set. + """ + + def __init__(self, frequency, timezone=None, maxLen=None): + super(Feed, self).__init__(frequency, maxLen) + + self.__timezone = timezone + # Assume bars don't have adjusted close. This will be set to True after + # loading the first file if the adj_close column is there. + self.__haveAdjClose = False + + self.__barClass = bar.BasicBar + + self.__dateTimeFormat = "%Y-%m-%d %H:%M:%S" + self.__columnNames = { + "datetime": "Date Time", + "open": "Open", + "high": "High", + "low": "Low", + "close": "Close", + "volume": "Volume", + "adj_close": "Adj Close", + } + # self.__dateTimeFormat expects time to be set so there is no need to + # fix time. + self.setDailyBarTime(None) + + def barsHaveAdjClose(self): + return self.__haveAdjClose + + def setNoAdjClose(self): + self.__columnNames["adj_close"] = None + self.__haveAdjClose = False + + def setColumnName(self, col, name): + self.__columnNames[col] = name + + def setDateTimeFormat(self, dateTimeFormat): + self.__dateTimeFormat = dateTimeFormat + + def setBarClass(self, barClass): + self.__barClass = barClass + + def addBarsFromDataFrame(self, instrument, df, timezone=None): + """Loads bars for a given instrument from a Pandas DataFrame. + The instrument gets registered in the bar feed. + + :param instrument: Instrument identifier. + :type instrument: string. + :param df: The pandas DataFrame + :type df: pd.DataFrame + :param timezone: The timezone to use to localize bars. Check :mod:`pyalgotrade.marketsession`. + :type timezone: A pytz timezone. + """ + + if timezone is None: + timezone = self.__timezone + + df_has_adj_close = self.__columnNames['adj_close'] in df.columns + + missing_columns = [ + col for col in self.__columnNames.values() + if col not in df.columns + ] + if missing_columns: + raise ValueError('Missing required columns: {}'.format(repr(missing_columns))) + + # Convert DataFrame row(s) to Bar(s) + loadedBars = df.apply( + lambda row: bar.BasicBar( + row[self.__columnNames['datetime']], + row[self.__columnNames['open']], + row[self.__columnNames['close']], + row[self.__columnNames['high']], + row[self.__columnNames['low']], + row[self.__columnNames['volume']], + row[self.__columnNames['adj_close']], + self.getFrequency(), + extra = row[set(row.columns).difference(self.__columnNames.values())] + ) + ) + loadedBars = filter( + lambda bar_: (bar_ is not None) and + (self.__barFilter is None or self.__barFilter.includeBar(bar_)), + loadedBars + ) + self.addBarsFromSequence(instrument, loadedBars) + + if df_has_adj_close: + self.__haveAdjClose = True + elif self.__haveAdjClose: + raise Exception("Previous bars had adjusted close and these ones don't have.") From f1998e0f2001e71ab4bf04dbdcd36aa2523869c9 Mon Sep 17 00:00:00 2001 From: ttymck Date: Fri, 27 Dec 2019 23:22:45 -0500 Subject: [PATCH 3/8] feat(pandas): update to quantworks, add test cases --- .gitignore | 1 + poetry.lock | 16 +++++- pyproject.toml | 1 + quantworks/barfeed/csvfeed.py | 4 +- quantworks/barfeed/listfeed.py | 58 ++++++++++++-------- quantworks/barfeed/pandasfeed.py | 54 +++++++++++-------- testcases/unit/listfeed_test.py | 90 +++++++++++++++++++++++++++++++ testcases/unit/pandasfeed_test.py | 90 +++++++++++++++++++++++++++++++ 8 files changed, 269 insertions(+), 45 deletions(-) create mode 100644 testcases/unit/listfeed_test.py create mode 100644 testcases/unit/pandasfeed_test.py diff --git a/.gitignore b/.gitignore index d2657c83f..896e3392d 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ doc/_build /build /QuantWorks.egg-info /notebooks +.vscode /testcases/credentials.py .cache diff --git a/poetry.lock b/poetry.lock index f93da44e0..9d60b6e15 100644 --- a/poetry.lock +++ b/poetry.lock @@ -170,6 +170,19 @@ version = "19.2" pyparsing = ">=2.0.2" six = "*" +[[package]] +category = "dev" +description = "Powerful data structures for data analysis, time series, and statistics" +name = "pandas" +optional = false +python-versions = ">=3.5.3" +version = "0.25.3" + +[package.dependencies] +numpy = ">=1.13.3" +python-dateutil = ">=2.6.1" +pytz = ">=2017.2" + [[package]] category = "dev" description = "plugin and hook calling mechanisms for python" @@ -427,7 +440,7 @@ more-itertools = "*" TALib = ["TA-Lib"] [metadata] -content-hash = "00da9abad140acc3f1ea59919d31ba7a734a75180d70e0a54ec16dfbd16b9a16" +content-hash = "85d1c82e3de234c78692ef4c8ae0bff655f75647c4bd6a582a3843ceebcd0b07" python-versions = "^3.7" [metadata.hashes] @@ -449,6 +462,7 @@ more-itertools = ["b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c74964 numpy = ["03bbde29ac8fba860bb2c53a1525b3604a9b60417855ac3119d89868ec6041c3", "1baefd1fb4695e7f2e305467dbd876d765e6edd30c522894df76f8301efaee36", "1c35fb1131362e6090d30286cfda52ddd42e69d3e2bf1fea190a0fad83ea3a18", "3c68c827689ca0ca713dba598335073ce0966850ec0b30715527dce4ecd84055", "443ab93fc35b31f01db8704681eb2fd82f3a1b2fa08eed2dd0e71f1f57423d4a", "56710a756c5009af9f35b91a22790701420406d9ac24cf6b652b0e22cfbbb7ff", "62506e9e4d2a39c87984f081a2651d4282a1d706b1a82fe9d50a559bb58e705a", "6f8113c8dbfc192b58996ee77333696469ea121d1c44ea429d8fd266e4c6be51", "712f0c32555132f4b641b918bdb1fd3c692909ae916a233ce7f50eac2de87e37", "854f6ed4fa91fa6da5d764558804ba5b0f43a51e5fe9fc4fdc93270b052f188a", "88c5ccbc4cadf39f32193a5ef22e3f84674418a9fd877c63322917ae8f295a56", "905cd6fa6ac14654a6a32b21fad34670e97881d832e24a3ca32e19b455edb4a8", "9d6de2ad782aae68f7ed0e0e616477fbf693d6d7cc5f0f1505833ff12f84a673", "a30f5c3e1b1b5d16ec1f03f4df28e08b8a7529d8c920bbed657f4fde61f1fbcd", "a9d72d9abaf65628f0f31bbb573b7d9304e43b1e6bbae43149c17737a42764c4", "ac3cf835c334fcc6b74dc4e630f9b5ff7b4c43f7fb2a7813208d95d4e10b5623", "b091e5d4cbbe79f0e8b6b6b522346e54a282eadb06e3fd761e9b6fafc2ca91ad", "cc070fc43a494e42732d6ae2f6621db040611c1dde64762a40c8418023af56d7", "e1080e37c090534adb2dd7ae1c59ee883e5d8c3e63d2a4d43c20ee348d0459c5", "f084d513de729ff10cd72a1f80db468cff464fedb1ef2fea030221a0f62d7ff4", "f6a7421da632fc01e8a3ecd19c3f7350258d82501a646747664bae9c6a87c731"] oauthlib = ["bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", "df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"] packaging = ["28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", "d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"] +pandas = ["00dff3a8e337f5ed7ad295d98a31821d3d0fe7792da82d78d7fd79b89c03ea9d", "22361b1597c8c2ffd697aa9bf85423afa9e1fcfa6b1ea821054a244d5f24d75e", "255920e63850dc512ce356233081098554d641ba99c3767dde9e9f35630f994b", "26382aab9c119735908d94d2c5c08020a4a0a82969b7e5eefb92f902b3b30ad7", "33970f4cacdd9a0ddb8f21e151bfb9f178afb7c36eb7c25b9094c02876f385c2", "4545467a637e0e1393f7d05d61dace89689ad6d6f66f267f86fff737b702cce9", "52da74df8a9c9a103af0a72c9d5fdc8e0183a90884278db7f386b5692a2220a4", "61741f5aeb252f39c3031d11405305b6d10ce663c53bc3112705d7ad66c013d0", "6a3ac2c87e4e32a969921d1428525f09462770c349147aa8e9ab95f88c71ec71", "7458c48e3d15b8aaa7d575be60e1e4dd70348efcd9376656b72fecd55c59a4c3", "78bf638993219311377ce9836b3dc05f627a666d0dbc8cec37c0ff3c9ada673b", "8153705d6545fd9eb6dd2bc79301bff08825d2e2f716d5dced48daafc2d0b81f", "975c461accd14e89d71772e89108a050fa824c0b87a67d34cedf245f6681fc17", "9962957a27bfb70ab64103d0a7b42fa59c642fb4ed4cb75d0227b7bb9228535d", "adc3d3a3f9e59a38d923e90e20c4922fc62d1e5a03d083440468c6d8f3f1ae0a", "bbe3eb765a0b1e578833d243e2814b60c825b7fdbf4cdfe8e8aae8a08ed56ecf", "df8864824b1fe488cf778c3650ee59c3a0d8f42e53707de167ba6b4f7d35f133", "e45055c30a608076e31a9fcd780a956ed3b1fa20db61561b8d88b79259f526f7", "ee50c2142cdcf41995655d499a157d0a812fce55c97d9aad13bc1eef837ed36c"] pluggy = ["15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", "966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"] py = ["64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", "dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"] pyparsing = ["4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", "c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"] diff --git a/pyproject.toml b/pyproject.toml index 877323d85..743701d71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ coveralls = "^1.9" pytest = "^5.3" tox = "^3.14" pytest-cov = "^2.8" +pandas = "^0.25.3" [tool.poetry.extras] TALib = ["cython", "TA-Lib"] diff --git a/quantworks/barfeed/csvfeed.py b/quantworks/barfeed/csvfeed.py index 56e5bc3d5..33945587c 100644 --- a/quantworks/barfeed/csvfeed.py +++ b/quantworks/barfeed/csvfeed.py @@ -158,8 +158,8 @@ def __init__(self, columnNames, dateTimeFormat, dailyBarTime, frequency, timezon self.__adjCloseColName = columnNames["adj_close"] self.__columnNames = columnNames - def _parseDate(self, dateString): - ret = datetime.datetime.strptime(dateString, self.__dateTimeFormat) + def _parseDate(self, dateTime): + ret = datetime.datetime.strptime(dateTime, self.__dateTimeFormat) if self.__dailyBarTime is not None: ret = datetime.datetime.combine(ret, self.__dailyBarTime) diff --git a/quantworks/barfeed/listfeed.py b/quantworks/barfeed/listfeed.py index 322cd4f84..152737ea8 100644 --- a/quantworks/barfeed/listfeed.py +++ b/quantworks/barfeed/listfeed.py @@ -1,5 +1,6 @@ -# PyAlgoTrade +# QuantWorks # +# Copyright 2019 Tyler M Kontra # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,16 +19,16 @@ .. moduleauthor:: Alex McFarlane , Tyler Kontra """ -from pyalgotrade.utils import dt -from pyalgotrade.barfeed import membf -from pyalgotrade.barfeed import csvfeed -from pyalgotrade import bar +from quantworks.utils import dt +from quantworks.barfeed import membf +from quantworks.barfeed import csvfeed +from quantworks import bar import datetime class BarFeed(membf.BarFeed): - """Base class for Iterable[Dict] based :class:`pyalgotrade.barfeed.BarFeed`. + """Base class for Iterable[Dict] based :class:`quantworks.barfeed.BarFeed`. .. note:: This is a base class and should not be used directly. @@ -51,8 +52,7 @@ def getBarFilter(self): def setBarFilter(self, barFilter): self.__barFilter = barFilter - def _addBarsFromListofDicts(self, instrument, iterable, rowParser): - loadedBars = map(rowParser.parseBar, iterable) + def _addBarsFromListofDicts(self, instrument, loadedBars): loadedBars = filter( lambda bar_: (bar_ is not None) and (self.__barFilter is None or self.__barFilter.includeBar(bar_)), @@ -69,10 +69,10 @@ class Feed(BarFeed): 2015-08-14 09:06:00 0.00690 0.00690 0.00690 0.00690 1.346117 9567 - :param frequency: The frequency of the bars. Check :class:`pyalgotrade.bar.Frequency`. - :param timezone: The default timezone to use to localize bars. Check :mod:`pyalgotrade.marketsession`. + :param frequency: The frequency of the bars. Check :class:`quantworks.bar.Frequency`. + :param timezone: The default timezone to use to localize bars. Check :mod:`quantworks.marketsession`. :type timezone: A pytz timezone. - :param maxLen: The maximum number of values that the :class:`pyalgotrade.dataseries.bards.BarDataSeries` will hold. + :param maxLen: The maximum number of values that the :class:`quantworks.dataseries.bards.BarDataSeries` will hold. Once a bounded length is full, when new items are added, a corresponding number of items are discarded from the opposite end. If None then dataseries.DEFAULT_MAX_LEN is used. :type maxLen: int. @@ -130,6 +130,13 @@ def setDateTimeFormat(self, dateTimeFormat): def setBarClass(self, barClass): self.__barClass = barClass + + def __localize_dt(self, dateTime): + # Localize the datetime if a timezone was given. + if self.__timezone: + return dt.localize(dateTime, self.__timezone) + else: + return dateTime def addBarsFromListofDicts(self, instrument, list_of_dicts, timezone=None): """Loads bars for a given instrument from a list of dictionaries. @@ -140,25 +147,34 @@ def addBarsFromListofDicts(self, instrument, list_of_dicts, timezone=None): :param list_of_dicts: A list of dicts. First item should contain columns. :type list_of_dicts: list - :param timezone: The timezone to use to localize bars. Check :mod:`pyalgotrade.marketsession`. + :param timezone: The timezone to use to localize bars. Check :mod:`quantworks.marketsession`. :type timezone: A pytz timezone. """ if timezone is None: - timezone = self.__timezones + timezone = self.__timezone if not isinstance(list_of_dicts, (list, tuple)): raise ValueError('This function only supports types: {list, tuple}') if not isinstance(list_of_dicts[0], dict): raise ValueError('List should only contain dicts') - rowParser = csvfeed.GenericRowParser( - self.__columnNames, - self.__dateTimeFormat, - self.getDailyBarTime(), + dicts_have_adj_close = self.__columnNames['adj_close'] in list_of_dicts[0].keys() + + # Convert dicts to Bar(s) + loadedBars = map( + lambda row: bar.BasicBar( + self.__localize_dt(row[self.__columnNames['datetime']]), + row[self.__columnNames['open']], + row[self.__columnNames['high']], + row[self.__columnNames['low']], + row[self.__columnNames['close']], + row[self.__columnNames['volume']], + row[self.__columnNames['adj_close']], self.getFrequency(), - timezone, - self.__barClass + extra = {key: row[key] for key in set(row.keys()).difference(self.__columnNames.values())} + ), + list_of_dicts ) missing_columns = [ @@ -169,9 +185,9 @@ def addBarsFromListofDicts(self, instrument, list_of_dicts, timezone=None): raise ValueError('Missing required columns: {}'.format(repr(missing_columns))) super(Feed, self)._addBarsFromListofDicts( - instrument, list_of_dicts, rowParser) + instrument, loadedBars) - if rowParser.barsHaveAdjClose(): + if dicts_have_adj_close: self.__haveAdjClose = True elif self.__haveAdjClose: raise Exception("Previous bars had adjusted close and these ones don't have.") \ No newline at end of file diff --git a/quantworks/barfeed/pandasfeed.py b/quantworks/barfeed/pandasfeed.py index 5246a38f6..4a297c542 100644 --- a/quantworks/barfeed/pandasfeed.py +++ b/quantworks/barfeed/pandasfeed.py @@ -1,5 +1,6 @@ -# PyAlgoTrade +# QuantWorks # +# Copyright 2019 Tyler M Kontra # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,16 +19,16 @@ .. moduleauthor:: Alex McFarlane , Tyler Kontra """ -from pyalgotrade.utils import dt -from pyalgotrade.barfeed import membf -from pyalgotrade.barfeed import csvfeed -from pyalgotrade import bar +from quantworks.utils import dt +from quantworks.barfeed import membf +from quantworks.barfeed import csvfeed +from quantworks import bar import datetime class BarFeed(membf.BarFeed): - """Base class for Pandas DataFrame based :class:`pyalgotrade.barfeed.BarFeed`. + """Base class for Pandas DataFrame based :class:`quantworks.barfeed.BarFeed`. .. note:: This is a base class and should not be used directly. @@ -51,6 +52,14 @@ def getBarFilter(self): def setBarFilter(self, barFilter): self.__barFilter = barFilter + def _addBars(self, instrument, bar_list): + loadedBars = filter( + lambda bar_: (bar_ is not None) and + (self.__barFilter is None or self.__barFilter.includeBar(bar_)), + bar_list + ) + self.addBarsFromSequence(instrument, loadedBars) + class Feed(BarFeed): """A BarFeed that loads bars from a custom feed that has the following columns: @@ -60,10 +69,10 @@ class Feed(BarFeed): 2015-08-14 09:06:00 0.00690 0.00690 0.00690 0.00690 1.346117 9567 - :param frequency: The frequency of the bars. Check :class:`pyalgotrade.bar.Frequency`. - :param timezone: The default timezone to use to localize bars. Check :mod:`pyalgotrade.marketsession`. + :param frequency: The frequency of the bars. Check :class:`quantworks.bar.Frequency`. + :param timezone: The default timezone to use to localize bars. Check :mod:`quantworks.marketsession`. :type timezone: A pytz timezone. - :param maxLen: The maximum number of values that the :class:`pyalgotrade.dataseries.bards.BarDataSeries` will hold. + :param maxLen: The maximum number of values that the :class:`quantworks.dataseries.bards.BarDataSeries` will hold. Once a bounded length is full, when new items are added, a corresponding number of items are discarded from the opposite end. If None then dataseries.DEFAULT_MAX_LEN is used. :type maxLen: int. @@ -122,6 +131,13 @@ def setDateTimeFormat(self, dateTimeFormat): def setBarClass(self, barClass): self.__barClass = barClass + def __localize_dt(self, dateTime): + # Localize the datetime if a timezone was given. + if self.__timezone: + return dt.localize(dateTime, self.__timezone) + else: + return dateTime + def addBarsFromDataFrame(self, instrument, df, timezone=None): """Loads bars for a given instrument from a Pandas DataFrame. The instrument gets registered in the bar feed. @@ -130,7 +146,7 @@ def addBarsFromDataFrame(self, instrument, df, timezone=None): :type instrument: string. :param df: The pandas DataFrame :type df: pd.DataFrame - :param timezone: The timezone to use to localize bars. Check :mod:`pyalgotrade.marketsession`. + :param timezone: The timezone to use to localize bars. Check :mod:`quantworks.marketsession`. :type timezone: A pytz timezone. """ @@ -147,25 +163,21 @@ def addBarsFromDataFrame(self, instrument, df, timezone=None): raise ValueError('Missing required columns: {}'.format(repr(missing_columns))) # Convert DataFrame row(s) to Bar(s) - loadedBars = df.apply( + loadedBars = map( lambda row: bar.BasicBar( - row[self.__columnNames['datetime']], + self.__localize_dt(row[self.__columnNames['datetime']]), row[self.__columnNames['open']], - row[self.__columnNames['close']], row[self.__columnNames['high']], row[self.__columnNames['low']], + row[self.__columnNames['close']], row[self.__columnNames['volume']], row[self.__columnNames['adj_close']], self.getFrequency(), - extra = row[set(row.columns).difference(self.__columnNames.values())] - ) + extra = {key: row[key] for key in set(row.keys()).difference(self.__columnNames.values())} + ), + df.to_dict(orient='record') ) - loadedBars = filter( - lambda bar_: (bar_ is not None) and - (self.__barFilter is None or self.__barFilter.includeBar(bar_)), - loadedBars - ) - self.addBarsFromSequence(instrument, loadedBars) + self._addBars(instrument, loadedBars) if df_has_adj_close: self.__haveAdjClose = True diff --git a/testcases/unit/listfeed_test.py b/testcases/unit/listfeed_test.py new file mode 100644 index 000000000..4850886ff --- /dev/null +++ b/testcases/unit/listfeed_test.py @@ -0,0 +1,90 @@ +# QuantWorks +# +# Copyright 2019 Tyler M Kontra +# Copyright 2011-2018 Gabriel Martin Becedillas Ruiz +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +.. moduleauthor:: Gabriel Martin Becedillas Ruiz , Tyler M Kontra +""" + +import datetime +import pandas as pd +import pytz + +from . import common +from . import feed_test + +from quantworks import barfeed +from quantworks.barfeed import Frequency +from quantworks.barfeed import common as bfcommon +from quantworks.barfeed import listfeed +from quantworks import bar +from quantworks.utils import dt + + +class ListFeedTestCase(common.TestCase): + __columnNames = [ + "Date Time", + "Open", + "High", + "Low", + "Close", + "Volume", + "Adj Close", + ] + + def __loadIntradayBarFeed(self, timezone=None, maxLen=None): + dt0 = datetime.datetime.utcnow() + idx = range(1, 7) + data = { + "Date Time": [dt0 + datetime.timedelta(hours = i) for i in idx], + "Open": [i for i in idx], + "High": [i*3 for i in idx], + "Low": [i/2 for i in idx], + "Close": [i*2 for i in idx], + "Volume": [i**2 for i in idx], + "Adj Close": [i*2 for i in idx], + } + ret = listfeed.Feed(Frequency.MINUTE, timezone, maxLen) + rows = pd.DataFrame(data).to_dict(orient='record') + ret.addBarsFromListofDicts("tst", rows, timezone) + ret.loadAll() + return ret + + def testBaseFeedInterface(self): + barFeed = self.__loadIntradayBarFeed() + feed_test.tstBaseFeedInterface(self, barFeed) + + def testWithTimezone(self): + timeZone = pytz.timezone("US/Central") + barFeed = self.__loadIntradayBarFeed(timeZone) + ds = barFeed.getDataSeries() + + for i, currentBar in enumerate(ds): + self.assertFalse(dt.datetime_is_naive(currentBar.getDateTime())) + self.assertEqual(ds[i].getDateTime(), ds.getDateTimes()[i]) + + def testBounded(self): + barFeed = self.__loadIntradayBarFeed(maxLen=2) + + barDS = barFeed["tst"] + self.assertEqual(len(barDS), 2) + self.assertEqual(len(barDS.getDateTimes()), 2) + self.assertEqual(len(barDS.getCloseDataSeries()), 2) + self.assertEqual(len(barDS.getCloseDataSeries().getDateTimes()), 2) + self.assertEqual(len(barDS.getOpenDataSeries()), 2) + self.assertEqual(len(barDS.getHighDataSeries()), 2) + self.assertEqual(len(barDS.getLowDataSeries()), 2) + self.assertEqual(len(barDS.getAdjCloseDataSeries()), 2) diff --git a/testcases/unit/pandasfeed_test.py b/testcases/unit/pandasfeed_test.py new file mode 100644 index 000000000..a7dc2e0b2 --- /dev/null +++ b/testcases/unit/pandasfeed_test.py @@ -0,0 +1,90 @@ +# QuantWorks +# +# Copyright 2019 Tyler M Kontra +# Copyright 2011-2018 Gabriel Martin Becedillas Ruiz +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +.. moduleauthor:: Gabriel Martin Becedillas Ruiz , Tyler M Kontra +""" + +import datetime +import pandas as pd +import pytz + +from . import common +from . import feed_test + +from quantworks import barfeed +from quantworks.barfeed import Frequency +from quantworks.barfeed import common as bfcommon +from quantworks.barfeed import pandasfeed +from quantworks import bar +from quantworks.utils import dt + + +class PandasFeedTestCase(common.TestCase): + __columnNames = [ + "Date Time", + "Open", + "High", + "Low", + "Close", + "Volume", + "Adj Close", + ] + + def __loadIntradayBarFeed(self, timezone=None, maxLen=None): + dt0 = datetime.datetime.utcnow() + idx = range(1, 7) + data = { + "Date Time": [dt0 + datetime.timedelta(hours = i) for i in idx], + "Open": [i for i in idx], + "High": [i*3 for i in idx], + "Low": [i/2 for i in idx], + "Close": [i*2 for i in idx], + "Volume": [i**2 for i in idx], + "Adj Close": [i*2 for i in idx], + } + ret = pandasfeed.Feed(Frequency.MINUTE, timezone, maxLen) + sampleDataFrame = pd.DataFrame(data) + ret.addBarsFromDataFrame("tst", sampleDataFrame, timezone) + ret.loadAll() + return ret + + def testBaseFeedInterface(self): + barFeed = self.__loadIntradayBarFeed() + feed_test.tstBaseFeedInterface(self, barFeed) + + def testWithTimezone(self): + timeZone = pytz.timezone("US/Central") + barFeed = self.__loadIntradayBarFeed(timeZone) + ds = barFeed.getDataSeries() + + for i, currentBar in enumerate(ds): + self.assertFalse(dt.datetime_is_naive(currentBar.getDateTime())) + self.assertEqual(ds[i].getDateTime(), ds.getDateTimes()[i]) + + def testBounded(self): + barFeed = self.__loadIntradayBarFeed(maxLen=2) + + barDS = barFeed["tst"] + self.assertEqual(len(barDS), 2) + self.assertEqual(len(barDS.getDateTimes()), 2) + self.assertEqual(len(barDS.getCloseDataSeries()), 2) + self.assertEqual(len(barDS.getCloseDataSeries().getDateTimes()), 2) + self.assertEqual(len(barDS.getOpenDataSeries()), 2) + self.assertEqual(len(barDS.getHighDataSeries()), 2) + self.assertEqual(len(barDS.getLowDataSeries()), 2) + self.assertEqual(len(barDS.getAdjCloseDataSeries()), 2) From 88bc85518b2c2367a5ccbc48281275f4e6250fac Mon Sep 17 00:00:00 2001 From: ttymck Date: Fri, 27 Dec 2019 23:35:20 -0500 Subject: [PATCH 4/8] fix(travis): cache installed ta-lib --- .travis.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index ebac62e2b..b462a2028 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,9 @@ language: python python: - '3.7' - +cache: + - "$HOME/.travis-cache" + env: global: - secure: "SJPwuOKJDiGgOB+VyhJ/i/3Hb8z0leDo7hdgTp3X9+9dAdPQlwXSnCKoOEjLqpCz9gkr0pw6PCEnyDMcZo9ig2IBKt2y1r2M1Oc6c1d201EI+Fe6RGsRyG1StqU9iNSPZ9UEK+ld4Oa1FspNlFy1msd3N6sgpRRAa7HTENsCMKTl7Sq+wPwGtmm6qACCZN2Eqtth1ZyvwW5+64xRJPBNLcxNMnU2IJCPdn4j7MFsVpTufWAcV3kghvue0y73pKQRKnLrNGANt33uocU/PaKLYpGa/D0loEbRglgmG1ufnPCs4XyvRBgVN2iK2hLVHeXQvOXkfaH4joQzf+6PG/2UzP9EG/hRSSsI+Ul11mxxXrljwNm3a6N3E/JR7xhXWn1bGckLyoMm9tButedjs9zKhCRQWgh6f63l0JugFG86FLdAL1OpNjP77kpT+xwlrYaXvteJvr+TYu2H6QvsUJnT0QuRu9ydQD5UbaelVVZk4M8XK0cmD33HKEpBXNBAQjrvVcQu5VGJeyWgMYMmrDwjP4UsframpU8kAQa+cFu/ZaK624onROoxzD8/Am5sLdesJM6LWj1iSJSfHlGJT/KcfW+wM6+3JPuyR0X9Foyv1EkxzzqMMyOUZ04/hN8X681qBhEk9rnpPNphNnYOrmzS1JNXTNXt2/Boea5B58JNVDU=" @@ -10,10 +12,14 @@ sudo: required before_install: - sudo apt-get update - - sudo apt-get install build-essential wget - - wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz - - tar -xvzf ta-lib-0.4.0-src.tar.gz - - cd ta-lib/ && ./configure --prefix=/usr LDFLAGS="-lm" && make && sudo make install && cd .. && rm -r ta-lib + - if [ ! -f "$HOME/.travis-cache/ta-lib" ]; then + sudo apt-get install build-essential wget; + wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz; + tar -xvzf ta-lib-0.4.0-src.tar.gz; + cd ta-lib/ && ./configure --prefix=/usr LDFLAGS="-lm" && make && sudo make install && cd .. && rm -r ta-lib; + mkdir -p $HOME/.travis-cache; + touch $HOME/.travis-cache/ta-lib; + fi - python -m pip install poetry - poetry install From a373d23b8294af7788f88c25514c76e6704aab67 Mon Sep 17 00:00:00 2001 From: ttymck Date: Fri, 27 Dec 2019 23:41:32 -0500 Subject: [PATCH 5/8] fix(travis): set -e --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index b462a2028..9c2c800d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ sudo: required before_install: - sudo apt-get update + - set -e - if [ ! -f "$HOME/.travis-cache/ta-lib" ]; then sudo apt-get install build-essential wget; wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz; From c1126a40cdecec33fb63d250b7d9ad45ff507e9b Mon Sep 17 00:00:00 2001 From: ttymck Date: Fri, 27 Dec 2019 23:49:23 -0500 Subject: [PATCH 6/8] fix(travis): checking if ta-lib installed --- .travis.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9c2c800d3..e4371f37f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ language: python python: - '3.7' -cache: - - "$HOME/.travis-cache" env: global: @@ -13,13 +11,11 @@ sudo: required before_install: - sudo apt-get update - set -e - - if [ ! -f "$HOME/.travis-cache/ta-lib" ]; then + - if [ ! -f /usr/lib/libta_lib.so ]; then sudo apt-get install build-essential wget; wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz; tar -xvzf ta-lib-0.4.0-src.tar.gz; cd ta-lib/ && ./configure --prefix=/usr LDFLAGS="-lm" && make && sudo make install && cd .. && rm -r ta-lib; - mkdir -p $HOME/.travis-cache; - touch $HOME/.travis-cache/ta-lib; fi - python -m pip install poetry - poetry install From 3dc44463a77271507fa7dab842ba0a426f420b28 Mon Sep 17 00:00:00 2001 From: ttymck Date: Fri, 27 Dec 2019 23:51:29 -0500 Subject: [PATCH 7/8] fix(travis): caching ta-lib install (/usr/lib) --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index e4371f37f..275eb5952 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: python python: - '3.7' +cache: + - /usr/lib env: global: From e4d0d6a4f20c97ee03dcbc15f3ce5c00ce8b0094 Mon Sep 17 00:00:00 2001 From: ttymck Date: Sat, 28 Dec 2019 12:44:39 -0500 Subject: [PATCH 8/8] fix(travis): skip coveralls --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 275eb5952..959ae1dc2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,5 +27,5 @@ script: - export PYTHONPATH=. - poetry run tox -v -after_success: - - poetry run coveralls +# after_success: +# - poetry run coveralls