From 16436dc124a3a71b7705575a7e4585f2ba3434b4 Mon Sep 17 00:00:00 2001 From: Juan Pablo Amoroso Date: Thu, 30 May 2019 11:42:52 -0300 Subject: [PATCH] Added tests for filter. Added pytest dependency. --- Makefile | 6 +- Pipfile | 1 + Pipfile.lock | 143 ++++++++++++------ .../datahandler/historical_options_data.py | 4 +- backtester/datahandler/schema.py | 2 + backtester/test/datahandler/test_filter.py | 49 ++++++ backtester/test/test_balancedportfolio.py | 55 ------- backtester/test/test_portfolio.py | 32 ---- 8 files changed, 154 insertions(+), 138 deletions(-) create mode 100644 backtester/test/datahandler/test_filter.py delete mode 100644 backtester/test/test_balancedportfolio.py delete mode 100644 backtester/test/test_portfolio.py diff --git a/Makefile b/Makefile index 36386f2..d8490c6 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: image init env testdata test test_scraper scrape aggregate backup bench +.PHONY: image init env testdata test_backtester test_scraper scrape aggregate backup bench image: docker build -t data_scraper -f ./docker/data_scraper/Dockerfile . @@ -18,8 +18,8 @@ env: testdata: pipenv run python backtester/test/create_test_data.py -test: - pipenv run python -m unittest discover -s backtester/test +test_backtester: + pipenv run python -m pytest backtester/test test_scraper: pipenv run python -m unittest discover -s data_scraper diff --git a/Pipfile b/Pipfile index f5ac40f..9420418 100644 --- a/Pipfile +++ b/Pipfile @@ -12,6 +12,7 @@ jupyter = "*" pandas-datareader = "*" pandas-market-calendars = "*" boto3 = "*" +pytest = "*" [dev-packages] yapf = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 9a27b1d..cd7250b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "cd6cfed74640668b74dcbedc2cdf4426f24c7e71ab3351bfc4c331558994685c" + "sha256": "e6dd21683a090d52f61e0ae4c3be50e9906e212d12603846c4d70209886216cd" }, "pipfile-spec": 6, "requires": {}, @@ -30,6 +30,13 @@ "markers": "sys_platform == 'darwin'", "version": "==0.1.0" }, + "atomicwrites": { + "hashes": [ + "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", + "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" + ], + "version": "==1.3.0" + }, "attrs": { "hashes": [ "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", @@ -62,18 +69,18 @@ }, "boto3": { "hashes": [ - "sha256:a653dd7e155f0fb7d950cef7e2be7fa12b7787f4c8835f324abcfd1dbe4bb73d", - "sha256:f2054b788e19a2d91a3cc76814ca4e44050da7c89781a50924a132acf24fad56" + "sha256:98516650f72c02d0c7b47e6f0df69c19a0d1046d3973783ed7b0f093e8e81adb", + "sha256:aa06b240e49dbc8443bd96086c92baa0275281e0f7cc0439a328fef9ac254253" ], "index": "pypi", - "version": "==1.9.147" + "version": "==1.9.157" }, "botocore": { "hashes": [ - "sha256:57d351535ffe0f19a80cb408df4dfc43fe66815cad3320e6a02d2261b498a8f6", - "sha256:e49db28173955bd3674b3280911446e6bd4458a9c40abfec46bf7c378fd30b2b" + "sha256:2ded06af31f9e423fcf549e35f2fa0fd618e12995e37ebc52186dbaa870316c6", + "sha256:473955cd4eda6121047205fc43c56ab0e0616b93651bac5e9c747fc180603fe2" ], - "version": "==1.12.147" + "version": "==1.12.157" }, "certifi": { "hashes": [ @@ -125,12 +132,19 @@ ], "version": "==2.8" }, + "importlib-metadata": { + "hashes": [ + "sha256:027cfc6524613de726789072f95d2e4cc64dd1dee8096d42d13f2ead5bd302f5", + "sha256:0d05199e1f0b1a8707a1b9c46476d4a49807fb56cb1b0737db1d37feb42fe31d" + ], + "version": "==0.15" + }, "ipykernel": { "hashes": [ - "sha256:0aeb7ec277ac42cc2b59ae3d08b10909b2ec161dc6908096210527162b53675d", - "sha256:0fc0bf97920d454102168ec2008620066878848fcfca06c22b669696212e292f" + "sha256:346189536b88859937b5f4848a6fd85d1ad0729f01724a411de5cae9b618819c", + "sha256:f0e962052718068ad3b1d8bcc703794660858f58803c3798628817f492a8769c" ], - "version": "==5.1.0" + "version": "==5.1.1" }, "ipython": { "hashes": [ @@ -283,6 +297,14 @@ ], "version": "==0.8.4" }, + "more-itertools": { + "hashes": [ + "sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7", + "sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a" + ], + "markers": "python_version > '2.7'", + "version": "==7.0.0" + }, "nbconvert": { "hashes": [ "sha256:138381baa41d83584459b5cfecfc38c800ccf1f37d9ddd0bd440783346a4c39c", @@ -306,31 +328,31 @@ }, "numpy": { "hashes": [ - "sha256:0e2eed77804b2a6a88741f8fcac02c5499bba3953ec9c71e8b217fad4912c56c", - "sha256:1c666f04553ef70fda54adf097dbae7080645435fc273e2397f26bbf1d127bbb", - "sha256:1f46532afa7b2903bfb1b79becca2954c0a04389d19e03dc73f06b039048ac40", - "sha256:315fa1b1dfc16ae0f03f8fd1c55f23fd15368710f641d570236f3d78af55e340", - "sha256:3d5fcea4f5ed40c3280791d54da3ad2ecf896f4c87c877b113576b8280c59441", - "sha256:48241759b99d60aba63b0e590332c600fc4b46ad597c9b0a53f350b871ef0634", - "sha256:4b4f2924b36d857cf302aec369caac61e43500c17eeef0d7baacad1084c0ee84", - "sha256:54fe3b7ed9e7eb928bbc4318f954d133851865f062fa4bbb02ef8940bc67b5d2", - "sha256:5a8f021c70e6206c317974c93eaaf9bc2b56295b6b1cacccf88846e44a1f33fc", - "sha256:754a6be26d938e6ca91942804eb209307b73f806a1721176278a6038869a1686", - "sha256:771147e654e8b95eea1293174a94f34e2e77d5729ad44aefb62fbf8a79747a15", - "sha256:78a6f89da87eeb48014ec652a65c4ffde370c036d780a995edaeb121d3625621", - "sha256:7fde5c2a3a682a9e101e61d97696687ebdba47637611378b4127fe7e47fdf2bf", - "sha256:80d99399c97f646e873dd8ce87c38cfdbb668956bbc39bc1e6cac4b515bba2a0", - "sha256:88a72c1e45a0ae24d1f249a529d9f71fe82e6fa6a3fd61414b829396ec585900", - "sha256:a4f4460877a16ac73302a9c077ca545498d9fe64e6a81398d8e1a67e4695e3df", - "sha256:a61255a765b3ac73ee4b110b28fccfbf758c985677f526c2b4b39c48cc4b509d", - "sha256:ab4896a8c910b9a04c0142871d8800c76c8a2e5ff44763513e1dd9d9631ce897", - "sha256:abbd6b1c2ef6199f4b7ca9f818eb6b31f17b73a6110aadc4e4298c3f00fab24e", - "sha256:b16d88da290334e33ea992c56492326ea3b06233a00a1855414360b77ca72f26", - "sha256:b78a1defedb0e8f6ae1eb55fa6ac74ab42acc4569c3a2eacc2a407ee5d42ebcb", - "sha256:cfef82c43b8b29ca436560d51b2251d5117818a8d1fb74a8384a83c096745dad", - "sha256:d160e57731fcdec2beda807ebcabf39823c47e9409485b5a3a1db3a8c6ce763e" + "sha256:0778076e764e146d3078b17c24c4d89e0ecd4ac5401beff8e1c87879043a0633", + "sha256:141c7102f20abe6cf0d54c4ced8d565b86df4d3077ba2343b61a6db996cefec7", + "sha256:14270a1ee8917d11e7753fb54fc7ffd1934f4d529235beec0b275e2ccf00333b", + "sha256:27e11c7a8ec9d5838bc59f809bfa86efc8a4fd02e58960fa9c49d998e14332d5", + "sha256:2a04dda79606f3d2f760384c38ccd3d5b9bb79d4c8126b67aff5eb09a253763e", + "sha256:3c26010c1b51e1224a3ca6b8df807de6e95128b0908c7e34f190e7775455b0ca", + "sha256:52c40f1a4262c896420c6ea1c6fda62cf67070e3947e3307f5562bd783a90336", + "sha256:6e4f8d9e8aa79321657079b9ac03f3cf3fd067bf31c1cca4f56d49543f4356a5", + "sha256:7242be12a58fec245ee9734e625964b97cf7e3f2f7d016603f9e56660ce479c7", + "sha256:7dc253b542bfd4b4eb88d9dbae4ca079e7bf2e2afd819ee18891a43db66c60c7", + "sha256:94f5bd885f67bbb25c82d80184abbf7ce4f6c3c3a41fbaa4182f034bba803e69", + "sha256:a89e188daa119ffa0d03ce5123dee3f8ffd5115c896c2a9d4f0dbb3d8b95bfa3", + "sha256:ad3399da9b0ca36e2f24de72f67ab2854a62e623274607e37e0ce5f5d5fa9166", + "sha256:b0348be89275fd1d4c44ffa39530c41a21062f52299b1e3ee7d1c61f060044b8", + "sha256:b5554368e4ede1856121b0dfa35ce71768102e4aa55e526cb8de7f374ff78722", + "sha256:cbddc56b2502d3f87fda4f98d948eb5b11f36ff3902e17cb6cc44727f2200525", + "sha256:d79f18f41751725c56eceab2a886f021d70fd70a6188fd386e29a045945ffc10", + "sha256:dc2ca26a19ab32dc475dbad9dfe723d3a64c835f4c23f625c2b6566ca32b9f29", + "sha256:dd9bcd4f294eb0633bb33d1a74febdd2b9018b8b8ed325f861fffcd2c7660bb8", + "sha256:e8baab1bc7c9152715844f1faca6744f2416929de10d7639ed49555a85549f52", + "sha256:ec31fe12668af687b99acf1567399632a7c47b0e17cfb9ae47c098644ef36797", + "sha256:f12b4f7e2d8f9da3141564e6737d79016fe5336cc92de6814eba579744f65b0a", + "sha256:f58ac38d5ca045a377b3b377c84df8175ab992c970a53332fa8ac2373df44ff7" ], - "version": "==1.16.3" + "version": "==1.16.4" }, "pandas": { "hashes": [ @@ -402,6 +424,13 @@ ], "version": "==0.7.5" }, + "pluggy": { + "hashes": [ + "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc", + "sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c" + ], + "version": "==0.12.0" + }, "prometheus-client": { "hashes": [ "sha256:1b38b958750f66f208bcd9ab92a633c0c994d8859c831f7abc1f46724fcee490" @@ -424,12 +453,19 @@ "markers": "os_name != 'nt'", "version": "==0.6.0" }, + "py": { + "hashes": [ + "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", + "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + ], + "version": "==1.8.0" + }, "pygments": { "hashes": [ - "sha256:31cba6ffb739f099a85e243eff8cb717089fdd3c7300767d9fc34cb8e1b065f5", - "sha256:5ad302949b3c98dd73f8d9fcdc7e9cb592f120e32a18e23efd7f3dc51194472b" + "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", + "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297" ], - "version": "==2.4.0" + "version": "==2.4.2" }, "pyrsistent": { "hashes": [ @@ -437,6 +473,14 @@ ], "version": "==0.15.2" }, + "pytest": { + "hashes": [ + "sha256:1a8aa4fa958f8f451ac5441f3ac130d9fc86ea38780dd2715e6d5c5882700b24", + "sha256:b8bf138592384bd4e87338cb0f256bf5f615398a649d4bd83915f0e4047a5ca6" + ], + "index": "pypi", + "version": "==4.5.0" + }, "python-dateutil": { "hashes": [ "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", @@ -484,18 +528,18 @@ }, "qtconsole": { "hashes": [ - "sha256:a667558c7b1e1442a2e5bcef1686c55e096efd0b58d8b2a0a8415f4579991ee3", - "sha256:fdfc6002d9d2834c88f9c92e0f6f590284ff3740fa53016f188a62d58bcca6d8" + "sha256:4af84facdd6f00a6b9b2927255f717bb23ae4b7a20ba1d9ef0a5a5a8dbe01ae2", + "sha256:60d61d93f7d67ba2b265c6d599d413ffec21202fec999a952f658ff3a73d252b" ], - "version": "==4.4.4" + "version": "==4.5.1" }, "requests": { "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", + "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" ], "index": "pypi", - "version": "==2.21.0" + "version": "==2.22.0" }, "s3transfer": { "hashes": [ @@ -566,11 +610,11 @@ }, "urllib3": { "hashes": [ - "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", - "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" + "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", + "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232" ], "markers": "python_version >= '3.4'", - "version": "==1.24.3" + "version": "==1.25.3" }, "wcwidth": { "hashes": [ @@ -598,6 +642,13 @@ "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" ], "version": "==1.11.1" + }, + "zipp": { + "hashes": [ + "sha256:8c1019c6aad13642199fbe458275ad6a84907634cc9f0989877ccc4a2840139d", + "sha256:ca943a7e809cc12257001ccfb99e3563da9af99d52f261725e96dfe0f9275bc3" + ], + "version": "==0.5.1" } }, "develop": { diff --git a/backtester/datahandler/historical_options_data.py b/backtester/datahandler/historical_options_data.py index 8ff18cd..3e32920 100644 --- a/backtester/datahandler/historical_options_data.py +++ b/backtester/datahandler/historical_options_data.py @@ -5,7 +5,7 @@ from .schema import Schema class HistoricalOptionsData: """Historical Options Data container class.""" - def __init__(self, file, key=None, query=None, schema=None): + def __init__(self, file, schema=None, **params): if schema: assert isinstance(schema, Schema) else: @@ -13,7 +13,7 @@ class HistoricalOptionsData: schema.update({"contract": "optionroot", "date": "quotedate"}) self.schema = schema - self._data = pd.read_hdf(file, key=key, where=query) + self._data = pd.read_hdf(file, **params) columns = self._data.columns assert all((col in columns for col in schema)) diff --git a/backtester/datahandler/schema.py b/backtester/datahandler/schema.py index a704322..8dfb185 100644 --- a/backtester/datahandler/schema.py +++ b/backtester/datahandler/schema.py @@ -59,6 +59,8 @@ class Field: return self._create_filter(">=", value) def __eq__(self, value): + if isinstance(value, str): + value = "'{}'".format(value) return self._create_filter("==", value) def __ne__(self, value): diff --git a/backtester/test/datahandler/test_filter.py b/backtester/test/datahandler/test_filter.py new file mode 100644 index 0000000..944731b --- /dev/null +++ b/backtester/test/datahandler/test_filter.py @@ -0,0 +1,49 @@ +from backtester.datahandler import Field + + +def test_strike_eq_100(): + """Test filter for 'strike' == 100""" + strike_field = Field("strike", "strike") + ft = strike_field == 100 + assert ft.query == "strike == 100" + + +def test_strike_lt_100(): + """Test filter for 'strike' < 100""" + strike_field = Field("strike", "strike") + ft = strike_field < 100 + assert ft.query == "strike < 100" + + +def test_strike_ge_100(): + """Test filter for 'strike' >= 100""" + strike_field = Field("strike", "strike") + ft = strike_field >= 100 + assert ft.query == "strike >= 100" + + +def test_negate_filter(): + """Test negations of a filter""" + symbol_field = Field("underlying", "underlying") + ft = symbol_field == "SPX" + negated = ~ft + assert negated.query == "!(underlying == 'SPX')" + + +def test_compose_filters_with_and(): + """Test composition of two filters with and""" + symbol_field = Field("underlying", "underlying") + strike_field = Field("strike", "strike") + ft1 = symbol_field == "SPX" + ft2 = strike_field < 200 + composed = ft1 & ft2 + assert composed.query == "(underlying == 'SPX') & (strike < 200)" + + +def test_compose_filters_with_or(): + """Test composition of two filters with or""" + strike_field = Field("strike", "strike") + ft1 = strike_field >= 100 + ft2 = strike_field < 200 + composed = ft1 | ft2 + assert composed.query == "(strike >= 100) | (strike < 200)" diff --git a/backtester/test/test_balancedportfolio.py b/backtester/test/test_balancedportfolio.py deleted file mode 100644 index 4904098..0000000 --- a/backtester/test/test_balancedportfolio.py +++ /dev/null @@ -1,55 +0,0 @@ -import unittest -import os -import backtester as bt -from backtester.utils import get_data_dir - - -class TestBalancedPortfolio(unittest.TestCase): - """Tests benchmark strategy using synthetic data""" - - @classmethod - def setUpClass(cls): - data_dir = get_data_dir() - balanced_file = os.path.join(data_dir, "balanced_2015.csv") - cls.port = bt.run(balanced_file) - cls.report = cls.port.create_report() - cls.initial_capital = 1000000 - cls.symbols = [ - "VOO", "GLD", "VNQ", "VNQI", "TLT", "TIP", "BNDX", "EEM", "RJI" - ] - - def test_first_day_allocation(self): - """First day allocation of cash should be equal to initial capital""" - self.assertEqual(TestBalancedPortfolio.report["Cash"].iloc[0], - TestBalancedPortfolio.initial_capital) - - def test_total_return(self): - self.assertAlmostEqual( - TestBalancedPortfolio.report["% Price"][-1], -0.041, delta=0.001) - - def test_voo_allocation(self): - """Default VOO allocation should equal 30%""" - self.assertAlmostEqual( - TestBalancedPortfolio.report["VOO Exposure"].iloc[1], - TestBalancedPortfolio.initial_capital * 0.3, - delta=TestBalancedPortfolio.initial_capital * 0.01) - - def test_voo_amount_constant(self): - """Default VOO amount should remain constant throughout backtest""" - voo_amounts = TestBalancedPortfolio.report["VOO Amount"] - self.assertTrue(all(voo_amounts[1:] == voo_amounts.iloc[1])) - - def test_sum_of_all_symbols(self): - """Sum of all allocations should equal initial capital""" - total_allocations = sum([ - TestBalancedPortfolio.report[symbol + " Exposure"].iloc[2] - for symbol in TestBalancedPortfolio.symbols - ]) - self.assertAlmostEqual( - total_allocations, - TestBalancedPortfolio.initial_capital, - delta=TestBalancedPortfolio.initial_capital * 0.1) - - -if __name__ == "__main__": - unittest.main() diff --git a/backtester/test/test_portfolio.py b/backtester/test/test_portfolio.py deleted file mode 100644 index 96e5cd8..0000000 --- a/backtester/test/test_portfolio.py +++ /dev/null @@ -1,32 +0,0 @@ -import unittest -import os -import backtester as bt -from backtester.utils import get_data_dir - - -class TestPortfolio(unittest.TestCase): - """Tests benchmark strategy using synthetic data""" - - def setUp(self): - data_dir = get_data_dir() - synth_file = os.path.join(data_dir, "synthetic_data.csv") - self.port = bt.run(synth_file) - self.report = self.port.create_report() - - def test_last_pct_price(self): - self.assertAlmostEqual(self.report["% Price"].iloc[-1], 1999) - - def test_mean_10_day_return(self): - self.assertAlmostEqual( - self.report["Interval Change"][:10].mean(), 0.31432, delta=0.0001) - - def test_mean_100_day_return(self): - self.assertAlmostEqual( - self.report["Interval Change"][:100].mean(), 0.05229, delta=0.0001) - - def test_last_total(self): - self.assertEqual(self.report["Total Portfolio"].iloc[-1], 2000000000) - - -if __name__ == "__main__": - unittest.main()