Skip to content
All library documents

Portfolio Position Scaling, Diversification, and Risk Controls

Code pysystemtrade

Summary

The document describes a portfolio stage in a systematic trading framework that converts subsystem positions into portfolio-level positions. It applies instrument weights and a diversification multiplier, optionally scales positions with a risk overlay, then adjusts them by a capital multiplier to obtain actual positions. It also calculates trading buffers around positions, with buffer levels affected by portfolio diversification and instrument weights.

The code exposes both fixed and estimated diversification multipliers, with estimates based on correlations and weights. Related utilities connect contract positions to capital weights using contract value, price, and foreign-exchange conversion, and include annualized volatility calculations. The examples provide selected output values, but the source excerpt is incomplete and relies on surrounding system components and configuration. It describes portfolio mechanics rather than demonstrating that a particular allocation or scaling choice improves realized performance; the behavior depends on inputs, estimates, and configuration.

Key ideas

  • Subsystem positions are multiplied by instrument weights and a diversification multiplier to form notional positions.
  • An optional risk overlay scales notional positions before capital scaling produces actual positions.
  • The diversification multiplier can be fixed or estimated from correlations and portfolio weights.
  • Position buffers are calculated using position and portfolio-level inputs, then scaled for actual capital.
  • Contract positions can be expressed as capital weights using contract value and aligned price and foreign-exchange data.

Tags

Full text
# portfolio.py


```py
import pandas as pd
import datetime
from copy import copy

from syscore.dateutils import ROOT_BDAYS_INYEAR
from syscore.exceptions import missingData
from syscore.genutils import str2Bool, list_union
from syscore.pandas.pdutils import (
    from_dict_of_values_to_df,
    from_scalar_values_to_ts,
)
from syscore.pandas.find_data import get_row_of_df_aligned_to_weights_as_dict
from syscore.pandas.strategy_functions import (
    weights_sum_to_one,
    fix_weights_vs_position_or_forecast,
)
from syscore.objects import resolve_function
from syscore.constants import arg_not_supplied

from sysdata.config.configdata import Config

from sysquant.estimators.stdev_estimator import stdevEstimates, seriesOfStdevEstimates
from sysquant.estimators.correlations import (
    correlationEstimate,
    create_boring_corr_matrix,
    CorrelationList,
)
from sysquant.estimators.covariance import (
    covarianceEstimate,
    covariance_from_stdev_and_correlation,
)
from sysquant.estimators.turnover import turnoverDataAcrossSubsystems
from sysquant.portfolio_risk import (
    calc_portfolio_risk_series,
    calc_sum_annualised_risk_given_portfolio_weights,
)
from sysquant.optimisation.pre_processing import returnsPreProcessor
from sysquant.optimisation.weights import portfolioWeights, seriesOfPortfolioWeights

from sysquant.returns import (
    dictOfReturnsForOptimisationWithCosts,
    returnsForOptimisationWithCosts,
)

from systems.buffering import (
    calculate_buffers,
    calculate_actual_buffers,
    apply_buffers_to_position,
)
from systems.stage import SystemStage
from systems.system_cache import input, dont_cache, diagnostic, output
from systems.positionsizing import PositionSizing
from systems.accounts.curves.account_curve_group import accountCurveGroup
from systems.risk_overlay import get_risk_multiplier
from systems.basesystem import get_instrument_weights_from_config

"""
Stage for portfolios

Gets the position, accounts for instrument weights and diversification
multiplier


Note: At this stage we're dealing with a notional, fixed, amount of capital.
     We'll need to work out p&l to scale positions properly
"""


class Portfolios(SystemStage):
    @property
    def name(self):
        return "portfolio"

    # actual positions and buffers
    @output()
    def get_actual_position(self, instrument_code: str) -> pd.Series:
        """
        Gets the actual position, accounting for cap multiplier

        :param instrument_code: instrument to get values for
        :type instrument_code: str

        :returns: Tx1 pd.Series

        KEY OUTPUT
        """

        self.log.debug(
            "Calculating actual position for %s" % instrument_code,
            instrument_code=instrument_code,
        )

        notional_position = self.get_notional_position(instrument_code)
        cap_multiplier = self.capital_multiplier()
        cap_multiplier = cap_multiplier.reindex(notional_position.index).ffill()

        actual_position = notional_position * cap_multiplier

        return actual_position

    @output()
    def get_actual_buffers_for_position(self, instrument_code: str) -> pd.DataFrame:
        """
        Gets the actual buffers for a position, accounting for cap multiplier
        :param instrument_code: instrument to get values for
        :type instrument_code: str
        :returns: Tx1 pd.Series
        KEY OUTPUT
        """

        self.log.debug(
            "Calculating actual buffers for position for %s" % instrument_code,
            instrument_code=instrument_code,
        )

        cap_multiplier = self.capital_multiplier()
        buffers = self.get_buffers_for_position(instrument_code)

        actual_buffers_for_position = calculate_actual_buffers(buffers, cap_multiplier)

        return actual_buffers_for_position

    # buffers
    @output()
    def get_buffers_for_position(self, instrument_code: str) -> pd.DataFrame:
        """
        Gets the buffers for positions, using method depending on config.buffer_method

        KEY OUTPUT

        :param instrument_code: instrument to get values for
        :type instrument_code: str

        :returns: Tx2 pd.DataFrame

        >>> from systems.tests.testdata import get_test_object_futures_with_pos_sizing
        >>> from systems.basesystem import System
        >>> (posobject, combobject, capobject, rules, rawdata, data, config)=get_test_object_futures_with_pos_sizing()
        >>> system=System([rawdata, rules, posobject, combobject, capobject,Portfolios()], data, config)
        >>>
        >>> ## from config
        >>> system.portfolio.get_buffers_for_position("EDOLLAR").tail(2)
                     top_pos   bot_pos
        2015-12-10  1.195567  0.978191
        2015-12-11  1.679435  1.374083
        """

        position = self.get_notional_position(instrument_code)
        buffer = self.get_buffers(instrument_code)

        pos_buffers = apply_buffers_to_position(position=position, buffer=buffer)

        return pos_buffers

    @diagnostic()
    def get_buffers(self, instrument_code: str) -> pd.Series:
        position = self.get_notional_position(instrument_code)
        vol_scalar = self.get_average_position_at_subsystem_level(instrument_code)
        log = self.log
        config = self.config
        idm = self.get_instrument_diversification_multiplier()
        instr_weights = self.get_instrument_weights()

        buffer = calculate_buffers(
            instrument_code=instrument_code,
            position=position,
            log=log,
            config=config,
            idm=idm,
            instr_weights=instr_weights,
            vol_scalar=vol_scalar,
        )

        return buffer

    ## notional position
    @output()
    def get_notional_position(self, instrument_code: str) -> pd.Series:
        """
        Gets the position, accounts for instrument weights and diversification multiplier

        Note: At this stage we're dealing with a notional, fixed, amount of capital.
             We'll need to work out p&l to scale positions properly

        :param instrument_code: instrument to get values for
        :type instrument_code: str

        :returns: Tx1 pd.DataFrame

        KEY OUTPUT
        >>> from systems.tests.testdata import get_test_object_futures_with_pos_sizing
        >>> from systems.basesystem import System
        >>> (posobject, combobject, capobject, rules, rawdata, data, config)=get_test_object_futures_with_pos_sizing()
        >>> system=System([rawdata, rules, posobject, combobject, capobject,Portfolios()], data, config)
        >>>
        >>> ## from config
        >>> system.portfolio.get_notional_position("EDOLLAR").tail(2)
                         pos
        2015-12-10  1.086879
        2015-12-11  1.526759

        """

        self.log.debug(
            "Calculating notional position for %s" % instrument_code,
            instrument_code=instrument_code,
        )

        # same frequency as subsystem / forecasts
        notional_position_without_risk_scalar = (
            self.get_notional_position_before_risk_scaling(instrument_code)
        )

        try:
            risk_scalar = self.get_risk_scalar()
        except missingData:
            self.log.debug("No risk overlay in config: won't apply risk scaling")
            notional_position = notional_position_without_risk_scalar
        else:
            risk_scalar_reindex = risk_scalar.reindex(
                notional_position_without_risk_scalar.index
            )
            notional_position = (
                notional_position_without_risk_scalar * risk_scalar_reindex.ffill()
            )

        return notional_position

    ## notional position
    @diagnostic()
    def get_notional_position_before_risk_scaling(
        self, instrument_code: str
    ) -> pd.Series:
        """ """

        # same frequency as subsystem / forecasts
        notional_position_without_idm = self.get_notional_position_without_idm(
            instrument_code
        )

        ## daily
        idm = self.get_instrument_diversification_multiplier()
        idm_reindexed = idm.reindex(notional_position_without_idm.index).ffill()

        notional_position = notional_position_without_idm * idm_reindexed

        # same frequency as subsystem / forecasts
        return notional_position

    @diagnostic()
    def get_notional_position_without_idm(self, instrument_code: str) -> pd.Series:
        instr_weights = self.get_instrument_weights()

        # unknown frequency
        subsys_position = self.get_subsystem_position(instrument_code)

        # daily
        instrument_weight_this_code = instr_weights[instrument_code]

        inst_weight_this_code_reindexed = instrument_weight_this_code.reindex(
            subsys_position.index, method="ffill"
        )

        notional_position_without_idm = (
            subsys_position * inst_weight_this_code_reindexed
        )

        # subsystem frequency
        return notional_position_without_idm

    # IDM
    @dont_cache
    def get_instrument_diversification_multiplier(self) -> pd.Series:
        if self.use_estimated_instrument_div_mult:
            idm = self.get_estimated_instrument_diversification_multiplier()
        else:
            idm = self.get_fixed_instrument_diversification_multiplier()

        return idm

    @property
    def use_estimated_instrument_div_mult(self) -> bool:
        """
        It will determine if we use an estimate or a fixed class of object
        """
        return str2Bool(self.config.use_instrument_div_mult_estimates)

    @diagnostic()
    def get_estimated_instrument_diversification_multiplier(self) -> pd.Series:
        """

        Estimate the diversification multiplier for the portfolio

        Estimated from correlations and weights

        :returns: Tx1 pd.DataFrame

        >>> from systems.tests.testdata import get_test_object_futures_with_pos_sizing_estimates
        >>> from systems.basesystem import System
        >>> (account, posobject, combobject, capobject, rules, rawdata, data, config)=get_test_object_futures_with_pos_sizing_estimates()
        >>> system=System([rawdata, rules, posobject, combobject, capobject,Portfolios(), account], data, config)
        >>> system.config.forecast_weight_estimate["method"]="shrinkage" ## speed things up
        >>> system.config.forecast_weight_estimate["date_method"]="in_sample" ## speed things up
        >>> system.config.instrument_weight_estimate["date_method"]="in_sample" ## speed things up
        >>> system.config.instrument_weight_estimate["method"]="shrinkage" ## speed things up
        >>> system.portfolio.get_instrument_diversification_multiplier().tail(3)
                         IDM
        2015-12-09  1.133220
        2015-12-10  1.133186
        2015-12-11  1.133153
        """

        self.log.info("Calculating instrument div. multiplier")

        # Get some useful stuff from the config
        div_mult_params = copy(self.config.instrument_div_mult_estimate)

        idm_func = resolve_function(div_mult_params.pop("func"))

        # annual
        correlation_list = self.get_instrument_correlation_matrix()

        # daily
        weight_df = self.get_instrument_weights()

        ts_idm = idm_func(correlation_list, weight_df, **div_mult_params)

        # daily

        return ts_idm

    @diagnostic()
    def get_fixed_instrument_diversification_multiplier(self) -> pd.Series:
        """
        Get the instrument diversification multiplier

        :returns: TxK pd.DataFrame containing weights, columns are instrument names, T covers all subsystem positions

        >>> from systems.tests.testdata import get_test_object_futures_with_pos_sizing
        >>> from systems.basesystem import System
        >>> (posobject, combobject, capobject, rules, rawdata, data, config)=get_test_object_futures_with_pos_sizing()
        >>> system=System([rawdata, rules, posobject, combobject, capobject,Portfolios()], data, config)
        >>>
        >>> ## from config
        >>> system.portfolio.get_instrument_diversification_multiplier().tail(2)
                    idm
        2015-12-10  1.2
        2015-12-11  1.2
        >>>
        >>> ## from defaults
        >>> del(config.instrument_div_multiplier)
        >>> system2=System([rawdata, rules, posobject, combobject, capobject,Portfolios()], data, config)
        >>> system2.portfolio.get_instrument_diversification_multiplier().tail(2)
                    idm
        2015-12-10    1
        2015-12-11    1
        """

        div_mult = self.config.instrument_div_multiplier

        self.log.info("Using fixed diversification multiplier %f" % div_mult)

        # Now we have a fixed weight
        # Need to turn into a two row timeseries covering the range of forecast
        # dates

        weight_ts = self.get_instrument_weights().index

        ts_idm = from_scalar_values_to_ts(div_mult, weight_ts)

        return ts_idm

    # CORRELATIONS USED FOR IDM

    @diagnostic(protected=True, not_pickable=True)
    def get_instrument_correlation_matrix(self):
        """
        Returns a correlationList object which contains a history of correlation matricies

        :returns: correlation_list object

        >>> from systems.tests.testdata import get_test_object_futures_with_pos_sizing_estimates
        >>> from systems.basesystem import System
        >>> (account, posobject, combobject, capobject, rules, rawdata, data, config)=get_test_object_futures_with_pos_sizing_estimates()
        >>> system=System([rawdata, rules, posobject, combobject, capobject,Portfolios(), account], data, config)
        >>> system.config.forecast_weight_estimate["method"]="shrinkage" ## speed things up
        >>> system.config.forecast_weight_estimate["date_method"]="in_sample" ## speed things up
        >>> system.config.instrument_weight_estimate["date_method"]="in_sample" ## speed things up
        >>> system.config.instrument_weight_estimate["method"]="shrinkage" ## speed things up
        >>> ans=system.portfolio.get_instrument_correlation_matrix()
        >>> ans.corr_list[-1]
        array([[ 1.        ,  0.56981346,  0.62458477],
               [ 0.56981346,  1.        ,  0.88087893],
               [ 0.62458477,  0.88087893,  1.        ]])
        >>> print(ans.corr_list[0])
        [[ 1.    0.99  0.99]
         [ 0.99  1.    0.99]
         [ 0.99  0.99  1.  ]]
        >>> print(ans.corr_list[10])
        [[ 1.          0.99        0.99      ]
         [ 0.99        1.          0.78858156]
         [ 0.99        0.78858156  1.        ]]
        """

        self.log.info("Calculating instrument correlations")

        config = self.config

        # Get some useful stuff from the config
        corr_params = copy(config.instrument_correlation_estimate)

        # which function to use for calculation
        corr_func = resolve_function(corr_params.pop("func"))

        pandl = self.pandl_across_subsystems().to_frame()

        return corr_func(pandl, **corr_params)

    ## INSTRUMENT WEIGHTS

    @diagnostic()
    def get_instrument_weights(self) -> pd.DataFrame:
        """
        Get the time series of instrument weights, accounting for potentially missing positions, and weights that don't add up.

        :returns: TxK pd.DataFrame containing weights, columns are instrument names, T covers all subsystem positions


        """

        smooth_weighting = self.config.instrument_weight_ewma_span
        daily_unsmoothed_instrument_weights = (
            self.get_unsmoothed_instrument_weights_fitted_to_position_lengths()
        )

        # smooth to avoid jumps when they change
        smoothed_instrument_weights = daily_unsmoothed_instrument_weights.ewm(
            span=smooth_weighting
        ).mean()

        normalised_smoothed_instrument_weights = weights_sum_to_one(
            smoothed_instrument_weights
        )

        # daily

        return normalised_smoothed_instrument_weights

    @diagnostic()
    def get_unsmoothed_instrument_weights_fitted_to_position_lengths(
        self,
    ) -> pd.DataFrame:
        raw_instrument_weights = self.get_unsmoothed_raw_instrument_weights()

        instrument_list = list(raw_instrument_weights.columns)

        subsystem_positions = self.get_subsystem_positions_for_instrument_list(
            instrument_list
        )

        ## this should remove when have NAN's
        ## FIXME CHECK

        instrument_weights = fix_weights_vs_position_or_forecast(
            raw_instrument_weights, subsystem_positions
        )

        # now on same frequency as positions
        # Move to daily for space saving and so smoothing makes sense
        daily_unsmoothed_instrument_weights = instrument_weights.resample("1B").mean()

        return daily_unsmoothed_instrument_weights

    @diagnostic()
    def get_subsystem_positions_for_instrument_list(
        self, instrument_list: list
    ) -> pd.DataFrame:
        subsystem_positions = [
            self.get_subsystem_position(instrument_code)
            for instrument_code in instrument_list
        ]

        subsystem_positions = pd.concat(subsystem_positions, axis=1).ffill()
        subsystem_positions.columns = instrument_list

        return subsystem_positions

    @diagnostic()
    def get_unsmoothed_raw_instrument_weights(self) -> pd.DataFrame:
        self.log.info("Calculating instrument weights")

        if self.use_estimated_instrument_weights():
            ## will probably be annnual
            raw_instrument_weights = self.get_raw_estimated_instrument_weights()
        else:
            ## will be 2*N
            raw_instrument_weights = self.get_raw_fixed_instrument_weights()

        return raw_instrument_weights

    @input
    def use_estimated_instrument_weights(self):
        """
        It will determine if we use an estimate or a fixed class of object
        """
        return str2Bool(self.parent.config.use_instrument_weight_estimates)

    ## FIXED INSTRUMENT WEIGHTS
    @diagnostic()
    def get_raw_fixed_instrument_weights(self) -> pd.DataFrame:
        """
        Get the instrument weights
        These are 'raw' because we need to account for potentially missing positions, and weights that don't add up.
        From: (a) passed into subsystem when created
              (b) ... if not found then: in system.config.instrument_weights
        :returns: TxK pd.DataFrame containing weights, columns are instrument names, T covers all subsystem positions
        >>> from systems.tests.testdata import get_test_object_futures_with_pos_sizing
        >>> from systems.basesystem import System
        >>> (posobject, combobject, capobject, rules, rawdata, data, config)=get_test_object_futures_with_pos_sizing()
        >>> config.instrument_weights=dict(EDOLLAR=0.1, US10=0.9)
        >>> system=System([rawdata, rules, posobject, combobject, capobject,Portfolios()], data, config)
        >>>
        >>> ## from config
        >>> system.portfolio.get_instrument_weights().tail(2)
                    EDOLLAR  US10
        2015-12-10      0.1   0.9
        2015-12-11      0.1   0.9
        >>>
        >>> del(config.instrument_weights)
        >>> system2=System([rawdata, rules, posobject, combobject, capobject,Portfolios()], data, config)
        >>> system2.portfolio.get_instrument_weights().tail(2)
        WARNING: No instrument weights  - using equal weights of 0.3333 over all 3 instruments in data
                        BUND   EDOLLAR      US10
        2015-12-10  0.333333  0.333333  0.333333
        2015-12-11  0.333333  0.333333  0.333333
        """

        self.log.debug("Calculating raw instrument weights")
        instrument_weights_dict = self.get_fixed_instrument_weights_from_config()

        # Now we have a dict, fixed_weights.
        # Need to turn into a timeseries covering the range of subsystem positions
        instrument_list = self.get_instrument_list()

        subsystem_positions = self._get_all_subsystem_positions()
        position_series_index = subsystem_positions.index

        # CHANGE TO TXN DATAFRAME
        instrument_weights = from_dict_of_values_to_df(
            instrument_weights_dict, position_series_index, columns=instrument_list
        )

        return instrument_weights

    @diagnostic()
    def get_fixed_instrument_weights_from_config(self) -> dict:
        try:
            instrument_weights_dict = get_instrument_weights_from_config(self.config)
        except:
            instrument_weights_dict = self.get_equal_instrument_weights_dict()

        instrument_weights_dict = self._add_zero_instrument_weights(
            instrument_weights_dict
        )

        return instrument_weights_dict

    @dont_cache
    def get_equal_instrument_weights_dict(self) -> dict:
        instruments_with_weights = self.get_instrument_list(for_instrument_weights=True)
        weight = 1.0 / len(instruments_with_weights)

        warn_msg = (
            "WARNING: No instrument weights  - using equal weights of %.4f over all %d instruments in data"
            % (weight, len(instruments_with_weights))
        )

        self.log.warning(warn_msg)

        instrument_weights = dict(
            [(instrument_code, weight) for instrument_code in instruments_with_weights]
        )

        return instrument_weights

    def _add_zero_instrument_weights(self, instrument_weights: dict) -> dict:
        copy_instrument_weights = copy(instrument_weights)
        instruments_with_zero_weights = (
            self.allocate_zero_instrument_weights_to_these_instruments()
        )
        for instrument_code in instruments_with_zero_weights:
            copy_instrument_weights[instrument_code] = 0.0

        return copy_instrument_weights

    def _remove_zero_weighted_instruments_from_df(
        self, some_data_frame: pd.DataFrame
    ) -> pd.DataFrame:
        copy_df = copy(some_data_frame)
        instruments_with_zero_weights = (
            self.allocate_zero_instrument_weights_to_these_instruments()
        )
        copy_df.drop(labels=instruments_with_zero_weights)

        return copy_df

    ## INPUT
    @diagnostic()
    def _get_all_subsystem_positions(self) -> pd.DataFrame:
        """

        :return: single pd.matrix of all the positions
        """
        instrument_list = self.get_instrument_list()

        positions = self.get_subsystem_positions_for_instrument_list(instrument_list)

        return positions

    ## ESTIMATED WEIGHTS
    @diagnostic()
    def get_raw_estimated_instrument_weights(self) -> pd.DataFrame:
        """
        Estimate the instrument weights


        :returns: TxK pd.DataFrame containing weights, columns are trading rule variation names, T covers all

        >>> from systems.tests.testdata import get_test_object_futures_with_pos_sizing_estimates
        >>> from systems.basesystem import System
        >>> (account, posobject, combobject, capobject, rules, rawdata, data, config)=get_test_object_futures_with_pos_sizing_estimates()
        >>> system=System([account, rawdata, rules, posobject, combobject, capobject,Portfolios()], data, config)
        >>> system.config.forecast_weight_estimate["method"]="shrinkage" ## speed things up
        >>> system.config.forecast_weight_estimate["date_method"]="in_sample" ## speed things up
        >>> system.config.instrument_weight_estimate["method"]="shrinkage"
        >>> system.portfolio.get_raw_instrument_weights().tail(3)
                            BUND   EDOLLAR      US10
        2015-05-30  4.006172e-17  0.499410  0.500590
        2015-06-01  5.645388e-01  0.217462  0.217999
        2015-12-12  5.645388e-01  0.217462  0.217999
        """

        # these will probably be annual
        optimiser = self.calculation_of_raw_instrument_weights()
        instrument_weights = optimiser.weights()

        instrument_weights_with_zeros = self._add_zero_weights_to_instrument_weights_df(
            instrument_weights
        )

        return instrument_weights_with_zeros

    def fit_periods(self):
        # FIXME, NO GUARANTEE THIS OBJECT HAS AN ESTIMATOR UNLESS IT INHERITS FROM
        # SOME KIND OF BASECLASS

        weight_calculator = self.calculation_of_raw_instrument_weights()

        return weight_calculator.fit_dates

    @diagnostic()
    def correlation_estimator_for_subsystem_returns(self):
        # FIXME, NO GUARANTEE THIS OBJECT HAS AN ESTIMATOR UNLESS IT INHERITS FROM
        # SOME KIND OF BASECLASS

        weight_calculator = self.calculation_of_raw_instrument_weights()

        return weight_calculator.correlation_estimator

    @diagnostic(protected=True, not_pickable=True)
    def calculation_of_raw_instrument_weights(self):
        """
        Estimate the instrument weights

        Done like this to expose calculations

        :returns: TxK pd.DataFrame containing weights, columns are instrument names, T covers all

        """

        # Get some useful stuff from the config
        weighting_params = copy(self.config.instrument_weight_estimate)

        # which function to use for calculation
        weighting_func = resolve_function(weighting_params.pop("func"))

        returns_pre_processor = self.returns_pre_processor()

        self.log.info("Calculating raw instrument weights")

        weight_func = weighting_func(
            returns_pre_processor, log=self.log, **weighting_params
        )

        return weight_func

    @diagnostic(not_pickable=True)
    def returns_pre_processor(self) -> returnsPreProcessor:
        instrument_list = self.get_instrument_list(for_instrument_weights=True)
        pandl_across_subsystems_raw = self.pandl_across_subsystems(
            instrument_list=instrument_list
        )
        pandl_across_subsystems_as_returns_object = returnsForOptimisationWithCosts(
            pandl_across_subsystems_raw
        )
        pandl_across_subsystems = dictOfReturnsForOptimisationWithCosts(
            pandl_across_subsystems_as_returns_object
        )

        turnovers = self.turnover_across_subsystems()
        config = self.config

        weighting_params = copy(config.instrument_weight_estimate)

        returns_pre_processor = returnsPreProcessor(
            pandl_across_subsystems,
            turnovers=turnovers,
            log=self.log,
            **weighting_params,
        )

        return returns_pre_processor

    def _add_zero_weights_to_instrument_weights_df(
        self, instrument_weights: pd.DataFrame
    ) -> pd.DataFrame:
        instrument_list_to_add = (
            self.allocate_zero_instrument_weights_to_these_instruments()
        )

        padded_instrument_weights = copy(instrument_weights)
        for zero_instr in instrument_list_to_add:
            padded_instrument_weights[zero_instr] = 0.0

        return padded_instrument_weights

    @diagnostic()
    def allocate_zero_instrument_weights_to_these_instruments(
        self, auto_remove_bad_instruments: bool = False
    ) -> list:
        config_allocate_zero_instrument_weights_to_these_instruments = (
            self.config_allocates_zero_instrument_weights_to_these_instruments(
                auto_remove_bad_instruments=auto_remove_bad_instruments
            )
        )

        instruments_without_data_or_weights = self.instruments_without_data_or_weights()

        all_instruments_to_allocate_zero_to = list_union(
            instruments_without_data_or_weights,
            config_allocate_zero_instrument_weights_to_these_instruments,
        )

        return all_instruments_to_allocate_zero_to

    def config_allocates_zero_instrument_weights_to_these_instruments(
        self, auto_remove_bad_instruments: bool = False
    ):
        bad_from_config = self.parent.get_list_of_markets_not_trading_but_with_data()
        config = self.config
        config_allocates_zero_instrument_weights_to_these_instruments = getattr(
            config, "allocate_zero_instrument_weights_to_these_instruments", []
        )
        instrument_list = self.get_instrument_list()
        config_marks_bad_and_in_instrument_list = list(
            set(instrument_list).intersection(set(bad_from_config))
        )
        configured_bad_but_not_configured_zero_allocation = list(
            set(config_marks_bad_and_in_instrument_list).difference(
                set(config_allocates_zero_instrument_weights_to_these_instruments)
            )
        )

        allocate_zero_instrument_weights_to_these_instruments = copy(
            config_allocates_zero_instrument_weights_to_these_instruments
        )
        if len(configured_bad_but_not_configured_zero_allocation) > 0:
            if auto_remove_bad_instruments:
                self.log.warning(
                    "*** Following instruments are listed as trading_restrictions and/or bad_markets and will be removed from instrument weight optimisation: ***\n%s"
                    % str(configured_bad_but_not_configured_zero_allocation)
                )
                allocate_zero_instrument_weights_to_these_instruments = (
                    allocate_zero_instrument_weights_to_these_instruments
                    + configured_bad_but_not_configured_zero_allocation
                )
            else:
                self.log.warning(
                    "*** Following instruments are listed as trading_restrictions and/or bad_markets but still included in instrument weight optimisation: ***\n%s"
                    % str(configured_bad_but_not_configured_zero_allocation)
                )
                self.log.warning(
                    "This is fine for dynamic systems where we remove them in later optimisation, but may be problematic for static systems"
                )
                self.log.warning(
                    "Consider adding to config element allocate_zero_instrument_weights_to_these_instruments"
                )

        if len(allocate_zero_instrument_weights_to_these_instruments) > 0:
            self.log.debug(
                "Following instruments will have zero weight in optimisation of instrument weights as configured zero or auto removal of configured bad%s"
                % str(allocate_zero_instrument_weights_to_these_instruments)
            )

        return allocate_zero_instrument_weights_to_these_instruments

    def instruments_without_data_or_weights(self) -> list:
        subsystem_positions = copy(self._get_all_subsystem_positions())
        subsystem_positions[subsystem_positions.isna()] = 0
        not_zero = subsystem_positions != 0
        index_of_empty_markets = not_zero.sum(axis=0) == 0
        list_of_empty_markets = [
            instrument_code
            for instrument_code, empty in index_of_empty_markets.items()
            if empty
        ]

        self.log.debug(
            "Following instruments will have zero weight in optimisation of instrument weights as they have no positions (possibly too expensive?) %s"
            % str(list_of_empty_markets)
        )

        return list_of_empty_markets

    @input
    def get_subsystem_position(self, instrument_code: str) -> pd.Series:
        """
        Get the position assuming all capital in one position, from a previous
        module

        :param instrument_code: instrument to get values for
        :type instrument_code: str

        :returns: Tx1 pd.DataFrame

        KEY INPUT

        >>> from systems.tests.testdata import get_test_object_futures_with_pos_sizing
        >>> from systems.basesystem import System
        >>> (posobject, combobject, capobject, rules, rawdata, data, config)=get_test_object_futures_with_pos_sizing()
        >>> system=System([rawdata, rules, posobject, combobject, capobject,Portfolios()], data, config)
        >>>
        >>> ## from config
        >>> system.portfolio.get_subsystem_position("EDOLLAR").tail(2)
                    ss_position
        2015-12-10     1.811465
        2015-12-11     2.544598

        """

        return self.position_size_stage.get_subsystem_position(instrument_code)

    @input
    def pandl_across_subsystems(
        self, instrument_list: list = arg_not_supplied
    ) -> accountCurveGroup:
        """
        Return profitability of each instrument

        KEY INPUT

        :param instrument_code:
        :type str:

        :returns: accountCurveGroup object
        """

        try:
            accounts = self.accounts_stage
        except missingData as e:
            error_msg = "You need an accounts stage in the system to estimate instrument weights or IDM"
            self.log.critical(error_msg)
            raise missingData(error_msg) from e

        if instrument_list is arg_not_supplied:
            instrument_list = self.get_instrument_list()

        ## roundpositions=True required to make IDM work with order simulator
        return accounts.pandl_across_subsystems_given_instrument_list(
            instrument_list, roundpositions=True
        )

    @input
    def turnover_across_subsystems(self) -> turnoverDataAcrossSubsystems:
        instrument_list = self.get_instrument_list(for_instrument_weights=True)
        turnover_as_list = [
            self.accounts_stage.subsystem_turnover(instrument_code)
            for instrument_code in instrument_list
        ]

        turnover_as_dict = dict(
            [
                (instrument_code, turnover)
                for (instrument_code, turnover) in zip(
                    instrument_list, turnover_as_list
                )
            ]
        )

        turnovers = turnoverDataAcrossSubsystems(turnover_as_dict)

        return turnovers

    @input
    def get_average_position_at_subsystem_level(
        self, instrument_code: str
    ) -> pd.Series:
        """
        Get the vol scalar, from a previous module

        :param instrument_code: instrument to get values for
        :type instrument_code: str

        :returns: Tx1 pd.DataFrame

        KEY INPUT

        >>> from systems.tests.testdata import get_test_object_futures_with_pos_sizing
        >>> from systems.basesystem import System
        >>> (posobject, combobject, capobject, rules, rawdata, data, config)=get_test_object_futures_with_pos_sizing()
        >>> system=System([rawdata, rules, posobject, combobject, capobject,Portfolios
        ()], data, config)
        >>>
        >>> ## from config
        >>> system.portfolio.get_average_position_at_subsystem_level("EDOLLAR").tail(2)
                    vol_scalar
        2015-12-10   11.187869
        2015-12-11   10.332930
        """

        return self.position_size_stage.get_average_position_at_subsystem_level(
            instrument_code
        )

    @input
    def capital_multiplier(self):
        try:
            accounts_stage = self.accounts_stage
        except missingData as e:
            msg = "If using capital_multiplier to work out actual positions, need an accounts module"
            self.log.critical(msg)
            raise missingData(msg) from e
        else:
            return accounts_stage.capital_multiplier()

    ## RISK
    @diagnostic()
    def get_risk_scalar(self) -> pd.Series:
        risk_overlay_config = self.config.get_element("risk_overlay")

        normal_risk = self.get_portfolio_risk_for_original_positions()
        shocked_vol_risk = (
            self.get_portfolio_risk_for_original_positions_with_shocked_vol()
        )
        sum_abs_risk = self.get_sum_annualised_risk_for_original_positions()
        leverage = self.get_leverage_for_original_position()
        percentage_vol_target = self.get_percentage_vol_target()

        risk_scalar = get_risk_multiplier(
            risk_overlay_config=risk_overlay_config,
            normal_risk=normal_risk,
            shocked_vol_risk=shocked_vol_risk,
            sum_abs_risk=sum_abs_risk,
            leverage=leverage,
            percentage_vol_target=percentage_vol_target,
        )

        return risk_scalar

    @diagnostic()
    def get_leverage_for_original_position(self) -> pd.Series:
        portfolio_weights = self.get_original_portfolio_weight_df()
        leverage = portfolio_weights.get_sum_leverage()

        return leverage

    @diagnostic()
    def get_sum_annualised_risk_for_original_positions(
        self,
    ) -> pd.Series:
        portfolio_weights = self.get_original_portfolio_weight_df()
        return self.get_sum_annualised_risk_given_portfolio_weights(portfolio_weights)

    def get_sum_annualised_risk_given_portfolio_weights(
        self,
        portfolio_weights: seriesOfPortfolioWeights,
    ) -> pd.Series:
        pd_of_stdev = self.get_stdev_df()
        risk_series = calc_sum_annualised_risk_given_portfolio_weights(
            portfolio_weights=portfolio_weights, pd_of_stdev=pd_of_stdev
        )

        return risk_series

    @diagnostic()
    def get_portfolio_risk_for_original_positions(self) -> pd.Series:
        weights = self.get_original_portfolio_weight_df()
        return self.get_portfolio_risk_given_weights(weights)

    @diagnostic()
    def get_portfolio_risk_for_original_positions_with_shocked_vol(self) -> pd.Series:
        weights = self.get_original_portfolio_weight_df()
        return self.get_portfolio_risk_given_weights(weights, use_shocked_vol=True)

    def get_portfolio_risk_given_weights(
        self, portfolio_weights: seriesOfPortfolioWeights, use_shocked_vol=False
    ) -> pd.Series:
        list_of_correlations = self.get_list_of_instrument_returns_correlations()
        pd_of_stdev = self.get_stdev_df(shocked=use_shocked_vol)
        risk_series = calc_portfolio_risk_series(
            portfolio_weights=portfolio_weights,
            list_of_correlations=list_of_correlations,
            pd_of_stdev=pd_of_stdev,
        )

        return risk_series

    def get_stdev_df(self, shocked: bool = False) -> seriesOfStdevEstimates:
        if shocked:
            return self.get_shocked_df_of_perc_vol()
        else:
            return self.get_df_of_perc_vol()

    @diagnostic()
    def get_shocked_df_of_perc_vol(self) -> seriesOfStdevEstimates:
        df_of_vol = self.get_df_of_perc_vol()
        shocked_df_of_vol = df_of_vol.shocked()

        return shocked_df_of_vol

    ## PORTFOLIO WEIGHTS
    def get_position_contracts_for_relevant_date(
        self, relevant_date: datetime.datetime = arg_not_supplied
    ) -> portfolioWeights:
        position_contracts_as_df = self.get_position_contracts_as_df()
        position_contracts_at_date = get_row_of_df_aligned_to_weights_as_dict(
            position_contracts_as_df, relevant_date
        )

        position_contracts = portfolioWeights(position_contracts_at_date)

        return position_contracts

    def get_covariance_matrix(
        self,
        relevant_date: datetime.datetime = arg_not_supplied,
        correlation_estimation_parameters=arg_not_supplied,
    ) -> covarianceEstimate:
        correlation_estimate = self.get_correlation_matrix(
            relevant_date=relevant_date,
            correlation_estimation_parameters=correlation_estimation_parameters,
        )
        stdev_estimate = self.get_stdev_estimate(relevant_date=relevant_date)

        covariance = covariance_from_stdev_and_correlation(
            correlation_estimate, stdev_estimate
        )

        return covariance

    def get_correlation_matrix(
        self,
        relevant_date: datetime.datetime = arg_not_supplied,
        correlation_estimation_parameters: dict = arg_not_supplied,
    ) -> correlationEstimate:
        list_of_correlations = self.get_list_of_instrument_returns_correlations(
            correlation_estimation_parameters=correlation_estimation_parameters
        )
        try:
            correlation_matrix = (
                list_of_correlations.most_recent_correlation_before_date(relevant_date)
            )
        except:
            instrument_list = self.get_instrument_list()
            correlation_matrix = create_boring_corr_matrix(
                len(instrument_list), columns=instrument_list, offdiag=0.0
            )

        return correlation_matrix

    @diagnostic(not_pickable=True)
    def get_list_of_instrument_returns_correlations(
        self, correlation_estimation_parameters: dict = arg_not_supplied
    ) -> CorrelationList:
        config = self.config
        if correlation_estimation_parameters is arg_not_supplied:
            # Get some useful stuff from the config
            corr_parameters = copy(config.instrument_returns_correlation)
        else:
            corr_parameters = copy(correlation_estimation_parameters)

        # which function to use for calculation
        corr_func = resolve_function(corr_parameters.pop("func"))

        returns_as_pd = self.returns_across_instruments_as_df()

        return corr_func(returns_as_pd, **corr_parameters)

    @diagnostic()
    def returns_across_instruments_as_df(self) -> pd.DataFrame:
        instrument_list = self.get_instrument_list()
        returns_as_dict = dict(
            [
                (
                    instrument_code,
                    self.percentage_return_for_instrument(instrument_code),
                )
                for instrument_code in instrument_list
            ]
        )

        returns_as_pd = pd.DataFrame(returns_as_dict)

        return returns_as_pd

    def percentage_return_for_instrument(self, instrument_code) -> pd.Series:
        return self.rawdata.get_daily_percentage_returns(instrument_code)

    def get_per_contract_value(
        self, relevant_date: datetime.datetime = arg_not_supplied
    ) -> portfolioWeights:
        df_of_values = self.get_per_contract_value_as_proportion_of_capital_df()
        values_at_date = get_row_of_df_aligned_to_weights_as_dict(
            df_of_values, relevant_date
        )
        contract_values = portfolioWeights(values_at_date)

        return contract_values

    def get_stdev_estimate(
        self, relevant_date: datetime.datetime = arg_not_supplied
    ) -> stdevEstimates:
        df_of_vol = self.get_df_of_perc_vol()

        return df_of_vol.get_stdev_on_date(relevant_date)

    @diagnostic()
    def get_df_of_perc_vol(self) -> seriesOfStdevEstimates:
        instrument_list = self.get_instrument_list()
        vol_as_dict = dict(
            [
                (instrument_code, self.annualised_percentage_vol(instrument_code))
                for instrument_code in instrument_list
            ]
        )

        vol_as_pd = pd.DataFrame(vol_as_dict)
        vol_as_pd = vol_as_pd.ffill()

        return seriesOfStdevEstimates(vol_as_pd)

    @diagnostic()
    def common_index(self):
        portfolio_weights = self.get_original_portfolio_weight_df()
        common_index = portfolio_weights.index

        return common_index

    @diagnostic()
    def get_original_portfolio_weight_df(self) -> seriesOfPortfolioWeights:
        instrument_list = self.get_instrument_list()
        weights_as_dict = dict(
            [
                (
                    instrument_code,
                    self.get_portfolio_weight_series_from_contract_positions(
                        instrument_code
                    ),
                )
                for instrument_code in instrument_list
            ]
        )

        weights_as_pd = pd.DataFrame(weights_as_dict)
        weights_as_pd = weights_as_pd.ffill()

        return seriesOfPortfolioWeights(weights_as_pd)

    @diagnostic()
    def get_per_contract_value_as_proportion_of_capital_df(self) -> pd.DataFrame:
        instrument_list = self.get_instrument_list()
        values_as_dict = dict(
            [
                (
                    instrument_code,
                    self.get_per_contract_value_as_proportion_of_capital(
                        instrument_code
                    ),
                )
                for instrument_code in instrument_list
            ]
        )

        values_as_pd = pd.DataFrame(values_as_dict)
        common_index = self.common_index()

        values_as_pd = values_as_pd.reindex(common_index)
        values_as_pd = values_as_pd.ffill()

        ## slight cheating
        values_as_pd = values_as_pd.bfill()

        return values_as_pd

    def get_position_contracts_as_df(self) -> pd.DataFrame:
        instrument_list = self.get_instrument_list()
        values_as_dict = dict(
            [
                (
                    instrument_code,
                    self.get_notional_position_before_risk_scaling(instrument_code),
                )
                for instrument_code in instrument_list
            ]
        )

        values_as_pd = pd.DataFrame(values_as_dict)
        common_index = self.common_index()

        values_as_pd = values_as_pd.reindex(common_index)
        values_as_pd = values_as_pd.ffill()

        return values_as_pd

    @diagnostic()
    def get_portfolio_weight_series_from_contract_positions(
        self, instrument_code: str
    ) -> pd.Series:
        contract_positions = self.get_notional_position_before_risk_scaling(
            instrument_code
        )
        per_contract_value_as_proportion_of_capital = (
            self.get_per_contract_value_as_proportion_of_capital(instrument_code)
        )

        weights_as_proportion_of_capital = get_portfolio_weights_from_contract_positions(
            contract_positions=contract_positions,
            per_contract_value_as_proportion_of_capital=per_contract_value_as_proportion_of_capital,
        )
        return weights_as_proportion_of_capital

    def get_per_contract_value_as_proportion_of_capital(
        self, instrument_code: str
    ) -> pd.Series:
        trading_capital = self.get_trading_capital()
        contract_values = self.get_baseccy_value_per_contract(instrument_code)

        per_contract_value_as_proportion_of_capital = contract_values / trading_capital

        return per_contract_value_as_proportion_of_capital

    def get_baseccy_value_per_contract(self, instrument_code: str) -> pd.Series:
        contract_prices = self.get_contract_prices(instrument_code)
        contract_multiplier = self.get_contract_multiplier(instrument_code)
        fx_rate = self.get_fx_for_contract(instrument_code)

        fx_rate_aligned = fx_rate.reindex(contract_prices.index, method="ffill")

        return fx_rate_aligned * contract_prices * contract_multiplier

    def annualised_percentage_vol(self, instrument_code: str) -> pd.Series:
        daily_vol = self.daily_percentage_vol100scale(instrument_code)
        return ROOT_BDAYS_INYEAR * daily_vol / 100.0

    ## INPUT
    def get_instrument_list(
        self, for_instrument_weights=False, auto_remove_bad_instruments=False
    ) -> list:
        instrument_list = self.parent.get_instrument_list()
        if for_instrument_weights:
            instrument_list = copy(instrument_list)
            allocate_zero_instrument_weights_to_these_instruments = (
                self.allocate_zero_instrument_weights_to_these_instruments(
                    auto_remove_bad_instruments
                )
            )

            for (
                instrument_code_to_remove
            ) in allocate_zero_instrument_weights_to_these_instruments:
                if instrument_code_to_remove in instrument_list:
                    instrument_list.remove(instrument_code_to_remove)

        return instrument_list

    ## INPUTS
    def daily_percentage_vol100scale(self, instrument_code: str) -> pd.Series:
        return self.rawdata.get_daily_percentage_volatility(instrument_code)

    def get_percentage_vol_target(self) -> float:
        return self.position_size_stage.get_percentage_vol_target()

    def get_trading_capital(self) -> float:
        return self.position_size_stage.get_notional_trading_capital()

    def get_contract_prices(self, instrument_code: str) -> pd.Series:
        return self.position_size_stage.get_underlying_price(instrument_code)

    def get_contract_multiplier(self, instrument_code: str) -> float:
        return float(self.data.get_value_of_block_price_move(instrument_code))

    def get_fx_for_contract(self, instrument_code: str) -> pd.Series:
        return self.position_size_stage.get_fx_rate(instrument_code)

    ## stages
    @property
    def rawdata(self):
        return self.parent.rawdata

    @property
    def data(self):
        return self.parent.data

    @property
    def accounts_stage(self):
        try:
            accounts_stage = getattr(self.parent, "accounts")
        except AttributeError as e:
            raise missingData from e

        return accounts_stage

    @property
    def config(self) -> Config:
        return self.parent.config

    @property
    def position_size_stage(self) -> PositionSizing:
        return self.parent.positionSize


def get_portfolio_weights_from_contract_positions(
    contract_positions: pd.Series,
    per_contract_value_as_proportion_of_capital: pd.Series,
) -> pd.Series:
    aligned_values = per_contract_value_as_proportion_of_capital.reindex(
        contract_positions.index, method="ffill"
    )
    weights_as_proportion_of_capital = contract_positions * aligned_values

    return weights_as_proportion_of_capital


if __name__ == "__main__":
    import doctest

    doctest.testmod()

```

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.