diff --git a/demos/dashboards-xray/demo.css b/demos/dashboards-xray/demo.css
new file mode 100644
index 0000000..23cc8f9
--- /dev/null
+++ b/demos/dashboards-xray/demo.css
@@ -0,0 +1,38 @@
+@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;
+ }
+}
diff --git a/demos/dashboards-xray/demo.html b/demos/dashboards-xray/demo.html
new file mode 100644
index 0000000..58fa1c7
--- /dev/null
+++ b/demos/dashboards-xray/demo.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+ Highcharts Dashboards DataGrid + Morningstar XRay Connector
+
+
+ Highcharts Dashboards DataGrid + Morningstar XRay Connector
+
+ Add your Postman environment file from Morningstar to start the demo:
+
+
+ Loading data…
+
+
+
+
diff --git a/demos/dashboards-xray/demo.js b/demos/dashboards-xray/demo.js
new file mode 100644
index 0000000..06dd06d
--- /dev/null
+++ b/demos/dashboards-xray/demo.js
@@ -0,0 +1,216 @@
+const globalStockSectorMap = {
+ 99: 'Not Classified',
+ 101: 'Basic Materials',
+ 102: 'Consumer Staples',
+ 103: 'Financial Services',
+ 104: 'Real Estate',
+ 205: 'Consumer Defensive',
+ 206: 'Healthcare',
+ 207: 'Utilities',
+ 308: 'Communication Services',
+ 309: 'Energy',
+ 310: 'Industrials',
+ 311: 'Technology'
+};
+
+const assetAllocationMap = {
+ 1: 'Stock',
+ 2: 'Bond',
+ 3: 'Cash',
+ 4: 'Other',
+ 99: 'Not Classified'
+};
+
+const regionalExposureMap = {
+ 1: 'United States',
+ 2: 'Canada',
+ 3: 'Latin America',
+ 4: 'United Kingdom',
+ 5: 'Eurozone',
+ 6: 'Europe - ex Euro',
+ 7: 'Europe - Emerging',
+ 8: 'Africa',
+ 9: 'Middle East',
+ 10: 'Japan',
+ 11: 'Australasia',
+ 12: 'Asia - Developed',
+ 13: 'Asia - Emerging',
+ 14: 'Emerging Market',
+ 15: 'Developed Country',
+ 16: 'Not Classified',
+ 99: 'Other'
+}
+
+async function displaySecurityDetails (postmanJSON) {
+ const board = Dashboards.board('container', {
+ dataPool: {
+ connectors: [{
+ id: 'xray',
+ type: 'MorningstarXRay',
+ options: {
+ postman: {
+ environmentJSON: postmanJSON
+ },
+ currencyId: 'GBP',
+ dataPoints: {
+ type: 'portfolio',
+ dataPoints: [
+ 'AssetAllocationMorningstarEUR3',
+ 'GlobalStockSector',
+ 'RegionalExposure'
+ ]
+ },
+ holdings: [
+ {
+ id: 'F0GBR052QA',
+ idType: 'MSID',
+ type: 'FO',
+ weight: '100',
+ name: 'BlackRock Income and Growth Ord',
+ holdingType: 'weight'
+ }
+ ]
+ }
+ }]
+ },
+ components: [
+ {
+ renderTo: 'dashboard-col-0',
+ connector: {
+ id: 'xray'
+ },
+ type: 'DataGrid',
+ title: 'Global Stock Sector',
+ dataGridOptions: {
+ header: [{
+ format: 'Net',
+ columnId: 'XRay_GlobalStockSector_N_Categories'
+ }, {
+ format: 'Values',
+ columnId: 'XRay_GlobalStockSector_N_Values'
+ }],
+ columns: [{
+ id: 'XRay_GlobalStockSector_N_Categories',
+ cells: {
+ formatter: function () {
+ return this.value !== void 0 ?
+ globalStockSectorMap[this.value] : '';
+ }
+ }
+ }]
+ }
+ }, {
+ renderTo: 'dashboard-col-1',
+ connector: {
+ id: 'xray'
+ },
+ type: 'DataGrid',
+ title: 'Morningstar EUR3',
+ dataGridOptions: {
+ header: [{
+ format: 'Long',
+ columnId: 'XRay_MorningstarEUR3_L_Categories'
+ }, {
+ format: 'Values',
+ columnId: 'XRay_MorningstarEUR3_L_Values'
+ }, {
+ format: 'Net',
+ columnId: 'XRay_MorningstarEUR3_N_Categories'
+ }, {
+ format: 'Values',
+ columnId: 'XRay_MorningstarEUR3_N_Values'
+ }, {
+ format: 'Short',
+ columnId: 'XRay_MorningstarEUR3_S_Categories'
+ }, {
+ format: 'Values',
+ columnId: 'XRay_MorningstarEUR3_S_Values'
+ }],
+ columns: [{
+ id: 'XRay_MorningstarEUR3_L_Categories',
+ cells: {
+ formatter: function () {
+ return this.value !== void 0 ?
+ assetAllocationMap[this.value] : '';
+ }
+ }
+ }, {
+ id: 'XRay_MorningstarEUR3_N_Categories',
+ cells: {
+ formatter: function () {
+ return this.value !== void 0 ?
+ assetAllocationMap[this.value] : '';
+ }
+ }
+ }, {
+ id: 'XRay_MorningstarEUR3_S_Categories',
+ cells: {
+ formatter: function () {
+ return this.value !== void 0 ?
+ assetAllocationMap[this.value] : '';
+ }
+ }
+ }]
+ }
+ }, {
+ renderTo: 'dashboard-col-2',
+ connector: {
+ id: 'xray'
+ },
+ type: 'DataGrid',
+ title: 'Regional Exposure',
+ dataGridOptions: {
+ header: [{
+ format: 'Net',
+ columnId: 'XRay_RegionalExposure_N_Categories'
+ }, {
+ format: 'Values',
+ columnId: 'XRay_RegionalExposure_N_Values'
+ }],
+ columns: [{
+ id: 'XRay_RegionalExposure_N_Categories',
+ cells: {
+ formatter: function () {
+ return this.value !== void 0 ?
+ regionalExposureMap[this.value] : '';
+ }
+ }
+ }]
+ }
+ }
+ ]
+});
+
+ board.dataPool.getConnectorTable('xray');
+
+}
+
+async function handleSelectEnvironment (evt) {
+ const target = evt.target;
+ const postmanJSON = await getPostmanJSON(target);
+
+ target.parentNode.style.display = 'none';
+
+ displaySecurityDetails(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 (HighchartsConnectors.Shared.Morningstar.isPostmanEnvironmentJSON(fileJSON)) {
+ break;
+ }
+ } catch (error) {
+ // fail silently
+ }
+ }
+
+ return fileJSON;
+}
diff --git a/demos/index.html b/demos/index.html
index 836ac72..0131f9a 100644
--- a/demos/index.html
+++ b/demos/index.html
@@ -8,6 +8,7 @@
Morningstar Connectors Demos
- Highcharts Dashboards + Morningstar RNA News
+ - Highcharts Dashboards + Morningstar XRay Connector
- Highcharts Stock + Morningstar TimeSeries
- Highcharts Stock + Morningstar OHLCV TimeSeries
- Highcharts Stock + Morningstar Security Details
diff --git a/src/Shared/MorningstarOptions.ts b/src/Shared/MorningstarOptions.ts
index 22b58a1..831e418 100644
--- a/src/Shared/MorningstarOptions.ts
+++ b/src/Shared/MorningstarOptions.ts
@@ -210,6 +210,16 @@ interface MorningstarSecurityOptionsGeneric {
*/
type?: (string|MorningstarSecurityType);
+ /**
+ * Type of holding.
+ */
+ holdingType?: string;
+
+ /**
+ * Weight of holding.
+ */
+ weight?: (number|string);
+
}
diff --git a/src/XRay/XRayConnector.ts b/src/XRay/XRayConnector.ts
index 52a8bfa..d33f0e7 100644
--- a/src/XRay/XRayConnector.ts
+++ b/src/XRay/XRayConnector.ts
@@ -47,7 +47,7 @@ interface XRayHoldingObject {
amount?: string;
identifier: string;
identifierType: string;
- holdingType: number;
+ holdingType: string|number;
name?: string;
securityType?: string;
weight?: string;
@@ -78,7 +78,7 @@ function convertHoldings (
const holding: XRayHoldingObject = {
identifier: security.id,
identifierType: security.idType,
- holdingType
+ holdingType: security.holdingType || holdingType
};
if (security.name) {
@@ -89,6 +89,10 @@ function convertHoldings (
holding.securityType = security.type;
}
+ if (security.weight) {
+ holding.weight = security.weight.toString();
+ }
+
return holding;
});
}
diff --git a/src/XRay/XRayConverter.ts b/src/XRay/XRayConverter.ts
index 5441749..219a965 100644
--- a/src/XRay/XRayConverter.ts
+++ b/src/XRay/XRayConverter.ts
@@ -98,6 +98,9 @@ export class XRayConverter extends MorningstarConverter {
if (xray.breakdowns) {
this.parseBreakdowns(benchmarkId, xray.breakdowns);
}
+ if (xray.riskStatistics) {
+ this.parseRiskStatistics(benchmarkId, xray.riskStatistics);
+ }
}
}
@@ -125,28 +128,90 @@ export class XRayConverter extends MorningstarConverter {
): void {
const table = this.table;
- for (const asset of json.assetAllocation) {
- const rowId = `${benchmarkId}_${asset.type}_${asset.salePosition}`;
- const values = asset.values;
+ if (json.assetAllocation) {
+ for (const asset of json.assetAllocation) {
+ const columnName = `${benchmarkId}_${asset.type}_${asset.salePosition}`;
+ table.setColumn(`${columnName}_Categories`);
+ table.setColumn(`${columnName}_Values`);
+ const values = asset.values;
+
+ const valueIndex = Object.keys(values);
- for (let i = 1; i < 100; ++i) {
- table.setCell(rowId, i - 1, values[i]);
+ for (let i = 0; i < valueIndex.length; i++) {
+ table.setCell(`${columnName}_Categories`, i, valueIndex[i]);
+ table.setCell(`${columnName}_Values`, i, values[parseInt(valueIndex[i])]);
+ }
}
}
if (json.regionalExposure) {
for (const exposure of json.regionalExposure) {
- const rowId = `${benchmarkId}_RegionalExposure_${exposure.salePosition}`;
+ const columnName = `${benchmarkId}_RegionalExposure_${exposure.salePosition}`;
+ table.setColumn(`${columnName}_Categories`);
+ table.setColumn(`${columnName}_Values`);
const values = exposure.values;
+ const valueIndex = Object.keys(values);
+
+ for (let i = 0; i < valueIndex.length; i++) {
+ table.setCell(`${columnName}_Categories`, i, valueIndex[i]);
+ table.setCell(`${columnName}_Values`, i, values[parseInt(valueIndex[i])]);
+ }
+ }
+ }
+
+ if (json.globalStockSector) {
+ for (const sector of json.globalStockSector) {
+ const columnName = `${benchmarkId}_GlobalStockSector_${sector.salePosition}`;
+ table.setColumn(`${columnName}_Categories`);
+ table.setColumn(`${columnName}_Values`);
+ const values = sector.values;
+ const valueIndex = Object.keys(values);
+
+ for (let i = 0; i < valueIndex.length; i++) {
+ table.setCell(`${columnName}_Categories`, i, valueIndex[i]);
+ table.setCell(`${columnName}_Values`, i, values[parseInt(valueIndex[i])]);
+ }
+ }
+ }
- for (let i = 1; i < 100; ++i) {
- table.setCell(rowId, i - 1, values[i] || 0);
+ if (json.styleBox) {
+ for (const styleBox of json.styleBox) {
+ const columnName = `${benchmarkId}_StyleBox_${styleBox.salePosition}`;
+ table.setColumn(`${columnName}_Categories`);
+ table.setColumn(`${columnName}_Values`);
+ const values = styleBox.values;
+ const valueIndex = Object.keys(values);
+
+ for (let i = 0; i < valueIndex.length; i++) {
+ table.setCell(`${columnName}_Categories`, i, valueIndex[i]);
+ table.setCell(`${columnName}_Values`, i, values[parseInt(valueIndex[i])]);
}
}
}
}
+ protected parseRiskStatistics (
+ benchmarkId: string,
+ json: XRayJSON.RiskStatistics
+ ): void {
+ const table = this.table;
+
+ if (json.sharpeRatio) {
+ for (const sharpeRatio of json.sharpeRatio) {
+ const columnName = `${benchmarkId}_SharpeRatio_${sharpeRatio.frequency}_${sharpeRatio.timePeriod}`;
+ table.setCell(columnName, 0, sharpeRatio.value);
+ }
+ }
+
+ if (json.standardDeviation) {
+ for (const standardDeviation of json.standardDeviation) {
+ const columnName = `${benchmarkId}_StandardDeviation_${standardDeviation.frequency}_${standardDeviation.timePeriod}`;
+ table.setCell(columnName, 0, standardDeviation.value);
+ }
+ }
+ }
+
protected parseHistoricalPerformance (
benchmarkId: string,
json: Array
diff --git a/src/XRay/XRayJSON.ts b/src/XRay/XRayJSON.ts
index 45b8c81..1dc4499 100644
--- a/src/XRay/XRayJSON.ts
+++ b/src/XRay/XRayJSON.ts
@@ -43,12 +43,15 @@ namespace XRayJSON {
breakdowns?: Breakdowns;
historicalPerformanceSeries?: Array;
trailingPerformance?: Array;
+ riskStatistics?: RiskStatistics;
}
export interface Breakdowns {
assetAllocation: Array;
regionalExposure?: Array;
+ globalStockSector?: Array;
+ styleBox?: Array;
}
@@ -60,6 +63,20 @@ namespace XRayJSON {
timePeriod: string;
}
+ export interface RiskStatistics {
+ currencyId: string;
+ endDate: string;
+ sharpeRatio?: Array;
+ standardDeviation?: Array;
+ type: string;
+ }
+
+ export interface RiskStatisticsReturn {
+ frequency: string;
+ timePeriod: string;
+ value: number;
+ }
+
export type HistoricalReturn = [
date: string,
@@ -72,6 +89,11 @@ namespace XRayJSON {
values: Record;
}
+ export interface GlobalStockSector {
+ salePosition: string;
+ values: Record;
+ }
+
export interface Response {
XRay: Array;
@@ -84,6 +106,11 @@ namespace XRayJSON {
statusDescription: string;
}
+ export interface StyleBox {
+ salePosition: string;
+ values: Record;
+ }
+
export interface TrailingPerformance {
currencyId: string;
@@ -127,6 +154,28 @@ namespace XRayJSON {
);
}
+ function isRegionalExposure (
+ json?: unknown
+ ): json is RegionalExposure {
+ return (
+ !!json &&
+ typeof json === 'object' &&
+ typeof (json as RegionalExposure).salePosition === 'string' &&
+ typeof (json as RegionalExposure).values === 'object'
+ );
+ }
+
+ function isGlobalStockSector (
+ json?: unknown
+ ): json is GlobalStockSector {
+ return (
+ !!json &&
+ typeof json === 'object' &&
+ typeof (json as GlobalStockSector).salePosition === 'string' &&
+ typeof (json as GlobalStockSector).values === 'object'
+ );
+ }
+
function isBenchmark (
json?: unknown
@@ -149,6 +198,17 @@ namespace XRayJSON {
);
}
+ function isStyleBox (
+ json?: unknown
+ ): json is StyleBox {
+ return (
+ !!json &&
+ typeof json === 'object' &&
+ typeof (json as StyleBox).salePosition === 'string' &&
+ typeof (json as StyleBox).values === 'object'
+ );
+ }
+
function isBreakdowns (
json?: unknown
@@ -156,14 +216,20 @@ namespace XRayJSON {
return (
!!json &&
typeof json === 'object' &&
+
(json as Breakdowns).assetAllocation instanceof Array &&
(
(json as Breakdowns).assetAllocation.length === 0 ||
isAssetAllocation((json as Breakdowns).assetAllocation[0])
- ) &&
- (
- typeof (json as Breakdowns).regionalExposure === 'undefined' ||
- isRegionalExposure((json as Breakdowns).regionalExposure)
+ ) || (
+ !(json as Breakdowns).globalStockSector ||
+ isGlobalStockSector((json as Breakdowns).globalStockSector?.[0])
+ ) || (
+ !(json as Breakdowns).regionalExposure ||
+ isRegionalExposure((json as Breakdowns).regionalExposure?.[0])
+ ) || (
+ !(json as Breakdowns).styleBox ||
+ isStyleBox((json as Breakdowns).styleBox?.[0])
)
);
}
@@ -221,19 +287,6 @@ namespace XRayJSON {
);
}
-
- function isRegionalExposure (
- json?: unknown
- ): json is RegionalExposure {
- return (
- !!json &&
- typeof json === 'object' &&
- typeof (json as RegionalExposure).salePosition === 'string' &&
- typeof (json as RegionalExposure).values === 'object'
- );
- }
-
-
function isStatus (
json?: unknown
): json is Status {
diff --git a/src/XRay/XRayOptions.ts b/src/XRay/XRayOptions.ts
index d6c9bf6..bcc932e 100644
--- a/src/XRay/XRayOptions.ts
+++ b/src/XRay/XRayOptions.ts
@@ -244,6 +244,7 @@ export type XRayPortfolioDataPoints = (
| 'RegionalExposure'
| 'RSquared'
| 'SharpeRatio'
+ | ['SharpeRatio', ...Array]
| 'Skewness'
| 'SortinoRatio'
| 'SRRI'
diff --git a/tests/XRay/Breakdown.test.ts b/tests/XRay/Breakdown.test.ts
index d4234c9..9ddb6da 100644
--- a/tests/XRay/Breakdown.test.ts
+++ b/tests/XRay/Breakdown.test.ts
@@ -59,3 +59,50 @@ export async function breakdownLoad (
);
}
+
+export async function portfolioBreakdown (
+ api: MC.Shared.MorningstarAPIOptions
+) {
+ const connector = new MC.XRayConnector({
+ api,
+ currencyId: 'GBP',
+ dataPoints: {
+ type: 'portfolio',
+ dataPoints: [
+ 'GlobalStockSector',
+ 'RegionalExposure',
+ 'StyleBox'
+ ]
+ },
+ holdings: [
+ {
+ id: 'F0GBR052QA',
+ idType: 'MSID',
+ type: 'FO',
+ weight: '100',
+ name: 'BlackRock Income and Growth Ord',
+ holdingType: 'weight'
+ }
+ ]
+ });
+
+ await connector.load();
+
+ Assert.deepStrictEqual(
+ connector.table.getColumnNames(),
+ [
+ 'XRay_RegionalExposure_N_Categories',
+ 'XRay_RegionalExposure_N_Values',
+ 'XRay_GlobalStockSector_N_Categories',
+ 'XRay_GlobalStockSector_N_Values',
+ 'XRay_StyleBox_N_Categories',
+ 'XRay_StyleBox_N_Values'
+ ],
+ 'Connector columns should return expected names.'
+ );
+
+ Assert.ok(
+ connector.table.getRowCount() > 0,
+ 'Connector should not return empty rows.'
+ );
+}
diff --git a/tests/XRay/RiskStatistics.test.ts b/tests/XRay/RiskStatistics.test.ts
new file mode 100644
index 0000000..792c53b
--- /dev/null
+++ b/tests/XRay/RiskStatistics.test.ts
@@ -0,0 +1,44 @@
+import * as Assert from 'node:assert/strict';
+import * as MC from '../../code/connectors-morningstar.src';
+
+export async function portfolioBreakdown (
+ api: MC.Shared.MorningstarAPIOptions
+) {
+ const connector = new MC.XRayConnector({
+ api,
+ currencyId: 'GBP',
+ dataPoints: {
+ type: 'portfolio',
+ dataPoints: [
+ ['StandardDeviation', 'M', 'M36'],
+ ['SharpeRatio', 'M', 'M36']
+ ]
+ },
+ holdings: [
+ {
+ id: 'F0GBR052QA',
+ idType: 'MSID',
+ type: 'FO',
+ weight: '100',
+ name: 'BlackRock Income and Growth Ord',
+ holdingType: 'weight'
+ }
+ ]
+ });
+
+ await connector.load();
+
+ Assert.deepStrictEqual(
+ connector.table.getColumnNames(),
+ [
+ 'XRay_SharpeRatio_M_M36',
+ 'XRay_StandardDeviation_M_M36'
+ ],
+ 'Connector columns should return expected names.'
+ );
+
+ Assert.ok(
+ connector.table.getRowCount() > 0,
+ 'Connector should not return empty rows.'
+ );
+}