diff --git a/docs/connectors/morningstar/x-ray.md b/docs/connectors/morningstar/x-ray.md new file mode 100644 index 0000000..bca3a2a --- /dev/null +++ b/docs/connectors/morningstar/x-ray.md @@ -0,0 +1,53 @@ +X-Ray Connector +=============== + +The Morningstar X-Ray capability enables you to quickly analyze a portfolio's +holdings. You define the portfolio individually in the connector options. + +The X-Ray Connector aggregates individual holdings data with the help of the +Morningstar API. Data returned by the Highcharts Connector shows how a portfolio +is diversified by region, sector, and investment style. + + + +How to use X-Ray +---------------- + +You can use the X-Ray Connector to fetch portfolio data points, holding data +points, or benchmark data points. Depending on the request additional breakdown +columns might be added to the table. + +In order to fetch a benchmark, you can for example + +```js +const xRayConnector = MC.XRayConnector({ + postman: { + environmentJSON: postmanJSON + }, + dataPoints: { + type: 'benchmark', + dataPoints: [ + 'HistoricalPerformanceSeries', + ['PerformanceReturn', 'M0', 'M1', 'M2', 'M3', 'M6', 'M12'], + 'ShowBreakdown' + ] + }, + holdings: [ + { + id: 'GB00BWDBJF10', + idType: 'ISIN', + weight: 100 + } + ] +}); +``` + +For more details, see [Morningstar's X-Ray API]. + + + + + + + +[Morningstar's X-Ray API]: https://developer.morningstar.com/direct-web-services/documentation/api-reference/portfolio-analysis-apacemea/x-ray diff --git a/src/RNANews/RNANewsConnector.ts b/src/RNANews/RNANewsConnector.ts index de9d586..31fa050 100644 --- a/src/RNANews/RNANewsConnector.ts +++ b/src/RNANews/RNANewsConnector.ts @@ -157,7 +157,7 @@ export class RNANewsConnector extends MorningstarConnector { } const api = this.api = this.api || new MorningstarAPI(options.api); - const url = new MorningstarURL('/ecint/v1/timeseries/rna-news', api.baseURL); + const url = new MorningstarURL('ecint/v1/timeseries/rna-news', api.baseURL); url.setSecuritiesOptions([security]); diff --git a/src/Shared/MorningstarAPI.ts b/src/Shared/MorningstarAPI.ts index 3ea9a9c..b161da4 100644 --- a/src/Shared/MorningstarAPI.ts +++ b/src/Shared/MorningstarAPI.ts @@ -58,8 +58,6 @@ export class MorningstarAPI { window.location.href ); - this.baseURL.pathname = '/ecint/v1/'; - if (this.baseURL.protocol !== 'https:') { throw new Error('Insecure API protocol'); } diff --git a/src/Shared/MorningstarAccess.ts b/src/Shared/MorningstarAccess.ts index 7783006..d0501ad 100644 --- a/src/Shared/MorningstarAccess.ts +++ b/src/Shared/MorningstarAccess.ts @@ -127,7 +127,7 @@ export class MorningstarAccess { this.url = ( options.url ? new URL(options.url, window.location.href) : - new URL('/token/oauth', MorningstarRegion.baseURLs[MorningstarRegion.detect()]) + new URL('token/oauth', MorningstarRegion.baseURLs[MorningstarRegion.detect()]) ); if (this.url.protocol !== 'https:') { diff --git a/src/Shared/MorningstarOptions.ts b/src/Shared/MorningstarOptions.ts index 8eabb51..a2979ca 100644 --- a/src/Shared/MorningstarOptions.ts +++ b/src/Shared/MorningstarOptions.ts @@ -106,21 +106,11 @@ export interface MorningstarHoldingAmountOptions extends MorningstarSecurityOpti */ amount: number; - /** - * Name of holding. - */ - name?: string; - } export interface MorningstarHoldingWeightOptions extends MorningstarSecurityOptions { - /** - * Name of holding. - */ - name?: string; - /** * Holding weight. */ @@ -131,11 +121,6 @@ export interface MorningstarHoldingWeightOptions extends MorningstarSecurityOpti export interface MorningstarHoldingValueOptions extends MorningstarSecurityOptions { - /** - * Name of holding. - */ - name?: string; - /** * Holding value. */ @@ -196,6 +181,11 @@ export interface MorningstarSecurityOptions { */ idType: string; + /** + * Name of the security. + */ + name?: string; + /** * Type of security. */ @@ -212,22 +202,22 @@ export interface MorningstarSecurityOptions { export enum MorningstarSecurityType { - 'Bond' = 'BD', - '529 Portfolio' = 'CT', - 'Cash' = 'CASH', - 'Category Average' = 'CA', - 'Closed-End Fund' = 'FC', - 'Economics Series' = 'EI', - 'Exchange-Traded Fund' = 'FE', - 'Index' = 'XI', - 'Insurance Product Fund' = 'FV', - 'Money Market Fund' = 'FM', - 'Open-End Fund' = 'FO', - 'Separate Account' = 'SA', - 'Stock' = 'ST', - 'UK LP SubAccounts' = 'VA', - 'Unit Investment Trust' = 'FI', - 'Variable Annuity' = 'V1' + Bond = 'BD', + Cash = 'CASH', + CategoryAverage = 'CA', + ClosedEndFund = 'FC', + EconomicsSeries = 'EI', + ExchangeTradedFund = 'FE', + Index = 'XI', + InsuranceProductFund = 'FV', + MoneyMarketFund = 'FM', + OpenEndFund = 'FO', + Portfolio529 = 'CT', + SeparateAccount = 'SA', + Stock = 'ST', + UKLPSubAccounts = 'VA', + UnitInvestmentTrust = 'FI', + VariableAnnuity = 'V1' } diff --git a/src/Shared/MorningstarPostman.ts b/src/Shared/MorningstarPostman.ts index e5756c5..fed78ba 100644 --- a/src/Shared/MorningstarPostman.ts +++ b/src/Shared/MorningstarPostman.ts @@ -80,6 +80,9 @@ export namespace MorningstarPostman { password: password.value, username: username.value }; + if (url) { + apiOptions.access.url = `https://${url.value}`; + } } if (url) { diff --git a/src/TimeSeries/Converters/index.ts b/src/TimeSeries/Converters/index.ts index 9396124..aded649 100644 --- a/src/TimeSeries/Converters/index.ts +++ b/src/TimeSeries/Converters/index.ts @@ -25,9 +25,9 @@ import CumulativeReturnSeriesConverter from './CumulativeReturnSeriesConverter'; import DividendSeriesConverter from './DividendSeriesConverter'; import GrowthSeriesConverter from './GrowthSeriesConverter'; +import OHLCVSeriesConverter from './OHLCVSeriesConverter'; import PriceSeriesConverter from './PriceSeriesConverter'; import RatingSeriesConverter from './RatingSeriesConverter'; -import OHLCVSeriesConverter from './OHLCVSeriesConverter'; /* * @@ -40,9 +40,9 @@ import OHLCVSeriesConverter from './OHLCVSeriesConverter'; export * from './CumulativeReturnSeriesConverter'; export * from './DividendSeriesConverter'; export * from './GrowthSeriesConverter'; +export * from './OHLCVSeriesConverter'; export * from './PriceSeriesConverter'; export * from './RatingSeriesConverter'; -export * from './OHLCVSeriesConverter'; /* * @@ -56,7 +56,7 @@ export default { CumulativeReturnSeriesConverter, DividendSeriesConverter, GrowthSeriesConverter, + OHLCVSeriesConverter, PriceSeriesConverter, - RatingSeriesConverter, - OHLCVSeriesConverter + RatingSeriesConverter }; diff --git a/src/TimeSeries/TimeSeriesConnector.ts b/src/TimeSeries/TimeSeriesConnector.ts index 0c49f7b..35a5af0 100644 --- a/src/TimeSeries/TimeSeriesConnector.ts +++ b/src/TimeSeries/TimeSeriesConnector.ts @@ -22,18 +22,13 @@ * */ +import Converters from './Converters'; import External from '../Shared/External'; -import GrowthSeriesConverter from './Converters/GrowthSeriesConverter'; import MorningstarAPI from '../Shared/MorningstarAPI'; import MorningstarConnector from '../Shared/MorningstarConnector'; import MorningstarURL from '../Shared/MorningstarURL'; -import CumulativeReturnSeriesConverter from './Converters/CumulativeReturnSeriesConverter'; -import DividendSeriesConverter from './Converters/DividendSeriesConverter'; -import TimeSeriesOptions, { OHLCVSeriesOptions } from './TimeSeriesOptions'; -import TimeSeriesRatingConverter from './Converters/RatingSeriesConverter'; -import PriceSeriesConverter from './Converters/PriceSeriesConverter'; import TimeSeriesConverter from './TimeSeriesConverter'; -import { OHLCVSeriesConverter } from './Converters'; +import TimeSeriesOptions from './TimeSeriesOptions'; /* * @@ -54,54 +49,52 @@ export class TimeSeriesConnector extends MorningstarConnector { public constructor ( - options: TimeSeriesOptions + options: TimeSeriesOptions = {} ) { super(options); switch (options.series?.type) { case 'CumulativeReturn': - this.converter = new CumulativeReturnSeriesConverter({ + this.converter = new Converters.CumulativeReturnSeriesConverter({ ...options.converter, ...options.series }); break; case 'Dividend': - this.converter = new DividendSeriesConverter({ + this.converter = new Converters.DividendSeriesConverter({ ...options.converter, ...options.series }); break; case 'Growth': - this.converter = new GrowthSeriesConverter({ + this.converter = new Converters.GrowthSeriesConverter({ ...options.converter, ...options.series }); break; case 'Rating': - this.converter = new TimeSeriesRatingConverter({ + this.converter = new Converters.RatingSeriesConverter({ ...options.converter, ...options.series }); break; case 'Price': - this.converter = new PriceSeriesConverter({ + this.converter = new Converters.PriceSeriesConverter({ ...options.converter, ...options.series }); break; case 'OHLCV': - this.converter = new OHLCVSeriesConverter({ + this.converter = new Converters.OHLCVSeriesConverter({ ...options.converter, ...options.series, - securities: options.securities, - replaceZeroWithCloseValue: (options as OHLCVSeriesOptions) - .replaceZeroWithCloseValue + securities: options.securities }); break; @@ -136,6 +129,7 @@ export class TimeSeriesConnector extends MorningstarConnector { public override async load (): Promise { + await super.load(); const options = this.options; @@ -151,7 +145,7 @@ export class TimeSeriesConnector extends MorningstarConnector { } const api = this.api = this.api || new MorningstarAPI(options.api); - const url = new MorningstarURL('/ecint/v1/' + this.converter.path, api.baseURL); + const url = new MorningstarURL('ecint/v1/' + this.converter.path, api.baseURL); url.setSecuritiesOptions(securities); diff --git a/src/TimeSeries/TimeSeriesJSON.ts b/src/TimeSeries/TimeSeriesJSON.ts index 88514c5..fa36058 100644 --- a/src/TimeSeries/TimeSeriesJSON.ts +++ b/src/TimeSeries/TimeSeriesJSON.ts @@ -32,6 +32,24 @@ namespace TimeSeriesJSON { * */ + export type CompactHistoryDetail = ( + | CompactHistoryOHLCV + ); + + + export type CompactJSON = Array; + + + export type CompactHistoryOHLCV = [ + date: number, + open: number, + high: number, + low: number, + close: number, + volume: number + ]; + + export interface CurrencyHistory { HistoryDetail: Array; } @@ -78,20 +96,6 @@ namespace TimeSeriesJSON { Security: Array; } - export type CompactJSON = Array; - - export type CompactHistoryDetail = - | CompactHistoryOHLCV; - - export type CompactHistoryOHLCV = [ - date: number, - open: number, - high: number, - low: number, - close: number, - volume: number - ]; - /* * * @@ -100,6 +104,40 @@ namespace TimeSeriesJSON { * */ + export function isCompactJSONOHLCVResponse ( + json?: unknown + ): json is CompactJSON { + return ( + isCompactJSONResponse(json) && + ( + json.length === 0 || + isCompactHistoryOHLCV(json[0]) + ) + ); + } + + + function isCompactJSONResponse ( + json?: unknown + ): json is CompactJSON { + return ( + !!json && + json instanceof Array + ); + } + + + function isCompactHistoryOHLCV ( + json?: unknown + ): json is CompactHistoryOHLCV { + return ( + !!json && + Array.isArray(json) && + json.length === 6 + ); + } + + export function isHistory ( json?: unknown ): json is History { @@ -182,37 +220,6 @@ namespace TimeSeriesJSON { ); } - export function isCompactJSONOHLCVResponse ( - json?: unknown - ): json is CompactJSON { - return ( - isCompactJSONResponse(json) && - ( - json.length === 0 || - isCompactHistoryOHLCV(json[0]) - ) - ); - } - - function isCompactJSONResponse ( - json?: unknown - ): json is CompactJSON { - return ( - !!json && - Array.isArray(json) - ); - } - - function isCompactHistoryOHLCV ( - json?: unknown - ): json is CompactHistoryOHLCV { - return ( - !!json && - Array.isArray(json) && - json.length === 6 - ); - } - } diff --git a/src/TimeSeries/TimeSeriesOptions.ts b/src/TimeSeries/TimeSeriesOptions.ts index 380aedf..be64d98 100644 --- a/src/TimeSeries/TimeSeriesOptions.ts +++ b/src/TimeSeries/TimeSeriesOptions.ts @@ -175,7 +175,7 @@ export interface TimeSeriesOptions extends MorningstarOptions { localization?: LocalizationOptions; /** - * Security to retrieve. + * Securities to retrieve. * * **NOTE: When series type is `OHLCV`, only one security is supported.** */ diff --git a/src/TimeSeries/index.ts b/src/TimeSeries/index.ts index 35935dc..9d9adbc 100644 --- a/src/TimeSeries/index.ts +++ b/src/TimeSeries/index.ts @@ -36,7 +36,6 @@ export * from './TimeSeriesConnector'; export * from './TimeSeriesConverter'; export * as TimeSeriesConverters from './Converters/index'; export * from './TimeSeriesOptions'; -export * from './TimeSeriesConverter'; /* * diff --git a/src/XRay/XRayConnector.ts b/src/XRay/XRayConnector.ts new file mode 100644 index 0000000..52a8bfa --- /dev/null +++ b/src/XRay/XRayConnector.ts @@ -0,0 +1,300 @@ +/* * + * + * (c) 2009-2024 Highsoft AS + * + * License: www.highcharts.com/license + * + * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! + * + * Authors: + * - Sophie Bremer + * + * */ + + +'use strict'; + + +/* * + * + * Imports + * + * */ + + +import External from '../Shared/External'; +import MorningstarAPI from '../Shared/MorningstarAPI'; +import MorningstarConnector from '../Shared/MorningstarConnector'; +import { + isMorningstarHoldingAmountOptions, + isMorningstarHoldingWeightOptions, + MorningstarHoldingAmountOptions, + MorningstarHoldingWeightOptions +} from '../Shared/MorningstarOptions'; +import MorningstarURL from '../Shared/MorningstarURL'; +import XRayConverter from './XRayConverter'; +import XRayOptions from './XRayOptions'; + + +/* * + * + * Declarations + * + * */ + + +interface XRayHoldingObject { + amount?: string; + identifier: string; + identifierType: string; + holdingType: number; + name?: string; + securityType?: string; + weight?: string; +} + + +interface XRayPortfolioObject { + benchmarkId?: string; + currencyId?: string; + holdings?: Array; + /** 1: Value / 2: Weight / 3: Amount */ + type?: (2|3); +} + + +/* * + * + * Functions + * + * */ + + +function convertHoldings ( + holdings: (Array|Array), + holdingType: number +): Array { + return holdings.map(security => { + const holding: XRayHoldingObject = { + identifier: security.id, + identifierType: security.idType, + holdingType + }; + + if (security.name) { + holding.name = security.name; + } + + if (security.type) { + holding.securityType = security.type; + } + + return holding; + }); +} + +function escapeDataPoints ( + dataPoints: Array<(string|Array)> +): string { + + dataPoints = dataPoints.map(dataPoint => ( + dataPoint instanceof Array ? + dataPoint.join('|') : + dataPoint + )); + + if (!dataPoints.length) { + return ''; + } + + return `UseMongoSecurities,RunInThread,${dataPoints.join(',')}`; +} + + +/* * + * + * Class + * + * */ + + +export class XRayConnector extends MorningstarConnector { + + + /* * + * + * Constructors + * + * */ + + + public constructor ( + options: XRayOptions = {} + ) { + super(options); + + this.converter = new XRayConverter(options?.converter); + this.metadata = { columns: {} }; + this.options = options; + } + + + /* * + * + * Properties + * + * */ + + + public override readonly converter: XRayConverter; + + + public override readonly metadata: XRayConnector.MetaData; + + + public override readonly options: XRayOptions; + + + /* * + * + * Functions + * + * */ + + + public override async load (): Promise { + + await super.load(); + + const options = this.options; + const dataPoints = options.dataPoints; + const holdings = options.holdings || []; + + if (!dataPoints || !holdings.length) { + return this; + } + + const amountHoldings: Array = []; + const weightHoldings: Array = []; + + for (const holding of holdings) { + if (isMorningstarHoldingAmountOptions(holding)) { + amountHoldings.push(holding); + } + if (isMorningstarHoldingWeightOptions(holding)) { + weightHoldings.push(holding); + } + } + + const bodyJSON: XRayPortfolioObject = { + benchmarkId: (options.benchmarkId || 'XIUSA0010V'), + currencyId: options.currencyId + }; + + if (amountHoldings.length > weightHoldings.length) { + bodyJSON.type = 3; + bodyJSON.holdings = convertHoldings(amountHoldings, bodyJSON.type); + } else { + bodyJSON.type = 2; + bodyJSON.holdings = convertHoldings(weightHoldings, bodyJSON.type); + } + + this.metadata.benchmarkId = bodyJSON.benchmarkId; + + const api = this.api = this.api || new MorningstarAPI(options.api); + const url = new MorningstarURL('ecint/v1/xray/json', api.baseURL); + + switch (dataPoints.type) { + case 'benchmark': + url.searchParams.set( + 'benchmarkDataPoints', + escapeDataPoints(dataPoints.dataPoints) + ); + break; + case 'holding': + url.searchParams.set( + 'holdingDataPoints', + escapeDataPoints(dataPoints.dataPoints) + ); + break; + case 'portfolio': + url.searchParams.set( + 'portfolioDataPoints', + escapeDataPoints(dataPoints.dataPoints) + ); + break; + } + + const response = await api.fetch(url, { + body: JSON.stringify(JSON.stringify(bodyJSON)), + headers: { + 'Content-Type': 'application/json' + }, + method: 'POST' + }); + const json = await response.json() as unknown; + + this.converter.parse({ json }); + + this.table.deleteColumns(); + this.table.setColumns(this.converter.getTable().getColumns()); + + return this; + } + + +} + + +/* * + * + * Class Namespace + * + * */ + + +export namespace XRayConnector { + + + /* * + * + * Declarations + * + * */ + + + export interface MetaData { + benchmarkId?: string; + /** @internal */ + columns: Record; + } + + +} + + +/* * + * + * Registry + * + * */ + + +declare module '@highcharts/dashboards/es-modules/Data/Connectors/DataConnectorType' { + interface DataConnectorTypes { + MorningstarXRay: typeof XRayConnector + } +} + + +External.DataConnector.registerType('MorningstarXRay', XRayConnector); + + +/* * + * + * Default Export + * + * */ + + +export default XRayConnector; diff --git a/src/XRay/XRayConverter.ts b/src/XRay/XRayConverter.ts new file mode 100644 index 0000000..5441749 --- /dev/null +++ b/src/XRay/XRayConverter.ts @@ -0,0 +1,203 @@ +/* * + * + * (c) 2009-2024 Highsoft AS + * + * License: www.highcharts.com/license + * + * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! + * + * Authors: + * - Sophie Bremer + * + * */ + + +'use strict'; + + +/* * + * + * Imports + * + * */ + + +import MorningstarConverter from '../Shared/MorningstarConverter'; +import { XRayConverterOptions } from './XRayOptions'; +import XRayJSON from './XRayJSON'; + + +/* * + * + * Classes + * + * */ + + +export class XRayConverter extends MorningstarConverter { + + + /* * + * + * Constructor + * + * */ + + + public constructor ( + options?: XRayConverterOptions + ) { + super(options); + } + + + /* * + * + * Functions + * + * */ + + + public parse ( + options: XRayConverterOptions + ): void { + const table = this.table; + const userOptions = { + ...this.options, + ...options + }; + const json = userOptions.json; + const xrays: Array = []; + + // Validate JSON + + if (XRayJSON.isResponse(json)) { + xrays.push(...json.XRay); + } else if (XRayJSON.isXRayResponse(json)) { + xrays.push(json); + } else { + throw new Error('Invalid data'); + } + + // Reset table + + table.deleteColumns(); + + for (const xray of xrays) { + const benchmarkId = xray.benchmarkId || 'XRay'; + + if (xray.benchmark) { + this.parseBenchmark(benchmarkId, xray.benchmark); + } + if (xray.historicalPerformanceSeries) { + this.parseHistoricalPerformance(benchmarkId, xray.historicalPerformanceSeries); + } + if (xray.trailingPerformance) { + this.parseTrailingPerformance(benchmarkId, xray.trailingPerformance); + } + if (xray.breakdowns) { + this.parseBreakdowns(benchmarkId, xray.breakdowns); + } + } + + } + + protected parseBenchmark ( + benchmarkId: string, + json: Array + ): void { + for (const benchmark of json) { + if (benchmark.breakdowns) { + this.parseBreakdowns(benchmarkId, benchmark.breakdowns); + } + if (benchmark.historicalPerformanceSeries) { + this.parseHistoricalPerformance(benchmarkId, benchmark.historicalPerformanceSeries); + } + if (benchmark.trailingPerformance) { + this.parseTrailingPerformance(benchmarkId, benchmark.trailingPerformance); + } + } + } + + protected parseBreakdowns ( + benchmarkId: string, + json: XRayJSON.Breakdowns + ): void { + const table = this.table; + + for (const asset of json.assetAllocation) { + const rowId = `${benchmarkId}_${asset.type}_${asset.salePosition}`; + const values = asset.values; + + for (let i = 1; i < 100; ++i) { + table.setCell(rowId, i - 1, values[i]); + } + } + + if (json.regionalExposure) { + for (const exposure of json.regionalExposure) { + const rowId = `${benchmarkId}_RegionalExposure_${exposure.salePosition}`; + const values = exposure.values; + + for (let i = 1; i < 100; ++i) { + table.setCell(rowId, i - 1, values[i] || 0); + } + } + } + + } + + protected parseHistoricalPerformance ( + benchmarkId: string, + json: Array + ): void { + const table = this.table; + + for (const historicalPerformance of json) { + const periodRowId = `${benchmarkId}_${historicalPerformance.returnType}_TimePeriod`; + const valueRowId = `${benchmarkId}_${historicalPerformance.returnType}_Value`; + + let rowIndex = 0; + + for (const historicalReturn of historicalPerformance.returns) { + table.setCell(periodRowId, rowIndex, historicalReturn[0]); + table.setCell(valueRowId, rowIndex, historicalReturn[1]); + ++rowIndex; + } + } + } + + protected parseTrailingPerformance ( + benchmarkId: string, + json: Array + ): void { + const table = this.table; + + for (const trailingPerformance of json) { + const periodRowId = `${benchmarkId}_${trailingPerformance.type}_TimePeriod`; + const valueRowId = `${benchmarkId}_${trailingPerformance.type}_Value`; + + let rowIndex = 0; + + for (const trailingReturn of trailingPerformance.returns) { + table.setCell(periodRowId, rowIndex, trailingReturn.timePeriod); + table.setCell(valueRowId, rowIndex, trailingReturn.value); + ++rowIndex; + } + } + + } + + + +} + + +/* * + * + * Default Export + * + * */ + + +export default XRayConverter; diff --git a/src/XRay/XRayJSON.ts b/src/XRay/XRayJSON.ts new file mode 100644 index 0000000..45b8c81 --- /dev/null +++ b/src/XRay/XRayJSON.ts @@ -0,0 +1,283 @@ +/* * + * + * (c) 2009-2024 Highsoft AS + * + * License: www.highcharts.com/license + * + * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! + * + * Authors: + * - Sophie Bremer + * + * */ + + +'use strict'; + + +/* * + * + * Namespace + * + * */ + + +namespace XRayJSON { + + + /* * + * + * Declarations + * + * */ + + + export interface AssetAllocation { + type: string; + salePosition: string; + values: Record; + } + + + export interface Benchmark { + breakdowns?: Breakdowns; + historicalPerformanceSeries?: Array; + trailingPerformance?: Array; + } + + + export interface Breakdowns { + assetAllocation: Array; + regionalExposure?: Array; + } + + + export interface HistoricalPerformance { + frequency: string; + returns: Array; + returnType: string; + startDate: string; + timePeriod: string; + } + + + export type HistoricalReturn = [ + date: string, + value: number + ]; + + + export interface RegionalExposure { + salePosition: string; + values: Record; + } + + + export interface Response { + XRay: Array; + } + + + export interface Status { + detailedStatusMessage: string; + statusCode: number; + statusDescription: string; + } + + + export interface TrailingPerformance { + currencyId: string; + endDate: string; + returns: Array; + returnType: string; + type: string; + } + + + export interface TrailingReturn { + timePeriod: string; + value: number; + } + + + export interface XRayResponse extends Benchmark { + benchmark?: Array; + benchmarkId?: string; + currencyId?: string; + status?: Status; + } + + + /* * + * + * Functions + * + * */ + + + function isAssetAllocation ( + json?: unknown + ): json is AssetAllocation { + return ( + !!json && + typeof json === 'object' && + typeof (json as AssetAllocation).salePosition === 'string' && + typeof (json as AssetAllocation).type === 'string' && + typeof (json as AssetAllocation).values === 'object' + ); + } + + + function isBenchmark ( + json?: unknown + ): json is Benchmark { + return ( + !!json && + typeof json === 'object' && + ( + typeof (json as Benchmark).breakdowns === 'undefined' || + isBreakdowns((json as Benchmark).breakdowns) + ) && + ( + typeof (json as Benchmark).historicalPerformanceSeries === 'undefined' || + isHistoricalPerformanceSeries((json as Benchmark).historicalPerformanceSeries) + ) && + ( + typeof (json as Benchmark).trailingPerformance === 'object' || + typeof (json as Benchmark).trailingPerformance === 'undefined' + ) + ); + } + + + function isBreakdowns ( + json?: unknown + ): json is Breakdowns { + 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) + ) + ); + } + + + function isHistoricalPerformance ( + json?: unknown + ): json is HistoricalPerformance { + return ( + !!json && + typeof json === 'object' && + typeof (json as HistoricalPerformance).frequency === 'string' && + typeof (json as HistoricalPerformance).returnType === 'string' && + typeof (json as HistoricalPerformance).startDate === 'string' && + typeof (json as HistoricalPerformance).timePeriod === 'string' && + (json as HistoricalPerformance).returns instanceof Array && + ( + (json as HistoricalPerformance).returns.length === 0 || + isHistoricalReturn((json as HistoricalPerformance).returns[0]) + ) + ); + } + + + function isHistoricalPerformanceSeries ( + json?: unknown + ): json is Array { + return ( + json instanceof Array && + ( + json.length === 0 || + isHistoricalPerformance(json[0]) + ) + ); + } + + + function isHistoricalReturn ( + json?: unknown + ): json is HistoricalReturn { + return ( + json instanceof Array && + typeof json[0] === 'string' && + typeof json[1] === 'number' + ); + } + + export function isResponse ( + json?: unknown + ): json is Response { + return ( + !!json && + typeof json === 'object' && + isXRayResponse((json as Response).XRay) + ); + } + + + 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 { + return ( + !!json && + typeof json === 'object' && + typeof (json as Status).detailedStatusMessage === 'string' && + typeof (json as Status).statusCode === 'number' && + typeof (json as Status).statusDescription === 'string' + ); + } + + + export function isXRayResponse ( + json?: unknown + ): json is XRayResponse { + return ( + !!json && + typeof json === 'object' && + isBenchmark(json as XRayResponse) && + ( + typeof (json as XRayResponse).benchmark === 'undefined' || + isBenchmark((json as XRayResponse).benchmark) + ) && + ( + typeof (json as XRayResponse).breakdowns === 'undefined' || + isBreakdowns((json as XRayResponse).breakdowns) + ) && + ( + typeof (json as XRayResponse).status === 'undefined' || + isStatus((json as XRayResponse).status) + ) + ); + } + + +} + + +/* * + * + * Default Export + * + * */ + + +export default XRayJSON; diff --git a/src/XRay/XRayOptions.ts b/src/XRay/XRayOptions.ts new file mode 100644 index 0000000..d6c9bf6 --- /dev/null +++ b/src/XRay/XRayOptions.ts @@ -0,0 +1,269 @@ +/* * + * + * (c) 2009-2024 Highsoft AS + * + * License: www.highcharts.com/license + * + * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! + * + * Authors: + * - Sophie Bremer + * + * */ + + +'use strict'; + + +/* * + * + * Imports + * + * */ + + +import type { + MorningstarConverterOptions, + MorningstarHoldingAmountOptions, + MorningstarHoldingWeightOptions, + MorningstarOptions +} from '../Shared/MorningstarOptions'; + + +/* * + * + * API Options + * + * */ + + +export interface XRayBenchmarkDataPointOptions { + + /** + * A comma-separated list of benchmark data points to return. + */ + dataPoints: Array; + + /** + * Type of data points. + */ + type: 'benchmark'; + +} + + +export type XRayBenchmarkDataPoints = ( + | 'GrowthSeries' + | 'HistoricalPerformanceSeries' + | 'PerformanceReturn' + | ['PerformanceReturn', ...Array] + | 'ShowBreakdown' +); + + +export interface XRayConverterOptions extends MorningstarConverterOptions { + +} + + +export type XRayDataPointOptions = ( + | XRayBenchmarkDataPointOptions + | XRayHoldingDataPointOptions + | XRayPortfolioDataPointOptions +); + + +export interface XRayHoldingDataPointOptions { + + /** + * A comma-separated list of holding data points to return. + */ + dataPoints: Array; + + /** + * Type of data points. + */ + type: 'holding'; + +} + + +/** + * @todo Consider sub-collections or reduction to `string` + */ +export type XRayHoldingDataPoints = ( + | 'Alpha' + | 'ArithmeticMean' + | 'AveragePrice' + | 'Beta' + | 'CalculatedSRRI' + | 'CalmarRatio' + | 'CouponFrequency' + | 'CouponType' + | 'CurrencyId' + | 'CurrencyName' + | 'DiscountPremium' + | 'EquityWeighting' + | 'EquityWeightingLong' + | 'HoldingId' + | 'HoldingType' + | 'InitialAmount' + | 'InitialWeight' + | 'Kurtosis' + | 'MarketValue' + | 'MorningstarRiskM255' + | 'Name' + | 'NAVCFLC' + | 'NAVCFLE' + | 'NAVCFLP' + | 'NAVEPLC' + | 'NAVEPLE' + | 'NAVEPLP' + | 'OngoingCharge' + | 'PortfolioId' + | 'RiskRewardVolatility' + | 'SecurityId' + | 'SecurityType' + | 'SharpeRatio' + | 'SortinoRatio' + | 'SRRI' + | 'StandardDeviation' + | 'StarRating' + | 'TDYNAV' + | 'UCITS' + | 'Weight' +); + + +export type XRayHoldingOptions = ( + | Array + | Array +); + + +export interface XRayOptions extends MorningstarOptions { + + /** + * Morningstar-defined unique identifier of a benchmark. + * + * If no benchmark is set, it will default to + * `Morningstar US Market TR USD`. + * + * @default 'XIUSA0010V' + */ + benchmarkId?: string; + + /** + * Options related to the x-ray data parser. + */ + converter?: XRayConverterOptions; + + /** + * Currency of the portfolio. ISO 4217 3-character currency code. + */ + currencyId?: string; + + /** + * Data points for the x-ray. + */ + dataPoints?: XRayDataPointOptions; + + /** + * Array of portfolio holdings. + */ + holdings?: XRayHoldingOptions; + +} + + +export interface XRayPortfolioDataPointOptions { + + /** + * A comma-separated list of portfolio data points to return. + */ + dataPoints: Array; + + /** + * Type of data points. + */ + type: 'portfolio'; + +} + + +export type XRayPortfolioDataPoints = ( + // Version 1 + | 'ActualManagementFee' + | 'Alpha' + | 'ArithmeticMean' + | 'AssetAllocation' + | 'BenchmarkId' + | 'BenchmarkName' + | 'BestWorstPeriods' + | 'Beta' + | 'BondStyleBox' + | 'CalmarRatio' + | 'Correlation' + | 'CorrelationMatrix' + | ['CorrelationMatrix', ...Array] + | 'CountryExposure' + | 'CreditQuality' + | 'CumulativePerformanceSeries' + | 'CurrencyId' + | 'DailyGrowthSeries' + | 'DailyPerformanceReturn' + | ['DailyPerformanceReturn', ...Array] + | 'DownsideCaptureRatio' + | 'EffectiveDuration' + | 'EffectiveMaturity' + | 'Esg' + | 'ExcessReturnArithmetic' + | 'ExcessReturnGeometric' + | 'ExpenseRatio' + | 'GlobalBondSector' + | 'GlobalStockSector' + | 'GrowthSeries' + | 'HistoricalPerformanceSeries' + | ['HistoricalPerformanceSeries', ...Array] + | 'HoldingCount' + | 'InformationRatio' + | 'Kurtosis' + | 'ManagementExpenseRatio' + | 'ManagementFee' + | 'MarketCapital' + | 'NegativeHoldingCount' + | 'OngoingCharge' + | 'PerformanceReturn' + | ['PerformanceReturn', ...Array] + | 'PortfolioId' + | 'PositiveHoldingCount' + | 'ProspectiveBookValueYield' + | 'ProspectiveCashFlowYield' + | 'ProspectiveEarningsYield' + | 'ProspectiveRevenueYield' + | 'RegionalExposure' + | 'RSquared' + | 'SharpeRatio' + | 'Skewness' + | 'SortinoRatio' + | 'SRRI' + | 'StandardDeviation' + | ['StandardDeviation', ...Array] + | 'StyleBox' + | 'TrackingError' + | 'TreynorRatio' + | 'Type' + | 'UpsideCaptureRatio' + // Version 2 + | 'portfolioriskscoreV2' +); + + +/* * + * + * Default Export + * + * */ + + +export default XRayOptions; diff --git a/src/XRay/index.ts b/src/XRay/index.ts new file mode 100644 index 0000000..2fe9f87 --- /dev/null +++ b/src/XRay/index.ts @@ -0,0 +1,47 @@ +/* * + * + * (c) 2009-2024 Highsoft AS + * + * License: www.highcharts.com/license + * + * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! + * + * Authors: + * - Sophie Bremer + * + * */ + + +'use strict'; + + +/* * + * + * Imports + * + * */ + + +import XRayConnector from './XRayConnector'; + + +/* * + * + * Exports + * + * */ + + +export * from './XRayConnector'; +export * from './XRayConverter'; +export * from './XRayOptions'; + + +/* * + * + * Default Export + * + * */ + + +export default XRayConnector; diff --git a/src/index.ts b/src/index.ts index 8a6683b..e68a391 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,7 @@ import RNANews from './RNANews/index'; import * as Shared from './Shared/index'; import TimeSeries from './TimeSeries/index'; import { version } from './version'; +import XRay from './XRay/index'; /* * @@ -39,6 +40,7 @@ export * from './RNANews/index'; export * as Shared from './Shared/index'; export * from './TimeSeries/index'; export { version } from './version'; +export * from './XRay/index'; /* * @@ -52,5 +54,6 @@ export default { RNANews, Shared, TimeSeries, + XRay, version }; diff --git a/test/unit-tests/TimeSeries/DividendConverter.test.ts b/test/unit-tests/TimeSeries/DividendConverter.test.ts index b84dc94..e2ee01e 100644 --- a/test/unit-tests/TimeSeries/DividendConverter.test.ts +++ b/test/unit-tests/TimeSeries/DividendConverter.test.ts @@ -24,8 +24,7 @@ export async function ratingLoad ( ); Assert.ok( - connector.converter instanceof - MC.TimeSeriesConverters.DividendSeriesConverter, + connector.converter instanceof MC.TimeSeriesConverters.DividendSeriesConverter, 'Converter should be instance of TimeSeries DividendSeriesConverter.' ); diff --git a/test/unit-tests/XRay/Breakdown.test.ts b/test/unit-tests/XRay/Breakdown.test.ts new file mode 100644 index 0000000..1048027 --- /dev/null +++ b/test/unit-tests/XRay/Breakdown.test.ts @@ -0,0 +1,54 @@ +import * as Assert from 'node:assert/strict'; +import * as MC from '../../../code/connectors-morningstar.src'; + +export async function breakdownLoad ( + api: MC.Shared.MorningstarAPIOptions +) { + const connector = new MC.XRayConnector({ + api, + currencyId: 'GBP', + dataPoints: { + type: 'benchmark', + dataPoints: [ + 'HistoricalPerformanceSeries', + ['PerformanceReturn', 'M0', 'M1', 'M2', 'M3', 'M6', 'M12'], + 'ShowBreakdown' + ] + }, + holdings: [ + { + id: 'GB00BWDBJF10', + idType: 'ISIN', + type: MC.Shared.MorningstarSecurityType.OpenEndFund, + weight: 100 + } + ] + }); + + Assert.ok( + connector instanceof MC.XRayConnector, + 'Connector should be instance of XRayConnector class.' + ); + + Assert.ok( + connector.converter instanceof MC.XRayConverter, + 'Converter should be instance of XRayConverter class.' + ); + + await connector.load(); + + process.stdout.write(JSON.stringify(connector.table.getColumnNames())); + + Assert.deepStrictEqual( + connector.table.getColumnNames(), + ['XRay_TotalReturn_TimePeriod', 'XRay_TotalReturn_Value'], + 'Connector table should exist of expected columns.' + ); + + Assert.strictEqual( + connector.table.getRowCount(), + 392, + 'Connector table should have the expected amount of rows.' + ); + +}