Streamlining historical options data retrieval and storage with RD libraries and MongoDB

Authors:

Dr. Haykaz Aramyan
Developer Advocate Developer Advocate
Gurpreet Bal
Platform Application Developer Platform Application Developer

Overview

This article is a part of the project we have introduced in the article Building a proprietary web application with Refinitiv Workspace SDK and the Refinitiv Data Platform: An Options RIC Search use case. In this first article of the series, we will explain how to use Refinitiv Data (RD) Libraries for Typescript to ingest and process data before building the front and back-end of our application. We cover topics such as connecting to the API, ingesting data using symbology conversion, search, and historical pricing capabilities. We also provide functions for reconstructing and validating options RICs that trade on supported exchanges, using the capabilities of the Data Libraries. Finally, we present how to define a MongoDB schema and store our ingested data in it.

So, let’s get started...

Section 1: Obtaining Options data: Valid RIC(s) and Prices

Before we proceed with obtaining valid option RICs and pricing data,  please make sure  that you have installed the required dependency software following the overview article of this project and created an RD session following the QuickStart guide for the RD library.

We would like our application to work both with RICs and ISINs, therefore, as a first step, we define a Symbology conversion function. Since options on a specific asset can be trained on multiple exchanges (e.g., options on Barclays are traded both in EUREX and Intercontinental Exchange) we utilise the Search capabilities of the RD Library to identify all possible exchanges. Finally, we define separate functions for each exchange to construct Options RICs following RIC construction rules. Next, we request historical pricing using the respective function in RD Libraries. These functions are triggered by a wrapper function which converts ISIN to RIC, identifies exchanges and calls the respective function for each exchange.

The following diagram provides the overall workflow of getting the Option RIC for Barclays using ISIN (shapes in green are the functions which are called after user input). 

In the following subsections we present the above functions in a greater detail. First, we will build RD Libraries API request functions, such as SymbolConversion for ISIN to RIC conversion, Search for exchange identification and HistorcalPricing for price retrieval. Next, we will present an exchange specific function (IEU, to see all implemented functions, please refer to the project’s GitHub folder) for RIC construction and the wrapper getOptionRIC function which will trigger this and the rest of exchange specific respectively.

1.1: Building API requests

Symbol Conversion

Let’s start by defining the symbol conversion function which, in our use case, converts an ISIN into a RIC. For that, we will be using the SymbolConversion content object from the RD Library which allows us to convert various types of symbols like RICs, ISINs, CUSIPs, and so on, using the Search/Lookup API of the Refinitiv Data Platform. Below, we import the getSession object from the session file we have defined in the previous section, and SymbolConversion from the RD Libraries.

    	
            

import { getSession } from '../Common/session';

import { SymbolConversion } from '@refinitiv-data/data';

After we have the getSession object, we define a session to be passed as an input to the SymbolConversion function:

    	
            const session = getSession();
        
        
    

Finally, we define the ISINtoRIC function which takes the asset ISIN and the session as an input and returns the RIC as an output. The following code demonstrates how to retrieve symbol conversion data using instrument ISIN and fromSymbolType and toSymbolType parameters from SymbolConversion.Definition object.

    	
            

async function ISINtoRIC(asset: string, openedSession: any = null) {

    const doSymbolConversion = async function (params: SymbolConversion.Params) {

        const definition = SymbolConversion.Definition(params);

        const convRes = await definition.getData(session);

        if (convRes.data.raw.Matches.length > 0) {

            return convRes.data.raw.Matches[asset].RIC;

        }

        console.log('No symbol conversion result received');

        return ''

    };

    try {

        if (!openedSession) {

            await session.open();

            console.log('Session open, sending data request');

        };

        return await doSymbolConversion({

            symbols: [asset],

            fromSymbolType: SymbolConversion.SymbolType.ISIN,

            toSymbolType: SymbolConversion.SymbolType.RIC,

        });

    }

    catch (e) {

        console.log(e);

    }

    finally {

        if (!openedSession) { await session.close() }

    };

}

In this function, we first define a Symbol conversion object (doSymbolConversion), then open a session, if no active session is passed as an argument, and call the conversion object with the provided asset ISIN. Below, let’s run the function for Microsoft using its ISIN. Since ISINtoRIC is an asynchronous function, we use a then() promise to print the function output.

    	
            

ISINtoRIC('US5949181045').then((ric: any) => {

    console.log(ric)

})

Session open, sending data request 

MSFT.O

Search capabilities

Moving into the next component of our workflow, which is the identification of exchanges where options on the given asset are traded, we demonstrate how to use the Search capabilities of RD Libraries. For that, we will use the Search content object which gives access to the Refinitiv Search API that covers a wide range of content. 

Below, we first import the getSession object from the session file we have defined in the previous section and the Search object from the RD Libraries. 

Below, we first import getSession object from the session file and the Search object from the RD Libraries. After we have the getSession object,  we define a session to be passed as an input the getExchangeCode function:

    	
            

import { getSession } from '../Common/session';

import { Search } from '@refinitiv-data/data';

const session = getSession();

Finally, we define the getExchangeCode function which takes the asset RIC and the session as an input and returns the exchange codes as an output. The following code demonstrates how to use the Search object to retrieve the exchange codes where the options on a given asset are traded:

    	
            

async function getExchangeCode(asset: string, openedSession: any = null) {

    const displaySearchResponse = async function (params: Search.Params) {

        const definition = Search.Definition(params);

        const searchRes = await definition.getData(session);

        if (searchRes.data.raw)

            return searchRes.data.raw["Navigators"]["ExchangeCode"];

        else

            console.log('No search result received');

    };

    try {

        if (!openedSession) {

            await session.open();

            console.log('Session open, sending data request');

        };

        return await displaySearchResponse({

            query: asset,

            filter: "SearchAllCategory eq 'Options' and Periodicity eq 'Monthly' ",

            select: ' RIC, DocumentTitle, UnderlyingQuoteRIC,Periodicity, ExchangeCode',

            navigators: "ExchangeCode",

        });

    }

    catch (e) {

        console.log(e);

    }

    finally {

        if (!openedSession) { await session.close() }

    };

}

As before, we first define a Search object (displaySearchResponse), then open a session, if no active session is passed as an argument, and call the object with provided asset RIC and several Search parameters. The latter, includes a filter parameter which restricts the found results to Options with a monthly periodicity. The select parameter allows us to specify fields for the response. One of the most important components of this request is the navigator parameter, which allows for an aggregated response by a specified field which in our use case is the ExchangeCode.

Below, let’s run the function for Barclays using its RIC. Since getExchangeCode is an asynchronous function, we use a then() promise to print the function output.

    	
            

getExchangeCode('BARC.L').then((exchs: any) => {

    console.log(exchs)

})

Session open, sending data request 

{ Buckets: [ { Label: 'IEU', Count: 1190 }, { Label: 'EUX', Count: 326 } ] }

In the output above, we see the Labels as ExchangeCodes and Count which shows the number of discovered elements within the label. Since we are interested in ExchangeCodes only, we retrieve the dictionary keys for our subsequent requests.

Historical prices

Finally, let’s define the getPrices function to retrieve historical prices on constructed options and demonstrate how to use the pricing capabilities of RD Libraries. For that, we will use the HistorcalPricing content object.

Below, we first import the getSession object from the session file we have defined in the previous section and the HistoricalPricing object from the RD Libraries.

    	
            

import { HistoricalPricing } from '@refinitiv-data/data';

import { getSession } from '../Common/session';

const session = getSession();

In the following code snippet, we define the getPrices function which takes the asset RIC, session and request period (start and end dates) as an input and returns an object containing the prices for the requested period. The following code demonstrates how to use the HistoricalPricing object to retrieve the prices on a given asset for a given period:

    	
            

async function getPrices(asset: string, sDate: string, eDate: string, openedSession: any = null, fields: any= ['TRDPRC_1', 'BID', 'ASK']) {

    try {

        if (!openedSession) {

            await session.open();

            console.log('Session open, sending data request');

        };

        const request = HistoricalPricing.Summaries.Definition({

            universe: asset,

            interval: HistoricalPricing.Summaries.InterdayInterval.DAILY,

            fields: fields,

            start: sDate,

            end: eDate

        });

        const historicalPrices = await request.getData(openedSession || session);

        return historicalPrices.data.table;

    }

    catch (e) {

        console.log(e);

    }

    finally {

        if (!openedSession) { await session.close() }

    };

};

As can be seen from the code snipped above, we first open a session, if no open session is passed to the function, then form the API request with the input parameters. Additionally, we request Trade, BID and ASK prices for the asset.

Next, let’s run the function for an LSEG option. Here again, since the getPrices function is an asynchronous one, we use a then() promise to print the function output. 

    	
            

getPrices('LSEG7800C3.L^C23', '2023-03-01', '2023-03-10').then((prices: any) => {

    console.log(prices)

})

Session open, sending data request 

{ '0': { DATE: '2023-03-10', TRDPRC_1: null, BID: 0.25, ASK: 15 }, '1': { DATE: '2023-03-09', TRDPRC_1: null, BID: 0.25, ASK: 15 }, '2': { DATE: '2023-03-08', TRDPRC_1: null, BID: 2.25, ASK: 17.25 }, '3': { DATE: '2023-03-07', TRDPRC_1: null, BID: 2, ASK: 17 }, '4': { DATE: '2023-03-06', TRDPRC_1: null, BID: 10, ASK: 25 }, '5': { DATE: '2023-03-03', TRDPRC_1: null, BID: 37.5, ASK: 52.5 }, '6': { DATE: '2023-03-02', TRDPRC_1: null, BID: 19, ASK: 34 }, '7': { DATE: '2023-03-01', TRDPRC_1: null, BID: 57.5, ASK: 77.5 } } 

1.2: Constructing option RICs for various exchanges

According to the Refinitiv RIC Rules (see RIC RULES|FUTURES AND OPTIONS|OPTIONS RICS AND EXPIRY CODES|RIC CONSTRUCTION in Refinitiv Workspace) Options RICs are comprised of Asset name as a RIC Root, Strike price, Expiration month code and Exchange identifier. Additionally, after an option is expired, Delivery month code and Expiration year are added followed by “^” symbol. Below, we present an example for FTSE call and put option with 7900 strike price expired March 2023.

With that in mind, in this section, we build separate functions to construct individual RIC components and join the components to form the final RIC. The function below demonstrates the construction of the month code for put and call options:

    	
            

const moment = require('moment');

function getExpMont(expDate: string, optType: string) {

    let expMonth = '';

    const ident = {

        '1': { 'exp': 'A', 'C': 'A', 'P': 'M' },

        '2': { 'exp': 'B', 'C': 'B', 'P': 'N' },

        '3': { 'exp': 'C', 'C': 'C', 'P': 'O' },

        '4': { 'exp': 'D', 'C': 'D', 'P': 'P' },

        '5': { 'exp': 'E', 'C': 'E', 'P': 'Q' },

        '6': { 'exp': 'F', 'C': 'F', 'P': 'R' },

        '7': { 'exp': 'G', 'C': 'G', 'P': 'S' },

        '8': { 'exp': 'H', 'C': 'H', 'P': 'T' },

        '9': { 'exp': 'I', 'C': 'I', 'P': 'U' },

        '10': { 'exp': 'J', 'C': 'J', 'P': 'V' },

        '11': { 'exp': 'K', 'C': 'K', 'P': 'W' },

        '12': { 'exp': 'L', 'C': 'L', 'P': 'X' }

    }

    if (optType.toUpperCase() === 'C') {

        expMonth = ident[moment(expDate).format('M')].C

    }

    else if (optType.toUpperCase() === 'P') {

        expMonth = ident[moment(expDate).format('M')].P

    }

    return [ident, expMonth]

}

The function above,  takes expiration date and an option type as an input, and returns the month code along with month identifier dictionary as an output.

Next, we provide a function for the asset name component which accepts the asset RIC as an input and returns asset name as a RIC root. 

    	
            

function getAssetName(asset: string) {

    let assetName = '';

    if (asset.startsWith('.')) {

        assetName = asset.split('.', 2)[1];

        if (assetName === 'FTSE') {

            assetName = 'LFE'

        }

    }

    else {

        assetName = asset.split('.', 2)[0];

    };

    return assetName

}

In this function we check if the given asset is an equity or an index by examining if the asset name starts with the “.” symbol. If it does, we consider an asset as an index. Finally, we split the asset name on “.”and take the respective part as an asset name depending on the asset type.

Next, we construct the Strike price component of the RIC via the following function: 

    	
            

function getStrikeRIC(strike: number) {

    let intPart = null;

    let decPart = null;

    let strikeRIC = '';

    if (String(Math.floor(strike)).length === 2) {

        strikeRIC = `0${String(Math.floor(strike))}`

    }

    else {

        strikeRIC = `${String(Math.floor(strike))}`

    }

    if (strike % 1 !== 0 && String(Math.floor(strike)).length === 1) {

        intPart = Math.floor(strike);

        decPart = String(strike).split('.', 2)[1][0]

        strikeRIC = `0${String(intPart)}${decPart}`

    }

    return strikeRIC

}

This function accepts the actual strike price as an input and returns the Strike Price component of the RIC. We start by checking the length of the strike price and add a “0” if the integer part of the strike has two digits (strike prices at IEU start from 10). Additionally, we check if the strike is a floating number and if so, we handle the integer and decimal parts separately and join the components.

We also check if the suggested maturity day is in the past to appropriately build the expiration component of the RIC using the following function:

    	
            

const moment = require('moment');

function getExpComponent(expDate: Date, ident: any) {

    let exp_comp = ''

    if (expDate < moment().format('YYYY-MM-DD')) {

        exp_comp = `^${ident[moment(expDate).format('M')].exp}${moment(expDate).format('Y').slice(-2)}`

    }

    return exp_comp

};

We now have the key building blocks of the RIC and can proceed with the RIC construction. The following function accepts asset RIC, maturity date, strike, option type and a session as an input and connects the building blocks to form the final RIC. Furthermore, we validate the constructed RIC by requesting the historical prices. 

    	
            

async function getIeuRIC(asset: string, maturity: string, strike: number, optType: string, session: any) {

    let expDate = moment(new Date(maturity)).format('YYYY-MM-DD');

    const expDetails = getExpMonth(expDate, optType);

    const assetName = getAssetName(asset)

    const strikeRIC = getStrikeRIC(strike)

    const expComp = getExpComponent(expDate, expDetails[0])

    const generations = ['', 'a', 'b', 'c', 'd']

    for (let gen in generations) {

        const ric =`${assetName}${strikeRIC}${generations[gen]}${expDetails[1]}${moment(expDate).format('Y').slice(-1)}.L${expComp}`

        let ricWithPrices = await getRICWithPrices(ric, maturity, session);

        if (Object.keys(ricWithPrices[1]).length > 0) {

            return ricWithPrices

        }

    }

    return []

}

The function builds individual components of the RIC by calling the respective function and constructs a potential RIC for a validation. To validate the RIC, the function calls the getRICWithPrices (please refer to the source code in GitHub) function which requests its historical prices. If prices are received, the getIeuRICfunction considers the RIC valid and returns the RIC with prices.

Below we run the function to find a call option on FTSE with a strike of 7900 expiring on March 18, 2023.

    	
            

getIeuRIC('.FTSE', '2023-03-18', 7900, 'C', session).then((a: any) => {

    console.log(a)

})

[ 'LFE7900C3.L^C23', { '0': { DATE: '2023-03-17', TRDPRC_1: null, BID: null, ASK: null, SETTLE: 0 }, '1': { DATE: '2023-03-16', TRDPRC_1: null, BID: null, ASK: null, SETTLE: 0.5 }, …, '60': { DATE: '2022-12-20', TRDPRC_1: null, BID: 24, ASK: 27, SETTLE: 24.5 }, '61': { DATE: '2022-12-19', TRDPRC_1: 24.5, BID: 22, ASK: 25, SETTLE: 24.5 } } ]

Functions for the rest of supported exchanges can be found in the project’s GitHub folder. Furthermore, in this article, we present the main wrapper getOptionRIC function.  This accepts the option parameters as an input, if necessary converts ISIN to RIC, finds all the exchanges where options on a given asset are traded and triggers the respective RIC constructor function. The function is presented below:

    	
            

async function getOptionRIC(asset: string, maturity: string, strike: number, optType: string, session: any) {

    optType = optType === 'Call' ? "C" : 'P'

    const exchanges = {

        'OPQ': getOPRA,

        'IEU': getIEU,

        'EUX': getEUREX,

        'HKG': getHK,

        'HFE': getHK,

        'OSA': getOSE

    }

    const exchanges_names = {

        'OPQ': 'OPRA',

        'IEU': 'Intercontinetal Exchange',

        'EUX': 'EUREX',

        'HKG': 'Honk Kong Exchange',

        'HFE': 'Hong Kong Exchange',

        'OSA': 'Osaka Exchange'

    }

    let optionRics = {}

    let pricesList = []

    let expDates = []

    let ric = await getAssetRic(asset)

    if (ric.length) {

        const exchnageCodes = await getExchangeCode(ric)

        for (let exch in exchnageCodes.Buckets) {

            let exchange = exchnageCodes.Buckets[exch].Label

            if (exchange in exchanges) {

                const response = await exchanges[exchange](ric, maturity, strike, optType, session)

                if (response[1] && typeof (response[1]) === 'object') {

                    optionRics[exchanges_names[exchange]] = (response[0])

                    if (response[0].includes("^")) {

                        expDates.push(response[1][0]['DATE'])

                    }

                    else {

                        expDates.push(await getExpiryDate(response[0]))

                    }

                    pricesList.push(response[1])

                    console.log(`Option RIC for ${exchange} exchange is successfully constructed`)

                }

            }

            else {

                console.log(`The ${exchange} exchange is not supported yet`)

            }

        }

    }

    return [optionRics, pricesList, expDates]

}

Although we have the maturity date as an input, we don’t rely on that but rather derive the maturity date after successful RIC construction. The reason we do this, is that for all the exchanges except OPRA, the option RIC doesn’t have the expiration day as a component (it has only the month). As a result, an option for a specific month can be found even if providing incorrect expiration day. (It should match the month though, e.g., user may input 18-Mar-2023 and will get a correct RIC which expires on 19-Mar-2023). As we want to store the correct value in our DB, we double check the expiration date in getOptionRIC function. To do that, we utilise Search capabilities of RD Library for active options by calling the getExpiryDate function (can be found in project’s GitHub folder). Because the expired options can’t be found in Search API, we consider the last price date as the expiration date.

Section 2: Storing data in MongoDB

In this section, we will show how to store the found option RIC and associated metadata in a MongoDB. Before adding the data to the DB, we need to create a Mongoose Schema which defines the structure and property of the document in the MongoDB collection. The following code creates the Mongoose schema to store the constructed RIC, it’s metadata and the prices.

    	
            

const mongoose = require('mongoose');

const Schema = mongoose.Schema;

const ricPricesSchema = new Schema({

    asset: String,

    strike: Number,

    maturity: String,

    ric: String,

    optionType: String,

    exchange: String,

    prices: Object,

    createdDate: Date

});

module.exports = mongoose.model('ricPrices', ricPricesSchema);

Finally, the following function is called from the frontend User form after a user submits a search query with the required parameters, such as Asset ISIN/RIC, maturity date, strike price and the option type. 

    	
            

const constructRIC = async (req: any) => {

    await session.open()

    let ricAndPrices: any = []

    await getOptionRIC(req.body.asset, req.body.maturity, req.body.strike, req.body.optionType, session).then((output: any) => {

        let i = -1

        if (Object.keys(output[1][0] || {}).length > 0) {

            for (let [key, value] of Object.entries(output[0])) {

                i++

                let vals = {

                    asset: req.body.asset,

                    strike: req.body.strike,

                    maturity: output[2][i],

                    ric: value,

                    optionType: req.body.optionType,

                    exchange: key,

                    prices: output[1][i],

                    createdDate: moment().format()

                }

                let newricPrices = new ricPrices(vals)

                newricPrices.save()

                ricAndPrices.push(newricPrices)

            }

        }

        else {

            ricAndPrices = [null]

        }

    })

    await session.close();

    return ricAndPrices

}

This function passes the parameters to our wrapper function which provides all found RICs and prices. Then we add the RIC data into an object and save that into the MongoDB collection defined above.

Conclusion

In this article, we have illustrated how to connect Refinitiv Data Libraries for Typescript and ingest the data. Particularly, we have showcased the Symbol Conversion, Search and Pricing capabilities of the library. Most importantly, we built option RIC constructor functions for each of the supported exchanges following Refinitiv RIC construction Rules. Finally, we presented how the retrieved data can be effectively stored in MongoDB using a typed mongoose schema.

References

For further details, please check out the following resources:

For any questions related to this article or the used APIs, please use the Developer Community Q&A Forum.

  • Register or Log in to applaud this article
  • Let the author know how much this article helped you
If you require assistance, please contact us here