boosted.api.api_type

   1# Copyright (C) 2020 Gradient Boosted Investments, Inc. - All Rights Reserved
   2
   3import copy
   4import datetime as dt
   5import json
   6import logging
   7import re
   8import string
   9from collections import defaultdict
  10from dataclasses import dataclass
  11from enum import Enum
  12from typing import Dict, List, Literal, Optional, Union
  13
  14import numpy as np
  15import pandas as pd
  16
  17logger = logging.getLogger("boosted.api.api_type")
  18
  19BoostedDate = Union[dt.date, str]
  20
  21
  22class Status(Enum):
  23    UNKNOWN = 0
  24    FAIL = 1
  25    SUCCESS = 2
  26
  27
  28class DataAddType(Enum):
  29    CREATION = 1
  30    HISTORICAL = 2
  31    LIVE = 3
  32
  33    def __str__(self):
  34        if self.name == "CREATION":
  35            return "CREATION"
  36        elif self.name == "HISTORICAL":
  37            return "HISTORICAL"
  38        elif self.name == "LIVE":
  39            return "LIVE"
  40
  41
  42class ChunkStatus(Enum):
  43    PROCESSING = "PROCESSING"
  44    ABORTED = "ABORTED"
  45    SUCCESS = "SUCCESS"
  46    WARNING = "WARNING"
  47    ERROR = "ERROR"
  48
  49
  50class DataSetType(Enum):
  51    UNKNOWN = 0
  52    STOCK = 1
  53    GLOBAL = 2
  54    STRATEGY = 3
  55
  56    def __str__(self):
  57        if self.name == "STOCK":
  58            return "STOCK"
  59        elif self.name == "GLOBAL":
  60            return "GLOBAL"
  61        elif self.name == "STRATEGY":
  62            return "STRATEGY"
  63        else:
  64            return "UNKNOWN"
  65
  66
  67class DataSetSubType(Enum):
  68    UNKNOWN = 0
  69    DENSE = 1
  70    SPARSE_HIST = 2
  71    SPARSE_FWD = 3
  72
  73    def __str__(self):
  74        if self.name == "DENSE":
  75            return "DENSE"
  76        elif self.name == "SPARSE_HIST":
  77            return "SPARSE_HIST"
  78        elif self.name == "SPARSE_FWD":
  79            return "SPARSE_FWD"
  80        else:
  81            return "UNKNOWN"
  82
  83
  84class DataSetFrequency(Enum):
  85    UNKNOWN = 0
  86    DAILY = 1
  87    WEEKLY = 2
  88    MONTHLY = 3
  89    QUARTERLY = 4
  90    SEMIANNUAL = 5
  91    ANNUAL = 6
  92
  93    def __str__(self):
  94        if self.name == "DAILY":
  95            return "DAILY"
  96        elif self.name == "WEEKLY":
  97            return "WEEKLY"
  98        elif self.name == "MONTHLY":
  99            return "MONTHLY"
 100        elif self.name == "QUARTERLY":
 101            return "QUARTERLY"
 102        elif self.name == "SEMIANNUAL":
 103            return "SEMIANNUAL"
 104        elif self.name == "ANNUAL":
 105            return "ANNUAL"
 106        else:
 107            return "UNKNOWN"
 108
 109
 110class ColumnRole(Enum):
 111    UNKNOWN = 0
 112    VARIABLE = 1
 113    GOAL = 2
 114    METRIC = 3
 115    IDENTIFIER = 4
 116
 117    def __str__(self):
 118        if self.name == "VARIABLE":
 119            return "VARIABLE"
 120        elif self.name == "GOAL":
 121            return "GOAL"
 122        elif self.name == "METRIC":
 123            return "METRIC"
 124        elif self.name == "IDENTIFIER":
 125            return "IDENTIFIER"
 126        else:
 127            return "UNKNOWN"
 128
 129
 130class ColumnSubRole(Enum):
 131    UNKNOWN = 0
 132    GBI_ID = 1
 133    ISIN = 2
 134    GVKEY = 3
 135    IID = 4
 136    SYMBOL = 5
 137    COUNTRY = 6
 138    CURRENCY = 7
 139    DATE = 8
 140    COMPANY_NAME = 9
 141    REPORT_DATE = 10
 142    REPORT_PERIOD = 11
 143
 144    def __str__(self):
 145        if self.name == "GBI_ID":
 146            return "GBI_ID"
 147        elif self.name == "ISIN":
 148            return "ISIN"
 149        elif self.name == "GVKEY":
 150            return "GVKEY"
 151        elif self.name == "IID":
 152            return "IID"
 153        elif self.name == "SYMBOL":
 154            return "SYMBOL"
 155        elif self.name == "COUNTRY":
 156            return "COUNTRY"
 157        elif self.name == "CURRENCY":
 158            return "CURRENCY"
 159        elif self.name == "DATE":
 160            return "DATE"
 161        elif self.name == "COMPANY_NAME":
 162            return "COMPANY_NAME"
 163        elif self.name == "REPORT_DATE":
 164            return "REPORT_DATE"
 165        elif self.name == "REPORT_PERIOD":
 166            return "REPORT_PERIOD"
 167        else:
 168            return "UNKNOWN"
 169
 170    def get_match(column_name):
 171        def clean_str(name):
 172            translation = str.maketrans("", "", string.punctuation)
 173            clean_name = name.strip().translate(translation).lower()
 174            return re.sub(r"\s+", "", clean_name)
 175
 176        identifiers = [
 177            ColumnSubRole.GBI_ID,
 178            ColumnSubRole.ISIN,
 179            ColumnSubRole.GVKEY,
 180            ColumnSubRole.IID,
 181            ColumnSubRole.SYMBOL,
 182            ColumnSubRole.COUNTRY,
 183            ColumnSubRole.CURRENCY,
 184            ColumnSubRole.COMPANY_NAME,
 185            ColumnSubRole.REPORT_DATE,
 186            ColumnSubRole.REPORT_PERIOD,
 187        ]
 188
 189        for identifier in identifiers:
 190            if clean_str(str(identifier)) == clean_str(column_name):
 191                return identifier
 192        return None
 193
 194
 195class ColumnValueType(Enum):
 196    UNKNOWN = 0
 197    NUMBER = 1
 198    STRING = 2
 199
 200    def __str__(self):
 201        if self.name == "UNKNOWN":
 202            return "UNKNOWN"
 203        elif self.name == "NUMBER":
 204            return "NUMBER"
 205        elif self.name == "STRING":
 206            return "STRING"
 207        else:
 208            return "UNKNOWN"
 209
 210
 211class ColumnConfig:
 212    def __init__(
 213        self,
 214        name=None,
 215        role=None,
 216        sub_role=None,
 217        value_type=ColumnValueType.NUMBER,
 218        description="",
 219        investment_horizon=None,
 220    ):
 221        self.name = name
 222        self.role = role
 223        self.sub_role = sub_role
 224        self.value_type = value_type
 225        # More value_types will be supported in the future.
 226        self.description = description
 227        self.investment_horizon = investment_horizon
 228
 229    def __str__(self):
 230        return 'Name: "{0}", Role: {1}, SubRole: {2}, Value type: {3}, Desc: {4} IH: {5}.'.format(
 231            self.name,
 232            self.role,
 233            self.sub_role,
 234            self.value_type,
 235            self.description,
 236            self.investment_horizon,
 237        )
 238
 239    def __repr__(self):
 240        return self.__str__()
 241
 242
 243class StrategyConfig:
 244    def __init__(self, name=None, source_name=None):
 245        self.name = name
 246        self.source_name = source_name
 247
 248    def __str__(self):
 249        return 'Name: "{0}", Source Name: {1}.'.format(self.name, self.source_name)
 250
 251    def __repr__(self):
 252        return self.__str__()
 253
 254
 255class BoostedDataSetSchemaException(Exception):
 256    def __init__(self, value, data=None):
 257        self.value = value
 258        self.data = data
 259
 260    def __str__(self):
 261        return repr(self.value)
 262
 263
 264class DataSetConfig:
 265    def __init__(
 266        self,
 267        name,
 268        datasetType=DataSetType.STOCK,
 269        datasetSubType=DataSetSubType.DENSE,
 270        datasetFrequency=DataSetFrequency.DAILY,
 271    ):
 272        self.name = name
 273        self.type = datasetType
 274        self.subtype = datasetSubType
 275        self.frequency = datasetFrequency
 276        self.columns = []
 277        self.strategies = []
 278
 279    def addColumn(self, columnConfig):
 280        self.columns.append(columnConfig)
 281
 282    def addStrategy(self, strategyConfig):
 283        self.strategies.append(strategyConfig)
 284
 285    def __getColumnByName(self, col_name):
 286        for column in self.columns:
 287            if col_name == column.name:
 288                return column
 289        raise BoostedDataSetSchemaException(f"Unable to find column {col_name}")
 290
 291    def updateColumnToGoal(self, col_name, investment_horizon):
 292        column = self.__getColumnByName(col_name)
 293        column.role = ColumnRole.GOAL
 294        column.investment_horizon = int(investment_horizon)
 295
 296    def updateColumnToMetric(self, col_name):
 297        column = self.__getColumnByName(col_name)
 298        column.role = ColumnRole.METRIC
 299
 300    def updateColumnToVariable(self, col_name):
 301        column = self.__getColumnByName(col_name)
 302        column.role = ColumnRole.VARIABLE
 303
 304    def __validateSchema(self):
 305        num_goals = 0
 306        num_metrics = 0
 307        dt = self.type
 308        dst = self.subtype
 309        dsf = self.frequency
 310        if len(self.columns) == 0:
 311            msg = "No feature columns exist."
 312            raise BoostedDataSetSchemaException(msg)
 313
 314        if dst in [DataSetSubType.SPARSE_HIST, DataSetSubType.SPARSE_FWD]:
 315            if dsf not in [DataSetFrequency.QUARTERLY]:
 316                msg = f"{dsf} frequency is not supported for {dst} sub data"
 317                raise BoostedDataSetSchemaException(msg)
 318            if dt not in [DataSetType.STOCK]:
 319                msg = f"{dst} subtype is not supported for {dt} data"
 320                raise BoostedDataSetSchemaException(msg)
 321
 322        for column in self.columns:
 323            if column.role == ColumnRole.GOAL:
 324                ih = column.investment_horizon
 325                gn = column.name
 326                if dt == DataSetType.GLOBAL:
 327                    msg = f"{dt} data can not have {ColumnRole.GOAL} type"
 328                    raise BoostedDataSetSchemaException(msg)
 329                if not isinstance(ih, int):
 330                    msg = f"Investment horizon for {gn} must be an integer"
 331                    raise BoostedDataSetSchemaException(msg)
 332                if ih < 1 or ih > 252:
 333                    msg = f"Investment horizon must be between 1 and 252 for {gn}"
 334                    raise BoostedDataSetSchemaException(msg)
 335                num_goals += 1
 336            elif column.role == ColumnRole.METRIC:
 337                if dt in [DataSetType.GLOBAL, DataSetType.STOCK]:
 338                    msg = f"{dt} data can not have {ColumnRole.METRIC} type"
 339                    raise BoostedDataSetSchemaException(msg)
 340                num_metrics += 1
 341
 342        if dt == DataSetType.STRATEGY:
 343            if num_goals == 0:
 344                msg = "Independent data requires at least one goal."
 345                raise BoostedDataSetSchemaException(msg)
 346            if num_metrics == 0:
 347                msg = "Independent data requires at least one metric."
 348                raise BoostedDataSetSchemaException(msg)
 349            if len(self.strategies) <= 1:
 350                msg = "Independent data requires more than 1 strategy"
 351                raise BoostedDataSetSchemaException(msg)
 352
 353    def toDict(self):
 354        self.__validateSchema()
 355        config = {}
 356        config["name"] = self.name
 357        config["type"] = str(self.type)
 358        config["subType"] = str(self.subtype)
 359        config["frequency"] = str(self.frequency)
 360        featureList = []
 361        for i, f in enumerate(self.columns):
 362            fm = {}
 363            fm["name"] = f.name
 364            fm["description"] = f.description
 365            fm["type"] = str(f.role)
 366            fm["role"] = str(f.role)
 367            fm["subRole"] = str(f.sub_role) if f.sub_role is not None else None
 368            fm["valuesType"] = str(f.value_type)
 369            fm["columnName"] = f.name
 370            fm["investmentHorizon"] = f.investment_horizon
 371            featureList.append(fm)
 372        config["features"] = featureList
 373        strategyList = []
 374        for i, s in enumerate(self.strategies):
 375            sm = {}
 376            sm["name"] = s.name
 377            sm["sourceName"] = s.source_name
 378            strategyList.append(sm)
 379        config["strategies"] = strategyList
 380        return config
 381
 382    def __str__(self):
 383        a = ""
 384        a += "Name: {0}\n".format(self.name)
 385        a += "Columns: \n"
 386        for c in self.columns:
 387            a += "  {0}\n".format(c)
 388        return a
 389
 390    def __repr__(self):
 391        return self.__str__()
 392
 393    def fromDict(schema):
 394        subtype = DataSetSubType[schema.get("subtype", str(DataSetSubType.DENSE))]
 395        frequency = DataSetFrequency[schema.get("frequency", str(DataSetFrequency.DAILY))]
 396        config = DataSetConfig(
 397            schema["name"],
 398            datasetType=DataSetType[schema["type"]],
 399            datasetSubType=subtype,
 400            datasetFrequency=frequency,
 401        )
 402        # two things left to fill in - strategies and columns
 403        for strategy_data in schema["strategies"]:
 404            config.addStrategy(
 405                StrategyConfig(name=strategy_data["name"], source_name=strategy_data["sourceName"])
 406            )
 407
 408        for feature_data in schema["features"]:
 409            config.addColumn(
 410                ColumnConfig(
 411                    name=feature_data["columnName"],
 412                    description=feature_data["description"] or "",
 413                    investment_horizon=feature_data["investmentHorizon"],
 414                    value_type=ColumnValueType[feature_data["valuesType"]]
 415                    if feature_data["valuesType"] is not None
 416                    else ColumnValueType.NUMBER,
 417                    role=ColumnRole[feature_data["role"]]
 418                    if feature_data["role"] is not None
 419                    else None,
 420                    sub_role=ColumnSubRole[feature_data["subRole"]]
 421                    if feature_data["subRole"] is not None
 422                    else None,
 423                )
 424            )
 425
 426        config.__validateSchema()
 427        return config
 428
 429
 430class GbiIdSecurityException(Exception):
 431    def __init__(self, value, data=None):
 432        self.value = value
 433        self.data = data
 434
 435    def __str__(self):
 436        return repr(self.value)
 437
 438
 439class GbiIdSecurity:
 440    def __init__(self, gbi_id, isin_country_currency_date, ticker, company_name):
 441        self.gbi_id = gbi_id
 442        self.isin_info = isin_country_currency_date
 443        self.ticker = ticker
 444        self.company_name = company_name
 445
 446    def __str__(self):
 447        return '\n(gbi_id: "{0}", isin_info: {1}, ticker: {2}, company_name: {3})'.format(
 448            self.gbi_id, self.isin_info.__str__(), self.ticker, self.company_name
 449        )
 450
 451    def __repr__(self):
 452        return self.__str__()
 453
 454    def __eq__(self, __o: object) -> bool:
 455        return self.gbi_id == __o.gbi_id
 456
 457    def __hash__(self):
 458        return hash(self.gbi_id)
 459
 460
 461class DateIdentCountryCurrencyException(Exception):
 462    def __init__(self, value, data=None):
 463        self.value = value
 464        self.data = data
 465
 466    def __str__(self):
 467        return repr(self.value)
 468
 469
 470# for backward compatibility
 471class DateIsinCountryCurrencyException(DateIdentCountryCurrencyException):
 472    pass
 473
 474
 475class DateIdentCountryCurrency:
 476    def __init__(
 477        self,
 478        date: str,
 479        identifier: str,
 480        country: Optional[str] = None,
 481        currency: Optional[str] = None,
 482        id_type: ColumnSubRole = ColumnSubRole.ISIN,
 483    ):
 484        self.date = date
 485        self.identifier = identifier
 486        self.id_type = id_type
 487        self.country = country
 488        self.currency = currency
 489        self.__validateInput()
 490
 491    def __str__(self):
 492        return '(date: "{0}", identifier: "{1}", country: {2}, currency: {3})'.format(
 493            self.date, self.identifier, self.country, self.currency
 494        )
 495
 496    def __repr__(self):
 497        return self.__str__()
 498
 499    def __validateInput(self):
 500        # simply ensure that at least isin and date are filled out.
 501        # country/currency defaults to ANY if not filled out, but
 502        # still is not recommended.
 503        if self.identifier is None or self.date is None:
 504            raise DateIdentCountryCurrencyException(
 505                "ISIN or Date not provided while constructing DateIsinCountryCurrency"
 506            )
 507
 508        if self.id_type not in (ColumnSubRole.ISIN, ColumnSubRole.SYMBOL):
 509            raise DateIdentCountryCurrencyException(f"Invalid ID Type: {self.id_type}")
 510
 511        def check_is_str(value, fieldname):
 512            if value is not None and not isinstance(value, str):
 513                raise DateIdentCountryCurrencyException(
 514                    f"Field {fieldname} in DateIsinCountryCurrency was not a string."
 515                )
 516
 517        check_is_str(self.date, "date")
 518        check_is_str(self.identifier, "identifier")
 519        check_is_str(self.country, "country")
 520        check_is_str(self.currency, "currency")
 521
 522
 523# for backwards compatibility
 524class DateIsinCountryCurrency(DateIdentCountryCurrency):
 525    def __init__(self, date, isin, country, currency):
 526        super().__init__(date, isin, country, currency, ColumnSubRole.ISIN)
 527        self.isin = isin
 528
 529    def __str__(self):
 530        return '(date: "{0}", isin: "{1}", country: {2}, currency: {3})'.format(
 531            self.date, self.isin, self.country, self.currency
 532        )
 533
 534
 535class IdentifierToColumnMapping:
 536    """
 537    IdentifierToColumnMapping is a mapping from string to boosted.api.api_type.ColumnSubRole.
 538    The key of the mapping corresponds to a CSV column name, and the value corresponds
 539    to the type of identifier that it maps to. Only columns specifying an identifying factor
 540    for the stock needs to be specified (e.g. ISIN, Date, Symbol, Currency)
 541    """
 542
 543    def __init__(self, mapping):
 544        self.mapping = mapping
 545        self.__validateInput()
 546
 547    def __str__(self):
 548        return repr(self.value)
 549
 550    def __validateInput(self):
 551        def validate_key_val(key, val):
 552            if not isinstance(key, str):
 553                raise Exception(f"key in IdentifierToColumnMapping {key} was not a string!")
 554            if not isinstance(val, ColumnSubRole):
 555                raise Exception(f"val in IdentifierToColumnMapping {val} was not a ColumnSubRole!")
 556
 557        for key, val in self.mapping:
 558            validate_key_val(key, val)
 559
 560
 561class PortfolioSettings:
 562    """
 563    This config is a documentation wrapper around a dict representing the portfolio settings json.
 564    Keys missing in the dict provided will fallback to the listed default when updating the
 565    the portfolio settings on the server.
 566
 567    Parameters
 568    ----------
 569    activeRisk: bool
 570        - **Active Risk**:
 571            Set the portfolio optimization parameters to be relative to the score from the stock
 572            universe. i.e. if you set Momentum to 0.5 to 1.0 and the stock universe has a market cap
 573            weighed score of 0.2 then the optimizer will solve for 0.7 to 1.2.
 574        - Constraints:
 575            Boolean value
 576        - Default Value: false
 577
 578    allowCompanyCollisions: bool
 579        - **Allow Multiple Securities Per Company**:
 580            Allows the machine to trade multiple securities (at the same time) for the same company.
 581            If it is ON and a company has listed PREF shares and COMMON shares, both will show up as
 582            tradeable options for that company. If it is OFF then system will try to determine the
 583            "correct" security to trade based on volume, liquidity, and share type.
 584        - Constraints:
 585            Boolean value
 586        - Default Value: true
 587
 588    allowDR: bool
 589        - **Allow Depositary Receipts**:
 590            Companies with tickers ending in .Y and .F will be disallowed from trading within the
 591            model with this setting off. Turn it on to allow trading in these securities.
 592        - Constraints:
 593            Boolean value
 594        - Default Value: true
 595
 596    benchmark: array
 597        - **Benchmark**:
 598            Set one or more benchmarks and the weighting for each.
 599        - Constraints:
 600            Required.
 601            List of dicts with keys:
 602            gbi_id: integer representing the GBI ID of the benchmark index.
 603            weight: floating point value between 0 and 1.
 604            All gbi_id's in the dictionary must be unique, and the benchmark weights must sum to a
 605            number between 0 and 1.
 606        - Default Value: []
 607
 608    benchmarkAllocation: float
 609        - **Benchmark Allocation**:
 610            None
 611        - Constraints:
 612            Floating point value representing a percentage between -100 and 100
 613        - Default Value: 0
 614
 615    beta_neutral: bool
 616        - **Beta Neutral**:
 617            Adjust the weights to get the ex-ante Beta to be the net exposure of the portfolio. i.e.
 618            70% Long / 30% Short = 0.4 Beta"
 619        - Constraints:
 620            Boolean value
 621        - Default Value: false
 622
 623    bounds_method: str | null
 624        - **Optimizer Bounds**:
 625            Tight: The optimizer will only allow the weightings of securities in your portfolio to
 626            stay tight (close) to their initial weighting.
 627            Loose: The optimizer will allow the weightings of securities in your portfolio to move a
 628            medium amount more from their initial weighting.
 629            Wide: The optimizer will allow almost any weight between the minimum and maximum weight
 630            per long or short position.
 631        - Constraints:
 632            String enum of {loose, tight, wide}, or null
 633        - Default Value: null
 634
 635    compounding: bool
 636        - **Compound Returns**:
 637            When toggled on, this will allow the portfolio to compound returns.
 638        - Constraints:
 639            Boolean value
 640        - Default Value: true
 641
 642    currency: str
 643        - **Currency**:
 644            The currency you would like your portfolio to be calculated in. Note that if the
 645            currency differs from its exchange (i.e. GBP on the S&P), the prior close foreign
 646            exchange rate will be used for calculations.
 647        - Constraints:
 648            String representing a 3 digit ISO currency code.
 649        - Default Value: "USD"
 650
 651    equalWeightBenchmark: bool
 652        - **Equal Weight Benchmark**:
 653            Instead of using the defined benchmarks, set the benchmark to be an equal weighted (per
 654            rebalance period) version of the stock universe.
 655        - Constraints:
 656            Boolean value
 657        - Default Value: false
 658
 659    executionDelay: int
 660        - **Execution Delay**:
 661            This option adds the number of days selected to the trade execution. If you select T+1
 662            and your rebalance period is set to Weekly (Monday), it will trade on Tuesday, and so
 663            on.
 664        - Constraints:
 665            Integer value between 0 and 100, inclusive.
 666        - Default Value: 0
 667
 668    factorNeutralizeSignal: bool
 669        - **Signal Optimizer**:
 670            Turn on optimization at the signal level that will adjust signals and rankings according
 671            to the specifications you set. This is done prior to portfolio construction and can help
 672            reduce bias in the model.
 673        - Constraints:
 674            Boolean value
 675        - Default Value: false
 676
 677    investmentHorizon: int
 678        - **Investment Horizon**:
 679            The investment horizon for the portfolio.
 680        - Constraints:
 681            An integer representing a number of days
 682        - Default Value: 21
 683
 684    lower_turnover: bool
 685        - **Lower Turnover**:
 686            If toggled on the optimizer will try to solve the secondary goal of optimizing the
 687            primary goals while limiting turnover.
 688        - Constraints:
 689            Boolean value
 690        - Default Value: false
 691
 692    marketCapBounds: float
 693        - **Maximum Market Cap Variation**:
 694            How far from the target market cap weighted index each stock can deviate (positive or
 695            negative).
 696        - Constraints:
 697            Floating point value representing a percentage between 0 and 10
 698        - Default Value: 1
 699
 700    marketCapConstraint: bool
 701        - **Constrain Market Cap**:
 702            Force weighting to be near the target weights for a market cap weighted index of your
 703            entire stock universe.
 704        - Constraints:
 705            Boolean value
 706        - Default Value: false
 707
 708    marketCapFlex: float
 709        - **Market Cap Flex**:
 710            How far from the target market cap weighted the stock can deviate relative to the
 711            existing weight (i.e. at 75% a 1% position cannot exceed 1.75%).
 712        - Constraints:
 713            Floating point value representing a percentage between 10 and 200
 714        - Default Value: 100
 715
 716    maxLongPosition: float
 717        - **Max Long Position per Stock**:
 718            The maximum weighting of a long position within the portfolio.
 719        - Constraints:
 720            Required. Floating point value representing a percentage between 0.0 and 300.0
 721        - Default Value: 10.0
 722
 723    maxNumStockLong: int
 724        - **Maximum Number of Longs**:
 725            The maximum number of stocks to buy. This is on a per “portfolio” basis, so if you have
 726            sector neutral turned on it will be maximum per sector.
 727        - Constraints:
 728            Integer value between 0 and 1000, inclusive. Must be >= minNumStockLong
 729        - Default Value: 50
 730
 731    maxNumStockShort: int
 732        - **Maximum Number of Shorts**:
 733            The maximum number of stocks to short. This is on a per “portfolio” basis, so if you
 734            have sector neutral turned on it will be maximum per sector.
 735        - Constraints:
 736            Integer value between 0 and 1000, inclusive. Must be >= minNumStockShort
 737        - Default Value: 50
 738
 739    maxOwnership: float
 740        - **Maximum Ownership**:
 741            The maximum percentage of a company the portfolio is allowed to own. For example, if a
 742            company had a market cap of $100MM and this was 5% the portfolio could not own more than
 743            $5MM of that company.
 744        - Constraints:
 745            Floating point value representing a percentage between 0 and 100
 746        - Default Value: 5
 747
 748    maxShortPosition: float
 749        - **Max Short Position per Stock**:
 750            The maximum weighting of a short position within the portfolio.
 751        - Constraints:
 752            Required. Floating point value representing a percentage between 0.0 and 300.0
 753        - Default Value: 5.0
 754
 755    minimumSharePrice: float
 756        - **Minimum Share Price**:
 757            The minimum share price you want the portfolio to allow.
 758        - Constraints:
 759            Floating point value between 0 and 100000
 760        - Default Value: 2
 761
 762    minLongPosition: float
 763        - **Min Long Position per Stock**:
 764            The minimum weighting of a long position within the portfolio.
 765        - Constraints:
 766            Floating point value representing a percentage between 0.0 and 300.0
 767        - Default Value: 0
 768
 769    minNumStockLong: int
 770        - **Minimum Number of Longs**:
 771            The minimum number of stocks to buy. This is on a per “portfolio” basis, so if you have
 772            sector neutral turned on it will be minimum per sector.
 773        - Constraints:
 774            Integer value between 0 and 1000, inclusive. Must be <= maxNumStockLong
 775        - Default Value: 5
 776
 777    minNumStockShort: int
 778        - **Minimum Number of Shorts**:
 779            The minimum number of stocks to short. This is on a per “portfolio” basis, so if you
 780            have sector neutral turned on it will be minimum per sector.
 781        - Constraints:
 782            Integer value between 0 and 1000, inclusive. Must be <= maxNumStockShort
 783        - Default Value: 5
 784
 785    minShortPosition: float
 786        - **Min Short Position per Stock**:
 787            The minimum weighting of a short position within the portfolio.
 788        - Constraints:
 789            Floating point value representing a percentage between 0.0 and 100.0
 790        - Default Value: 0
 791
 792    missingMarketCap: float
 793        - **Missing Market Cap**:
 794            The amount in millions (MM) to replace any missing market capitalization information
 795            with. This will impact the maximum ownership setting.
 796        - Constraints:
 797            Floating point value between 0 and 100
 798        - Default Value: 10
 799
 800    nameBasedSectorWeighting: bool
 801        - **Name Based Sector Weighting**:
 802            Base sector weightings on number of names. For example, if there are 200 names in the
 803            index and financials has 20 companies, the weight of financials should be 10% (20/200).
 804        - Constraints:
 805            Boolean value
 806        - Default Value: false
 807
 808    netBenchmark: bool
 809        - **Adjust Benchmark for Net Exposure**:
 810            If your portfolio has a net exposure greater or less than 100%, this will adjust the
 811            benchmark returns to match that net exposure.
 812        - Constraints:
 813            Boolean value
 814        - Default Value: false
 815
 816    optimizer: str
 817        - **Optimizer Type**:
 818            No Optimization: None
 819            Reduce Risk: Optimizer designed to reduce portfolio volatility. This tends to result in
 820            the best overall performance.
 821            Maximize Sharpe: Optimizer designed to maximize Sharpe using out of sample estimates for
 822            expected return. Estimates for expected return have an outsized impact on the
 823            performance of this optimizer.
 824            Maximize Alpha: Optimizer designed to maximize expected return based on out-of-sample
 825            estimates for expected return. Estimates for expected return have an outsized impact on
 826            the performance of this optimizer.
 827            Min VaR: Minimize Value at Risk by looking back at the proposed portfolio over the last
 828            year and trying to minimize drawdowns.
 829            Max VaR Sharpe: Maximize Value at Risk Sharpe by looking back at the proposed portfolio
 830            over the last year and trying to maximize Sharpe by observed return for the portfolio vs
 831            observed volatility.
 832            Minimize Skew: Minimize the skew of the portfolio by trying to find a set of portfolio
 833            weightings that results in returns that are closer to the mean.
 834        - Constraints:
 835            Required. String enum: one of
 836            default: No Optimization
 837            min_vol: Reduce Risk
 838            max_sharpe: Maximize Sharpe
 839            max_alpha: Maximize Alpha
 840            min_var: Min VaR
 841            max_var_sharpe: Max VaR Sharpe
 842            min_skew: Minimize Skew
 843        - Default Value: "default"
 844
 845    optimizeTurnover: bool
 846        - **Turnover Optimization**:
 847            Turnover optimization will reduce the turnover at the signal level, making that ranked
 848            list more stable.
 849        - Constraints:
 850            Boolean value
 851        - Default Value: false
 852
 853    pairsTrading: bool
 854        - **Pairs Trading**:
 855            Pairs Trading forces the portfolio to be created by going long / short an equal number
 856            of stocks. The portfolio will try to find an offsetting position for every long by
 857            finding securities that are both highly correlated and have a significant difference in
 858            star rating.
 859        - Constraints:
 860            Boolean value
 861        - Default Value: false
 862
 863    percentPortfolioLong: float
 864        - **Percent of Portfolio Long**:
 865            The exact sum of all long position weightings in the portfolio.
 866        - Constraints:
 867            Required. Floating point value representing a percentage between 0.0 and 300.0
 868        - Default Value: 100.0
 869
 870    percentPortfolioShort: float
 871        - **Percent of Portfolio Short**:
 872            The exact sum of all short position weightings in the portfolio.
 873        - Constraints:
 874            Required. Floating point value representing a percentage between 0.0 and 300.0
 875        - Default Value: 0.0
 876
 877    percentToLong: float
 878        - **Percent to Long**:
 879            The machine will attempt to buy the top X% of stocks, subject to the minimum and maximum
 880            specified. This is on a per “portfolio” basis, so if you have sector neutral turned on
 881            it will be top X% per sector. This will be subject to the maximum and minimum number of
 882            securities you specify below.
 883        - Constraints:
 884            Floating point value representing a percentage between 0 and 100
 885        - Default Value: 20.0
 886
 887    percentToShort: float
 888        - **Percent to Short**:
 889            The machine will attempt to short the bottom X% of stocks, subject to the minimum and
 890            maximum specified. This is on a per “portfolio” basis, so if you have sector neutral
 891            turned on it will be bottom X% per sector. This will be subject to the maximum and
 892            minimum number of securities you specify below.
 893        - Constraints:
 894            Floating point value representing a percentage between 0 and 100
 895        - Default Value: 20.0
 896
 897    portfolioStartingValue: float
 898        - **Portfolio Starting Value**:
 899            The value of the portfolio in MM when it begins trading. If its backtest spans from 2005
 900            - 2020, the starting value in 2005 will be whatever you set here.
 901        - Constraints:
 902            Floating point value between 0 and 100000.
 903        - Default Value: 100.0
 904
 905    priceCleaning: bool
 906        - **Price Cleaning**:
 907            Price Cleaning removes any very large price movements from the securities, specifically
 908            with a daily change greater than +500% or -83.33%. There are some legitimate securities
 909            with moves this large that will be impacted by turning this on, but the advantage is it
 910            prevents these securities from overwhelming the backtest.
 911        - Constraints:
 912            Boolean value
 913        - Default Value: true
 914
 915    priceType: str
 916        - **Price Type**:
 917            None
 918        - Constraints:
 919            Required. String enum of {CLOSE, OPEN, VWAP}
 920        - Default Value: "VWAP"
 921
 922    rebalancePeriod: str
 923        - **Rebalance Period**:
 924            When the machine rebalances the portfolio. You can choose a specific day if you prefer
 925            to execute trades on a specific day.
 926        - Constraints:
 927            Required.
 928            An enum value in the following list:
 929            "daily"
 930            "weekly": Rebalances on every Monday.
 931            Chooses the next available trade date to rebalance when Monday is a holiday
 932            "weekly_wednesday": Rebalances on every Wednesday.
 933            Chooses the next available trade date to rebalance when Wednesday is a holiday
 934            "weekly_friday": Rebalances on every Friday.
 935            Chooses the previous available trade date to rebalance when Friday is a holiday
 936            "monthly"
 937            "quarterly"
 938            OR a string in the following format: "custom_x" where x is an integer between 1 and 252
 939            (days).
 940        - Default Value: "weekly"
 941
 942    sectorNeutral: bool
 943        - **Sector Neutral**:
 944            Will be constructed as a series of portfolios - each matching the market cap weighting
 945            of each of the stock universe sectors. This means that min. and max. number of stocks
 946            "per portfolio" becomes "per sector".
 947        - Constraints:
 948            Boolean value
 949        - Default Value: false
 950
 951    sectorNeutralEqualWeightBenchmark: bool
 952        - **Sector Neutral Equal Weight Benchmark**:
 953            Instead of using the defined benchmarks, set the benchmark to be a "sector neutral"
 954            equal weighted (per rebalance period) version of the stock universe. This takes the
 955            sector weighting and divides by the number of stocks in that sector, so each sector will
 956            have a different individual security weighting.
 957        - Constraints:
 958            Boolean value
 959        - Default Value: false
 960
 961    sectorNeutralSpread: float
 962        - **Sector Neutral Spread**:
 963            The sector neutral spread is how far away from the benchmark sector allocation the
 964            machine can deviate. This is adjusted for net exposure, i.e. if your net exposure is 0%
 965            the target sector allocations will be 0%.
 966        - Constraints:
 967            Floating point value representing a percentage between 0 and 100
 968        - Default Value: 0
 969
 970    signalIndustryNeutral: bool
 971        - **Industry Neutral**:
 972            Make the signal industry neutral, plus or minus the signal sector neutral spread.
 973        - Constraints:
 974            Boolean value
 975        - Default Value: false
 976
 977    signalSectorNeutral: bool
 978        - **Sector Neutral**:
 979            Make the signal sector neutral, plus or minus the signal sector neutral spread.
 980        - Constraints:
 981            Boolean value
 982        - Default Value: false
 983
 984    signalSectorNeutralSpread: float
 985        - **Sector Neutral Spread**:
 986            Make the signal sector neutral, plus or minus the signal sector neutral spread.
 987        - Constraints:
 988            Floating point value representing a percentage between 0 and 100
 989        - Default Value: 1.0
 990
 991    smoothPeriods: int
 992        - **Smooth Periods**:
 993            The number of periods over which to smooth the signals
 994        - Constraints:
 995            Integer value between 1 and 100.
 996        - Default Value: 1
 997
 998    smoothWeighting: str
 999        - **Smooth Weighting Type**:
1000            Alpha Weight: The strength of the signal matters in the smoothing. For example, if a
1001            stock had a score of 5 stars in period A, 2 stars in period B, your 2 period smoothing
1002            would be 7 / 2 = 3.5.
1003            Equal Weight: Only the direction of the signal matters here. For example, if you were 5
1004            stars in period A and 2 stars in period B, your 2 period smoothing would be 0 / 2 = 0
1005            (+1 in period 1 and -1 in period 2)
1006        - Constraints:
1007            One of {alpha_weight, equal_weight}
1008        - Default Value: "alpha_weight"
1009
1010    stopGain: bool
1011        - **Enable Stop Gain**:
1012            Turn on stop gains for the portfolio. This forces sales of securities that have exceeded
1013            the stop gain level. All trades will occur at the next trading time (i.e. next day OPEN
1014            for OPEN price models).
1015        - Constraints:
1016            Boolean value
1017        - Default Value: false
1018
1019    stopGainAmount: float
1020        - **Stop Gain Percentage to Sell**:
1021            The percentage of the position the machine will sell if the stop gain is triggered.
1022        - Constraints:
1023            Floating point value representing a percentage between 1 and 100.
1024        - Default Value: 50.0
1025
1026    stopGainLevel: float
1027        - **Stop Gain Threshold**:
1028            Will sell positions after they hit a threshold. For example, 10% / stop gain, when a
1029            position gains 10% the machine will sell a portion of the position (defined by stop gain
1030            percentage).
1031        - Constraints:
1032            Floating point value representing a percentage between 1 and 100.
1033        - Default Value: 10.0
1034
1035    stopLoss: bool
1036        - **Enable Stop Loss**:
1037            Turn on stop losses for the portfolio. This forces sales of securities that have
1038            exceeded the stop loss level. All trades will occur at the next trading time (i.e. next
1039            day OPEN for OPEN price models).
1040        - Constraints:
1041            Boolean value
1042        - Default Value: false
1043
1044    stopLossAmount: float
1045        - **Stop Loss Percentage to Sell**:
1046            The percentage of the position the machine will sell if the stop loss is triggered.
1047        - Constraints:
1048            Floating point value representing a percentage between 1 and 100.
1049        - Default Value: 50.0
1050
1051    stopLossLevel: float
1052        - **Stop Loss Threshold**:
1053            Will sell positions after they hit a threshold. For example, 10% / stop loss, when a
1054            position loses 10% the machine will sell a portion of the position (defined by stop loss
1055            percentage).
1056        - Constraints:
1057            Floating point value representing a percentage between 1 and 100.
1058        - Default Value: 10.0
1059
1060    stopLossReset: bool
1061        - **Allow multiple stop loss/gain between rebalance days**:
1062            Allows the machine to repeatedly trigger stop losses within a rebalance period. For
1063            example, if you trade Mondays and the stock drops 15% per day during the week and the
1064            stop loss trigger is 10%, then every day will trigger the stop loss. If toggled off the
1065            stop loss can only be triggered once per stock per rebalance period.
1066        - Constraints:
1067            Boolean value
1068        - Default Value: false
1069
1070    trackingConstraint: float
1071        - **Maximum Tracking Error (vs Benchmark)**:
1072            Amount of ex-ante tracking error to constrain the optimizer by.
1073        - Constraints:
1074            Floating point value representing a percentage between 0 and 30
1075        - Default Value: 10
1076
1077    trackingError: bool
1078        - **Allocate Weight to Benchmark**:
1079            Allocate a portion (%) of portfolio to the benchmark
1080        - Constraints:
1081            Boolean value
1082        - Default Value: false
1083
1084    trackingErrorOptimizer: bool
1085        - **Tracking Error**:
1086            If toggled on the optimizer will try to solve for an expected tracking error less than
1087            the amount specified. This is done out-of-sample and the ex post tracking error will
1088            likely be higher.
1089        - Constraints:
1090            Boolean value
1091        - Default Value: false
1092
1093    tradingCost: float
1094        - **Trading Cost**:
1095            A cost that will be applied to every trade.
1096        - Constraints:
1097            Required. Floating point value representing a percentage between 0.0 and 5.0
1098        - Default Value: 0.01
1099
1100    turnoverImportance: float
1101        - **Turnover Importance**:
1102            High importance means the algorithm will aim to keep turnover low, whereas low
1103            importance of turnover will let it vary more.
1104        - Constraints:
1105            Floating point value between 0.2 and 5 nonlinearly mapped onto the 1-9 interval.
1106        - Default Value: 1
1107
1108    useHoldingPeriod: bool
1109        - **Use Holding Period**:
1110            Set any covariance or other calculations within the optimizer to use a holding period
1111            return instead of daily return series
1112        - Constraints:
1113            Boolean value
1114        - Default Value: false
1115
1116    weightingType: str
1117        - **Initial Weighting**:
1118            Alpha Weight: The initial weighting will be done such that higher ranked stocks have a
1119            higher weight.
1120            Equal Weight: The initial weighting will be done such that all stocks selected to be in
1121            the portfolio will have an equal weight.
1122            Market Cap Weight: The initial weighting will be done such that all stocks are weighted
1123            according to their previous day’s market capitalization.
1124        - Constraints:
1125            One of {alpha_weight, equal_weight, market_cap_weight}
1126        - Default Value: "alpha_weight"
1127    """
1128
1129    # note, this is just the collection of portfolio settings
1130    # that is exposed to the API:
1131    # SELECT key, default_value
1132    # FROM portfolio_settings_validation
1133    # WHERE exposed_to_client_api;
1134    # TODO: expand to entire settings? why this way initially?
1135    __default_settings = {
1136        "maxOwnership": "5",
1137        "pairsTrading": "false",
1138        "minNumStockLong": "5",
1139        "stopLossAmount": "50.0",
1140        "stopGainLevel": "10.0",
1141        "smoothWeighting": '"alpha_weight"',
1142        "minimumSharePrice": "2",
1143        "maxShortPosition": "5.0",
1144        "tradingCost": "0.01",
1145        "stopGainAmount": "50.0",
1146        "smoothPeriods": "1",
1147        "executionDelay": "0",
1148        "benchmarkAllocation": "0",
1149        "sectorNeutralSpread": "0",
1150        "percentToLong": "20.0",
1151        "maxNumStockLong": "50",
1152        "trackingConstraint": "10",
1153        "compounding": "true",
1154        "priceCleaning": "true",
1155        "lower_turnover": "false",
1156        "missingMarketCap": "10",
1157        "allowCompanyCollisions": "true",
1158        "allowDR": "true",
1159        "sectorNeutral": "false",
1160        "marketCapFlex": "100",
1161        "marketCapBounds": "1",
1162        "turnoverImportance": "1",
1163        "minNumStockShort": "5",
1164        "maxNumStockShort": "50",
1165        "beta_neutral": "false",
1166        "nameBasedSectorWeighting": "false",
1167        "trackingError": "false",
1168        "stopLoss": "false",
1169        "stopGain": "false",
1170        "stopLossReset": "false",
1171        "netBenchmark": "false",
1172        "equalWeightBenchmark": "false",
1173        "marketCapConstraint": "false",
1174        "signalIndustryNeutral": "false",
1175        "signalSectorNeutral": "false",
1176        "sectorNeutralEqualWeightBenchmark": "false",
1177        "percentToShort": "20.0",
1178        "trackingErrorOptimizer": "false",
1179        "currency": '"USD"',
1180        "rebalancePeriod": '"weekly"',
1181        "benchmark": "[]",
1182        "signalSectorNeutralSpread": "1.0",
1183        "optimizeTurnover": "false",
1184        "minShortPosition": "0",
1185        "minLongPosition": "0",
1186        "percentPortfolioLong": "100.0",
1187        "portfolioStartingValue": "100.0",
1188        "stopLossLevel": "10.0",
1189        "maxLongPosition": "10.0",
1190        "percentPortfolioShort": "0.0",
1191        "bounds_method": None,
1192        "optimizer": '"default"',
1193        "activeRisk": "false",
1194        "useHoldingPeriod": "false",
1195        "weightingType": '"alpha_weight"',
1196        "priceType": '"VWAP"',
1197        "factorNeutralizeSignal": "false",
1198        "investmentHorizon": "21",
1199    }
1200
1201    def __init__(self, settings: Optional[Dict] = None):
1202        # TODO: i'd rather have kwargs for the different keys and then allow
1203        # for per-key overrides, but we start with this more conservative approach
1204        if settings is None:
1205            self.settings = copy.deepcopy(PortfolioSettings.__default_settings)
1206        else:
1207            self.settings = settings
1208
1209    def toDict(self):
1210        return self.settings
1211
1212    def __str__(self):
1213        return json.dumps(self.settings)
1214
1215    def __repr__(self):
1216        return self.__str__()
1217
1218    def fromDict(settings):
1219        return PortfolioSettings(settings)
1220
1221
1222hedge_experiment_type = Literal["HEDGE", "MIMIC"]
1223
1224
1225@dataclass(frozen=True)  # from _python client's_ perspective, instances will never change!
1226class HedgeExperiment:
1227    id: str  # really a uuid, which we represent as a string
1228    name: str
1229    user_id: str  # see id comment
1230    description: str
1231    experiment_type: hedge_experiment_type  # TODO: enum worth it?
1232    last_calculated: dt.datetime
1233    last_modified: dt.datetime
1234    status: str  # TODO: enum worth it?
1235    portfolio_calculation_status: str  # TODO: enum worth it?
1236    selected_models: List[str]  # see id comment
1237    target_securities: Dict[GbiIdSecurity, float]  # Security is a hashable type because frozen=True
1238    target_portfolios: List[str]
1239    selected_stock_universe_ids: List[str]  # see id comment
1240    baseline_model: Optional[str] = None
1241    baseline_stock_universe_id: Optional[str] = None
1242    baseline_scenario: Optional["HedgeExperimentScenario"] = None
1243
1244    @property
1245    def config(self) -> Dict:
1246        """
1247        Returns a hedge experiment configuration dictionary, ready for JSON-serialization and
1248        submission.
1249        """
1250        weights_by_gbiid = [
1251            {"gbiId": sec.id, "weight": weight} for sec, weight in self.target_securities.items()
1252        ]
1253        return {
1254            "experimentType": self.experiment_type,
1255            "selectedModels": self.selected_models,
1256            "targetSecurities": weights_by_gbiid,
1257            "selectedStockUniverseIds": self._selected_stock_universe_ids,
1258        }
1259
1260    @classmethod
1261    def from_json_dict(cls, d: Dict) -> "HedgeExperiment":
1262        # drafts arent fully populated
1263        if d["status"] == "DRAFT":
1264            weights_by_security = {}
1265            selected_models = []
1266            selected_stock_universe_ids = []
1267        else:
1268            weights_by_security = {
1269                GbiIdSecurity(
1270                    sec_dict["gbiId"],
1271                    None,
1272                    sec_dict["security"]["symbol"],
1273                    sec_dict["security"]["name"],
1274                ): sec_dict["weight"]
1275                for sec_dict in d["targetSecurities"]
1276            }
1277            experiment_config = json.loads(d["config"])
1278            selected_models = experiment_config["selectedModels"]
1279            selected_stock_universe_ids = experiment_config["selectedStockUniverseIds"]
1280        baseline_scenario = None
1281        if d["baselineScenario"] is not None:
1282            baseline_scenario = HedgeExperimentScenario.from_json_dict(d["baselineScenario"])
1283        return cls(
1284            d["hedgeExperimentId"],
1285            d["experimentName"],
1286            d["userId"],
1287            d["description"],
1288            d["experimentType"],
1289            d["lastCalculated"],
1290            d["lastModified"],
1291            d["status"],
1292            d["portfolioCalcStatus"],
1293            selected_models,
1294            weights_by_security,
1295            d.get("targetPortfolios"),
1296            selected_stock_universe_ids or [],
1297            d.get("baselineModel"),
1298            d.get("baselineStockUniverseId"),
1299            baseline_scenario,
1300        )
1301
1302
1303@dataclass(frozen=True)
1304class HedgeExperimentScenario:
1305    id: str  # really a uuid, which we represent as a string;
1306    name: str
1307    description: str
1308    status: str  # TODO: enum worth it?
1309    settings: PortfolioSettings
1310    summary: pd.DataFrame
1311    # we currently a portfolios field. they get wrapped up into the summary table
1312
1313    @classmethod
1314    def from_json_dict(cls, d: Dict) -> "HedgeExperimentScenario":
1315        return cls(
1316            d["hedgeExperimentScenarioId"],
1317            d["scenarioName"],
1318            d["description"],
1319            d.get("status"),
1320            PortfolioSettings(json.loads(d["portfolioSettingsJson"])),
1321            HedgeExperimentScenario._build_portfolio_summary(d.get("hedgeExperimentPortfolios")),
1322        )
1323
1324    @staticmethod
1325    def _build_portfolio_summary(portfolios: Dict) -> pd.DataFrame:
1326        # this happens for added scenarios that havent run yet
1327        if portfolios is None:
1328            return pd.DataFrame()
1329
1330        df_dict = defaultdict(list)
1331        for pf in portfolios:
1332            p = pf["portfolio"]
1333
1334            df_dict["portfolio_id"].append(p["id"])
1335            df_dict["scenario_name"].append(p["name"])
1336            df_dict["model_id"].append(p["modelId"])
1337            df_dict["status"].append(p["status"])
1338
1339            if p["status"] != "READY":  # data isn't yet available
1340                df_dict["1M"].append(np.nan)
1341                df_dict["3M"].append(np.nan)
1342                df_dict["1Y"].append(np.nan)
1343                df_dict["sharpe"].append(np.nan)
1344                df_dict["vol"].append(np.nan)
1345            else:
1346                # NOTE:
1347                # these are the magic indices/keys of these values; inspect passed arg to see
1348                # if upstream changes the order of (sub-)elements in the response...uh oh
1349                # TODO: specific key search? wrap with try/except and fail loudly?
1350                df_dict["1M"].append(p["performanceGrid"][0][4])
1351                df_dict["3M"].append(p["performanceGrid"][0][5])
1352                df_dict["1Y"].append(p["performanceGrid"][0][7])
1353                df_dict["sharpe"].append(p["tearSheet"][0]["members"][1]["value"])
1354                pf_stddev = p["tearSheet"][1]["members"][1]["value"]
1355                bnchmk_stddev = p["tearSheet"][1]["members"][2]["value"]
1356                df_dict["vol"].append((pf_stddev - bnchmk_stddev) * 100)
1357                # TODO: this is how it is done on the front-end,
1358                # should be pulled out of both here and there
1359
1360        df = pd.DataFrame(df_dict)
1361        return df
1362
1363
1364@dataclass(frozen=True)  # from _python client's_ perspective, instances will never change!
1365class HedgeExperimentDetails:
1366    # this will probably lead to violations of the law of demeter, but it's easy to do right now,
1367    # particularly for an unproven API
1368    experiment: HedgeExperiment
1369    scenarios: Dict[str, HedgeExperimentScenario]
1370
1371    @property
1372    def summary(self) -> pd.DataFrame:
1373        """
1374        Returns a dataframe corresponding to the Hedge Experiment "flat view" - all portfolio
1375        options are displayed
1376        """
1377        if len(self.scenarios) == 0 or all(s.status != "READY" for s in self.scenarios.values()):
1378            return pd.DataFrame()
1379        return pd.concat(scenario.summary for scenario in self.scenarios.values()).sort_values(
1380            ["scenario_name"]
1381        )  # sort for stable presentation
1382
1383    @classmethod
1384    def from_json_dict(cls, d: Dict) -> "HedgeExperimentDetails":
1385        he = HedgeExperiment.from_json_dict(d)
1386        scenarios = {
1387            scenario_dict["hedgeExperimentScenarioId"]: HedgeExperimentScenario.from_json_dict(
1388                scenario_dict
1389            )
1390            for scenario_dict in d["hedgeExperimentScenarios"]
1391        }
1392        return cls(he, scenarios)
class Status(enum.Enum):
23class Status(Enum):
24    UNKNOWN = 0
25    FAIL = 1
26    SUCCESS = 2

An enumeration.

UNKNOWN = <Status.UNKNOWN: 0>
FAIL = <Status.FAIL: 1>
SUCCESS = <Status.SUCCESS: 2>
Inherited Members
enum.Enum
name
value
class DataAddType(enum.Enum):
29class DataAddType(Enum):
30    CREATION = 1
31    HISTORICAL = 2
32    LIVE = 3
33
34    def __str__(self):
35        if self.name == "CREATION":
36            return "CREATION"
37        elif self.name == "HISTORICAL":
38            return "HISTORICAL"
39        elif self.name == "LIVE":
40            return "LIVE"

An enumeration.

CREATION = <DataAddType.CREATION: 1>
HISTORICAL = <DataAddType.HISTORICAL: 2>
LIVE = <DataAddType.LIVE: 3>
Inherited Members
enum.Enum
name
value
class ChunkStatus(enum.Enum):
43class ChunkStatus(Enum):
44    PROCESSING = "PROCESSING"
45    ABORTED = "ABORTED"
46    SUCCESS = "SUCCESS"
47    WARNING = "WARNING"
48    ERROR = "ERROR"

An enumeration.

PROCESSING = <ChunkStatus.PROCESSING: 'PROCESSING'>
ABORTED = <ChunkStatus.ABORTED: 'ABORTED'>
SUCCESS = <ChunkStatus.SUCCESS: 'SUCCESS'>
WARNING = <ChunkStatus.WARNING: 'WARNING'>
ERROR = <ChunkStatus.ERROR: 'ERROR'>
Inherited Members
enum.Enum
name
value
class DataSetType(enum.Enum):
51class DataSetType(Enum):
52    UNKNOWN = 0
53    STOCK = 1
54    GLOBAL = 2
55    STRATEGY = 3
56
57    def __str__(self):
58        if self.name == "STOCK":
59            return "STOCK"
60        elif self.name == "GLOBAL":
61            return "GLOBAL"
62        elif self.name == "STRATEGY":
63            return "STRATEGY"
64        else:
65            return "UNKNOWN"

An enumeration.

UNKNOWN = <DataSetType.UNKNOWN: 0>
STOCK = <DataSetType.STOCK: 1>
GLOBAL = <DataSetType.GLOBAL: 2>
STRATEGY = <DataSetType.STRATEGY: 3>
Inherited Members
enum.Enum
name
value
class DataSetSubType(enum.Enum):
68class DataSetSubType(Enum):
69    UNKNOWN = 0
70    DENSE = 1
71    SPARSE_HIST = 2
72    SPARSE_FWD = 3
73
74    def __str__(self):
75        if self.name == "DENSE":
76            return "DENSE"
77        elif self.name == "SPARSE_HIST":
78            return "SPARSE_HIST"
79        elif self.name == "SPARSE_FWD":
80            return "SPARSE_FWD"
81        else:
82            return "UNKNOWN"

An enumeration.

UNKNOWN = <DataSetSubType.UNKNOWN: 0>
DENSE = <DataSetSubType.DENSE: 1>
SPARSE_HIST = <DataSetSubType.SPARSE_HIST: 2>
SPARSE_FWD = <DataSetSubType.SPARSE_FWD: 3>
Inherited Members
enum.Enum
name
value
class DataSetFrequency(enum.Enum):
 85class DataSetFrequency(Enum):
 86    UNKNOWN = 0
 87    DAILY = 1
 88    WEEKLY = 2
 89    MONTHLY = 3
 90    QUARTERLY = 4
 91    SEMIANNUAL = 5
 92    ANNUAL = 6
 93
 94    def __str__(self):
 95        if self.name == "DAILY":
 96            return "DAILY"
 97        elif self.name == "WEEKLY":
 98            return "WEEKLY"
 99        elif self.name == "MONTHLY":
100            return "MONTHLY"
101        elif self.name == "QUARTERLY":
102            return "QUARTERLY"
103        elif self.name == "SEMIANNUAL":
104            return "SEMIANNUAL"
105        elif self.name == "ANNUAL":
106            return "ANNUAL"
107        else:
108            return "UNKNOWN"

An enumeration.

UNKNOWN = <DataSetFrequency.UNKNOWN: 0>
DAILY = <DataSetFrequency.DAILY: 1>
WEEKLY = <DataSetFrequency.WEEKLY: 2>
MONTHLY = <DataSetFrequency.MONTHLY: 3>
QUARTERLY = <DataSetFrequency.QUARTERLY: 4>
SEMIANNUAL = <DataSetFrequency.SEMIANNUAL: 5>
ANNUAL = <DataSetFrequency.ANNUAL: 6>
Inherited Members
enum.Enum
name
value
class ColumnRole(enum.Enum):
111class ColumnRole(Enum):
112    UNKNOWN = 0
113    VARIABLE = 1
114    GOAL = 2
115    METRIC = 3
116    IDENTIFIER = 4
117
118    def __str__(self):
119        if self.name == "VARIABLE":
120            return "VARIABLE"
121        elif self.name == "GOAL":
122            return "GOAL"
123        elif self.name == "METRIC":
124            return "METRIC"
125        elif self.name == "IDENTIFIER":
126            return "IDENTIFIER"
127        else:
128            return "UNKNOWN"

An enumeration.

UNKNOWN = <ColumnRole.UNKNOWN: 0>
VARIABLE = <ColumnRole.VARIABLE: 1>
GOAL = <ColumnRole.GOAL: 2>
METRIC = <ColumnRole.METRIC: 3>
IDENTIFIER = <ColumnRole.IDENTIFIER: 4>
Inherited Members
enum.Enum
name
value
class ColumnSubRole(enum.Enum):
131class ColumnSubRole(Enum):
132    UNKNOWN = 0
133    GBI_ID = 1
134    ISIN = 2
135    GVKEY = 3
136    IID = 4
137    SYMBOL = 5
138    COUNTRY = 6
139    CURRENCY = 7
140    DATE = 8
141    COMPANY_NAME = 9
142    REPORT_DATE = 10
143    REPORT_PERIOD = 11
144
145    def __str__(self):
146        if self.name == "GBI_ID":
147            return "GBI_ID"
148        elif self.name == "ISIN":
149            return "ISIN"
150        elif self.name == "GVKEY":
151            return "GVKEY"
152        elif self.name == "IID":
153            return "IID"
154        elif self.name == "SYMBOL":
155            return "SYMBOL"
156        elif self.name == "COUNTRY":
157            return "COUNTRY"
158        elif self.name == "CURRENCY":
159            return "CURRENCY"
160        elif self.name == "DATE":
161            return "DATE"
162        elif self.name == "COMPANY_NAME":
163            return "COMPANY_NAME"
164        elif self.name == "REPORT_DATE":
165            return "REPORT_DATE"
166        elif self.name == "REPORT_PERIOD":
167            return "REPORT_PERIOD"
168        else:
169            return "UNKNOWN"
170
171    def get_match(column_name):
172        def clean_str(name):
173            translation = str.maketrans("", "", string.punctuation)
174            clean_name = name.strip().translate(translation).lower()
175            return re.sub(r"\s+", "", clean_name)
176
177        identifiers = [
178            ColumnSubRole.GBI_ID,
179            ColumnSubRole.ISIN,
180            ColumnSubRole.GVKEY,
181            ColumnSubRole.IID,
182            ColumnSubRole.SYMBOL,
183            ColumnSubRole.COUNTRY,
184            ColumnSubRole.CURRENCY,
185            ColumnSubRole.COMPANY_NAME,
186            ColumnSubRole.REPORT_DATE,
187            ColumnSubRole.REPORT_PERIOD,
188        ]
189
190        for identifier in identifiers:
191            if clean_str(str(identifier)) == clean_str(column_name):
192                return identifier
193        return None

An enumeration.

UNKNOWN = <ColumnSubRole.UNKNOWN: 0>
GBI_ID = <ColumnSubRole.GBI_ID: 1>
ISIN = <ColumnSubRole.ISIN: 2>
GVKEY = <ColumnSubRole.GVKEY: 3>
IID = <ColumnSubRole.IID: 4>
SYMBOL = <ColumnSubRole.SYMBOL: 5>
COUNTRY = <ColumnSubRole.COUNTRY: 6>
CURRENCY = <ColumnSubRole.CURRENCY: 7>
DATE = <ColumnSubRole.DATE: 8>
COMPANY_NAME = <ColumnSubRole.COMPANY_NAME: 9>
REPORT_DATE = <ColumnSubRole.REPORT_DATE: 10>
REPORT_PERIOD = <ColumnSubRole.REPORT_PERIOD: 11>
def get_match(column_name)
171    def get_match(column_name):
172        def clean_str(name):
173            translation = str.maketrans("", "", string.punctuation)
174            clean_name = name.strip().translate(translation).lower()
175            return re.sub(r"\s+", "", clean_name)
176
177        identifiers = [
178            ColumnSubRole.GBI_ID,
179            ColumnSubRole.ISIN,
180            ColumnSubRole.GVKEY,
181            ColumnSubRole.IID,
182            ColumnSubRole.SYMBOL,
183            ColumnSubRole.COUNTRY,
184            ColumnSubRole.CURRENCY,
185            ColumnSubRole.COMPANY_NAME,
186            ColumnSubRole.REPORT_DATE,
187            ColumnSubRole.REPORT_PERIOD,
188        ]
189
190        for identifier in identifiers:
191            if clean_str(str(identifier)) == clean_str(column_name):
192                return identifier
193        return None
Inherited Members
enum.Enum
name
value
class ColumnValueType(enum.Enum):
196class ColumnValueType(Enum):
197    UNKNOWN = 0
198    NUMBER = 1
199    STRING = 2
200
201    def __str__(self):
202        if self.name == "UNKNOWN":
203            return "UNKNOWN"
204        elif self.name == "NUMBER":
205            return "NUMBER"
206        elif self.name == "STRING":
207            return "STRING"
208        else:
209            return "UNKNOWN"

An enumeration.

UNKNOWN = <ColumnValueType.UNKNOWN: 0>
NUMBER = <ColumnValueType.NUMBER: 1>
STRING = <ColumnValueType.STRING: 2>
Inherited Members
enum.Enum
name
value
class ColumnConfig:
212class ColumnConfig:
213    def __init__(
214        self,
215        name=None,
216        role=None,
217        sub_role=None,
218        value_type=ColumnValueType.NUMBER,
219        description="",
220        investment_horizon=None,
221    ):
222        self.name = name
223        self.role = role
224        self.sub_role = sub_role
225        self.value_type = value_type
226        # More value_types will be supported in the future.
227        self.description = description
228        self.investment_horizon = investment_horizon
229
230    def __str__(self):
231        return 'Name: "{0}", Role: {1}, SubRole: {2}, Value type: {3}, Desc: {4} IH: {5}.'.format(
232            self.name,
233            self.role,
234            self.sub_role,
235            self.value_type,
236            self.description,
237            self.investment_horizon,
238        )
239
240    def __repr__(self):
241        return self.__str__()
ColumnConfig( name=None, role=None, sub_role=None, value_type=<ColumnValueType.NUMBER: 1>, description='', investment_horizon=None)
213    def __init__(
214        self,
215        name=None,
216        role=None,
217        sub_role=None,
218        value_type=ColumnValueType.NUMBER,
219        description="",
220        investment_horizon=None,
221    ):
222        self.name = name
223        self.role = role
224        self.sub_role = sub_role
225        self.value_type = value_type
226        # More value_types will be supported in the future.
227        self.description = description
228        self.investment_horizon = investment_horizon
class StrategyConfig:
244class StrategyConfig:
245    def __init__(self, name=None, source_name=None):
246        self.name = name
247        self.source_name = source_name
248
249    def __str__(self):
250        return 'Name: "{0}", Source Name: {1}.'.format(self.name, self.source_name)
251
252    def __repr__(self):
253        return self.__str__()
StrategyConfig(name=None, source_name=None)
245    def __init__(self, name=None, source_name=None):
246        self.name = name
247        self.source_name = source_name
class BoostedDataSetSchemaException(builtins.Exception):
256class BoostedDataSetSchemaException(Exception):
257    def __init__(self, value, data=None):
258        self.value = value
259        self.data = data
260
261    def __str__(self):
262        return repr(self.value)

Common base class for all non-exit exceptions.

BoostedDataSetSchemaException(value, data=None)
257    def __init__(self, value, data=None):
258        self.value = value
259        self.data = data
Inherited Members
builtins.BaseException
with_traceback
args
class DataSetConfig:
265class DataSetConfig:
266    def __init__(
267        self,
268        name,
269        datasetType=DataSetType.STOCK,
270        datasetSubType=DataSetSubType.DENSE,
271        datasetFrequency=DataSetFrequency.DAILY,
272    ):
273        self.name = name
274        self.type = datasetType
275        self.subtype = datasetSubType
276        self.frequency = datasetFrequency
277        self.columns = []
278        self.strategies = []
279
280    def addColumn(self, columnConfig):
281        self.columns.append(columnConfig)
282
283    def addStrategy(self, strategyConfig):
284        self.strategies.append(strategyConfig)
285
286    def __getColumnByName(self, col_name):
287        for column in self.columns:
288            if col_name == column.name:
289                return column
290        raise BoostedDataSetSchemaException(f"Unable to find column {col_name}")
291
292    def updateColumnToGoal(self, col_name, investment_horizon):
293        column = self.__getColumnByName(col_name)
294        column.role = ColumnRole.GOAL
295        column.investment_horizon = int(investment_horizon)
296
297    def updateColumnToMetric(self, col_name):
298        column = self.__getColumnByName(col_name)
299        column.role = ColumnRole.METRIC
300
301    def updateColumnToVariable(self, col_name):
302        column = self.__getColumnByName(col_name)
303        column.role = ColumnRole.VARIABLE
304
305    def __validateSchema(self):
306        num_goals = 0
307        num_metrics = 0
308        dt = self.type
309        dst = self.subtype
310        dsf = self.frequency
311        if len(self.columns) == 0:
312            msg = "No feature columns exist."
313            raise BoostedDataSetSchemaException(msg)
314
315        if dst in [DataSetSubType.SPARSE_HIST, DataSetSubType.SPARSE_FWD]:
316            if dsf not in [DataSetFrequency.QUARTERLY]:
317                msg = f"{dsf} frequency is not supported for {dst} sub data"
318                raise BoostedDataSetSchemaException(msg)
319            if dt not in [DataSetType.STOCK]:
320                msg = f"{dst} subtype is not supported for {dt} data"
321                raise BoostedDataSetSchemaException(msg)
322
323        for column in self.columns:
324            if column.role == ColumnRole.GOAL:
325                ih = column.investment_horizon
326                gn = column.name
327                if dt == DataSetType.GLOBAL:
328                    msg = f"{dt} data can not have {ColumnRole.GOAL} type"
329                    raise BoostedDataSetSchemaException(msg)
330                if not isinstance(ih, int):
331                    msg = f"Investment horizon for {gn} must be an integer"
332                    raise BoostedDataSetSchemaException(msg)
333                if ih < 1 or ih > 252:
334                    msg = f"Investment horizon must be between 1 and 252 for {gn}"
335                    raise BoostedDataSetSchemaException(msg)
336                num_goals += 1
337            elif column.role == ColumnRole.METRIC:
338                if dt in [DataSetType.GLOBAL, DataSetType.STOCK]:
339                    msg = f"{dt} data can not have {ColumnRole.METRIC} type"
340                    raise BoostedDataSetSchemaException(msg)
341                num_metrics += 1
342
343        if dt == DataSetType.STRATEGY:
344            if num_goals == 0:
345                msg = "Independent data requires at least one goal."
346                raise BoostedDataSetSchemaException(msg)
347            if num_metrics == 0:
348                msg = "Independent data requires at least one metric."
349                raise BoostedDataSetSchemaException(msg)
350            if len(self.strategies) <= 1:
351                msg = "Independent data requires more than 1 strategy"
352                raise BoostedDataSetSchemaException(msg)
353
354    def toDict(self):
355        self.__validateSchema()
356        config = {}
357        config["name"] = self.name
358        config["type"] = str(self.type)
359        config["subType"] = str(self.subtype)
360        config["frequency"] = str(self.frequency)
361        featureList = []
362        for i, f in enumerate(self.columns):
363            fm = {}
364            fm["name"] = f.name
365            fm["description"] = f.description
366            fm["type"] = str(f.role)
367            fm["role"] = str(f.role)
368            fm["subRole"] = str(f.sub_role) if f.sub_role is not None else None
369            fm["valuesType"] = str(f.value_type)
370            fm["columnName"] = f.name
371            fm["investmentHorizon"] = f.investment_horizon
372            featureList.append(fm)
373        config["features"] = featureList
374        strategyList = []
375        for i, s in enumerate(self.strategies):
376            sm = {}
377            sm["name"] = s.name
378            sm["sourceName"] = s.source_name
379            strategyList.append(sm)
380        config["strategies"] = strategyList
381        return config
382
383    def __str__(self):
384        a = ""
385        a += "Name: {0}\n".format(self.name)
386        a += "Columns: \n"
387        for c in self.columns:
388            a += "  {0}\n".format(c)
389        return a
390
391    def __repr__(self):
392        return self.__str__()
393
394    def fromDict(schema):
395        subtype = DataSetSubType[schema.get("subtype", str(DataSetSubType.DENSE))]
396        frequency = DataSetFrequency[schema.get("frequency", str(DataSetFrequency.DAILY))]
397        config = DataSetConfig(
398            schema["name"],
399            datasetType=DataSetType[schema["type"]],
400            datasetSubType=subtype,
401            datasetFrequency=frequency,
402        )
403        # two things left to fill in - strategies and columns
404        for strategy_data in schema["strategies"]:
405            config.addStrategy(
406                StrategyConfig(name=strategy_data["name"], source_name=strategy_data["sourceName"])
407            )
408
409        for feature_data in schema["features"]:
410            config.addColumn(
411                ColumnConfig(
412                    name=feature_data["columnName"],
413                    description=feature_data["description"] or "",
414                    investment_horizon=feature_data["investmentHorizon"],
415                    value_type=ColumnValueType[feature_data["valuesType"]]
416                    if feature_data["valuesType"] is not None
417                    else ColumnValueType.NUMBER,
418                    role=ColumnRole[feature_data["role"]]
419                    if feature_data["role"] is not None
420                    else None,
421                    sub_role=ColumnSubRole[feature_data["subRole"]]
422                    if feature_data["subRole"] is not None
423                    else None,
424                )
425            )
426
427        config.__validateSchema()
428        return config
DataSetConfig( name, datasetType=<DataSetType.STOCK: 1>, datasetSubType=<DataSetSubType.DENSE: 1>, datasetFrequency=<DataSetFrequency.DAILY: 1>)
266    def __init__(
267        self,
268        name,
269        datasetType=DataSetType.STOCK,
270        datasetSubType=DataSetSubType.DENSE,
271        datasetFrequency=DataSetFrequency.DAILY,
272    ):
273        self.name = name
274        self.type = datasetType
275        self.subtype = datasetSubType
276        self.frequency = datasetFrequency
277        self.columns = []
278        self.strategies = []
def addColumn(self, columnConfig)
280    def addColumn(self, columnConfig):
281        self.columns.append(columnConfig)
def addStrategy(self, strategyConfig)
283    def addStrategy(self, strategyConfig):
284        self.strategies.append(strategyConfig)
def updateColumnToGoal(self, col_name, investment_horizon)
292    def updateColumnToGoal(self, col_name, investment_horizon):
293        column = self.__getColumnByName(col_name)
294        column.role = ColumnRole.GOAL
295        column.investment_horizon = int(investment_horizon)
def updateColumnToMetric(self, col_name)
297    def updateColumnToMetric(self, col_name):
298        column = self.__getColumnByName(col_name)
299        column.role = ColumnRole.METRIC
def updateColumnToVariable(self, col_name)
301    def updateColumnToVariable(self, col_name):
302        column = self.__getColumnByName(col_name)
303        column.role = ColumnRole.VARIABLE
def toDict(self)
354    def toDict(self):
355        self.__validateSchema()
356        config = {}
357        config["name"] = self.name
358        config["type"] = str(self.type)
359        config["subType"] = str(self.subtype)
360        config["frequency"] = str(self.frequency)
361        featureList = []
362        for i, f in enumerate(self.columns):
363            fm = {}
364            fm["name"] = f.name
365            fm["description"] = f.description
366            fm["type"] = str(f.role)
367            fm["role"] = str(f.role)
368            fm["subRole"] = str(f.sub_role) if f.sub_role is not None else None
369            fm["valuesType"] = str(f.value_type)
370            fm["columnName"] = f.name
371            fm["investmentHorizon"] = f.investment_horizon
372            featureList.append(fm)
373        config["features"] = featureList
374        strategyList = []
375        for i, s in enumerate(self.strategies):
376            sm = {}
377            sm["name"] = s.name
378            sm["sourceName"] = s.source_name
379            strategyList.append(sm)
380        config["strategies"] = strategyList
381        return config
def fromDict(schema)
394    def fromDict(schema):
395        subtype = DataSetSubType[schema.get("subtype", str(DataSetSubType.DENSE))]
396        frequency = DataSetFrequency[schema.get("frequency", str(DataSetFrequency.DAILY))]
397        config = DataSetConfig(
398            schema["name"],
399            datasetType=DataSetType[schema["type"]],
400            datasetSubType=subtype,
401            datasetFrequency=frequency,
402        )
403        # two things left to fill in - strategies and columns
404        for strategy_data in schema["strategies"]:
405            config.addStrategy(
406                StrategyConfig(name=strategy_data["name"], source_name=strategy_data["sourceName"])
407            )
408
409        for feature_data in schema["features"]:
410            config.addColumn(
411                ColumnConfig(
412                    name=feature_data["columnName"],
413                    description=feature_data["description"] or "",
414                    investment_horizon=feature_data["investmentHorizon"],
415                    value_type=ColumnValueType[feature_data["valuesType"]]
416                    if feature_data["valuesType"] is not None
417                    else ColumnValueType.NUMBER,
418                    role=ColumnRole[feature_data["role"]]
419                    if feature_data["role"] is not None
420                    else None,
421                    sub_role=ColumnSubRole[feature_data["subRole"]]
422                    if feature_data["subRole"] is not None
423                    else None,
424                )
425            )
426
427        config.__validateSchema()
428        return config
class GbiIdSecurityException(builtins.Exception):
431class GbiIdSecurityException(Exception):
432    def __init__(self, value, data=None):
433        self.value = value
434        self.data = data
435
436    def __str__(self):
437        return repr(self.value)

Common base class for all non-exit exceptions.

GbiIdSecurityException(value, data=None)
432    def __init__(self, value, data=None):
433        self.value = value
434        self.data = data
Inherited Members
builtins.BaseException
with_traceback
args
class GbiIdSecurity:
440class GbiIdSecurity:
441    def __init__(self, gbi_id, isin_country_currency_date, ticker, company_name):
442        self.gbi_id = gbi_id
443        self.isin_info = isin_country_currency_date
444        self.ticker = ticker
445        self.company_name = company_name
446
447    def __str__(self):
448        return '\n(gbi_id: "{0}", isin_info: {1}, ticker: {2}, company_name: {3})'.format(
449            self.gbi_id, self.isin_info.__str__(), self.ticker, self.company_name
450        )
451
452    def __repr__(self):
453        return self.__str__()
454
455    def __eq__(self, __o: object) -> bool:
456        return self.gbi_id == __o.gbi_id
457
458    def __hash__(self):
459        return hash(self.gbi_id)
GbiIdSecurity(gbi_id, isin_country_currency_date, ticker, company_name)
441    def __init__(self, gbi_id, isin_country_currency_date, ticker, company_name):
442        self.gbi_id = gbi_id
443        self.isin_info = isin_country_currency_date
444        self.ticker = ticker
445        self.company_name = company_name
class DateIdentCountryCurrencyException(builtins.Exception):
462class DateIdentCountryCurrencyException(Exception):
463    def __init__(self, value, data=None):
464        self.value = value
465        self.data = data
466
467    def __str__(self):
468        return repr(self.value)

Common base class for all non-exit exceptions.

DateIdentCountryCurrencyException(value, data=None)
463    def __init__(self, value, data=None):
464        self.value = value
465        self.data = data
Inherited Members
builtins.BaseException
with_traceback
args
class DateIsinCountryCurrencyException(DateIdentCountryCurrencyException):
472class DateIsinCountryCurrencyException(DateIdentCountryCurrencyException):
473    pass

Common base class for all non-exit exceptions.

Inherited Members
DateIdentCountryCurrencyException
DateIdentCountryCurrencyException
builtins.BaseException
with_traceback
args
class DateIdentCountryCurrency:
476class DateIdentCountryCurrency:
477    def __init__(
478        self,
479        date: str,
480        identifier: str,
481        country: Optional[str] = None,
482        currency: Optional[str] = None,
483        id_type: ColumnSubRole = ColumnSubRole.ISIN,
484    ):
485        self.date = date
486        self.identifier = identifier
487        self.id_type = id_type
488        self.country = country
489        self.currency = currency
490        self.__validateInput()
491
492    def __str__(self):
493        return '(date: "{0}", identifier: "{1}", country: {2}, currency: {3})'.format(
494            self.date, self.identifier, self.country, self.currency
495        )
496
497    def __repr__(self):
498        return self.__str__()
499
500    def __validateInput(self):
501        # simply ensure that at least isin and date are filled out.
502        # country/currency defaults to ANY if not filled out, but
503        # still is not recommended.
504        if self.identifier is None or self.date is None:
505            raise DateIdentCountryCurrencyException(
506                "ISIN or Date not provided while constructing DateIsinCountryCurrency"
507            )
508
509        if self.id_type not in (ColumnSubRole.ISIN, ColumnSubRole.SYMBOL):
510            raise DateIdentCountryCurrencyException(f"Invalid ID Type: {self.id_type}")
511
512        def check_is_str(value, fieldname):
513            if value is not None and not isinstance(value, str):
514                raise DateIdentCountryCurrencyException(
515                    f"Field {fieldname} in DateIsinCountryCurrency was not a string."
516                )
517
518        check_is_str(self.date, "date")
519        check_is_str(self.identifier, "identifier")
520        check_is_str(self.country, "country")
521        check_is_str(self.currency, "currency")
DateIdentCountryCurrency( date: str, identifier: str, country: Union[str, NoneType] = None, currency: Union[str, NoneType] = None, id_type: boosted.api.api_type.ColumnSubRole = <ColumnSubRole.ISIN: 2>)
477    def __init__(
478        self,
479        date: str,
480        identifier: str,
481        country: Optional[str] = None,
482        currency: Optional[str] = None,
483        id_type: ColumnSubRole = ColumnSubRole.ISIN,
484    ):
485        self.date = date
486        self.identifier = identifier
487        self.id_type = id_type
488        self.country = country
489        self.currency = currency
490        self.__validateInput()
class DateIsinCountryCurrency(DateIdentCountryCurrency):
525class DateIsinCountryCurrency(DateIdentCountryCurrency):
526    def __init__(self, date, isin, country, currency):
527        super().__init__(date, isin, country, currency, ColumnSubRole.ISIN)
528        self.isin = isin
529
530    def __str__(self):
531        return '(date: "{0}", isin: "{1}", country: {2}, currency: {3})'.format(
532            self.date, self.isin, self.country, self.currency
533        )
DateIsinCountryCurrency(date, isin, country, currency)
526    def __init__(self, date, isin, country, currency):
527        super().__init__(date, isin, country, currency, ColumnSubRole.ISIN)
528        self.isin = isin
class IdentifierToColumnMapping:
536class IdentifierToColumnMapping:
537    """
538    IdentifierToColumnMapping is a mapping from string to boosted.api.api_type.ColumnSubRole.
539    The key of the mapping corresponds to a CSV column name, and the value corresponds
540    to the type of identifier that it maps to. Only columns specifying an identifying factor
541    for the stock needs to be specified (e.g. ISIN, Date, Symbol, Currency)
542    """
543
544    def __init__(self, mapping):
545        self.mapping = mapping
546        self.__validateInput()
547
548    def __str__(self):
549        return repr(self.value)
550
551    def __validateInput(self):
552        def validate_key_val(key, val):
553            if not isinstance(key, str):
554                raise Exception(f"key in IdentifierToColumnMapping {key} was not a string!")
555            if not isinstance(val, ColumnSubRole):
556                raise Exception(f"val in IdentifierToColumnMapping {val} was not a ColumnSubRole!")
557
558        for key, val in self.mapping:
559            validate_key_val(key, val)

IdentifierToColumnMapping is a mapping from string to boosted.api.api_type.ColumnSubRole. The key of the mapping corresponds to a CSV column name, and the value corresponds to the type of identifier that it maps to. Only columns specifying an identifying factor for the stock needs to be specified (e.g. ISIN, Date, Symbol, Currency)

IdentifierToColumnMapping(mapping)
544    def __init__(self, mapping):
545        self.mapping = mapping
546        self.__validateInput()
class PortfolioSettings:
 562class PortfolioSettings:
 563    """
 564    This config is a documentation wrapper around a dict representing the portfolio settings json.
 565    Keys missing in the dict provided will fallback to the listed default when updating the
 566    the portfolio settings on the server.
 567
 568    Parameters
 569    ----------
 570    activeRisk: bool
 571        - **Active Risk**:
 572            Set the portfolio optimization parameters to be relative to the score from the stock
 573            universe. i.e. if you set Momentum to 0.5 to 1.0 and the stock universe has a market cap
 574            weighed score of 0.2 then the optimizer will solve for 0.7 to 1.2.
 575        - Constraints:
 576            Boolean value
 577        - Default Value: false
 578
 579    allowCompanyCollisions: bool
 580        - **Allow Multiple Securities Per Company**:
 581            Allows the machine to trade multiple securities (at the same time) for the same company.
 582            If it is ON and a company has listed PREF shares and COMMON shares, both will show up as
 583            tradeable options for that company. If it is OFF then system will try to determine the
 584            "correct" security to trade based on volume, liquidity, and share type.
 585        - Constraints:
 586            Boolean value
 587        - Default Value: true
 588
 589    allowDR: bool
 590        - **Allow Depositary Receipts**:
 591            Companies with tickers ending in .Y and .F will be disallowed from trading within the
 592            model with this setting off. Turn it on to allow trading in these securities.
 593        - Constraints:
 594            Boolean value
 595        - Default Value: true
 596
 597    benchmark: array
 598        - **Benchmark**:
 599            Set one or more benchmarks and the weighting for each.
 600        - Constraints:
 601            Required.
 602            List of dicts with keys:
 603            gbi_id: integer representing the GBI ID of the benchmark index.
 604            weight: floating point value between 0 and 1.
 605            All gbi_id's in the dictionary must be unique, and the benchmark weights must sum to a
 606            number between 0 and 1.
 607        - Default Value: []
 608
 609    benchmarkAllocation: float
 610        - **Benchmark Allocation**:
 611            None
 612        - Constraints:
 613            Floating point value representing a percentage between -100 and 100
 614        - Default Value: 0
 615
 616    beta_neutral: bool
 617        - **Beta Neutral**:
 618            Adjust the weights to get the ex-ante Beta to be the net exposure of the portfolio. i.e.
 619            70% Long / 30% Short = 0.4 Beta"
 620        - Constraints:
 621            Boolean value
 622        - Default Value: false
 623
 624    bounds_method: str | null
 625        - **Optimizer Bounds**:
 626            Tight: The optimizer will only allow the weightings of securities in your portfolio to
 627            stay tight (close) to their initial weighting.
 628            Loose: The optimizer will allow the weightings of securities in your portfolio to move a
 629            medium amount more from their initial weighting.
 630            Wide: The optimizer will allow almost any weight between the minimum and maximum weight
 631            per long or short position.
 632        - Constraints:
 633            String enum of {loose, tight, wide}, or null
 634        - Default Value: null
 635
 636    compounding: bool
 637        - **Compound Returns**:
 638            When toggled on, this will allow the portfolio to compound returns.
 639        - Constraints:
 640            Boolean value
 641        - Default Value: true
 642
 643    currency: str
 644        - **Currency**:
 645            The currency you would like your portfolio to be calculated in. Note that if the
 646            currency differs from its exchange (i.e. GBP on the S&P), the prior close foreign
 647            exchange rate will be used for calculations.
 648        - Constraints:
 649            String representing a 3 digit ISO currency code.
 650        - Default Value: "USD"
 651
 652    equalWeightBenchmark: bool
 653        - **Equal Weight Benchmark**:
 654            Instead of using the defined benchmarks, set the benchmark to be an equal weighted (per
 655            rebalance period) version of the stock universe.
 656        - Constraints:
 657            Boolean value
 658        - Default Value: false
 659
 660    executionDelay: int
 661        - **Execution Delay**:
 662            This option adds the number of days selected to the trade execution. If you select T+1
 663            and your rebalance period is set to Weekly (Monday), it will trade on Tuesday, and so
 664            on.
 665        - Constraints:
 666            Integer value between 0 and 100, inclusive.
 667        - Default Value: 0
 668
 669    factorNeutralizeSignal: bool
 670        - **Signal Optimizer**:
 671            Turn on optimization at the signal level that will adjust signals and rankings according
 672            to the specifications you set. This is done prior to portfolio construction and can help
 673            reduce bias in the model.
 674        - Constraints:
 675            Boolean value
 676        - Default Value: false
 677
 678    investmentHorizon: int
 679        - **Investment Horizon**:
 680            The investment horizon for the portfolio.
 681        - Constraints:
 682            An integer representing a number of days
 683        - Default Value: 21
 684
 685    lower_turnover: bool
 686        - **Lower Turnover**:
 687            If toggled on the optimizer will try to solve the secondary goal of optimizing the
 688            primary goals while limiting turnover.
 689        - Constraints:
 690            Boolean value
 691        - Default Value: false
 692
 693    marketCapBounds: float
 694        - **Maximum Market Cap Variation**:
 695            How far from the target market cap weighted index each stock can deviate (positive or
 696            negative).
 697        - Constraints:
 698            Floating point value representing a percentage between 0 and 10
 699        - Default Value: 1
 700
 701    marketCapConstraint: bool
 702        - **Constrain Market Cap**:
 703            Force weighting to be near the target weights for a market cap weighted index of your
 704            entire stock universe.
 705        - Constraints:
 706            Boolean value
 707        - Default Value: false
 708
 709    marketCapFlex: float
 710        - **Market Cap Flex**:
 711            How far from the target market cap weighted the stock can deviate relative to the
 712            existing weight (i.e. at 75% a 1% position cannot exceed 1.75%).
 713        - Constraints:
 714            Floating point value representing a percentage between 10 and 200
 715        - Default Value: 100
 716
 717    maxLongPosition: float
 718        - **Max Long Position per Stock**:
 719            The maximum weighting of a long position within the portfolio.
 720        - Constraints:
 721            Required. Floating point value representing a percentage between 0.0 and 300.0
 722        - Default Value: 10.0
 723
 724    maxNumStockLong: int
 725        - **Maximum Number of Longs**:
 726            The maximum number of stocks to buy. This is on a per “portfolio” basis, so if you have
 727            sector neutral turned on it will be maximum per sector.
 728        - Constraints:
 729            Integer value between 0 and 1000, inclusive. Must be >= minNumStockLong
 730        - Default Value: 50
 731
 732    maxNumStockShort: int
 733        - **Maximum Number of Shorts**:
 734            The maximum number of stocks to short. This is on a per “portfolio” basis, so if you
 735            have sector neutral turned on it will be maximum per sector.
 736        - Constraints:
 737            Integer value between 0 and 1000, inclusive. Must be >= minNumStockShort
 738        - Default Value: 50
 739
 740    maxOwnership: float
 741        - **Maximum Ownership**:
 742            The maximum percentage of a company the portfolio is allowed to own. For example, if a
 743            company had a market cap of $100MM and this was 5% the portfolio could not own more than
 744            $5MM of that company.
 745        - Constraints:
 746            Floating point value representing a percentage between 0 and 100
 747        - Default Value: 5
 748
 749    maxShortPosition: float
 750        - **Max Short Position per Stock**:
 751            The maximum weighting of a short position within the portfolio.
 752        - Constraints:
 753            Required. Floating point value representing a percentage between 0.0 and 300.0
 754        - Default Value: 5.0
 755
 756    minimumSharePrice: float
 757        - **Minimum Share Price**:
 758            The minimum share price you want the portfolio to allow.
 759        - Constraints:
 760            Floating point value between 0 and 100000
 761        - Default Value: 2
 762
 763    minLongPosition: float
 764        - **Min Long Position per Stock**:
 765            The minimum weighting of a long position within the portfolio.
 766        - Constraints:
 767            Floating point value representing a percentage between 0.0 and 300.0
 768        - Default Value: 0
 769
 770    minNumStockLong: int
 771        - **Minimum Number of Longs**:
 772            The minimum number of stocks to buy. This is on a per “portfolio” basis, so if you have
 773            sector neutral turned on it will be minimum per sector.
 774        - Constraints:
 775            Integer value between 0 and 1000, inclusive. Must be <= maxNumStockLong
 776        - Default Value: 5
 777
 778    minNumStockShort: int
 779        - **Minimum Number of Shorts**:
 780            The minimum number of stocks to short. This is on a per “portfolio” basis, so if you
 781            have sector neutral turned on it will be minimum per sector.
 782        - Constraints:
 783            Integer value between 0 and 1000, inclusive. Must be <= maxNumStockShort
 784        - Default Value: 5
 785
 786    minShortPosition: float
 787        - **Min Short Position per Stock**:
 788            The minimum weighting of a short position within the portfolio.
 789        - Constraints:
 790            Floating point value representing a percentage between 0.0 and 100.0
 791        - Default Value: 0
 792
 793    missingMarketCap: float
 794        - **Missing Market Cap**:
 795            The amount in millions (MM) to replace any missing market capitalization information
 796            with. This will impact the maximum ownership setting.
 797        - Constraints:
 798            Floating point value between 0 and 100
 799        - Default Value: 10
 800
 801    nameBasedSectorWeighting: bool
 802        - **Name Based Sector Weighting**:
 803            Base sector weightings on number of names. For example, if there are 200 names in the
 804            index and financials has 20 companies, the weight of financials should be 10% (20/200).
 805        - Constraints:
 806            Boolean value
 807        - Default Value: false
 808
 809    netBenchmark: bool
 810        - **Adjust Benchmark for Net Exposure**:
 811            If your portfolio has a net exposure greater or less than 100%, this will adjust the
 812            benchmark returns to match that net exposure.
 813        - Constraints:
 814            Boolean value
 815        - Default Value: false
 816
 817    optimizer: str
 818        - **Optimizer Type**:
 819            No Optimization: None
 820            Reduce Risk: Optimizer designed to reduce portfolio volatility. This tends to result in
 821            the best overall performance.
 822            Maximize Sharpe: Optimizer designed to maximize Sharpe using out of sample estimates for
 823            expected return. Estimates for expected return have an outsized impact on the
 824            performance of this optimizer.
 825            Maximize Alpha: Optimizer designed to maximize expected return based on out-of-sample
 826            estimates for expected return. Estimates for expected return have an outsized impact on
 827            the performance of this optimizer.
 828            Min VaR: Minimize Value at Risk by looking back at the proposed portfolio over the last
 829            year and trying to minimize drawdowns.
 830            Max VaR Sharpe: Maximize Value at Risk Sharpe by looking back at the proposed portfolio
 831            over the last year and trying to maximize Sharpe by observed return for the portfolio vs
 832            observed volatility.
 833            Minimize Skew: Minimize the skew of the portfolio by trying to find a set of portfolio
 834            weightings that results in returns that are closer to the mean.
 835        - Constraints:
 836            Required. String enum: one of
 837            default: No Optimization
 838            min_vol: Reduce Risk
 839            max_sharpe: Maximize Sharpe
 840            max_alpha: Maximize Alpha
 841            min_var: Min VaR
 842            max_var_sharpe: Max VaR Sharpe
 843            min_skew: Minimize Skew
 844        - Default Value: "default"
 845
 846    optimizeTurnover: bool
 847        - **Turnover Optimization**:
 848            Turnover optimization will reduce the turnover at the signal level, making that ranked
 849            list more stable.
 850        - Constraints:
 851            Boolean value
 852        - Default Value: false
 853
 854    pairsTrading: bool
 855        - **Pairs Trading**:
 856            Pairs Trading forces the portfolio to be created by going long / short an equal number
 857            of stocks. The portfolio will try to find an offsetting position for every long by
 858            finding securities that are both highly correlated and have a significant difference in
 859            star rating.
 860        - Constraints:
 861            Boolean value
 862        - Default Value: false
 863
 864    percentPortfolioLong: float
 865        - **Percent of Portfolio Long**:
 866            The exact sum of all long position weightings in the portfolio.
 867        - Constraints:
 868            Required. Floating point value representing a percentage between 0.0 and 300.0
 869        - Default Value: 100.0
 870
 871    percentPortfolioShort: float
 872        - **Percent of Portfolio Short**:
 873            The exact sum of all short position weightings in the portfolio.
 874        - Constraints:
 875            Required. Floating point value representing a percentage between 0.0 and 300.0
 876        - Default Value: 0.0
 877
 878    percentToLong: float
 879        - **Percent to Long**:
 880            The machine will attempt to buy the top X% of stocks, subject to the minimum and maximum
 881            specified. This is on a per “portfolio” basis, so if you have sector neutral turned on
 882            it will be top X% per sector. This will be subject to the maximum and minimum number of
 883            securities you specify below.
 884        - Constraints:
 885            Floating point value representing a percentage between 0 and 100
 886        - Default Value: 20.0
 887
 888    percentToShort: float
 889        - **Percent to Short**:
 890            The machine will attempt to short the bottom X% of stocks, subject to the minimum and
 891            maximum specified. This is on a per “portfolio” basis, so if you have sector neutral
 892            turned on it will be bottom X% per sector. This will be subject to the maximum and
 893            minimum number of securities you specify below.
 894        - Constraints:
 895            Floating point value representing a percentage between 0 and 100
 896        - Default Value: 20.0
 897
 898    portfolioStartingValue: float
 899        - **Portfolio Starting Value**:
 900            The value of the portfolio in MM when it begins trading. If its backtest spans from 2005
 901            - 2020, the starting value in 2005 will be whatever you set here.
 902        - Constraints:
 903            Floating point value between 0 and 100000.
 904        - Default Value: 100.0
 905
 906    priceCleaning: bool
 907        - **Price Cleaning**:
 908            Price Cleaning removes any very large price movements from the securities, specifically
 909            with a daily change greater than +500% or -83.33%. There are some legitimate securities
 910            with moves this large that will be impacted by turning this on, but the advantage is it
 911            prevents these securities from overwhelming the backtest.
 912        - Constraints:
 913            Boolean value
 914        - Default Value: true
 915
 916    priceType: str
 917        - **Price Type**:
 918            None
 919        - Constraints:
 920            Required. String enum of {CLOSE, OPEN, VWAP}
 921        - Default Value: "VWAP"
 922
 923    rebalancePeriod: str
 924        - **Rebalance Period**:
 925            When the machine rebalances the portfolio. You can choose a specific day if you prefer
 926            to execute trades on a specific day.
 927        - Constraints:
 928            Required.
 929            An enum value in the following list:
 930            "daily"
 931            "weekly": Rebalances on every Monday.
 932            Chooses the next available trade date to rebalance when Monday is a holiday
 933            "weekly_wednesday": Rebalances on every Wednesday.
 934            Chooses the next available trade date to rebalance when Wednesday is a holiday
 935            "weekly_friday": Rebalances on every Friday.
 936            Chooses the previous available trade date to rebalance when Friday is a holiday
 937            "monthly"
 938            "quarterly"
 939            OR a string in the following format: "custom_x" where x is an integer between 1 and 252
 940            (days).
 941        - Default Value: "weekly"
 942
 943    sectorNeutral: bool
 944        - **Sector Neutral**:
 945            Will be constructed as a series of portfolios - each matching the market cap weighting
 946            of each of the stock universe sectors. This means that min. and max. number of stocks
 947            "per portfolio" becomes "per sector".
 948        - Constraints:
 949            Boolean value
 950        - Default Value: false
 951
 952    sectorNeutralEqualWeightBenchmark: bool
 953        - **Sector Neutral Equal Weight Benchmark**:
 954            Instead of using the defined benchmarks, set the benchmark to be a "sector neutral"
 955            equal weighted (per rebalance period) version of the stock universe. This takes the
 956            sector weighting and divides by the number of stocks in that sector, so each sector will
 957            have a different individual security weighting.
 958        - Constraints:
 959            Boolean value
 960        - Default Value: false
 961
 962    sectorNeutralSpread: float
 963        - **Sector Neutral Spread**:
 964            The sector neutral spread is how far away from the benchmark sector allocation the
 965            machine can deviate. This is adjusted for net exposure, i.e. if your net exposure is 0%
 966            the target sector allocations will be 0%.
 967        - Constraints:
 968            Floating point value representing a percentage between 0 and 100
 969        - Default Value: 0
 970
 971    signalIndustryNeutral: bool
 972        - **Industry Neutral**:
 973            Make the signal industry neutral, plus or minus the signal sector neutral spread.
 974        - Constraints:
 975            Boolean value
 976        - Default Value: false
 977
 978    signalSectorNeutral: bool
 979        - **Sector Neutral**:
 980            Make the signal sector neutral, plus or minus the signal sector neutral spread.
 981        - Constraints:
 982            Boolean value
 983        - Default Value: false
 984
 985    signalSectorNeutralSpread: float
 986        - **Sector Neutral Spread**:
 987            Make the signal sector neutral, plus or minus the signal sector neutral spread.
 988        - Constraints:
 989            Floating point value representing a percentage between 0 and 100
 990        - Default Value: 1.0
 991
 992    smoothPeriods: int
 993        - **Smooth Periods**:
 994            The number of periods over which to smooth the signals
 995        - Constraints:
 996            Integer value between 1 and 100.
 997        - Default Value: 1
 998
 999    smoothWeighting: str
1000        - **Smooth Weighting Type**:
1001            Alpha Weight: The strength of the signal matters in the smoothing. For example, if a
1002            stock had a score of 5 stars in period A, 2 stars in period B, your 2 period smoothing
1003            would be 7 / 2 = 3.5.
1004            Equal Weight: Only the direction of the signal matters here. For example, if you were 5
1005            stars in period A and 2 stars in period B, your 2 period smoothing would be 0 / 2 = 0
1006            (+1 in period 1 and -1 in period 2)
1007        - Constraints:
1008            One of {alpha_weight, equal_weight}
1009        - Default Value: "alpha_weight"
1010
1011    stopGain: bool
1012        - **Enable Stop Gain**:
1013            Turn on stop gains for the portfolio. This forces sales of securities that have exceeded
1014            the stop gain level. All trades will occur at the next trading time (i.e. next day OPEN
1015            for OPEN price models).
1016        - Constraints:
1017            Boolean value
1018        - Default Value: false
1019
1020    stopGainAmount: float
1021        - **Stop Gain Percentage to Sell**:
1022            The percentage of the position the machine will sell if the stop gain is triggered.
1023        - Constraints:
1024            Floating point value representing a percentage between 1 and 100.
1025        - Default Value: 50.0
1026
1027    stopGainLevel: float
1028        - **Stop Gain Threshold**:
1029            Will sell positions after they hit a threshold. For example, 10% / stop gain, when a
1030            position gains 10% the machine will sell a portion of the position (defined by stop gain
1031            percentage).
1032        - Constraints:
1033            Floating point value representing a percentage between 1 and 100.
1034        - Default Value: 10.0
1035
1036    stopLoss: bool
1037        - **Enable Stop Loss**:
1038            Turn on stop losses for the portfolio. This forces sales of securities that have
1039            exceeded the stop loss level. All trades will occur at the next trading time (i.e. next
1040            day OPEN for OPEN price models).
1041        - Constraints:
1042            Boolean value
1043        - Default Value: false
1044
1045    stopLossAmount: float
1046        - **Stop Loss Percentage to Sell**:
1047            The percentage of the position the machine will sell if the stop loss is triggered.
1048        - Constraints:
1049            Floating point value representing a percentage between 1 and 100.
1050        - Default Value: 50.0
1051
1052    stopLossLevel: float
1053        - **Stop Loss Threshold**:
1054            Will sell positions after they hit a threshold. For example, 10% / stop loss, when a
1055            position loses 10% the machine will sell a portion of the position (defined by stop loss
1056            percentage).
1057        - Constraints:
1058            Floating point value representing a percentage between 1 and 100.
1059        - Default Value: 10.0
1060
1061    stopLossReset: bool
1062        - **Allow multiple stop loss/gain between rebalance days**:
1063            Allows the machine to repeatedly trigger stop losses within a rebalance period. For
1064            example, if you trade Mondays and the stock drops 15% per day during the week and the
1065            stop loss trigger is 10%, then every day will trigger the stop loss. If toggled off the
1066            stop loss can only be triggered once per stock per rebalance period.
1067        - Constraints:
1068            Boolean value
1069        - Default Value: false
1070
1071    trackingConstraint: float
1072        - **Maximum Tracking Error (vs Benchmark)**:
1073            Amount of ex-ante tracking error to constrain the optimizer by.
1074        - Constraints:
1075            Floating point value representing a percentage between 0 and 30
1076        - Default Value: 10
1077
1078    trackingError: bool
1079        - **Allocate Weight to Benchmark**:
1080            Allocate a portion (%) of portfolio to the benchmark
1081        - Constraints:
1082            Boolean value
1083        - Default Value: false
1084
1085    trackingErrorOptimizer: bool
1086        - **Tracking Error**:
1087            If toggled on the optimizer will try to solve for an expected tracking error less than
1088            the amount specified. This is done out-of-sample and the ex post tracking error will
1089            likely be higher.
1090        - Constraints:
1091            Boolean value
1092        - Default Value: false
1093
1094    tradingCost: float
1095        - **Trading Cost**:
1096            A cost that will be applied to every trade.
1097        - Constraints:
1098            Required. Floating point value representing a percentage between 0.0 and 5.0
1099        - Default Value: 0.01
1100
1101    turnoverImportance: float
1102        - **Turnover Importance**:
1103            High importance means the algorithm will aim to keep turnover low, whereas low
1104            importance of turnover will let it vary more.
1105        - Constraints:
1106            Floating point value between 0.2 and 5 nonlinearly mapped onto the 1-9 interval.
1107        - Default Value: 1
1108
1109    useHoldingPeriod: bool
1110        - **Use Holding Period**:
1111            Set any covariance or other calculations within the optimizer to use a holding period
1112            return instead of daily return series
1113        - Constraints:
1114            Boolean value
1115        - Default Value: false
1116
1117    weightingType: str
1118        - **Initial Weighting**:
1119            Alpha Weight: The initial weighting will be done such that higher ranked stocks have a
1120            higher weight.
1121            Equal Weight: The initial weighting will be done such that all stocks selected to be in
1122            the portfolio will have an equal weight.
1123            Market Cap Weight: The initial weighting will be done such that all stocks are weighted
1124            according to their previous day’s market capitalization.
1125        - Constraints:
1126            One of {alpha_weight, equal_weight, market_cap_weight}
1127        - Default Value: "alpha_weight"
1128    """
1129
1130    # note, this is just the collection of portfolio settings
1131    # that is exposed to the API:
1132    # SELECT key, default_value
1133    # FROM portfolio_settings_validation
1134    # WHERE exposed_to_client_api;
1135    # TODO: expand to entire settings? why this way initially?
1136    __default_settings = {
1137        "maxOwnership": "5",
1138        "pairsTrading": "false",
1139        "minNumStockLong": "5",
1140        "stopLossAmount": "50.0",
1141        "stopGainLevel": "10.0",
1142        "smoothWeighting": '"alpha_weight"',
1143        "minimumSharePrice": "2",
1144        "maxShortPosition": "5.0",
1145        "tradingCost": "0.01",
1146        "stopGainAmount": "50.0",
1147        "smoothPeriods": "1",
1148        "executionDelay": "0",
1149        "benchmarkAllocation": "0",
1150        "sectorNeutralSpread": "0",
1151        "percentToLong": "20.0",
1152        "maxNumStockLong": "50",
1153        "trackingConstraint": "10",
1154        "compounding": "true",
1155        "priceCleaning": "true",
1156        "lower_turnover": "false",
1157        "missingMarketCap": "10",
1158        "allowCompanyCollisions": "true",
1159        "allowDR": "true",
1160        "sectorNeutral": "false",
1161        "marketCapFlex": "100",
1162        "marketCapBounds": "1",
1163        "turnoverImportance": "1",
1164        "minNumStockShort": "5",
1165        "maxNumStockShort": "50",
1166        "beta_neutral": "false",
1167        "nameBasedSectorWeighting": "false",
1168        "trackingError": "false",
1169        "stopLoss": "false",
1170        "stopGain": "false",
1171        "stopLossReset": "false",
1172        "netBenchmark": "false",
1173        "equalWeightBenchmark": "false",
1174        "marketCapConstraint": "false",
1175        "signalIndustryNeutral": "false",
1176        "signalSectorNeutral": "false",
1177        "sectorNeutralEqualWeightBenchmark": "false",
1178        "percentToShort": "20.0",
1179        "trackingErrorOptimizer": "false",
1180        "currency": '"USD"',
1181        "rebalancePeriod": '"weekly"',
1182        "benchmark": "[]",
1183        "signalSectorNeutralSpread": "1.0",
1184        "optimizeTurnover": "false",
1185        "minShortPosition": "0",
1186        "minLongPosition": "0",
1187        "percentPortfolioLong": "100.0",
1188        "portfolioStartingValue": "100.0",
1189        "stopLossLevel": "10.0",
1190        "maxLongPosition": "10.0",
1191        "percentPortfolioShort": "0.0",
1192        "bounds_method": None,
1193        "optimizer": '"default"',
1194        "activeRisk": "false",
1195        "useHoldingPeriod": "false",
1196        "weightingType": '"alpha_weight"',
1197        "priceType": '"VWAP"',
1198        "factorNeutralizeSignal": "false",
1199        "investmentHorizon": "21",
1200    }
1201
1202    def __init__(self, settings: Optional[Dict] = None):
1203        # TODO: i'd rather have kwargs for the different keys and then allow
1204        # for per-key overrides, but we start with this more conservative approach
1205        if settings is None:
1206            self.settings = copy.deepcopy(PortfolioSettings.__default_settings)
1207        else:
1208            self.settings = settings
1209
1210    def toDict(self):
1211        return self.settings
1212
1213    def __str__(self):
1214        return json.dumps(self.settings)
1215
1216    def __repr__(self):
1217        return self.__str__()
1218
1219    def fromDict(settings):
1220        return PortfolioSettings(settings)

This config is a documentation wrapper around a dict representing the portfolio settings json. Keys missing in the dict provided will fallback to the listed default when updating the the portfolio settings on the server.

Parameters

activeRisk: bool - Active Risk: Set the portfolio optimization parameters to be relative to the score from the stock universe. i.e. if you set Momentum to 0.5 to 1.0 and the stock universe has a market cap weighed score of 0.2 then the optimizer will solve for 0.7 to 1.2. - Constraints: Boolean value - Default Value: false

allowCompanyCollisions: bool - Allow Multiple Securities Per Company: Allows the machine to trade multiple securities (at the same time) for the same company. If it is ON and a company has listed PREF shares and COMMON shares, both will show up as tradeable options for that company. If it is OFF then system will try to determine the "correct" security to trade based on volume, liquidity, and share type. - Constraints: Boolean value - Default Value: true

allowDR: bool - Allow Depositary Receipts: Companies with tickers ending in .Y and .F will be disallowed from trading within the model with this setting off. Turn it on to allow trading in these securities. - Constraints: Boolean value - Default Value: true

benchmark: array - Benchmark: Set one or more benchmarks and the weighting for each. - Constraints: Required. List of dicts with keys: gbi_id: integer representing the GBI ID of the benchmark index. weight: floating point value between 0 and 1. All gbi_id's in the dictionary must be unique, and the benchmark weights must sum to a number between 0 and 1. - Default Value: []

benchmarkAllocation: float - Benchmark Allocation: None - Constraints: Floating point value representing a percentage between -100 and 100 - Default Value: 0

beta_neutral: bool - Beta Neutral: Adjust the weights to get the ex-ante Beta to be the net exposure of the portfolio. i.e. 70% Long / 30% Short = 0.4 Beta" - Constraints: Boolean value - Default Value: false

bounds_method: str | null - Optimizer Bounds: Tight: The optimizer will only allow the weightings of securities in your portfolio to stay tight (close) to their initial weighting. Loose: The optimizer will allow the weightings of securities in your portfolio to move a medium amount more from their initial weighting. Wide: The optimizer will allow almost any weight between the minimum and maximum weight per long or short position. - Constraints: String enum of {loose, tight, wide}, or null - Default Value: null

compounding: bool - Compound Returns: When toggled on, this will allow the portfolio to compound returns. - Constraints: Boolean value - Default Value: true

currency: str - Currency: The currency you would like your portfolio to be calculated in. Note that if the currency differs from its exchange (i.e. GBP on the S&P), the prior close foreign exchange rate will be used for calculations. - Constraints: String representing a 3 digit ISO currency code. - Default Value: "USD"

equalWeightBenchmark: bool - Equal Weight Benchmark: Instead of using the defined benchmarks, set the benchmark to be an equal weighted (per rebalance period) version of the stock universe. - Constraints: Boolean value - Default Value: false

executionDelay: int - Execution Delay: This option adds the number of days selected to the trade execution. If you select T+1 and your rebalance period is set to Weekly (Monday), it will trade on Tuesday, and so on. - Constraints: Integer value between 0 and 100, inclusive. - Default Value: 0

factorNeutralizeSignal: bool - Signal Optimizer: Turn on optimization at the signal level that will adjust signals and rankings according to the specifications you set. This is done prior to portfolio construction and can help reduce bias in the model. - Constraints: Boolean value - Default Value: false

investmentHorizon: int - Investment Horizon: The investment horizon for the portfolio. - Constraints: An integer representing a number of days - Default Value: 21

lower_turnover: bool - Lower Turnover: If toggled on the optimizer will try to solve the secondary goal of optimizing the primary goals while limiting turnover. - Constraints: Boolean value - Default Value: false

marketCapBounds: float - Maximum Market Cap Variation: How far from the target market cap weighted index each stock can deviate (positive or negative). - Constraints: Floating point value representing a percentage between 0 and 10 - Default Value: 1

marketCapConstraint: bool - Constrain Market Cap: Force weighting to be near the target weights for a market cap weighted index of your entire stock universe. - Constraints: Boolean value - Default Value: false

marketCapFlex: float - Market Cap Flex: How far from the target market cap weighted the stock can deviate relative to the existing weight (i.e. at 75% a 1% position cannot exceed 1.75%). - Constraints: Floating point value representing a percentage between 10 and 200 - Default Value: 100

maxLongPosition: float - Max Long Position per Stock: The maximum weighting of a long position within the portfolio. - Constraints: Required. Floating point value representing a percentage between 0.0 and 300.0 - Default Value: 10.0

maxNumStockLong: int - Maximum Number of Longs: The maximum number of stocks to buy. This is on a per “portfolio” basis, so if you have sector neutral turned on it will be maximum per sector. - Constraints: Integer value between 0 and 1000, inclusive. Must be >= minNumStockLong - Default Value: 50

maxNumStockShort: int - Maximum Number of Shorts: The maximum number of stocks to short. This is on a per “portfolio” basis, so if you have sector neutral turned on it will be maximum per sector. - Constraints: Integer value between 0 and 1000, inclusive. Must be >= minNumStockShort - Default Value: 50

maxOwnership: float - Maximum Ownership: The maximum percentage of a company the portfolio is allowed to own. For example, if a company had a market cap of $100MM and this was 5% the portfolio could not own more than $5MM of that company. - Constraints: Floating point value representing a percentage between 0 and 100 - Default Value: 5

maxShortPosition: float - Max Short Position per Stock: The maximum weighting of a short position within the portfolio. - Constraints: Required. Floating point value representing a percentage between 0.0 and 300.0 - Default Value: 5.0

minimumSharePrice: float - Minimum Share Price: The minimum share price you want the portfolio to allow. - Constraints: Floating point value between 0 and 100000 - Default Value: 2

minLongPosition: float - Min Long Position per Stock: The minimum weighting of a long position within the portfolio. - Constraints: Floating point value representing a percentage between 0.0 and 300.0 - Default Value: 0

minNumStockLong: int - Minimum Number of Longs: The minimum number of stocks to buy. This is on a per “portfolio” basis, so if you have sector neutral turned on it will be minimum per sector. - Constraints: Integer value between 0 and 1000, inclusive. Must be <= maxNumStockLong - Default Value: 5

minNumStockShort: int - Minimum Number of Shorts: The minimum number of stocks to short. This is on a per “portfolio” basis, so if you have sector neutral turned on it will be minimum per sector. - Constraints: Integer value between 0 and 1000, inclusive. Must be <= maxNumStockShort - Default Value: 5

minShortPosition: float - Min Short Position per Stock: The minimum weighting of a short position within the portfolio. - Constraints: Floating point value representing a percentage between 0.0 and 100.0 - Default Value: 0

missingMarketCap: float - Missing Market Cap: The amount in millions (MM) to replace any missing market capitalization information with. This will impact the maximum ownership setting. - Constraints: Floating point value between 0 and 100 - Default Value: 10

nameBasedSectorWeighting: bool - Name Based Sector Weighting: Base sector weightings on number of names. For example, if there are 200 names in the index and financials has 20 companies, the weight of financials should be 10% (20/200). - Constraints: Boolean value - Default Value: false

netBenchmark: bool - Adjust Benchmark for Net Exposure: If your portfolio has a net exposure greater or less than 100%, this will adjust the benchmark returns to match that net exposure. - Constraints: Boolean value - Default Value: false

optimizer: str - Optimizer Type: No Optimization: None Reduce Risk: Optimizer designed to reduce portfolio volatility. This tends to result in the best overall performance. Maximize Sharpe: Optimizer designed to maximize Sharpe using out of sample estimates for expected return. Estimates for expected return have an outsized impact on the performance of this optimizer. Maximize Alpha: Optimizer designed to maximize expected return based on out-of-sample estimates for expected return. Estimates for expected return have an outsized impact on the performance of this optimizer. Min VaR: Minimize Value at Risk by looking back at the proposed portfolio over the last year and trying to minimize drawdowns. Max VaR Sharpe: Maximize Value at Risk Sharpe by looking back at the proposed portfolio over the last year and trying to maximize Sharpe by observed return for the portfolio vs observed volatility. Minimize Skew: Minimize the skew of the portfolio by trying to find a set of portfolio weightings that results in returns that are closer to the mean. - Constraints: Required. String enum: one of default: No Optimization min_vol: Reduce Risk max_sharpe: Maximize Sharpe max_alpha: Maximize Alpha min_var: Min VaR max_var_sharpe: Max VaR Sharpe min_skew: Minimize Skew - Default Value: "default"

optimizeTurnover: bool - Turnover Optimization: Turnover optimization will reduce the turnover at the signal level, making that ranked list more stable. - Constraints: Boolean value - Default Value: false

pairsTrading: bool - Pairs Trading: Pairs Trading forces the portfolio to be created by going long / short an equal number of stocks. The portfolio will try to find an offsetting position for every long by finding securities that are both highly correlated and have a significant difference in star rating. - Constraints: Boolean value - Default Value: false

percentPortfolioLong: float - Percent of Portfolio Long: The exact sum of all long position weightings in the portfolio. - Constraints: Required. Floating point value representing a percentage between 0.0 and 300.0 - Default Value: 100.0

percentPortfolioShort: float - Percent of Portfolio Short: The exact sum of all short position weightings in the portfolio. - Constraints: Required. Floating point value representing a percentage between 0.0 and 300.0 - Default Value: 0.0

percentToLong: float - Percent to Long: The machine will attempt to buy the top X% of stocks, subject to the minimum and maximum specified. This is on a per “portfolio” basis, so if you have sector neutral turned on it will be top X% per sector. This will be subject to the maximum and minimum number of securities you specify below. - Constraints: Floating point value representing a percentage between 0 and 100 - Default Value: 20.0

percentToShort: float - Percent to Short: The machine will attempt to short the bottom X% of stocks, subject to the minimum and maximum specified. This is on a per “portfolio” basis, so if you have sector neutral turned on it will be bottom X% per sector. This will be subject to the maximum and minimum number of securities you specify below. - Constraints: Floating point value representing a percentage between 0 and 100 - Default Value: 20.0

portfolioStartingValue: float - Portfolio Starting Value: The value of the portfolio in MM when it begins trading. If its backtest spans from 2005 - 2020, the starting value in 2005 will be whatever you set here. - Constraints: Floating point value between 0 and 100000. - Default Value: 100.0

priceCleaning: bool - Price Cleaning: Price Cleaning removes any very large price movements from the securities, specifically with a daily change greater than +500% or -83.33%. There are some legitimate securities with moves this large that will be impacted by turning this on, but the advantage is it prevents these securities from overwhelming the backtest. - Constraints: Boolean value - Default Value: true

priceType: str - Price Type: None - Constraints: Required. String enum of {CLOSE, OPEN, VWAP} - Default Value: "VWAP"

rebalancePeriod: str - Rebalance Period: When the machine rebalances the portfolio. You can choose a specific day if you prefer to execute trades on a specific day. - Constraints: Required. An enum value in the following list: "daily" "weekly": Rebalances on every Monday. Chooses the next available trade date to rebalance when Monday is a holiday "weekly_wednesday": Rebalances on every Wednesday. Chooses the next available trade date to rebalance when Wednesday is a holiday "weekly_friday": Rebalances on every Friday. Chooses the previous available trade date to rebalance when Friday is a holiday "monthly" "quarterly" OR a string in the following format: "custom_x" where x is an integer between 1 and 252 (days). - Default Value: "weekly"

sectorNeutral: bool - Sector Neutral: Will be constructed as a series of portfolios - each matching the market cap weighting of each of the stock universe sectors. This means that min. and max. number of stocks "per portfolio" becomes "per sector". - Constraints: Boolean value - Default Value: false

sectorNeutralEqualWeightBenchmark: bool - Sector Neutral Equal Weight Benchmark: Instead of using the defined benchmarks, set the benchmark to be a "sector neutral" equal weighted (per rebalance period) version of the stock universe. This takes the sector weighting and divides by the number of stocks in that sector, so each sector will have a different individual security weighting. - Constraints: Boolean value - Default Value: false

sectorNeutralSpread: float - Sector Neutral Spread: The sector neutral spread is how far away from the benchmark sector allocation the machine can deviate. This is adjusted for net exposure, i.e. if your net exposure is 0% the target sector allocations will be 0%. - Constraints: Floating point value representing a percentage between 0 and 100 - Default Value: 0

signalIndustryNeutral: bool - Industry Neutral: Make the signal industry neutral, plus or minus the signal sector neutral spread. - Constraints: Boolean value - Default Value: false

signalSectorNeutral: bool - Sector Neutral: Make the signal sector neutral, plus or minus the signal sector neutral spread. - Constraints: Boolean value - Default Value: false

signalSectorNeutralSpread: float - Sector Neutral Spread: Make the signal sector neutral, plus or minus the signal sector neutral spread. - Constraints: Floating point value representing a percentage between 0 and 100 - Default Value: 1.0

smoothPeriods: int - Smooth Periods: The number of periods over which to smooth the signals - Constraints: Integer value between 1 and 100. - Default Value: 1

smoothWeighting: str - Smooth Weighting Type: Alpha Weight: The strength of the signal matters in the smoothing. For example, if a stock had a score of 5 stars in period A, 2 stars in period B, your 2 period smoothing would be 7 / 2 = 3.5. Equal Weight: Only the direction of the signal matters here. For example, if you were 5 stars in period A and 2 stars in period B, your 2 period smoothing would be 0 / 2 = 0 (+1 in period 1 and -1 in period 2) - Constraints: One of {alpha_weight, equal_weight} - Default Value: "alpha_weight"

stopGain: bool - Enable Stop Gain: Turn on stop gains for the portfolio. This forces sales of securities that have exceeded the stop gain level. All trades will occur at the next trading time (i.e. next day OPEN for OPEN price models). - Constraints: Boolean value - Default Value: false

stopGainAmount: float - Stop Gain Percentage to Sell: The percentage of the position the machine will sell if the stop gain is triggered. - Constraints: Floating point value representing a percentage between 1 and 100. - Default Value: 50.0

stopGainLevel: float - Stop Gain Threshold: Will sell positions after they hit a threshold. For example, 10% / stop gain, when a position gains 10% the machine will sell a portion of the position (defined by stop gain percentage). - Constraints: Floating point value representing a percentage between 1 and 100. - Default Value: 10.0

stopLoss: bool - Enable Stop Loss: Turn on stop losses for the portfolio. This forces sales of securities that have exceeded the stop loss level. All trades will occur at the next trading time (i.e. next day OPEN for OPEN price models). - Constraints: Boolean value - Default Value: false

stopLossAmount: float - Stop Loss Percentage to Sell: The percentage of the position the machine will sell if the stop loss is triggered. - Constraints: Floating point value representing a percentage between 1 and 100. - Default Value: 50.0

stopLossLevel: float - Stop Loss Threshold: Will sell positions after they hit a threshold. For example, 10% / stop loss, when a position loses 10% the machine will sell a portion of the position (defined by stop loss percentage). - Constraints: Floating point value representing a percentage between 1 and 100. - Default Value: 10.0

stopLossReset: bool - Allow multiple stop loss/gain between rebalance days: Allows the machine to repeatedly trigger stop losses within a rebalance period. For example, if you trade Mondays and the stock drops 15% per day during the week and the stop loss trigger is 10%, then every day will trigger the stop loss. If toggled off the stop loss can only be triggered once per stock per rebalance period. - Constraints: Boolean value - Default Value: false

trackingConstraint: float - Maximum Tracking Error (vs Benchmark): Amount of ex-ante tracking error to constrain the optimizer by. - Constraints: Floating point value representing a percentage between 0 and 30 - Default Value: 10

trackingError: bool - Allocate Weight to Benchmark: Allocate a portion (%) of portfolio to the benchmark - Constraints: Boolean value - Default Value: false

trackingErrorOptimizer: bool - Tracking Error: If toggled on the optimizer will try to solve for an expected tracking error less than the amount specified. This is done out-of-sample and the ex post tracking error will likely be higher. - Constraints: Boolean value - Default Value: false

tradingCost: float - Trading Cost: A cost that will be applied to every trade. - Constraints: Required. Floating point value representing a percentage between 0.0 and 5.0 - Default Value: 0.01

turnoverImportance: float - Turnover Importance: High importance means the algorithm will aim to keep turnover low, whereas low importance of turnover will let it vary more. - Constraints: Floating point value between 0.2 and 5 nonlinearly mapped onto the 1-9 interval. - Default Value: 1

useHoldingPeriod: bool - Use Holding Period: Set any covariance or other calculations within the optimizer to use a holding period return instead of daily return series - Constraints: Boolean value - Default Value: false

weightingType: str - Initial Weighting: Alpha Weight: The initial weighting will be done such that higher ranked stocks have a higher weight. Equal Weight: The initial weighting will be done such that all stocks selected to be in the portfolio will have an equal weight. Market Cap Weight: The initial weighting will be done such that all stocks are weighted according to their previous day’s market capitalization. - Constraints: One of {alpha_weight, equal_weight, market_cap_weight} - Default Value: "alpha_weight"

PortfolioSettings(settings: Union[Dict, NoneType] = None)
1202    def __init__(self, settings: Optional[Dict] = None):
1203        # TODO: i'd rather have kwargs for the different keys and then allow
1204        # for per-key overrides, but we start with this more conservative approach
1205        if settings is None:
1206            self.settings = copy.deepcopy(PortfolioSettings.__default_settings)
1207        else:
1208            self.settings = settings
def toDict(self)
1210    def toDict(self):
1211        return self.settings
def fromDict(settings)
1219    def fromDict(settings):
1220        return PortfolioSettings(settings)
class HedgeExperiment:
1227class HedgeExperiment:
1228    id: str  # really a uuid, which we represent as a string
1229    name: str
1230    user_id: str  # see id comment
1231    description: str
1232    experiment_type: hedge_experiment_type  # TODO: enum worth it?
1233    last_calculated: dt.datetime
1234    last_modified: dt.datetime
1235    status: str  # TODO: enum worth it?
1236    portfolio_calculation_status: str  # TODO: enum worth it?
1237    selected_models: List[str]  # see id comment
1238    target_securities: Dict[GbiIdSecurity, float]  # Security is a hashable type because frozen=True
1239    target_portfolios: List[str]
1240    selected_stock_universe_ids: List[str]  # see id comment
1241    baseline_model: Optional[str] = None
1242    baseline_stock_universe_id: Optional[str] = None
1243    baseline_scenario: Optional["HedgeExperimentScenario"] = None
1244
1245    @property
1246    def config(self) -> Dict:
1247        """
1248        Returns a hedge experiment configuration dictionary, ready for JSON-serialization and
1249        submission.
1250        """
1251        weights_by_gbiid = [
1252            {"gbiId": sec.id, "weight": weight} for sec, weight in self.target_securities.items()
1253        ]
1254        return {
1255            "experimentType": self.experiment_type,
1256            "selectedModels": self.selected_models,
1257            "targetSecurities": weights_by_gbiid,
1258            "selectedStockUniverseIds": self._selected_stock_universe_ids,
1259        }
1260
1261    @classmethod
1262    def from_json_dict(cls, d: Dict) -> "HedgeExperiment":
1263        # drafts arent fully populated
1264        if d["status"] == "DRAFT":
1265            weights_by_security = {}
1266            selected_models = []
1267            selected_stock_universe_ids = []
1268        else:
1269            weights_by_security = {
1270                GbiIdSecurity(
1271                    sec_dict["gbiId"],
1272                    None,
1273                    sec_dict["security"]["symbol"],
1274                    sec_dict["security"]["name"],
1275                ): sec_dict["weight"]
1276                for sec_dict in d["targetSecurities"]
1277            }
1278            experiment_config = json.loads(d["config"])
1279            selected_models = experiment_config["selectedModels"]
1280            selected_stock_universe_ids = experiment_config["selectedStockUniverseIds"]
1281        baseline_scenario = None
1282        if d["baselineScenario"] is not None:
1283            baseline_scenario = HedgeExperimentScenario.from_json_dict(d["baselineScenario"])
1284        return cls(
1285            d["hedgeExperimentId"],
1286            d["experimentName"],
1287            d["userId"],
1288            d["description"],
1289            d["experimentType"],
1290            d["lastCalculated"],
1291            d["lastModified"],
1292            d["status"],
1293            d["portfolioCalcStatus"],
1294            selected_models,
1295            weights_by_security,
1296            d.get("targetPortfolios"),
1297            selected_stock_universe_ids or [],
1298            d.get("baselineModel"),
1299            d.get("baselineStockUniverseId"),
1300            baseline_scenario,
1301        )

HedgeExperiment(id: str, name: str, user_id: str, description: str, experiment_type: Literal['HEDGE', 'MIMIC'], last_calculated: datetime.datetime, last_modified: datetime.datetime, status: str, portfolio_calculation_status: str, selected_models: List[str], target_securities: Dict[boosted.api.api_type.GbiIdSecurity, float], target_portfolios: List[str], selected_stock_universe_ids: List[str], baseline_model: Union[str, NoneType] = None, baseline_stock_universe_id: Union[str, NoneType] = None, baseline_scenario: Union[ForwardRef('HedgeExperimentScenario'), NoneType] = None)

HedgeExperiment( id: str, name: str, user_id: str, description: str, experiment_type: Literal['HEDGE', 'MIMIC'], last_calculated: datetime.datetime, last_modified: datetime.datetime, status: str, portfolio_calculation_status: str, selected_models: List[str], target_securities: Dict[boosted.api.api_type.GbiIdSecurity, float], target_portfolios: List[str], selected_stock_universe_ids: List[str], baseline_model: Union[str, NoneType] = None, baseline_stock_universe_id: Union[str, NoneType] = None, baseline_scenario: Union[boosted.api.api_type.HedgeExperimentScenario, NoneType] = None)
baseline_model: Union[str, NoneType] = None
baseline_stock_universe_id: Union[str, NoneType] = None
baseline_scenario: Union[boosted.api.api_type.HedgeExperimentScenario, NoneType] = None
config: Dict

Returns a hedge experiment configuration dictionary, ready for JSON-serialization and submission.

@classmethod
def from_json_dict(cls, d: Dict) -> boosted.api.api_type.HedgeExperiment:
1261    @classmethod
1262    def from_json_dict(cls, d: Dict) -> "HedgeExperiment":
1263        # drafts arent fully populated
1264        if d["status"] == "DRAFT":
1265            weights_by_security = {}
1266            selected_models = []
1267            selected_stock_universe_ids = []
1268        else:
1269            weights_by_security = {
1270                GbiIdSecurity(
1271                    sec_dict["gbiId"],
1272                    None,
1273                    sec_dict["security"]["symbol"],
1274                    sec_dict["security"]["name"],
1275                ): sec_dict["weight"]
1276                for sec_dict in d["targetSecurities"]
1277            }
1278            experiment_config = json.loads(d["config"])
1279            selected_models = experiment_config["selectedModels"]
1280            selected_stock_universe_ids = experiment_config["selectedStockUniverseIds"]
1281        baseline_scenario = None
1282        if d["baselineScenario"] is not None:
1283            baseline_scenario = HedgeExperimentScenario.from_json_dict(d["baselineScenario"])
1284        return cls(
1285            d["hedgeExperimentId"],
1286            d["experimentName"],
1287            d["userId"],
1288            d["description"],
1289            d["experimentType"],
1290            d["lastCalculated"],
1291            d["lastModified"],
1292            d["status"],
1293            d["portfolioCalcStatus"],
1294            selected_models,
1295            weights_by_security,
1296            d.get("targetPortfolios"),
1297            selected_stock_universe_ids or [],
1298            d.get("baselineModel"),
1299            d.get("baselineStockUniverseId"),
1300            baseline_scenario,
1301        )
class HedgeExperimentScenario:
1305class HedgeExperimentScenario:
1306    id: str  # really a uuid, which we represent as a string;
1307    name: str
1308    description: str
1309    status: str  # TODO: enum worth it?
1310    settings: PortfolioSettings
1311    summary: pd.DataFrame
1312    # we currently a portfolios field. they get wrapped up into the summary table
1313
1314    @classmethod
1315    def from_json_dict(cls, d: Dict) -> "HedgeExperimentScenario":
1316        return cls(
1317            d["hedgeExperimentScenarioId"],
1318            d["scenarioName"],
1319            d["description"],
1320            d.get("status"),
1321            PortfolioSettings(json.loads(d["portfolioSettingsJson"])),
1322            HedgeExperimentScenario._build_portfolio_summary(d.get("hedgeExperimentPortfolios")),
1323        )
1324
1325    @staticmethod
1326    def _build_portfolio_summary(portfolios: Dict) -> pd.DataFrame:
1327        # this happens for added scenarios that havent run yet
1328        if portfolios is None:
1329            return pd.DataFrame()
1330
1331        df_dict = defaultdict(list)
1332        for pf in portfolios:
1333            p = pf["portfolio"]
1334
1335            df_dict["portfolio_id"].append(p["id"])
1336            df_dict["scenario_name"].append(p["name"])
1337            df_dict["model_id"].append(p["modelId"])
1338            df_dict["status"].append(p["status"])
1339
1340            if p["status"] != "READY":  # data isn't yet available
1341                df_dict["1M"].append(np.nan)
1342                df_dict["3M"].append(np.nan)
1343                df_dict["1Y"].append(np.nan)
1344                df_dict["sharpe"].append(np.nan)
1345                df_dict["vol"].append(np.nan)
1346            else:
1347                # NOTE:
1348                # these are the magic indices/keys of these values; inspect passed arg to see
1349                # if upstream changes the order of (sub-)elements in the response...uh oh
1350                # TODO: specific key search? wrap with try/except and fail loudly?
1351                df_dict["1M"].append(p["performanceGrid"][0][4])
1352                df_dict["3M"].append(p["performanceGrid"][0][5])
1353                df_dict["1Y"].append(p["performanceGrid"][0][7])
1354                df_dict["sharpe"].append(p["tearSheet"][0]["members"][1]["value"])
1355                pf_stddev = p["tearSheet"][1]["members"][1]["value"]
1356                bnchmk_stddev = p["tearSheet"][1]["members"][2]["value"]
1357                df_dict["vol"].append((pf_stddev - bnchmk_stddev) * 100)
1358                # TODO: this is how it is done on the front-end,
1359                # should be pulled out of both here and there
1360
1361        df = pd.DataFrame(df_dict)
1362        return df

HedgeExperimentScenario(id: str, name: str, description: str, status: str, settings: boosted.api.api_type.PortfolioSettings, summary: pandas.core.frame.DataFrame)

HedgeExperimentScenario( id: str, name: str, description: str, status: str, settings: boosted.api.api_type.PortfolioSettings, summary: pandas.core.frame.DataFrame)
@classmethod
def from_json_dict(cls, d: Dict) -> boosted.api.api_type.HedgeExperimentScenario:
1314    @classmethod
1315    def from_json_dict(cls, d: Dict) -> "HedgeExperimentScenario":
1316        return cls(
1317            d["hedgeExperimentScenarioId"],
1318            d["scenarioName"],
1319            d["description"],
1320            d.get("status"),
1321            PortfolioSettings(json.loads(d["portfolioSettingsJson"])),
1322            HedgeExperimentScenario._build_portfolio_summary(d.get("hedgeExperimentPortfolios")),
1323        )
class HedgeExperimentDetails:
1366class HedgeExperimentDetails:
1367    # this will probably lead to violations of the law of demeter, but it's easy to do right now,
1368    # particularly for an unproven API
1369    experiment: HedgeExperiment
1370    scenarios: Dict[str, HedgeExperimentScenario]
1371
1372    @property
1373    def summary(self) -> pd.DataFrame:
1374        """
1375        Returns a dataframe corresponding to the Hedge Experiment "flat view" - all portfolio
1376        options are displayed
1377        """
1378        if len(self.scenarios) == 0 or all(s.status != "READY" for s in self.scenarios.values()):
1379            return pd.DataFrame()
1380        return pd.concat(scenario.summary for scenario in self.scenarios.values()).sort_values(
1381            ["scenario_name"]
1382        )  # sort for stable presentation
1383
1384    @classmethod
1385    def from_json_dict(cls, d: Dict) -> "HedgeExperimentDetails":
1386        he = HedgeExperiment.from_json_dict(d)
1387        scenarios = {
1388            scenario_dict["hedgeExperimentScenarioId"]: HedgeExperimentScenario.from_json_dict(
1389                scenario_dict
1390            )
1391            for scenario_dict in d["hedgeExperimentScenarios"]
1392        }
1393        return cls(he, scenarios)

HedgeExperimentDetails(experiment: boosted.api.api_type.HedgeExperiment, scenarios: Dict[str, boosted.api.api_type.HedgeExperimentScenario])

HedgeExperimentDetails( experiment: boosted.api.api_type.HedgeExperiment, scenarios: Dict[str, boosted.api.api_type.HedgeExperimentScenario])
summary: pandas.core.frame.DataFrame

Returns a dataframe corresponding to the Hedge Experiment "flat view" - all portfolio options are displayed

@classmethod
def from_json_dict(cls, d: Dict) -> boosted.api.api_type.HedgeExperimentDetails:
1384    @classmethod
1385    def from_json_dict(cls, d: Dict) -> "HedgeExperimentDetails":
1386        he = HedgeExperiment.from_json_dict(d)
1387        scenarios = {
1388            scenario_dict["hedgeExperimentScenarioId"]: HedgeExperimentScenario.from_json_dict(
1389                scenario_dict
1390            )
1391            for scenario_dict in d["hedgeExperimentScenarios"]
1392        }
1393        return cls(he, scenarios)