# Xpress Insight Portfolio Optimization - Getting started example
# Copyright (c) 2025 Fair Isaac Corporation. All rights reserved.

import xpressinsight as xi
import xpress as xp
import pandas as pd
import sys

@xi.AppConfig(name="Portfolio Optimization", version=xi.AppVersion(1, 0, 0))
class InsightApp(xi.AppBase):
    # Input entities
    MaxHighRisk: xi.types.Param(default=1/3)
    MaxPerShare: xi.types.Param(default=0.3)
    MinNorthAmerica: xi.types.Param(default=0.5)
    ShareIds: xi.types.Index(dtype=xi.string, alias="Shares")

    # Input and result entities indexed over ShareIds
    Shares: xi.types.DataFrame(index="ShareIds", columns=[
        xi.types.Column("Return", dtype=xi.real, alias="Expected Return on Investment"),
        xi.types.Column("HighRisk", dtype=xi.boolean, alias="High-risk value"),
        xi.types.Column("NorthAmerica", dtype=xi.boolean, alias="Issued in North America"),
        xi.types.Column("fraction", dtype=xi.real, alias="Fraction used", manage=xi.Manage.RESULT)
    ])

    # Result entities
    TotalReturn: xi.types.Scalar(dtype=xi.real, alias="Total expected return on investment", manage=xi.Manage.RESULT)

    CtrSolIds: xi.types.Index(dtype=xi.string, alias="Constraints", manage=xi.Manage.RESULT)
    CtrSol: xi.types.DataFrame(index="CtrSolIds", columns=[
        xi.types.Column("Activity", dtype=xi.real, alias="Activity", manage=xi.Manage.RESULT),
        xi.types.Column("LowerLimit", dtype=xi.real, alias="Lower limit", manage=xi.Manage.RESULT),
        xi.types.Column("UpperLimit", dtype=xi.real, alias="Upper limit", manage=xi.Manage.RESULT),
    ])
    
    
    CtrSumIds: xi.types.Index(dtype=xi.string, alias="Constraints", manage=xi.Manage.RESULT)
    CtrSummary: xi.types.Series(index="CtrSumIds", dtype=xi.real, manage=xi.Manage.RESULT)

    # Constant class attribute
    DATAFILE = "shares.csv"

    @xi.ExecModeLoad(descr="Load input data and initialize all input entities.")
    def load(self):
        print("Loading data.")
        self.Shares = pd.read_csv(InsightApp.DATAFILE, index_col=['ShareIds'])
        self.ShareIds = self.Shares.index
        print("Loading finished.")

    @xi.ExecModeRun(descr="Solve problem and initialize all result entities.")
    def run(self):
        print('Starting optimization.')

        # Create Xpress problem and variables
        p = xp.problem("portfolio")
        self.Shares['fractionVar'] = pd.Series(p.addVariables(self.ShareIds, vartype=xp.continuous, name='fractionVar'))

        # Objective: expected total return
        objective = xp.Sum(self.Shares.Return * self.Shares.fractionVar)
        p.setObjective(objective, sense=xp.maximize)

        # Limit the percentage of high-risk values
        limit_high_risk = \
            xp.Sum(self.Shares.loc[self.Shares.HighRisk, 'fractionVar']) \
            <= self.MaxHighRisk
        p.addConstraint(limit_high_risk)

        # Minimum amount of North-American values
        limit_north_america = \
            xp.Sum(self.Shares.loc[self.Shares.NorthAmerica, 'fractionVar']) \
            >= self.MinNorthAmerica
        p.addConstraint(limit_north_america)

        # Spend all the capital
        p.addConstraint(xp.Sum(self.Shares.fractionVar) == 1)

        # Upper bounds on the investment per share
        p.addConstraint(share.fractionVar <= self.MaxPerShare for share_id, share in self.Shares.iterrows())

        # Solve optimization problem
        p.optimize()

        # Save results and key indicator values for GUI display
        self.Shares["fraction"] = pd.Series(p.getSolution(), index=self.ShareIds)
        self.TotalReturn = p.attributes.objval

        # Programmatic logic to compute evaluation metrics for constraints
        self.CtrSol = pd.DataFrame(columns=["Activity", "LowerLimit", "UpperLimit"])
        self.CtrSol.loc["Limit high risk shares"] = {
            "Activity": sum(self.Shares.loc[self.Shares.HighRisk, 'fraction']),
            "LowerLimit": 0,
            "UpperLimit": self.MaxHighRisk}

        self.CtrSol.loc["Limit North-American"] = {
            "Activity": sum(self.Shares.loc[self.Shares.NorthAmerica, 'fraction']),
            "LowerLimit": self.MinNorthAmerica,
            "UpperLimit": 1}

        for s in self.ShareIds:
            if self.Shares.loc[s, 'fraction'] > 0:
                self.CtrSol.loc[f"Limit per value: {s}"] = {
                    "Activity": self.Shares.loc[s, 'fraction'],
                    "LowerLimit": 0,
                    "UpperLimit": self.MaxPerShare}

        self.CtrSolIds = self.CtrSol.index
        
        self.CtrSummary = pd.Series()
        self.CtrSummary.loc["Largest position"] = max(self.Shares.fraction)
        self.CtrSummary.loc["Total high risk shares"] = sum(self.Shares.loc[self.Shares.HighRisk, 'fraction'])
        self.CtrSummary.loc["Total North-American"] = sum(self.Shares.loc[self.Shares.NorthAmerica, 'fraction'])
        self.CtrSummary.loc["Total return"] = p.attributes.objval
        
        self.CtrSumIds = self.CtrSummary.index
        
        print('Optimization finished.')


if __name__ == "__main__":
    app = xi.create_app(InsightApp)
    sys.exit(app.call_exec_modes(["LOAD", "RUN"]))
