Portfolio Weight Bounds for Long-Only, No-Trade, and Reduce-Only Rules
Summary
This code translates per-instrument trading restrictions into minimum and maximum portfolio weights, a direction for permitted adjustment, and a starting weight. It begins with wide default bounds, then applies long-only, no-trade, reduce-only, and maximum-position rules. A no-trade instrument is fixed at its prior weight when available, or at zero otherwise. A reduce-only rule limits changes to shrinking an existing positive or negative position, while a zero prior weight leaves no trade possible.
The resulting bounds also guide optimizer initialization: fixed bounds use that fixed value, one-sided bounds start at their nearest allowed edge, and bounds spanning zero start at zero. Direction follows a binding sign restriction or the sign of the optimum weight, with a default direction when that optimum is missing. The code is a utility for portfolio optimization, not a complete allocation strategy. It does not explain how prior or optimal weights are estimated, how constraints interact with portfolio-wide limits, or how the large finite defaults should be calibrated for a particular optimizer.
Key ideas
- The function builds per-instrument minimum and maximum weights from trading restrictions and position limits.
- Long-only rules set the lower bound to zero, while no-trade rules fix the weight at its prior value or zero.
- Reduce-only bounds allow an existing position to shrink toward zero without increasing or reversing it.
- Starting weights are selected from the feasible interval, using zero when zero is allowed.
- Direction is inferred from binding bounds or the optimizer's target weight, with a fallback when the target is missing.
Tags
Full text
# set_up_constraints.py
```py
from dataclasses import dataclass
import numpy as np
from syscore.genutils import sign
from syscore.constants import arg_not_supplied
from sysquant.optimisation.weights import portfolioWeights
A_VERY_LARGE_NUMBER = 999 ##
A_VERY_SMALL_NUMBER = 0.000001
class minMaxAndDirectionAndStart(dict):
@property
def minima(self) -> portfolioWeights:
return portfolioWeights(self._get_dict_for_value_across_codes("minimum"))
@property
def maxima(self) -> portfolioWeights:
return portfolioWeights(self._get_dict_for_value_across_codes("maximum"))
@property
def direction(self) -> portfolioWeights:
return portfolioWeights(self._get_dict_for_value_across_codes("direction"))
@property
def starting_weights(self) -> portfolioWeights:
return portfolioWeights(self._get_dict_for_value_across_codes("start_weight"))
def _get_dict_for_value_across_codes(self, entry_name: str):
return dict(
[
(instrument_code, getattr(dict_value, entry_name))
for instrument_code, dict_value in self.items()
]
)
@dataclass
class minMaxAndDirectionAndStartForCode:
minimum: float
maximum: float
direction: float
start_weight: float
def calculate_min_max_and_direction_and_start(
input_data: "dataForOptimisation",
) -> minMaxAndDirectionAndStart:
all_codes = list(input_data.keys_with_valid_data)
all_results = dict(
[
(
instrument_code,
get_data_and_calculate_for_code(instrument_code, input_data=input_data),
)
for instrument_code in all_codes
]
)
return minMaxAndDirectionAndStart(all_results)
def get_data_and_calculate_for_code(
instrument_code: str, input_data: "dataForOptimisation"
) -> minMaxAndDirectionAndStartForCode:
if input_data.reduce_only_keys is arg_not_supplied:
reduce_only = False
else:
reduce_only = instrument_code in input_data.reduce_only_keys
if input_data.no_trade_keys is arg_not_supplied:
no_trade = False
else:
no_trade = instrument_code in input_data.no_trade_keys
if input_data.long_only_keys is arg_not_supplied:
long_only = False
else:
long_only = instrument_code in input_data.long_only_keys
max_position = input_data.maximum_position_weight_for_code(instrument_code)
weight_prior = input_data.prior_weight_for_code(instrument_code)
optimium_weight = input_data.optimal_weights_for_code(instrument_code)
min_max_and_direction_and_start_for_code = calculations_for_code(
reduce_only=reduce_only,
no_trade=no_trade,
max_position=max_position,
weight_prior=weight_prior,
optimium_weight=optimium_weight,
long_only=long_only,
)
return min_max_and_direction_and_start_for_code
def calculations_for_code(
reduce_only: bool = False,
no_trade: bool = False,
max_position: float = arg_not_supplied,
weight_prior: float = arg_not_supplied,
optimium_weight: float = np.nan,
long_only: bool = False,
):
minimum, maximum = calculate_minima_and_maxima(
reduce_only=reduce_only,
no_trade=no_trade,
max_position=max_position,
weight_prior=weight_prior,
long_only=long_only,
)
assert maximum >= minimum
direction = calculate_direction(
optimum_weight=optimium_weight, minimum=minimum, maximum=maximum
)
start_weight = calculate_starting_weight(minimum=minimum, maximum=maximum)
return minMaxAndDirectionAndStartForCode(
minimum=minimum, maximum=maximum, direction=direction, start_weight=start_weight
)
def calculate_minima_and_maxima(
reduce_only: bool = False,
long_only: bool = False,
no_trade: bool = False,
max_position: float = arg_not_supplied,
weight_prior: float = arg_not_supplied,
) -> tuple:
minimum = -A_VERY_LARGE_NUMBER
maximum = A_VERY_LARGE_NUMBER
if long_only:
minimum = 0.0
if no_trade:
if weight_prior is not arg_not_supplied:
return weight_prior, weight_prior
else:
return 0.0, 0.0
if reduce_only:
if weight_prior is not arg_not_supplied:
if weight_prior > 0:
minimum = 0.0
maximum = weight_prior
elif weight_prior < 0:
minimum = max(minimum, weight_prior)
maximum = 0.0
else:
## prior weight equals zero, so no trade
return 0.0, 0.0
if max_position is not arg_not_supplied:
max_position = abs(max_position)
# Most conservative of existing minima/maximum if any
minimum = max(-max_position, minimum)
maximum = min(max_position, maximum)
return minimum, maximum
def calculate_direction(
optimum_weight: float,
minimum: float = -A_VERY_LARGE_NUMBER,
maximum: float = A_VERY_LARGE_NUMBER,
) -> float:
## always start at zero, so if minima/maxima already bind we can only go up or down
if minimum >= 0.0:
return 1
if maximum <= 0.0:
return -1
if np.isnan(optimum_weight):
return 1
return sign(optimum_weight)
def calculate_starting_weight(minimum, maximum) -> float:
if maximum == minimum:
## no trade possible
return maximum
if minimum > 0:
return minimum
if maximum < 0:
return maximum
return 0.0
```Shown in full with attribution under the source's licence. Licence: GPL-3.0
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.