mirror of
https://github.com/wassname/catalyst.git
synced 2026-09-09 11:19:23 +08:00
ENH/BUG: Modeling API enhancements.
- Fixes an error where Modeling API data known as of the close of `day N` would be shown to algorithms during `before_trading_start` as of the close of the same day. Algorithms should now only receive data during `before_trading_start/handle_data` that was known as of the simulation time at which the function would be called. - All Term instances now have a `mask` attribute that must be a `Filter` or an instance of `AssetExists()`. `mask` can be used to specify that a Factor should be computed in a manner that ignores the values that were not `True` in the mask. - Changed the interface for `FFCLoader.load_adjusted_array` and `Term._compute` from `(columns, mask)`, with mask as a DataFrame, to `(columns, dates, assets, mask)`, where mask is a numpy array. This is primarily to avoid having to reconstruct extra DataFrames when using masks produced by non `AssetExists` filters. - Adds `BoundColumn.latest`, which gives the most-recently-known value of a column.
This commit is contained in:
+113
-75
@@ -7,8 +7,8 @@ from itertools import product
|
||||
|
||||
from numpy import (
|
||||
full,
|
||||
isnan,
|
||||
nan,
|
||||
zeros,
|
||||
)
|
||||
from numpy.testing import assert_array_equal
|
||||
from pandas import (
|
||||
@@ -191,33 +191,21 @@ class ConstantInputTestCase(TestCase):
|
||||
expected_high_low = 3.0 * (constants[high] - constants[low])
|
||||
assert_frame_equal(
|
||||
high_low_result,
|
||||
DataFrame(
|
||||
expected_high_low,
|
||||
index=dates,
|
||||
columns=self.assets,
|
||||
)
|
||||
DataFrame(expected_high_low, index=dates, columns=self.assets),
|
||||
)
|
||||
|
||||
open_close_result = results['open_close'].unstack()
|
||||
expected_open_close = 3.0 * (constants[open] - constants[close])
|
||||
assert_frame_equal(
|
||||
open_close_result,
|
||||
DataFrame(
|
||||
expected_open_close,
|
||||
index=dates,
|
||||
columns=self.assets,
|
||||
)
|
||||
DataFrame(expected_open_close, index=dates, columns=self.assets),
|
||||
)
|
||||
|
||||
avg_result = results['avg'].unstack()
|
||||
expected_avg = (expected_high_low + expected_open_close) / 2.0
|
||||
assert_frame_equal(
|
||||
avg_result,
|
||||
DataFrame(
|
||||
expected_avg,
|
||||
index=dates,
|
||||
columns=self.assets,
|
||||
)
|
||||
DataFrame(expected_avg, index=dates, columns=self.assets),
|
||||
)
|
||||
|
||||
|
||||
@@ -338,20 +326,18 @@ class SyntheticBcolzTestCase(TestCase):
|
||||
def setUpClass(cls):
|
||||
cls.first_asset_start = Timestamp('2015-04-01', tz='UTC')
|
||||
cls.env = TradingEnvironment()
|
||||
cls.trading_day = cls.env.trading_day
|
||||
cls.trading_day = day = cls.env.trading_day
|
||||
cls.calendar = date_range('2015', '2015-08', tz='UTC', freq=day)
|
||||
|
||||
cls.asset_info = make_rotating_asset_info(
|
||||
num_assets=6,
|
||||
first_start=cls.first_asset_start,
|
||||
frequency=cls.trading_day,
|
||||
frequency=day,
|
||||
periods_between_starts=4,
|
||||
asset_lifetime=8,
|
||||
)
|
||||
cls.last_asset_end = cls.asset_info['end_date'].max()
|
||||
cls.all_assets = cls.asset_info.index
|
||||
cls.all_dates = date_range(
|
||||
start=cls.first_asset_start,
|
||||
end=cls.asset_info['end_date'].max(),
|
||||
freq=cls.trading_day,
|
||||
)
|
||||
|
||||
cls.env.write_data(equities_df=cls.asset_info)
|
||||
cls.finder = cls.env.asset_finder
|
||||
@@ -359,34 +345,74 @@ class SyntheticBcolzTestCase(TestCase):
|
||||
cls.temp_dir = TempDirectory()
|
||||
cls.temp_dir.create()
|
||||
|
||||
cls.writer = SyntheticDailyBarWriter(
|
||||
asset_info=cls.asset_info[['start_date', 'end_date']],
|
||||
calendar=cls.all_dates,
|
||||
)
|
||||
table = cls.writer.write(
|
||||
cls.temp_dir.getpath('testdata.bcolz'),
|
||||
cls.all_dates,
|
||||
cls.all_assets,
|
||||
)
|
||||
try:
|
||||
cls.writer = SyntheticDailyBarWriter(
|
||||
asset_info=cls.asset_info[['start_date', 'end_date']],
|
||||
calendar=cls.calendar,
|
||||
)
|
||||
table = cls.writer.write(
|
||||
cls.temp_dir.getpath('testdata.bcolz'),
|
||||
cls.calendar,
|
||||
cls.all_assets,
|
||||
)
|
||||
|
||||
cls.ffc_loader = USEquityPricingLoader(
|
||||
BcolzDailyBarReader(table),
|
||||
NullAdjustmentReader(),
|
||||
)
|
||||
cls.ffc_loader = USEquityPricingLoader(
|
||||
BcolzDailyBarReader(table),
|
||||
NullAdjustmentReader(),
|
||||
)
|
||||
except:
|
||||
cls.temp_dir.cleanup()
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
del cls.env
|
||||
cls.temp_dir.cleanup()
|
||||
|
||||
def write_nans(self, df):
|
||||
"""
|
||||
Write nans to the locations in data corresponding to the (date, asset)
|
||||
pairs for which we wouldn't have data for `asset` on `date` in a
|
||||
backtest.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
df : pd.DataFrame
|
||||
A DataFrame with a DatetimeIndex as index and an object index of
|
||||
Assets as columns.
|
||||
|
||||
This means that we write nans for dates after an asset's end_date and
|
||||
**on or before** an asset's start_date. The assymetry here is because
|
||||
of the fact that, on the morning of an asset's first date, we haven't
|
||||
yet seen any trades for that asset, so we wouldn't be able to show any
|
||||
useful data to the user.
|
||||
"""
|
||||
# Mask out with nans all the dates on which each asset didn't exist
|
||||
index = df.index
|
||||
min_, max_ = index[[0, -1]]
|
||||
for asset in df.columns:
|
||||
if asset.start_date >= min_:
|
||||
start = index.get_loc(asset.start_date, method='bfill')
|
||||
df.loc[:start + 1, asset] = nan # +1 to overwrite start_date
|
||||
if asset.end_date <= max_:
|
||||
end = index.get_loc(asset.end_date)
|
||||
df.ix[end + 1:, asset] = nan # +1 to *not* overwrite end_date
|
||||
|
||||
def test_SMA(self):
|
||||
engine = SimpleFFCEngine(
|
||||
self.ffc_loader,
|
||||
self.env.trading_days,
|
||||
self.finder,
|
||||
)
|
||||
dates, assets = self.all_dates, self.all_assets
|
||||
window_length = 5
|
||||
assets = self.all_assets
|
||||
dates = date_range(
|
||||
self.first_asset_start + self.trading_day,
|
||||
self.last_asset_end,
|
||||
freq=self.trading_day,
|
||||
)
|
||||
dates_to_test = dates[window_length:]
|
||||
|
||||
SMA = SimpleMovingAverage(
|
||||
inputs=(USEquityPricing.close,),
|
||||
window_length=window_length,
|
||||
@@ -394,27 +420,30 @@ class SyntheticBcolzTestCase(TestCase):
|
||||
|
||||
results = engine.factor_matrix(
|
||||
{'sma': SMA},
|
||||
dates[window_length],
|
||||
dates[-1],
|
||||
dates_to_test[0],
|
||||
dates_to_test[-1],
|
||||
)
|
||||
raw_closes = self.writer.expected_values_2d(dates, assets, 'close')
|
||||
expected_sma_result = rolling_mean(
|
||||
raw_closes,
|
||||
|
||||
# Shift back the raw inputs by a trading day because we expect our
|
||||
# computed results to be computed using values anchored on the
|
||||
# **previous** day's data.
|
||||
expected_raw = rolling_mean(
|
||||
self.writer.expected_values_2d(
|
||||
dates - self.trading_day, assets, 'close',
|
||||
),
|
||||
window_length,
|
||||
min_periods=1,
|
||||
)
|
||||
expected_sma_result[isnan(raw_closes)] = nan
|
||||
expected_sma_result = expected_sma_result[window_length:]
|
||||
|
||||
sma_result = results['sma'].unstack()
|
||||
assert_frame_equal(
|
||||
sma_result,
|
||||
DataFrame(
|
||||
expected_sma_result,
|
||||
index=dates[window_length:],
|
||||
columns=assets,
|
||||
),
|
||||
expected = DataFrame(
|
||||
# Truncate off the extra rows needed to compute the SMAs.
|
||||
expected_raw[window_length:],
|
||||
index=dates_to_test, # dates_to_test is dates[window_length:]
|
||||
columns=self.finder.retrieve_all(assets),
|
||||
)
|
||||
self.write_nans(expected)
|
||||
result = results['sma'].unstack()
|
||||
assert_frame_equal(result, expected)
|
||||
|
||||
def test_drawdown(self):
|
||||
# The monotonically-increasing data produced by SyntheticDailyBarWriter
|
||||
@@ -427,8 +456,15 @@ class SyntheticBcolzTestCase(TestCase):
|
||||
self.env.trading_days,
|
||||
self.finder,
|
||||
)
|
||||
dates, assets = self.all_dates, self.all_assets
|
||||
window_length = 5
|
||||
assets = self.all_assets
|
||||
dates = date_range(
|
||||
self.first_asset_start + self.trading_day,
|
||||
self.last_asset_end,
|
||||
freq=self.trading_day,
|
||||
)
|
||||
dates_to_test = dates[window_length:]
|
||||
|
||||
drawdown = MaxDrawdown(
|
||||
inputs=(USEquityPricing.close,),
|
||||
window_length=window_length,
|
||||
@@ -436,32 +472,27 @@ class SyntheticBcolzTestCase(TestCase):
|
||||
|
||||
results = engine.factor_matrix(
|
||||
{'drawdown': drawdown},
|
||||
dates[window_length],
|
||||
dates[-1],
|
||||
dates_to_test[0],
|
||||
dates_to_test[-1],
|
||||
)
|
||||
|
||||
dd_result = results['drawdown']
|
||||
|
||||
# We expect NaNs when the asset was undefined, otherwise 0 everywhere,
|
||||
# since the input is always increasing.
|
||||
expected = self.writer.expected_values_2d(dates, assets, 'close')
|
||||
expected[~isnan(expected)] = 0
|
||||
expected = expected[window_length:]
|
||||
|
||||
assert_frame_equal(
|
||||
dd_result.unstack(),
|
||||
DataFrame(
|
||||
expected,
|
||||
index=dates[window_length:],
|
||||
columns=assets,
|
||||
),
|
||||
expected = DataFrame(
|
||||
data=zeros((len(dates_to_test), len(assets)), dtype=float),
|
||||
index=dates_to_test,
|
||||
columns=self.finder.retrieve_all(assets),
|
||||
)
|
||||
self.write_nans(expected)
|
||||
result = results['drawdown'].unstack()
|
||||
|
||||
assert_frame_equal(expected, result)
|
||||
|
||||
|
||||
class MultiColumnLoaderTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.assets = [1, 2, 3]
|
||||
self.dates = date_range('2014-01-01', '2014-02-01', freq='D', tz='UTC')
|
||||
self.dates = date_range('2014-01', '2014-03', freq='D', tz='UTC')
|
||||
|
||||
asset_info = make_simple_asset_info(
|
||||
self.assets,
|
||||
@@ -475,6 +506,11 @@ class MultiColumnLoaderTestCase(TestCase):
|
||||
def test_engine_with_multicolumn_loader(self):
|
||||
open_, close = USEquityPricing.open, USEquityPricing.close
|
||||
|
||||
# Test for thirty days up to the second to last day that we think all
|
||||
# the assets existed. If we test the last day of our calendar, no
|
||||
# assets will be in our output, because their end dates are all
|
||||
dates_to_test = self.dates[-32:-2]
|
||||
|
||||
loader = MultiColumnLoader({
|
||||
open_: ConstantLoader(dates=self.dates,
|
||||
assets=self.assets,
|
||||
@@ -489,12 +525,14 @@ class MultiColumnLoaderTestCase(TestCase):
|
||||
factor = RollingSumDifference()
|
||||
|
||||
result = engine.factor_matrix({'f': factor},
|
||||
self.dates[2],
|
||||
self.dates[-1])
|
||||
dates_to_test[0],
|
||||
dates_to_test[-1])
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual({'f'}, set(result.columns))
|
||||
|
||||
# (close - open) * window = (1 - 2) * 3 = -3
|
||||
# skipped 2 from the start, so that the window is full
|
||||
check_arrays(result['f'],
|
||||
Series([-3] * len(self.assets) * (len(self.dates) - 2)))
|
||||
result_index = self.assets * len(dates_to_test)
|
||||
result_shape = (len(result_index),)
|
||||
check_arrays(
|
||||
result['f'],
|
||||
Series(index=result_index, data=full(result_shape, -3)),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user