diff --git a/bt/algos.py b/bt/algos.py index 027af0f..a041b3a 100644 --- a/bt/algos.py +++ b/bt/algos.py @@ -1621,6 +1621,66 @@ def __call__(self, target): return True +class CorporateActions(Algo): + """ + Used to model dividends and share splits. + + This Algo can be used to model dividend flows and share splits. This + allows the backtest to be run using unadjusted price data and to get + historically correct transactions (position sizes and commissions). + + Security positions are adjusted on dates where a split value other than + 1.0 is given. A value above 1.0 causes the position size to increase + (compared to the previous date) and vice versa. + + On a given date, a position on a security causes a cash inflow (when + long) or outflow (when short) given by the size of the position times + the amount in the `dividends` dataframe. Cash adjustments are made on + the `ex` date given as input, since payment date information is not + easily obtainable. + + This Algo must run at every iteration to be able to change security + positions as required by splits. All dates in the `dividends`and + `splits` dataframes must exist in the price data for the calculations + to work correctly. + + Args: + * dividends (dataframe): dataframe of dividend amounts per unit of + security, indexed by `ex` date. Values should be 0.0 or NaN when + there is no dividend. + * splits (dataframe): dataframe of split ratios from previous date. + Values should be 1.0 or NaN when there is no split. + + """ + + def __init__(self, dividends, splits): + super(CorporateActions, self).__init__() + self.dividends = dividends.fillna(0.0) + self.splits = splits.fillna(1.0) + + def __call__(self, target): + # adjust last position if there is a split + if target.now in self.splits.index: + for c in target.children: + if c in self.splits.columns: + spl = self.splits.loc[target.now, c] + if spl != 1.0: + target.children[c]._position *= spl + + # adjust capital due to dividends + if target.now in self.dividends.index: + div_inflow = 0.0 + for c in target.children: + if c in self.dividends.columns: + div = self.dividends.loc[target.now, c] + if div != 0.0: + div_inflow += div * target.children[c]._position + + target.adjust(div_inflow, flow=False) + + return True + + class CloseDead(Algo): """ Closes all positions for which prices are equal to zero (we assume diff --git a/tests/test_algos.py b/tests/test_algos.py index e6ea784..b1b22bd 100644 --- a/tests/test_algos.py +++ b/tests/test_algos.py @@ -2302,3 +2302,46 @@ def test_hedge_risk_pseudo_over(): assert c1.position == 100 assert c2.position == -5 assert c3.position == -5 + + +def test_corporate_actions(): + dts = pd.date_range("2010-01-01", periods=3) + + data = pd.DataFrame(index=dts, columns=["c1", "c2", "c3"], data=100) + divs = pd.DataFrame(index=dts, columns=["c1", "c2"], data=0.0) + divs.loc[dts[1], "c1"] = 2.0 + splits = pd.DataFrame(index=dts, columns=["c1", "c2"], data=1.0) + splits.loc[dts[2], "c2"] = 10.0 + + algo = algos.CorporateActions(divs, splits) + + s = bt.Strategy("s", children=["c1", "c2", "c3"]) + s.setup(data) + s.adjust(30000) + + s.update(dts[0]) + s.allocate(10000, "c1", update=True) + s.allocate(10000, "c2", update=True) + s.allocate(10000, "c3", update=True) + + assert algo(s) + assert s.capital == 0 + assert s["c1"].position == 100 + assert s["c2"].position == 100 + assert s["c3"].position == 100 + + s.update(dts[1]) + + assert algo(s) + assert s.capital == 100 * 2.0 + assert s["c1"].position == 100 + assert s["c2"].position == 100 + assert s["c3"].position == 100 + + s.update(dts[2]) + + assert algo(s) + assert s.capital == 100 * 2.0 + assert s["c1"].position == 100 + assert s["c2"].position == 100 * 10.0 + assert s["c3"].position == 100