Portfolio API

  • SECTIONS
  • Overview
  • API Definition
  • API Documentation
  • SDK Libraries
  • Code Snippet
  • Changelog
Overview

The Portfolio API allows you to upload and edit holdings data to FactSet, this enables you to access the power of our world-class Portfolio Analytics suite and instantly start evaluating your portfolio’s performance via the Portfolio Analytics API.

Use Cases

The API allows you to perform CRUD operations (Create, Read, Update, and Delete) on your portfolios, referenced by FactSet via an account file (.ACCT).

Create:

  • Use the PUT endpoint to create an account and indicate its schema. The account’s name is a required path parameter that must include the name and location of where the account will be stored within FactSet. It must:
    • Include a valid FactSet directory. One of: ‘Client:/’, ‘Super_client:/’, or ‘Personal:/’
    • End with a .acct file extension
    • Only include folders and subfolders that are already existing. The Portfolio API will not create a folder structure that does not currently exist
    • Sample names:
      • “Client:/accountName.acct”
  • “Client:/folder/accountName.acct” The request body can contain iterative fields (those that vary by date) and/or non-iterative fields (static over time). It must contain one or more date and symbol combination and a weight for each symbol. If weight is not applicable, you can send in # of shares.
  • Optionally include prices and a currency ISO for each symbol, along with any custom fields.
  • A successful creation returns a 201 status code.
  • All accounts will leverage FactSet’s Pricing and Analytics Sources by default.

Read:

  • Use any one of the GET endpoints to fetch account data:
    • GET by a date and symbol combination: GET /{name}/dates/{date}/symbols/{symbol}
    • GET by a date: GET /{name}/dates/{date}
    • GET by symbol GET /{name}/symbols/{symbol}/
  • By default, the data is returned in FactSet’s own STACH v2 format (JsonStach format), but you can choose to specify a non-Stach JSON response format (AccountModel format)

Update:

  • Use the PUT /{name} endpoint to update an existing account with:
    • New date/symbol combinations
    • Updated values for existing date/symbol combinations
  • A successful update returns a 200 status code.
  • Note that you may not add a metadata description or create custom fields when updating an existing account. These can be specified only when creating an account.
  • When updating an account with a new date-symbol combination, you will be appending data onto the account. However, when updating an account with an existing date-symbol combination, you will be overwriting the data.
  • The API can modify any account that the API user has access to, regardless of whether it was created via the API. Please proceed with caution.

Delete:

  • Use the DELETE /{name} endpoint to delete an account
  • Use the DELETE /{name}/symbols/{symbol} endpoint to delete all entries for a symbol or specific date entries for a symbol from a previously created account
  • Use the DELETE /{name}/dates/{date} endpoint to delete all entries for a date or specific symbol entries for a date from a previously created account
  • A successful deletion returns a 204 status code.
  • Please proceed with caution when deleting accounts

Limitations & Related APIs

  • You cannot define or update account settings via the Portfolio API. If you are interested in getting more fine-grained, programmatic control over your portfolios’ settings, this can be done using the Portfolio Metadata API (currently in Beta)
  • Note that there may be certain account locations that are restricted to a specific set of users.
  • Use the OFDB API for uploading data for non-portfolio centric use cases like sector classification and/or mappings

Getting Started with Uploading your Holdings

  1. Use the PUT endpoint to upload and/or update your holdings to FactSet to be referenced via a .acct file.
    1. The Portfolio API will create a FactSet account (.acct) entity based on the name specified in the “name” path parameter, and the holdings uploaded in the PUT request body.
    2. Your portfolio can include “iterative” fields – capture a single point in time and vary by date, or “noniterative” fields – capture data over time (date is not required). For example, the price and weight for a certain symbol will vary over time, whereas the symbol’s tax status would stay constant.
    3. For “iterative” fields:
      1. The holdings date (in yyyymmdd format) will be used as the user-defined key, followed by the symbol name
        Factset_Porfolio_Api_Iterative_Example
      2. The user-defined symbol object can contain the following fields:
        1. “price” and “priceiso” (optional, but “priceiso” is required if “price” is set)
        2. One of “weight” OR “shares” (required)
    4. For “nonIterative” fields:
      1. The symbol name will be used as the only user-defined key:
        Factset_Porfolio_Api_NonIterative_Example
      2. The user-defined symbol object can contain the following fields:
        1. “price” and “priceiso” (optional, but “priceiso” is required if “price” is set)
        2. One of “weight” OR “shares” (required)
  2. Once created, this account can be used within FactSet to analyze your portfolio. Click here to get started using the Portfolio Analytics API.

Additional Information

Check out the recipes we’ve created to help illustrate the art of possible by leveraging the Portfolio API

API Definition
SDK Libraries
Portfolio API SDK Section
nuget install FactSet.SDK.Portfolio
Code Snippet
Sample API Call - Uploading Portfolio Data from Excel
import enum
import json
import os

from fds.sdk.Portfolio import Configuration, ApiClient
from fds.sdk.Portfolio.api.model_accounts_api import ModelAccountsApi
from fds.sdk.Portfolio.model.model_account_fields import ModelAccountFields
from fds.sdk.Portfolio.model.model_account_fields_root import ModelAccountFieldsRoot
from fds.sdk.utils.authentication import ConfidentialClient
from pandas import read_excel, read_csv

# region Description :  Example

# CSV

# Symbol,Date,Price,Price iso,Shares  ----> Headers
# BTCA,20210707,100,USD,1000
# MCHB,20210706,100,USD,1000
# MCHB,20210707,100,USD,1000
# BTCA,20210706,100,USD,1000

# Excel : Don't Keep Empty Rows

# Symbol |	Date	   |Price	|Price iso	  |Shares  ----> Headers
# ---------------------------------------------------
# BTCA	 |   20210707  |100		|USD	      |  1000
# ---------------------------------------------------
# MCHB	 |  20210706   |100		|USD	      |  1000
# ---------------------------------------------------
# MCHB	 |  20210707   |100		|USD	      |  1000
# ---------------------------------------------------
# BTCA	 |  20210706   |100		|USD	      |  1000
# ---------------------------------------------------


# end region

host = os.getenv("FACTSET_HOST", "https://api.factset.com") + "/analytics/accounts/v3"
username = os.getenv("FACTSET_USERNAME")
password = os.getenv("FACTSET_PASSWORD")


def main():
    # Uncomment the below lines for Basic Authentication
    # region Basic authentication: FactSetApiKey
    # config = Configuration(host=host, username=username, password=password)
    # endregion

    # region (Preferred) OAuth 2.0: FactSetOAuth2
    config = Configuration(
        fds_oauth_client=ConfidentialClient('/path/to/app-config.json'),
        host=host
    )
    # endregion
    model_account_file_name = "Client:/aapi/testing01.acct"  # The path and filename of the model account to create

    # Note : Change it to the name of your excel file with sheet name or CSV file
    source_file_name = 'Test.xlsx'  # Change it to the name of your excel or CSV file
    source_excel_sheet_name = 'Sheet1'  # Enter the name of the worksheet in the input excel file. Not applicable if 
    # the input is CSV

    source_type = SourceType.Excel  # Assign Source type accordingly b/w. SourceType.Csv or SourceType.Excel

    sub_request_size = 500  # Size of sub_requests to be made.This defines the size of sub-request generated after 
    # splitting the bigger request in smaller subset of requests

    model_account_helper = ModelAccountHelper(config=config, model_account_file_name=model_account_file_name, sub_request_size=sub_request_size, source_type=source_type,
                                              source_file_name=source_file_name, source_excel_sheet_name=source_excel_sheet_name)

    model_account_helper.parse_source_file()
    for sub_request_no in range(model_account_helper.no_of_sub_requests):
        model_account_helper.prepare_sub_request(sub_request_no)
        delete = model_account_helper.portfolio_api.delete_a_model_account_with_http_info(name=model_account_file_name) # delete account if exists
        create_account_response = model_account_helper.portfolio_api.create_or_update_model_account_with_http_info(
            name=model_account_file_name, model_account_fields_root=model_account_helper.model_account_request_body)
        if create_account_response[1] == 201:
            print(f"Model Account created successfully for {sub_request_no + 1}")
            get_account_response = model_account_helper.portfolio_api.get_account_with_http_info(name=model_account_file_name)
            if get_account_response[1] == 200:
                print(f"Model Account retrieved successfully for {sub_request_no + 1}")
                print(get_account_response[0].data)
            else:
                print(f"Model Account retrieval failed for {sub_request_no + 1}")
        elif create_account_response[1] == 200:
            print(f"Model Account updated successfully for {sub_request_no + 1}")
            get_account_response = model_account_helper.portfolio_api.get_account_with_http_info(name=model_account_file_name)
            if get_account_response[1] == 200:
                print(f"Model Account retrieved successfully for {sub_request_no + 1}")
                print(get_account_response[0].data)
            else:
                print(f"Model Account retrieval failed for {sub_request_no + 1}")
        else:
            print(f"Model Account creation failed for {sub_request_no + 1}")


class SourceType(enum.Enum):
    Excel = 1
    Csv = 2


class ModelAccountHelper:
    config: Configuration
    model_account_file_name: str
    source_type: SourceType
    source_file_name: str
    source_excel_sheet_name: str
    sub_request_size: int
    request_source_object: object
    total_row_count: int
    no_of_sub_requests: int
    begin_idx: int
    end_idx: int
    sub_request_json_object: object
    model_account_request_body: ModelAccountFieldsRoot
    new_model_account: bool
    portfolio_api: ModelAccountsApi

    # Constructor
    def __init__(self, config, model_account_file_name, sub_request_size, source_type,
                 source_file_name, source_excel_sheet_name=None):
        self.config = config
        self.model_account_file_name = model_account_file_name
        self.source_type = source_type
        self.source_file_name = source_file_name
        self.source_excel_sheet_name = source_excel_sheet_name
        self.sub_request_size = sub_request_size
        self.portfolio_api = ModelAccountsApi(api_client=ApiClient(configuration=config))
        self.is_new_model_account()  # Check if the file doesn't exist before

    # Extracting data from source file
    def parse_source_file(self):
        if self.source_type == SourceType.Excel:
            if self.source_excel_sheet_name is not None:
                self.request_source_object = read_excel(self.source_file_name,
                                                        sheet_name=self.source_excel_sheet_name)
            else:
                self.request_source_object = read_excel(self.source_file_name)
        else:
            self.request_source_object = read_csv(self.source_file_name)

        self.total_row_count = self.request_source_object.shape[0]
        self.no_of_sub_requests = int(self.total_row_count / self.sub_request_size) + (
            1 if self.total_row_count % self.sub_request_size != 0 else 0)

    # Prepares Model accounts create post-body
    def prepare_sub_request(self, sub_request_no):
        self.begin_idx = sub_request_no * self.sub_request_size
        if (self.begin_idx + self.sub_request_size) < self.total_row_count:
            self.end_idx = (self.begin_idx + self.sub_request_size)
        else:
            self.end_idx = self.total_row_count
        current_sub_request = self.request_source_object.iloc[self.begin_idx:self.end_idx]
        self.sub_request_json_object = json.loads(
            current_sub_request.to_json())  # converts current sub request parameters to jObject
        iterative_model_account_field = {}
        for i in range(self.begin_idx, self.end_idx):
            # Extracting Excel Column values for each Row w.r.t column names
            row_no = str(i)
            date = self.sub_request_json_object["Date"][row_no]
            symbol = self.sub_request_json_object["Symbol"][row_no]
            price = self.sub_request_json_object["Price"][row_no]
            price_iso = self.sub_request_json_object["Price iso"][row_no]
            shares = self.sub_request_json_object["Shares"][row_no]

            # Preparing Model Accounts request body params
            model_account_params_dict = {
                "price": str(price),
                "priceiso": str(price_iso),
                "shares": str(shares)
            }

            if str(date) in iterative_model_account_field.keys():
                symbol_dict = iterative_model_account_field[str(date)]
            else:
                symbol_dict = {}

            symbol_dict[str(symbol)] = model_account_params_dict  # Adding parameters to dict with symbol as key
            iterative_model_account_field[str(date)] = symbol_dict  # Adding parameters to dict with date as key

        # Adding data and meta object
        if self.new_model_account:  # If it's a new account passing metadata along
            meta = {"description": "model accounts sample request script"}

            data = ModelAccountFields(iterative=iterative_model_account_field)
            data_object = {"data": data}
            model_account_request_body_object = ModelAccountFieldsRoot(data=data, *meta)
        else:  # Existing accounts don't support metadata
            data = ModelAccountFields(iterative=iterative_model_account_field)
            data_object = {"data": data}
            model_account_request_body_object = ModelAccountFieldsRoot(data=data)

        # Converting model account object body to json, and removing null fields from post-body
        self.model_account_request_body = model_account_request_body_object

    # Checking for new model account file name
    def is_new_model_account(self):
        get_account = self.portfolio_api.get_account_with_http_info(name=self.model_account_file_name)
        if get_account[1] == 200:
            self.new_model_account = False
        else:
            self.new_model_account = True


if __name__ == "__main__":
    main()
Sample API Call
import os

from fds.sdk.Portfolio import Configuration, ApiClient, ApiException
from fds.sdk.Portfolio.api.model_accounts_api import ModelAccountsApi
from fds.sdk.utils.authentication import ConfidentialClient

host = os.getenv("FACTSET_HOST", "https://api.factset.com") + "/analytics/accounts/v3"
username = os.getenv("FACTSET_USERNAME")
password = os.getenv("FACTSET_PASSWORD")


def main():
    # Uncomment the below lines for Basic Authentication
    # region Basic authentication: FactSetApiKey
    # config = Configuration(host=host, username=username, password=password)
    # endregion

    # region (Preferred) OAuth 2.0: FactSetOAuth2
    config = Configuration(
        fds_oauth_client=ConfidentialClient('/path/to/app-config.json'),
        host=host
    )
    # endregion
    portfolio_api = ModelAccountsApi(api_client=ApiClient(configuration=config))
    # delete account if exists
    delete = portfolio_api.delete_a_model_account_with_http_info(name="client:v3EsdkCreate1.acct")
    request = {
        "data": {
            "iterative": {
                "20191010": {
                    "FDS": {"weight": "20", "price": "50", "priceiso": "USD"}
                }
            },
            "nonIterative": {
                "FDS": {"taxable": "false"}
            },
            "additionalFields": [{
                "description": "Taxable",
                "splitDirection": "NONE",
                "name": "TAXABLE",
                "type": "STRING",
                "iteration": False
            }]
        },
        "meta": {
            "description": "Test description"
        }
    }

    try:
        create_account_response = portfolio_api.create_or_update_model_account_with_http_info(
            name="client:v3EsdkCreate1.acct", model_account_fields_root=request)
        create_status = create_account_response[1]
        print(f"Create model account Put request response code: {create_status}")
        get_account_response = portfolio_api.get_account_with_http_info(name="client:v3EsdkCreate1.acct",
                                                                        format="AccountModel")
        print(f"Get model account response in AccountModel format:")
        print(get_account_response[0])
    except ApiException as e:
        print(f"Status code: {e.status}")
        print(f"Reason: {e.reason}")
        print(e.body)


if __name__ == "__main__":
    main()
Changelog

v3

Summary

  • v3.3.0 - Released on 01/23/23
  • v3.2.0 - Released on 06/06/22
  • v3.1.0 - Released on 11/22/21
  • v3.0.6 - Released on 10/18/21
  • v3.0.5 - Released on 07/28/21
  • v3.0.4 - Released on 06/07/21
  • v3.0.3 - Released on 05/20/21
  • v3.0.2 - Released on 04/21/21
  • v3.0.1 - Released on 03/20/21
  • v3.0.0 - Released on 03/18/21

Functionality Additions

  • Added new Security Response headers.[v3.3.0]
  • Added two new endpoints to delete symbols and date from Model Accounts.[v3.2.0]
  • Support for creating 3-Dimensional custom fields.[v3.1.0]
  • Returning non-iterative fields in get endpoint. [v3.1.0]
  • Support for updating arbitrary fields [v3.0.0]
  • Support for updating existing accounts which are not created using model accounts API [v3.0.0]
  • Support for update non iterative fields [v3.0.0]
  • Returns response in STACHv2 format while STACHv1 response format discontinued.[v3.0.0]

Bug Fixes

  • Minor bug fixes. [v3.1.0]
  • Used the logic from UpdateHoldingsFieldsAsync() in ModelAccountsService, to get the holdings_file_name for the given account, instead of directly using the account name as the OFDB name. Also updated the get schema endpoint with the same change. [v3.0.6]
  • Verification of being able to handle more columns. [v3.0.6]

v2

Summary

  • v2.3.0 - Released on 12/15/20
  • v2.2.5 - Released on 10/29/20
  • v2.2.4 - Released on 10/06/20
  • v2.2.3 - Released on 08/27/20
  • v2.2.2 - Released on 08/13/20
  • v2.2.1 - Released on 07/23/20
  • v2.2.0 - Released on 06/25/20
  • v2.1.3 - Released on 04/19/20
  • v2.1.2 - Released on 02/27/20
  • v2.1.1 - Released on 01/16/20
  • v2.1.0 - Released on 09/26/19
  • v2.0.2 - Released on 07/11/19
  • v2.0.1 - Released on 01/29/19
  • v2.0.0 - Released on 11/29/18

Functionality Additions

  • Optional Account Description  [v2.2.0]
  • Support of Account Model format to return the model account in the same format used for inputs [v2.2.0]
  • Better logging [v2.1.3]
  • Performance improvements [v2.1.2]
  • Added support for price, currencyisocode and weight properties for a holding when using ‘Create Account’ endpoint [v2.1.0]
  • Added support for Accept request header to all HTTP GET endpoints [v2.0.0]

Changes

  • HTTP GET endpoint to fetch model accounts now returns data in FactSet's JSON format [v2.0.0]
  • Renamed "shares" property to "quantity" in HTTP POST and GET endpoints [v2.0.0]

Bug Fixes

  • Minor bug fixes [v2.3.0]
  • Fix Price logic for accounts [v2.2.5]
  • Model Portfolios uploaded with 'WEIGHTS' will no longer create an empty 'SHARES' field [v2.2.4]
  • Fix bug in rate limiting [v2.2.3]
  • Cache control header fix [v2.2.0]
  • Rate limiting header fix [v2.2.0]
  • Fixed crash when duplicate symbols were entered for the same date while using Create Account endpoint [v2.0.2]