diff --git a/demos/dashboards-risk-score/demo.css b/demos/dashboards-risk-score/demo.css index 88e22e8..23cc8f9 100644 --- a/demos/dashboards-risk-score/demo.css +++ b/demos/dashboards-risk-score/demo.css @@ -31,10 +31,6 @@ body { border-bottom: none; } -#dashboard-col-1 .highcharts-dashboards-component-title { - background-color: #336; -} - @media screen and (max-width: 1000px) { .row { flex-direction: column; diff --git a/demos/dashboards-risk-score/demo.js b/demos/dashboards-risk-score/demo.js index 2d6b123..2c057f8 100644 --- a/demos/dashboards-risk-score/demo.js +++ b/demos/dashboards-risk-score/demo.js @@ -1,30 +1,107 @@ const loadingLabel = document.getElementById('loading-label'); function displayRiskScore (postmanJSON) { - Dashboards.board('container', { + + const highRiskRetirementPortfolio = { + name: 'HighRisk', + currency: 'USD', + totalValue: 100, + holdings: [ + { + id: 'VTIAX', + idType: 'TradingSymbol', + weight: 33 + }, + { + id: 'POGRX', + idType: 'TradingSymbol', + weight: 20 + }, + { + id: 'OAKMX', + idType: 'TradingSymbol', + weight: 20 + }, + { + id: 'VEXAX', + idType: 'TradingSymbol', + weight: 15 + }, + { + id: 'OAKEX', + idType: 'TradingSymbol', + weight: 7 + }, + { + id: 'MWTRX', + idType: 'TradingSymbol', + weight: 5 + } + ] + }; + + const lowRiskRetirementPortfolio = { + name: 'LowRisk', + currency: 'USD', + totalValue: 100, + holdings: [ + { + id: 'VTIAX', + idType: 'TradingSymbol', + weight: 10 + }, + { + id: 'POGRX', + idType: 'TradingSymbol', + weight: 10 + }, + { + id: 'VDADX', + idType: 'TradingSymbol', + weight: 10 + }, + { + id: 'OAKMX', + idType: 'TradingSymbol', + weight: 10 + }, + { + id: 'VEXAX', + idType: 'TradingSymbol', + weight: 5 + }, + { + id: 'OAKEX', + idType: 'TradingSymbol', + weight: 5 + }, + { + id: 'MWTRX', + idType: 'TradingSymbol', + weight: 30 + }, + { + id: 'FSHBX', + idType: 'TradingSymbol', + weight: 10 + }, + { + id: 'VTAPX', + idType: 'TradingSymbol', + weight: 10 + } + ] + } + + const board = Dashboards.board('container', { dataPool: { connectors: [{ id: 'risk-score', type: 'MorningstarRiskScore', options: { portfolios: [ - { - name: 'TestPortfolio1', - currency: 'USD', - totalValue: 100, - holdings: [ - { - id: 'F00000VCTT', - idType: 'SecurityID', - weight: 50 - }, - { - id: 'AAPL', - idType: 'TradingSymbol', - weight: 50 - } - ] - } + lowRiskRetirementPortfolio, + highRiskRetirementPortfolio ], postman: { environmentJSON: postmanJSON @@ -35,44 +112,94 @@ function displayRiskScore (postmanJSON) { components: [ { renderTo: 'dashboard-col-0', + connector: { + id: 'risk-score', + columnAssignment: [ + { + seriesId: 'low-risk', + data: ['LowRisk_RiskScore'] + }, + { + seriesId: 'high-risk', + data: ['HighRisk_RiskScore'] + } + ] + }, + type: 'Highcharts', + chartOptions: { + chart: { + animation: false, + type: 'column' + }, + credits: { + enabled: false + }, + title: { + text: 'Risk score for each portfolio' + }, + subtitle: { + text: 'Conservative is a low risk portfolio, ' + + 'while Aggressive is a high risk portfolio' + }, + yAxis: { + title: { + text: 'Risk Score' + } + }, + xAxis: { + categories: ['Conservative', 'Aggressive'] + }, + series: [ + { + id: 'low-risk', + name: 'Conservative', + tooltip: { + headerFormat: 'Stock/Bond ratio: 50/50
', + useHTML: true + } + }, + { + id: 'high-risk', + name: 'Aggressive', + tooltip: { + headerFormat: 'Stock/Bond ratio: 95/5
', + useHTML: true + } + } + ] + } + }, + { + renderTo: 'dashboard-col-1', connector: { id: 'risk-score' }, + visibleColumns: [ + 'LowRisk_RiskScore', + 'HighRisk_RiskScore' + ], type: 'DataGrid', title: 'RiskScore', dataGridOptions: { editable: false, columns: { - 'TestPortfolio1_EffectiveDate': { - headerFormat: 'Date', - cellFormatter: function () { - return new Date(this.value) - .toISOString() - .substring(0, 10); - } + 'LowRisk_RiskScore': { + headerFormat: 'RiskScore, Conservative' }, - 'TestPortfolio1_RiskScore': { - headerFormat: 'RiskScore' - }, - 'TestPortfolio1_AlignmentScore': { - headerFormat: 'AlignmentScore' - }, - 'TestPortfolio1_RSquared': { - headerFormat: 'RSquared' - }, - 'TestPortfolio1_RetainedWeightProxied': { - headerFormat: 'RetainedWeight' - }, - 'TestPortfolio1_ScoringMethodUsed': { - headerFormat: 'ScoringMethod' + 'HighRisk_RiskScore': { + headerFormat: 'RiskScore, Aggressive' } } } } ] }); - - loadingLabel.style.display = 'none'; + + board.dataPool + .getConnectorTable('risk-score') + .then(() => { + loadingLabel.style.display = 'none'; + }); } async function handleSelectEnvironment (evt) { diff --git a/docs/connectors/morningstar/risk-score/risk-score.md b/docs/connectors/morningstar/risk-score/risk-score.md index 17cae39..74898f8 100644 --- a/docs/connectors/morningstar/risk-score/risk-score.md +++ b/docs/connectors/morningstar/risk-score/risk-score.md @@ -10,19 +10,26 @@ Use the `RiskScoreConnector` to load risk scores. In dashboards, this connector is called `MorningstarRiskScore`. -Specify the holdings in a portfolio in the options along with a postman environment +Specify the portfolio holdings in the options along with a postman environment file for authentication, and other parameters such as `currency`. ### Holdings -Holdings are the securities that make up the portfolio. You can specify a holding using different kinds of id’s. +Holdings are the securities that make up the portfolio. You can specify a +holding using different kinds of id’s. Supported id-types are: `CUSIP`, `FundCode`, `ISIN`, `MSID`, `PerformanceId`, `SecurityID`, `TradingSymbol`. -You can specify the quantity of this holding in the portfolio by using either `weight` or `value`. If you decide to use `weight`, you need to specify the `totalValue` of the portfolio. +You can specify the quantity of this holding in the portfolio by using either +`weight` or `value`. If you decide to use `weight`, you need to specify +the `totalValue` of the portfolio. -> **NOTE:** You cannot mix and match `weight` and `value`. Be consistent and stick to one. +> **NOTE:** You cannot mix and match `weight` and `value`. +Be consistent and stick to one. + +If you specify any holdings that are invalid, the connector will still yield +a result. The invalid holdings are in the connector’s `metadata` after load. For more details, see [Morningstar’s RiskScore API]. diff --git a/package-lock.json b/package-lock.json index cca568d..de0bedb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@highcharts/connectors-morningstar", - "version": "0.0.1-dev", + "version": "0.0.1-repository", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@highcharts/connectors-morningstar", - "version": "0.0.1-dev", + "version": "0.0.1-repository", "license": "UNLICENSED", "bin": { "connectors-morningstar": "bin/morningstar-connectors.js" diff --git a/src/RiskScore/RiskScoreConnector.ts b/src/RiskScore/RiskScoreConnector.ts index 0060914..f813d09 100644 --- a/src/RiskScore/RiskScoreConnector.ts +++ b/src/RiskScore/RiskScoreConnector.ts @@ -26,7 +26,7 @@ import External from '../Shared/External'; import MorningstarConnector from '../Shared/MorningstarConnector'; import MorningstarAPI from '../Shared/MorningstarAPI'; import MorningstarURL from '../Shared/MorningstarURL'; -import RiskScoreOptions, { BaseRiskScorePortfolio, RiskScorePortfolio } from './RiskScoreOptions'; +import RiskScoreOptions, { BaseRiskScorePortfolio, RiskScoreMetadata, RiskScorePortfolio } from './RiskScoreOptions'; import RiskScoreConverter from './RiskScoreConverter'; import { HoldingIdentiferType, MorningstarHoldingOptions, MorningstarHoldingValueOptions, MorningstarHoldingWeightOptions } from '../Shared/MorningstarOptions'; @@ -197,6 +197,7 @@ export class RiskScoreConnector extends MorningstarConnector { super(options); this.converter = new RiskScoreConverter(options.converter); + this.metadata = this.converter.metadata; this.options = options; } @@ -210,6 +211,10 @@ export class RiskScoreConnector extends MorningstarConnector { public override readonly converter: RiskScoreConverter; + /** + * Metadata from the previous load. + */ + public override readonly metadata: RiskScoreMetadata; public override readonly options: RiskScoreOptions; diff --git a/src/RiskScore/RiskScoreConverter.ts b/src/RiskScore/RiskScoreConverter.ts index 77529bc..b37cd57 100644 --- a/src/RiskScore/RiskScoreConverter.ts +++ b/src/RiskScore/RiskScoreConverter.ts @@ -22,7 +22,7 @@ import MorningstarConverter from '../Shared/MorningstarConverter'; import RiskScoreJSON from './RiskScoreJSON'; -import { RiskScoreConverterOptions } from './RiskScoreOptions'; +import { RiskScoreConverterOptions, RiskScoreMetadata, RiskScoreMetadataMessage } from './RiskScoreOptions'; /* * @@ -78,6 +78,10 @@ export class RiskScoreConverter extends MorningstarConverter { super(options); this.options = options as Required; + this.metadata = { + columns: {}, + messages: [] + }; } /* * @@ -92,6 +96,11 @@ export class RiskScoreConverter extends MorningstarConverter { */ public override readonly options: Required; + /** + * Metadata from the previous load. + */ + public readonly metadata: RiskScoreMetadata; + /* * * * Functions @@ -124,7 +133,7 @@ export class RiskScoreConverter extends MorningstarConverter { // Parse and cumulate risk scores by date const sortedPortfolios: RiskScorePortfolio[] = []; - const errors: string[] = []; + const messages: RiskScoreMetadataMessage[] = []; for (const riskScorePortfolio of json.riskScores) { const { @@ -153,15 +162,7 @@ export class RiskScoreConverter extends MorningstarConverter { riskScorePortfolio.metadata !== undefined && riskScorePortfolio.metadata.messages.length > 0 ) { - for (const message of riskScorePortfolio.metadata.messages) { - const holdingNames = message.invalidHoldings.map( - invalidHolding => invalidHolding.identifier - ); - - errors.push( - `The holding(s) ${holdingNames.join(', ')} are invalid. ${message.message}` - ); - } + messages.push(...riskScorePortfolio.metadata.messages); } } @@ -212,9 +213,7 @@ export class RiskScoreConverter extends MorningstarConverter { } } - if (errors.length > 0) { - throw new Error(errors.join('\n')); - } + this.metadata.messages = messages; return true; } diff --git a/src/RiskScore/RiskScoreJSON.ts b/src/RiskScore/RiskScoreJSON.ts index bafc292..7080625 100644 --- a/src/RiskScore/RiskScoreJSON.ts +++ b/src/RiskScore/RiskScoreJSON.ts @@ -14,6 +14,8 @@ 'use strict'; +import { RiskScoreInvalidHolding, RiskScoreMetadataMessage } from './RiskScoreOptions'; + /* * * @@ -55,18 +57,6 @@ export namespace RiskScoreJSON { messages: RiskScoreMetadataMessage[] }; - export type RiskScoreMetadataMessage = { - type: string, - message: string, - invalidHoldings: RiskScoreInvalidHolding[] - }; - - export type RiskScoreInvalidHolding = { - identifier: string, - identifierType: string, - status: string - }; - /* * * diff --git a/src/RiskScore/RiskScoreOptions.ts b/src/RiskScore/RiskScoreOptions.ts index ee11c77..c8ccb01 100644 --- a/src/RiskScore/RiskScoreOptions.ts +++ b/src/RiskScore/RiskScoreOptions.ts @@ -19,7 +19,7 @@ * */ import { Currency } from '../Shared/LocalizationOptions'; -import MorningstarOptions, { MorningstarConverterOptions, MorningstarHoldingValueOptions, MorningstarHoldingWeightOptions } from '../Shared/MorningstarOptions'; +import MorningstarOptions, { MorningstarConverterOptions, MorningstarHoldingValueOptions, MorningstarHoldingWeightOptions, MorningstarMetadata } from '../Shared/MorningstarOptions'; /* * @@ -35,6 +35,22 @@ export interface RiskScoreConverterOptions extends MorningstarConverterOptions { } +export type RiskScoreMetadataMessage = { + type: string, + message: string, + invalidHoldings: RiskScoreInvalidHolding[] +}; + +export type RiskScoreInvalidHolding = { + identifier: string, + identifierType: string, + status: string +}; + +export interface RiskScoreMetadata extends MorningstarMetadata { + messages: RiskScoreMetadataMessage[] +} + export interface BaseRiskScorePortfolio { /** * The name of the portfolio. diff --git a/src/Shared/External.ts b/src/Shared/External.ts index 77d69ba..ae4194c 100644 --- a/src/Shared/External.ts +++ b/src/Shared/External.ts @@ -38,6 +38,9 @@ import _DataTable from '@highcharts/dashboards/es-modules/Data/DataTable'; * */ +export type DataConnectorMetadata = _DataConnector['metadata']; + + export type DataConnectorOptions = Partial<_DataConnector.UserOptions>; diff --git a/src/Shared/MorningstarOptions.ts b/src/Shared/MorningstarOptions.ts index 33e6e80..829eefc 100644 --- a/src/Shared/MorningstarOptions.ts +++ b/src/Shared/MorningstarOptions.ts @@ -129,6 +129,11 @@ export interface MorningstarHoldingValueOptions extends MorningstarHoldingOption } +export interface MorningstarMetadata extends External.DataConnectorMetadata { + // Nothing to add yet +} + + export interface MorningstarOptions extends External.DataConnectorOptions { /** diff --git a/tests/RiskScore/RiskScore.test.ts b/tests/RiskScore/RiskScore.test.ts index 429e1c0..ff6477e 100644 --- a/tests/RiskScore/RiskScore.test.ts +++ b/tests/RiskScore/RiskScore.test.ts @@ -59,6 +59,49 @@ export async function riskScoreLoad ( ); } +export async function riskScoreLoadWithInvalidHoldings ( + api: MC.Shared.MorningstarAPIOptions +) { + const connector = new MC.RiskScoreConnector({ + api, + portfolios: [ + { + name: 'PortfolioWithInvalidHoldings', + currency: 'USD', + totalValue: 100, + holdings: [ + { + id: 'F00000VCTT', + idType: 'SecurityID', + weight: 50 + }, + { + id: 'AAPLL', + idType: 'TradingSymbol', + weight: 50 + } + ] + } + ] + }); + + await connector.load(); + + Assert.deepStrictEqual( + connector.metadata.messages, + [{ + type: 'Warning', + message: 'Invalid or unentitled holdings', + invalidHoldings: [{ + identifier: 'AAPLL', + identifierType: 'TradingSymbol', + status: 'Invalid' + }] + }] + ); + +} + export function riskScoreResponseValidation () { const exampleResponse = { 'riskScores': [