From a9ef98d1b2efcf5c4c1a1ebf09da2eade95f70da Mon Sep 17 00:00:00 2001 From: pawellysy Date: Fri, 29 Nov 2024 14:01:41 +0100 Subject: [PATCH] Feature/investment screener (#52) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implemented investment screener. * Implemented InvestmentScreenerConnector together with the demo. * Fixed demo and changed the eslint config. * Implemented investment screener tests. * Fixed tests. * Fixed test. * Fixed linting errors. * Added investment screener demo to list of demos. * Fixed typo * Added docs for Investment Screener. * Added comments to demo. * Fixed docs. * Fix typo * Update docs/connectors/morningstar/screeners/investment-screener.md Co-authored-by: Kamil Musiałowski <105275628+kamil-musialowski@users.noreply.github.com> * Added console warn on error in demo. * Added missing typo fixes. --------- Co-authored-by: Kamil Musiałowski <105275628+kamil-musialowski@users.noreply.github.com> Co-authored-by: Kamil Musiałowski --- demos/dashboards-investment-screener/demo.css | 63 +++++ .../dashboards-investment-screener/demo.html | 38 +++ demos/dashboards-investment-screener/demo.js | 219 ++++++++++++++++++ demos/index.html | 1 + docs/connectors/morningstar.md | 1 + .../screeners/investment-screener.md | 63 +++++ .../InvestmentScreenerConnector.ts | 172 ++++++++++++++ .../InvestmentScreenerConverter.ts | 99 ++++++++ .../InvestmentScreenerJSON.ts | 72 ++++++ .../InvestmentScreenerOptions.ts | 123 ++++++++++ src/Screeners/InvestmentScreener/README.md | 8 + src/Screeners/InvestmentScreener/index.ts | 45 ++++ src/api.d.ts | 3 +- src/index.ts | 10 +- tests/Screeners/InvestmentScreener.test.ts | 74 ++++++ 15 files changed, 983 insertions(+), 8 deletions(-) create mode 100644 demos/dashboards-investment-screener/demo.css create mode 100644 demos/dashboards-investment-screener/demo.html create mode 100644 demos/dashboards-investment-screener/demo.js create mode 100644 docs/connectors/morningstar/screeners/investment-screener.md create mode 100644 src/Screeners/InvestmentScreener/InvestmentScreenerConnector.ts create mode 100644 src/Screeners/InvestmentScreener/InvestmentScreenerConverter.ts create mode 100644 src/Screeners/InvestmentScreener/InvestmentScreenerJSON.ts create mode 100644 src/Screeners/InvestmentScreener/InvestmentScreenerOptions.ts create mode 100644 src/Screeners/InvestmentScreener/README.md create mode 100644 src/Screeners/InvestmentScreener/index.ts create mode 100644 tests/Screeners/InvestmentScreener.test.ts diff --git a/demos/dashboards-investment-screener/demo.css b/demos/dashboards-investment-screener/demo.css new file mode 100644 index 0000000..d3d6439 --- /dev/null +++ b/demos/dashboards-investment-screener/demo.css @@ -0,0 +1,63 @@ +@import url("https://code.highcharts.com/dashboards/css/datagrid.css"); +@import url("https://code.highcharts.com/css/highcharts.css"); +@import url("https://code.highcharts.com/dashboards/css/dashboards.css"); + +body { + font-family: "Helvetica Neue", Helvetica, "Segoe UI", Arial, sans-serif; +} + +.row { + display: flex; + flex-wrap: wrap; +} + +.cell { + flex: 1; + min-width: 20px; +} + +.cell>.highcharts-dashboards-component { + position: relative; + margin: 10px; + background-clip: border-box; +} + +.highcharts-dashboards-component-title { + padding: 10px; + margin: 0; + background-color: var(--highcharts-neutral-color-5); + color: var(--highcharts-neutral-color-100); + border: solid 1px var(--highcharts-neutral-color-20); + border-bottom: none; +} + +@media screen and (max-width: 1000px) { + .row { + flex-direction: column; + } +} + +.filters-row { + flex-direction: row; + justify-content: space-evenly; +} + +#dashboard-col-1 { + height: 500px; +} + +.filters-row>button { + background: #f2f2f2; + border: none; + border-radius: 4px; + cursor: pointer; + display: inline-block; + font-size: 0.8rem; + padding: 0.5rem 1.5rem; + margin: 0.5rem -5px 0.5rem 10px; + transition: background 250ms; +} + +.filters-row>button:hover { + background: #e6e6e6; +} diff --git a/demos/dashboards-investment-screener/demo.html b/demos/dashboards-investment-screener/demo.html new file mode 100644 index 0000000..ac9053a --- /dev/null +++ b/demos/dashboards-investment-screener/demo.html @@ -0,0 +1,38 @@ + + + + + + + + + + + Highcharts Dashboards + Morningstar Portfolio Investment Details + + +

Highcharts Dashboards + Morningstar Portfolio Investment Details

+

+ Add your Postman environment file from Morningstar to start the demo: + +

+ +
+ + + + +
+
+ Current filter: + +
+
+
+
+ - + - + - + + + diff --git a/demos/dashboards-investment-screener/demo.js b/demos/dashboards-investment-screener/demo.js new file mode 100644 index 0000000..777b671 --- /dev/null +++ b/demos/dashboards-investment-screener/demo.js @@ -0,0 +1,219 @@ +const loadingLabel = document.getElementById('loading-label'); + +function displayInvestmentScreener (postmanJSON) { + const secIds = [ + 'secId', + 'tenforeId', + 'name', + 'closePrice', + 'ongoingCharge', + 'initialPurchase', + 'maxFrontEndLoad', + 'analystRatingScale', + 'average12MonthCarbonRiskScore', + 'investmentType', + 'holdingTypeId', + 'universe' + ]; + + const columns = secIds.map(id => ({ + id: `InvestmentScreener_${id}`, + header: { + format: id + } + })); + + const board = Dashboards.board('container', { + dataPool: { + connectors: [ + { + id: 'investment-screener', + type: 'MorningstarInvestmentScreener', + options: { + page: 1, + pageSize: 20, + langageId: 'en-GB', + currencyId: 'USD', + securityDataPoints: secIds, + universeIds: ['FOALL$$ALL'], + postman: { + environmentJSON: postmanJSON + } + } + } + ] + }, + components: [ + { + renderTo: 'dashboard-col-1', + connector: { + id: 'investment-screener' + }, + type: 'DataGrid', + + dataGridOptions: { + editable: false, + columns + }, + title: 'Investment Screener' + } + ] + }); + + board.dataPool.getConnector('investment-screener').then(connector => { + loadingLabel.style.display = 'none'; + document.getElementById('total').innerHTML = + `total - ${connector.metadata.total}`; + document.getElementById('page').innerHTML = + `page - ${connector.metadata.page}`; + document.getElementById('total-pages').innerHTML = + `out of ${Math.ceil(connector.metadata.total / connector.metadata.pageSize)}`; + }); + + /** + * Add filter to a connector + * + * @param {InvestmentScreenerFilter[]} filters + */ + function setFilter (filters) { + loadingLabel.style.display = 'block'; + board.dataPool.getConnector('investment-screener').then(connector => { + const options = { + filters + }; + connector.load(options).then(() => { + loadingLabel.style.display = 'none'; + document.getElementById('total').innerHTML = + `total - ${connector.metadata.total}`; + document.getElementById('page').innerHTML = + `page - ${connector.metadata.page}`; + document.getElementById('total-pages').innerHTML = + `out of ${Math.ceil(connector.metadata.total / connector.metadata.pageSize)}`; + }); + }); + } + + document.getElementById('filter-1').addEventListener('click', e => { + e.target.classList.add('button-active'); + document.getElementById('current-filter').innerHTML = + e.target.innerHTML; + // Create a filter that will check if the star rating is equal to 5 + // and if the analyst rating is equal to 5 + setFilter([ + { + dataPointId: 'StarRatingM255', + comparatorCode: 'IN', + value: 5 + }, + { + dataPointId: 'AnalystRatingScale', + comparatorCode: 'IN', + value: 5 + } + ]); + }); + + document.getElementById('filter-2').addEventListener('click', e => { + document.getElementById('current-filter').innerHTML = + e.target.innerHTML; + // Create a filter that will check if the GBR return is between + // 39 and 60 + setFilter([ + { + dataPointId: 'GBRReturnM0', + comparatorCode: 'BTW', + value: '40:60' + } + ]); + }); + + document.getElementById('filter-3').addEventListener('click', e => { + document.getElementById('current-filter').innerHTML = + e.target.innerHTML; + // Create a filter that will filter on the Low Carbon Designation + // and Carbon Risk Score. These Investments are considered highly + // sustainable + setFilter([ + { + dataPointId: 'LowCarbonDesignation', + comparatorCode: 'IN', + value: 'TRUE' + }, + { + dataPointId: 'CarbonRiskScore', + comparatorCode: 'EQ', + value: 0 + }, + { + dataPointId: 'SustainabilityRank', + comparatorCode: 'IN', + value: 5 + } + ]); + }); + + document.getElementById('filter-4').addEventListener('click', e => { + document.getElementById('current-filter').innerHTML = + e.target.innerHTML; + // Create a filter that will check if the investment is considered + // as "low expenses" + setFilter([ + { + dataPointId: 'StarRatingM255', + comparatorCode: 'IN', + value: 5 + }, + { dataPointId: 'OngoingCharge', comparatorCode: 'LT', value: 0.5 }, + { + dataPointId: 'initialPurchase', + comparatorCode: 'LT', + value: 500000000 + }, + { dataPointId: 'maxFrontEndLoad', comparatorCode: 'LT', value: 5 }, + { dataPointId: 'maxDeferredLoad', comparatorCode: 'LT', value: 5 } + ]); + }); +} + +async function handleSelectEnvironment (evt) { + const target = evt.target; + const postmanJSON = await getPostmanJSON(target); + + if (!postmanJSON) { + loadingLabel.textContent = + 'The provided file is not a Postman Environment Configuration.'; + loadingLabel.style.display = 'block'; + + return; + } + + target.parentNode.style.display = 'none'; + + loadingLabel.style.display = 'block'; + loadingLabel.textContent = 'Loading data…'; + + displayInvestmentScreener(postmanJSON); +} + +document + .getElementById('postman-json') + .addEventListener('change', handleSelectEnvironment); + +async function getPostmanJSON (htmlInputFile) { + let file; + let fileJSON; + + for (file of htmlInputFile.files) { + try { + fileJSON = JSON.parse(await file.text()); + if (Connectors.Morningstar.isPostmanEnvironmentJSON(fileJSON)) { + break; + } + } catch (error) { + // eslint-disable-next-line no-console + console.warn(error); + } + } + + return fileJSON; +} diff --git a/demos/index.html b/demos/index.html index 8199545..836ac72 100644 --- a/demos/index.html +++ b/demos/index.html @@ -12,6 +12,7 @@

Morningstar Connectors Demos

  • Highcharts Stock + Morningstar OHLCV TimeSeries
  • Highcharts Stock + Morningstar Security Details
  • Highcharts Dashboards + Morningstar Risk Score
  • +
  • Highcharts Dashboards + Morningstar Investment Screener
  • Using fetch without connectors
  • diff --git a/docs/connectors/morningstar.md b/docs/connectors/morningstar.md index 74bb696..9f64e1f 100644 --- a/docs/connectors/morningstar.md +++ b/docs/connectors/morningstar.md @@ -54,4 +54,5 @@ types. * [Risk Score](morningstar/risk-score.md) * [TimeSeries](morningstar/time-series/time-series.md) * [XRay](morningstar/x-ray.md) +* [Investment Screener](morningstar/screeners/investment-screener.md) * [Security Details](morningstar/security-details.md) diff --git a/docs/connectors/morningstar/screeners/investment-screener.md b/docs/connectors/morningstar/screeners/investment-screener.md new file mode 100644 index 0000000..74e245e --- /dev/null +++ b/docs/connectors/morningstar/screeners/investment-screener.md @@ -0,0 +1,63 @@ +Investment Screener +============================= + +Using Morningstar Investment Screener endpoint allows you to filter Morningstar's database of global investments using hundreds of data points including Morningstar proprietary data. + +How to use Investment Screener +---------------- + +Data can be filtered on any data point which is comprehensively covered by Morningstar, including proprietary data such as: + + * Morningstar Sustainability Ratings + * Analyst Rating + * Fair Value + * Style Box + * With the solution, you can develop a sophisticated screening tool offering dozens of filters for advanced users. + +You can also use it to power predefined screeners for common investment research interests such as Top Fixed Income Funds, Top Rated US Bond Index Funds, Sustainable Investments, and so on. + +For more details, see [Morningstar's Investment Screener API]. + + +[Morningstar's Investment Screener API]: https://developer.morningstar.com/direct-web-services/documentation/api-reference/screener/investment-screener + +This connector is designed to be interacted with using external buttons, that might filter data on the backend, provide pagination as well as sorting. + +Here is an example of how to use the Investment Screener connector: + +```js +const screenerConnector = MC.InvestmentScreenerConnector({ + page: 1, + pageSize: 20, + languageId: 'en-GB', + currencyId: 'USD', + filters: [ + { + dataPointId: 'StarRatingM255', + comparatorCode: 'IN', + value: 5 + } + ], + securityDataPoints: [ + 'secId', + 'tenforeId', + 'name', + 'closePrice', + 'ongoingCharge', + 'initialPurchase', + 'maxFrontEndLoad', + 'analystRatingScale', + 'average12MonthCarbonRiskScore', + 'investmentType', + 'holdingTypeId', + 'universe' + ], + universeIds: ['FOALL$$ALL'], + postman: { + environmentJSON: postmanJSON + } +}); +``` + +For details see [Morningstar's Investment Screener API]. + diff --git a/src/Screeners/InvestmentScreener/InvestmentScreenerConnector.ts b/src/Screeners/InvestmentScreener/InvestmentScreenerConnector.ts new file mode 100644 index 0000000..066d746 --- /dev/null +++ b/src/Screeners/InvestmentScreener/InvestmentScreenerConnector.ts @@ -0,0 +1,172 @@ +/* * + * + * (c) 2009-2024 Highsoft AS + * + * License: www.highcharts.com/license + * + * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! + * + * Authors: + * - Pawel Lysy + * + * */ + +'use strict'; + +/* * + * + * Imports + * + * */ + +import type InvestmentScreenerOptions from './InvestmentScreenerOptions'; +import type { + InvestmentScreenerFilter, + InvestmentScreenerMetadata +} from './InvestmentScreenerOptions'; +import External from '../../Shared/External'; +import MorningstarAPI from '../../Shared/MorningstarAPI'; +import MorningstarConnector from '../../Shared/MorningstarConnector'; +import InvestmentScreenerConverter from './InvestmentScreenerConverter'; +import MorningstarURL from '../../Shared/MorningstarURL'; + +/* * + * + * Constants + * + * */ + +const UTF_PIPE = '|'; +const UTF_COLON = ':'; + +/* * + * + * Class + * + * */ +export class InvestmentScreenerConnector extends MorningstarConnector { + public constructor ( + options: InvestmentScreenerOptions = { universeIds: [] } + ) { + super(options); + + this.converter = new InvestmentScreenerConverter(options.converter); + this.metadata = this.converter.metadata; + this.options = options; + } + + public override readonly converter: InvestmentScreenerConverter; + + public override readonly options: InvestmentScreenerOptions; + + public override readonly metadata: InvestmentScreenerMetadata; + + public override async load ( + options?: InvestmentScreenerOptions + ): Promise { + await super.load(); + + const userOptions = { ...this.options, ...options }; + this.api ??= new MorningstarAPI(userOptions.api); + const api = this.api; + const url = new MorningstarURL('ecint/v1/screener', api.baseURL); + const searchParams = url.searchParams; + + searchParams.set('outputType', 'json'); + searchParams.set('universeIds', userOptions.universeIds.join(UTF_PIPE)); + + if (userOptions.securityDataPoints) { + searchParams.set( + 'securityDataPoints', + userOptions.securityDataPoints.join(',') + ); + } + + if (userOptions.filters) { + const filters = userOptions.filters.reduce( + (prev, curr) => + prev === '' ? + this.getFilter(curr) : + prev + UTF_PIPE + this.getFilter(curr), + '' + ); + searchParams.set('filters', filters); + } + + if (userOptions.ignoreRestructure) { + searchParams.set( + 'ignoreRestructure', + `${userOptions.ignoreRestructure}` + ); + } + + if (userOptions.term) { + searchParams.set('term', userOptions.term); + } + + if (userOptions.applyTrackRecordExtension) { + searchParams.set( + 'applyTrackRecordExtension', + `${userOptions.applyTrackRecordExtension}` + ); + } + + if (userOptions.page) { + searchParams.set('page', `${userOptions.page}`); + } + + if (userOptions.pageSize) { + searchParams.set('pageSize', `${userOptions.pageSize || 1}`); + } + + if (userOptions.sortOrder) { + searchParams.set('sortOrder', userOptions.sortOrder); + } + + if (userOptions.countryId) { + searchParams.set('countryId', userOptions.countryId); + } + + if (userOptions.currencyId) { + searchParams.set('currencyId', userOptions.currencyId); + } + + const response = await api.fetch(url); + const json = (await response.json()) as unknown; + + this.converter.parse({ json }); + this.table.deleteColumns(); + this.table.setColumns(this.converter.getTable().getColumns()); + + return this; + } + + private getFilter (filter: InvestmentScreenerFilter): string { + return `${filter.dataPointId}${UTF_COLON}${filter.comparatorCode}${UTF_COLON}${filter.value}`; + } +} + +/* * + * + * Registry + * + * */ + +declare module '@highcharts/dashboards/es-modules/Data/Connectors/DataConnectorType' { + interface DataConnectorTypes { + MorningstarInvestmentScreener: typeof InvestmentScreenerConnector; + } +} + +External.DataConnector.registerType( + 'MorningstarInvestmentScreener', + InvestmentScreenerConnector +); + +/* * + * + * Default Export + * + * */ + +export default InvestmentScreenerConnector; diff --git a/src/Screeners/InvestmentScreener/InvestmentScreenerConverter.ts b/src/Screeners/InvestmentScreener/InvestmentScreenerConverter.ts new file mode 100644 index 0000000..3dfbd95 --- /dev/null +++ b/src/Screeners/InvestmentScreener/InvestmentScreenerConverter.ts @@ -0,0 +1,99 @@ +/* * + * + * (c) 2009-2024 Highsoft AS + * + * License: www.highcharts.com/license + * + * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! + * + * Authors: + * - Pawel Lysy + * + * */ + +'use strict'; + +/* * + * + * Imports + * + * */ + +import type { InvestmentScreenerConverterOptions, InvestmentScreenerMetadata } from './InvestmentScreenerOptions'; +import type { DataTable } from '@highcharts/dashboards'; + +import MorningstarConverter from '../../Shared/MorningstarConverter'; +import InvestmentScreenerJSON from './InvestmentScreenerJSON'; + +/* * + * + * Class + * + * */ + +export class InvestmentScreenerConverter extends MorningstarConverter { + public constructor (options?: InvestmentScreenerConverterOptions) { + super(options); + + this.metadata = { + columns: {} + }; + } + /* * + * + * Properties + * + * */ + + public readonly metadata: InvestmentScreenerMetadata; + + /* * + * + * Functions + * + * */ + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public parse (options: InvestmentScreenerConverterOptions): void { + const metadata = this.metadata; + const table = this.table; + const userOptions = { + ...this.options, + ...options + }; + const json = userOptions.json; + + if (!InvestmentScreenerJSON.isInvestmentScreenerResponse(json)) { + throw new Error('Invalid data'); + } + table.deleteColumns(); + const rows = json.rows; + if (rows.length > 0) { + const row = rows[0]; + for (const element of Object.keys(row)) { + table.setColumn(`InvestmentScreener_${element}`); + } + + for (let i = 0; i < rows.length; i++) { + for (const [key, val] of Object.entries(rows[i])) { + table.setCell( + `InvestmentScreener_${key}`, + i, + val as DataTable.CellType + ); + } + } + metadata.page = json.page; + metadata.total = json.total; + metadata.pageSize = json.pageSize; + } + } +} + +/* * + * + * Default Export + * + * */ + +export default InvestmentScreenerConverter; diff --git a/src/Screeners/InvestmentScreener/InvestmentScreenerJSON.ts b/src/Screeners/InvestmentScreener/InvestmentScreenerJSON.ts new file mode 100644 index 0000000..4b479e0 --- /dev/null +++ b/src/Screeners/InvestmentScreener/InvestmentScreenerJSON.ts @@ -0,0 +1,72 @@ +/* * + * + * (c) 2009-2024 Highsoft AS + * + * License: www.highcharts.com/license + * + * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! + * + * Authors: + * - Pawel Lysy + * + * */ + +'use strict'; + +/* * + * + * Namespace + * + * */ + +namespace InvestmentScreenerJSON { + export interface Response { + InvestmentScreener: InvestmentScreenerResponse; + } + + export interface InvestmentScreenerResponse { + total: number; + page: number; + pageSize: number; + rows: InvestmentScreenerRow[]; + } + + export interface InvestmentScreenerRow { + [key: string]: any; + } + + /* * + * + * Functions + * + * */ + + export function isInvestmentScreenerResponse ( + json?: unknown + ): json is InvestmentScreenerResponse { + return ( + !!json && + typeof json === 'object' && + typeof (json as InvestmentScreenerResponse).rows === 'object' && + (json as InvestmentScreenerResponse).rows instanceof Array && + ((json as InvestmentScreenerResponse).rows.length === 0 || + isInvestmentScreenerRow( + (json as InvestmentScreenerResponse).rows[0] + )) + ); + } + + export function isInvestmentScreenerRow ( + json?: unknown + ): json is InvestmentScreenerRow { + return !!json && typeof json === 'object'; + } +} + +/* * + * + * Default Export + * + * */ + +export default InvestmentScreenerJSON; diff --git a/src/Screeners/InvestmentScreener/InvestmentScreenerOptions.ts b/src/Screeners/InvestmentScreener/InvestmentScreenerOptions.ts new file mode 100644 index 0000000..f4963f5 --- /dev/null +++ b/src/Screeners/InvestmentScreener/InvestmentScreenerOptions.ts @@ -0,0 +1,123 @@ +import type { + MorningstarConverterOptions, + MorningstarMetadata, + MorningstarOptions +} from '../../Shared/MorningstarOptions'; +export interface InvestmentScreenerOptions extends MorningstarOptions { + applyTrackRecordExtension?: boolean; + /** + * + * ISO alpha-3 country code. + * + */ + countryId?: string; + /** + * + * ISO alpha-3 currency code. + * The currency to be used for calculated data points such as performance + * returns. To return list of securities with a specific base currency, + * you must pass the following filter in the request: + * CURRENCY:EQ:{currency code}. For example: Currency:EQ:EUR. See the + * Filters section for more information. + */ + currencyId?: string; + /** + * + * A list of filter data points. + * Multiple values must be separated by a pipe character ( | ) URL-encoded + * as “%7C”. In a UI screening tool, these values define the filters that + * are seen on the screen. Custom data points can be configured. See + * Filters for information about how to get a list of filter data points. + */ + filterDataPoints?: string[]; + /** + * + * A list of criteria a security must meet to be included in the results. + */ + filters?: InvestmentScreenerFilter[]; + /** + * + * When true returns will not be calculated using the restructure date. + * + */ + ignoreRestructure?: boolean; + /** + * + * The number of output pages. + * + */ + page?: number; + /** + * + * The number of rows per page. + * + */ + pageSize?: number; + /** + * + * A list of security data points to return in the response. + * + */ + securityDataPoints?: SecurityDataPointType[]; + /** + * + * Data points to sort on and the order in which results are sorted + * + */ + sortOrder?: string; + /** + * + * Search string to use to search for securities by name, identifiers, or + * symbols. Can be used with filter to run a combined search. + * + */ + term?: string; + /** + * + * A list of investment universe identifiers to query. Values may end with + * `:1` to signify that a custom universe is not only client funds. + * + */ + universeIds: string[]; +} + +export interface InvestmentScreenerFilter { + /** The data point to filter on. */ + dataPointId: string; + /** The comparator to use. */ + comparatorCode: ComparatorCode; + /** The value to compare against. */ + value: any; +} + +export interface InvestmentScreenerMetadata extends MorningstarMetadata { + page?: number; + total?: number; + pageSize?: number; +} + +export interface InvestmentScreenerConverterOptions + extends MorningstarConverterOptions { + // Nothing to add yet +} + +export type ComparatorCode = + | 'IN' + | 'NIN' + | 'EQ' + | 'NE' + | 'GT' + | 'GTN' + | 'GTE' + | 'GTEN' + | 'LT' + | 'LTN' + | 'LTE' + | 'LTEN' + | 'CONTAINS' + | 'BTW' + | 'STARTSWITH'; + +export type SecurityDataPointType = string; + +export default InvestmentScreenerOptions; diff --git a/src/Screeners/InvestmentScreener/README.md b/src/Screeners/InvestmentScreener/README.md new file mode 100644 index 0000000..33837c1 --- /dev/null +++ b/src/Screeners/InvestmentScreener/README.md @@ -0,0 +1,8 @@ +Morningstar Investment Screener Connector +================================ + +This Highcharts Dashboards connector provides data from Morningstar’s [Investment Screener API]. The JSON from the API is converted into a DataTable. + + + +[Portfolio Risk Score API]: https://developer.morningstar.com/direct-web-services/documentation/api-reference/screener/investment-screener diff --git a/src/Screeners/InvestmentScreener/index.ts b/src/Screeners/InvestmentScreener/index.ts new file mode 100644 index 0000000..15dfb6f --- /dev/null +++ b/src/Screeners/InvestmentScreener/index.ts @@ -0,0 +1,45 @@ +/* * + * + * (c) 2009-2024 Highsoft AS + * + * License: www.highcharts.com/license + * + * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! + * + * Authors: + * - Pawel Lysy + * + * */ + +'use strict'; + +/* * + * + * Imports + * + * */ + +import InvestmentScreenerConnector from './InvestmentScreenerConnector'; + + +/* * + * + * Exports + * + * */ + + +export * from './InvestmentScreenerConnector'; +export * from './InvestmentScreenerConverter'; +export * from './InvestmentScreenerOptions'; + + + +/* * + * + * Default Export + * + * */ + + +export default InvestmentScreenerConnector; diff --git a/src/api.d.ts b/src/api.d.ts index 6fdc664..b9d0844 100644 --- a/src/api.d.ts +++ b/src/api.d.ts @@ -24,7 +24,7 @@ import RiskScoreOptions from './RiskScore/RiskScoreOptions'; import RNANewsOptions from './RNANews/RNANewsOptions'; import TimeSeriesOptions from './TimeSeries/TimeSeriesOptions'; import XRayOptions from './XRay/XRayOptions'; - +import InvestmentScreenerOptions from './Screeners/InvestmentScreener/InvestmentScreenerOptions'; /* * * @@ -42,4 +42,5 @@ interface Options { RNANews: RNANewsOptions; TimeSeries: TimeSeriesOptions; XRay: XRayOptions; + InvestmentScreener: InvestmentScreenerOptions; } diff --git a/src/index.ts b/src/index.ts index 86e2d76..83c0fc6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,35 +13,32 @@ * * */ - 'use strict'; - /* * * * Imports * * */ - import GoalAnalysis from './GoalAnalysis/index'; import SecurityDetails from './SecurityDetails/index'; import RNANews from './RNANews/index'; +import InvestmentScreener from './Screeners/InvestmentScreener/index'; import RiskScore from './RiskScore/index'; import * as Shared from './Shared/index'; import TimeSeries from './TimeSeries/index'; import { version } from './version'; import XRay from './XRay/index'; - /* * * * Exports * * */ - export * from './GoalAnalysis/index'; +export * from './Screeners/InvestmentScreener/index'; export * from './SecurityDetails/index'; export * from './RNANews/index'; export * from './RiskScore/index'; @@ -50,14 +47,12 @@ export * from './TimeSeries/index'; export { version } from './version'; export * from './XRay/index'; - /* * * * Default Export * * */ - export default { GoalAnalysis, SecurityDetails, @@ -66,5 +61,6 @@ export default { Shared, TimeSeries, XRay, + InvestmentScreener, version }; diff --git a/tests/Screeners/InvestmentScreener.test.ts b/tests/Screeners/InvestmentScreener.test.ts new file mode 100644 index 0000000..31ae2c9 --- /dev/null +++ b/tests/Screeners/InvestmentScreener.test.ts @@ -0,0 +1,74 @@ +import * as Assert from 'node:assert/strict'; +import * as MC from '../../code/connectors-morningstar.src'; +import InvestmentScreenerJSON from +'../../code/es-modules/Screeners/InvestmentScreener/InvestmentScreenerJSON'; + +export async function investmentScreenerLoad ( + api: MC.Shared.MorningstarAPIOptions +) { + const secIds = [ + 'secId', + 'name', + 'investmentType', + 'holdingTypeId', + 'universe', + 'tenforeId', + 'closePrice', + 'ongoingCharge', + 'initialPurchase', + 'maxFrontEndLoad' + ]; + const connector = new MC.InvestmentScreenerConnector({ + api, + page: 1, + pageSize: 20, + currencyId: 'USD', + securityDataPoints: secIds, + universeIds: ['FOALL$$ALL'] + }); + + Assert.ok( + connector instanceof MC.InvestmentScreenerConnector, + 'Connector should be instance of InvestmentScreenerConnector class.' + ); + + Assert.ok( + connector.converter instanceof MC.InvestmentScreenerConverter, + 'Converter should be instance of InvestmentScreenerConverter.' + ); + + await connector.load(); + + Assert.deepStrictEqual( + connector.table.getColumnNames(), + secIds.map(id => `InvestmentScreener_${id}`), + 'Connector table should exist of expected columns.' + ); + + Assert.strictEqual( + connector.table.getRowCount(), + 15, + 'Connector table should have 15 rows.' + ); +} + +export function InvestmentScreenerResponseValidation () { + const exampleResponse = { + total: 18238, + page: 1, + pageSize: 20, + rows: [ + { + SecId: 'F000002PLH', + Name: '(LF)-Flexi Allocation Greece Eurobank', + riskrating: 4, + rating: 4 + } + ] + }; + + Assert.ok( + InvestmentScreenerJSON.isInvestmentScreenerResponse(exampleResponse), + 'InvestmentScreenerJSON should validate correct response.' + ); +}