Skip to content
Merged
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
14 changes: 14 additions & 0 deletions pypfopt/discrete_allocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ def __init__(
if ``latest_prices`` isn't a series
ValueError
if ``short_ratio < 0``
ValueError
if a weighted ticker has no price in ``latest_prices``, or such
a price is not positive and finite. An empty ``weights`` dict is
accepted here (for utility methods) but rejected by the
allocation methods.
"""
if not isinstance(weights, dict):
raise TypeError("weights should be a dictionary of {ticker: weight}")
Expand All @@ -95,6 +100,11 @@ def __init__(
raise ValueError("total_portfolio_value must be greater than zero")
if short_ratio is not None and short_ratio < 0:
raise ValueError("short_ratio must be non-negative")
missing = [t for t in weights if t not in latest_prices.index]
if missing:
raise ValueError(f"latest_prices is missing tickers: {missing}")
if any(not np.isfinite(latest_prices[t]) or latest_prices[t] <= 0 for t in weights):
raise ValueError("latest_prices must be positive and finite")

# Drop any companies with negligible weights. Use a tuple because order matters.
self.weights = list(weights.items())
Expand Down Expand Up @@ -173,6 +183,8 @@ def greedy_portfolio(self, reinvest=False, verbose=False):
the number of shares of each ticker that should be purchased,
along with the amount of funds leftover.
"""
if len(self.weights) == 0:
raise ValueError("weights must contain at least one ticker")
# Sort in descending order of weight
self.weights.sort(key=lambda x: x[1], reverse=True)

Expand Down Expand Up @@ -319,6 +331,8 @@ def lp_portfolio(self, reinvest=False, verbose=False, solver=None):
)
# end todo

if len(self.weights) == 0:
raise ValueError("weights must contain at least one ticker")
if any([w < 0 for _, w in self.weights]):
longs = {t: w for t, w in self.weights if w >= 0}
shorts = {t: -w for t, w in self.weights if w < 0}
Expand Down
32 changes: 32 additions & 0 deletions tests/test_discrete_allocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,3 +481,35 @@ def test_allocation_errors():
latest_prices.iloc[0] = np.nan
with pytest.raises(TypeError):
DiscreteAllocation(w, latest_prices)


def test_discrete_allocation_empty_weights_rejected_on_allocate():
da = DiscreteAllocation({}, pd.Series({"A": 10.0}), total_portfolio_value=100)
with pytest.raises(ValueError, match="at least one ticker"):
da.greedy_portfolio()
with pytest.raises(ValueError, match="at least one ticker"):
da.lp_portfolio()


def test_discrete_allocation_rejects_missing_prices():
with pytest.raises(ValueError, match="missing tickers"):
DiscreteAllocation(
{"A": 0.5, "C": 0.5}, pd.Series({"A": 10.0}), total_portfolio_value=100
)


@pytest.mark.parametrize("bad_price", [0.0, -5.0, float("inf")])
def test_discrete_allocation_rejects_nonpositive_or_infinite_prices(bad_price):
with pytest.raises(ValueError, match="positive and finite"):
DiscreteAllocation(
{"A": 0.5, "B": 0.5},
pd.Series({"A": 10.0, "B": bad_price}),
total_portfolio_value=100,
)


def test_discrete_allocation_ignores_prices_of_unweighted_assets():
da = DiscreteAllocation(
{"A": 1.0}, pd.Series({"A": 10.0, "B": 0.0}), total_portfolio_value=100
)
assert da.greedy_portfolio()[0] == {"A": 10}
Loading