diff --git a/apps/api/src/app/portfolio/current-rate.service.mock.ts b/apps/api/src/app/portfolio/current-rate.service.mock.ts index 004f642bf..cac110f05 100644 --- a/apps/api/src/app/portfolio/current-rate.service.mock.ts +++ b/apps/api/src/app/portfolio/current-rate.service.mock.ts @@ -1,4 +1,5 @@ import { parseDate, resetHours } from '@ghostfolio/common/helper'; +import { DataProviderInfo } from '@ghostfolio/common/interfaces'; import { addDays, endOfDay, isBefore, isSameDay } from 'date-fns'; import { GetValueObject } from './interfaces/get-value-object.interface'; @@ -48,8 +49,11 @@ export const CurrentRateServiceMock = { getValues: ({ dataGatheringItems, dateQuery - }: GetValuesParams): Promise => { - const result: GetValueObject[] = []; + }: GetValuesParams): Promise<{ + dataProviderInfos: DataProviderInfo[]; + values: GetValueObject[]; + }> => { + const values: GetValueObject[] = []; if (dateQuery.lt) { for ( let date = resetHours(dateQuery.gte); @@ -57,7 +61,7 @@ export const CurrentRateServiceMock = { date = addDays(date, 1) ) { for (const dataGatheringItem of dataGatheringItems) { - result.push({ + values.push({ date, marketPriceInBaseCurrency: mockGetValue( dataGatheringItem.symbol, @@ -70,7 +74,7 @@ export const CurrentRateServiceMock = { } else { for (const date of dateQuery.in) { for (const dataGatheringItem of dataGatheringItems) { - result.push({ + values.push({ date, marketPriceInBaseCurrency: mockGetValue( dataGatheringItem.symbol, @@ -81,6 +85,6 @@ export const CurrentRateServiceMock = { } } } - return Promise.resolve(result); + return Promise.resolve({ values, dataProviderInfos: [] }); } }; diff --git a/apps/api/src/app/portfolio/current-rate.service.spec.ts b/apps/api/src/app/portfolio/current-rate.service.spec.ts index 9528f980f..9171e5fa3 100644 --- a/apps/api/src/app/portfolio/current-rate.service.spec.ts +++ b/apps/api/src/app/portfolio/current-rate.service.spec.ts @@ -1,6 +1,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; +import { DataProviderInfo } from '@ghostfolio/common/interfaces'; import { DataSource, MarketData } from '@prisma/client'; import { CurrentRateService } from './current-rate.service'; @@ -103,17 +104,23 @@ describe('CurrentRateService', () => { }, userCurrency: 'CHF' }) - ).toMatchObject([ - { - date: undefined, - marketPriceInBaseCurrency: 1841.823902, - symbol: 'AMZN' - }, - { - date: undefined, - marketPriceInBaseCurrency: 1847.839966, - symbol: 'AMZN' - } - ]); + ).toMatchObject<{ + dataProviderInfos: DataProviderInfo[]; + values: GetValueObject[]; + }>({ + dataProviderInfos: [], + values: [ + { + date: undefined, + marketPriceInBaseCurrency: 1841.823902, + symbol: 'AMZN' + }, + { + date: undefined, + marketPriceInBaseCurrency: 1847.839966, + symbol: 'AMZN' + } + ] + }); }); }); diff --git a/apps/api/src/app/portfolio/current-rate.service.ts b/apps/api/src/app/portfolio/current-rate.service.ts index 86020ce2e..b3a432521 100644 --- a/apps/api/src/app/portfolio/current-rate.service.ts +++ b/apps/api/src/app/portfolio/current-rate.service.ts @@ -2,6 +2,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { resetHours } from '@ghostfolio/common/helper'; +import { DataProviderInfo } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; import { isBefore, isToday } from 'date-fns'; import { flatten } from 'lodash'; @@ -22,7 +23,11 @@ export class CurrentRateService { dataGatheringItems, dateQuery, userCurrency - }: GetValuesParams): Promise { + }: GetValuesParams): Promise<{ + dataProviderInfos: DataProviderInfo[]; + values: GetValueObject[]; + }> { + const dataProviderInfos: DataProviderInfo[] = []; const includeToday = (!dateQuery.lt || isBefore(new Date(), dateQuery.lt)) && (!dateQuery.gte || isBefore(dateQuery.gte, new Date())) && @@ -38,6 +43,14 @@ export class CurrentRateService { .then((dataResultProvider) => { const result: GetValueObject[] = []; for (const dataGatheringItem of dataGatheringItems) { + if ( + dataResultProvider?.[dataGatheringItem.symbol]?.dataProviderInfo + ) { + dataProviderInfos.push( + dataResultProvider[dataGatheringItem.symbol].dataProviderInfo + ); + } + result.push({ date: today, marketPriceInBaseCurrency: @@ -81,7 +94,10 @@ export class CurrentRateService { }) ); - return flatten(await Promise.all(promises)); + return { + dataProviderInfos, + values: flatten(await Promise.all(promises)) + }; } private containsToday(dates: Date[]): boolean { diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts index 48a0ac671..827aa25fe 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts @@ -1,4 +1,5 @@ import { + DataProviderInfo, EnhancedSymbolProfile, HistoricalDataItem } from '@ghostfolio/common/interfaces'; @@ -7,6 +8,7 @@ import { Tag } from '@prisma/client'; export interface PortfolioPositionDetail { averagePrice: number; + dataProviderInfo: DataProviderInfo; dividendInBaseCurrency: number; feeInBaseCurrency: number; firstBuyDate: string; diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index f99defd53..06c617166 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -1,7 +1,11 @@ import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; -import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces'; +import { + DataProviderInfo, + ResponseError, + TimelinePosition +} from '@ghostfolio/common/interfaces'; import { GroupBy } from '@ghostfolio/common/types'; import { Logger } from '@nestjs/common'; import { Type as TypeOfOrder } from '@prisma/client'; @@ -45,6 +49,7 @@ export class PortfolioCalculator { private currency: string; private currentRateService: CurrentRateService; + private dataProviderInfos: DataProviderInfo[]; private orders: PortfolioOrder[]; private transactionPoints: TransactionPoint[]; @@ -202,14 +207,17 @@ export class PortfolioCalculator { symbols[item.symbol] = true; } - const marketSymbols = await this.currentRateService.getValues({ - currencies, - dataGatheringItems, - dateQuery: { - in: dates - }, - userCurrency: this.currency - }); + const { dataProviderInfos, values: marketSymbols } = + await this.currentRateService.getValues({ + currencies, + dataGatheringItems, + dateQuery: { + in: dates + }, + userCurrency: this.currency + }); + + this.dataProviderInfos = dataProviderInfos; const marketSymbolMap: { [date: string]: { [symbol: string]: Big }; @@ -368,14 +376,17 @@ export class PortfolioCalculator { dates.push(resetHours(end)); - const marketSymbols = await this.currentRateService.getValues({ - currencies, - dataGatheringItems, - dateQuery: { - in: dates - }, - userCurrency: this.currency - }); + const { dataProviderInfos, values: marketSymbols } = + await this.currentRateService.getValues({ + currencies, + dataGatheringItems, + dateQuery: { + in: dates + }, + userCurrency: this.currency + }); + + this.dataProviderInfos = dataProviderInfos; const marketSymbolMap: { [date: string]: { [symbol: string]: Big }; @@ -463,6 +474,10 @@ export class PortfolioCalculator { }; } + public getDataProviderInfos() { + return this.dataProviderInfos; + } + public getInvestments(): { date: string; investment: Big }[] { if (this.transactionPoints.length === 0) { return []; @@ -748,7 +763,7 @@ export class PortfolioCalculator { let marketSymbols: GetValueObject[] = []; if (dataGatheringItems.length > 0) { try { - marketSymbols = await this.currentRateService.getValues({ + const { values } = await this.currentRateService.getValues({ currencies, dataGatheringItems, dateQuery: { @@ -757,6 +772,7 @@ export class PortfolioCalculator { }, userCurrency: this.currency }); + marketSymbols = values; } catch (error) { Logger.error( `Failed to fetch info for date ${startDate} with exception`, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 2564d1d49..1677ffc29 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -678,6 +678,7 @@ export class PortfolioService { return { tags, averagePrice: undefined, + dataProviderInfo: undefined, dividendInBaseCurrency: undefined, feeInBaseCurrency: undefined, firstBuyDate: undefined, @@ -849,6 +850,7 @@ export class PortfolioService { tags, transactionCount, averagePrice: averagePrice.toNumber(), + dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0], dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), feeInBaseCurrency: this.exchangeRateDataService.toCurrency( fee.toNumber(), @@ -911,6 +913,7 @@ export class PortfolioService { SymbolProfile, tags, averagePrice: 0, + dataProviderInfo: undefined, dividendInBaseCurrency: 0, feeInBaseCurrency: 0, firstBuyDate: undefined, diff --git a/apps/api/src/services/interfaces/interfaces.ts b/apps/api/src/services/interfaces/interfaces.ts index 1148dd6af..58f25243c 100644 --- a/apps/api/src/services/interfaces/interfaces.ts +++ b/apps/api/src/services/interfaces/interfaces.ts @@ -1,4 +1,4 @@ -import { UniqueAsset } from '@ghostfolio/common/interfaces'; +import { DataProviderInfo, UniqueAsset } from '@ghostfolio/common/interfaces'; import { MarketState } from '@ghostfolio/common/types'; import { Account, @@ -28,6 +28,7 @@ export interface IDataProviderHistoricalResponse { export interface IDataProviderResponse { currency: string; + dataProviderInfo?: DataProviderInfo; dataSource: DataSource; marketPrice: number; marketState: MarketState; diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts index aa2880d81..9869ad453 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts @@ -13,6 +13,7 @@ import { import { DataService } from '@ghostfolio/client/services/data.service'; import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper'; import { + DataProviderInfo, EnhancedSymbolProfile, LineChartItem } from '@ghostfolio/common/interfaces'; @@ -40,6 +41,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit { public countries: { [code: string]: { name: string; value: number }; }; + public dataProviderInfo: DataProviderInfo; public dividendInBaseCurrency: number; public feeInBaseCurrency: number; public firstBuyDate: string; @@ -83,6 +85,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit { .subscribe( ({ averagePrice, + dataProviderInfo, dividendInBaseCurrency, feeInBaseCurrency, firstBuyDate, @@ -105,6 +108,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit { this.averagePrice = averagePrice; this.benchmarkDataItems = []; this.countries = {}; + this.dataProviderInfo = dataProviderInfo; this.dividendInBaseCurrency = dividendInBaseCurrency; this.feeInBaseCurrency = feeInBaseCurrency; this.firstBuyDate = firstBuyDate; diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index 33aebd8f3..af5152f9f 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -249,7 +249,7 @@
-
+
Tags
{{ tag.name }} @@ -261,7 +261,7 @@ *ngIf="data.hasPermissionToReportDataGlitch === true && orders?.length > 0" class="row" > - + +
+
+ + +
diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts index 59ff9a21c..ef4f342e4 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts @@ -6,6 +6,7 @@ import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/lega import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; +import { GfDataProviderCreditsModule } from '@ghostfolio/ui/data-provider-credits/data-provider-credits.module'; import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module'; import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module'; import { GfValueModule } from '@ghostfolio/ui/value'; @@ -18,6 +19,7 @@ import { PositionDetailDialog } from './position-detail-dialog.component'; imports: [ CommonModule, GfActivitiesTableModule, + GfDataProviderCreditsModule, GfDialogFooterModule, GfDialogHeaderModule, GfLineChartModule, diff --git a/libs/common/src/lib/interfaces/data-provider-info.interface.ts b/libs/common/src/lib/interfaces/data-provider-info.interface.ts new file mode 100644 index 000000000..59f3a0b69 --- /dev/null +++ b/libs/common/src/lib/interfaces/data-provider-info.interface.ts @@ -0,0 +1,4 @@ +export interface DataProviderInfo { + name: string; + url: string; +} diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index a8ff96e0f..68b882601 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -10,6 +10,7 @@ import { import { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface'; import { Benchmark } from './benchmark.interface'; import { Coupon } from './coupon.interface'; +import { DataProviderInfo } from './data-provider-info.interface'; import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface'; import { Export } from './export.interface'; import { FilterGroup } from './filter-group.interface'; @@ -54,6 +55,7 @@ export { BenchmarkMarketDataDetails, BenchmarkResponse, Coupon, + DataProviderInfo, EnhancedSymbolProfile, Export, Filter, diff --git a/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.html b/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.html new file mode 100644 index 000000000..c8e50acc6 --- /dev/null +++ b/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.html @@ -0,0 +1,9 @@ + + Data provided by {{ + dataProviderInfo.name + }}, . + diff --git a/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.scss b/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.scss new file mode 100644 index 000000000..d732e2f02 --- /dev/null +++ b/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.scss @@ -0,0 +1,7 @@ +:host { + display: block; + + a { + color: rgba(var(--palette-primary-500), 1); + } +} diff --git a/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.ts b/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.ts new file mode 100644 index 000000000..1da2fcfc9 --- /dev/null +++ b/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.ts @@ -0,0 +1,14 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { DataProviderInfo } from '@ghostfolio/common/interfaces'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'gf-data-provider-credits', + styleUrls: ['./data-provider-credits.component.scss'], + templateUrl: './data-provider-credits.component.html' +}) +export class DataProviderCreditsComponent { + @Input() dataProviderInfos: DataProviderInfo[]; + + public constructor() {} +} diff --git a/libs/ui/src/lib/data-provider-credits/data-provider-credits.module.ts b/libs/ui/src/lib/data-provider-credits/data-provider-credits.module.ts new file mode 100644 index 000000000..e5dd9d3b9 --- /dev/null +++ b/libs/ui/src/lib/data-provider-credits/data-provider-credits.module.ts @@ -0,0 +1,12 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; + +import { DataProviderCreditsComponent } from './data-provider-credits.component'; + +@NgModule({ + declarations: [DataProviderCreditsComponent], + exports: [DataProviderCreditsComponent], + imports: [CommonModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class GfDataProviderCreditsModule {} diff --git a/libs/ui/src/lib/data-provider-credits/index.ts b/libs/ui/src/lib/data-provider-credits/index.ts new file mode 100644 index 000000000..5d3759577 --- /dev/null +++ b/libs/ui/src/lib/data-provider-credits/index.ts @@ -0,0 +1 @@ +export * from './data-provider-credits.module';