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

An enumeration.

XIC = <ThemeUniverse.XIC: 'XIC Membership (TSX Composite)'>
SPY = <ThemeUniverse.SPY: 'SPY Membership (S&P 500)'>
QQQ = <ThemeUniverse.QQQ: 'QQQ Membership (Nasdaq)'>
IWM = <ThemeUniverse.IWM: 'IWM Membership (Russell 2000)'>
IWB = <ThemeUniverse.IWB: 'IWB Membership (Russell 1000)'>
IWV = <ThemeUniverse.IWV: 'IWV Membership (iShares Russell 3000 ETF)'>
EXSA = <ThemeUniverse.EXSA: 'EXSA Membership (Stoxx 600)'>
ASHR = <ThemeUniverse.ASHR: 'ASHR Membership (CSI 300)'>
Inherited Members
enum.Enum
name
value
class NewsHorizon(enum.Enum):
122class NewsHorizon(Enum):
123    ONE_DAY = "1D"
124    ONE_WEEK = "1W"
125    ONE_MONTH = "1M"

An enumeration.

ONE_DAY = <NewsHorizon.ONE_DAY: '1D'>
ONE_WEEK = <NewsHorizon.ONE_WEEK: '1W'>
ONE_MONTH = <NewsHorizon.ONE_MONTH: '1M'>
Inherited Members
enum.Enum
name
value
class ColumnRole(enum.Enum):
128class ColumnRole(Enum):
129    UNKNOWN = 0
130    VARIABLE = 1
131    GOAL = 2
132    METRIC = 3
133    IDENTIFIER = 4
134
135    def __str__(self):
136        if self.name == "VARIABLE":
137            return "VARIABLE"
138        elif self.name == "GOAL":
139            return "GOAL"
140        elif self.name == "METRIC":
141            return "METRIC"
142        elif self.name == "IDENTIFIER":
143            return "IDENTIFIER"
144        else:
145            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):
148class ColumnSubRole(Enum):
149    UNKNOWN = 0
150    GBI_ID = 1
151    ISIN = 2
152    GVKEY = 3
153    IID = 4
154    SYMBOL = 5
155    COUNTRY = 6
156    CURRENCY = 7
157    DATE = 8
158    COMPANY_NAME = 9
159    REPORT_DATE = 10
160    REPORT_PERIOD = 11
161
162    def __str__(self):
163        if self.name == "GBI_ID":
164            return "GBI_ID"
165        elif self.name == "ISIN":
166            return "ISIN"
167        elif self.name == "GVKEY":
168            return "GVKEY"
169        elif self.name == "IID":
170            return "IID"
171        elif self.name == "SYMBOL":
172            return "SYMBOL"
173        elif self.name == "COUNTRY":
174            return "COUNTRY"
175        elif self.name == "CURRENCY":
176            return "CURRENCY"
177        elif self.name == "DATE":
178            return "DATE"
179        elif self.name == "COMPANY_NAME":
180            return "COMPANY_NAME"
181        elif self.name == "REPORT_DATE":
182            return "REPORT_DATE"
183        elif self.name == "REPORT_PERIOD":
184            return "REPORT_PERIOD"
185        else:
186            return "UNKNOWN"
187
188    def get_match(column_name):
189        def clean_str(name):
190            translation = str.maketrans("", "", string.punctuation)
191            clean_name = name.strip().translate(translation).lower()
192            return re.sub(r"\s+", "", clean_name)
193
194        identifiers = [
195            ColumnSubRole.GBI_ID,
196            ColumnSubRole.ISIN,
197            ColumnSubRole.GVKEY,
198            ColumnSubRole.IID,
199            ColumnSubRole.SYMBOL,
200            ColumnSubRole.COUNTRY,
201            ColumnSubRole.CURRENCY,
202            ColumnSubRole.COMPANY_NAME,
203            ColumnSubRole.REPORT_DATE,
204            ColumnSubRole.REPORT_PERIOD,
205        ]
206
207        for identifier in identifiers:
208            if clean_str(str(identifier)) == clean_str(column_name):
209                return identifier
210        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):
188    def get_match(column_name):
189        def clean_str(name):
190            translation = str.maketrans("", "", string.punctuation)
191            clean_name = name.strip().translate(translation).lower()
192            return re.sub(r"\s+", "", clean_name)
193
194        identifiers = [
195            ColumnSubRole.GBI_ID,
196            ColumnSubRole.ISIN,
197            ColumnSubRole.GVKEY,
198            ColumnSubRole.IID,
199            ColumnSubRole.SYMBOL,
200            ColumnSubRole.COUNTRY,
201            ColumnSubRole.CURRENCY,
202            ColumnSubRole.COMPANY_NAME,
203            ColumnSubRole.REPORT_DATE,
204            ColumnSubRole.REPORT_PERIOD,
205        ]
206
207        for identifier in identifiers:
208            if clean_str(str(identifier)) == clean_str(column_name):
209                return identifier
210        return None
Inherited Members
enum.Enum
name
value
class ColumnValueType(enum.Enum):
213class ColumnValueType(Enum):
214    UNKNOWN = 0
215    NUMBER = 1
216    STRING = 2
217
218    def __str__(self):
219        if self.name == "UNKNOWN":
220            return "UNKNOWN"
221        elif self.name == "NUMBER":
222            return "NUMBER"
223        elif self.name == "STRING":
224            return "STRING"
225        else:
226            return "UNKNOWN"

An enumeration.

UNKNOWN = <ColumnValueType.UNKNOWN: 0>
NUMBER = <ColumnValueType.NUMBER: 1>
STRING = <ColumnValueType.STRING: 2>
Inherited Members
enum.Enum
name
value
class ColumnConfig:
229class ColumnConfig:
230    def __init__(
231        self,
232        name=None,
233        role=None,
234        sub_role=None,
235        value_type=ColumnValueType.NUMBER,
236        description="",
237        investment_horizon=None,
238    ):
239        self.name = name
240        self.role = role
241        self.sub_role = sub_role
242        self.value_type = value_type
243        # More value_types will be supported in the future.
244        self.description = description
245        self.investment_horizon = investment_horizon
246
247    def __str__(self):
248        return 'Name: "{0}", Role: {1}, SubRole: {2}, Value type: {3}, Desc: {4} IH: {5}.'.format(
249            self.name,
250            self.role,
251            self.sub_role,
252            self.value_type,
253            self.description,
254            self.investment_horizon,
255        )
256
257    def __repr__(self):
258        return self.__str__()
ColumnConfig( name=None, role=None, sub_role=None, value_type=<ColumnValueType.NUMBER: 1>, description='', investment_horizon=None)
230    def __init__(
231        self,
232        name=None,
233        role=None,
234        sub_role=None,
235        value_type=ColumnValueType.NUMBER,
236        description="",
237        investment_horizon=None,
238    ):
239        self.name = name
240        self.role = role
241        self.sub_role = sub_role
242        self.value_type = value_type
243        # More value_types will be supported in the future.
244        self.description = description
245        self.investment_horizon = investment_horizon
name
role
sub_role
value_type
description
investment_horizon
class StrategyConfig:
261class StrategyConfig:
262    def __init__(self, name=None, source_name=None):
263        self.name = name
264        self.source_name = source_name
265
266    def __str__(self):
267        return 'Name: "{0}", Source Name: {1}.'.format(self.name, self.source_name)
268
269    def __repr__(self):
270        return self.__str__()
StrategyConfig(name=None, source_name=None)
262    def __init__(self, name=None, source_name=None):
263        self.name = name
264        self.source_name = source_name
name
source_name
class BoostedAPIException(builtins.Exception):
273class BoostedAPIException(Exception):
274    def __init__(self, value, data=None):
275        self.value = value
276        self.data = data
277
278    def __str__(self):
279        return repr(self.value)

Common base class for all non-exit exceptions.

BoostedAPIException(value, data=None)
274    def __init__(self, value, data=None):
275        self.value = value
276        self.data = data
value
data
Inherited Members
builtins.BaseException
with_traceback
args
class BoostedDataSetSchemaException(builtins.Exception):
282class BoostedDataSetSchemaException(Exception):
283    def __init__(self, value, data=None):
284        self.value = value
285        self.data = data
286
287    def __str__(self):
288        return repr(self.value)

Common base class for all non-exit exceptions.

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

Common base class for all non-exit exceptions.

GbiIdSecurityException(value, data=None)
458    def __init__(self, value, data=None):
459        self.value = value
460        self.data = data
value
data
Inherited Members
builtins.BaseException
with_traceback
args
class GbiIdTickerISIN:
466class GbiIdTickerISIN:
467    def __init__(self, gbi_id: int, ticker: str, isin: str):
468        self.gbi_id = int(gbi_id)
469        self.ticker = ticker
470        self.isin = isin
471
472    def __str__(self):
473        return "(gbi_id: {0}, ticker: {1}, isin: {2})".format(self.gbi_id, self.ticker, self.isin)
474
475    def __repr__(self):
476        return self.__str__()
GbiIdTickerISIN(gbi_id: int, ticker: str, isin: str)
467    def __init__(self, gbi_id: int, ticker: str, isin: str):
468        self.gbi_id = int(gbi_id)
469        self.ticker = ticker
470        self.isin = isin
gbi_id
ticker
isin
class GbiIdSecurity:
479class GbiIdSecurity:
480    def __init__(self, gbi_id, isin_country_currency_date, ticker, company_name):
481        self.gbi_id = gbi_id
482        self.isin_info = isin_country_currency_date
483        self.ticker = ticker
484        self.company_name = company_name
485
486    def __str__(self):
487        return '\n(gbi_id: "{0}", isin_info: {1}, ticker: {2}, company_name: {3})'.format(
488            self.gbi_id, self.isin_info.__str__(), self.ticker, self.company_name
489        )
490
491    def __repr__(self):
492        return self.__str__()
493
494    def __eq__(self, __o: object) -> bool:
495        return self.gbi_id == __o.gbi_id
496
497    def __hash__(self):
498        return hash(self.gbi_id)
GbiIdSecurity(gbi_id, isin_country_currency_date, ticker, company_name)
480    def __init__(self, gbi_id, isin_country_currency_date, ticker, company_name):
481        self.gbi_id = gbi_id
482        self.isin_info = isin_country_currency_date
483        self.ticker = ticker
484        self.company_name = company_name
gbi_id
isin_info
ticker
company_name
class DateIdentCountryCurrencyException(builtins.Exception):
501class DateIdentCountryCurrencyException(Exception):
502    def __init__(self, value, data=None):
503        self.value = value
504        self.data = data
505
506    def __str__(self):
507        return repr(self.value)

Common base class for all non-exit exceptions.

DateIdentCountryCurrencyException(value, data=None)
502    def __init__(self, value, data=None):
503        self.value = value
504        self.data = data
value
data
Inherited Members
builtins.BaseException
with_traceback
args
class DateIsinCountryCurrencyException(DateIdentCountryCurrencyException):
511class DateIsinCountryCurrencyException(DateIdentCountryCurrencyException):
512    pass

Common base class for all non-exit exceptions.

Inherited Members
DateIdentCountryCurrencyException
DateIdentCountryCurrencyException
value
data
builtins.BaseException
with_traceback
args
class DateIdentCountryCurrency:
515class DateIdentCountryCurrency:
516    def __init__(
517        self,
518        date: str,
519        identifier: str,
520        country: Optional[str] = None,
521        currency: Optional[str] = None,
522        id_type: ColumnSubRole = ColumnSubRole.ISIN,
523    ):
524        self.date = date
525        self.identifier = identifier
526        self.id_type = id_type
527        self.country = country
528        self.currency = currency
529        self.__validateInput()
530
531    def __str__(self):
532        return '(date: "{0}", identifier: "{1}", country: {2}, currency: {3})'.format(
533            self.date, self.identifier, self.country, self.currency
534        )
535
536    def __repr__(self):
537        return self.__str__()
538
539    def __validateInput(self):
540        # simply ensure that at least isin and date are filled out.
541        # country/currency defaults to ANY if not filled out, but
542        # still is not recommended.
543        if self.identifier is None or self.date is None:
544            raise DateIdentCountryCurrencyException(
545                "ISIN or Date not provided while constructing DateIsinCountryCurrency"
546            )
547
548        if self.id_type not in (ColumnSubRole.ISIN, ColumnSubRole.SYMBOL):
549            raise DateIdentCountryCurrencyException(f"Invalid ID Type: {self.id_type}")
550
551        def check_is_str(value, fieldname):
552            if value is not None and not isinstance(value, str):
553                raise DateIdentCountryCurrencyException(
554                    f"Field {fieldname} in DateIsinCountryCurrency was not a string."
555                )
556
557        check_is_str(self.date, "date")
558        check_is_str(self.identifier, "identifier")
559        check_is_str(self.country, "country")
560        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>)
516    def __init__(
517        self,
518        date: str,
519        identifier: str,
520        country: Optional[str] = None,
521        currency: Optional[str] = None,
522        id_type: ColumnSubRole = ColumnSubRole.ISIN,
523    ):
524        self.date = date
525        self.identifier = identifier
526        self.id_type = id_type
527        self.country = country
528        self.currency = currency
529        self.__validateInput()
date
identifier
id_type
country
currency
class DateIsinCountryCurrency(DateIdentCountryCurrency):
564class DateIsinCountryCurrency(DateIdentCountryCurrency):
565    def __init__(self, date, isin, country, currency):
566        super().__init__(date, isin, country, currency, ColumnSubRole.ISIN)
567        self.isin = isin
568
569    def __str__(self):
570        return '(date: "{0}", isin: "{1}", country: {2}, currency: {3})'.format(
571            self.date, self.isin, self.country, self.currency
572        )
DateIsinCountryCurrency(date, isin, country, currency)
565    def __init__(self, date, isin, country, currency):
566        super().__init__(date, isin, country, currency, ColumnSubRole.ISIN)
567        self.isin = isin
isin
class IdentifierToColumnMapping:
575class IdentifierToColumnMapping:
576    """
577    IdentifierToColumnMapping is a mapping from string to boosted.api.api_type.ColumnSubRole.
578    The key of the mapping corresponds to a CSV column name, and the value corresponds
579    to the type of identifier that it maps to. Only columns specifying an identifying factor
580    for the stock needs to be specified (e.g. ISIN, Date, Symbol, Currency)
581    """
582
583    def __init__(self, mapping):
584        self.mapping = mapping
585        self.__validateInput()
586
587    def __str__(self):
588        return repr(self.value)
589
590    def __validateInput(self):
591        def validate_key_val(key, val):
592            if not isinstance(key, str):
593                raise Exception(f"key in IdentifierToColumnMapping {key} was not a string!")
594            if not isinstance(val, ColumnSubRole):
595                raise Exception(f"val in IdentifierToColumnMapping {val} was not a ColumnSubRole!")
596
597        for key, val in self.mapping:
598            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)
583    def __init__(self, mapping):
584        self.mapping = mapping
585        self.__validateInput()
mapping
class PortfolioSettings:
 601class PortfolioSettings:
 602    """
 603    This config is a documentation wrapper around a dict representing the portfolio settings json.
 604    Keys missing in the dict provided will fallback to the listed default when updating the
 605    the portfolio settings on the server.
 606
 607    Parameters
 608    ----------
 609    activeRisk: bool
 610        - **Active Risk**:
 611            Set the portfolio optimization parameters to be relative to the score from the stock
 612            universe. i.e. if you set Momentum to 0.5 to 1.0 and the stock universe has a market cap
 613            weighed score of 0.2 then the optimizer will solve for 0.7 to 1.2.
 614        - Constraints:
 615            Boolean value
 616        - Default Value: false
 617
 618    allowCompanyCollisions: bool
 619        - **Allow Multiple Securities Per Company**:
 620            Allows the machine to trade multiple securities (at the same time) for the same company.
 621            If it is ON and a company has listed PREF shares and COMMON shares, both will show up as
 622            tradeable options for that company. If it is OFF then system will try to determine the
 623            "correct" security to trade based on volume, liquidity, and share type.
 624        - Constraints:
 625            Boolean value
 626        - Default Value: true
 627
 628    allowDR: bool
 629        - **Allow Depositary Receipts**:
 630            Companies with tickers ending in .Y and .F will be disallowed from trading within the
 631            model with this setting off. Turn it on to allow trading in these securities.
 632        - Constraints:
 633            Boolean value
 634        - Default Value: true
 635
 636    benchmark: array
 637        - **Benchmark**:
 638            Set one or more benchmarks and the weighting for each.
 639        - Constraints:
 640            Required.
 641            List of dicts with keys:
 642            gbi_id: integer representing the GBI ID of the benchmark index.
 643            weight: floating point value between 0 and 1.
 644            All gbi_id's in the dictionary must be unique, and the benchmark weights must sum to a
 645            number between 0 and 1.
 646        - Default Value: []
 647
 648    benchmarkAllocation: float
 649        - **Benchmark Allocation**:
 650            None
 651        - Constraints:
 652            Floating point value representing a percentage between -100 and 100
 653        - Default Value: 0
 654
 655    beta_neutral: bool
 656        - **Beta Neutral**:
 657            Adjust the weights to get the ex-ante Beta to be the net exposure of the portfolio. i.e.
 658            70% Long / 30% Short = 0.4 Beta"
 659        - Constraints:
 660            Boolean value
 661        - Default Value: false
 662
 663    bounds_method: str | null
 664        - **Optimizer Bounds**:
 665            Tight: The optimizer will only allow the weightings of securities in your portfolio to
 666            stay tight (close) to their initial weighting.
 667            Loose: The optimizer will allow the weightings of securities in your portfolio to move a
 668            medium amount more from their initial weighting.
 669            Wide: The optimizer will allow almost any weight between the minimum and maximum weight
 670            per long or short position.
 671        - Constraints:
 672            String enum of {loose, tight, wide}, or null
 673        - Default Value: null
 674
 675    compounding: bool
 676        - **Compound Returns**:
 677            When toggled on, this will allow the portfolio to compound returns.
 678        - Constraints:
 679            Boolean value
 680        - Default Value: true
 681
 682    currency: str
 683        - **Currency**:
 684            The currency you would like your portfolio to be calculated in. Note that if the
 685            currency differs from its exchange (i.e. GBP on the S&P), the prior close foreign
 686            exchange rate will be used for calculations.
 687        - Constraints:
 688            String representing a 3 digit ISO currency code.
 689        - Default Value: "USD"
 690
 691    equalWeightBenchmark: bool
 692        - **Equal Weight Benchmark**:
 693            Instead of using the defined benchmarks, set the benchmark to be an equal weighted (per
 694            rebalance period) version of the stock universe.
 695        - Constraints:
 696            Boolean value
 697        - Default Value: false
 698
 699    executionDelay: int
 700        - **Execution Delay**:
 701            This option adds the number of days selected to the trade execution. If you select T+1
 702            and your rebalance period is set to Weekly (Monday), it will trade on Tuesday, and so
 703            on.
 704        - Constraints:
 705            Integer value between 0 and 100, inclusive.
 706        - Default Value: 0
 707
 708    factorNeutralizeSignal: bool
 709        - **Signal Optimizer**:
 710            Turn on optimization at the signal level that will adjust signals and rankings according
 711            to the specifications you set. This is done prior to portfolio construction and can help
 712            reduce bias in the model.
 713        - Constraints:
 714            Boolean value
 715        - Default Value: false
 716
 717    investmentHorizon: int
 718        - **Investment Horizon**:
 719            The investment horizon for the portfolio.
 720        - Constraints:
 721            An integer representing a number of days
 722        - Default Value: 21
 723
 724    lower_turnover: bool
 725        - **Lower Turnover**:
 726            If toggled on the optimizer will try to solve the secondary goal of optimizing the
 727            primary goals while limiting turnover.
 728        - Constraints:
 729            Boolean value
 730        - Default Value: false
 731
 732    marketCapBounds: float
 733        - **Maximum Market Cap Variation**:
 734            How far from the target market cap weighted index each stock can deviate (positive or
 735            negative).
 736        - Constraints:
 737            Floating point value representing a percentage between 0 and 10
 738        - Default Value: 1
 739
 740    marketCapConstraint: bool
 741        - **Constrain Market Cap**:
 742            Force weighting to be near the target weights for a market cap weighted index of your
 743            entire stock universe.
 744        - Constraints:
 745            Boolean value
 746        - Default Value: false
 747
 748    marketCapFlex: float
 749        - **Market Cap Flex**:
 750            How far from the target market cap weighted the stock can deviate relative to the
 751            existing weight (i.e. at 75% a 1% position cannot exceed 1.75%).
 752        - Constraints:
 753            Floating point value representing a percentage between 10 and 200
 754        - Default Value: 100
 755
 756    maxLongPosition: float
 757        - **Max Long Position per Stock**:
 758            The maximum weighting of a long position within the portfolio.
 759        - Constraints:
 760            Required. Floating point value representing a percentage between 0.0 and 300.0
 761        - Default Value: 10.0
 762
 763    maxNumStockLong: int
 764        - **Maximum Number of Longs**:
 765            The maximum number of stocks to buy. This is on a per “portfolio” basis, so if you have
 766            sector neutral turned on it will be maximum per sector.
 767        - Constraints:
 768            Integer value between 0 and 1000, inclusive. Must be >= minNumStockLong
 769        - Default Value: 50
 770
 771    maxNumStockShort: int
 772        - **Maximum Number of Shorts**:
 773            The maximum number of stocks to short. This is on a per “portfolio” basis, so if you
 774            have sector neutral turned on it will be maximum per sector.
 775        - Constraints:
 776            Integer value between 0 and 1000, inclusive. Must be >= minNumStockShort
 777        - Default Value: 50
 778
 779    maxOwnership: float
 780        - **Maximum Ownership**:
 781            The maximum percentage of a company the portfolio is allowed to own. For example, if a
 782            company had a market cap of $100MM and this was 5% the portfolio could not own more than
 783            $5MM of that company.
 784        - Constraints:
 785            Floating point value representing a percentage between 0 and 100
 786        - Default Value: 5
 787
 788    maxShortPosition: float
 789        - **Max Short Position per Stock**:
 790            The maximum weighting of a short position within the portfolio.
 791        - Constraints:
 792            Required. Floating point value representing a percentage between 0.0 and 300.0
 793        - Default Value: 5.0
 794
 795    minimumSharePrice: float
 796        - **Minimum Share Price**:
 797            The minimum share price you want the portfolio to allow.
 798        - Constraints:
 799            Floating point value between 0 and 100000
 800        - Default Value: 2
 801
 802    minLongPosition: float
 803        - **Min Long Position per Stock**:
 804            The minimum weighting of a long position within the portfolio.
 805        - Constraints:
 806            Floating point value representing a percentage between 0.0 and 300.0
 807        - Default Value: 0
 808
 809    minNumStockLong: int
 810        - **Minimum Number of Longs**:
 811            The minimum number of stocks to buy. This is on a per “portfolio” basis, so if you have
 812            sector neutral turned on it will be minimum per sector.
 813        - Constraints:
 814            Integer value between 0 and 1000, inclusive. Must be <= maxNumStockLong
 815        - Default Value: 5
 816
 817    minNumStockShort: int
 818        - **Minimum Number of Shorts**:
 819            The minimum number of stocks to short. This is on a per “portfolio” basis, so if you
 820            have sector neutral turned on it will be minimum per sector.
 821        - Constraints:
 822            Integer value between 0 and 1000, inclusive. Must be <= maxNumStockShort
 823        - Default Value: 5
 824
 825    minShortPosition: float
 826        - **Min Short Position per Stock**:
 827            The minimum weighting of a short position within the portfolio.
 828        - Constraints:
 829            Floating point value representing a percentage between 0.0 and 100.0
 830        - Default Value: 0
 831
 832    missingMarketCap: float
 833        - **Missing Market Cap**:
 834            The amount in millions (MM) to replace any missing market capitalization information
 835            with. This will impact the maximum ownership setting.
 836        - Constraints:
 837            Floating point value between 0 and 100
 838        - Default Value: 10
 839
 840    nameBasedSectorWeighting: bool
 841        - **Name Based Sector Weighting**:
 842            Base sector weightings on number of names. For example, if there are 200 names in the
 843            index and financials has 20 companies, the weight of financials should be 10% (20/200).
 844        - Constraints:
 845            Boolean value
 846        - Default Value: false
 847
 848    netBenchmark: bool
 849        - **Adjust Benchmark for Net Exposure**:
 850            If your portfolio has a net exposure greater or less than 100%, this will adjust the
 851            benchmark returns to match that net exposure.
 852        - Constraints:
 853            Boolean value
 854        - Default Value: false
 855
 856    optimizer: str
 857        - **Optimizer Type**:
 858            No Optimization: None
 859            Reduce Risk: Optimizer designed to reduce portfolio volatility. This tends to result in
 860            the best overall performance.
 861            Maximize Sharpe: Optimizer designed to maximize Sharpe using out of sample estimates for
 862            expected return. Estimates for expected return have an outsized impact on the
 863            performance of this optimizer.
 864            Maximize Alpha: Optimizer designed to maximize expected return based on out-of-sample
 865            estimates for expected return. Estimates for expected return have an outsized impact on
 866            the performance of this optimizer.
 867            Min VaR: Minimize Value at Risk by looking back at the proposed portfolio over the last
 868            year and trying to minimize drawdowns.
 869            Max VaR Sharpe: Maximize Value at Risk Sharpe by looking back at the proposed portfolio
 870            over the last year and trying to maximize Sharpe by observed return for the portfolio vs
 871            observed volatility.
 872            Minimize Skew: Minimize the skew of the portfolio by trying to find a set of portfolio
 873            weightings that results in returns that are closer to the mean.
 874        - Constraints:
 875            Required. String enum: one of
 876            default: No Optimization
 877            min_vol: Reduce Risk
 878            max_sharpe: Maximize Sharpe
 879            max_alpha: Maximize Alpha
 880            min_var: Min VaR
 881            max_var_sharpe: Max VaR Sharpe
 882            min_skew: Minimize Skew
 883        - Default Value: "default"
 884
 885    optimizeTurnover: bool
 886        - **Turnover Optimization**:
 887            Turnover optimization will reduce the turnover at the signal level, making that ranked
 888            list more stable.
 889        - Constraints:
 890            Boolean value
 891        - Default Value: false
 892
 893    pairsTrading: bool
 894        - **Pairs Trading**:
 895            Pairs Trading forces the portfolio to be created by going long / short an equal number
 896            of stocks. The portfolio will try to find an offsetting position for every long by
 897            finding securities that are both highly correlated and have a significant difference in
 898            star rating.
 899        - Constraints:
 900            Boolean value
 901        - Default Value: false
 902
 903    percentPortfolioLong: float
 904        - **Percent of Portfolio Long**:
 905            The exact sum of all long position weightings in the portfolio.
 906        - Constraints:
 907            Required. Floating point value representing a percentage between 0.0 and 300.0
 908        - Default Value: 100.0
 909
 910    percentPortfolioShort: float
 911        - **Percent of Portfolio Short**:
 912            The exact sum of all short position weightings in the portfolio.
 913        - Constraints:
 914            Required. Floating point value representing a percentage between 0.0 and 300.0
 915        - Default Value: 0.0
 916
 917    percentToLong: float
 918        - **Percent to Long**:
 919            The machine will attempt to buy the top X% of stocks, subject to the minimum and maximum
 920            specified. This is on a per “portfolio” basis, so if you have sector neutral turned on
 921            it will be top X% per sector. This will be subject to the maximum and minimum number of
 922            securities you specify below.
 923        - Constraints:
 924            Floating point value representing a percentage between 0 and 100
 925        - Default Value: 20.0
 926
 927    percentToShort: float
 928        - **Percent to Short**:
 929            The machine will attempt to short the bottom X% of stocks, subject to the minimum and
 930            maximum specified. This is on a per “portfolio” basis, so if you have sector neutral
 931            turned on it will be bottom X% per sector. This will be subject to the maximum and
 932            minimum number of securities you specify below.
 933        - Constraints:
 934            Floating point value representing a percentage between 0 and 100
 935        - Default Value: 20.0
 936
 937    portfolioStartingValue: float
 938        - **Portfolio Starting Value**:
 939            The value of the portfolio in MM when it begins trading. If its backtest spans from 2005
 940            - 2020, the starting value in 2005 will be whatever you set here.
 941        - Constraints:
 942            Floating point value between 0 and 100000.
 943        - Default Value: 100.0
 944
 945    priceCleaning: bool
 946        - **Price Cleaning**:
 947            Price Cleaning removes any very large price movements from the securities, specifically
 948            with a daily change greater than +500% or -83.33%. There are some legitimate securities
 949            with moves this large that will be impacted by turning this on, but the advantage is it
 950            prevents these securities from overwhelming the backtest.
 951        - Constraints:
 952            Boolean value
 953        - Default Value: true
 954
 955    priceType: str
 956        - **Price Type**:
 957            None
 958        - Constraints:
 959            Required. String enum of {CLOSE, OPEN, VWAP}
 960        - Default Value: "VWAP"
 961
 962    rebalancePeriod: str
 963        - **Rebalance Period**:
 964            When the machine rebalances the portfolio. You can choose a specific day if you prefer
 965            to execute trades on a specific day.
 966        - Constraints:
 967            Required.
 968            An enum value in the following list:
 969            "daily"
 970            "weekly": Rebalances on every Monday.
 971            Chooses the next available trade date to rebalance when Monday is a holiday
 972            "weekly_wednesday": Rebalances on every Wednesday.
 973            Chooses the next available trade date to rebalance when Wednesday is a holiday
 974            "weekly_friday": Rebalances on every Friday.
 975            Chooses the previous available trade date to rebalance when Friday is a holiday
 976            "monthly"
 977            "quarterly"
 978            OR a string in the following format: "custom_x" where x is an integer between 1 and 252
 979            (days).
 980        - Default Value: "weekly"
 981
 982    sectorNeutral: bool
 983        - **Sector Neutral**:
 984            Will be constructed as a series of portfolios - each matching the market cap weighting
 985            of each of the stock universe sectors. This means that min. and max. number of stocks
 986            "per portfolio" becomes "per sector".
 987        - Constraints:
 988            Boolean value
 989        - Default Value: false
 990
 991    sectorNeutralEqualWeightBenchmark: bool
 992        - **Sector Neutral Equal Weight Benchmark**:
 993            Instead of using the defined benchmarks, set the benchmark to be a "sector neutral"
 994            equal weighted (per rebalance period) version of the stock universe. This takes the
 995            sector weighting and divides by the number of stocks in that sector, so each sector will
 996            have a different individual security weighting.
 997        - Constraints:
 998            Boolean value
 999        - Default Value: false
1000
1001    sectorNeutralSpread: float
1002        - **Sector Neutral Spread**:
1003            The sector neutral spread is how far away from the benchmark sector allocation the
1004            machine can deviate. This is adjusted for net exposure, i.e. if your net exposure is 0%
1005            the target sector allocations will be 0%.
1006        - Constraints:
1007            Floating point value representing a percentage between 0 and 100
1008        - Default Value: 0
1009
1010    signalIndustryNeutral: bool
1011        - **Industry Neutral**:
1012            Make the signal industry neutral, plus or minus the signal sector neutral spread.
1013        - Constraints:
1014            Boolean value
1015        - Default Value: false
1016
1017    signalSectorNeutral: bool
1018        - **Sector Neutral**:
1019            Make the signal sector neutral, plus or minus the signal sector neutral spread.
1020        - Constraints:
1021            Boolean value
1022        - Default Value: false
1023
1024    signalSectorNeutralSpread: float
1025        - **Sector Neutral Spread**:
1026            Make the signal sector neutral, plus or minus the signal sector neutral spread.
1027        - Constraints:
1028            Floating point value representing a percentage between 0 and 100
1029        - Default Value: 1.0
1030
1031    smoothPeriods: int
1032        - **Smooth Periods**:
1033            The number of periods over which to smooth the signals
1034        - Constraints:
1035            Integer value between 1 and 100.
1036        - Default Value: 1
1037
1038    smoothWeighting: str
1039        - **Smooth Weighting Type**:
1040            Alpha Weight: The strength of the signal matters in the smoothing. For example, if a
1041            stock had a score of 5 stars in period A, 2 stars in period B, your 2 period smoothing
1042            would be 7 / 2 = 3.5.
1043            Equal Weight: Only the direction of the signal matters here. For example, if you were 5
1044            stars in period A and 2 stars in period B, your 2 period smoothing would be 0 / 2 = 0
1045            (+1 in period 1 and -1 in period 2)
1046        - Constraints:
1047            One of {alpha_weight, equal_weight}
1048        - Default Value: "alpha_weight"
1049
1050    stopGain: bool
1051        - **Enable Stop Gain**:
1052            Turn on stop gains for the portfolio. This forces sales of securities that have exceeded
1053            the stop gain level. All trades will occur at the next trading time (i.e. next day OPEN
1054            for OPEN price models).
1055        - Constraints:
1056            Boolean value
1057        - Default Value: false
1058
1059    stopGainAmount: float
1060        - **Stop Gain Percentage to Sell**:
1061            The percentage of the position the machine will sell if the stop gain is triggered.
1062        - Constraints:
1063            Floating point value representing a percentage between 1 and 100.
1064        - Default Value: 50.0
1065
1066    stopGainLevel: float
1067        - **Stop Gain Threshold**:
1068            Will sell positions after they hit a threshold. For example, 10% / stop gain, when a
1069            position gains 10% the machine will sell a portion of the position (defined by stop gain
1070            percentage).
1071        - Constraints:
1072            Floating point value representing a percentage between 1 and 100.
1073        - Default Value: 10.0
1074
1075    stopLoss: bool
1076        - **Enable Stop Loss**:
1077            Turn on stop losses for the portfolio. This forces sales of securities that have
1078            exceeded the stop loss level. All trades will occur at the next trading time (i.e. next
1079            day OPEN for OPEN price models).
1080        - Constraints:
1081            Boolean value
1082        - Default Value: false
1083
1084    stopLossAmount: float
1085        - **Stop Loss Percentage to Sell**:
1086            The percentage of the position the machine will sell if the stop loss is triggered.
1087        - Constraints:
1088            Floating point value representing a percentage between 1 and 100.
1089        - Default Value: 50.0
1090
1091    stopLossLevel: float
1092        - **Stop Loss Threshold**:
1093            Will sell positions after they hit a threshold. For example, 10% / stop loss, when a
1094            position loses 10% the machine will sell a portion of the position (defined by stop loss
1095            percentage).
1096        - Constraints:
1097            Floating point value representing a percentage between 1 and 100.
1098        - Default Value: 10.0
1099
1100    stopLossReset: bool
1101        - **Allow multiple stop loss/gain between rebalance days**:
1102            Allows the machine to repeatedly trigger stop losses within a rebalance period. For
1103            example, if you trade Mondays and the stock drops 15% per day during the week and the
1104            stop loss trigger is 10%, then every day will trigger the stop loss. If toggled off the
1105            stop loss can only be triggered once per stock per rebalance period.
1106        - Constraints:
1107            Boolean value
1108        - Default Value: false
1109
1110    trackingConstraint: float
1111        - **Maximum Tracking Error (vs Benchmark)**:
1112            Amount of ex-ante tracking error to constrain the optimizer by.
1113        - Constraints:
1114            Floating point value representing a percentage between 0 and 30
1115        - Default Value: 10
1116
1117    trackingError: bool
1118        - **Allocate Weight to Benchmark**:
1119            Allocate a portion (%) of portfolio to the benchmark
1120        - Constraints:
1121            Boolean value
1122        - Default Value: false
1123
1124    trackingErrorOptimizer: bool
1125        - **Tracking Error**:
1126            If toggled on the optimizer will try to solve for an expected tracking error less than
1127            the amount specified. This is done out-of-sample and the ex post tracking error will
1128            likely be higher.
1129        - Constraints:
1130            Boolean value
1131        - Default Value: false
1132
1133    tradingCost: float
1134        - **Trading Cost**:
1135            A cost that will be applied to every trade.
1136        - Constraints:
1137            Required. Floating point value representing a percentage between 0.0 and 5.0
1138        - Default Value: 0.01
1139
1140    turnoverImportance: float
1141        - **Turnover Importance**:
1142            High importance means the algorithm will aim to keep turnover low, whereas low
1143            importance of turnover will let it vary more.
1144        - Constraints:
1145            Floating point value between 0.2 and 5 nonlinearly mapped onto the 1-9 interval.
1146        - Default Value: 1
1147
1148    useHoldingPeriod: bool
1149        - **Use Holding Period**:
1150            Set any covariance or other calculations within the optimizer to use a holding period
1151            return instead of daily return series
1152        - Constraints:
1153            Boolean value
1154        - Default Value: false
1155
1156    weightingType: str
1157        - **Initial Weighting**:
1158            Alpha Weight: The initial weighting will be done such that higher ranked stocks have a
1159            higher weight.
1160            Equal Weight: The initial weighting will be done such that all stocks selected to be in
1161            the portfolio will have an equal weight.
1162            Market Cap Weight: The initial weighting will be done such that all stocks are weighted
1163            according to their previous day’s market capitalization.
1164        - Constraints:
1165            One of {alpha_weight, equal_weight, market_cap_weight}
1166        - Default Value: "alpha_weight"
1167    """
1168
1169    # note, this is just the collection of portfolio settings
1170    # that is exposed to the API:
1171    # SELECT key, default_value
1172    # FROM portfolio_settings_validation
1173    # WHERE exposed_to_client_api;
1174    # TODO: expand to entire settings? why this way initially?
1175    __default_settings = {
1176        "maxOwnership": "5",
1177        "pairsTrading": "false",
1178        "minNumStockLong": "5",
1179        "stopLossAmount": "50.0",
1180        "stopGainLevel": "10.0",
1181        "smoothWeighting": '"alpha_weight"',
1182        "minimumSharePrice": "2",
1183        "maxShortPosition": "5.0",
1184        "tradingCost": "0.01",
1185        "stopGainAmount": "50.0",
1186        "smoothPeriods": "1",
1187        "executionDelay": "0",
1188        "benchmarkAllocation": "0",
1189        "sectorNeutralSpread": "0",
1190        "percentToLong": "20.0",
1191        "maxNumStockLong": "50",
1192        "trackingConstraint": "10",
1193        "compounding": "true",
1194        "priceCleaning": "true",
1195        "lower_turnover": "false",
1196        "missingMarketCap": "10",
1197        "allowCompanyCollisions": "true",
1198        "allowDR": "true",
1199        "sectorNeutral": "false",
1200        "marketCapFlex": "100",
1201        "marketCapBounds": "1",
1202        "turnoverImportance": "1",
1203        "minNumStockShort": "5",
1204        "maxNumStockShort": "50",
1205        "beta_neutral": "false",
1206        "nameBasedSectorWeighting": "false",
1207        "trackingError": "false",
1208        "stopLoss": "false",
1209        "stopGain": "false",
1210        "stopLossReset": "false",
1211        "netBenchmark": "false",
1212        "equalWeightBenchmark": "false",
1213        "marketCapConstraint": "false",
1214        "signalIndustryNeutral": "false",
1215        "signalSectorNeutral": "false",
1216        "sectorNeutralEqualWeightBenchmark": "false",
1217        "percentToShort": "20.0",
1218        "trackingErrorOptimizer": "false",
1219        "currency": '"USD"',
1220        "rebalancePeriod": '"weekly"',
1221        "benchmark": "[]",
1222        "signalSectorNeutralSpread": "1.0",
1223        "optimizeTurnover": "false",
1224        "minShortPosition": "0",
1225        "minLongPosition": "0",
1226        "percentPortfolioLong": "100.0",
1227        "portfolioStartingValue": "100.0",
1228        "stopLossLevel": "10.0",
1229        "maxLongPosition": "10.0",
1230        "percentPortfolioShort": "0.0",
1231        "bounds_method": None,
1232        "optimizer": '"default"',
1233        "activeRisk": "false",
1234        "useHoldingPeriod": "false",
1235        "weightingType": '"alpha_weight"',
1236        "priceType": '"VWAP"',
1237        "factorNeutralizeSignal": "false",
1238        "investmentHorizon": "21",
1239    }
1240
1241    def __init__(self, settings: Optional[Dict] = None):
1242        # TODO: i'd rather have kwargs for the different keys and then allow
1243        # for per-key overrides, but we start with this more conservative approach
1244        if settings is None:
1245            self.settings = copy.deepcopy(PortfolioSettings.__default_settings)
1246        else:
1247            self.settings = settings
1248
1249    def toDict(self):
1250        return self.settings
1251
1252    def __str__(self):
1253        return json.dumps(self.settings)
1254
1255    def __repr__(self):
1256        return self.__str__()
1257
1258    def fromDict(settings):
1259        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)
1241    def __init__(self, settings: Optional[Dict] = None):
1242        # TODO: i'd rather have kwargs for the different keys and then allow
1243        # for per-key overrides, but we start with this more conservative approach
1244        if settings is None:
1245            self.settings = copy.deepcopy(PortfolioSettings.__default_settings)
1246        else:
1247            self.settings = settings
def toDict(self):
1249    def toDict(self):
1250        return self.settings
def fromDict(settings):
1258    def fromDict(settings):
1259        return PortfolioSettings(settings)
hedge_experiment_type = typing.Literal['HEDGE', 'MIMIC']
class HedgeExperiment:
1266class HedgeExperiment:
1267    id: str  # really a uuid, which we represent as a string
1268    name: str
1269    user_id: str  # see id comment
1270    description: str
1271    experiment_type: hedge_experiment_type  # TODO: enum worth it?
1272    last_calculated: dt.datetime
1273    last_modified: dt.datetime
1274    status: str  # TODO: enum worth it?
1275    portfolio_calculation_status: str  # TODO: enum worth it?
1276    selected_models: List[str]  # see id comment
1277    target_securities: Dict[GbiIdSecurity, float]  # Security is a hashable type because frozen=True
1278    target_portfolios: List[str]
1279    selected_stock_universe_ids: List[str]  # see id comment
1280    baseline_model: Optional[str] = None
1281    baseline_stock_universe_id: Optional[str] = None
1282    baseline_scenario: Optional["HedgeExperimentScenario"] = None
1283
1284    @property
1285    def config(self) -> Dict:
1286        """
1287        Returns a hedge experiment configuration dictionary, ready for JSON-serialization and
1288        submission.
1289        """
1290        weights_by_gbiid = [
1291            {"gbiId": sec.id, "weight": weight} for sec, weight in self.target_securities.items()
1292        ]
1293        return {
1294            "experimentType": self.experiment_type,
1295            "selectedModels": self.selected_models,
1296            "targetSecurities": weights_by_gbiid,
1297            "selectedStockUniverseIds": self._selected_stock_universe_ids,
1298        }
1299
1300    @classmethod
1301    def from_json_dict(cls, d: Dict) -> "HedgeExperiment":
1302        # drafts arent fully populated
1303        if d["status"] == "DRAFT":
1304            weights_by_security = {}
1305            selected_models = []
1306            selected_stock_universe_ids = []
1307        else:
1308            weights_by_security = {
1309                GbiIdSecurity(
1310                    sec_dict["gbiId"],
1311                    None,
1312                    sec_dict["security"]["symbol"],
1313                    sec_dict["security"]["name"],
1314                ): sec_dict["weight"]
1315                for sec_dict in d["targetSecurities"]
1316            }
1317            experiment_config = json.loads(d["config"])
1318            selected_models = experiment_config["selectedModels"]
1319            selected_stock_universe_ids = experiment_config["selectedStockUniverseIds"]
1320        baseline_scenario = None
1321        if d["baselineScenario"] is not None:
1322            baseline_scenario = HedgeExperimentScenario.from_json_dict(d["baselineScenario"])
1323        return cls(
1324            d["hedgeExperimentId"],
1325            d["experimentName"],
1326            d["userId"],
1327            d["description"],
1328            d["experimentType"],
1329            d["lastCalculated"],
1330            d["lastModified"],
1331            d["status"],
1332            d["portfolioCalcStatus"],
1333            selected_models,
1334            weights_by_security,
1335            d.get("targetPortfolios"),
1336            selected_stock_universe_ids or [],
1337            d.get("baselineModel"),
1338            d.get("baselineStockUniverseId"),
1339            baseline_scenario,
1340        )
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)
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
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:
1300    @classmethod
1301    def from_json_dict(cls, d: Dict) -> "HedgeExperiment":
1302        # drafts arent fully populated
1303        if d["status"] == "DRAFT":
1304            weights_by_security = {}
1305            selected_models = []
1306            selected_stock_universe_ids = []
1307        else:
1308            weights_by_security = {
1309                GbiIdSecurity(
1310                    sec_dict["gbiId"],
1311                    None,
1312                    sec_dict["security"]["symbol"],
1313                    sec_dict["security"]["name"],
1314                ): sec_dict["weight"]
1315                for sec_dict in d["targetSecurities"]
1316            }
1317            experiment_config = json.loads(d["config"])
1318            selected_models = experiment_config["selectedModels"]
1319            selected_stock_universe_ids = experiment_config["selectedStockUniverseIds"]
1320        baseline_scenario = None
1321        if d["baselineScenario"] is not None:
1322            baseline_scenario = HedgeExperimentScenario.from_json_dict(d["baselineScenario"])
1323        return cls(
1324            d["hedgeExperimentId"],
1325            d["experimentName"],
1326            d["userId"],
1327            d["description"],
1328            d["experimentType"],
1329            d["lastCalculated"],
1330            d["lastModified"],
1331            d["status"],
1332            d["portfolioCalcStatus"],
1333            selected_models,
1334            weights_by_security,
1335            d.get("targetPortfolios"),
1336            selected_stock_universe_ids or [],
1337            d.get("baselineModel"),
1338            d.get("baselineStockUniverseId"),
1339            baseline_scenario,
1340        )
class HedgeExperimentScenario:
1344class HedgeExperimentScenario:
1345    id: str  # really a uuid, which we represent as a string;
1346    name: str
1347    description: str
1348    status: str  # TODO: enum worth it?
1349    settings: PortfolioSettings
1350    summary: pd.DataFrame
1351    # we currently a portfolios field. they get wrapped up into the summary table
1352
1353    @classmethod
1354    def from_json_dict(cls, d: Dict) -> "HedgeExperimentScenario":
1355        return cls(
1356            d["hedgeExperimentScenarioId"],
1357            d["scenarioName"],
1358            d["description"],
1359            d.get("status"),
1360            PortfolioSettings(json.loads(d["portfolioSettingsJson"])),
1361            HedgeExperimentScenario._build_portfolio_summary(d.get("hedgeExperimentPortfolios")),
1362        )
1363
1364    @staticmethod
1365    def _build_portfolio_summary(portfolios: Dict) -> pd.DataFrame:
1366        # this happens for added scenarios that havent run yet
1367        if portfolios is None:
1368            return pd.DataFrame()
1369
1370        df_dict = defaultdict(list)
1371        for pf in portfolios:
1372            p = pf["portfolio"]
1373
1374            df_dict["portfolio_id"].append(p["id"])
1375            df_dict["scenario_name"].append(p["name"])
1376            df_dict["model_id"].append(p["modelId"])
1377            df_dict["status"].append(p["status"])
1378
1379            if p["status"] != "READY":  # data isn't yet available
1380                df_dict["1M"].append(np.nan)
1381                df_dict["3M"].append(np.nan)
1382                df_dict["1Y"].append(np.nan)
1383                df_dict["sharpe"].append(np.nan)
1384                df_dict["vol"].append(np.nan)
1385            else:
1386                # NOTE:
1387                # these are the magic indices/keys of these values; inspect passed arg to see
1388                # if upstream changes the order of (sub-)elements in the response...uh oh
1389                # TODO: specific key search? wrap with try/except and fail loudly?
1390                df_dict["1M"].append(p["performanceGrid"][0][4])
1391                df_dict["3M"].append(p["performanceGrid"][0][5])
1392                df_dict["1Y"].append(p["performanceGrid"][0][7])
1393                df_dict["sharpe"].append(p["tearSheet"][0]["members"][1]["value"])
1394                pf_stddev = p["tearSheet"][1]["members"][1]["value"]
1395                bnchmk_stddev = p["tearSheet"][1]["members"][2]["value"]
1396                df_dict["vol"].append((pf_stddev - bnchmk_stddev) * 100)
1397                # TODO: this is how it is done on the front-end,
1398                # should be pulled out of both here and there
1399
1400        df = pd.DataFrame(df_dict)
1401        return df
HedgeExperimentScenario( id: str, name: str, description: str, status: str, settings: boosted.api.api_type.PortfolioSettings, summary: pandas.core.frame.DataFrame)
id: str
name: str
description: str
status: str
summary: pandas.core.frame.DataFrame
@classmethod
def from_json_dict(cls, d: Dict) -> boosted.api.api_type.HedgeExperimentScenario:
1353    @classmethod
1354    def from_json_dict(cls, d: Dict) -> "HedgeExperimentScenario":
1355        return cls(
1356            d["hedgeExperimentScenarioId"],
1357            d["scenarioName"],
1358            d["description"],
1359            d.get("status"),
1360            PortfolioSettings(json.loads(d["portfolioSettingsJson"])),
1361            HedgeExperimentScenario._build_portfolio_summary(d.get("hedgeExperimentPortfolios")),
1362        )
class HedgeExperimentDetails:
1405class HedgeExperimentDetails:
1406    # this will probably lead to violations of the law of demeter, but it's easy to do right now,
1407    # particularly for an unproven API
1408    experiment: HedgeExperiment
1409    scenarios: Dict[str, HedgeExperimentScenario]
1410
1411    @property
1412    def summary(self) -> pd.DataFrame:
1413        """
1414        Returns a dataframe corresponding to the Hedge Experiment "flat view" - all portfolio
1415        options are displayed
1416        """
1417        if len(self.scenarios) == 0 or all(s.status != "READY" for s in self.scenarios.values()):
1418            return pd.DataFrame()
1419        return pd.concat(scenario.summary for scenario in self.scenarios.values()).sort_values(
1420            ["scenario_name"]
1421        )  # sort for stable presentation
1422
1423    @classmethod
1424    def from_json_dict(cls, d: Dict) -> "HedgeExperimentDetails":
1425        he = HedgeExperiment.from_json_dict(d)
1426        scenarios = {
1427            scenario_dict["hedgeExperimentScenarioId"]: HedgeExperimentScenario.from_json_dict(
1428                scenario_dict
1429            )
1430            for scenario_dict in d["hedgeExperimentScenarios"]
1431        }
1432        return cls(he, scenarios)
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:
1423    @classmethod
1424    def from_json_dict(cls, d: Dict) -> "HedgeExperimentDetails":
1425        he = HedgeExperiment.from_json_dict(d)
1426        scenarios = {
1427            scenario_dict["hedgeExperimentScenarioId"]: HedgeExperimentScenario.from_json_dict(
1428                scenario_dict
1429            )
1430            for scenario_dict in d["hedgeExperimentScenarios"]
1431        }
1432        return cls(he, scenarios)