Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CorporateActions: new algo to model dividends and splits #382

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions bt/algos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions tests/test_algos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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