Skip to content
All library documents

Portfolio Weight Bounds for Long-Only, No-Trade, and Reduce-Only Rules

Code pysystemtrade

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.