- SECTIONS
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
- Use the PUT endpoint to upload and/or update your holdings to FactSet to be referenced via a .acct file.
- 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.
- 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.
- For “iterative” fields:
- The holdings date (in yyyymmdd format) will be used as the user-defined key, followed by the symbol name
Factset_Porfolio_Api_Iterative_Example - The user-defined symbol object can contain the following fields:
- “price” and “priceiso” (optional, but “priceiso” is required if “price” is set)
- One of “weight” OR “shares” (required)
- The holdings date (in yyyymmdd format) will be used as the user-defined key, followed by the symbol name
- For “nonIterative” fields:
- The symbol name will be used as the only user-defined key:
Factset_Porfolio_Api_NonIterative_Example - The user-defined symbol object can contain the following fields:
- “price” and “priceiso” (optional, but “priceiso” is required if “price” is set)
- One of “weight” OR “shares” (required)
- The symbol name will be used as the only user-defined key:
- 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
- Elevate Client Conversations with Model Portfolio Analytics from Your Digital Portal https://developer.factset.com/recipe-catalog/model-portfolio-enriched-content-digital-portals
- Model Portfolio Derived Analytics for Business Development Tools https://developer.factset.com/recipe-catalog/model-portfolio-derived-analytics-business-development-tools
API Definition
API Documentation
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]