Weights & Short Positions¶
Weights are just a pandas.Series indexed by asset name, summing to 1. PortPy allows negative entries by default, a negative weight is a short position, and every metric downstream (returns, risk, drawdowns, covariance decomposition, ...) treats it exactly as you'd expect: a negative-weighted asset's losses become the portfolio's gains, and vice versa.
Building weights¶
from portpy.core import equal_weights, normalize_weights
equal_weights(["AAPL", "MSFT", "GOOGL"])
# AAPL 0.333333
# MSFT 0.333333
# GOOGL 0.333333
normalize_weights({"AAPL": 0.6, "MSFT": 0.3, "XOM": -0.2})
# AAPL 0.857143
# MSFT 0.428571
# XOM -0.285714
# rescaled to sum to 1 - negative entries are preserved as shorts
normalize_weights is what Portfolio.__init__ and .set_weights() call internally. It accepts a Series, dict, or plain array/list (with a matching names list), validates there are no NaNs and the weights don't sum to zero, and rescales the result to sum to exactly 1.
Long-only validation¶
Pass allow_negative=False to reject a book with shorts - useful if you're feeding weights into something that assumes long-only (e.g. a future optimizer with a long-only constraint):
normalize_weights({"AAPL": 0.6, "MSFT": 0.3, "XOM": -0.2}, allow_negative=False)
# ValueError: Negative weights are not allowed here (long-only constraint).
A long/short portfolio end to end¶
from portpy import Portfolio
weights = {
"AAPL": 0.5,
"MSFT": 0.5,
"XOM": -0.2, # short - a hedge, not held long
}
portfolio = Portfolio(prices, weights=weights, name="Long/Short Tech")
portfolio.weights
# AAPL 0.625
# MSFT 0.625
# XOM -0.250
# Name: weight, dtype: float64
Notice the weights were rescaled so they still sum to 1 (net exposure), while the relative proportions you specified (0.5 : 0.5 : -0.2) are preserved. .returns() then combines each asset's return by its (possibly negative) weight every period - no special-casing needed anywhere else in the library. portpy.metrics.covariance.portfolio_variance and others use the same signed weight vector, so risk decomposition (marginal/component contribution to risk) correctly attributes risk reduction to a short that's negatively correlated with the rest of the book.
Changing weights later¶
set_weights just replaces the static weight vector used by every subsequent .metrics call - it doesn't generate a trade list or account for turnover on its own. For that, see .models.management.rebalance, which turns a target allocation (e.g. from .models.optimize) into an actual trade list from the portfolio's current weights, or portpy.metrics.costs if you want to model transaction costs against an assumed weight-history DataFrame you construct yourself.