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': [