From 48e488d42356c481f74e4dc280167e69e1355295 Mon Sep 17 00:00:00 2001 From: jfkirk Date: Mon, 9 Nov 2015 12:10:12 -0500 Subject: [PATCH 1/5] ENH: Adds versions to asset databases --- tests/test_assets.py | 51 +++++++++++++++++- zipline/assets/asset_writer.py | 94 +++++++++++++++++++++++++++++++++- zipline/assets/assets.py | 14 +++-- zipline/errors.py | 12 +++++ 4 files changed, 164 insertions(+), 7 deletions(-) diff --git a/tests/test_assets.py b/tests/test_assets.py index f081d939..6954db47 100644 --- a/tests/test_assets.py +++ b/tests/test_assets.py @@ -29,25 +29,32 @@ from pandas.tseries.tools import normalize_date from pandas.util.testing import assert_frame_equal from numpy import full +import sqlalchemy as sa from zipline.assets import ( Asset, Equity, Future, AssetFinder, - AssetFinderCachedEquities + AssetFinderCachedEquities, ) - from zipline.assets.futures import ( cme_code_to_month, FutureChain, month_to_cme_code ) +from zipline.assets.asset_writer import ( + check_version_info, + write_version_info, + ASSET_DB_VERSION, + _version_table_schema, +) from zipline.errors import ( SymbolNotFound, MultipleSymbolsFound, SidAssignmentError, RootSymbolNotFound, + AssetDBVersionError, ) from zipline.finance.trading import TradingEnvironment, noop_load from zipline.utils.test_utils import ( @@ -1082,3 +1089,43 @@ class TestFutureChain(TestCase): } for key in codes: self.assertEqual(codes[key], month_to_cme_code(key)) + + +class TestAssetDBVersioning(TestCase): + + def test_check_version(self): + env = TradingEnvironment(load=noop_load) + version_table = env.asset_finder.version + + self.assertTrue(check_version_info(version_table, ASSET_DB_VERSION)) + + # This should fail because the version is too low + with self.assertRaises(AssetDBVersionError): + check_version_info(version_table, ASSET_DB_VERSION - 1) + + # This should fail because the version is too high + with self.assertRaises(AssetDBVersionError): + check_version_info(version_table, ASSET_DB_VERSION + 1) + + def test_write_version(self): + env = TradingEnvironment(load=noop_load) + metadata = sa.MetaData(bind=env.engine) + version_table = _version_table_schema(metadata) + version_table.delete().execute() + + # Assert that the version is not present in the table + self.assertIsNone(sa.select((version_table.c.version,)).scalar()) + + # This should return false because there is no version info in the db + self.assertFalse(check_version_info(version_table, -2)) + + # This return true because the version has been written + write_version_info(version_table, -2) + self.assertTrue(check_version_info(version_table, -2)) + + # Assert that the version is in the table and correct + self.assertEqual(sa.select((version_table.c.version,)).scalar(), -2) + + # Assert that trying to overwrite the version fails + with self.assertRaises(sa.exc.IntegrityError): + write_version_info(version_table, -3) diff --git a/zipline/assets/asset_writer.py b/zipline/assets/asset_writer.py index 5d6fc776..2e0d2ba3 100644 --- a/zipline/assets/asset_writer.py +++ b/zipline/assets/asset_writer.py @@ -25,7 +25,7 @@ import numpy as np from six import with_metaclass import sqlalchemy as sa -from zipline.errors import SidAssignmentError +from zipline.errors import SidAssignmentError, AssetDBVersionError from zipline.assets._assets import Asset SQLITE_MAX_VARIABLE_NUMBER = 999 @@ -165,6 +165,90 @@ def _generate_output_dataframe(data_subset, defaults): return output +# Define a version number for the database generated by these writers +# Increment this version number any time a breaking change is made to the +# schema and readers of the database +ASSET_DB_VERSION = 0 + + +def check_version_info(version_table, expected_version): + """ + Checks for a version value in the version table. + + Parameters + ---------- + version_table : sa.Table + The version table of the asset database + expected_version : int + The expected version of the asset database + + Returns + ------- + bool + True if the version information is present and correct. + False if the version information is missing. + + Raises + ------ + AssetDBVersionError + If the version is in the table and not equal to ASSET_DB_VERSION. + """ + + # Read the version out of the table + version_from_table = sa.select((version_table.c.version,)).scalar() + + # If a version exists in the table... + if version_from_table is not None: + + # Raise an error if the versions do not match + if (version_from_table != expected_version): + raise AssetDBVersionError(db_version=version_from_table, + expected_version=expected_version) + + # A matching version was found + return True + + # No version info found + return False + + +def write_version_info(version_table, version_value): + """ + Inserts the version value in to the version table. + + Parameters + ---------- + version_table : sa.Table + The version table of the asset database + version_value : int + The version to write in to the database + + """ + sa.insert(version_table, values={'version': version_value}).execute() + + +def _version_table_schema(metadata): + return sa.Table( + 'version', + metadata, + sa.Column( + 'id', + sa.Integer, + unique=True, + nullable=False, + primary_key=True, + ), + sa.Column( + 'version', + sa.Integer, + unique=True, + nullable=False, + ), + # This constraint ensures a single entry in this table + sa.CheckConstraint('id <= 1'), + ) + + class AssetDBWriter(with_metaclass(ABCMeta)): """ Class used to write arbitrary data to SQLite database. @@ -373,8 +457,16 @@ class AssetDBWriter(with_metaclass(ABCMeta)): primary_key=True), sa.Column('asset_type', sa.Text), ) + self.version_table = _version_table_schema(metadata) # Create the SQL tables if they do not already exist. metadata.create_all(checkfirst=True) + + # Check if the version is mismatched or, if it is not present, add it + version_found = check_version_info(self.version_table, + ASSET_DB_VERSION) + if not version_found: + write_version_info(self.version_table, ASSET_DB_VERSION) + return metadata def load_data(self): diff --git a/zipline/assets/assets.py b/zipline/assets/assets.py index fee1b35c..f6a62cf8 100644 --- a/zipline/assets/assets.py +++ b/zipline/assets/assets.py @@ -37,6 +37,8 @@ from zipline.assets import ( ) from zipline.assets.asset_writer import ( split_delimited_symbol, + check_version_info, + ASSET_DB_VERSION, ) log = Logger('assets.py') @@ -80,14 +82,18 @@ class AssetFinder(object): self.engine = engine metadata = sa.MetaData(bind=engine) - table_names = ['equities', 'futures_exchanges', 'futures_root_symbols', - 'futures_contracts', 'asset_router'] + table_names = ['version', 'equities', 'futures_exchanges', + 'futures_root_symbols', 'futures_contracts', + 'asset_router'] metadata.reflect(only=table_names) for table_name in table_names: setattr(self, table_name, metadata.tables[table_name]) - # Cache for lookup of assets by sid, the objects in the asset lookp may - # be shared with the results from equity and future lookup caches. + # Check the version info of the db for compatibility + check_version_info(self.version, ASSET_DB_VERSION) + + # Cache for lookup of assets by sid, the objects in the asset lookup + # may be shared with the results from equity and future lookup caches. # # The top level cache exists to minimize lookups on the asset type # routing. diff --git a/zipline/errors.py b/zipline/errors.py index eacfcc64..aab1f414 100644 --- a/zipline/errors.py +++ b/zipline/errors.py @@ -439,3 +439,15 @@ class PositionTrackerMissingAssetFinder(ZiplineError): "not have an AssetFinder. This may be caused by a failure to properly " "de-serialize a TradingAlgorithm." ) + + +class AssetDBVersionError(ZiplineError): + """ + Raised by an AssetDBWriter or AssetFinder if the version number in the + versions table does not match the ASSET_DB_VERSION in asset_writer.py. + """ + msg = ( + "The existing Asset database has an incorrect version: {db_version}. " + "Expected version: {expected_version}. Try rebuilding your asset " + "database or updating your version of Zipline." + ) From 6aac54544e95799d8551da27de94fc77720e02f8 Mon Sep 17 00:00:00 2001 From: jfkirk Date: Mon, 9 Nov 2015 14:37:01 -0500 Subject: [PATCH 2/5] MAINT: Only write asset db version on db creation --- tests/test_assets.py | 13 +++++---- zipline/assets/asset_writer.py | 52 ++++++++++++++++++++-------------- zipline/assets/assets.py | 5 +--- 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/tests/test_assets.py b/tests/test_assets.py index 6954db47..3ca3b4ae 100644 --- a/tests/test_assets.py +++ b/tests/test_assets.py @@ -1097,7 +1097,8 @@ class TestAssetDBVersioning(TestCase): env = TradingEnvironment(load=noop_load) version_table = env.asset_finder.version - self.assertTrue(check_version_info(version_table, ASSET_DB_VERSION)) + # This should not raise an error + check_version_info(version_table, ASSET_DB_VERSION) # This should fail because the version is too low with self.assertRaises(AssetDBVersionError): @@ -1116,12 +1117,14 @@ class TestAssetDBVersioning(TestCase): # Assert that the version is not present in the table self.assertIsNone(sa.select((version_table.c.version,)).scalar()) - # This should return false because there is no version info in the db - self.assertFalse(check_version_info(version_table, -2)) + # This should fail because the table has no version info and is, + # therefore, consdered v0 + with self.assertRaises(AssetDBVersionError): + check_version_info(version_table, -2) - # This return true because the version has been written + # This should not raise an error because the version has been written write_version_info(version_table, -2) - self.assertTrue(check_version_info(version_table, -2)) + check_version_info(version_table, -2) # Assert that the version is in the table and correct self.assertEqual(sa.select((version_table.c.version,)).scalar(), -2) diff --git a/zipline/assets/asset_writer.py b/zipline/assets/asset_writer.py index 2e0d2ba3..7d4b6011 100644 --- a/zipline/assets/asset_writer.py +++ b/zipline/assets/asset_writer.py @@ -33,6 +33,9 @@ SQLITE_MAX_VARIABLE_NUMBER = 999 # Define a namedtuple for use with the load_data and _load_data methods AssetData = namedtuple('AssetData', 'equities futures exchanges root_symbols') +# A list of the names of all tables in the assets db +table_names = ['version', 'equities', 'futures_exchanges', + 'futures_root_symbols', 'futures_contracts', 'asset_router'] # Default values for the equities DataFrame _equities_defaults = { @@ -182,12 +185,6 @@ def check_version_info(version_table, expected_version): expected_version : int The expected version of the asset database - Returns - ------- - bool - True if the version information is present and correct. - False if the version information is missing. - Raises ------ AssetDBVersionError @@ -197,19 +194,14 @@ def check_version_info(version_table, expected_version): # Read the version out of the table version_from_table = sa.select((version_table.c.version,)).scalar() - # If a version exists in the table... - if version_from_table is not None: + # A db without a version is considered v0 + if version_from_table is None: + version_from_table = 0 - # Raise an error if the versions do not match - if (version_from_table != expected_version): - raise AssetDBVersionError(db_version=version_from_table, - expected_version=expected_version) - - # A matching version was found - return True - - # No version info found - return False + # Raise an error if the versions do not match + if (version_from_table != expected_version): + raise AssetDBVersionError(db_version=version_from_table, + expected_version=expected_version) def write_version_info(version_table, version_value): @@ -352,6 +344,21 @@ class AssetDBWriter(with_metaclass(ABCMeta)): def _write_equities(self, equities, bind): self._write_assets(equities, self.equities, 'equity', bind) + def check_for_tables(self, engine): + """ + Checks if any tables are present in the current assets database. + + Returns + ------- + bool + True if any tables are present, otherwise False. + """ + conn = engine.connect() + for table_name in table_names: + if engine.dialect.has_table(conn, table_name): + return True + return False + def init_db(self, engine): """Connect to database and create tables. @@ -364,6 +371,8 @@ class AssetDBWriter(with_metaclass(ABCMeta)): """ metadata = sa.MetaData(bind=engine) + tables_already_exist = self.check_for_tables(engine=engine) + self.equities = sa.Table( 'equities', metadata, @@ -461,10 +470,9 @@ class AssetDBWriter(with_metaclass(ABCMeta)): # Create the SQL tables if they do not already exist. metadata.create_all(checkfirst=True) - # Check if the version is mismatched or, if it is not present, add it - version_found = check_version_info(self.version_table, - ASSET_DB_VERSION) - if not version_found: + if tables_already_exist: + check_version_info(self.version_table, ASSET_DB_VERSION) + else: write_version_info(self.version_table, ASSET_DB_VERSION) return metadata diff --git a/zipline/assets/assets.py b/zipline/assets/assets.py index f6a62cf8..f08c24d6 100644 --- a/zipline/assets/assets.py +++ b/zipline/assets/assets.py @@ -39,6 +39,7 @@ from zipline.assets.asset_writer import ( split_delimited_symbol, check_version_info, ASSET_DB_VERSION, + table_names, ) log = Logger('assets.py') @@ -81,10 +82,6 @@ class AssetFinder(object): self.engine = engine metadata = sa.MetaData(bind=engine) - - table_names = ['version', 'equities', 'futures_exchanges', - 'futures_root_symbols', 'futures_contracts', - 'asset_router'] metadata.reflect(only=table_names) for table_name in table_names: setattr(self, table_name, metadata.tables[table_name]) From 85dd4b70dd570bf9ba157ee459020d47d8ccdb20 Mon Sep 17 00:00:00 2001 From: jfkirk Date: Tue, 10 Nov 2015 10:51:53 -0500 Subject: [PATCH 3/5] MAINT: Renames 'version' table to 'version_info' for clarity --- tests/test_assets.py | 2 +- zipline/assets/asset_writer.py | 4 ++-- zipline/assets/assets.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_assets.py b/tests/test_assets.py index 3ca3b4ae..1c8b6a33 100644 --- a/tests/test_assets.py +++ b/tests/test_assets.py @@ -1095,7 +1095,7 @@ class TestAssetDBVersioning(TestCase): def test_check_version(self): env = TradingEnvironment(load=noop_load) - version_table = env.asset_finder.version + version_table = env.asset_finder.version_info # This should not raise an error check_version_info(version_table, ASSET_DB_VERSION) diff --git a/zipline/assets/asset_writer.py b/zipline/assets/asset_writer.py index 7d4b6011..543b53c2 100644 --- a/zipline/assets/asset_writer.py +++ b/zipline/assets/asset_writer.py @@ -34,7 +34,7 @@ SQLITE_MAX_VARIABLE_NUMBER = 999 AssetData = namedtuple('AssetData', 'equities futures exchanges root_symbols') # A list of the names of all tables in the assets db -table_names = ['version', 'equities', 'futures_exchanges', +table_names = ['version_info', 'equities', 'futures_exchanges', 'futures_root_symbols', 'futures_contracts', 'asset_router'] # Default values for the equities DataFrame @@ -221,7 +221,7 @@ def write_version_info(version_table, version_value): def _version_table_schema(metadata): return sa.Table( - 'version', + 'version_info', metadata, sa.Column( 'id', diff --git a/zipline/assets/assets.py b/zipline/assets/assets.py index f08c24d6..c1d593bd 100644 --- a/zipline/assets/assets.py +++ b/zipline/assets/assets.py @@ -87,7 +87,7 @@ class AssetFinder(object): setattr(self, table_name, metadata.tables[table_name]) # Check the version info of the db for compatibility - check_version_info(self.version, ASSET_DB_VERSION) + check_version_info(self.version_info, ASSET_DB_VERSION) # Cache for lookup of assets by sid, the objects in the asset lookup # may be shared with the results from equity and future lookup caches. From e1e160a128d7d68d409c4a87f6930cbfe29c6e2d Mon Sep 17 00:00:00 2001 From: jfkirk Date: Tue, 10 Nov 2015 12:24:49 -0500 Subject: [PATCH 4/5] MAINT: Factors-out asset db table schemas for cleanliness --- zipline/assets/asset_writer.py | 228 +++++++++++++++++++-------------- zipline/assets/assets.py | 6 +- 2 files changed, 132 insertions(+), 102 deletions(-) diff --git a/zipline/assets/asset_writer.py b/zipline/assets/asset_writer.py index 543b53c2..328b02e1 100644 --- a/zipline/assets/asset_writer.py +++ b/zipline/assets/asset_writer.py @@ -33,10 +33,6 @@ SQLITE_MAX_VARIABLE_NUMBER = 999 # Define a namedtuple for use with the load_data and _load_data methods AssetData = namedtuple('AssetData', 'equities futures exchanges root_symbols') -# A list of the names of all tables in the assets db -table_names = ['version_info', 'equities', 'futures_exchanges', - 'futures_root_symbols', 'futures_contracts', 'asset_router'] - # Default values for the equities DataFrame _equities_defaults = { 'symbol': None, @@ -219,6 +215,121 @@ def write_version_info(version_table, version_value): sa.insert(version_table, values={'version': version_value}).execute() +# A list of the names of all tables in the assets db +asset_db_table_names = ['version_info', 'equities', 'futures_exchanges', + 'futures_root_symbols', 'futures_contracts', + 'asset_router'] + + +def _equities_table_schema(metadata): + return sa.Table( + 'equities', + metadata, + sa.Column( + 'sid', + sa.Integer, + unique=True, + nullable=False, + primary_key=True, + ), + sa.Column('symbol', sa.Text), + sa.Column('company_symbol', sa.Text, index=True), + sa.Column('share_class_symbol', sa.Text), + sa.Column('fuzzy_symbol', sa.Text, index=True), + sa.Column('asset_name', sa.Text), + sa.Column('start_date', sa.Integer, default=0, nullable=False), + sa.Column('end_date', sa.Integer, nullable=False), + sa.Column('first_traded', sa.Integer, nullable=False), + sa.Column('exchange', sa.Text), + ) + + +def _futures_exchanges_schema(metadata): + return sa.Table( + 'futures_exchanges', + metadata, + sa.Column( + 'exchange', + sa.Text, + unique=True, + nullable=False, + primary_key=True, + ), + sa.Column('timezone', sa.Text), + ) + + +def _futures_root_symbols_schema(metadata, futures_exchanges): + return sa.Table( + 'futures_root_symbols', + metadata, + sa.Column( + 'root_symbol', + sa.Text, + unique=True, + nullable=False, + primary_key=True, + ), + sa.Column('root_symbol_id', sa.Integer), + sa.Column('sector', sa.Text), + sa.Column('description', sa.Text), + sa.Column( + 'exchange', + sa.Text, + sa.ForeignKey(futures_exchanges.c.exchange), + ), + ) + + +def _futures_contracts_schema(metadata, futures_root_symbols, + futures_exchanges): + return sa.Table( + 'futures_contracts', + metadata, + sa.Column( + 'sid', + sa.Integer, + unique=True, + nullable=False, + primary_key=True, + ), + sa.Column('symbol', sa.Text, unique=True, index=True), + sa.Column( + 'root_symbol', + sa.Text, + sa.ForeignKey(futures_root_symbols.c.root_symbol), + index=True + ), + sa.Column('asset_name', sa.Text), + sa.Column('start_date', sa.Integer, default=0, nullable=False), + sa.Column('end_date', sa.Integer, nullable=False), + sa.Column('first_traded', sa.Integer, nullable=False), + sa.Column( + 'exchange', + sa.Text, + sa.ForeignKey(futures_exchanges.c.exchange), + ), + sa.Column('notice_date', sa.Integer, nullable=False), + sa.Column('expiration_date', sa.Integer, nullable=False), + sa.Column('auto_close_date', sa.Integer, nullable=False), + sa.Column('contract_multiplier', sa.Float), + ) + + +def _asset_router_schema(metadata): + return sa.Table( + 'asset_router', + metadata, + sa.Column( + 'sid', + sa.Integer, + unique=True, + nullable=False, + primary_key=True), + sa.Column('asset_type', sa.Text), + ) + + def _version_table_schema(metadata): return sa.Table( 'version_info', @@ -354,7 +465,7 @@ class AssetDBWriter(with_metaclass(ABCMeta)): True if any tables are present, otherwise False. """ conn = engine.connect() - for table_name in table_names: + for table_name in asset_db_table_names: if engine.dialect.has_table(conn, table_name): return True return False @@ -373,107 +484,26 @@ class AssetDBWriter(with_metaclass(ABCMeta)): tables_already_exist = self.check_for_tables(engine=engine) - self.equities = sa.Table( - 'equities', - metadata, - sa.Column( - 'sid', - sa.Integer, - unique=True, - nullable=False, - primary_key=True, - ), - sa.Column('symbol', sa.Text), - sa.Column('company_symbol', sa.Text, index=True), - sa.Column('share_class_symbol', sa.Text), - sa.Column('fuzzy_symbol', sa.Text, index=True), - sa.Column('asset_name', sa.Text), - sa.Column('start_date', sa.Integer, default=0, nullable=False), - sa.Column('end_date', sa.Integer, nullable=False), - sa.Column('first_traded', sa.Integer, nullable=False), - sa.Column('exchange', sa.Text), + self.equities = _equities_table_schema(metadata) + self.futures_exchanges = _futures_exchanges_schema(metadata) + self.futures_root_symbols = _futures_root_symbols_schema( + metadata=metadata, + futures_exchanges=self.futures_exchanges, ) - self.futures_exchanges = sa.Table( - 'futures_exchanges', - metadata, - sa.Column( - 'exchange', - sa.Text, - unique=True, - nullable=False, - primary_key=True, - ), - sa.Column('timezone', sa.Text), + self.futures_contracts = _futures_contracts_schema( + metadata=metadata, + futures_root_symbols=self.futures_root_symbols, + futures_exchanges=self.futures_exchanges, ) - self.futures_root_symbols = sa.Table( - 'futures_root_symbols', - metadata, - sa.Column( - 'root_symbol', - sa.Text, - unique=True, - nullable=False, - primary_key=True, - ), - sa.Column('root_symbol_id', sa.Integer), - sa.Column('sector', sa.Text), - sa.Column('description', sa.Text), - sa.Column( - 'exchange', - sa.Text, - sa.ForeignKey(self.futures_exchanges.c.exchange), - ), - ) - self.futures_contracts = sa.Table( - 'futures_contracts', - metadata, - sa.Column( - 'sid', - sa.Integer, - unique=True, - nullable=False, - primary_key=True, - ), - sa.Column('symbol', sa.Text, unique=True, index=True), - sa.Column( - 'root_symbol', - sa.Text, - sa.ForeignKey(self.futures_root_symbols.c.root_symbol), - index=True - ), - sa.Column('asset_name', sa.Text), - sa.Column('start_date', sa.Integer, default=0, nullable=False), - sa.Column('end_date', sa.Integer, nullable=False), - sa.Column('first_traded', sa.Integer, nullable=False), - sa.Column( - 'exchange', - sa.Text, - sa.ForeignKey(self.futures_exchanges.c.exchange), - ), - sa.Column('notice_date', sa.Integer, nullable=False), - sa.Column('expiration_date', sa.Integer, nullable=False), - sa.Column('auto_close_date', sa.Integer, nullable=False), - sa.Column('contract_multiplier', sa.Float), - ) - self.asset_router = sa.Table( - 'asset_router', - metadata, - sa.Column( - 'sid', - sa.Integer, - unique=True, - nullable=False, - primary_key=True), - sa.Column('asset_type', sa.Text), - ) - self.version_table = _version_table_schema(metadata) + self.asset_router = _asset_router_schema(metadata) + self.version_info = _version_table_schema(metadata) # Create the SQL tables if they do not already exist. metadata.create_all(checkfirst=True) if tables_already_exist: - check_version_info(self.version_table, ASSET_DB_VERSION) + check_version_info(self.version_info, ASSET_DB_VERSION) else: - write_version_info(self.version_table, ASSET_DB_VERSION) + write_version_info(self.version_info, ASSET_DB_VERSION) return metadata diff --git a/zipline/assets/assets.py b/zipline/assets/assets.py index c1d593bd..8d059072 100644 --- a/zipline/assets/assets.py +++ b/zipline/assets/assets.py @@ -39,7 +39,7 @@ from zipline.assets.asset_writer import ( split_delimited_symbol, check_version_info, ASSET_DB_VERSION, - table_names, + asset_db_table_names, ) log = Logger('assets.py') @@ -82,8 +82,8 @@ class AssetFinder(object): self.engine = engine metadata = sa.MetaData(bind=engine) - metadata.reflect(only=table_names) - for table_name in table_names: + metadata.reflect(only=asset_db_table_names) + for table_name in asset_db_table_names: setattr(self, table_name, metadata.tables[table_name]) # Check the version info of the db for compatibility From 7a1abc88f8fa96ef0cd34955b10d789e9dddbecf Mon Sep 17 00:00:00 2001 From: jfkirk Date: Thu, 12 Nov 2015 14:44:20 -0500 Subject: [PATCH 5/5] DOC: Adds whatsnew for asset db versioning --- docs/source/whatsnew/0.8.4.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/whatsnew/0.8.4.txt b/docs/source/whatsnew/0.8.4.txt index 6acb0c89..fb053b13 100644 --- a/docs/source/whatsnew/0.8.4.txt +++ b/docs/source/whatsnew/0.8.4.txt @@ -46,7 +46,8 @@ then directs `lookup_symbol` to these dictionaries to find matching equities Maintenance and Refactorings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -None +* Asset databases now contain version information to ensure compatibility + with current Zipline version (:issue:`815`). Build ~~~~~