diff --git a/.travis.yml b/.travis.yml index ebac62e2b..959ae1dc2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,9 @@ language: python python: - '3.7' - +cache: + - /usr/lib + 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,13 @@ 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 + - set -e + - 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; + fi - python -m pip install poetry - poetry install @@ -22,5 +27,5 @@ script: - export PYTHONPATH=. - poetry run tox -v -after_success: - - poetry run coveralls +# after_success: +# - poetry run coveralls 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 638f61af7..5a390648e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,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 new file mode 100644 index 000000000..152737ea8 --- /dev/null +++ b/quantworks/barfeed/listfeed.py @@ -0,0 +1,193 @@ +# QuantWorks +# +# Copyright 2019 Tyler M Kontra +# 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 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:`quantworks.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, loadedBars): + loadedBars = filter( + lambda bar_: (bar_ is not None) and + (self.__barFilter is None or self.__barFilter.includeBar(bar_)), + loadedBars + ) + self.addBarsFromSequence(instrument, loadedBars) + + +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:`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:`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. + + .. 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 __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. + 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:`quantworks.marketsession`. + :type timezone: A pytz timezone. + """ + + if timezone is None: + 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') + + 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(), + extra = {key: row[key] for key in set(row.keys()).difference(self.__columnNames.values())} + ), + list_of_dicts + ) + + 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, loadedBars) + + 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 new file mode 100644 index 000000000..4a297c542 --- /dev/null +++ b/quantworks/barfeed/pandasfeed.py @@ -0,0 +1,185 @@ +# QuantWorks +# +# Copyright 2019 Tyler M Kontra +# 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 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:`quantworks.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 _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: + :: + + 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:`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:`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. + + .. 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 __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. + + :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:`quantworks.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 = 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(), + extra = {key: row[key] for key in set(row.keys()).difference(self.__columnNames.values())} + ), + df.to_dict(orient='record') + ) + self._addBars(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.") 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)