Browse Source

Add data provider info

pull/1730/head
Thomas 3 years ago
parent
commit
e049aa70af
  1. 14
      apps/api/src/app/portfolio/current-rate.service.mock.ts
  2. 11
      apps/api/src/app/portfolio/current-rate.service.spec.ts
  3. 20
      apps/api/src/app/portfolio/current-rate.service.ts
  4. 2
      apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts
  5. 24
      apps/api/src/app/portfolio/portfolio-calculator.ts
  6. 3
      apps/api/src/app/portfolio/portfolio.service.ts
  7. 3
      apps/api/src/services/interfaces/interfaces.ts
  8. 4
      apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts
  9. 10
      apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html
  10. 2
      apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts
  11. 4
      libs/common/src/lib/interfaces/data-provider-info.interface.ts
  12. 2
      libs/common/src/lib/interfaces/index.ts
  13. 9
      libs/ui/src/lib/data-provider-credits/data-provider-credits.component.html
  14. 7
      libs/ui/src/lib/data-provider-credits/data-provider-credits.component.scss
  15. 14
      libs/ui/src/lib/data-provider-credits/data-provider-credits.component.ts
  16. 12
      libs/ui/src/lib/data-provider-credits/data-provider-credits.module.ts
  17. 1
      libs/ui/src/lib/data-provider-credits/index.ts

14
apps/api/src/app/portfolio/current-rate.service.mock.ts

@ -1,4 +1,5 @@
import { parseDate, resetHours } from '@ghostfolio/common/helper'; import { parseDate, resetHours } from '@ghostfolio/common/helper';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { addDays, endOfDay, isBefore, isSameDay } from 'date-fns'; import { addDays, endOfDay, isBefore, isSameDay } from 'date-fns';
import { GetValueObject } from './interfaces/get-value-object.interface'; import { GetValueObject } from './interfaces/get-value-object.interface';
@ -48,8 +49,11 @@ export const CurrentRateServiceMock = {
getValues: ({ getValues: ({
dataGatheringItems, dataGatheringItems,
dateQuery dateQuery
}: GetValuesParams): Promise<GetValueObject[]> => { }: GetValuesParams): Promise<{
const result: GetValueObject[] = []; dataProviderInfos: DataProviderInfo[];
values: GetValueObject[];
}> => {
const values: GetValueObject[] = [];
if (dateQuery.lt) { if (dateQuery.lt) {
for ( for (
let date = resetHours(dateQuery.gte); let date = resetHours(dateQuery.gte);
@ -57,7 +61,7 @@ export const CurrentRateServiceMock = {
date = addDays(date, 1) date = addDays(date, 1)
) { ) {
for (const dataGatheringItem of dataGatheringItems) { for (const dataGatheringItem of dataGatheringItems) {
result.push({ values.push({
date, date,
marketPriceInBaseCurrency: mockGetValue( marketPriceInBaseCurrency: mockGetValue(
dataGatheringItem.symbol, dataGatheringItem.symbol,
@ -70,7 +74,7 @@ export const CurrentRateServiceMock = {
} else { } else {
for (const date of dateQuery.in) { for (const date of dateQuery.in) {
for (const dataGatheringItem of dataGatheringItems) { for (const dataGatheringItem of dataGatheringItems) {
result.push({ values.push({
date, date,
marketPriceInBaseCurrency: mockGetValue( marketPriceInBaseCurrency: mockGetValue(
dataGatheringItem.symbol, dataGatheringItem.symbol,
@ -81,6 +85,6 @@ export const CurrentRateServiceMock = {
} }
} }
} }
return Promise.resolve(result); return Promise.resolve({ values, dataProviderInfos: [] });
} }
}; };

11
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 { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { DataSource, MarketData } from '@prisma/client'; import { DataSource, MarketData } from '@prisma/client';
import { CurrentRateService } from './current-rate.service'; import { CurrentRateService } from './current-rate.service';
@ -103,7 +104,12 @@ describe('CurrentRateService', () => {
}, },
userCurrency: 'CHF' userCurrency: 'CHF'
}) })
).toMatchObject<GetValueObject[]>([ ).toMatchObject<{
dataProviderInfos: DataProviderInfo[];
values: GetValueObject[];
}>({
dataProviderInfos: [],
values: [
{ {
date: undefined, date: undefined,
marketPriceInBaseCurrency: 1841.823902, marketPriceInBaseCurrency: 1841.823902,
@ -114,6 +120,7 @@ describe('CurrentRateService', () => {
marketPriceInBaseCurrency: 1847.839966, marketPriceInBaseCurrency: 1847.839966,
symbol: 'AMZN' symbol: 'AMZN'
} }
]); ]
});
}); });
}); });

20
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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
import { resetHours } from '@ghostfolio/common/helper'; import { resetHours } from '@ghostfolio/common/helper';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { isBefore, isToday } from 'date-fns'; import { isBefore, isToday } from 'date-fns';
import { flatten } from 'lodash'; import { flatten } from 'lodash';
@ -22,7 +23,11 @@ export class CurrentRateService {
dataGatheringItems, dataGatheringItems,
dateQuery, dateQuery,
userCurrency userCurrency
}: GetValuesParams): Promise<GetValueObject[]> { }: GetValuesParams): Promise<{
dataProviderInfos: DataProviderInfo[];
values: GetValueObject[];
}> {
const dataProviderInfos: DataProviderInfo[] = [];
const includeToday = const includeToday =
(!dateQuery.lt || isBefore(new Date(), dateQuery.lt)) && (!dateQuery.lt || isBefore(new Date(), dateQuery.lt)) &&
(!dateQuery.gte || isBefore(dateQuery.gte, new Date())) && (!dateQuery.gte || isBefore(dateQuery.gte, new Date())) &&
@ -38,6 +43,14 @@ export class CurrentRateService {
.then((dataResultProvider) => { .then((dataResultProvider) => {
const result: GetValueObject[] = []; const result: GetValueObject[] = [];
for (const dataGatheringItem of dataGatheringItems) { for (const dataGatheringItem of dataGatheringItems) {
if (
dataResultProvider?.[dataGatheringItem.symbol]?.dataProviderInfo
) {
dataProviderInfos.push(
dataResultProvider[dataGatheringItem.symbol].dataProviderInfo
);
}
result.push({ result.push({
date: today, date: today,
marketPriceInBaseCurrency: 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 { private containsToday(dates: Date[]): boolean {

2
apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts

@ -1,4 +1,5 @@
import { import {
DataProviderInfo,
EnhancedSymbolProfile, EnhancedSymbolProfile,
HistoricalDataItem HistoricalDataItem
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -7,6 +8,7 @@ import { Tag } from '@prisma/client';
export interface PortfolioPositionDetail { export interface PortfolioPositionDetail {
averagePrice: number; averagePrice: number;
dataProviderInfo: DataProviderInfo;
dividendInBaseCurrency: number; dividendInBaseCurrency: number;
feeInBaseCurrency: number; feeInBaseCurrency: number;
firstBuyDate: string; firstBuyDate: string;

24
apps/api/src/app/portfolio/portfolio-calculator.ts

@ -1,7 +1,11 @@
import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface'; import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; 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 { GroupBy } from '@ghostfolio/common/types';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { Type as TypeOfOrder } from '@prisma/client'; import { Type as TypeOfOrder } from '@prisma/client';
@ -45,6 +49,7 @@ export class PortfolioCalculator {
private currency: string; private currency: string;
private currentRateService: CurrentRateService; private currentRateService: CurrentRateService;
private dataProviderInfos: DataProviderInfo[];
private orders: PortfolioOrder[]; private orders: PortfolioOrder[];
private transactionPoints: TransactionPoint[]; private transactionPoints: TransactionPoint[];
@ -202,7 +207,8 @@ export class PortfolioCalculator {
symbols[item.symbol] = true; symbols[item.symbol] = true;
} }
const marketSymbols = await this.currentRateService.getValues({ const { dataProviderInfos, values: marketSymbols } =
await this.currentRateService.getValues({
currencies, currencies,
dataGatheringItems, dataGatheringItems,
dateQuery: { dateQuery: {
@ -211,6 +217,8 @@ export class PortfolioCalculator {
userCurrency: this.currency userCurrency: this.currency
}); });
this.dataProviderInfos = dataProviderInfos;
const marketSymbolMap: { const marketSymbolMap: {
[date: string]: { [symbol: string]: Big }; [date: string]: { [symbol: string]: Big };
} = {}; } = {};
@ -368,7 +376,8 @@ export class PortfolioCalculator {
dates.push(resetHours(end)); dates.push(resetHours(end));
const marketSymbols = await this.currentRateService.getValues({ const { dataProviderInfos, values: marketSymbols } =
await this.currentRateService.getValues({
currencies, currencies,
dataGatheringItems, dataGatheringItems,
dateQuery: { dateQuery: {
@ -377,6 +386,8 @@ export class PortfolioCalculator {
userCurrency: this.currency userCurrency: this.currency
}); });
this.dataProviderInfos = dataProviderInfos;
const marketSymbolMap: { const marketSymbolMap: {
[date: string]: { [symbol: string]: Big }; [date: string]: { [symbol: string]: Big };
} = {}; } = {};
@ -463,6 +474,10 @@ export class PortfolioCalculator {
}; };
} }
public getDataProviderInfos() {
return this.dataProviderInfos;
}
public getInvestments(): { date: string; investment: Big }[] { public getInvestments(): { date: string; investment: Big }[] {
if (this.transactionPoints.length === 0) { if (this.transactionPoints.length === 0) {
return []; return [];
@ -748,7 +763,7 @@ export class PortfolioCalculator {
let marketSymbols: GetValueObject[] = []; let marketSymbols: GetValueObject[] = [];
if (dataGatheringItems.length > 0) { if (dataGatheringItems.length > 0) {
try { try {
marketSymbols = await this.currentRateService.getValues({ const { values } = await this.currentRateService.getValues({
currencies, currencies,
dataGatheringItems, dataGatheringItems,
dateQuery: { dateQuery: {
@ -757,6 +772,7 @@ export class PortfolioCalculator {
}, },
userCurrency: this.currency userCurrency: this.currency
}); });
marketSymbols = values;
} catch (error) { } catch (error) {
Logger.error( Logger.error(
`Failed to fetch info for date ${startDate} with exception`, `Failed to fetch info for date ${startDate} with exception`,

3
apps/api/src/app/portfolio/portfolio.service.ts

@ -678,6 +678,7 @@ export class PortfolioService {
return { return {
tags, tags,
averagePrice: undefined, averagePrice: undefined,
dataProviderInfo: undefined,
dividendInBaseCurrency: undefined, dividendInBaseCurrency: undefined,
feeInBaseCurrency: undefined, feeInBaseCurrency: undefined,
firstBuyDate: undefined, firstBuyDate: undefined,
@ -849,6 +850,7 @@ export class PortfolioService {
tags, tags,
transactionCount, transactionCount,
averagePrice: averagePrice.toNumber(), averagePrice: averagePrice.toNumber(),
dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0],
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
feeInBaseCurrency: this.exchangeRateDataService.toCurrency( feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
fee.toNumber(), fee.toNumber(),
@ -911,6 +913,7 @@ export class PortfolioService {
SymbolProfile, SymbolProfile,
tags, tags,
averagePrice: 0, averagePrice: 0,
dataProviderInfo: undefined,
dividendInBaseCurrency: 0, dividendInBaseCurrency: 0,
feeInBaseCurrency: 0, feeInBaseCurrency: 0,
firstBuyDate: undefined, firstBuyDate: undefined,

3
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 { MarketState } from '@ghostfolio/common/types';
import { import {
Account, Account,
@ -28,6 +28,7 @@ export interface IDataProviderHistoricalResponse {
export interface IDataProviderResponse { export interface IDataProviderResponse {
currency: string; currency: string;
dataProviderInfo?: DataProviderInfo;
dataSource: DataSource; dataSource: DataSource;
marketPrice: number; marketPrice: number;
marketState: MarketState; marketState: MarketState;

4
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 { DataService } from '@ghostfolio/client/services/data.service';
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper'; import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
import { import {
DataProviderInfo,
EnhancedSymbolProfile, EnhancedSymbolProfile,
LineChartItem LineChartItem
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -40,6 +41,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
public countries: { public countries: {
[code: string]: { name: string; value: number }; [code: string]: { name: string; value: number };
}; };
public dataProviderInfo: DataProviderInfo;
public dividendInBaseCurrency: number; public dividendInBaseCurrency: number;
public feeInBaseCurrency: number; public feeInBaseCurrency: number;
public firstBuyDate: string; public firstBuyDate: string;
@ -83,6 +85,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
.subscribe( .subscribe(
({ ({
averagePrice, averagePrice,
dataProviderInfo,
dividendInBaseCurrency, dividendInBaseCurrency,
feeInBaseCurrency, feeInBaseCurrency,
firstBuyDate, firstBuyDate,
@ -105,6 +108,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
this.averagePrice = averagePrice; this.averagePrice = averagePrice;
this.benchmarkDataItems = []; this.benchmarkDataItems = [];
this.countries = {}; this.countries = {};
this.dataProviderInfo = dataProviderInfo;
this.dividendInBaseCurrency = dividendInBaseCurrency; this.dividendInBaseCurrency = dividendInBaseCurrency;
this.feeInBaseCurrency = feeInBaseCurrency; this.feeInBaseCurrency = feeInBaseCurrency;
this.firstBuyDate = firstBuyDate; this.firstBuyDate = firstBuyDate;

10
apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html

@ -249,7 +249,7 @@
</div> </div>
<div *ngIf="tags?.length > 0" class="row"> <div *ngIf="tags?.length > 0" class="row">
<div class="col mb-3"> <div class="col">
<div class="h5" i18n>Tags</div> <div class="h5" i18n>Tags</div>
<mat-chip-list> <mat-chip-list>
<mat-chip *ngFor="let tag of tags">{{ tag.name }}</mat-chip> <mat-chip *ngFor="let tag of tags">{{ tag.name }}</mat-chip>
@ -261,7 +261,7 @@
*ngIf="data.hasPermissionToReportDataGlitch === true && orders?.length > 0" *ngIf="data.hasPermissionToReportDataGlitch === true && orders?.length > 0"
class="row" class="row"
> >
<div class="col mb-3"> <div class="col">
<hr /> <hr />
<a color="warn" mat-stroked-button [href]="reportDataGlitchMail" <a color="warn" mat-stroked-button [href]="reportDataGlitchMail"
><ion-icon class="mr-1" name="flag-outline"></ion-icon ><ion-icon class="mr-1" name="flag-outline"></ion-icon
@ -269,6 +269,12 @@
> >
</div> </div>
</div> </div>
<div *ngIf="dataProviderInfo" class="text-center">
<hr />
<gf-data-provider-credits [dataProviderInfos]="[dataProviderInfo]">
</gf-data-provider-credits>
</div>
</div> </div>
</div> </div>

2
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 { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.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 { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module'; import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
@ -18,6 +19,7 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
imports: [ imports: [
CommonModule, CommonModule,
GfActivitiesTableModule, GfActivitiesTableModule,
GfDataProviderCreditsModule,
GfDialogFooterModule, GfDialogFooterModule,
GfDialogHeaderModule, GfDialogHeaderModule,
GfLineChartModule, GfLineChartModule,

4
libs/common/src/lib/interfaces/data-provider-info.interface.ts

@ -0,0 +1,4 @@
export interface DataProviderInfo {
name: string;
url: string;
}

2
libs/common/src/lib/interfaces/index.ts

@ -10,6 +10,7 @@ import {
import { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface'; import { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface';
import { Benchmark } from './benchmark.interface'; import { Benchmark } from './benchmark.interface';
import { Coupon } from './coupon.interface'; import { Coupon } from './coupon.interface';
import { DataProviderInfo } from './data-provider-info.interface';
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface'; import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
import { Export } from './export.interface'; import { Export } from './export.interface';
import { FilterGroup } from './filter-group.interface'; import { FilterGroup } from './filter-group.interface';
@ -54,6 +55,7 @@ export {
BenchmarkMarketDataDetails, BenchmarkMarketDataDetails,
BenchmarkResponse, BenchmarkResponse,
Coupon, Coupon,
DataProviderInfo,
EnhancedSymbolProfile, EnhancedSymbolProfile,
Export, Export,
Filter, Filter,

9
libs/ui/src/lib/data-provider-credits/data-provider-credits.component.html

@ -0,0 +1,9 @@
<small class="text-muted">
<ng-container i18n>Data provided by</ng-container>&nbsp;<ng-container
*ngFor="let dataProviderInfo of dataProviderInfos; let last = last"
><a target="_blank" [href]="dataProviderInfo.url">{{
dataProviderInfo.name
}}</a
><ng-container *ngIf="!last">, </ng-container></ng-container
>.
</small>

7
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);
}
}

14
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() {}
}

12
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 {}

1
libs/ui/src/lib/data-provider-credits/index.ts

@ -0,0 +1 @@
export * from './data-provider-credits.module';
Loading…
Cancel
Save